diff --git a/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx b/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx index f1acd4eb418..0e71277c26a 100644 --- a/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx +++ b/public/app/features/dashboard-scene/settings/VariablesEditView.test.tsx @@ -1,6 +1,18 @@ +import { of } from 'rxjs'; + +import { + FieldType, + LoadingState, + PanelData, + VariableSupportType, + getDefaultTimeRange, + toDataFrame, +} from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks'; -import { setPluginImportUtils } from '@grafana/runtime'; +import { setPluginImportUtils, setRunRequest } from '@grafana/runtime'; import { SceneVariableSet, CustomVariable, SceneGridItem, SceneGridLayout, VizPanel } from '@grafana/scenes'; +import { mockDataSource } from 'app/features/alerting/unified/mocks'; +import { LegacyVariableQueryEditor } from 'app/features/variables/editor/LegacyVariableQueryEditor'; import { DashboardScene } from '../scene/DashboardScene'; import { activateFullSceneTree } from '../utils/test-utils'; @@ -12,6 +24,46 @@ setPluginImportUtils({ getPanelPluginFromCache: (id: string) => undefined, }); +const defaultDatasource = mockDataSource({ + name: 'Default Test Data Source', + type: 'test', +}); + +const promDatasource = mockDataSource({ + name: 'Prometheus', + type: 'prometheus', +}); + +jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => ({ + ...jest.requireActual('@grafana/runtime/src/services/dataSourceSrv'), + getDataSourceSrv: () => ({ + get: async () => ({ + ...defaultDatasource, + variables: { + getType: () => VariableSupportType.Custom, + query: jest.fn(), + editor: jest.fn().mockImplementation(LegacyVariableQueryEditor), + }, + }), + getList: () => [defaultDatasource, promDatasource], + getInstanceSettings: () => ({ ...defaultDatasource }), + }), +})); + +const runRequestMock = jest.fn().mockReturnValue( + of({ + state: LoadingState.Done, + series: [ + toDataFrame({ + fields: [{ name: 'text', type: FieldType.string, values: ['val1', 'val2', 'val11'] }], + }), + ], + timeRange: getDefaultTimeRange(), + }) +); + +setRunRequest(runRequestMock); + describe('VariablesEditView', () => { describe('Dashboard Variables state', () => { let dashboard: DashboardScene; @@ -130,40 +182,15 @@ describe('VariablesEditView', () => { expect(variableView.state.editIndex).toBeUndefined(); }); - it('should reset editing variable when discarding changes', () => { - variableView.onEdit('customVar2'); - const editIndex = variableView.state.editIndex!; - const variable = variableView.getVariables()[editIndex]; - const originalState = { ...variable.state }; - - variable.setState({ name: 'newName' }); - variableView.onDiscardChanges(); - - const newVariable = variableView.getVariables()[editIndex]; - expect(newVariable.state).toEqual(originalState); + it('should add default new query variable when onAdd is called', () => { + variableView.onAdd(); + expect(variableView.getVariables()).toHaveLength(3); + expect(variableView.getVariables()[2].state.name).toBe('query0'); + expect(variableView.getVariables()[2].state.type).toBe('query'); }); - it('should reset editing variable when discarding changes after the type being changed', () => { - variableView.onEdit('customVar2'); - const editIndex = variableView.state.editIndex!; - const variable = variableView.getVariables()[editIndex]; - const originalState = { ...variable.state }; - - variableView.onTypeChange('constant'); - variableView.onDiscardChanges(); - - const newVariable = variableView.getVariables()[editIndex]; - expect(newVariable.state).toEqual(originalState); - }); - - it('should go back when discarding changes', () => { - variableView.onEdit('customVar2'); - const editIndex = variableView.state.editIndex!; - expect(editIndex).toBeDefined(); - - variableView.onDiscardChanges(); - - expect(variableView.state.editIndex).toBeUndefined(); + afterEach(() => { + jest.clearAllMocks(); }); }); diff --git a/public/app/features/dashboard-scene/settings/VariablesEditView.tsx b/public/app/features/dashboard-scene/settings/VariablesEditView.tsx index 48ede5a45c3..2f79d02a8df 100644 --- a/public/app/features/dashboard-scene/settings/VariablesEditView.tsx +++ b/public/app/features/dashboard-scene/settings/VariablesEditView.tsx @@ -5,7 +5,6 @@ import { SceneComponentProps, SceneObjectBase, SceneVariable, - SceneVariableState, SceneVariables, sceneGraph, AdHocFilterSet, @@ -20,10 +19,9 @@ import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync'; import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils'; import { VariableEditorForm } from './variables/VariableEditorForm'; import { VariableEditorList } from './variables/VariableEditorList'; -import { EditableVariableType, getVariableScene, isEditableVariableType } from './variables/utils'; +import { EditableVariableType, getVariableDefault, getVariableScene } from './variables/utils'; export interface VariablesEditViewState extends DashboardEditViewState { editIndex?: number | undefined; - originalVariableState?: SceneVariableState; } export class VariablesEditView extends SceneObjectBase implements DashboardEditView { @@ -104,19 +102,18 @@ export class VariablesEditView extends SceneObjectBase i return; } - const originalVariable = variables[variableIndex]; + const variableToUpdate = variables[variableIndex]; let copyNumber = 0; - let newName = `copy_of_${originalVariable.state.name}`; + let newName = `copy_of_${variableToUpdate.state.name}`; // Check if the name is unique, if not, increment the copy number while (variables.some((v) => v.state.name === newName)) { copyNumber++; - newName = `copy_of_${originalVariable.state.name}_${copyNumber}`; + newName = `copy_of_${variableToUpdate.state.name}_${copyNumber}`; } //clone the original variable - - const newVariable = originalVariable.clone(originalVariable.state); + const newVariable = variableToUpdate.clone(variableToUpdate.state); // update state name of the new variable newVariable.setState({ name: newName }); @@ -153,7 +150,20 @@ export class VariablesEditView extends SceneObjectBase i console.error('Variable not found'); return; } - this.setState({ editIndex: variableIndex, originalVariableState: { ...this.getVariables()[variableIndex].state } }); + this.setState({ editIndex: variableIndex }); + }; + + public onAdd = () => { + const variables = this.getVariables(); + const variableIndex = variables.length; + //add the new variable to the end of the array + const defaultNewVariable = getVariableDefault(variables); + if (defaultNewVariable instanceof AdHocFilterSet) { + // TODO: Update controls in adding this fiter set to the dashboard + } else { + this.getVariableSet().setState({ variables: [...this.getVariables(), defaultNewVariable] }); + this.setState({ editIndex: variableIndex }); + } }; public onTypeChange = (type: EditableVariableType) => { @@ -176,36 +186,13 @@ export class VariablesEditView extends SceneObjectBase i public onGoBack = () => { this.setState({ editIndex: undefined }); }; - - public onDiscardChanges: () => void = () => { - const variables = this.getVariableSet().state.variables; - const { editIndex, originalVariableState } = this.state; - if (editIndex === undefined || !originalVariableState) { - return; - } - const variable = variables[editIndex]; - if (!variable) { - return; - } - if (isEditableVariableType(originalVariableState.type)) { - const newVariable = getVariableScene(originalVariableState.type, originalVariableState); - if (newVariable instanceof AdHocFilterSet) { - // TODO: Update controls in adding this fiter set to the dashboard - } else { - const updatedVariables = [...variables.slice(0, editIndex), newVariable, ...variables.slice(editIndex + 1)]; - this.getVariableSet().setState({ variables: updatedVariables }); - } - } - - this.setState({ editIndex: undefined, originalVariableState: undefined }); - }; } function VariableEditorSettingsListView({ model }: SceneComponentProps) { const dashboard = model.getDashboard(); const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); // get variables from dashboard state - const { onDelete, onDuplicated, onOrderChanged, onEdit, onTypeChange, onGoBack, onDiscardChanges } = model; + const { onDelete, onDuplicated, onOrderChanged, onEdit, onTypeChange, onGoBack, onAdd } = model; const { variables } = model.getVariableSet().useState(); const { editIndex } = model.useState(); @@ -217,7 +204,6 @@ function VariableEditorSettingsListView({ model }: SceneComponentProps {}} + onAdd={onAdd} onEdit={onEdit} /> @@ -248,7 +234,6 @@ interface VariableEditorSettingsEditViewProps { dashboard: DashboardScene; onTypeChange: (variableType: EditableVariableType) => void; onGoBack: () => void; - onDiscardChanges: () => void; } function VariableEditorSettingsView({ @@ -258,7 +243,6 @@ function VariableEditorSettingsView({ dashboard, onTypeChange, onGoBack, - onDiscardChanges, }: VariableEditorSettingsEditViewProps) { const parentTab = pageNav.children!.find((p) => p.active)!; parentTab.parentItem = pageNav; @@ -271,12 +255,7 @@ function VariableEditorSettingsView({ return ( - + ); } diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx index 14cecad19ae..3038cb6fe04 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx @@ -22,10 +22,9 @@ interface VariableEditorFormProps { variable: SceneVariable; onTypeChange: (type: EditableVariableType) => void; onGoBack: () => void; - onDiscardChanges: () => void; } -export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDiscardChanges }: VariableEditorFormProps) { +export function VariableEditorForm({ variable, onTypeChange, onGoBack }: VariableEditorFormProps) { const { name, type, label, description, hide } = variable.useState(); const EditorToRender = isEditableVariableType(type) ? getVariableEditor(type) : undefined; const [runQueryState, onRunQuery] = useAsyncFn(async () => { @@ -106,13 +105,6 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDiscard {runQueryState.loading ? : `Run query`} )} - diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditorList.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditorList.tsx index 8cbe42568ef..27d16634ae8 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariableEditorList.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditorList.tsx @@ -5,7 +5,7 @@ import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd'; import { selectors } from '@grafana/e2e-selectors'; import { reportInteraction } from '@grafana/runtime'; import { SceneVariable, SceneVariableState } from '@grafana/scenes'; -import { useStyles2, Stack } from '@grafana/ui'; +import { useStyles2, Stack, Button } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; import { VariableEditorListRow } from './VariableEditorListRow'; @@ -80,6 +80,15 @@ export function VariableEditorList({ + + + )} @@ -94,7 +103,6 @@ function EmptyVariablesList({ onAdd }: { onAdd: () => void }): ReactElement { title="There are no variables yet" buttonIcon="calculator-alt" buttonTitle="Add variable" - buttonDisabled infoBox={{ __html: `

Variables enable more interactive and dynamic dashboards. Instead of hard-coding things like server diff --git a/public/app/features/dashboard-scene/settings/variables/utils.test.ts b/public/app/features/dashboard-scene/settings/variables/utils.test.ts index 9cdafc13411..9725ee5fe5c 100644 --- a/public/app/features/dashboard-scene/settings/variables/utils.test.ts +++ b/public/app/features/dashboard-scene/settings/variables/utils.test.ts @@ -8,6 +8,7 @@ import { DataSourceVariable, AdHocFiltersVariable, TextBoxVariable, + SceneVariableSet, } from '@grafana/scenes'; import { DataQuery, DataSourceJsonData, VariableType } from '@grafana/schema'; import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard'; @@ -31,6 +32,8 @@ import { EditableVariableType, getDefinition, getOptionDataSourceTypes, + getNextAvailableId, + getVariableDefault, } from './utils'; const templateSrv = { @@ -249,3 +252,47 @@ describe('getOptionDataSourceTypes', () => { expect(optionTypes[1].label).toBe('ds1'); }); }); + +describe('getNextAvailableId', () => { + it('should return the initial ID for an empty array', () => { + const sceneVariables = new SceneVariableSet({ + variables: [], + }); + + expect(getNextAvailableId('query', sceneVariables.state.variables)).toBe('query0'); + }); + + it('should return a non-conflicting ID for a non-empty array', () => { + const variable = new QueryVariable({ + name: 'query0', + label: 'test-label', + description: 'test-desc', + value: ['selected-value'], + text: ['selected-value-text'], + datasource: { uid: 'fake-std', type: 'fake-std' }, + query: 'query', + includeAll: true, + allValue: 'test-all', + isMulti: true, + }); + + const sceneVariables = new SceneVariableSet({ + variables: [variable], + }); + + expect(getNextAvailableId('query', sceneVariables.state.variables)).toBe('query1'); + }); +}); + +describe('getVariableDefault', () => { + it('should return a QueryVariable instance with the correct name', () => { + const sceneVariables = new SceneVariableSet({ + variables: [], + }); + + const defaultVariable = getVariableDefault(sceneVariables.state.variables); + + expect(defaultVariable).toBeInstanceOf(QueryVariable); + expect(defaultVariable.state.name).toBe('query0'); + }); +}); diff --git a/public/app/features/dashboard-scene/settings/variables/utils.ts b/public/app/features/dashboard-scene/settings/variables/utils.ts index 4b1514a336e..8b6480b6c9e 100644 --- a/public/app/features/dashboard-scene/settings/variables/utils.ts +++ b/public/app/features/dashboard-scene/settings/variables/utils.ts @@ -12,6 +12,7 @@ import { AdHocFilterSet, SceneVariable, MultiValueVariable, + SceneVariableState, } from '@grafana/scenes'; import { VariableType } from '@grafana/schema'; @@ -122,6 +123,25 @@ export function getVariableScene(type: EditableVariableType, initialState: Commo } } +export function getVariableDefault(variables: Array>) { + const defaultVariableType = 'query'; + const nextVariableIdName = getNextAvailableId(defaultVariableType, variables); + return new QueryVariable({ + name: nextVariableIdName, + }); +} + +export function getNextAvailableId(type: VariableType, variables: Array>): string { + let counter = 0; + let nextId = `${type}${counter}`; + + while (variables.find((variable) => variable.state.name === nextId)) { + nextId = `${type}${++counter}`; + } + + return nextId; +} + export function hasVariableOptions(variable: SceneVariable): variable is MultiValueVariable { // variable options can be defined by state.options or state.intervals in case of interval variable return 'options' in variable.state || 'intervals' in variable.state;