mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboard: Migration - Dashboard Settings Variables (List, Duplicate, Delete) (#78917)
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
parent
f3cdb44898
commit
4c6bbabc1c
@ -36,7 +36,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
|||||||
const update: Partial<DashboardSceneState> = {};
|
const update: Partial<DashboardSceneState> = {};
|
||||||
|
|
||||||
if (typeof values.editview === 'string' && meta.canEdit) {
|
if (typeof values.editview === 'string' && meta.canEdit) {
|
||||||
update.editview = createDashboardEditViewFor(values.editview, this._scene.getRef());
|
update.editview = createDashboardEditViewFor(values.editview);
|
||||||
|
|
||||||
// If we are not in editing (for example after full page reload)
|
// If we are not in editing (for example after full page reload)
|
||||||
if (!isEditing) {
|
if (!isEditing) {
|
||||||
|
@ -122,6 +122,25 @@ describe('transformSaveModelToScene', () => {
|
|||||||
expect(scene.state.$behaviors![1]).toBeInstanceOf(behaviors.CursorSync);
|
expect(scene.state.$behaviors![1]).toBeInstanceOf(behaviors.CursorSync);
|
||||||
expect((scene.state.$behaviors![1] as behaviors.CursorSync).state.sync).toEqual(DashboardCursorSync.Crosshair);
|
expect((scene.state.$behaviors![1] as behaviors.CursorSync).state.sync).toEqual(DashboardCursorSync.Crosshair);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should initialize the Dashboard Scene with empty template variables', () => {
|
||||||
|
const dash = {
|
||||||
|
...defaultDashboard,
|
||||||
|
title: 'test empty dashboard with no variables',
|
||||||
|
uid: 'test-uid',
|
||||||
|
time: { from: 'now-10h', to: 'now' },
|
||||||
|
weekStart: 'saturday',
|
||||||
|
fiscalYearStartMonth: 2,
|
||||||
|
timezone: 'America/New_York',
|
||||||
|
templating: {
|
||||||
|
list: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const oldModel = new DashboardModel(dash);
|
||||||
|
|
||||||
|
const scene = createDashboardSceneFromDashboardModel(oldModel);
|
||||||
|
expect(scene.state.$variables?.state.variables).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when organizing panels as scene children', () => {
|
describe('when organizing panels as scene children', () => {
|
||||||
|
@ -195,6 +195,11 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
|
|||||||
variables = new SceneVariableSet({
|
variables = new SceneVariableSet({
|
||||||
variables: variableObjects,
|
variables: variableObjects,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Create empty variable set
|
||||||
|
variables = new SceneVariableSet({
|
||||||
|
variables: [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldModel.annotations?.list?.length) {
|
if (oldModel.annotations?.list?.length) {
|
||||||
|
@ -9,6 +9,7 @@ import { Page } from 'app/core/components/Page/Page';
|
|||||||
|
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
||||||
|
import { getDashboardSceneFor } from '../utils/utils';
|
||||||
|
|
||||||
import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync';
|
import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync';
|
||||||
import { DashboardEditView, DashboardEditListViewState, useDashboardEditPageNav } from './utils';
|
import { DashboardEditView, DashboardEditListViewState, useDashboardEditPageNav } from './utils';
|
||||||
@ -26,8 +27,8 @@ export class DashboardLinksEditView extends SceneObjectBase<DashboardLinksEditVi
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DashboardLinksEditViewRenderer({ model }: SceneComponentProps<DashboardLinksEditView>) {
|
function DashboardLinksEditViewRenderer({ model }: SceneComponentProps<DashboardLinksEditView>) {
|
||||||
const { dashboardRef, editIndex } = model.useState();
|
const { editIndex } = model.useState();
|
||||||
const dashboard = dashboardRef.resolve();
|
const dashboard = getDashboardSceneFor(model);
|
||||||
const links = dashboard.state.links || [];
|
const links = dashboard.state.links || [];
|
||||||
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
||||||
|
|
||||||
|
@ -112,6 +112,7 @@ describe('GeneralSettingsEditView', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function buildTestScene() {
|
async function buildTestScene() {
|
||||||
|
const settings = new GeneralSettingsEditView({});
|
||||||
const dashboard = new DashboardScene({
|
const dashboard = new DashboardScene({
|
||||||
$timeRange: new SceneTimeRange({}),
|
$timeRange: new SceneTimeRange({}),
|
||||||
$behaviors: [new behaviors.CursorSync({ sync: DashboardCursorSync.Off })],
|
$behaviors: [new behaviors.CursorSync({ sync: DashboardCursorSync.Off })],
|
||||||
@ -143,10 +144,7 @@ async function buildTestScene() {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
});
|
editview: settings,
|
||||||
|
|
||||||
const settings = new GeneralSettingsEditView({
|
|
||||||
dashboardRef: dashboard.getRef(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
activateFullSceneTree(dashboard);
|
activateFullSceneTree(dashboard);
|
||||||
|
@ -1,14 +1,7 @@
|
|||||||
import React, { ChangeEvent } from 'react';
|
import React, { ChangeEvent } from 'react';
|
||||||
|
|
||||||
import { PageLayoutType } from '@grafana/data';
|
import { PageLayoutType } from '@grafana/data';
|
||||||
import {
|
import { behaviors, SceneComponentProps, SceneObjectBase, SceneTimePicker, sceneGraph } from '@grafana/scenes';
|
||||||
behaviors,
|
|
||||||
SceneComponentProps,
|
|
||||||
SceneObjectBase,
|
|
||||||
SceneObjectRef,
|
|
||||||
SceneTimePicker,
|
|
||||||
sceneGraph,
|
|
||||||
} from '@grafana/scenes';
|
|
||||||
import { TimeZone } from '@grafana/schema';
|
import { TimeZone } from '@grafana/schema';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@ -31,12 +24,11 @@ import { DashboardControls } from '../scene/DashboardControls';
|
|||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
||||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||||
|
import { getDashboardSceneFor } from '../utils/utils';
|
||||||
|
|
||||||
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
|
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
|
||||||
|
|
||||||
export interface GeneralSettingsEditViewState extends DashboardEditViewState {
|
export interface GeneralSettingsEditViewState extends DashboardEditViewState {}
|
||||||
dashboardRef: SceneObjectRef<DashboardScene>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EDITABLE_OPTIONS = [
|
const EDITABLE_OPTIONS = [
|
||||||
{ label: 'Editable', value: true },
|
{ label: 'Editable', value: true },
|
||||||
@ -54,7 +46,7 @@ export class GeneralSettingsEditView
|
|||||||
implements DashboardEditView
|
implements DashboardEditView
|
||||||
{
|
{
|
||||||
private get _dashboard(): DashboardScene {
|
private get _dashboard(): DashboardScene {
|
||||||
return this.state.dashboardRef.resolve();
|
return getDashboardSceneFor(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUrlKey(): string {
|
public getUrlKey(): string {
|
||||||
|
@ -0,0 +1,148 @@
|
|||||||
|
import { SceneVariableSet, CustomVariable, SceneGridItem, SceneGridLayout } from '@grafana/scenes';
|
||||||
|
|
||||||
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
|
import { activateFullSceneTree } from '../utils/test-utils';
|
||||||
|
|
||||||
|
import { VariablesEditView } from './VariablesEditView';
|
||||||
|
|
||||||
|
describe('VariablesEditView', () => {
|
||||||
|
describe('Dashboard Variables state', () => {
|
||||||
|
let dashboard: DashboardScene;
|
||||||
|
let variableView: VariablesEditView;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const result = await buildTestScene();
|
||||||
|
dashboard = result.dashboard;
|
||||||
|
variableView = result.variableView;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct urlKey', () => {
|
||||||
|
expect(variableView.getUrlKey()).toBe('variables');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the dashboard', () => {
|
||||||
|
expect(variableView.getDashboard()).toBe(dashboard);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the list of variables', () => {
|
||||||
|
const expectedVariables = [
|
||||||
|
{
|
||||||
|
type: 'custom',
|
||||||
|
name: 'customVar',
|
||||||
|
query: 'test, test2',
|
||||||
|
value: 'test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'custom',
|
||||||
|
name: 'customVar2',
|
||||||
|
query: 'test3, test4',
|
||||||
|
value: 'test3',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const variables = variableView.getVariables();
|
||||||
|
expect(variables).toHaveLength(2);
|
||||||
|
expect(variables[0].state).toMatchObject(expectedVariables[0]);
|
||||||
|
expect(variables[1].state).toMatchObject(expectedVariables[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Dashboard Variables actions', () => {
|
||||||
|
let variableView: VariablesEditView;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const result = await buildTestScene();
|
||||||
|
variableView = result.variableView;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should duplicate a variable', () => {
|
||||||
|
const variables = variableView.getVariables();
|
||||||
|
const variable = variables[0];
|
||||||
|
variableView.onDuplicated(variable.state.name);
|
||||||
|
expect(variableView.getVariables()).toHaveLength(3);
|
||||||
|
expect(variableView.getVariables()[1].state.name).toBe('copy_of_customVar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle name when duplicating a variable twice', () => {
|
||||||
|
const variableIdentifier = 'customVar';
|
||||||
|
variableView.onDuplicated(variableIdentifier);
|
||||||
|
variableView.onDuplicated(variableIdentifier);
|
||||||
|
expect(variableView.getVariables()).toHaveLength(4);
|
||||||
|
expect(variableView.getVariables()[1].state.name).toBe('copy_of_customVar_1');
|
||||||
|
expect(variableView.getVariables()[2].state.name).toBe('copy_of_customVar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete a variable', () => {
|
||||||
|
const variableIdentifier = 'customVar';
|
||||||
|
variableView.onDelete(variableIdentifier);
|
||||||
|
expect(variableView.getVariables()).toHaveLength(1);
|
||||||
|
expect(variableView.getVariables()[0].state.name).toBe('customVar2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change order of variables', () => {
|
||||||
|
const fromIndex = 0; // customVar is first
|
||||||
|
const toIndex = 1;
|
||||||
|
variableView.onOrderChanged(fromIndex, toIndex);
|
||||||
|
expect(variableView.getVariables()[0].state.name).toBe('customVar2');
|
||||||
|
expect(variableView.getVariables()[1].state.name).toBe('customVar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep the same order of variables with invalid indexes', () => {
|
||||||
|
const fromIndex = 0;
|
||||||
|
const toIndex = 2;
|
||||||
|
|
||||||
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
variableView.onOrderChanged(fromIndex, toIndex);
|
||||||
|
expect(errorSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(variableView.getVariables()[0].state.name).toBe('customVar');
|
||||||
|
expect(variableView.getVariables()[1].state.name).toBe('customVar2');
|
||||||
|
|
||||||
|
errorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function buildTestScene() {
|
||||||
|
const variableView = new VariablesEditView({});
|
||||||
|
const dashboard = new DashboardScene({
|
||||||
|
title: 'Dashboard with variables',
|
||||||
|
uid: 'dash-variables',
|
||||||
|
meta: {
|
||||||
|
canEdit: true,
|
||||||
|
},
|
||||||
|
$variables: new SceneVariableSet({
|
||||||
|
variables: [
|
||||||
|
new CustomVariable({
|
||||||
|
name: 'customVar',
|
||||||
|
query: 'test, test2',
|
||||||
|
}),
|
||||||
|
new CustomVariable({
|
||||||
|
name: 'customVar2',
|
||||||
|
query: 'test3, test4',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
body: new SceneGridLayout({
|
||||||
|
children: [
|
||||||
|
new SceneGridItem({
|
||||||
|
key: 'griditem-1',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 10,
|
||||||
|
height: 12,
|
||||||
|
body: undefined,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
editview: variableView,
|
||||||
|
});
|
||||||
|
|
||||||
|
activateFullSceneTree(dashboard);
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 1));
|
||||||
|
|
||||||
|
dashboard.onEnterEditMode();
|
||||||
|
variableView.activate();
|
||||||
|
|
||||||
|
return { dashboard, variableView };
|
||||||
|
}
|
@ -1,30 +1,133 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { PageLayoutType } from '@grafana/data';
|
import { PageLayoutType } from '@grafana/data';
|
||||||
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
|
import { SceneComponentProps, SceneObjectBase, SceneVariables, sceneGraph } from '@grafana/scenes';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
|
||||||
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
||||||
import { getDashboardSceneFor } from '../utils/utils';
|
import { getDashboardSceneFor } from '../utils/utils';
|
||||||
|
|
||||||
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
|
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
|
||||||
|
import { VariableEditorList } from './variables/VariableEditorList';
|
||||||
export interface VariablesEditViewState extends DashboardEditViewState {}
|
export interface VariablesEditViewState extends DashboardEditViewState {}
|
||||||
|
|
||||||
export class VariablesEditView extends SceneObjectBase<VariablesEditViewState> implements DashboardEditView {
|
export class VariablesEditView extends SceneObjectBase<VariablesEditViewState> implements DashboardEditView {
|
||||||
|
public static Component = VariableEditorSettingsListView;
|
||||||
|
|
||||||
public getUrlKey(): string {
|
public getUrlKey(): string {
|
||||||
return 'variables';
|
return 'variables';
|
||||||
}
|
}
|
||||||
|
|
||||||
static Component = ({ model }: SceneComponentProps<VariablesEditView>) => {
|
public getDashboard(): DashboardScene {
|
||||||
const dashboard = getDashboardSceneFor(model);
|
return getDashboardSceneFor(this);
|
||||||
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
}
|
||||||
|
|
||||||
return (
|
public getVariableSet(): SceneVariables {
|
||||||
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
|
return sceneGraph.getVariables(this.getDashboard());
|
||||||
<NavToolbarActions dashboard={dashboard} />
|
}
|
||||||
<div>variables todo</div>
|
|
||||||
</Page>
|
private getVariableIndex = (identifier: string) => {
|
||||||
);
|
const variables = this.getVariables();
|
||||||
|
return variables.findIndex((variable) => variable.state.name === identifier);
|
||||||
|
};
|
||||||
|
|
||||||
|
public onDelete = (identifier: string) => {
|
||||||
|
// Find the index of the variable to be deleted
|
||||||
|
const variableIndex = this.getVariableIndex(identifier);
|
||||||
|
const { variables } = this.getVariableSet().state;
|
||||||
|
if (variableIndex === -1) {
|
||||||
|
// Handle the case where the variable is not found
|
||||||
|
console.error('Variable not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new array excluding the variable to be deleted
|
||||||
|
const updatedVariables = [...variables.slice(0, variableIndex), ...variables.slice(variableIndex + 1)];
|
||||||
|
|
||||||
|
// Update the state or the variables array
|
||||||
|
this.getVariableSet().setState({ variables: updatedVariables });
|
||||||
|
};
|
||||||
|
|
||||||
|
public getVariables() {
|
||||||
|
return this.getVariableSet().state.variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDuplicated = (identifier: string) => {
|
||||||
|
const variableIndex = this.getVariableIndex(identifier);
|
||||||
|
const variables = this.getVariableSet().state.variables;
|
||||||
|
|
||||||
|
if (variableIndex === -1) {
|
||||||
|
console.error('Variable not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalVariable = variables[variableIndex];
|
||||||
|
let copyNumber = 0;
|
||||||
|
let newName = `copy_of_${originalVariable.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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
//clone the original variable
|
||||||
|
|
||||||
|
const newVariable = originalVariable.clone(originalVariable.state);
|
||||||
|
// update state name of the new variable
|
||||||
|
newVariable.setState({ name: newName });
|
||||||
|
|
||||||
|
const updatedVariables = [
|
||||||
|
...variables.slice(0, variableIndex + 1),
|
||||||
|
newVariable,
|
||||||
|
...variables.slice(variableIndex + 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
this.getVariableSet().setState({ variables: updatedVariables });
|
||||||
|
};
|
||||||
|
|
||||||
|
public onOrderChanged = (fromIndex: number, toIndex: number) => {
|
||||||
|
const variables = this.getVariableSet().state.variables;
|
||||||
|
if (!this.getVariableSet()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// check the index are within the variables array
|
||||||
|
if (fromIndex < 0 || fromIndex >= variables.length || toIndex < 0 || toIndex >= variables.length) {
|
||||||
|
console.error('Invalid index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const updatedVariables = [...variables];
|
||||||
|
// Remove the variable from the array
|
||||||
|
const movedItem = updatedVariables.splice(fromIndex, 1);
|
||||||
|
updatedVariables.splice(toIndex, 0, movedItem[0]);
|
||||||
|
const variablesScene = this.getVariableSet();
|
||||||
|
variablesScene.setState({ variables: updatedVariables });
|
||||||
|
};
|
||||||
|
|
||||||
|
public onEdit = (identifier: string) => {
|
||||||
|
return 'not implemented';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function VariableEditorSettingsListView({ model }: SceneComponentProps<VariablesEditView>) {
|
||||||
|
const dashboard = model.getDashboard();
|
||||||
|
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
||||||
|
// get variables from dashboard state
|
||||||
|
const { onDelete, onDuplicated, onOrderChanged, onEdit } = model;
|
||||||
|
const { variables } = model.getVariableSet().useState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
|
||||||
|
<NavToolbarActions dashboard={dashboard} />
|
||||||
|
<VariableEditorList
|
||||||
|
variables={variables}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onDuplicate={onDuplicated}
|
||||||
|
onChangeOrder={onOrderChanged}
|
||||||
|
onAdd={() => {}}
|
||||||
|
onEdit={onEdit}
|
||||||
|
/>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { locationUtil, NavModelItem } from '@grafana/data';
|
import { locationUtil, NavModelItem } from '@grafana/data';
|
||||||
import { SceneObject, SceneObjectRef, SceneObjectState } from '@grafana/scenes';
|
import { SceneObject, SceneObjectState } from '@grafana/scenes';
|
||||||
import { t } from 'app/core/internationalization';
|
import { t } from 'app/core/internationalization';
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import { useSelector } from 'app/types';
|
import { useSelector } from 'app/types';
|
||||||
@ -13,9 +13,7 @@ import { DashboardLinksEditView } from './DashboardLinksEditView';
|
|||||||
import { GeneralSettingsEditView } from './GeneralSettingsEditView';
|
import { GeneralSettingsEditView } from './GeneralSettingsEditView';
|
||||||
import { VariablesEditView } from './VariablesEditView';
|
import { VariablesEditView } from './VariablesEditView';
|
||||||
|
|
||||||
export interface DashboardEditViewState extends SceneObjectState {
|
export interface DashboardEditViewState extends SceneObjectState {}
|
||||||
dashboardRef: SceneObjectRef<DashboardScene>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DashboardEditListViewState extends DashboardEditViewState {
|
export interface DashboardEditListViewState extends DashboardEditViewState {
|
||||||
/** Index of the list item to edit */
|
/** Index of the list item to edit */
|
||||||
@ -63,19 +61,16 @@ export function useDashboardEditPageNav(dashboard: DashboardScene, currentEditVi
|
|||||||
return { navModel, pageNav };
|
return { navModel, pageNav };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDashboardEditViewFor(
|
export function createDashboardEditViewFor(editview: string): DashboardEditView {
|
||||||
editview: string,
|
|
||||||
dashboardRef: SceneObjectRef<DashboardScene>
|
|
||||||
): DashboardEditView {
|
|
||||||
switch (editview) {
|
switch (editview) {
|
||||||
case 'annotations':
|
case 'annotations':
|
||||||
return new AnnotationsEditView({ dashboardRef });
|
return new AnnotationsEditView({});
|
||||||
case 'variables':
|
case 'variables':
|
||||||
return new VariablesEditView({ dashboardRef });
|
return new VariablesEditView({});
|
||||||
case 'links':
|
case 'links':
|
||||||
return new DashboardLinksEditView({ dashboardRef });
|
return new DashboardLinksEditView({});
|
||||||
case 'settings':
|
case 'settings':
|
||||||
default:
|
default:
|
||||||
return new GeneralSettingsEditView({ dashboardRef });
|
return new GeneralSettingsEditView({});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,125 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React, { ReactElement } from 'react';
|
||||||
|
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 EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||||
|
|
||||||
|
import { VariableEditorListRow } from './VariableEditorListRow';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
variables: Array<SceneVariable<SceneVariableState>>;
|
||||||
|
onAdd: () => void;
|
||||||
|
onChangeOrder: (fromIndex: number, toIndex: number) => void;
|
||||||
|
onDuplicate: (identifier: string) => void;
|
||||||
|
onDelete: (identifier: string) => void;
|
||||||
|
onEdit: (identifier: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VariableEditorList({
|
||||||
|
variables,
|
||||||
|
onChangeOrder,
|
||||||
|
onDelete,
|
||||||
|
onDuplicate,
|
||||||
|
onAdd,
|
||||||
|
onEdit,
|
||||||
|
}: Props): ReactElement {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const onDragEnd = (result: DropResult) => {
|
||||||
|
if (!result.destination || !result.source) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reportInteraction('Variable drag and drop');
|
||||||
|
onChangeOrder(result.source.index, result.destination.index);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{variables.length === 0 && <EmptyVariablesList onAdd={onAdd} />}
|
||||||
|
|
||||||
|
{variables.length > 0 && (
|
||||||
|
<Stack direction="column" gap={4}>
|
||||||
|
<div className={styles.tableContainer}>
|
||||||
|
<table
|
||||||
|
className="filter-table filter-table--hover"
|
||||||
|
data-testid={selectors.pages.Dashboard.Settings.Variables.List.table}
|
||||||
|
role="grid"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Variable</th>
|
||||||
|
<th>Definition</th>
|
||||||
|
<th colSpan={5} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
|
<Droppable droppableId="variables-list" direction="vertical">
|
||||||
|
{(provided) => (
|
||||||
|
<tbody ref={provided.innerRef} {...provided.droppableProps}>
|
||||||
|
{variables.map((variableScene, index) => {
|
||||||
|
const variableState = variableScene.state;
|
||||||
|
return (
|
||||||
|
<VariableEditorListRow
|
||||||
|
index={index}
|
||||||
|
key={`${variableState.name}-${index}`}
|
||||||
|
variable={variableScene}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onDuplicate={onDuplicate}
|
||||||
|
onEdit={onEdit}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{provided.placeholder}
|
||||||
|
</tbody>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmptyVariablesList({ onAdd }: { onAdd: () => void }): ReactElement {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<EmptyListCTA
|
||||||
|
title="There are no variables yet"
|
||||||
|
buttonIcon="calculator-alt"
|
||||||
|
buttonTitle="Add variable"
|
||||||
|
buttonDisabled
|
||||||
|
infoBox={{
|
||||||
|
__html: ` <p>
|
||||||
|
Variables enable more interactive and dynamic dashboards. Instead of hard-coding things like server
|
||||||
|
or sensor names in your metric queries you can use variables in their place. Variables are shown as
|
||||||
|
list boxes at the top of the dashboard. These drop-down lists make it easy to change the data
|
||||||
|
being displayed in your dashboard. Check out the
|
||||||
|
<a class="external-link" href="https://grafana.com/docs/grafana/latest/variables/" target="_blank">
|
||||||
|
Templates and variables documentation
|
||||||
|
</a>
|
||||||
|
for more information.
|
||||||
|
</p>`,
|
||||||
|
}}
|
||||||
|
infoBoxTitle="What do variables do?"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
onAdd();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = () => ({
|
||||||
|
tableContainer: css({
|
||||||
|
overflow: 'scroll',
|
||||||
|
width: '100%',
|
||||||
|
}),
|
||||||
|
});
|
@ -0,0 +1,173 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React, { ReactElement, useState } from 'react';
|
||||||
|
import { Draggable } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
|
import { QueryVariable, SceneVariable } from '@grafana/scenes';
|
||||||
|
import { Button, ConfirmModal, Icon, IconButton, useStyles2, useTheme2 } from '@grafana/ui';
|
||||||
|
import { hasOptions } from 'app/features/variables/guard';
|
||||||
|
|
||||||
|
export interface VariableEditorListRowProps {
|
||||||
|
index: number;
|
||||||
|
variable: SceneVariable;
|
||||||
|
onEdit: (identifier: string) => void;
|
||||||
|
onDuplicate: (identifier: string) => void;
|
||||||
|
onDelete: (identifier: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VariableEditorListRow({
|
||||||
|
index,
|
||||||
|
variable,
|
||||||
|
onEdit: propsOnEdit,
|
||||||
|
onDuplicate: propsOnDuplicate,
|
||||||
|
onDelete: propsOnDelete,
|
||||||
|
}: VariableEditorListRowProps): ReactElement {
|
||||||
|
const theme = useTheme2();
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const definition = getDefinition(variable);
|
||||||
|
const variableState = variable.state;
|
||||||
|
const identifier = variableState.name;
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
const handleDeleteVariableModal = (show: boolean) => () => {
|
||||||
|
setShowDeleteModal(show);
|
||||||
|
};
|
||||||
|
const onDeleteVariable = () => {
|
||||||
|
reportInteraction('Delete variable');
|
||||||
|
propsOnDelete(identifier);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Draggable draggableId={JSON.stringify(identifier)} index={index}>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<tr
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
style={{
|
||||||
|
userSelect: snapshot.isDragging ? 'none' : 'auto',
|
||||||
|
background: snapshot.isDragging ? theme.colors.background.secondary : undefined,
|
||||||
|
...provided.draggableProps.style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td role="gridcell" className={styles.column}>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
fill="text"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
propsOnEdit(identifier);
|
||||||
|
}}
|
||||||
|
className={styles.nameLink}
|
||||||
|
data-testid={selectors.pages.Dashboard.Settings.Variables.List.tableRowNameFields(variableState.name)}
|
||||||
|
>
|
||||||
|
{variableState.name}
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
role="gridcell"
|
||||||
|
className={styles.definitionColumn}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
propsOnEdit(identifier);
|
||||||
|
}}
|
||||||
|
data-testid={selectors.pages.Dashboard.Settings.Variables.List.tableRowDefinitionFields(variableState.name)}
|
||||||
|
>
|
||||||
|
{definition}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td role="gridcell" className={styles.column}>
|
||||||
|
<div className={styles.icons}>
|
||||||
|
<IconButton
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
reportInteraction('Duplicate variable');
|
||||||
|
propsOnDuplicate(identifier);
|
||||||
|
}}
|
||||||
|
name="copy"
|
||||||
|
tooltip="Duplicate variable"
|
||||||
|
data-testid={selectors.pages.Dashboard.Settings.Variables.List.tableRowDuplicateButtons(
|
||||||
|
variableState.name
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setShowDeleteModal(true);
|
||||||
|
}}
|
||||||
|
name="trash-alt"
|
||||||
|
tooltip="Remove variable"
|
||||||
|
data-testid={selectors.pages.Dashboard.Settings.Variables.List.tableRowRemoveButtons(
|
||||||
|
variableState.name
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={showDeleteModal}
|
||||||
|
title="Delete variable"
|
||||||
|
body={`Are you sure you want to delete: ${variableState.name}?`}
|
||||||
|
confirmText="Delete variable"
|
||||||
|
onConfirm={onDeleteVariable}
|
||||||
|
onDismiss={handleDeleteVariableModal(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div {...provided.dragHandleProps} className={styles.dragHandle}>
|
||||||
|
<Icon name="draggabledots" size="lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefinition(model: SceneVariable): string {
|
||||||
|
let definition = '';
|
||||||
|
if (model instanceof QueryVariable) {
|
||||||
|
if (model.state.definition) {
|
||||||
|
definition = model.state.definition;
|
||||||
|
} else if (typeof model.state.query === 'string') {
|
||||||
|
definition = model.state.query;
|
||||||
|
}
|
||||||
|
} else if (hasOptions(model.state)) {
|
||||||
|
definition = model.state.query;
|
||||||
|
}
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStyles(theme: GrafanaTheme2) {
|
||||||
|
return {
|
||||||
|
dragHandle: css({
|
||||||
|
cursor: 'grab',
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
}),
|
||||||
|
column: css({
|
||||||
|
width: '1%',
|
||||||
|
}),
|
||||||
|
nameLink: css({
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: theme.colors.primary.text,
|
||||||
|
}),
|
||||||
|
definitionColumn: css({
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '200px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}),
|
||||||
|
iconPassed: css({
|
||||||
|
color: theme.v1.palette.greenBase,
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
}),
|
||||||
|
iconFailed: css({
|
||||||
|
color: theme.v1.palette.orange,
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
}),
|
||||||
|
icons: css({
|
||||||
|
display: 'flex',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
alignItems: 'center',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user