add migration tests

This commit is contained in:
ryan 2019-03-14 09:50:59 -07:00
commit ccf66154bb
12 changed files with 122 additions and 23 deletions

View File

@ -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)

View File

@ -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.
![](/img/docs/v60/cloudwatch_metric_math.png)
## Templated queries
Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place.

View File

@ -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"]
}
]
```

View File

@ -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

View File

@ -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;
}
}

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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);

View 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();
});
});
});

View File

@ -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);

View File

@ -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;
});