Dashboard Controls: Add UI for displaying under menu (#113517)
* feat: add options to render under the controls menu * fix: filter out hidden annotation layers from the dashboard-controls menu * fix: adjust spacing between annotation controls in the dashboard-controls menu * fix: e2e test for variables * feat: move the menu button next to the variables * fix: remove duplicate link controls * fix: show dashboard controls when the dashboard is not saved
This commit is contained in:
@@ -54,7 +54,13 @@ test.describe(
|
||||
await expect(descriptionInput).toHaveAttribute('placeholder', 'Descriptive text');
|
||||
await expect(descriptionInput).toHaveValue('');
|
||||
|
||||
await expect(page.locator('label').filter({ hasText: 'Hide' })).toBeVisible();
|
||||
// Display
|
||||
await expect(page.locator('label', { hasText: /^Display$/ })).toBeVisible();
|
||||
const displaySelect = dashboardPage.getByGrafanaSelector(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.General.generalDisplaySelect
|
||||
);
|
||||
await expect(displaySelect).toBeVisible();
|
||||
await expect(displaySelect).toHaveValue('Above dashboard');
|
||||
|
||||
// Check datasource selector
|
||||
const datasourceSelect = dashboardPage.getByGrafanaSelector(
|
||||
|
||||
@@ -1978,16 +1978,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/settings/annotations/AnnotationSettingsEdit.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 7
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/settings/links/DashboardLinkForm.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 10
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx": {
|
||||
"react-hooks/rules-of-hooks": {
|
||||
"count": 4
|
||||
@@ -2008,11 +1998,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/settings/variables/components/VariableHideSelect.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/dashboard-scene/settings/variables/components/VariableSelectField.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
|
||||
@@ -352,6 +352,9 @@ export const versionedPages = {
|
||||
showInLabel: {
|
||||
'11.1.0': 'data-testid show-in-label',
|
||||
},
|
||||
annotationControlsDisplay: {
|
||||
'12.4.0': 'data-testid annotation-controls-display-label',
|
||||
},
|
||||
previewInDashboard: {
|
||||
'10.0.0': 'data-testid annotations-preview',
|
||||
},
|
||||
@@ -445,6 +448,9 @@ export const versionedPages = {
|
||||
generalHideSelectV2: {
|
||||
[MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Hide select',
|
||||
},
|
||||
generalDisplaySelect: {
|
||||
'12.4.0': 'data-testid Variable editor Display select',
|
||||
},
|
||||
selectionOptionsAllowCustomValueSwitch: {
|
||||
[MIN_GRAFANA_VERSION]: 'data-testid Variable editor Form Allow Custom Value switch',
|
||||
},
|
||||
|
||||
@@ -23,11 +23,12 @@ import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
import { PanelEditControls } from '../panel-edit/PanelEditControls';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
import { DashboardControlsButton } from './DashboardControlsMenu';
|
||||
import { DashboardDataLayerControls } from './DashboardDataLayerControls';
|
||||
import { DashboardLinksControls } from './DashboardLinksControls';
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { VariableControls } from './VariableControls';
|
||||
import { DashboardControlsButton } from './dashboard-controls-menu/DashboardControlsMenuButton';
|
||||
import { hasDashboardControls, useHasDashboardControls } from './dashboard-controls-menu/utils';
|
||||
import { EditDashboardSwitch } from './new-toolbar/actions/EditDashboardSwitch';
|
||||
import { SaveDashboard } from './new-toolbar/actions/SaveDashboard';
|
||||
import { ShareDashboardButton } from './new-toolbar/actions/ShareDashboardButton';
|
||||
@@ -117,19 +118,8 @@ export class DashboardControls extends SceneObjectBase<DashboardControlsState> {
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard controls is a separate dropdown menu at the top-right of the controls
|
||||
public hasDashboardControls(): boolean {
|
||||
const dashboard = getDashboardSceneFor(this);
|
||||
const { links } = dashboard.state;
|
||||
const hasControlMenuVariables = sceneGraph
|
||||
.getVariables(dashboard)
|
||||
?.state.variables.some((v) => v.state.hide === VariableHide.inControlsMenu);
|
||||
const hasControlMenuLinks = links.some((link) => link.placement === 'inControlsMenu');
|
||||
|
||||
return hasControlMenuVariables || hasControlMenuLinks;
|
||||
}
|
||||
|
||||
public hasControls(): boolean {
|
||||
const dashboard = getDashboardSceneFor(this);
|
||||
const hasVariables = sceneGraph
|
||||
.getVariables(this)
|
||||
?.state.variables.some((v) => v.state.hide !== VariableHide.hideVariable);
|
||||
@@ -138,7 +128,7 @@ export class DashboardControls extends SceneObjectBase<DashboardControlsState> {
|
||||
const hideLinks = this.state.hideLinksControls || !hasLinks;
|
||||
const hideVariables = this.state.hideVariableControls || (!hasAnnotations && !hasVariables);
|
||||
const hideTimePicker = this.state.hideTimeControls;
|
||||
const hideDashboardControls = this.state.hideDashboardControls || !this.hasDashboardControls();
|
||||
const hideDashboardControls = this.state.hideDashboardControls || !hasDashboardControls(dashboard);
|
||||
|
||||
return !(hideVariables && hideLinks && hideTimePicker && hideDashboardControls);
|
||||
}
|
||||
@@ -157,10 +147,10 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
|
||||
const { links, editPanel } = dashboard.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const showDebugger = window.location.search.includes('scene-debugger');
|
||||
const hasDashboardControls = useHasDashboardControls(dashboard);
|
||||
|
||||
if (!model.hasControls()) {
|
||||
// To still have spacing when no controls are rendered
|
||||
|
||||
return <Box padding={1}>{renderHiddenVariables(dashboard)}</Box>;
|
||||
}
|
||||
|
||||
@@ -176,11 +166,6 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
|
||||
<refreshPicker.Component model={refreshPicker} />
|
||||
</div>
|
||||
)}
|
||||
{!hideDashboardControls && model.hasDashboardControls() && (
|
||||
<div className={styles.dashboardControlsButton}>
|
||||
<DashboardControlsButton dashboard={dashboard} />
|
||||
</div>
|
||||
)}
|
||||
{config.featureToggles.dashboardNewLayouts && (
|
||||
<div className={styles.fixedControls}>
|
||||
<DashboardControlActions dashboard={dashboard} />
|
||||
@@ -194,6 +179,7 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
|
||||
<DashboardDataLayerControls dashboard={dashboard} />
|
||||
</>
|
||||
)}
|
||||
{!hideDashboardControls && hasDashboardControls && <DashboardControlsButton dashboard={dashboard} />}
|
||||
{editPanel && <PanelEditControls panelEditor={editPanel} />}
|
||||
{showDebugger && <SceneDebugger scene={model} key={'scene-debugger'} />}
|
||||
</div>
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { SceneDataLayerProvider, sceneGraph, SceneVariable } from '@grafana/scenes';
|
||||
import { DashboardLink, VariableHide } from '@grafana/schema';
|
||||
import { Box, Dropdown, Menu, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { isDashboardDataLayerSetState } from './DashboardDataLayerSet';
|
||||
import { DashboardLinkRenderer } from './DashboardLinkRenderer';
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { DataLayerControl } from './DataLayerControl';
|
||||
import { VariableValueSelectWrapper } from './VariableControls';
|
||||
|
||||
export const DASHBOARD_CONTROLS_MENU_ARIA_LABEL = 'Dashboard controls menu';
|
||||
export const DASHBOARD_CONTROLS_MENU_TITLE = 'Dashboard controls';
|
||||
|
||||
export function DashboardControlsButton({ dashboard }: { dashboard: DashboardScene }) {
|
||||
const { links, uid } = dashboard.useState();
|
||||
// Dashboard links are not supported at the moment.
|
||||
// Reason: nesting <Dropdown> components causes issues since the inner dropdown is rendered in a portal,
|
||||
// so clicking it closes the parent dropdown (the parent sees it as an overlay click, and the event cannot easily be intercepted,
|
||||
// as it is in different HTML subtree).
|
||||
const filteredLinks = links.filter((link) => link.placement === 'inControlsMenu' && link.type !== 'dashboards');
|
||||
const variables = sceneGraph
|
||||
.getVariables(dashboard)!
|
||||
.useState()
|
||||
.variables.filter((v) => v.state.hide === VariableHide.inControlsMenu);
|
||||
const dataState = sceneGraph.getData(dashboard).useState();
|
||||
const annotationLayers = isDashboardDataLayerSetState(dataState) ? dataState.annotationLayers : [];
|
||||
const filteredAnnotationLayers = annotationLayers.filter((layer) => layer.state.placement === 'inControlsMenu');
|
||||
|
||||
if ((variables.length === 0 && filteredLinks.length === 0 && filteredAnnotationLayers.length === 0) || !uid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
overlay={
|
||||
<DashboardControlsMenu
|
||||
variables={variables}
|
||||
links={filteredLinks}
|
||||
annotationLayers={filteredAnnotationLayers}
|
||||
dashboardUID={uid}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ToolbarButton
|
||||
aria-label={t('dashboard.controls.menu.aria-label', DASHBOARD_CONTROLS_MENU_ARIA_LABEL)}
|
||||
title={t('dashboard.controls.menu.title', DASHBOARD_CONTROLS_MENU_TITLE)}
|
||||
icon="ellipsis-v"
|
||||
iconSize="md"
|
||||
narrow
|
||||
variant="canvas"
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
interface DashboardControlsMenuProps {
|
||||
variables: SceneVariable[];
|
||||
links: DashboardLink[];
|
||||
annotationLayers: SceneDataLayerProvider[];
|
||||
dashboardUID: string;
|
||||
}
|
||||
|
||||
function DashboardControlsMenu({ variables, links, annotationLayers, dashboardUID }: DashboardControlsMenuProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<Box
|
||||
minWidth={32}
|
||||
borderColor={'weak'}
|
||||
borderStyle={'solid'}
|
||||
boxShadow={'z3'}
|
||||
display={'flex'}
|
||||
direction={'column'}
|
||||
borderRadius={'default'}
|
||||
backgroundColor={'primary'}
|
||||
padding={1.5}
|
||||
gap={0.5}
|
||||
onClick={(e) => {
|
||||
// Normally, clicking the overlay closes the dropdown.
|
||||
// We stop event propagation here to keep it open while users interact with variable controls.
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{/* Variables */}
|
||||
{variables.map((variable, index) => (
|
||||
<div className={cx({ [styles.menuItem]: index > 0 })} key={variable.state.key}>
|
||||
<VariableValueSelectWrapper variable={variable} inMenu />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Annotation layers */}
|
||||
{annotationLayers.length > 0 &&
|
||||
annotationLayers.map((layer, index) => (
|
||||
<div className={cx({ [styles.menuItem]: variables.length > 0 || index > 0 })} key={layer.state.key}>
|
||||
<DataLayerControl layer={layer} inMenu />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Links */}
|
||||
{links.length > 0 && (
|
||||
<>
|
||||
{(variables.length > 0 || annotationLayers.length > 0) && <MenuDivider />}
|
||||
{links.map((link, index) => (
|
||||
<div key={`${link.title}-${index}`}>
|
||||
<DashboardLinkRenderer link={link} dashboardUID={dashboardUID} inMenu />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuDivider() {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.divider}>
|
||||
<Menu.Divider />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
divider: css({
|
||||
marginTop: theme.spacing(2),
|
||||
padding: theme.spacing(0, 0.5),
|
||||
}),
|
||||
menuItem: css({
|
||||
marginTop: theme.spacing(2),
|
||||
}),
|
||||
});
|
||||
@@ -66,6 +66,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
controlWrapper: css({
|
||||
height: theme.spacing(2),
|
||||
'& > div': {
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneDataLayerProvider, SceneVariable } from '@grafana/scenes';
|
||||
import { DashboardLink } from '@grafana/schema';
|
||||
import { Box, Menu, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { DashboardLinkRenderer } from '../DashboardLinkRenderer';
|
||||
import { DataLayerControl } from '../DataLayerControl';
|
||||
import { VariableValueSelectWrapper } from '../VariableControls';
|
||||
|
||||
interface DashboardControlsMenuProps {
|
||||
variables: SceneVariable[];
|
||||
links: DashboardLink[];
|
||||
annotations: SceneDataLayerProvider[];
|
||||
dashboardUID?: string;
|
||||
}
|
||||
|
||||
export function DashboardControlsMenu({ variables, links, annotations, dashboardUID }: DashboardControlsMenuProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<Box
|
||||
minWidth={32}
|
||||
borderColor={'weak'}
|
||||
borderStyle={'solid'}
|
||||
boxShadow={'z3'}
|
||||
display={'flex'}
|
||||
direction={'column'}
|
||||
borderRadius={'default'}
|
||||
backgroundColor={'primary'}
|
||||
padding={1.5}
|
||||
gap={0.5}
|
||||
onClick={(e) => {
|
||||
// Normally, clicking the overlay closes the dropdown.
|
||||
// We stop event propagation here to keep it open while users interact with variable controls.
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{/* Variables */}
|
||||
{variables.map((variable, index) => (
|
||||
<div className={cx({ [styles.menuItem]: index > 0 })} key={variable.state.key}>
|
||||
<VariableValueSelectWrapper variable={variable} inMenu />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Annotation layers */}
|
||||
{annotations.length > 0 &&
|
||||
annotations.map((layer, index) => (
|
||||
<div className={cx({ [styles.menuItem]: variables.length > 0 || index > 0 })} key={layer.state.key}>
|
||||
<DataLayerControl layer={layer} inMenu />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Links */}
|
||||
{links.length > 0 && dashboardUID && (
|
||||
<>
|
||||
{(variables.length > 0 || annotations.length > 0) && <MenuDivider />}
|
||||
{links.map((link, index) => (
|
||||
<div key={`${link.title}-${index}`}>
|
||||
<DashboardLinkRenderer link={link} dashboardUID={dashboardUID} inMenu />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuDivider() {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.divider}>
|
||||
<Menu.Divider />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
divider: css({
|
||||
marginTop: theme.spacing(2),
|
||||
padding: theme.spacing(0, 0.5),
|
||||
}),
|
||||
menuItem: css({
|
||||
marginTop: theme.spacing(2),
|
||||
}),
|
||||
});
|
||||
+3
-2
@@ -4,12 +4,13 @@ import userEvent from '@testing-library/user-event';
|
||||
import { VariableHide } from '@grafana/data';
|
||||
import { SceneVariableSet, TextBoxVariable, QueryVariable, CustomVariable, SceneVariable } from '@grafana/scenes';
|
||||
|
||||
import { DashboardScene } from '../DashboardScene';
|
||||
|
||||
import {
|
||||
DASHBOARD_CONTROLS_MENU_ARIA_LABEL,
|
||||
DASHBOARD_CONTROLS_MENU_TITLE,
|
||||
DashboardControlsButton,
|
||||
} from './DashboardControlsMenu';
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
} from './DashboardControlsMenuButton';
|
||||
|
||||
describe('DashboardControlsMenu', () => {
|
||||
it('should return null and not render anything when there are no variables', () => {
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { Dropdown, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { DashboardScene } from '../DashboardScene';
|
||||
|
||||
import { DashboardControlsMenu } from './DashboardControlsMenu';
|
||||
import { useDashboardControls } from './utils';
|
||||
|
||||
export const DASHBOARD_CONTROLS_MENU_ARIA_LABEL = 'Dashboard controls menu';
|
||||
export const DASHBOARD_CONTROLS_MENU_TITLE = 'Dashboard controls';
|
||||
|
||||
export function DashboardControlsButton({ dashboard }: { dashboard: DashboardScene }) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { uid } = dashboard.useState();
|
||||
const { variables, links, annotations } = useDashboardControls(dashboard);
|
||||
const dashboardControlsCount = variables.length + links.length + annotations.length;
|
||||
const hasDashboardControls = dashboardControlsCount > 0;
|
||||
|
||||
if (!hasDashboardControls) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
placement="bottom-start"
|
||||
overlay={
|
||||
<DashboardControlsMenu variables={variables} links={links} annotations={annotations} dashboardUID={uid} />
|
||||
}
|
||||
>
|
||||
<ToolbarButton
|
||||
aria-label={t('dashboard.controls.menu.aria-label', DASHBOARD_CONTROLS_MENU_ARIA_LABEL)}
|
||||
title={t('dashboard.controls.menu.title', DASHBOARD_CONTROLS_MENU_TITLE)}
|
||||
icon="sliders-v-alt"
|
||||
iconSize="md"
|
||||
variant="canvas"
|
||||
className={styles.dropdownButton}
|
||||
>
|
||||
+ {dashboardControlsCount}
|
||||
</ToolbarButton>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
dropdownButton: css({
|
||||
display: 'inline-flex',
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { SceneDataState, sceneGraph, SceneVariable } from '@grafana/scenes';
|
||||
import { DashboardLink, VariableHide } from '@grafana/schema';
|
||||
|
||||
import { isDashboardDataLayerSetState } from '../DashboardDataLayerSet';
|
||||
import { DashboardScene } from '../DashboardScene';
|
||||
|
||||
export function getDashboardControlsLinks(links: DashboardLink[]) {
|
||||
// Dashboard links are not supported at the moment.
|
||||
// Reason: nesting <Dropdown> components causes issues since the inner dropdown is rendered in a portal,
|
||||
// so clicking it closes the parent dropdown (the parent sees it as an overlay click, and the event cannot easily be intercepted,
|
||||
// as it is in different HTML subtree).
|
||||
return links.filter((link) => link.placement === 'inControlsMenu' && link.type !== 'dashboards');
|
||||
}
|
||||
|
||||
export function getDashboardControlsVariables(variables: SceneVariable[]) {
|
||||
return variables.filter((v) => v.state.hide === VariableHide.inControlsMenu);
|
||||
}
|
||||
|
||||
export function getDashboardControlsAnnotations(dataState: SceneDataState) {
|
||||
return (isDashboardDataLayerSetState(dataState) ? dataState.annotationLayers : []).filter(
|
||||
(layer) => layer.state.placement === 'inControlsMenu' && !layer.state.isHidden
|
||||
);
|
||||
}
|
||||
|
||||
export function getDashboardControls(dashboard: DashboardScene) {
|
||||
const variables = getDashboardControlsVariables(sceneGraph.getVariables(dashboard)?.state.variables);
|
||||
const links = getDashboardControlsLinks(dashboard.state.links);
|
||||
const annotations = getDashboardControlsAnnotations(sceneGraph.getData(dashboard).state);
|
||||
|
||||
return {
|
||||
variables,
|
||||
links,
|
||||
annotations,
|
||||
};
|
||||
}
|
||||
|
||||
export function useDashboardControls(dashboard: DashboardScene) {
|
||||
const dashboardState = dashboard.useState();
|
||||
const variablesState = sceneGraph.getVariables(dashboard).useState();
|
||||
const dataState = sceneGraph.getData(dashboard).useState();
|
||||
const links = getDashboardControlsLinks(dashboardState.links);
|
||||
const variables = getDashboardControlsVariables(variablesState.variables);
|
||||
const annotations = getDashboardControlsAnnotations(dataState);
|
||||
|
||||
return {
|
||||
variables,
|
||||
links,
|
||||
annotations,
|
||||
};
|
||||
}
|
||||
|
||||
export function useHasDashboardControls(dashboard: DashboardScene) {
|
||||
const { variables, links, annotations } = useDashboardControls(dashboard);
|
||||
|
||||
return variables.length > 0 || links.length > 0 || annotations.length > 0;
|
||||
}
|
||||
|
||||
export function hasDashboardControls(dashboard: DashboardScene) {
|
||||
const { variables, links, annotations } = getDashboardControls(dashboard);
|
||||
|
||||
return variables.length > 0 || links.length > 0 || annotations.length > 0;
|
||||
}
|
||||
@@ -119,6 +119,7 @@ export class AnnotationsEditView extends SceneObjectBase<AnnotationsEditViewStat
|
||||
name: annotation.name,
|
||||
isEnabled: Boolean(annotation.enable),
|
||||
isHidden: Boolean(annotation.hide),
|
||||
placement: annotation.placement,
|
||||
query: annotation,
|
||||
});
|
||||
|
||||
|
||||
+121
-23
@@ -1,4 +1,4 @@
|
||||
import { act, render } from '@testing-library/react';
|
||||
import { act, render, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
@@ -88,6 +88,22 @@ describe('AnnotationSettingsEdit', () => {
|
||||
};
|
||||
}
|
||||
|
||||
// For testing combobox
|
||||
beforeAll(() => {
|
||||
const mockGetBoundingClientRect = jest.fn(() => ({
|
||||
width: 120,
|
||||
height: 120,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
}));
|
||||
|
||||
Object.defineProperty(Element.prototype, 'getBoundingClientRect', {
|
||||
value: mockGetBoundingClientRect,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -100,7 +116,9 @@ describe('AnnotationSettingsEdit', () => {
|
||||
const nameInput = getByTestId(selectors.pages.Dashboard.Settings.Annotations.Settings.name);
|
||||
const dataSourceSelect = getByTestId(selectors.components.DataSourcePicker.container);
|
||||
const enableToggle = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.enable);
|
||||
const hideToggle = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.hide);
|
||||
const annotationControlsDisplaySelect = getByTestId(
|
||||
selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.annotationControlsDisplay
|
||||
);
|
||||
const iconColorToggle = getByTestId(selectors.components.ColorSwatch.name);
|
||||
const panelSelect = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.showInLabel);
|
||||
const deleteAnno = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.delete);
|
||||
@@ -109,7 +127,7 @@ describe('AnnotationSettingsEdit', () => {
|
||||
expect(nameInput).toBeInTheDocument();
|
||||
expect(dataSourceSelect).toBeInTheDocument();
|
||||
expect(enableToggle).toBeInTheDocument();
|
||||
expect(hideToggle).toBeInTheDocument();
|
||||
expect(annotationControlsDisplaySelect).toBeInTheDocument();
|
||||
expect(iconColorToggle).toBeInTheDocument();
|
||||
expect(panelSelect).toBeInTheDocument();
|
||||
expect(deleteAnno).toBeInTheDocument();
|
||||
@@ -147,26 +165,6 @@ describe('AnnotationSettingsEdit', () => {
|
||||
expect(mockOnUpdate).toHaveBeenCalledWith(annoArg, 1);
|
||||
});
|
||||
|
||||
it('should toggle annotation hide on change', async () => {
|
||||
const {
|
||||
renderer: { getByTestId },
|
||||
user,
|
||||
anno,
|
||||
} = await setup();
|
||||
|
||||
const annoArg = {
|
||||
...anno,
|
||||
hide: !anno.hide,
|
||||
};
|
||||
|
||||
const hideToggle = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.hide);
|
||||
|
||||
await user.click(hideToggle);
|
||||
|
||||
expect(mockOnUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnUpdate).toHaveBeenCalledWith(annoArg, 1);
|
||||
});
|
||||
|
||||
it('should set annotation filter', async () => {
|
||||
const {
|
||||
renderer: { getByTestId },
|
||||
@@ -207,4 +205,104 @@ describe('AnnotationSettingsEdit', () => {
|
||||
|
||||
expect(mockGoBackToList).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should render the annotation controls display combobox', async () => {
|
||||
const {
|
||||
renderer: { getByTestId },
|
||||
} = await setup();
|
||||
|
||||
const field = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.annotationControlsDisplay);
|
||||
const combobox = within(field).getByRole('combobox');
|
||||
expect(combobox).toBeInTheDocument();
|
||||
expect(combobox).toHaveValue('Above dashboard');
|
||||
});
|
||||
|
||||
it('should set placement to undefined when selecting "Above dashboard" instead of "Controls menu"', async () => {
|
||||
const annotationQuery: AnnotationQuery = {
|
||||
name: 'test',
|
||||
datasource: defaultDatasource,
|
||||
enable: true,
|
||||
hide: false,
|
||||
iconColor: 'blue',
|
||||
placement: 'inControlsMenu',
|
||||
};
|
||||
|
||||
const props = {
|
||||
annotation: annotationQuery,
|
||||
onUpdate: mockOnUpdate,
|
||||
editIndex: 1,
|
||||
panels: [],
|
||||
onBackToList: mockGoBackToList,
|
||||
onDelete: mockOnDelete,
|
||||
};
|
||||
|
||||
const {
|
||||
user,
|
||||
renderer: { getByTestId, findByText },
|
||||
} = {
|
||||
user: userEvent.setup(),
|
||||
renderer: await act(async () => render(<AnnotationSettingsEdit {...props} />)),
|
||||
};
|
||||
|
||||
const field = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.annotationControlsDisplay);
|
||||
const combobox = within(field).getByRole('combobox');
|
||||
await user.click(combobox);
|
||||
|
||||
const aboveDashboardOption = await findByText('Above dashboard');
|
||||
await user.click(aboveDashboardOption);
|
||||
|
||||
expect(mockOnUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnUpdate).toHaveBeenCalledWith(
|
||||
{
|
||||
...annotationQuery,
|
||||
placement: undefined,
|
||||
},
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
it('should set `hide: true` and `placement: undefined` when selecting "Hidden"', async () => {
|
||||
const annotationQuery: AnnotationQuery = {
|
||||
name: 'test',
|
||||
datasource: defaultDatasource,
|
||||
enable: true,
|
||||
hide: false,
|
||||
iconColor: 'blue',
|
||||
placement: 'inControlsMenu',
|
||||
};
|
||||
|
||||
const props = {
|
||||
annotation: annotationQuery,
|
||||
onUpdate: mockOnUpdate,
|
||||
editIndex: 1,
|
||||
panels: [],
|
||||
onBackToList: mockGoBackToList,
|
||||
onDelete: mockOnDelete,
|
||||
};
|
||||
|
||||
const {
|
||||
user,
|
||||
renderer: { getByTestId, findByText },
|
||||
} = {
|
||||
user: userEvent.setup(),
|
||||
renderer: await act(async () => render(<AnnotationSettingsEdit {...props} />)),
|
||||
};
|
||||
|
||||
const field = getByTestId(selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.annotationControlsDisplay);
|
||||
const combobox = within(field).getByRole('combobox');
|
||||
await user.click(combobox);
|
||||
|
||||
const hiddenOption = await findByText('Hidden');
|
||||
await user.click(hiddenOption);
|
||||
|
||||
expect(mockOnUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnUpdate).toHaveBeenCalledWith(
|
||||
{
|
||||
...annotationQuery,
|
||||
hide: true,
|
||||
placement: undefined,
|
||||
},
|
||||
1
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+192
-96
@@ -15,7 +15,20 @@ import { Trans, t } from '@grafana/i18n';
|
||||
import { config, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
import { AnnotationPanelFilter } from '@grafana/schema/src/raw/dashboard/x/dashboard_types.gen';
|
||||
import { Button, Checkbox, Field, FieldSet, Input, MultiSelect, Select, useStyles2, Stack, Alert } from '@grafana/ui';
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Field,
|
||||
FieldSet,
|
||||
Input,
|
||||
MultiSelect,
|
||||
Select,
|
||||
useStyles2,
|
||||
Stack,
|
||||
Alert,
|
||||
ComboboxOption,
|
||||
Combobox,
|
||||
} from '@grafana/ui';
|
||||
import { ColorValueEditor } from 'app/core/components/OptionsUI/color';
|
||||
import StandardAnnotationQueryEditor from 'app/features/annotations/components/StandardAnnotationQueryEditor';
|
||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||
@@ -31,8 +44,15 @@ type Props = {
|
||||
onDelete: (index: number) => void;
|
||||
};
|
||||
|
||||
const collator = Intl.Collator();
|
||||
export const newAnnotationName = 'New annotation';
|
||||
|
||||
enum AnnotationControlsDisplay {
|
||||
Hidden,
|
||||
AboveDashboard,
|
||||
InControlsMenu,
|
||||
}
|
||||
|
||||
export const AnnotationSettingsEdit = ({ annotation, editIndex, panels, onUpdate, onBackToList, onDelete }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
@@ -49,6 +69,63 @@ export const AnnotationSettingsEdit = ({ annotation, editIndex, panels, onUpdate
|
||||
|
||||
const dsi = getDataSourceSrv().getInstanceSettings(annotation.datasource);
|
||||
|
||||
const AnnotationControlsDisplayOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: AnnotationControlsDisplay.AboveDashboard,
|
||||
label: t(
|
||||
'dashboard-scene.annotation-settings-edit.control-display-options.above-dashboard.label',
|
||||
'Above dashboard'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: AnnotationControlsDisplay.InControlsMenu,
|
||||
label: t(
|
||||
'dashboard-scene.annotation-settings-edit.control-display-options.controls-menu.label',
|
||||
'Controls menu'
|
||||
),
|
||||
description: t(
|
||||
'dashboard-scene.annotation-settings-edit.control-display-options.controls-menu.description',
|
||||
'Can be accessed when the controls menu is open'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: AnnotationControlsDisplay.Hidden,
|
||||
label: t('dashboard-scene.annotation-settings-edit.control-display-options.hidden.label', 'Hidden'),
|
||||
description: t(
|
||||
'dashboard-scene.annotation-settings-edit.control-display-options.hidden.description',
|
||||
'Hides the toggle for turning this annotation on or off'
|
||||
),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// The UI is using a single select input for where to display the annotation controls, however under the hood
|
||||
// it is computed from different fields of the annotation.
|
||||
const annotationControlsDisplayValue = useMemo(() => {
|
||||
if (annotation.hide) {
|
||||
return AnnotationControlsDisplay.Hidden;
|
||||
}
|
||||
|
||||
if (annotation.placement === 'inControlsMenu') {
|
||||
return AnnotationControlsDisplay.InControlsMenu;
|
||||
}
|
||||
|
||||
return AnnotationControlsDisplay.AboveDashboard;
|
||||
}, [annotation]);
|
||||
|
||||
const onAnnotationControlDisplayChange = (option: ComboboxOption<AnnotationControlsDisplay>) => {
|
||||
onUpdate(
|
||||
{
|
||||
...annotation,
|
||||
placement: option.value === AnnotationControlsDisplay.InControlsMenu ? 'inControlsMenu' : undefined,
|
||||
hide: option.value === AnnotationControlsDisplay.Hidden ? true : false,
|
||||
},
|
||||
editIndex
|
||||
);
|
||||
};
|
||||
|
||||
const onNameChange = (ev: React.FocusEvent<HTMLInputElement>) => {
|
||||
onUpdate(
|
||||
{
|
||||
@@ -143,7 +220,7 @@ export const AnnotationSettingsEdit = ({ annotation, editIndex, panels, onUpdate
|
||||
|
||||
const sortFn = (a: SelectableValue<number>, b: SelectableValue<number>) => {
|
||||
if (a.label && b.label) {
|
||||
return a.label.toLowerCase().localeCompare(b.label.toLowerCase());
|
||||
return collator.compare(a.label, b.label);
|
||||
}
|
||||
|
||||
return -1;
|
||||
@@ -169,102 +246,121 @@ export const AnnotationSettingsEdit = ({ annotation, editIndex, panels, onUpdate
|
||||
return (
|
||||
<div>
|
||||
<FieldSet className={styles.settingsForm}>
|
||||
<Field label={t('dashboard-scene.annotation-settings-edit.label-name', 'Name')}>
|
||||
<Input
|
||||
data-testid={selectors.pages.Dashboard.Settings.Annotations.Settings.name}
|
||||
name="name"
|
||||
id="name"
|
||||
autoFocus={isNewAnnotation}
|
||||
value={annotation.name}
|
||||
onChange={onNameChange}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={t('dashboard-scene.annotation-settings-edit.label-data-source', 'Data source')}
|
||||
htmlFor="data-source-picker"
|
||||
>
|
||||
<DataSourcePicker annotations variables current={annotation.datasource} onChange={onDataSourceChange} />
|
||||
</Field>
|
||||
{!ds?.meta.annotations && (
|
||||
<Alert
|
||||
title={t(
|
||||
'dashboard-scene.annotation-settings-edit.title-annotation-support-source',
|
||||
'No annotation support for this data source'
|
||||
)}
|
||||
severity="error"
|
||||
>
|
||||
<Trans i18nKey="errors.dashboard-settings.annotations.datasource">
|
||||
The selected data source does not support annotations. Please select a different data source.
|
||||
</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
<Field
|
||||
label={t('dashboard-scene.annotation-settings-edit.label-enabled', 'Enabled')}
|
||||
description={t(
|
||||
'dashboard-scene.annotation-settings-edit.description-enabled-annotation-query-issued-every-dashboard',
|
||||
'When enabled the annotation query is issued every dashboard refresh'
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
name="enable"
|
||||
id="enable"
|
||||
value={annotation.enable}
|
||||
onChange={onChange}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.enable}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={t('dashboard-scene.annotation-settings-edit.label-hidden', 'Hidden')}
|
||||
description={t(
|
||||
'dashboard-scene.annotation-settings-edit.description-hidden',
|
||||
'Annotation queries can be toggled on or off at the top of the dashboard. With this option checked this toggle will be hidden.'
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
name="hide"
|
||||
id="hide"
|
||||
value={annotation.hide}
|
||||
onChange={onChange}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.hide}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={t('dashboard-scene.annotation-settings-edit.label-color', 'Color')}
|
||||
description={t(
|
||||
'dashboard-scene.annotation-settings-edit.description-color-annotation-event-markers',
|
||||
'Color to use for the annotation event markers'
|
||||
)}
|
||||
>
|
||||
<Stack>
|
||||
<ColorValueEditor value={annotation?.iconColor} onChange={onColorChange} />
|
||||
</Stack>
|
||||
</Field>
|
||||
<Field
|
||||
label={t('dashboard-scene.annotation-settings-edit.label-show-in', 'Show in')}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.showInLabel}
|
||||
>
|
||||
<>
|
||||
<Select
|
||||
options={getPanelFilters()}
|
||||
value={panelFilter}
|
||||
onChange={onFilterTypeChange}
|
||||
data-testid={selectors.components.Annotations.annotationsTypeInput}
|
||||
<Stack direction="column" gap={2}>
|
||||
{/* Name */}
|
||||
<Field noMargin label={t('dashboard-scene.annotation-settings-edit.label-name', 'Name')}>
|
||||
<Input
|
||||
data-testid={selectors.pages.Dashboard.Settings.Annotations.Settings.name}
|
||||
name="name"
|
||||
id="name"
|
||||
autoFocus={isNewAnnotation}
|
||||
value={annotation.name}
|
||||
onChange={onNameChange}
|
||||
/>
|
||||
{panelFilter !== PanelFilterType.AllPanels && (
|
||||
<MultiSelect
|
||||
options={selectablePanels}
|
||||
value={selectablePanels.filter((panel) => annotation.filter?.ids.includes(panel.value!))}
|
||||
onChange={onAddFilterPanelID}
|
||||
isClearable={true}
|
||||
placeholder={t('dashboard-scene.annotation-settings-edit.placeholder-choose-panels', 'Choose panels')}
|
||||
width={100}
|
||||
closeMenuOnSelect={false}
|
||||
className={styles.select}
|
||||
data-testid={selectors.components.Annotations.annotationsChoosePanelInput}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{/* Data source */}
|
||||
<Field
|
||||
noMargin
|
||||
label={t('dashboard-scene.annotation-settings-edit.label-data-source', 'Data source')}
|
||||
htmlFor="data-source-picker"
|
||||
>
|
||||
<DataSourcePicker annotations variables current={annotation.datasource} onChange={onDataSourceChange} />
|
||||
</Field>
|
||||
{!ds?.meta.annotations && (
|
||||
<Alert
|
||||
title={t(
|
||||
'dashboard-scene.annotation-settings-edit.title-annotation-support-source',
|
||||
'No annotation support for this data source'
|
||||
)}
|
||||
severity="error"
|
||||
>
|
||||
<Trans i18nKey="errors.dashboard-settings.annotations.datasource">
|
||||
The selected data source does not support annotations. Please select a different data source.
|
||||
</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Enabled */}
|
||||
<Field
|
||||
noMargin
|
||||
label={t('dashboard-scene.annotation-settings-edit.label-enabled', 'Enabled')}
|
||||
description={t(
|
||||
'dashboard-scene.annotation-settings-edit.description-enabled-annotation-query-issued-every-dashboard',
|
||||
'When enabled the annotation query is issued every dashboard refresh'
|
||||
)}
|
||||
</>
|
||||
</Field>
|
||||
>
|
||||
<Checkbox
|
||||
name="enable"
|
||||
id="enable"
|
||||
value={annotation.enable}
|
||||
onChange={onChange}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.enable}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{/* Color */}
|
||||
<Field
|
||||
noMargin
|
||||
label={t('dashboard-scene.annotation-settings-edit.label-color', 'Color')}
|
||||
description={t(
|
||||
'dashboard-scene.annotation-settings-edit.description-color-annotation-event-markers',
|
||||
'Color to use for the annotation event markers'
|
||||
)}
|
||||
>
|
||||
<Stack>
|
||||
<ColorValueEditor value={annotation?.iconColor} onChange={onColorChange} />
|
||||
</Stack>
|
||||
</Field>
|
||||
|
||||
{/* Annotation controls display */}
|
||||
<Field
|
||||
noMargin
|
||||
label={t(
|
||||
'dashboard-scene.annotation-settings-edit.label-annotation-controls-display',
|
||||
'Show annotation controls in'
|
||||
)}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.annotationControlsDisplay}
|
||||
>
|
||||
<Combobox
|
||||
id="annotationControlsDisplay"
|
||||
options={AnnotationControlsDisplayOptions}
|
||||
value={annotationControlsDisplayValue}
|
||||
onChange={onAnnotationControlDisplayChange}
|
||||
width="auto"
|
||||
minWidth={100}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{/* Show in */}
|
||||
<Field
|
||||
noMargin
|
||||
label={t('dashboard-scene.annotation-settings-edit.label-show-in', 'Show in')}
|
||||
data-testid={selectors.pages.Dashboard.Settings.Annotations.NewAnnotation.showInLabel}
|
||||
>
|
||||
<>
|
||||
<Select
|
||||
options={getPanelFilters()}
|
||||
value={panelFilter}
|
||||
onChange={onFilterTypeChange}
|
||||
data-testid={selectors.components.Annotations.annotationsTypeInput}
|
||||
/>
|
||||
{panelFilter !== PanelFilterType.AllPanels && (
|
||||
<MultiSelect
|
||||
options={selectablePanels}
|
||||
value={selectablePanels.filter((panel) => annotation.filter?.ids.includes(panel.value!))}
|
||||
onChange={onAddFilterPanelID}
|
||||
isClearable={true}
|
||||
placeholder={t('dashboard-scene.annotation-settings-edit.placeholder-choose-panels', 'Choose panels')}
|
||||
width={100}
|
||||
closeMenuOnSelect={false}
|
||||
className={styles.select}
|
||||
data-testid={selectors.components.Annotations.annotationsChoosePanelInput}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Field>
|
||||
</Stack>
|
||||
</FieldSet>
|
||||
<FieldSet>
|
||||
<h3 className="page-heading">
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { DashboardLink } from '@grafana/schema';
|
||||
|
||||
import { DashboardLinkForm } from './DashboardLinkForm';
|
||||
import { NEW_LINK } from './utils';
|
||||
|
||||
describe('DashboardLinkForm', () => {
|
||||
const defaultLink: DashboardLink = {
|
||||
title: 'Test Link',
|
||||
type: 'link',
|
||||
url: 'https://example.com',
|
||||
icon: 'external link',
|
||||
tags: [],
|
||||
asDropdown: false,
|
||||
targetBlank: false,
|
||||
includeVars: false,
|
||||
keepTime: false,
|
||||
tooltip: '',
|
||||
};
|
||||
|
||||
it('should render the "Show in controls menu" checkbox', () => {
|
||||
const onUpdate = jest.fn();
|
||||
const onGoBack = jest.fn();
|
||||
|
||||
render(<DashboardLinkForm link={defaultLink} onUpdate={onUpdate} onGoBack={onGoBack} />);
|
||||
|
||||
expect(screen.getByText('Show in controls menu')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onUpdate with placement="inControlsMenu" when checkbox is checked', async () => {
|
||||
const onUpdate = jest.fn();
|
||||
const onGoBack = jest.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<DashboardLinkForm link={defaultLink} onUpdate={onUpdate} onGoBack={onGoBack} />);
|
||||
|
||||
const checkbox = screen.getByRole('checkbox', { name: 'Show in controls menu' });
|
||||
await user.click(checkbox);
|
||||
|
||||
expect(onUpdate).toHaveBeenCalledWith({
|
||||
...defaultLink,
|
||||
placement: 'inControlsMenu',
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onUpdate with placement=undefined when checkbox is unchecked', async () => {
|
||||
const onUpdate = jest.fn();
|
||||
const onGoBack = jest.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
const linkWithPlacement: DashboardLink = {
|
||||
...defaultLink,
|
||||
placement: 'inControlsMenu',
|
||||
};
|
||||
|
||||
render(<DashboardLinkForm link={linkWithPlacement} onUpdate={onUpdate} onGoBack={onGoBack} />);
|
||||
|
||||
const checkbox = screen.getByRole('checkbox', { name: 'Show in controls menu' });
|
||||
await user.click(checkbox);
|
||||
|
||||
expect(onUpdate).toHaveBeenCalledWith({
|
||||
...linkWithPlacement,
|
||||
placement: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should have checkbox checked when placement is "inControlsMenu"', () => {
|
||||
const onUpdate = jest.fn();
|
||||
const onGoBack = jest.fn();
|
||||
|
||||
const linkWithPlacement: DashboardLink = {
|
||||
...defaultLink,
|
||||
placement: 'inControlsMenu',
|
||||
};
|
||||
|
||||
render(<DashboardLinkForm link={linkWithPlacement} onUpdate={onUpdate} onGoBack={onGoBack} />);
|
||||
|
||||
const checkbox = screen.getByRole('checkbox', { name: 'Show in controls menu' });
|
||||
expect(checkbox).toBeChecked();
|
||||
});
|
||||
|
||||
it('should have checkbox unchecked when placement is undefined', () => {
|
||||
const onUpdate = jest.fn();
|
||||
const onGoBack = jest.fn();
|
||||
|
||||
render(<DashboardLinkForm link={defaultLink} onUpdate={onUpdate} onGoBack={onGoBack} />);
|
||||
|
||||
const checkbox = screen.getByRole('checkbox', { name: 'Show in controls menu' });
|
||||
expect(checkbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('should render checkbox for dashboards type links', () => {
|
||||
const onUpdate = jest.fn();
|
||||
const onGoBack = jest.fn();
|
||||
|
||||
const dashboardsLink: DashboardLink = {
|
||||
...defaultLink,
|
||||
type: 'dashboards',
|
||||
url: '',
|
||||
};
|
||||
|
||||
render(<DashboardLinkForm link={dashboardsLink} onUpdate={onUpdate} onGoBack={onGoBack} />);
|
||||
|
||||
expect(screen.getByText('Show in controls menu')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should autofocus title input for new links', () => {
|
||||
const onUpdate = jest.fn();
|
||||
const onGoBack = jest.fn();
|
||||
|
||||
render(<DashboardLinkForm link={NEW_LINK} onUpdate={onUpdate} onGoBack={onGoBack} />);
|
||||
|
||||
const titleInput = screen.getByRole('textbox', { name: 'Title' });
|
||||
expect(titleInput).toHaveFocus();
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
import * as React from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { DashboardLink } from '@grafana/schema';
|
||||
import { CollapsableSection, TagsInput, Select, Field, Input, Checkbox, Button } from '@grafana/ui';
|
||||
import { CollapsableSection, TagsInput, Select, Field, Input, Checkbox, Button, Stack, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { LINK_ICON_MAP, NEW_LINK } from './utils';
|
||||
|
||||
@@ -16,6 +17,7 @@ interface DashboardLinkFormProps {
|
||||
}
|
||||
|
||||
export function DashboardLinkForm({ link, onUpdate, onGoBack }: DashboardLinkFormProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const linkTypeOptions = [
|
||||
{
|
||||
value: 'dashboards',
|
||||
@@ -53,86 +55,138 @@ export function DashboardLinkForm({ link, onUpdate, onGoBack }: DashboardLinkFor
|
||||
});
|
||||
};
|
||||
|
||||
const onPlacementChange = (ev: React.FocusEvent<HTMLInputElement>) => {
|
||||
const isChecked = ev.currentTarget.checked;
|
||||
onUpdate({
|
||||
...link,
|
||||
placement: isChecked ? 'inControlsMenu' : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const isNew = link.title === NEW_LINK.title;
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '600px' }}>
|
||||
<Field label={t('dashboard-scene.dashboard-link-form.label-title', 'Title')}>
|
||||
<Input name="title" id="title" value={link.title} onChange={onChange} autoFocus={isNew} />
|
||||
</Field>
|
||||
<Field label={t('dashboard-scene.dashboard-link-form.label-type', 'Type')}>
|
||||
<Select inputId="link-type-input" value={link.type} options={linkTypeOptions} onChange={onTypeChange} />
|
||||
</Field>
|
||||
{link.type === 'dashboards' && (
|
||||
<>
|
||||
<Field label={t('dashboard-scene.dashboard-link-form.label-with-tags', 'With tags')}>
|
||||
<Stack direction="column" gap={2}>
|
||||
{/* Title */}
|
||||
<Field noMargin label={t('dashboard-scene.dashboard-link-form.label-title', 'Title')}>
|
||||
<Input name="title" id="title" value={link.title} onChange={onChange} autoFocus={isNew} />
|
||||
</Field>
|
||||
|
||||
{/* Type */}
|
||||
<Field noMargin label={t('dashboard-scene.dashboard-link-form.label-type', 'Type')}>
|
||||
<Select inputId="link-type-input" value={link.type} options={linkTypeOptions} onChange={onTypeChange} />
|
||||
</Field>
|
||||
|
||||
{/* Tags */}
|
||||
{link.type === 'dashboards' && (
|
||||
<Field noMargin label={t('dashboard-scene.dashboard-link-form.label-with-tags', 'With tags')}>
|
||||
<TagsInput tags={link.tags} onChange={onTagsChange} />
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
{link.type === 'link' && (
|
||||
<>
|
||||
<Field label={t('dashboard-scene.dashboard-link-form.label-url', 'URL')}>
|
||||
<Input name="url" value={link.url} onChange={onChange} />
|
||||
</Field>
|
||||
<Field label={t('dashboard-scene.dashboard-link-form.label-tooltip', 'Tooltip')}>
|
||||
<Input
|
||||
name="tooltip"
|
||||
value={link.tooltip}
|
||||
onChange={onChange}
|
||||
placeholder={t('dashboard-scene.dashboard-link-form.placeholder-open-dashboard', 'Open dashboard')}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t('dashboard-scene.dashboard-link-form.label-icon', 'Icon')}>
|
||||
<Select value={link.icon} options={linkIconOptions} onChange={onIconChange} />
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
<CollapsableSection label={t('dashboard-scene.dashboard-link-form.label-options', 'Options')} isOpen={true}>
|
||||
{link.type === 'dashboards' && (
|
||||
<Field>
|
||||
<Checkbox
|
||||
label={t('dashboard-scene.dashboard-link-form.label-show-as-dropdown', 'Show as dropdown')}
|
||||
name="asDropdown"
|
||||
value={link.asDropdown}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
<Field>
|
||||
<Checkbox
|
||||
label={t(
|
||||
'dashboard-scene.dashboard-link-form.label-include-current-time-range',
|
||||
'Include current time range'
|
||||
|
||||
{link.type === 'link' && (
|
||||
<Stack direction="column" gap={2}>
|
||||
{/* URL */}
|
||||
<Field noMargin label={t('dashboard-scene.dashboard-link-form.label-url', 'URL')}>
|
||||
<Input name="url" value={link.url} onChange={onChange} />
|
||||
</Field>
|
||||
|
||||
{/* Tooltip */}
|
||||
<Field noMargin label={t('dashboard-scene.dashboard-link-form.label-tooltip', 'Tooltip')}>
|
||||
<Input
|
||||
name="tooltip"
|
||||
value={link.tooltip}
|
||||
onChange={onChange}
|
||||
placeholder={t('dashboard-scene.dashboard-link-form.placeholder-open-dashboard', 'Open dashboard')}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{/* Icon */}
|
||||
<Field noMargin label={t('dashboard-scene.dashboard-link-form.label-icon', 'Icon')}>
|
||||
<Select value={link.icon} options={linkIconOptions} onChange={onIconChange} />
|
||||
</Field>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<CollapsableSection
|
||||
label={t('dashboard-scene.dashboard-link-form.label-options', 'Options')}
|
||||
isOpen={true}
|
||||
contentClassName={styles.collapsableSection}
|
||||
>
|
||||
<Stack direction="column" gap={2}>
|
||||
{/* Show as dropdown */}
|
||||
{link.type === 'dashboards' && (
|
||||
<Field noMargin>
|
||||
<Checkbox
|
||||
label={t('dashboard-scene.dashboard-link-form.label-show-as-dropdown', 'Show as dropdown')}
|
||||
name="asDropdown"
|
||||
value={link.asDropdown}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
name="keepTime"
|
||||
value={link.keepTime}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Checkbox
|
||||
label={t(
|
||||
'dashboard-scene.dashboard-link-form.label-include-current-template-variable-values',
|
||||
'Include current template variable values'
|
||||
)}
|
||||
name="includeVars"
|
||||
value={link.includeVars}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Checkbox
|
||||
label={t('dashboard-scene.dashboard-link-form.label-open-link-in-new-tab', 'Open link in new tab')}
|
||||
name="targetBlank"
|
||||
value={link.targetBlank}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Field>
|
||||
</CollapsableSection>
|
||||
<Button variant="secondary" onClick={onGoBack}>
|
||||
<Trans i18nKey="dashboard-scene.dashboard-link-form.back-to-list">Back to list</Trans>
|
||||
</Button>
|
||||
|
||||
{/* Include time range */}
|
||||
<Field noMargin>
|
||||
<Checkbox
|
||||
label={t(
|
||||
'dashboard-scene.dashboard-link-form.label-include-current-time-range',
|
||||
'Include current time range'
|
||||
)}
|
||||
name="keepTime"
|
||||
value={link.keepTime}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{/* Include variables */}
|
||||
<Field noMargin>
|
||||
<Checkbox
|
||||
label={t(
|
||||
'dashboard-scene.dashboard-link-form.label-include-current-template-variable-values',
|
||||
'Include current template variable values'
|
||||
)}
|
||||
name="includeVars"
|
||||
value={link.includeVars}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{/* Open in new tab */}
|
||||
<Field noMargin>
|
||||
<Checkbox
|
||||
label={t('dashboard-scene.dashboard-link-form.label-open-link-in-new-tab', 'Open link in new tab')}
|
||||
name="targetBlank"
|
||||
value={link.targetBlank}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{/* Show in controls */}
|
||||
<Field noMargin>
|
||||
<Checkbox
|
||||
label={t('dashboard-scene.dashboard-link-form.label-show-in-controls-menu', 'Show in controls menu')}
|
||||
name="placement"
|
||||
value={link.placement === 'inControlsMenu'}
|
||||
onChange={onPlacementChange}
|
||||
/>
|
||||
</Field>
|
||||
</Stack>
|
||||
</CollapsableSection>
|
||||
|
||||
<div>
|
||||
<Button variant="secondary" onClick={onGoBack}>
|
||||
<Trans i18nKey="dashboard-scene.dashboard-link-form.back-to-list">Back to list</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = () => ({
|
||||
collapsableSection: css({
|
||||
padding: 0,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import { dashboardEditActions } from '../../edit-pane/shared';
|
||||
import { useEditPaneInputAutoFocus } from '../../scene/layouts-shared/utils';
|
||||
import { BulkActionElement } from '../../scene/types/BulkActionElement';
|
||||
import { EditableDashboardElement, EditableDashboardElementInfo } from '../../scene/types/EditableDashboardElement';
|
||||
import { VariableHideSelect } from '../../settings/variables/components/VariableHideSelect';
|
||||
import { VariableDisplaySelect } from '../../settings/variables/components/VariableDisplaySelect';
|
||||
import { getEditableVariableDefinition, validateVariableName } from '../../settings/variables/utils';
|
||||
|
||||
import { useVariableSelectionOptionsCategory } from './useVariableSelectionOptionsCategory';
|
||||
@@ -25,7 +25,7 @@ function useEditPaneOptions(this: VariableEditableElement, isNewElement: boolean
|
||||
const variableNameId = useId();
|
||||
const labelId = useId();
|
||||
const descriptionId = useId();
|
||||
const variableHideId = useId();
|
||||
const variableDisplayId = useId();
|
||||
|
||||
if (variable instanceof LocalValueVariable) {
|
||||
return useLocalVariableOptions(variable);
|
||||
@@ -59,12 +59,12 @@ function useEditPaneOptions(this: VariableEditableElement, isNewElement: boolean
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: '',
|
||||
id: variableHideId,
|
||||
id: variableDisplayId,
|
||||
skipField: true,
|
||||
render: () => <VariableHideInput variable={variable} />,
|
||||
render: () => <VariableDisplayInput variable={variable} />,
|
||||
})
|
||||
);
|
||||
}, [variableOptionsCategoryId, variableNameId, labelId, descriptionId, variableHideId, variable, isNewElement]);
|
||||
}, [variableOptionsCategoryId, variableNameId, labelId, descriptionId, variableDisplayId, variable, isNewElement]);
|
||||
|
||||
const categories = [basicOptions];
|
||||
const typeCategory = useVariableTypeCategory(variable);
|
||||
@@ -248,18 +248,18 @@ function VariableDescriptionTextArea({ variable, id }: VariableInputProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function VariableHideInput({ variable }: VariableInputProps) {
|
||||
const { hide = VariableHide.dontHide } = variable.useState();
|
||||
function VariableDisplayInput({ variable }: VariableInputProps) {
|
||||
const { hide: display = VariableHide.dontHide } = variable.useState();
|
||||
|
||||
const onChange = (option: VariableHide) => {
|
||||
dashboardEditActions.changeVariableHideValue({
|
||||
source: variable,
|
||||
oldValue: hide,
|
||||
oldValue: display,
|
||||
newValue: option,
|
||||
});
|
||||
};
|
||||
|
||||
return <VariableHideSelect hide={hide} type={variable.state.type} onChange={onChange} />;
|
||||
return <VariableDisplaySelect display={display} type={variable.state.type} onChange={onChange} />;
|
||||
}
|
||||
|
||||
function useVariableTypeCategory(variable: SceneVariable) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { reportInteraction } from '@grafana/runtime';
|
||||
import { SceneVariable } from '@grafana/scenes';
|
||||
import { VariableHide, defaultVariableModel } from '@grafana/schema';
|
||||
import { Button, LoadingPlaceholder, ConfirmModal, ModalsController, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { VariableHideSelect } from 'app/features/dashboard-scene/settings/variables/components/VariableHideSelect';
|
||||
import { VariableDisplaySelect } from 'app/features/dashboard-scene/settings/variables/components/VariableDisplaySelect';
|
||||
import { VariableLegend } from 'app/features/dashboard-scene/settings/variables/components/VariableLegend';
|
||||
import { VariableTextAreaField } from 'app/features/dashboard-scene/settings/variables/components/VariableTextAreaField';
|
||||
import { VariableTextField } from 'app/features/dashboard-scene/settings/variables/components/VariableTextField';
|
||||
@@ -35,7 +35,7 @@ interface VariableEditorFormProps {
|
||||
export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete }: VariableEditorFormProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [nameError, setNameError] = useState<string>();
|
||||
const { name, type, label, description, hide } = variable.useState();
|
||||
const { name, type, label, description, hide: display } = variable.useState();
|
||||
const EditorToRender = isEditableVariableType(type) ? getVariableEditor(type) : undefined;
|
||||
const [runQueryState, onRunQuery] = useAsyncFn(async () => {
|
||||
await lastValueFrom(variable.validateAndUpdate!());
|
||||
@@ -65,7 +65,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
|
||||
const onLabelBlur = (e: FormEvent<HTMLInputElement>) => variable.setState({ label: e.currentTarget.value });
|
||||
const onDescriptionBlur = (e: FormEvent<HTMLTextAreaElement>) =>
|
||||
variable.setState({ description: e.currentTarget.value });
|
||||
const onHideChange = (hide: VariableHide) => variable.setState({ hide });
|
||||
const onDisplayChange = (display: VariableHide) => variable.setState({ hide: display });
|
||||
|
||||
const isHasVariableOptions = hasVariableOptions(variable);
|
||||
|
||||
@@ -119,7 +119,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
|
||||
width={52}
|
||||
/>
|
||||
|
||||
<VariableHideSelect onChange={onHideChange} hide={hide || defaultVariableModel.hide!} type={type} />
|
||||
<VariableDisplaySelect onChange={onDisplayChange} display={display || defaultVariableModel.hide!} type={type} />
|
||||
|
||||
{EditorToRender && <EditorToRender variable={variable} onRunQuery={onRunQuery} />}
|
||||
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { VariableHide } from '@grafana/data';
|
||||
|
||||
import { VariableDisplaySelect } from './VariableDisplaySelect';
|
||||
|
||||
// For testing combobox
|
||||
beforeAll(() => {
|
||||
const mockGetBoundingClientRect = jest.fn(() => ({
|
||||
width: 120,
|
||||
height: 120,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
}));
|
||||
|
||||
Object.defineProperty(Element.prototype, 'getBoundingClientRect', {
|
||||
value: mockGetBoundingClientRect,
|
||||
});
|
||||
});
|
||||
|
||||
describe('VariableDisplaySelect', () => {
|
||||
it('should render all options when opening the combobox', async () => {
|
||||
const onChange = jest.fn();
|
||||
const user = userEvent.setup();
|
||||
render(<VariableDisplaySelect onChange={onChange} display={VariableHide.dontHide} type="query" />);
|
||||
|
||||
const combobox = screen.getByRole('combobox');
|
||||
await user.click(combobox);
|
||||
|
||||
expect(await screen.findByText('Above dashboard')).toBeInTheDocument();
|
||||
expect(screen.getByText('Above dashboard, label hidden')).toBeInTheDocument();
|
||||
expect(screen.getByText('Controls menu')).toBeInTheDocument();
|
||||
expect(screen.getByText('Hidden')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onChange() with the selected value', async () => {
|
||||
const onChange = jest.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<VariableDisplaySelect onChange={onChange} display={VariableHide.dontHide} type="query" />);
|
||||
|
||||
const combobox = screen.getByRole('combobox');
|
||||
await user.click(combobox);
|
||||
|
||||
const controlsMenuOption = await screen.findByText('Controls menu');
|
||||
await user.click(controlsMenuOption);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(VariableHide.inControlsMenu);
|
||||
});
|
||||
|
||||
it('should have "Controls menu" selected when `hide` is set to "inControlsMenu"', () => {
|
||||
const onChange = jest.fn();
|
||||
render(<VariableDisplaySelect onChange={onChange} display={VariableHide.inControlsMenu} type="query" />);
|
||||
|
||||
expect(screen.getByDisplayValue('Controls menu')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render anything for constant type variables', () => {
|
||||
const onChange = jest.fn();
|
||||
const { container } = render(
|
||||
<VariableDisplaySelect onChange={onChange} display={VariableHide.dontHide} type="constant" />
|
||||
);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should call onChange() when switching from "Controls menu" to "Above dashboard"', async () => {
|
||||
const onChange = jest.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<VariableDisplaySelect onChange={onChange} display={VariableHide.inControlsMenu} type="query" />);
|
||||
|
||||
const combobox = screen.getByRole('combobox');
|
||||
await user.click(combobox);
|
||||
|
||||
const aboveDashboardOption = await screen.findByText('Above dashboard');
|
||||
await user.click(aboveDashboardOption);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(VariableHide.dontHide);
|
||||
});
|
||||
});
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { PropsWithChildren, useMemo } from 'react';
|
||||
|
||||
import { VariableType, VariableHide } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { Combobox, Field } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
onChange: (option: VariableHide) => void;
|
||||
display: VariableHide;
|
||||
type: VariableType;
|
||||
minWidth?: number;
|
||||
}
|
||||
|
||||
export function VariableDisplaySelect({ onChange, display, type, minWidth = 52 }: PropsWithChildren<Props>) {
|
||||
const OPTIONS = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: VariableHide.dontHide,
|
||||
label: t('dashboard-scene.variable-display-select.options.above-dashboard.label', 'Above dashboard'),
|
||||
},
|
||||
{
|
||||
value: VariableHide.hideLabel,
|
||||
label: t('dashboard-scene.variable-display-select.options.hidden-label.label', 'Above dashboard, label hidden'),
|
||||
description: t(
|
||||
'dashboard-scene.variable-display-select.options.hidden-label.description',
|
||||
'Above the dashboard, but without showing the name of variable'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: VariableHide.inControlsMenu,
|
||||
label: t('dashboard-scene.variable-display-select.options.controls-menu.label', 'Controls menu'),
|
||||
description: t(
|
||||
'dashboard-scene.variable-display-select.options.controls-menu.description',
|
||||
'Visible when the controls menu is open'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: VariableHide.hideVariable,
|
||||
label: t('dashboard-scene.variable-display-select.options.hidden.label', 'Hidden'),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
const value = useMemo(() => OPTIONS.find((o) => o.value === display)?.value ?? OPTIONS[0].value, [display, OPTIONS]);
|
||||
|
||||
// Constant variables don't support display options
|
||||
if (type === 'constant') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
<Field label={t('dashboard-scene.variable-display-select.label', 'Display')}>
|
||||
<Combobox
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalDisplaySelect}
|
||||
options={OPTIONS}
|
||||
onChange={(option) => option && onChange(option.value)}
|
||||
value={value}
|
||||
width="auto"
|
||||
minWidth={minWidth}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
+1
@@ -38,6 +38,7 @@ export function VariableHideSelect({ onChange, hide, type }: PropsWithChildren<P
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
<Field label={t('dashboard-scene.variable-hide-select.label', 'Hide')}>
|
||||
<RadioButtonGroup options={HIDE_OPTIONS} onChange={onChange} value={value} />
|
||||
</Field>
|
||||
|
||||
@@ -5824,14 +5824,26 @@
|
||||
},
|
||||
"annotation-settings-edit": {
|
||||
"back-to-list": "Back to list",
|
||||
"control-display-options": {
|
||||
"above-dashboard": {
|
||||
"label": "Above dashboard"
|
||||
},
|
||||
"controls-menu": {
|
||||
"description": "Can be accessed when the controls menu is open",
|
||||
"label": "Controls menu"
|
||||
},
|
||||
"hidden": {
|
||||
"description": "Hides the toggle for turning this annotation on or off",
|
||||
"label": "Hidden"
|
||||
}
|
||||
},
|
||||
"delete": "Delete",
|
||||
"description-color-annotation-event-markers": "Color to use for the annotation event markers",
|
||||
"description-enabled-annotation-query-issued-every-dashboard": "When enabled the annotation query is issued every dashboard refresh",
|
||||
"description-hidden": "Annotation queries can be toggled on or off at the top of the dashboard. With this option checked this toggle will be hidden.",
|
||||
"label-annotation-controls-display": "Show annotation controls in",
|
||||
"label-color": "Color",
|
||||
"label-data-source": "Data source",
|
||||
"label-enabled": "Enabled",
|
||||
"label-hidden": "Hidden",
|
||||
"label-name": "Name",
|
||||
"label-show-in": "Show in",
|
||||
"placeholder-choose-panels": "Choose panels",
|
||||
@@ -5887,6 +5899,7 @@
|
||||
"label-open-link-in-new-tab": "Open link in new tab",
|
||||
"label-options": "Options",
|
||||
"label-show-as-dropdown": "Show as dropdown",
|
||||
"label-show-in-controls-menu": "Show in controls menu",
|
||||
"label-title": "Title",
|
||||
"label-tooltip": "Tooltip",
|
||||
"label-type": "Type",
|
||||
@@ -6448,6 +6461,25 @@
|
||||
"variable-controls": {
|
||||
"add-variable": "Add variable"
|
||||
},
|
||||
"variable-display-select": {
|
||||
"label": "Display",
|
||||
"options": {
|
||||
"above-dashboard": {
|
||||
"label": "Above dashboard"
|
||||
},
|
||||
"controls-menu": {
|
||||
"description": "Visible when the controls menu is open",
|
||||
"label": "Controls menu"
|
||||
},
|
||||
"hidden": {
|
||||
"label": "Hidden"
|
||||
},
|
||||
"hidden-label": {
|
||||
"description": "Above the dashboard, but without showing the name of variable",
|
||||
"label": "Above dashboard, label hidden"
|
||||
}
|
||||
}
|
||||
},
|
||||
"variable-editor-form": {
|
||||
"aria-label-variable-editor-form": "Variable editor form",
|
||||
"back-to-list": "Back to list",
|
||||
|
||||
Reference in New Issue
Block a user