mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
add migration tests
This commit is contained in:
commit
ccf66154bb
@ -7,6 +7,7 @@
|
||||
* **Cloudwatch**: Add AWS RDS MaximumUsedTransactionIDs metric [#15077](https://github.com/grafana/grafana/pull/15077), thx [@activeshadow](https://github.com/activeshadow)
|
||||
* **Heatmap**: `Middle` bucket bound option [#15683](https://github.com/grafana/grafana/issues/15683)
|
||||
* **Heatmap**: `Reverse order` option for changing order of buckets [#15683](https://github.com/grafana/grafana/issues/15683)
|
||||
* **VictorOps**: Adds more information to the victor ops notifiers [#15744](https://github.com/grafana/grafana/issues/15744), thx [@zhulongcheng](https://github.com/zhulongcheng)
|
||||
|
||||
### Bug Fixes
|
||||
* **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506)
|
||||
|
@ -105,6 +105,14 @@ region = us-west-2
|
||||
|
||||
You need to specify a namespace, metric, at least one stat, and at least one dimension.
|
||||
|
||||
## Metric Math
|
||||
|
||||
You can now create new time series metrics by operating on top of Cloudwatch metrics using mathematical functions. Arithmetic operators, unary subtraction and other functions are supported to be applied on cloudwatch metrics. More details on the available functions can be found on [AWS Metric Math](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-metric-math.html)
|
||||
|
||||
As an example, if you want to apply arithmetic operator on a metric, you can do it by giving an alias(a unique string) to the raw metric as shown below. Then you can use this alias and apply arithmetic operator to it in the Expression field of created metric.
|
||||
|
||||

|
||||
|
||||
## Templated queries
|
||||
|
||||
Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place.
|
||||
|
@ -163,7 +163,7 @@ Expected result from datasource.annotationQuery:
|
||||
"title": "Cluster outage",
|
||||
"time": 1457075272576,
|
||||
"text": "Joe causes brain split",
|
||||
"tags": "joe, cluster, failure"
|
||||
"tags": ["joe", "cluster", "failure"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
@ -120,7 +120,7 @@ $headings-line-height: ${theme.typography.lineHeight.sm} !default;
|
||||
$border-width: ${theme.border.width.sm} !default;
|
||||
|
||||
$border-radius: ${theme.border.radius.md} !default;
|
||||
$border-radius-lg: ${theme.border.radius.lg}!default;
|
||||
$border-radius-lg: ${theme.border.radius.lg} !default;
|
||||
$border-radius-sm: ${theme.border.radius.sm} !default;
|
||||
|
||||
// Page
|
||||
@ -191,7 +191,6 @@ $btn-padding-y-lg: 11px !default;
|
||||
$btn-padding-x-xl: 21px !default;
|
||||
$btn-padding-y-xl: 11px !default;
|
||||
|
||||
|
||||
$btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
|
||||
|
||||
// sidemenu
|
||||
|
@ -26,13 +26,21 @@ export interface PanelEditorProps<T = any> {
|
||||
onOptionsChange: (options: T) => void;
|
||||
}
|
||||
|
||||
export type PreservePanelOptionsHandler<TOptions = any> = (pluginId: string, prevOptions: any) => Partial<TOptions>;
|
||||
/**
|
||||
* Called before a panel is initalized
|
||||
*/
|
||||
export type PanelTypeChangedHook<TOptions = any> = (
|
||||
options: Partial<TOptions>,
|
||||
prevPluginId?: string,
|
||||
prevOptions?: any
|
||||
) => Partial<TOptions>;
|
||||
|
||||
export class ReactPanelPlugin<TOptions = any> {
|
||||
panel: ComponentClass<PanelProps<TOptions>>;
|
||||
editor?: ComponentClass<PanelEditorProps<TOptions>>;
|
||||
defaults?: TOptions;
|
||||
preserveOptions?: PreservePanelOptionsHandler<TOptions>;
|
||||
|
||||
panelTypeChangedHook?: PanelTypeChangedHook<TOptions>;
|
||||
|
||||
constructor(panel: ComponentClass<PanelProps<TOptions>>) {
|
||||
this.panel = panel;
|
||||
@ -46,8 +54,12 @@ export class ReactPanelPlugin<TOptions = any> {
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
setPreserveOptionsHandler(handler: PreservePanelOptionsHandler<TOptions>) {
|
||||
this.preserveOptions = handler;
|
||||
/**
|
||||
* Called when the visualization changes.
|
||||
* Lets you keep whatever settings made sense in the previous panel
|
||||
*/
|
||||
setPanelTypeChangedHook(v: PanelTypeChangedHook<TOptions>) {
|
||||
this.panelTypeChangedHook = v;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,14 +92,29 @@ func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
messageType = AlertStateRecovery
|
||||
}
|
||||
|
||||
fields := make(map[string]interface{}, 0)
|
||||
fieldLimitCount := 4
|
||||
for index, evt := range evalContext.EvalMatches {
|
||||
fields[evt.Metric] = evt.Value
|
||||
if index > fieldLimitCount {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
bodyJSON := simplejson.New()
|
||||
bodyJSON.Set("message_type", messageType)
|
||||
bodyJSON.Set("entity_id", evalContext.Rule.Name)
|
||||
bodyJSON.Set("entity_display_name", evalContext.GetNotificationTitle())
|
||||
bodyJSON.Set("timestamp", time.Now().Unix())
|
||||
bodyJSON.Set("state_start_time", evalContext.StartTime.Unix())
|
||||
bodyJSON.Set("state_message", evalContext.Rule.Message)
|
||||
bodyJSON.Set("monitoring_tool", "Grafana v"+setting.BuildVersion)
|
||||
bodyJSON.Set("alert_url", ruleUrl)
|
||||
bodyJSON.Set("metrics", fields)
|
||||
|
||||
if evalContext.Error != nil {
|
||||
bodyJSON.Set("error_message", evalContext.Error.Error())
|
||||
}
|
||||
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
bodyJSON.Set("image_url", evalContext.ImagePublicUrl)
|
||||
|
@ -14,6 +14,7 @@ import { PanelEditor } from '../panel_editor/PanelEditor';
|
||||
import { PanelModel, DashboardModel } from '../state';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
import { PanelResizer } from './PanelResizer';
|
||||
import { PanelTypeChangedHook } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
@ -91,7 +92,11 @@ export class DashboardPanel extends PureComponent<Props, State> {
|
||||
|
||||
this.props.panel.changeType(pluginId);
|
||||
} else {
|
||||
panel.changeType(pluginId, plugin.exports.reactPanel.preserveOptions);
|
||||
let hook: PanelTypeChangedHook | null = null;
|
||||
if (plugin.exports.reactPanel) {
|
||||
hook = plugin.exports.reactPanel.panelTypeChangedHook;
|
||||
}
|
||||
panel.changeType(pluginId, hook);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import _ from 'lodash';
|
||||
|
||||
// Types
|
||||
import { Emitter } from 'app/core/utils/emitter';
|
||||
import { DataQuery, TimeSeries, Threshold, ScopedVars } from '@grafana/ui';
|
||||
import { DataQuery, TimeSeries, Threshold, ScopedVars, PanelTypeChangedHook } from '@grafana/ui';
|
||||
import { TableData } from '@grafana/ui/src';
|
||||
|
||||
export interface GridPos {
|
||||
@ -237,7 +237,7 @@ export class PanelModel {
|
||||
});
|
||||
}
|
||||
|
||||
changeType(pluginId: string, preserveOptions?: any) {
|
||||
changeType(pluginId: string, hook?: PanelTypeChangedHook) {
|
||||
const oldOptions: any = this.getOptionsToRemember();
|
||||
const oldPluginId = this.type;
|
||||
|
||||
@ -255,9 +255,12 @@ export class PanelModel {
|
||||
this.cachedPluginOptions[oldPluginId] = oldOptions;
|
||||
this.restorePanelOptions(pluginId);
|
||||
|
||||
if (preserveOptions && oldOptions) {
|
||||
// Callback that can validate and migrate any existing settings
|
||||
if (hook) {
|
||||
this.options = this.options || {};
|
||||
Object.assign(this.options, preserveOptions(oldPluginId, oldOptions.options));
|
||||
const old = oldOptions ? oldOptions.options : null;
|
||||
|
||||
Object.assign(this.options, hook(this.options, oldPluginId, old));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,10 +3,10 @@ import { ReactPanelPlugin } from '@grafana/ui';
|
||||
import { BarGaugePanel } from './BarGaugePanel';
|
||||
import { BarGaugePanelEditor } from './BarGaugePanelEditor';
|
||||
import { BarGaugeOptions, defaults } from './types';
|
||||
import { gaugePreserveOptionsHandler } from '../gauge/module';
|
||||
import { gaugePanelTypeChangedHook } from '../gauge/module';
|
||||
|
||||
export const reactPanel = new ReactPanelPlugin<BarGaugeOptions>(BarGaugePanel);
|
||||
|
||||
reactPanel.setEditor(BarGaugePanelEditor);
|
||||
reactPanel.setDefaults(defaults);
|
||||
reactPanel.setPreserveOptionsHandler(gaugePreserveOptionsHandler);
|
||||
reactPanel.setPanelTypeChangedHook(gaugePanelTypeChangedHook);
|
||||
|
27
public/app/plugins/panel/gauge/module.test.ts
Normal file
27
public/app/plugins/panel/gauge/module.test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { gaugePanelTypeChangedHook } from './module';
|
||||
|
||||
describe('Gauge Module', () => {
|
||||
describe('migrations', () => {
|
||||
it('should migrate from 6.0 settings to 6.1', () => {
|
||||
const v60 = {
|
||||
minValue: 50,
|
||||
maxValue: 60,
|
||||
showThresholdMarkers: true,
|
||||
showThresholdLabels: false,
|
||||
valueOptions: {
|
||||
prefix: 'a',
|
||||
suffix: 'z',
|
||||
decimals: 4,
|
||||
stat: 'avg',
|
||||
unit: 'ms',
|
||||
},
|
||||
valueMappings: [],
|
||||
thresholds: [{ index: 0, value: -Infinity, color: 'green' }, { index: 1, value: 80, color: 'red' }],
|
||||
};
|
||||
|
||||
const after = gaugePanelTypeChangedHook(v60);
|
||||
expect((after.stat = 'avg'));
|
||||
expect(after).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
import { ReactPanelPlugin } from '@grafana/ui';
|
||||
import { ReactPanelPlugin, DisplayValueOptions } from '@grafana/ui';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
import { GaugePanelEditor } from './GaugePanelEditor';
|
||||
@ -8,9 +8,10 @@ import { GaugeOptions, defaults } from './types';
|
||||
export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel);
|
||||
|
||||
// Bar Gauge uses the same handler
|
||||
export const gaugePreserveOptionsHandler = (pluginId: string, prevOptions: any) => {
|
||||
const options: Partial<GaugeOptions> = {};
|
||||
|
||||
const optionsToCheck = ['display', 'stat', 'maxValue', 'maxValue'];
|
||||
|
||||
export const gaugePanelTypeChangedHook = (options: Partial<GaugeOptions>, prevPluginId?: string, prevOptions?: any) => {
|
||||
// TODO! migrate to new settings format
|
||||
//
|
||||
// thresholds?: Threshold[];
|
||||
@ -21,11 +22,32 @@ export const gaugePreserveOptionsHandler = (pluginId: string, prevOptions: any)
|
||||
// console.warn('TODO!! how do we best migration options?');
|
||||
// }
|
||||
|
||||
if (prevOptions.display) {
|
||||
options.stat = prevOptions.stat;
|
||||
options.display = cloneDeep(prevOptions.display);
|
||||
options.maxValue = prevOptions.maxValue;
|
||||
options.minValue = prevOptions.minValue;
|
||||
// 6.0 -> 6.1, settings were stored on the root, now moved to display
|
||||
if (!options.display && !prevOptions && options.hasOwnProperty('thresholds')) {
|
||||
console.log('Migrating old gauge settings format', options);
|
||||
const migrate = options as any;
|
||||
const display = (migrate.valueOptions || {}) as DisplayValueOptions;
|
||||
|
||||
display.thresholds = migrate.thresholds;
|
||||
display.mappings = migrate.valueMappings;
|
||||
if (migrate.valueMappings) {
|
||||
options.stat = migrate.valueMappings.stat;
|
||||
delete migrate.valueMappings.stat;
|
||||
}
|
||||
|
||||
delete migrate.valueOptions;
|
||||
delete migrate.thresholds;
|
||||
delete migrate.valueMappings;
|
||||
|
||||
options.display = display;
|
||||
}
|
||||
|
||||
if (prevOptions) {
|
||||
optionsToCheck.forEach(v => {
|
||||
if (prevOptions.hasOwnProperty(v)) {
|
||||
options[v] = cloneDeep(prevOptions.display);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
@ -33,4 +55,4 @@ export const gaugePreserveOptionsHandler = (pluginId: string, prevOptions: any)
|
||||
|
||||
reactPanel.setEditor(GaugePanelEditor);
|
||||
reactPanel.setDefaults(defaults);
|
||||
reactPanel.setPreserveOptionsHandler(gaugePreserveOptionsHandler);
|
||||
reactPanel.setPanelTypeChangedHook(gaugePanelTypeChangedHook);
|
||||
|
@ -8,3 +8,10 @@ export const reactPanel = new ReactPanelPlugin<TextOptions>(TextPanel);
|
||||
|
||||
reactPanel.setEditor(TextPanelEditor);
|
||||
reactPanel.setDefaults(defaults);
|
||||
reactPanel.setPanelTypeChangedHook((options: TextOptions, prevPluginId: string, prevOptions: any) => {
|
||||
if (prevPluginId === 'text') {
|
||||
return prevOptions as TextOptions;
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user