mirror of
https://github.com/grafana/grafana.git
synced 2025-02-20 11:48:34 -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> = {};
|
||||
|
||||
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 (!isEditing) {
|
||||
|
@ -122,6 +122,25 @@ describe('transformSaveModelToScene', () => {
|
||||
expect(scene.state.$behaviors![1]).toBeInstanceOf(behaviors.CursorSync);
|
||||
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', () => {
|
||||
|
@ -195,6 +195,11 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
|
||||
variables = new SceneVariableSet({
|
||||
variables: variableObjects,
|
||||
});
|
||||
} else {
|
||||
// Create empty variable set
|
||||
variables = new SceneVariableSet({
|
||||
variables: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (oldModel.annotations?.list?.length) {
|
||||
|
@ -9,6 +9,7 @@ import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync';
|
||||
import { DashboardEditView, DashboardEditListViewState, useDashboardEditPageNav } from './utils';
|
||||
@ -26,8 +27,8 @@ export class DashboardLinksEditView extends SceneObjectBase<DashboardLinksEditVi
|
||||
}
|
||||
|
||||
function DashboardLinksEditViewRenderer({ model }: SceneComponentProps<DashboardLinksEditView>) {
|
||||
const { dashboardRef, editIndex } = model.useState();
|
||||
const dashboard = dashboardRef.resolve();
|
||||
const { editIndex } = model.useState();
|
||||
const dashboard = getDashboardSceneFor(model);
|
||||
const links = dashboard.state.links || [];
|
||||
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
||||
|
||||
|
@ -112,6 +112,7 @@ describe('GeneralSettingsEditView', () => {
|
||||
});
|
||||
|
||||
async function buildTestScene() {
|
||||
const settings = new GeneralSettingsEditView({});
|
||||
const dashboard = new DashboardScene({
|
||||
$timeRange: new SceneTimeRange({}),
|
||||
$behaviors: [new behaviors.CursorSync({ sync: DashboardCursorSync.Off })],
|
||||
@ -143,10 +144,7 @@ async function buildTestScene() {
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const settings = new GeneralSettingsEditView({
|
||||
dashboardRef: dashboard.getRef(),
|
||||
editview: settings,
|
||||
});
|
||||
|
||||
activateFullSceneTree(dashboard);
|
||||
|
@ -1,14 +1,7 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
|
||||
import { PageLayoutType } from '@grafana/data';
|
||||
import {
|
||||
behaviors,
|
||||
SceneComponentProps,
|
||||
SceneObjectBase,
|
||||
SceneObjectRef,
|
||||
SceneTimePicker,
|
||||
sceneGraph,
|
||||
} from '@grafana/scenes';
|
||||
import { behaviors, SceneComponentProps, SceneObjectBase, SceneTimePicker, sceneGraph } from '@grafana/scenes';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
import {
|
||||
Box,
|
||||
@ -31,12 +24,11 @@ import { DashboardControls } from '../scene/DashboardControls';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
|
||||
|
||||
export interface GeneralSettingsEditViewState extends DashboardEditViewState {
|
||||
dashboardRef: SceneObjectRef<DashboardScene>;
|
||||
}
|
||||
export interface GeneralSettingsEditViewState extends DashboardEditViewState {}
|
||||
|
||||
const EDITABLE_OPTIONS = [
|
||||
{ label: 'Editable', value: true },
|
||||
@ -54,7 +46,7 @@ export class GeneralSettingsEditView
|
||||
implements DashboardEditView
|
||||
{
|
||||
private get _dashboard(): DashboardScene {
|
||||
return this.state.dashboardRef.resolve();
|
||||
return getDashboardSceneFor(this);
|
||||
}
|
||||
|
||||
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 { 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 { DashboardScene } from '../scene/DashboardScene';
|
||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
|
||||
|
||||
import { VariableEditorList } from './variables/VariableEditorList';
|
||||
export interface VariablesEditViewState extends DashboardEditViewState {}
|
||||
|
||||
export class VariablesEditView extends SceneObjectBase<VariablesEditViewState> implements DashboardEditView {
|
||||
public static Component = VariableEditorSettingsListView;
|
||||
|
||||
public getUrlKey(): string {
|
||||
return 'variables';
|
||||
}
|
||||
|
||||
static Component = ({ model }: SceneComponentProps<VariablesEditView>) => {
|
||||
const dashboard = getDashboardSceneFor(model);
|
||||
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
||||
public getDashboard(): DashboardScene {
|
||||
return getDashboardSceneFor(this);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
|
||||
<NavToolbarActions dashboard={dashboard} />
|
||||
<div>variables todo</div>
|
||||
</Page>
|
||||
);
|
||||
public getVariableSet(): SceneVariables {
|
||||
return sceneGraph.getVariables(this.getDashboard());
|
||||
}
|
||||
|
||||
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 { 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 { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { useSelector } from 'app/types';
|
||||
@ -13,9 +13,7 @@ import { DashboardLinksEditView } from './DashboardLinksEditView';
|
||||
import { GeneralSettingsEditView } from './GeneralSettingsEditView';
|
||||
import { VariablesEditView } from './VariablesEditView';
|
||||
|
||||
export interface DashboardEditViewState extends SceneObjectState {
|
||||
dashboardRef: SceneObjectRef<DashboardScene>;
|
||||
}
|
||||
export interface DashboardEditViewState extends SceneObjectState {}
|
||||
|
||||
export interface DashboardEditListViewState extends DashboardEditViewState {
|
||||
/** Index of the list item to edit */
|
||||
@ -63,19 +61,16 @@ export function useDashboardEditPageNav(dashboard: DashboardScene, currentEditVi
|
||||
return { navModel, pageNav };
|
||||
}
|
||||
|
||||
export function createDashboardEditViewFor(
|
||||
editview: string,
|
||||
dashboardRef: SceneObjectRef<DashboardScene>
|
||||
): DashboardEditView {
|
||||
export function createDashboardEditViewFor(editview: string): DashboardEditView {
|
||||
switch (editview) {
|
||||
case 'annotations':
|
||||
return new AnnotationsEditView({ dashboardRef });
|
||||
return new AnnotationsEditView({});
|
||||
case 'variables':
|
||||
return new VariablesEditView({ dashboardRef });
|
||||
return new VariablesEditView({});
|
||||
case 'links':
|
||||
return new DashboardLinksEditView({ dashboardRef });
|
||||
return new DashboardLinksEditView({});
|
||||
case 'settings':
|
||||
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