diff --git a/.betterer.results b/.betterer.results index 658ec507d17..28408dbcdcb 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3359,17 +3359,18 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "19"], [0, 0, 0, "Unexpected any. Specify a different type.", "20"], [0, 0, 0, "Unexpected any. Specify a different type.", "21"], - [0, 0, 0, "Unexpected any. Specify a different type.", "22"], + [0, 0, 0, "Do not use any type assertions.", "22"], [0, 0, 0, "Unexpected any. Specify a different type.", "23"], [0, 0, 0, "Unexpected any. Specify a different type.", "24"], [0, 0, 0, "Unexpected any. Specify a different type.", "25"], [0, 0, 0, "Unexpected any. Specify a different type.", "26"], [0, 0, 0, "Unexpected any. Specify a different type.", "27"], - [0, 0, 0, "Do not use any type assertions.", "28"], + [0, 0, 0, "Unexpected any. Specify a different type.", "28"], [0, 0, 0, "Do not use any type assertions.", "29"], - [0, 0, 0, "Unexpected any. Specify a different type.", "30"], + [0, 0, 0, "Do not use any type assertions.", "30"], [0, 0, 0, "Unexpected any. Specify a different type.", "31"], - [0, 0, 0, "Do not use any type assertions.", "32"] + [0, 0, 0, "Unexpected any. Specify a different type.", "32"], + [0, 0, 0, "Do not use any type assertions.", "33"] ], "public/app/features/dashboard/state/DashboardModel.repeat.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/packages/grafana-data/src/dataframe/utils.ts b/packages/grafana-data/src/dataframe/utils.ts index 774c6cd4b6d..6cd53545a4c 100644 --- a/packages/grafana-data/src/dataframe/utils.ts +++ b/packages/grafana-data/src/dataframe/utils.ts @@ -3,10 +3,16 @@ import { DataFrame, FieldType } from '../types/dataFrame'; import { getTimeField } from './processDataFrame'; export function isTimeSeriesFrame(frame: DataFrame) { - if (frame.fields.length > 2) { + // If we have less than two frames we can't have a timeseries + if (frame.fields.length < 2) { return false; } - return Boolean(frame.fields.find((field) => field.type === FieldType.time)); + + // In order to have a time series we need a time field + // and at least one number field + const timeField = frame.fields.find((field) => field.type === FieldType.time); + const numberField = frame.fields.find((field) => field.type === FieldType.number); + return timeField !== undefined && numberField !== undefined; } export function isTimeSeriesFrames(data: DataFrame[]) { diff --git a/public/app/features/dashboard/components/GenAI/jsonDiffText.test.ts b/public/app/features/dashboard/components/GenAI/jsonDiffText.test.ts index 96f09c6d92f..aa58a8a5fa3 100644 --- a/public/app/features/dashboard/components/GenAI/jsonDiffText.test.ts +++ b/public/app/features/dashboard/components/GenAI/jsonDiffText.test.ts @@ -1,3 +1,4 @@ +import { DASHBOARD_SCHEMA_VERSION } from '../../state/DashboardMigrator'; import { createDashboardModelFixture, createPanelSaveModel } from '../../state/__fixtures__/dashboardFixtures'; import { orderProperties, JSONArray, JSONValue, isObject, getDashboardStringDiff } from './jsonDiffText'; @@ -239,7 +240,7 @@ describe('isObject', () => { describe('getDashboardStringDiff', () => { const dashboard = { title: 'Original Title', - schemaVersion: 38, + schemaVersion: DASHBOARD_SCHEMA_VERSION, panels: [ createPanelSaveModel({ id: 1, diff --git a/public/app/features/dashboard/components/GenAI/utils.test.ts b/public/app/features/dashboard/components/GenAI/utils.test.ts index 0ad4eb886e4..41a2fb16f80 100644 --- a/public/app/features/dashboard/components/GenAI/utils.test.ts +++ b/public/app/features/dashboard/components/GenAI/utils.test.ts @@ -1,5 +1,6 @@ import { llms } from '@grafana/experimental'; +import { DASHBOARD_SCHEMA_VERSION } from '../../state/DashboardMigrator'; import { createDashboardModelFixture, createPanelSaveModel } from '../../state/__fixtures__/dashboardFixtures'; import { getDashboardChanges, isLLMPluginEnabled, sanitizeReply } from './utils'; @@ -21,7 +22,7 @@ describe('getDashboardChanges', () => { const deprecatedOptions = { legend: { displayMode: 'hidden', showLegend: false }, }; - const deprecatedVersion = 37; + const deprecatedVersion = DASHBOARD_SCHEMA_VERSION - 1; const dashboard = createDashboardModelFixture({ schemaVersion: deprecatedVersion, panels: [createPanelSaveModel({ title: 'Panel 1', options: deprecatedOptions })], @@ -48,8 +49,8 @@ describe('getDashboardChanges', () => { ' {\n' + ' "editable": true,\n' + ' "graphTooltip": 0,\n' + - '- "schemaVersion": 37,\n' + - '+ "schemaVersion": 38,\n' + + `- "schemaVersion": ${deprecatedVersion},\n` + + `+ "schemaVersion": ${DASHBOARD_SCHEMA_VERSION},\n` + ' "timezone": "",\n' + ' "panels": [\n' + ' {\n' + @@ -62,7 +63,7 @@ describe('getDashboardChanges', () => { '+++ After user changes\t\n' + '@@ -3,16 +3,17 @@\n' + ' "graphTooltip": 0,\n' + - ' "schemaVersion": 38,\n' + + ` "schemaVersion": ${DASHBOARD_SCHEMA_VERSION},\n` + ' "timezone": "",\n' + ' "panels": [\n' + ' {\n' + diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index cea0683ac3b..38440120815 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.test.ts @@ -13,6 +13,8 @@ import { VariableHide } from '../../variables/types'; import { DashboardModel } from '../state/DashboardModel'; import { PanelModel } from '../state/PanelModel'; +import { DASHBOARD_SCHEMA_VERSION } from './DashboardMigrator'; + jest.mock('app/core/services/context_srv', () => ({})); const dataSources = { @@ -228,7 +230,7 @@ describe('DashboardModel', () => { }); it('dashboard schema version should be set to latest', () => { - expect(model.schemaVersion).toBe(38); + expect(model.schemaVersion).toBe(DASHBOARD_SCHEMA_VERSION); }); it('graph thresholds should be migrated', () => { diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index 5ebed64deea..6eece6a4515 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -15,6 +15,7 @@ import { isEmptyObject, MappingType, PanelPlugin, + ReducerID, SpecialValueMatch, standardEditorsRegistry, standardFieldConfigEditorRegistry, @@ -42,6 +43,10 @@ import { import getFactors from 'app/core/utils/factors'; import kbn from 'app/core/utils/kbn'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { + RefIdTransformerOptions, + TimeSeriesTableTransformerOptions, +} from 'app/features/transformers/timeSeriesTable/timeSeriesTableTransformer'; import { isConstant, isMulti } from 'app/features/variables/guard'; import { alignCurrentWithMulti } from 'app/features/variables/shared/multiOptions'; import { CloudWatchMetricsQuery, LegacyAnnotationQuery } from 'app/plugins/datasource/cloudwatch/types'; @@ -63,6 +68,14 @@ standardEditorsRegistry.setInit(getAllOptionEditors); standardFieldConfigEditorRegistry.setInit(getAllStandardFieldConfigs); type PanelSchemeUpgradeHandler = (panel: PanelModel) => PanelModel; + +/** + * The current version of the dashboard schema. + * To add a dashboard migration increment this number + * and then add your migration at the bottom of 'updateSchema' + * hint: search "Add migration here" + */ +export const DASHBOARD_SCHEMA_VERSION = 39; export class DashboardMigrator { dashboard: DashboardModel; @@ -79,7 +92,7 @@ export class DashboardMigrator { let i, j, k, n; const oldVersion = this.dashboard.schemaVersion; const panelUpgrades: PanelSchemeUpgradeHandler[] = []; - this.dashboard.schemaVersion = 38; + this.dashboard.schemaVersion = DASHBOARD_SCHEMA_VERSION; if (oldVersion === this.dashboard.schemaVersion) { return; @@ -849,6 +862,46 @@ export class DashboardMigrator { }); } + // Update the configuration of the Timeseries to table transformation + // to support multiple options per query + if (oldVersion < 39) { + panelUpgrades.push((panel: PanelModel) => { + panel.transformations?.forEach((transformation) => { + // If we run into a timeSeriesTable transformation + // and it doesn't have undefined options then we migrate + if ( + transformation.id === 'timeSeriesTable' && + transformation.options !== undefined && + transformation.options.refIdToStat !== undefined + ) { + let tableTransformOptions: TimeSeriesTableTransformerOptions = {}; + + // For each {refIdtoStat} record which maps refId to a statistic + // we add that to the stat property of the the new + // RefIdTransformerOptions interface which includes multiple settings + for (const [refId, stat] of Object.entries(transformation.options.refIdToStat)) { + let newSettings: RefIdTransformerOptions = {}; + // In this case the easiest way is just to do a type + // assertion as iterated entries have unknown types + newSettings.stat = stat as ReducerID; + tableTransformOptions[refId] = newSettings; + } + + // Update the options + transformation.options = tableTransformOptions; + } + }); + + return panel; + }); + } + + /** + * -==- Add migration here -==- + * Your migration should go below the previous + * block and above this (hopefully) helpful message. + */ + if (panelUpgrades.length === 0) { return; } diff --git a/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx b/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx index 1d4d6ad5df2..13eefd29ef4 100644 --- a/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx +++ b/public/app/features/transformers/timeSeriesTable/TimeSeriesTableTransformEditor.tsx @@ -1,30 +1,63 @@ import React, { useCallback } from 'react'; -import { PluginState, TransformerRegistryItem, TransformerUIProps, ReducerID, isReducerID } from '@grafana/data'; -import { InlineFieldRow, InlineField, StatsPicker } from '@grafana/ui'; +import { + PluginState, + TransformerRegistryItem, + TransformerUIProps, + ReducerID, + isReducerID, + SelectableValue, + getFieldDisplayName, +} from '@grafana/data'; +import { InlineFieldRow, InlineField, StatsPicker, InlineSwitch, Select } from '@grafana/ui'; -import { timeSeriesTableTransformer, TimeSeriesTableTransformerOptions } from './timeSeriesTableTransformer'; +import { + timeSeriesTableTransformer, + TimeSeriesTableTransformerOptions, + getRefData, +} from './timeSeriesTableTransformer'; export function TimeSeriesTableTransformEditor({ input, options, onChange, }: TransformerUIProps) { - const refIds: string[] = input.reduce((acc, frame) => { - if (frame.refId && !acc.includes(frame.refId)) { - return [...acc, frame.refId]; + const timeFields: Array> = []; + const refIdMap = getRefData(input); + + // Retrieve time fields + for (const frame of input) { + for (const field of frame.fields) { + if (field.type === 'time') { + const name = getFieldDisplayName(field, frame, input); + timeFields.push({ label: name, value: name }); + } } - return acc; - }, []); + } + + const onSelectTimefield = useCallback( + (refId: string, value: SelectableValue) => { + const val = value?.value !== undefined ? value.value : ''; + onChange({ + ...options, + [refId]: { + ...options[refId], + timeField: val, + }, + }); + }, + [onChange, options] + ); const onSelectStat = useCallback( (refId: string, stats: string[]) => { const reducerID = stats[0]; if (reducerID && isReducerID(reducerID)) { onChange({ - refIdToStat: { - ...options.refIdToStat, - [refId]: reducerID, + ...options, + [refId]: { + ...options[refId], + stat: reducerID, }, }); } @@ -32,25 +65,55 @@ export function TimeSeriesTableTransformEditor({ [onChange, options] ); - return ( - <> - {refIds.map((refId) => { - return ( -
- - 1 ? ` #${refId}` : ''} value`}> - ext.id !== ReducerID.allValues && ext.id !== ReducerID.uniqueValues} - /> - - -
- ); - })} - + const onMergeSeriesToggle = useCallback( + (refId: string) => { + const mergeSeries = options[refId]?.mergeSeries !== undefined ? !options[refId].mergeSeries : false; + onChange({ + ...options, + [refId]: { + ...options[refId], + mergeSeries, + }, + }); + }, + [onChange, options] ); + + let configRows = []; + for (const refId of Object.keys(refIdMap)) { + configRows.push( + + +