mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Variables: Adds drag and drop in variables list (#42503)
* Variables: adds drag and drop in variables list * Refactor: fixes after PR comments * Chore: updates after PR comments * Refactor: adds styles during dragging * Docs: update doc * Chore: pushing Drone
This commit is contained in:
parent
fdc7eef0f4
commit
769c82cf66
@ -11,7 +11,7 @@ The variables page lets you [add]({{< relref "variable-types/_index.md" >}}) var
|
||||
|
||||
## Move
|
||||
|
||||
You can move a variable up or down the list using the up and down arrows respectively.
|
||||
You can move a variable up or down the list using drag and drop.
|
||||
|
||||
## Clone
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { StoreState, ThunkResult } from 'app/types';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { changeVariableEditorExtended } from '../editor/reducer';
|
||||
import { addVariable, changeVariableProp } from '../state/sharedReducer';
|
||||
import { getNewVariabelIndex, getVariable } from '../state/selectors';
|
||||
import { getNewVariableIndex, getVariable } from '../state/selectors';
|
||||
import { AddVariable, toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
|
||||
import {
|
||||
AdHocVariabelFilterUpdate,
|
||||
@ -144,7 +144,7 @@ const createAdHocVariable = (options: AdHocTableOptions): ThunkResult<void> => {
|
||||
};
|
||||
|
||||
const global = false;
|
||||
const index = getNewVariabelIndex(getState());
|
||||
const index = getNewVariableIndex(getState());
|
||||
const identifier: VariableIdentifier = { type: 'adhoc', id: model.id };
|
||||
|
||||
dispatch(
|
||||
|
@ -27,7 +27,6 @@ describe('AdHocFilter', () => {
|
||||
|
||||
// Select value
|
||||
userEvent.click(screen.getByText('select value'));
|
||||
screen.debug(screen.getAllByTestId('AdHocFilterValue-value-wrapper'));
|
||||
// There are already some filters rendered
|
||||
const selectEl2 = screen.getAllByTestId('AdHocFilterValue-value-wrapper')[2];
|
||||
await selectEvent.select(selectEl2, 'val3', { container: document.body });
|
||||
|
@ -50,8 +50,7 @@ class VariableEditorContainerUnconnected extends PureComponent<Props> {
|
||||
this.props.switchToEditMode(identifier);
|
||||
};
|
||||
|
||||
onNewVariable = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
onNewVariable = () => {
|
||||
this.props.switchToNewMode();
|
||||
};
|
||||
|
||||
@ -104,20 +103,19 @@ class VariableEditorContainerUnconnected extends PureComponent<Props> {
|
||||
</div>
|
||||
|
||||
{!variableToEdit && (
|
||||
<>
|
||||
<VariableEditorList
|
||||
dashboard={this.props.dashboard}
|
||||
variables={this.props.variables}
|
||||
onAddClick={this.onNewVariable}
|
||||
onEditClick={this.onEditVariable}
|
||||
onChangeVariableOrder={this.onChangeVariableOrder}
|
||||
onDuplicateVariable={this.onDuplicateVariable}
|
||||
onRemoveVariable={this.onRemoveVariable}
|
||||
usages={this.props.usages}
|
||||
usagesNetwork={this.props.usagesNetwork}
|
||||
/>
|
||||
<VariablesUnknownTable variables={this.props.variables} dashboard={this.props.dashboard} />
|
||||
</>
|
||||
<VariableEditorList
|
||||
variables={this.props.variables}
|
||||
onAdd={this.onNewVariable}
|
||||
onEdit={this.onEditVariable}
|
||||
onChangeOrder={this.onChangeVariableOrder}
|
||||
onDuplicate={this.onDuplicateVariable}
|
||||
onDelete={this.onRemoveVariable}
|
||||
usages={this.props.usages}
|
||||
usagesNetwork={this.props.usagesNetwork}
|
||||
/>
|
||||
)}
|
||||
{!variableToEdit && this.props.variables.length > 0 && (
|
||||
<VariablesUnknownTable variables={this.props.variables} dashboard={this.props.dashboard} />
|
||||
)}
|
||||
{variableToEdit && <VariableEditorEditor identifier={toVariableIdentifier(variableToEdit)} />}
|
||||
</div>
|
||||
|
@ -1,67 +1,98 @@
|
||||
import React, { FC, MouseEvent, PureComponent } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { Icon, IconButton, useStyles } from '@grafana/ui';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
|
||||
import EmptyListCTA from '../../../core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { QueryVariableModel, VariableModel } from '../types';
|
||||
import { toVariableIdentifier, VariableIdentifier } from '../state/types';
|
||||
import { DashboardModel } from '../../dashboard/state';
|
||||
import { getVariableUsages, UsagesToNetwork, VariableUsageTree } from '../inspect/utils';
|
||||
import { isAdHoc } from '../guard';
|
||||
import { VariableUsagesButton } from '../inspect/VariableUsagesButton';
|
||||
import { VariableModel } from '../types';
|
||||
import { VariableIdentifier } from '../state/types';
|
||||
import { UsagesToNetwork, VariableUsageTree } from '../inspect/utils';
|
||||
import { VariableEditorListRow } from './VariableEditorListRow';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
|
||||
export interface Props {
|
||||
variables: VariableModel[];
|
||||
dashboard: DashboardModel | null;
|
||||
usages: VariableUsageTree[];
|
||||
usagesNetwork: UsagesToNetwork[];
|
||||
onAddClick: (event: MouseEvent) => void;
|
||||
onEditClick: (identifier: VariableIdentifier) => void;
|
||||
onChangeVariableOrder: (identifier: VariableIdentifier, fromIndex: number, toIndex: number) => void;
|
||||
onDuplicateVariable: (identifier: VariableIdentifier) => void;
|
||||
onRemoveVariable: (identifier: VariableIdentifier) => void;
|
||||
onAdd: () => void;
|
||||
onEdit: (identifier: VariableIdentifier) => void;
|
||||
onChangeOrder: (identifier: VariableIdentifier, fromIndex: number, toIndex: number) => void;
|
||||
onDuplicate: (identifier: VariableIdentifier) => void;
|
||||
onDelete: (identifier: VariableIdentifier) => void;
|
||||
}
|
||||
|
||||
enum MoveType {
|
||||
down = 1,
|
||||
up = -1,
|
||||
}
|
||||
|
||||
export class VariableEditorList extends PureComponent<Props> {
|
||||
onEditClick = (event: MouseEvent, identifier: VariableIdentifier) => {
|
||||
event.preventDefault();
|
||||
this.props.onEditClick(identifier);
|
||||
export function VariableEditorList({
|
||||
variables,
|
||||
usages,
|
||||
usagesNetwork,
|
||||
onChangeOrder,
|
||||
onAdd,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onDuplicate,
|
||||
}: Props): ReactElement {
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result.destination || !result.source) {
|
||||
return;
|
||||
}
|
||||
const identifier = JSON.parse(result.draggableId);
|
||||
onChangeOrder(identifier, result.source.index, result.destination.index);
|
||||
};
|
||||
|
||||
onChangeVariableOrder = (event: MouseEvent, variable: VariableModel, moveType: MoveType) => {
|
||||
event.preventDefault();
|
||||
this.props.onChangeVariableOrder(toVariableIdentifier(variable), variable.index, variable.index + moveType);
|
||||
};
|
||||
|
||||
onDuplicateVariable = (event: MouseEvent, identifier: VariableIdentifier) => {
|
||||
event.preventDefault();
|
||||
this.props.onDuplicateVariable(identifier);
|
||||
};
|
||||
|
||||
onRemoveVariable = (event: MouseEvent, identifier: VariableIdentifier) => {
|
||||
event.preventDefault();
|
||||
this.props.onRemoveVariable(identifier);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
{this.props.variables.length === 0 && (
|
||||
<div>
|
||||
<EmptyListCTA
|
||||
title="There are no variables yet"
|
||||
buttonIcon="calculator-alt"
|
||||
buttonTitle="Add variable"
|
||||
infoBox={{
|
||||
__html: ` <p>
|
||||
{variables.length === 0 && <EmptyVariablesList onAdd={onAdd} />}
|
||||
|
||||
{variables.length > 0 && (
|
||||
<div>
|
||||
<table
|
||||
className="filter-table filter-table--hover"
|
||||
aria-label={selectors.pages.Dashboard.Settings.Variables.List.table}
|
||||
>
|
||||
<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((variable, index) => (
|
||||
<VariableEditorListRow
|
||||
index={index}
|
||||
key={`${variable.name}-${index}`}
|
||||
variable={variable}
|
||||
usageTree={usages}
|
||||
usagesNetwork={usagesNetwork}
|
||||
onDelete={onDelete}
|
||||
onDuplicate={onDuplicate}
|
||||
onEdit={onEdit}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</tbody>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyVariablesList({ onAdd }: { onAdd: () => void }): ReactElement {
|
||||
return (
|
||||
<div>
|
||||
<EmptyListCTA
|
||||
title="There are no variables yet"
|
||||
buttonIcon="calculator-alt"
|
||||
buttonTitle="Add variable"
|
||||
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
|
||||
@ -71,162 +102,13 @@ export class VariableEditorList extends PureComponent<Props> {
|
||||
</a>
|
||||
for more information.
|
||||
</p>`,
|
||||
}}
|
||||
infoBoxTitle="What do variables do?"
|
||||
onClick={this.props.onAddClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.props.variables.length > 0 && (
|
||||
<div>
|
||||
<table
|
||||
className="filter-table filter-table--hover"
|
||||
aria-label={selectors.pages.Dashboard.Settings.Variables.List.table}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Definition</th>
|
||||
<th colSpan={6} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.props.variables.map((state, index) => {
|
||||
const variable = state as QueryVariableModel;
|
||||
const definition = variable.definition
|
||||
? variable.definition
|
||||
: typeof variable.query === 'string'
|
||||
? variable.query
|
||||
: '';
|
||||
const usages = getVariableUsages(variable.id, this.props.usages);
|
||||
const passed = usages > 0 || isAdHoc(variable);
|
||||
return (
|
||||
<tr key={`${variable.name}-${index}`}>
|
||||
<td style={{ width: '1%' }}>
|
||||
<span
|
||||
onClick={(event) => this.onEditClick(event, toVariableIdentifier(variable))}
|
||||
className="pointer template-variable"
|
||||
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowNameFields(
|
||||
variable.name
|
||||
)}
|
||||
>
|
||||
{variable.name}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
style={{ maxWidth: '200px' }}
|
||||
onClick={(event) => this.onEditClick(event, toVariableIdentifier(variable))}
|
||||
className="pointer max-width"
|
||||
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowDefinitionFields(
|
||||
variable.name
|
||||
)}
|
||||
>
|
||||
{definition}
|
||||
</td>
|
||||
|
||||
<td style={{ width: '1%' }}>
|
||||
<VariableCheckIndicator passed={passed} />
|
||||
</td>
|
||||
|
||||
<td style={{ width: '1%' }}>
|
||||
<VariableUsagesButton
|
||||
id={variable.id}
|
||||
isAdhoc={isAdHoc(variable)}
|
||||
usages={this.props.usagesNetwork}
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td style={{ width: '1%' }}>
|
||||
{index > 0 && (
|
||||
<IconButton
|
||||
onClick={(event) => this.onChangeVariableOrder(event, variable, MoveType.up)}
|
||||
name="arrow-up"
|
||||
title="Move variable up"
|
||||
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowArrowUpButtons(
|
||||
variable.name
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td style={{ width: '1%' }}>
|
||||
{index < this.props.variables.length - 1 && (
|
||||
<IconButton
|
||||
onClick={(event) => this.onChangeVariableOrder(event, variable, MoveType.down)}
|
||||
name="arrow-down"
|
||||
title="Move variable down"
|
||||
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowArrowDownButtons(
|
||||
variable.name
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td style={{ width: '1%' }}>
|
||||
<IconButton
|
||||
onClick={(event) => this.onDuplicateVariable(event, toVariableIdentifier(variable))}
|
||||
name="copy"
|
||||
title="Duplicate variable"
|
||||
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowDuplicateButtons(
|
||||
variable.name
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td style={{ width: '1%' }}>
|
||||
<IconButton
|
||||
onClick={(event) => this.onRemoveVariable(event, toVariableIdentifier(variable))}
|
||||
name="trash-alt"
|
||||
title="Remove variable"
|
||||
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowRemoveButtons(
|
||||
variable.name
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface VariableCheckIndicatorProps {
|
||||
passed: boolean;
|
||||
}
|
||||
|
||||
const VariableCheckIndicator: FC<VariableCheckIndicatorProps> = ({ passed }) => {
|
||||
const style = useStyles(getStyles);
|
||||
if (passed) {
|
||||
return (
|
||||
<Icon
|
||||
name="check"
|
||||
className={style.iconPassed}
|
||||
title="This variable is referenced by other variables or dashboard."
|
||||
}}
|
||||
infoBoxTitle="What do variables do?"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
onAdd();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Icon
|
||||
name="exclamation-triangle"
|
||||
className={style.iconFailed}
|
||||
title="This variable is not referenced by any variable or dashboard."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
iconPassed: css`
|
||||
color: ${theme.palette.greenBase};
|
||||
`,
|
||||
iconFailed: css`
|
||||
color: ${theme.palette.orange};
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
185
public/app/features/variables/editor/VariableEditorListRow.tsx
Normal file
185
public/app/features/variables/editor/VariableEditorListRow.tsx
Normal file
@ -0,0 +1,185 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, IconButton, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { getVariableUsages, UsagesToNetwork, VariableUsageTree } from '../inspect/utils';
|
||||
import { hasOptions, isAdHoc, isQuery } from '../guard';
|
||||
import { toVariableIdentifier, VariableIdentifier } from '../state/types';
|
||||
import { VariableUsagesButton } from '../inspect/VariableUsagesButton';
|
||||
import { VariableModel } from '../types';
|
||||
|
||||
export interface VariableEditorListRowProps {
|
||||
index: number;
|
||||
variable: VariableModel;
|
||||
usageTree: VariableUsageTree[];
|
||||
usagesNetwork: UsagesToNetwork[];
|
||||
onEdit: (identifier: VariableIdentifier) => void;
|
||||
onDuplicate: (identifier: VariableIdentifier) => void;
|
||||
onDelete: (identifier: VariableIdentifier) => void;
|
||||
}
|
||||
|
||||
export function VariableEditorListRow({
|
||||
index,
|
||||
variable,
|
||||
usageTree,
|
||||
usagesNetwork,
|
||||
onEdit: propsOnEdit,
|
||||
onDuplicate: propsOnDuplicate,
|
||||
onDelete: propsOnDelete,
|
||||
}: VariableEditorListRowProps): ReactElement {
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles2(getStyles);
|
||||
const definition = getDefinition(variable);
|
||||
const usages = getVariableUsages(variable.id, usageTree);
|
||||
const passed = usages > 0 || isAdHoc(variable);
|
||||
const identifier = toVariableIdentifier(variable);
|
||||
|
||||
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 className={styles.column}>
|
||||
<span
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
propsOnEdit(identifier);
|
||||
}}
|
||||
className={styles.nameLink}
|
||||
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowNameFields(variable.name)}
|
||||
>
|
||||
{variable.name}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className={styles.definitionColumn}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
propsOnEdit(identifier);
|
||||
}}
|
||||
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowDefinitionFields(variable.name)}
|
||||
>
|
||||
{definition}
|
||||
</td>
|
||||
|
||||
<td className={styles.column}>
|
||||
<VariableCheckIndicator passed={passed} />
|
||||
</td>
|
||||
|
||||
<td className={styles.column}>
|
||||
<VariableUsagesButton id={variable.id} isAdhoc={isAdHoc(variable)} usages={usagesNetwork} />
|
||||
</td>
|
||||
|
||||
<td className={styles.column}>
|
||||
<IconButton
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
propsOnDuplicate(identifier);
|
||||
}}
|
||||
name="copy"
|
||||
title="Duplicate variable"
|
||||
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowDuplicateButtons(variable.name)}
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td className={styles.column}>
|
||||
<IconButton
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
propsOnDelete(identifier);
|
||||
}}
|
||||
name="trash-alt"
|
||||
title="Remove variable"
|
||||
aria-label={selectors.pages.Dashboard.Settings.Variables.List.tableRowRemoveButtons(variable.name)}
|
||||
/>
|
||||
</td>
|
||||
<td className={styles.column}>
|
||||
<div {...provided.dragHandleProps} className={styles.dragHandle}>
|
||||
<Icon name="draggabledots" size="lg" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
||||
function getDefinition(model: VariableModel): string {
|
||||
let definition = '';
|
||||
if (isQuery(model)) {
|
||||
if (model.definition) {
|
||||
definition = model.definition;
|
||||
} else if (typeof model.query === 'string') {
|
||||
definition = model.query;
|
||||
}
|
||||
} else if (hasOptions(model)) {
|
||||
definition = model.query;
|
||||
}
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
interface VariableCheckIndicatorProps {
|
||||
passed: boolean;
|
||||
}
|
||||
|
||||
function VariableCheckIndicator({ passed }: VariableCheckIndicatorProps): ReactElement {
|
||||
const styles = useStyles2(getStyles);
|
||||
if (passed) {
|
||||
return (
|
||||
<Icon
|
||||
name="check"
|
||||
className={styles.iconPassed}
|
||||
title="This variable is referenced by other variables or dashboard."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Icon
|
||||
name="exclamation-triangle"
|
||||
className={styles.iconFailed}
|
||||
title="This variable is not referenced by any variable or dashboard."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
dragHandle: css`
|
||||
cursor: grab;
|
||||
`,
|
||||
column: css`
|
||||
width: 1%;
|
||||
`,
|
||||
nameLink: css`
|
||||
cursor: pointer;
|
||||
color: ${theme.colors.primary.text};
|
||||
`,
|
||||
definitionColumn: css`
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-o-text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`,
|
||||
iconPassed: css`
|
||||
color: ${theme.v1.palette.greenBase};
|
||||
`,
|
||||
iconFailed: css`
|
||||
color: ${theme.v1.palette.orange};
|
||||
`,
|
||||
};
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { ThunkResult } from '../../../types';
|
||||
import { getEditorVariables, getNewVariabelIndex, getVariable, getVariables } from '../state/selectors';
|
||||
import { getEditorVariables, getNewVariableIndex, getVariable, getVariables } from '../state/selectors';
|
||||
import {
|
||||
changeVariableNameFailed,
|
||||
changeVariableNameSucceeded,
|
||||
@ -88,7 +88,7 @@ export const switchToNewMode = (type: VariableType = 'query'): ThunkResult<void>
|
||||
const id = getNextAvailableId(type, getVariables(getState()));
|
||||
const identifier = { type, id };
|
||||
const global = false;
|
||||
const index = getNewVariabelIndex(getState());
|
||||
const index = getNewVariableIndex(getState());
|
||||
const model = cloneDeep(variableAdapters.get(type).initialState);
|
||||
model.id = id;
|
||||
model.name = id;
|
||||
|
@ -2,7 +2,14 @@ import { combineReducers } from '@reduxjs/toolkit';
|
||||
import { LoadingState } from '@grafana/data';
|
||||
|
||||
import { NEW_VARIABLE_ID, VariablesState } from './types';
|
||||
import { VariableHide, VariableModel } from '../types';
|
||||
import {
|
||||
DashboardVariableModel,
|
||||
initialVariableModelState,
|
||||
OrgVariableModel,
|
||||
UserVariableModel,
|
||||
VariableHide,
|
||||
VariableModel,
|
||||
} from '../types';
|
||||
|
||||
import { VariableAdapter } from '../adapters';
|
||||
import { dashboardReducer } from 'app/features/dashboard/state/reducers';
|
||||
@ -12,10 +19,69 @@ import { DashboardState } from '../../../types';
|
||||
export const getVariableState = (
|
||||
noOfVariables: number,
|
||||
inEditorIndex = -1,
|
||||
includeEmpty = false
|
||||
includeEmpty = false,
|
||||
includeSystem = false
|
||||
): Record<string, VariableModel> => {
|
||||
const variables: Record<string, VariableModel> = {};
|
||||
|
||||
if (includeSystem) {
|
||||
const dashboardModel: DashboardVariableModel = {
|
||||
...initialVariableModelState,
|
||||
id: '__dashboard',
|
||||
name: '__dashboard',
|
||||
type: 'system',
|
||||
index: -3,
|
||||
skipUrlSync: true,
|
||||
hide: VariableHide.hideVariable,
|
||||
current: {
|
||||
value: {
|
||||
name: 'A dashboard title',
|
||||
uid: 'An dashboard UID',
|
||||
toString: () => 'A dashboard title',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const orgModel: OrgVariableModel = {
|
||||
...initialVariableModelState,
|
||||
id: '__org',
|
||||
name: '__org',
|
||||
type: 'system',
|
||||
index: -2,
|
||||
skipUrlSync: true,
|
||||
hide: VariableHide.hideVariable,
|
||||
current: {
|
||||
value: {
|
||||
name: 'An org name',
|
||||
id: 1,
|
||||
toString: () => '1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const userModel: UserVariableModel = {
|
||||
...initialVariableModelState,
|
||||
id: '__user',
|
||||
name: '__user',
|
||||
type: 'system',
|
||||
index: -1,
|
||||
skipUrlSync: true,
|
||||
hide: VariableHide.hideVariable,
|
||||
current: {
|
||||
value: {
|
||||
login: 'admin',
|
||||
id: 1,
|
||||
email: 'admin@test',
|
||||
toString: () => '1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
variables[dashboardModel.id] = dashboardModel;
|
||||
variables[orgModel.id] = orgModel;
|
||||
variables[userModel.id] = userModel;
|
||||
}
|
||||
|
||||
for (let index = 0; index < noOfVariables; index++) {
|
||||
variables[index] = {
|
||||
id: index.toString(),
|
||||
|
@ -29,13 +29,13 @@ export const getVariableWithName = (name: string, state: StoreState = getState()
|
||||
};
|
||||
|
||||
export const getVariables = (state: StoreState = getState()): VariableModel[] => {
|
||||
const filter = (variable: VariableModel) => {
|
||||
return variable.type !== 'system';
|
||||
};
|
||||
|
||||
return getFilteredVariables(filter, state);
|
||||
return getFilteredVariables(defaultVariablesFilter, state);
|
||||
};
|
||||
|
||||
export function defaultVariablesFilter(variable: VariableModel): boolean {
|
||||
return variable.type !== 'system';
|
||||
}
|
||||
|
||||
export const getSubMenuVariables = memoizeOne((variables: Record<string, VariableModel>): VariableModel[] => {
|
||||
return getVariables(getState());
|
||||
});
|
||||
@ -46,9 +46,14 @@ export const getEditorVariables = (state: StoreState): VariableModel[] => {
|
||||
|
||||
export type GetVariables = typeof getVariables;
|
||||
|
||||
export const getNewVariabelIndex = (state: StoreState = getState()): number => {
|
||||
return Object.values(state.templating.variables).length;
|
||||
};
|
||||
export function getNewVariableIndex(state: StoreState = getState()): number {
|
||||
return getNextVariableIndex(Object.values(state.templating.variables));
|
||||
}
|
||||
|
||||
export function getNextVariableIndex(variables: VariableModel[]): number {
|
||||
const sorted = variables.filter(defaultVariablesFilter).sort((v1, v2) => v1.index - v2.index);
|
||||
return sorted.length > 0 ? sorted[sorted.length - 1].index + 1 : 0;
|
||||
}
|
||||
|
||||
export function getVariablesIsDirty(state: StoreState = getState()): boolean {
|
||||
return state.templating.transaction.isDirty;
|
||||
|
@ -176,12 +176,13 @@ describe('sharedReducer', () => {
|
||||
|
||||
describe('when duplicateVariable is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
const initialState: VariablesState = getVariableState(3);
|
||||
const initialState: VariablesState = getVariableState(3, -1, false, true);
|
||||
const payload = toVariablePayload({ id: '1', type: 'query' }, { newId: '11' });
|
||||
reducerTester<VariablesState>()
|
||||
.givenReducer(sharedReducer, initialState)
|
||||
.whenActionIsDispatched(duplicateVariable(payload))
|
||||
.thenStateShouldEqual({
|
||||
...initialState,
|
||||
'0': {
|
||||
id: '0',
|
||||
type: 'query',
|
||||
@ -235,7 +236,7 @@ describe('sharedReducer', () => {
|
||||
describe('when changeVariableOrder is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
const initialState: VariablesState = getVariableState(3);
|
||||
const payload = toVariablePayload({ id: '1', type: 'query' }, { fromIndex: 1, toIndex: 0 });
|
||||
const payload = toVariablePayload({ id: '2', type: 'query' }, { fromIndex: 2, toIndex: 0 });
|
||||
reducerTester<VariablesState>()
|
||||
.givenReducer(sharedReducer, initialState)
|
||||
.whenActionIsDispatched(changeVariableOrder(payload))
|
||||
@ -253,6 +254,55 @@ describe('sharedReducer', () => {
|
||||
error: null,
|
||||
description: null,
|
||||
},
|
||||
'1': {
|
||||
id: '1',
|
||||
type: 'query',
|
||||
name: 'Name-1',
|
||||
hide: VariableHide.dontHide,
|
||||
index: 2,
|
||||
label: 'Label-1',
|
||||
skipUrlSync: false,
|
||||
global: false,
|
||||
state: LoadingState.NotStarted,
|
||||
error: null,
|
||||
description: null,
|
||||
},
|
||||
'2': {
|
||||
id: '2',
|
||||
type: 'query',
|
||||
name: 'Name-2',
|
||||
hide: VariableHide.dontHide,
|
||||
index: 0,
|
||||
label: 'Label-2',
|
||||
skipUrlSync: false,
|
||||
global: false,
|
||||
state: LoadingState.NotStarted,
|
||||
error: null,
|
||||
description: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('then state should be correct', () => {
|
||||
const initialState: VariablesState = getVariableState(3);
|
||||
const payload = toVariablePayload({ id: '0', type: 'query' }, { fromIndex: 0, toIndex: 2 });
|
||||
reducerTester<VariablesState>()
|
||||
.givenReducer(sharedReducer, initialState)
|
||||
.whenActionIsDispatched(changeVariableOrder(payload))
|
||||
.thenStateShouldEqual({
|
||||
'0': {
|
||||
id: '0',
|
||||
type: 'query',
|
||||
name: 'Name-0',
|
||||
hide: VariableHide.dontHide,
|
||||
index: 2,
|
||||
label: 'Label-0',
|
||||
skipUrlSync: false,
|
||||
global: false,
|
||||
state: LoadingState.NotStarted,
|
||||
error: null,
|
||||
description: null,
|
||||
},
|
||||
'1': {
|
||||
id: '1',
|
||||
type: 'query',
|
||||
@ -271,7 +321,7 @@ describe('sharedReducer', () => {
|
||||
type: 'query',
|
||||
name: 'Name-2',
|
||||
hide: VariableHide.dontHide,
|
||||
index: 2,
|
||||
index: 1,
|
||||
label: 'Label-2',
|
||||
skipUrlSync: false,
|
||||
global: false,
|
||||
|
@ -6,6 +6,7 @@ import { AddVariable, getInstanceState, initialVariablesState, VariablePayload,
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { changeVariableNameSucceeded } from '../editor/reducer';
|
||||
import { ensureStringValues } from '../utils';
|
||||
import { getNextVariableIndex } from './selectors';
|
||||
|
||||
const sharedReducerSlice = createSlice({
|
||||
name: 'templating/shared',
|
||||
@ -71,7 +72,7 @@ const sharedReducerSlice = createSlice({
|
||||
const original = cloneDeep<VariableModel>(state[action.payload.id]);
|
||||
const name = `copy_of_${original.name}`;
|
||||
const newId = action.payload.data?.newId ?? name;
|
||||
const index = Object.keys(state).length;
|
||||
const index = getNextVariableIndex(Object.values(state));
|
||||
state[newId] = {
|
||||
...cloneDeep(variableAdapters.get(action.payload.type).initialState),
|
||||
...original,
|
||||
@ -84,16 +85,17 @@ const sharedReducerSlice = createSlice({
|
||||
state: VariablesState,
|
||||
action: PayloadAction<VariablePayload<{ fromIndex: number; toIndex: number }>>
|
||||
) => {
|
||||
const variables = Object.values(state).map((s) => s);
|
||||
const fromVariable = variables.find((v) => v.index === action.payload.data.fromIndex);
|
||||
const toVariable = variables.find((v) => v.index === action.payload.data.toIndex);
|
||||
|
||||
if (fromVariable) {
|
||||
state[fromVariable.id].index = action.payload.data.toIndex;
|
||||
}
|
||||
|
||||
if (toVariable) {
|
||||
state[toVariable.id].index = action.payload.data.fromIndex;
|
||||
const { toIndex, fromIndex } = action.payload.data;
|
||||
const variableStates = Object.values(state);
|
||||
for (let index = 0; index < variableStates.length; index++) {
|
||||
const variable = variableStates[index];
|
||||
if (variable.index === fromIndex) {
|
||||
variable.index = toIndex;
|
||||
} else if (variable.index > fromIndex && variable.index <= toIndex) {
|
||||
variable.index--;
|
||||
} else if (variable.index < fromIndex && variable.index >= toIndex) {
|
||||
variable.index++;
|
||||
}
|
||||
}
|
||||
},
|
||||
changeVariableType: (state: VariablesState, action: PayloadAction<VariablePayload<{ newType: VariableType }>>) => {
|
||||
|
Loading…
Reference in New Issue
Block a user