mirror of
https://github.com/grafana/grafana.git
synced 2025-12-21 12:04:45 +08:00
Compare commits
5 Commits
repo-retur
...
viz-change
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
817c3cc6c1 | ||
|
|
52205fbf4f | ||
|
|
321e60e69c | ||
|
|
c6dda2dfc3 | ||
|
|
e03f7fe878 |
@@ -0,0 +1,59 @@
|
||||
import { test, expect } from '@grafana/plugin-e2e';
|
||||
|
||||
test.use({
|
||||
featureToggles: {
|
||||
kubernetesDashboards: true,
|
||||
dashboardNewLayouts: true,
|
||||
},
|
||||
});
|
||||
|
||||
test.describe('Dashboard keybindings with new layouts', { tag: ['@dashboards'] }, () => {
|
||||
test.use({
|
||||
viewport: { width: 1280, height: 1080 },
|
||||
});
|
||||
|
||||
test('should collapse and expand all rows', async ({ gotoDashboardPage, page, selectors }) => {
|
||||
const dashboardPage = await gotoDashboardPage({ uid: 'Repeating-rows-uid/repeating-rows' });
|
||||
|
||||
const panelContents = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.content);
|
||||
await expect(panelContents).toHaveCount(5);
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('server = A, pod = Bob'))
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('server = B, pod = Bob'))
|
||||
).toBeVisible();
|
||||
|
||||
// Collapse all rows using keyboard shortcut: d + Shift+C
|
||||
await page.keyboard.press('d');
|
||||
await page.keyboard.press('Shift+C');
|
||||
|
||||
await expect(panelContents).toHaveCount(0);
|
||||
await expect(page.getByText('server = A, pod = Bob')).toBeHidden();
|
||||
await expect(page.getByText('server = B, pod = Bob')).toBeHidden();
|
||||
|
||||
// Expand all rows using keyboard shortcut: d + Shift+E
|
||||
await page.keyboard.press('d');
|
||||
await page.keyboard.press('Shift+E');
|
||||
|
||||
await expect(panelContents).toHaveCount(6);
|
||||
await expect(page.getByText('server = A, pod = Bob')).toBeVisible();
|
||||
await expect(page.getByText('server = B, pod = Bob')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should open panel inspect', async ({ gotoDashboardPage, page, selectors }) => {
|
||||
const dashboardPage = await gotoDashboardPage({ uid: 'edediimbjhdz4b/a-tall-dashboard' });
|
||||
|
||||
// Find Panel #1 and press 'i' to open inspector
|
||||
const panel1 = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Panel #1'));
|
||||
await expect(panel1).toBeVisible();
|
||||
await panel1.press('i');
|
||||
|
||||
await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelInspector.Json.content)).toBeVisible();
|
||||
|
||||
// Press Escape to close inspector
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await expect(page.getByTestId(selectors.components.PanelInspector.Json.content)).toBeHidden();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { debounce } from 'lodash';
|
||||
import { useCallback, useId, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useId, useMemo, useState } from 'react';
|
||||
import { useSessionStorage } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2, PanelData } from '@grafana/data';
|
||||
@@ -45,6 +45,13 @@ export function PanelVizTypePicker({ panel, data, onChange, onClose, showBackBut
|
||||
const styles = useStyles2(getStyles);
|
||||
const panelModel = useMemo(() => new PanelModelCompatibilityWrapper(panel), [panel]);
|
||||
const filterId = useId();
|
||||
const [searchInputRef, setSearchInputRef] = useState<HTMLInputElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchInputRef) {
|
||||
searchInputRef.focus();
|
||||
}
|
||||
}, [searchInputRef]);
|
||||
|
||||
/** SEARCH */
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -124,6 +131,9 @@ export function PanelVizTypePicker({ panel, data, onChange, onClose, showBackBut
|
||||
)}
|
||||
<FilterInput
|
||||
id={filterId}
|
||||
ref={(ref) => {
|
||||
setSearchInputRef(ref);
|
||||
}}
|
||||
className={styles.filter}
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
|
||||
@@ -30,8 +30,10 @@ import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||
import { DashboardEventAction } from 'app/features/live/dashboard/types';
|
||||
import { VariablesChanged } from 'app/features/variables/types';
|
||||
import { ShowConfirmModalEvent } from 'app/types/events';
|
||||
|
||||
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||
import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer';
|
||||
import { createWorker } from '../saving/createDetectChangesWorker';
|
||||
import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
|
||||
@@ -39,6 +41,7 @@ import { historySrv } from '../settings/version-history/HistorySrv';
|
||||
import { getCloneKey } from '../utils/clone';
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { findVizPanelByKey, getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils';
|
||||
import * as utils from '../utils/utils';
|
||||
|
||||
import { DashboardControls } from './DashboardControls';
|
||||
import { DashboardScene, DashboardSceneState } from './DashboardScene';
|
||||
@@ -162,6 +165,38 @@ describe('DashboardScene', () => {
|
||||
expect(scene.state.meta.version).toEqual(2);
|
||||
});
|
||||
|
||||
it('Should exit edit mode after saving from unsaved changes modal when dashboardNewLayouts is enabled', () => {
|
||||
const originalFeatureToggle = config.featureToggles.dashboardNewLayouts;
|
||||
config.featureToggles.dashboardNewLayouts = true;
|
||||
|
||||
const publishSpy = jest.spyOn(appEvents, 'publish');
|
||||
const hasActualSaveChangesSpy = jest.spyOn(utils, 'hasActualSaveChanges').mockReturnValue(true);
|
||||
|
||||
scene.setState({ title: 'Updated title' });
|
||||
expect(scene.state.isDirty).toBe(true);
|
||||
scene.exitEditMode({ skipConfirm: false });
|
||||
|
||||
const modalCall = publishSpy.mock.calls.find((call) => call[0] instanceof ShowConfirmModalEvent);
|
||||
expect(modalCall).toBeDefined();
|
||||
|
||||
const modalEvent = modalCall![0] as ShowConfirmModalEvent;
|
||||
expect(modalEvent.payload.altActionText).toBeDefined();
|
||||
|
||||
modalEvent.payload.onAltAction?.();
|
||||
|
||||
expect(scene.state.overlay).toBeDefined();
|
||||
|
||||
const overlay = scene.state.overlay as SaveDashboardDrawer;
|
||||
expect(overlay.state.onSaveSuccess).toBeDefined();
|
||||
|
||||
overlay.state.onSaveSuccess!();
|
||||
expect(scene.state.isEditing).toBe(false);
|
||||
|
||||
publishSpy.mockRestore();
|
||||
hasActualSaveChangesSpy.mockRestore();
|
||||
config.featureToggles.dashboardNewLayouts = originalFeatureToggle;
|
||||
});
|
||||
|
||||
it('Should start the detect changes worker', () => {
|
||||
expect(worker.onmessage).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -335,7 +335,11 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
|
||||
yesText: t('dashboard-scene.dashboard-scene.modal.discard', 'Discard'),
|
||||
yesButtonVariant: 'destructive',
|
||||
onAltAction: () => {
|
||||
this.openSaveDrawer({});
|
||||
this.openSaveDrawer({
|
||||
onSaveSuccess: () => {
|
||||
this.exitEditModeConfirmed(false);
|
||||
},
|
||||
});
|
||||
},
|
||||
onConfirm: () => {
|
||||
this.exitEditModeConfirmed();
|
||||
|
||||
@@ -18,6 +18,7 @@ import { getPanelIdForVizPanel } from '../utils/utils';
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { onRemovePanel, toggleVizPanelLegend } from './PanelMenuBehavior';
|
||||
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
|
||||
import { RowsLayoutManager } from './layout-rows/RowsLayoutManager';
|
||||
|
||||
export function setupKeyboardShortcuts(scene: DashboardScene) {
|
||||
const keybindings = new KeybindingSet();
|
||||
@@ -265,6 +266,8 @@ export function setupKeyboardShortcuts(scene: DashboardScene) {
|
||||
onTrigger: () => {
|
||||
if (scene.state.body instanceof DefaultGridLayoutManager) {
|
||||
scene.state.body.collapseAllRows();
|
||||
} else if (scene.state.body instanceof RowsLayoutManager) {
|
||||
scene.state.body.collapseAllRows();
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -275,6 +278,8 @@ export function setupKeyboardShortcuts(scene: DashboardScene) {
|
||||
onTrigger: () => {
|
||||
if (scene.state.body instanceof DefaultGridLayoutManager) {
|
||||
scene.state.body.expandAllRows();
|
||||
} else if (scene.state.body instanceof RowsLayoutManager) {
|
||||
scene.state.body.expandAllRows();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -107,7 +107,7 @@ function RowRepeatSelect({ row, dashboard, id }: { row: SceneGridRow; dashboard:
|
||||
<>
|
||||
<RepeatRowSelect2
|
||||
id={id}
|
||||
sceneContext={dashboard}
|
||||
sceneContext={row}
|
||||
repeat={repeatBehavior?.state.variableName}
|
||||
onChange={(repeat) => {
|
||||
if (repeat) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Icon, useStyles2 } from '@grafana/ui';
|
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants';
|
||||
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
|
||||
|
||||
import { getQueryRunnerFor, useDashboard, useDashboardState } from '../../../utils/utils';
|
||||
import { getQueryRunnerFor, useDashboardState } from '../../../utils/utils';
|
||||
import { DashboardGridItem } from '../DashboardGridItem';
|
||||
import { RowRepeaterBehavior } from '../RowRepeaterBehavior';
|
||||
|
||||
@@ -18,7 +18,6 @@ import { RowOptionsButton } from './RowOptionsButton';
|
||||
export function RowActionsRenderer({ model }: SceneComponentProps<RowActions>) {
|
||||
const row = model.getParent();
|
||||
const { title, children } = row.useState();
|
||||
const dashboard = useDashboard(model);
|
||||
const { meta, isEditing } = useDashboardState(model);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
@@ -53,7 +52,7 @@ export function RowActionsRenderer({ model }: SceneComponentProps<RowActions>) {
|
||||
<RowOptionsButton
|
||||
title={title}
|
||||
repeat={behaviour instanceof RowRepeaterBehavior ? behaviour.state.variableName : undefined}
|
||||
parent={dashboard}
|
||||
parent={row}
|
||||
onUpdate={(title, repeat) => model.onUpdate(title, repeat)}
|
||||
isUsingDashboardDS={isUsingDashboardDS}
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useId, useMemo, useRef } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Alert, Input, Switch, TextLink, Field } from '@grafana/ui';
|
||||
import { Alert, Field, Input, Switch, TextLink } from '@grafana/ui';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect';
|
||||
@@ -11,7 +11,7 @@ import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSou
|
||||
|
||||
import { useConditionalRenderingEditor } from '../../conditional-rendering/hooks/useConditionalRenderingEditor';
|
||||
import { dashboardEditActions } from '../../edit-pane/shared';
|
||||
import { getQueryRunnerFor, useDashboard } from '../../utils/utils';
|
||||
import { getQueryRunnerFor } from '../../utils/utils';
|
||||
import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector';
|
||||
import { generateUniqueTitle, useEditPaneInputAutoFocus } from '../layouts-shared/utils';
|
||||
|
||||
@@ -128,7 +128,6 @@ function FillScreenSwitch({ row, id }: { row: RowItem; id?: string }) {
|
||||
|
||||
function RowRepeatSelect({ row, id }: { row: RowItem; id?: string }) {
|
||||
const { layout } = row.useState();
|
||||
const dashboard = useDashboard(row);
|
||||
|
||||
const isAnyPanelUsingDashboardDS = layout.getVizPanels().some((vizPanel) => {
|
||||
const runner = getQueryRunnerFor(vizPanel);
|
||||
@@ -143,7 +142,7 @@ function RowRepeatSelect({ row, id }: { row: RowItem; id?: string }) {
|
||||
<>
|
||||
<RepeatRowSelect2
|
||||
id={id}
|
||||
sceneContext={dashboard}
|
||||
sceneContext={row}
|
||||
repeat={row.state.repeatByVariable}
|
||||
onChange={(repeat) => row.onChangeRepeat(repeat)}
|
||||
/>
|
||||
|
||||
@@ -390,4 +390,30 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
|
||||
|
||||
return duplicateTitles;
|
||||
}
|
||||
|
||||
public collapseAllRows() {
|
||||
this.state.rows.forEach((row) => {
|
||||
if (!row.getCollapsedState()) {
|
||||
row.setCollapsedState(true);
|
||||
}
|
||||
row.state.repeatedRows?.forEach((repeatedRow) => {
|
||||
if (!repeatedRow.getCollapsedState()) {
|
||||
repeatedRow.setCollapsedState(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public expandAllRows() {
|
||||
this.state.rows.forEach((row) => {
|
||||
if (row.getCollapsedState()) {
|
||||
row.setCollapsedState(false);
|
||||
}
|
||||
row.state.repeatedRows?.forEach((repeatedRow) => {
|
||||
if (repeatedRow.getCollapsedState()) {
|
||||
repeatedRow.setCollapsedState(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo, useRef } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Alert, Input, Field, TextLink } from '@grafana/ui';
|
||||
import { Alert, Field, Input, TextLink } from '@grafana/ui';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect';
|
||||
@@ -11,7 +11,7 @@ import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSou
|
||||
|
||||
import { useConditionalRenderingEditor } from '../../conditional-rendering/hooks/useConditionalRenderingEditor';
|
||||
import { dashboardEditActions } from '../../edit-pane/shared';
|
||||
import { getQueryRunnerFor, useDashboard } from '../../utils/utils';
|
||||
import { getQueryRunnerFor } from '../../utils/utils';
|
||||
import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector';
|
||||
import { generateUniqueTitle, useEditPaneInputAutoFocus } from '../layouts-shared/utils';
|
||||
|
||||
@@ -99,7 +99,6 @@ function TabTitleInput({ tab, isNewElement, id }: { tab: TabItem; isNewElement:
|
||||
|
||||
function TabRepeatSelect({ tab, id }: { tab: TabItem; id?: string }) {
|
||||
const { layout } = tab.useState();
|
||||
const dashboard = useDashboard(tab);
|
||||
|
||||
const isAnyPanelUsingDashboardDS = layout.getVizPanels().some((vizPanel) => {
|
||||
const runner = getQueryRunnerFor(vizPanel);
|
||||
@@ -114,7 +113,7 @@ function TabRepeatSelect({ tab, id }: { tab: TabItem; id?: string }) {
|
||||
<>
|
||||
<RepeatRowSelect2
|
||||
id={id}
|
||||
sceneContext={dashboard}
|
||||
sceneContext={tab}
|
||||
repeat={tab.state.repeatByVariable}
|
||||
onChange={(repeat) => tab.onChangeRepeat(repeat)}
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { FormEvent, useCallback } from 'react';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { CustomVariable, SceneVariable } from '@grafana/scenes';
|
||||
import { CustomVariable } from '@grafana/scenes';
|
||||
|
||||
import { OptionsPaneItemDescriptor } from '../../../../../dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
import { CustomVariableForm } from '../../components/CustomVariableForm';
|
||||
|
||||
import { PaneItem } from './PaneItem';
|
||||
|
||||
interface CustomVariableEditorProps {
|
||||
variable: CustomVariable;
|
||||
onRunQuery: () => void;
|
||||
@@ -67,17 +63,3 @@ export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEdi
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneItemDescriptor[] {
|
||||
if (!(variable instanceof CustomVariable)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.custom-options.values', 'Values separated by comma'),
|
||||
id: 'custom-variable-values',
|
||||
render: ({ props }) => <PaneItem id={props.id} variable={variable} />,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { t } from '@grafana/i18n';
|
||||
import { CustomVariable, SceneVariable } from '@grafana/scenes';
|
||||
|
||||
import { OptionsPaneItemDescriptor } from '../../../../../dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
@@ -12,7 +11,6 @@ export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneIt
|
||||
|
||||
return [
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.edit-pane.variable.custom-options.values', 'Values separated by comma'),
|
||||
id: 'custom-variable-values',
|
||||
render: ({ props }) => <PaneItem id={props.id} variable={variable} />,
|
||||
}),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { SceneObject, sceneGraph } from '@grafana/scenes';
|
||||
import { LocalValueVariable, SceneObject, sceneGraph } from '@grafana/scenes';
|
||||
import { Combobox, ComboboxOption, Select } from '@grafana/ui';
|
||||
import { useSelector } from 'app/types/store';
|
||||
|
||||
@@ -59,10 +59,18 @@ export const RepeatRowSelect2 = ({ sceneContext, repeat, id, onChange }: Props2)
|
||||
const variables = sceneVars.useState().variables;
|
||||
|
||||
const variableOptions = useMemo(() => {
|
||||
const options: ComboboxOption[] = variables.map((item) => ({
|
||||
label: item.state.name,
|
||||
value: item.state.name,
|
||||
}));
|
||||
const options: ComboboxOption[] = variables
|
||||
.filter((item) => {
|
||||
if (sceneContext.parent) {
|
||||
// filter out local value variables (which are only set on repeated items)
|
||||
return !(sceneGraph.lookupVariable(item.state.name, sceneContext.parent) instanceof LocalValueVariable);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((item) => ({
|
||||
label: item.state.name,
|
||||
value: item.state.name,
|
||||
}));
|
||||
|
||||
options.unshift({
|
||||
label: t('dashboard.repeat-row-select2.variable-options.label.disable-repeating', 'Disable repeating'),
|
||||
@@ -70,7 +78,7 @@ export const RepeatRowSelect2 = ({ sceneContext, repeat, id, onChange }: Props2)
|
||||
});
|
||||
|
||||
return options;
|
||||
}, [variables]);
|
||||
}, [sceneContext, variables]);
|
||||
|
||||
const onSelectChange = useCallback((value: ComboboxOption | null) => value && onChange(value.value), [onChange]);
|
||||
|
||||
@@ -79,7 +87,7 @@ export const RepeatRowSelect2 = ({ sceneContext, repeat, id, onChange }: Props2)
|
||||
return (
|
||||
<Combobox
|
||||
id={id}
|
||||
value={repeat}
|
||||
value={repeat || ''}
|
||||
onChange={onSelectChange}
|
||||
options={variableOptions}
|
||||
disabled={isDisabled}
|
||||
|
||||
@@ -4821,8 +4821,7 @@
|
||||
"apply": "Apply",
|
||||
"change-value": "Change variable value",
|
||||
"discard": "Discard",
|
||||
"modal-title": "Custom Variable",
|
||||
"values": "Values separated by comma"
|
||||
"modal-title": "Custom Variable"
|
||||
},
|
||||
"datasource-options": {
|
||||
"name-filter": "Name filter",
|
||||
|
||||
Reference in New Issue
Block a user