mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardLinksSettings: Move them to Scenes (#79998)
This commit is contained in:
parent
313c43749c
commit
07778cb221
@ -2416,7 +2416,8 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
|
||||||
|
[0, 0, 0, "Unexpected any. Specify a different type.", "9"]
|
||||||
],
|
],
|
||||||
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts:5381": [
|
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
@ -2551,10 +2552,6 @@ exports[`better eslint`] = {
|
|||||||
"public/app/features/dashboard/components/Inspector/PanelInspector.tsx:5381": [
|
"public/app/features/dashboard/components/Inspector/PanelInspector.tsx:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/features/dashboard/components/LinksSettings/LinkSettingsList.tsx:5381": [
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
|
||||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
|
||||||
],
|
|
||||||
"public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx:5381": [
|
"public/app/features/dashboard/components/PanelEditor/DynamicConfigValueEditor.tsx:5381": [
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
[0, 0, 0, "Styles should be written using objects.", "1"]
|
||||||
|
@ -5,13 +5,13 @@ import { selectors } from '@grafana/e2e-selectors';
|
|||||||
import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
|
import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
|
||||||
import { DashboardLink } from '@grafana/schema';
|
import { DashboardLink } from '@grafana/schema';
|
||||||
import { Tooltip } from '@grafana/ui';
|
import { Tooltip } from '@grafana/ui';
|
||||||
import { linkIconMap } from 'app/features/dashboard/components/LinksSettings/LinkSettingsEdit';
|
|
||||||
import {
|
import {
|
||||||
DashboardLinkButton,
|
DashboardLinkButton,
|
||||||
DashboardLinksDashboard,
|
DashboardLinksDashboard,
|
||||||
} from 'app/features/dashboard/components/SubMenu/DashboardLinksDashboard';
|
} from 'app/features/dashboard/components/SubMenu/DashboardLinksDashboard';
|
||||||
import { getLinkSrv } from 'app/features/panel/panellinks/link_srv';
|
import { getLinkSrv } from 'app/features/panel/panellinks/link_srv';
|
||||||
|
|
||||||
|
import { LINK_ICON_MAP } from '../settings/links/utils';
|
||||||
import { getDashboardSceneFor } from '../utils/utils';
|
import { getDashboardSceneFor } from '../utils/utils';
|
||||||
|
|
||||||
interface DashboardLinksControlsState extends SceneObjectState {}
|
interface DashboardLinksControlsState extends SceneObjectState {}
|
||||||
@ -37,7 +37,7 @@ function DashboardLinksControlsRenderer({ model }: SceneComponentProps<Dashboard
|
|||||||
return <DashboardLinksDashboard key={key} link={link} linkInfo={linkInfo} dashboardUID={uid} />;
|
return <DashboardLinksDashboard key={key} link={link} linkInfo={linkInfo} dashboardUID={uid} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const icon = linkIconMap[link.icon];
|
const icon = LINK_ICON_MAP[link.icon];
|
||||||
|
|
||||||
const linkElement = (
|
const linkElement = (
|
||||||
<DashboardLinkButton
|
<DashboardLinkButton
|
||||||
|
@ -61,6 +61,7 @@ describe('DashboardScene', () => {
|
|||||||
${'description'} | ${'new description'}
|
${'description'} | ${'new description'}
|
||||||
${'tags'} | ${['tag3', 'tag4']}
|
${'tags'} | ${['tag3', 'tag4']}
|
||||||
${'editable'} | ${false}
|
${'editable'} | ${false}
|
||||||
|
${'links'} | ${[]}
|
||||||
`(
|
`(
|
||||||
'A change to $prop should set isDirty true',
|
'A change to $prop should set isDirty true',
|
||||||
({ prop, value }: { prop: keyof DashboardSceneState; value: any }) => {
|
({ prop, value }: { prop: keyof DashboardSceneState; value: any }) => {
|
||||||
|
@ -38,7 +38,7 @@ import { DashboardSceneUrlSync } from './DashboardSceneUrlSync';
|
|||||||
import { ViewPanelScene } from './ViewPanelScene';
|
import { ViewPanelScene } from './ViewPanelScene';
|
||||||
import { setupKeyboardShortcuts } from './keyboardShortcuts';
|
import { setupKeyboardShortcuts } from './keyboardShortcuts';
|
||||||
|
|
||||||
export const PERSISTED_PROPS = ['title', 'description', 'tags', 'editable', 'graphTooltip'];
|
export const PERSISTED_PROPS = ['title', 'description', 'tags', 'editable', 'graphTooltip', 'links'];
|
||||||
|
|
||||||
export interface DashboardSceneState extends SceneObjectState {
|
export interface DashboardSceneState extends SceneObjectState {
|
||||||
/** The title */
|
/** The title */
|
||||||
@ -48,7 +48,7 @@ export interface DashboardSceneState extends SceneObjectState {
|
|||||||
/** Tags */
|
/** Tags */
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
/** Links */
|
/** Links */
|
||||||
links?: DashboardLink[];
|
links: DashboardLink[];
|
||||||
/** Is editable */
|
/** Is editable */
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
/** A uid when saved */
|
/** A uid when saved */
|
||||||
@ -109,6 +109,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
|||||||
meta: {},
|
meta: {},
|
||||||
editable: true,
|
editable: true,
|
||||||
body: state.body ?? new SceneFlexLayout({ children: [] }),
|
body: state.body ?? new SceneFlexLayout({ children: [] }),
|
||||||
|
links: state.links ?? [],
|
||||||
...state,
|
...state,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -335,7 +335,20 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
|
|||||||
"fiscalYearStartMonth": 1,
|
"fiscalYearStartMonth": 1,
|
||||||
"graphTooltip": 0,
|
"graphTooltip": 0,
|
||||||
"id": 1351,
|
"id": 1351,
|
||||||
"links": [],
|
"links": [
|
||||||
|
{
|
||||||
|
"asDropdown": false,
|
||||||
|
"icon": "external link",
|
||||||
|
"includeVars": false,
|
||||||
|
"keepTime": false,
|
||||||
|
"tags": [],
|
||||||
|
"targetBlank": false,
|
||||||
|
"title": "Link 1",
|
||||||
|
"tooltip": "",
|
||||||
|
"type": "dashboards",
|
||||||
|
"url": "",
|
||||||
|
},
|
||||||
|
],
|
||||||
"panels": [
|
"panels": [
|
||||||
{
|
{
|
||||||
"datasource": {
|
"datasource": {
|
||||||
|
@ -45,6 +45,7 @@ import { DashboardControls } from '../scene/DashboardControls';
|
|||||||
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
|
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
|
||||||
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||||
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||||
|
import { NEW_LINK } from '../settings/links/utils';
|
||||||
import { getQueryRunnerFor } from '../utils/utils';
|
import { getQueryRunnerFor } from '../utils/utils';
|
||||||
|
|
||||||
import dashboard_to_load1 from './testfiles/dashboard_to_load1.json';
|
import dashboard_to_load1 from './testfiles/dashboard_to_load1.json';
|
||||||
@ -71,6 +72,7 @@ describe('transformSaveModelToScene', () => {
|
|||||||
...defaultTimePickerConfig,
|
...defaultTimePickerConfig,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
|
links: [{ ...NEW_LINK, title: 'Link 1' }],
|
||||||
templating: {
|
templating: {
|
||||||
list: [
|
list: [
|
||||||
{
|
{
|
||||||
@ -110,6 +112,8 @@ describe('transformSaveModelToScene', () => {
|
|||||||
|
|
||||||
expect(scene.state.title).toBe('test');
|
expect(scene.state.title).toBe('test');
|
||||||
expect(scene.state.uid).toBe('test-uid');
|
expect(scene.state.uid).toBe('test-uid');
|
||||||
|
expect(scene.state.links).toHaveLength(1);
|
||||||
|
expect(scene.state.links![0].title).toBe('Link 1');
|
||||||
expect(scene.state?.$timeRange?.state.value.raw).toEqual(dash.time);
|
expect(scene.state?.$timeRange?.state.value.raw).toEqual(dash.time);
|
||||||
expect(scene.state?.$timeRange?.state.fiscalYearStartMonth).toEqual(2);
|
expect(scene.state?.$timeRange?.state.fiscalYearStartMonth).toEqual(2);
|
||||||
expect(scene.state?.$timeRange?.state.timeZone).toEqual('America/New_York');
|
expect(scene.state?.$timeRange?.state.timeZone).toEqual('America/New_York');
|
||||||
|
@ -31,6 +31,7 @@ import { reduceTransformRegistryItem } from 'app/features/transformers/editors/R
|
|||||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||||
|
|
||||||
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||||
|
import { NEW_LINK } from '../settings/links/utils';
|
||||||
import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-utils';
|
import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-utils';
|
||||||
import { getVizPanelKeyForPanelId } from '../utils/utils';
|
import { getVizPanelKeyForPanelId } from '../utils/utils';
|
||||||
|
|
||||||
@ -185,6 +186,7 @@ describe('transformSceneToSaveModel', () => {
|
|||||||
time_options: ['5m', '15m', '30m'],
|
time_options: ['5m', '15m', '30m'],
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
|
links: [{ ...NEW_LINK, title: 'Link 1' }],
|
||||||
};
|
};
|
||||||
const scene = transformSaveModelToScene({ dashboard: dashboardWithCustomSettings as any, meta: {} });
|
const scene = transformSaveModelToScene({ dashboard: dashboardWithCustomSettings as any, meta: {} });
|
||||||
const saveModel = transformSceneToSaveModel(scene);
|
const saveModel = transformSceneToSaveModel(scene);
|
||||||
@ -817,15 +819,14 @@ describe('transformSceneToSaveModel', () => {
|
|||||||
expect(result.panels?.[0].gridPos).toEqual({ w: 24, x: 0, y: 0, h: 20 });
|
expect(result.panels?.[0].gridPos).toEqual({ w: 24, x: 0, y: 0, h: 20 });
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Uncomment when we support links
|
it('should remove links', async () => {
|
||||||
// it('should remove links', async () => {
|
const scene = transformSaveModelToScene({ dashboard: snapshotableDashboardJson as any, meta: {} });
|
||||||
// const scene = transformSaveModelToScene({ dashboard: snapshotableDashboardJson as any, meta: {} });
|
activateFullSceneTree(scene);
|
||||||
// activateFullSceneTree(scene);
|
const snapshot = transformSceneToSaveModel(scene, true);
|
||||||
// const snapshot = transformSceneToSaveModel(scene, true);
|
expect(snapshot.links?.length).toBe(1);
|
||||||
// expect(snapshot.links?.length).toBe(1);
|
const result = trimDashboardForSnapshot('Snap title', getTimeRange({ from: 'now-6h', to: 'now' }), snapshot);
|
||||||
// const result = trimDashboardForSnapshot('Snap title', getTimeRange({ from: 'now-6h', to: 'now' }), snapshot);
|
expect(result.links?.length).toBe(0);
|
||||||
// expect(result.links?.length).toBe(0);
|
});
|
||||||
// });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -138,6 +138,7 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
|
|||||||
fiscalYearStartMonth: timeRange.fiscalYearStartMonth,
|
fiscalYearStartMonth: timeRange.fiscalYearStartMonth,
|
||||||
weekStart: timeRange.weekStart,
|
weekStart: timeRange.weekStart,
|
||||||
tags: state.tags,
|
tags: state.tags,
|
||||||
|
links: state.links,
|
||||||
graphTooltip,
|
graphTooltip,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,268 @@
|
|||||||
|
import { render as RTLRender } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
|
|
||||||
|
import {
|
||||||
|
behaviors,
|
||||||
|
SceneGridLayout,
|
||||||
|
SceneGridItem,
|
||||||
|
SceneRefreshPicker,
|
||||||
|
SceneTimeRange,
|
||||||
|
SceneTimePicker,
|
||||||
|
} from '@grafana/scenes';
|
||||||
|
import { DashboardCursorSync } from '@grafana/schema';
|
||||||
|
|
||||||
|
import { DashboardControls } from '../scene/DashboardControls';
|
||||||
|
import { DashboardLinksControls } from '../scene/DashboardLinksControls';
|
||||||
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
|
import { activateFullSceneTree } from '../utils/test-utils';
|
||||||
|
|
||||||
|
import { DashboardLinksEditView } from './DashboardLinksEditView';
|
||||||
|
import { NEW_LINK } from './links/utils';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useLocation: jest.fn().mockReturnValue({
|
||||||
|
pathname: '/d/dash-1/settings/links',
|
||||||
|
search: '',
|
||||||
|
hash: '',
|
||||||
|
state: null,
|
||||||
|
key: '5nvxpbdafa',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function render(component: React.ReactNode) {
|
||||||
|
return RTLRender(<TestProvider>{component}</TestProvider>);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('DashboardLinksEditView', () => {
|
||||||
|
describe('Url state', () => {
|
||||||
|
let settings: DashboardLinksEditView;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const result = await buildTestScene();
|
||||||
|
settings = result.settings;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct urlKey', () => {
|
||||||
|
expect(settings.getUrlKey()).toBe('links');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Dashboard updates', () => {
|
||||||
|
let dashboard: DashboardScene;
|
||||||
|
let settings: DashboardLinksEditView;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const result = await buildTestScene();
|
||||||
|
dashboard = result.dashboard;
|
||||||
|
settings = result.settings;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have isDirty false', () => {
|
||||||
|
expect(dashboard.state.isDirty).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update dashboard state when adding a link', () => {
|
||||||
|
settings.onNewLink();
|
||||||
|
|
||||||
|
expect(dashboard.state.links[0]).toEqual(NEW_LINK);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update dashboard state when deleting a link', () => {
|
||||||
|
dashboard.setState({ links: [NEW_LINK] });
|
||||||
|
settings.onDelete(0);
|
||||||
|
|
||||||
|
expect(dashboard.state.links).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update dashboard state when duplicating a link', () => {
|
||||||
|
dashboard.setState({ links: [NEW_LINK] });
|
||||||
|
settings.onDuplicate(NEW_LINK);
|
||||||
|
|
||||||
|
expect(dashboard.state.links).toEqual([NEW_LINK, NEW_LINK]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update dashboard state when reordering a link', () => {
|
||||||
|
dashboard.setState({
|
||||||
|
links: [
|
||||||
|
{ ...NEW_LINK, title: 'link-1' },
|
||||||
|
{ ...NEW_LINK, title: 'link-2' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
settings.onOrderChange(0, 1);
|
||||||
|
|
||||||
|
expect(dashboard.state.links).toEqual([
|
||||||
|
{ ...NEW_LINK, title: 'link-2' },
|
||||||
|
{ ...NEW_LINK, title: 'link-1' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update dashboard state when editing a link', () => {
|
||||||
|
dashboard.setState({ links: [{ ...NEW_LINK, title: 'old title' }] });
|
||||||
|
settings.setState({ editIndex: 0 });
|
||||||
|
settings.onUpdateLink({ ...NEW_LINK, title: 'new title' });
|
||||||
|
|
||||||
|
expect(dashboard.state.links[0].title).toEqual('new title');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edit a link', () => {
|
||||||
|
let dashboard: DashboardScene;
|
||||||
|
let settings: DashboardLinksEditView;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const result = await buildTestScene();
|
||||||
|
dashboard = result.dashboard;
|
||||||
|
settings = result.settings;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set editIndex when editing a link', () => {
|
||||||
|
dashboard.setState({ links: [{ ...NEW_LINK, title: 'old title' }] });
|
||||||
|
settings.onEdit(0);
|
||||||
|
|
||||||
|
expect(settings.state.editIndex).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set editIndex when editing a link that does not exist', () => {
|
||||||
|
dashboard.setState({ links: [{ ...NEW_LINK, title: 'old title' }] });
|
||||||
|
settings.onEdit(1);
|
||||||
|
|
||||||
|
expect(settings.state.editIndex).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update dashboard state when editing a link', () => {
|
||||||
|
dashboard.setState({ links: [{ ...NEW_LINK, title: 'old title' }] });
|
||||||
|
settings.setState({ editIndex: 0 });
|
||||||
|
settings.onUpdateLink({ ...NEW_LINK, title: 'new title' });
|
||||||
|
|
||||||
|
expect(dashboard.state.links[0].title).toEqual('new title');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update dashboard state when going back', () => {
|
||||||
|
settings.setState({ editIndex: 0 });
|
||||||
|
settings.onGoBack();
|
||||||
|
|
||||||
|
expect(settings.state.editIndex).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Render the views', () => {
|
||||||
|
let dashboard: DashboardScene;
|
||||||
|
let settings: DashboardLinksEditView;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const result = await buildTestScene();
|
||||||
|
dashboard = result.dashboard;
|
||||||
|
settings = result.settings;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render with no errors', () => {
|
||||||
|
expect(() => render(<settings.Component model={settings} />)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the empty state when no links', () => {
|
||||||
|
dashboard.setState({ links: [] });
|
||||||
|
const { getByText } = render(<settings.Component model={settings} />);
|
||||||
|
|
||||||
|
expect(getByText('Add dashboard link')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the empty state when no links', () => {
|
||||||
|
dashboard.setState({ links: [] });
|
||||||
|
const { getByText } = render(<settings.Component model={settings} />);
|
||||||
|
|
||||||
|
expect(getByText('Add dashboard link')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the list of link when there are links', () => {
|
||||||
|
dashboard.setState({
|
||||||
|
links: [
|
||||||
|
{ ...NEW_LINK, title: 'link-1' },
|
||||||
|
{ ...NEW_LINK, title: 'link-2' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const { getByText } = render(<settings.Component model={settings} />);
|
||||||
|
|
||||||
|
expect(getByText('link-1')).toBeInTheDocument();
|
||||||
|
expect(getByText('link-2')).toBeInTheDocument();
|
||||||
|
expect(getByText('New link')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the list of link when the editing link does not exist', () => {
|
||||||
|
dashboard.setState({
|
||||||
|
links: [
|
||||||
|
{ ...NEW_LINK, title: 'link-1' },
|
||||||
|
{ ...NEW_LINK, title: 'link-2' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
settings.setState({ editIndex: 2 });
|
||||||
|
const { getByText } = render(<settings.Component model={settings} />);
|
||||||
|
|
||||||
|
expect(getByText('link-1')).toBeInTheDocument();
|
||||||
|
expect(getByText('link-2')).toBeInTheDocument();
|
||||||
|
expect(getByText('New link')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the link form when the editing link does exist', () => {
|
||||||
|
dashboard.setState({
|
||||||
|
links: [
|
||||||
|
{ ...NEW_LINK, title: 'link-1' },
|
||||||
|
{ ...NEW_LINK, title: 'link-2' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
settings.setState({ editIndex: 1 });
|
||||||
|
const { getByText } = render(<settings.Component model={settings} />);
|
||||||
|
|
||||||
|
expect(getByText('Edit link')).toBeInTheDocument();
|
||||||
|
expect(getByText('Apply')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function buildTestScene() {
|
||||||
|
const settings = new DashboardLinksEditView({});
|
||||||
|
const dashboard = new DashboardScene({
|
||||||
|
$timeRange: new SceneTimeRange({}),
|
||||||
|
$behaviors: [new behaviors.CursorSync({ sync: DashboardCursorSync.Off })],
|
||||||
|
controls: [
|
||||||
|
new DashboardControls({
|
||||||
|
variableControls: [],
|
||||||
|
linkControls: new DashboardLinksControls({}),
|
||||||
|
timeControls: [
|
||||||
|
new SceneTimePicker({}),
|
||||||
|
new SceneRefreshPicker({
|
||||||
|
intervals: ['1s'],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
title: 'hello',
|
||||||
|
uid: 'dash-1',
|
||||||
|
meta: {
|
||||||
|
canEdit: true,
|
||||||
|
},
|
||||||
|
body: new SceneGridLayout({
|
||||||
|
children: [
|
||||||
|
new SceneGridItem({
|
||||||
|
key: 'griditem-1',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 10,
|
||||||
|
height: 12,
|
||||||
|
body: undefined,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
editview: settings,
|
||||||
|
});
|
||||||
|
|
||||||
|
activateFullSceneTree(dashboard);
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 1));
|
||||||
|
|
||||||
|
dashboard.onEnterEditMode();
|
||||||
|
settings.activate();
|
||||||
|
|
||||||
|
return { dashboard, settings };
|
||||||
|
}
|
@ -1,14 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
|
import { NavModel, NavModelItem, PageLayoutType, arrayUtils } from '@grafana/data';
|
||||||
import { locationService } from '@grafana/runtime';
|
|
||||||
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
|
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
|
||||||
import { DashboardLink } from '@grafana/schema';
|
import { DashboardLink } from '@grafana/schema';
|
||||||
import { Link } from '@grafana/ui';
|
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
||||||
|
import { DashboardLinkForm } from '../settings/links/DashboardLinkForm';
|
||||||
|
import { DashboardLinkList } from '../settings/links/DashboardLinkList';
|
||||||
|
import { NEW_LINK } from '../settings/links/utils';
|
||||||
import { getDashboardSceneFor } from '../utils/utils';
|
import { getDashboardSceneFor } from '../utils/utils';
|
||||||
|
|
||||||
import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync';
|
import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync';
|
||||||
@ -24,49 +25,106 @@ export class DashboardLinksEditView extends SceneObjectBase<DashboardLinksEditVi
|
|||||||
public getUrlKey(): string {
|
public getUrlKey(): string {
|
||||||
return 'links';
|
return 'links';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get dashboard(): DashboardScene {
|
||||||
|
return getDashboardSceneFor(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get links(): DashboardLink[] {
|
||||||
|
return this.dashboard.state.links;
|
||||||
|
}
|
||||||
|
|
||||||
|
private set links(links: DashboardLink[]) {
|
||||||
|
this.dashboard.setState({ links });
|
||||||
|
}
|
||||||
|
|
||||||
|
public onNewLink = () => {
|
||||||
|
this.links = [...this.links, NEW_LINK];
|
||||||
|
this.setState({ editIndex: this.links.length - 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
public onDelete = (idx: number) => {
|
||||||
|
this.links = [...this.links.slice(0, idx), ...this.links.slice(idx + 1)];
|
||||||
|
|
||||||
|
this.setState({ editIndex: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
public onDuplicate = (link: DashboardLink) => {
|
||||||
|
this.links = [...this.links, { ...link }];
|
||||||
|
};
|
||||||
|
|
||||||
|
public onOrderChange = (idx: number, direction: number) => {
|
||||||
|
this.links = arrayUtils.moveItemImmutably(this.links, idx, idx + direction);
|
||||||
|
};
|
||||||
|
|
||||||
|
public onEdit = (editIndex: number) => {
|
||||||
|
this.setState({ editIndex });
|
||||||
|
};
|
||||||
|
|
||||||
|
public onUpdateLink = (link: DashboardLink) => {
|
||||||
|
const idx = this.state.editIndex;
|
||||||
|
|
||||||
|
if (idx === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.links = [...this.links.slice(0, idx), link, ...this.links.slice(idx + 1)];
|
||||||
|
};
|
||||||
|
|
||||||
|
public onGoBack = () => {
|
||||||
|
this.setState({ editIndex: undefined });
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function DashboardLinksEditViewRenderer({ model }: SceneComponentProps<DashboardLinksEditView>) {
|
function DashboardLinksEditViewRenderer({ model }: SceneComponentProps<DashboardLinksEditView>) {
|
||||||
const { editIndex } = model.useState();
|
const { editIndex } = model.useState();
|
||||||
const dashboard = getDashboardSceneFor(model);
|
const dashboard = getDashboardSceneFor(model);
|
||||||
const links = dashboard.state.links || [];
|
const { links, overlay } = dashboard.useState();
|
||||||
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
||||||
|
const linkToEdit = editIndex !== undefined ? links[editIndex] : undefined;
|
||||||
|
|
||||||
if (editIndex !== undefined) {
|
if (linkToEdit) {
|
||||||
const link = links[editIndex];
|
return (
|
||||||
if (link) {
|
<EditLinkView
|
||||||
return <EditLinkView pageNav={pageNav} navModel={navModel} link={link} dashboard={dashboard} />;
|
pageNav={pageNav}
|
||||||
}
|
navModel={navModel}
|
||||||
|
link={linkToEdit}
|
||||||
|
dashboard={dashboard}
|
||||||
|
onChange={model.onUpdateLink}
|
||||||
|
onGoBack={model.onGoBack}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
|
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
|
||||||
<NavToolbarActions dashboard={dashboard} />
|
<NavToolbarActions dashboard={dashboard} />
|
||||||
{links.map((link, i) => (
|
<DashboardLinkList
|
||||||
<Link
|
links={links}
|
||||||
key={`${link.title}-${i}`}
|
onNew={model.onNewLink}
|
||||||
onClick={(e) => {
|
onEdit={model.onEdit}
|
||||||
e.preventDefault();
|
onDelete={model.onDelete}
|
||||||
locationService.partial({ editIndex: i });
|
onDuplicate={model.onDuplicate}
|
||||||
}}
|
onOrderChange={model.onOrderChange}
|
||||||
>
|
/>
|
||||||
{link.title}
|
{overlay && <overlay.Component model={overlay} />}
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditLinkViewProps {
|
interface EditLinkViewProps {
|
||||||
link: DashboardLink;
|
link?: DashboardLink;
|
||||||
pageNav: NavModelItem;
|
pageNav: NavModelItem;
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
dashboard: DashboardScene;
|
dashboard: DashboardScene;
|
||||||
|
onChange: (link: DashboardLink) => void;
|
||||||
|
onGoBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditLinkView({ pageNav, link, navModel, dashboard }: EditLinkViewProps) {
|
function EditLinkView({ pageNav, link, navModel, dashboard, onChange, onGoBack }: EditLinkViewProps) {
|
||||||
const parentTab = pageNav.children!.find((p) => p.active)!;
|
const parentTab = pageNav.children!.find((p) => p.active)!;
|
||||||
parentTab.parentItem = pageNav;
|
parentTab.parentItem = pageNav;
|
||||||
|
const { overlay } = dashboard.useState();
|
||||||
|
|
||||||
const editLinkPageNav = {
|
const editLinkPageNav = {
|
||||||
text: 'Edit link',
|
text: 'Edit link',
|
||||||
@ -76,7 +134,8 @@ function EditLinkView({ pageNav, link, navModel, dashboard }: EditLinkViewProps)
|
|||||||
return (
|
return (
|
||||||
<Page navModel={navModel} pageNav={editLinkPageNav} layout={PageLayoutType.Standard}>
|
<Page navModel={navModel} pageNav={editLinkPageNav} layout={PageLayoutType.Standard}>
|
||||||
<NavToolbarActions dashboard={dashboard} />
|
<NavToolbarActions dashboard={dashboard} />
|
||||||
{JSON.stringify(link)}
|
<DashboardLinkForm link={link!} onUpdate={onChange} onGoBack={onGoBack} />
|
||||||
|
{overlay && <overlay.Component model={overlay} />}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,107 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
import { DashboardLink } from '@grafana/schema';
|
||||||
|
import { CollapsableSection, TagsInput, Select, Field, Input, Checkbox, Button } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { LINK_ICON_MAP, NEW_LINK } from './utils';
|
||||||
|
|
||||||
|
const linkTypeOptions = [
|
||||||
|
{ value: 'dashboards', label: 'Dashboards' },
|
||||||
|
{ value: 'link', label: 'Link' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const linkIconOptions = Object.keys(LINK_ICON_MAP).map((key) => ({ label: key, value: key }));
|
||||||
|
|
||||||
|
interface DashboardLinkFormProps {
|
||||||
|
link: DashboardLink;
|
||||||
|
onUpdate: (link: DashboardLink) => void;
|
||||||
|
onGoBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DashboardLinkForm({ link, onUpdate, onGoBack }: DashboardLinkFormProps) {
|
||||||
|
const onTagsChange = (tags: string[]) => {
|
||||||
|
onUpdate({ ...link, tags: tags });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTypeChange = (selectedItem: SelectableValue) => {
|
||||||
|
const update = { ...link, type: selectedItem.value };
|
||||||
|
|
||||||
|
// clear props that are no longe revant for this type
|
||||||
|
if (update.type === 'dashboards') {
|
||||||
|
update.url = '';
|
||||||
|
update.tooltip = '';
|
||||||
|
} else {
|
||||||
|
update.tags = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate(update);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onIconChange = (selectedItem: SelectableValue) => {
|
||||||
|
onUpdate({ ...link, icon: selectedItem.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChange = (ev: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
const target = ev.currentTarget;
|
||||||
|
onUpdate({
|
||||||
|
...link,
|
||||||
|
[target.name]: target.type === 'checkbox' ? target.checked : target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isNew = link.title === NEW_LINK.title;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: '600px' }}>
|
||||||
|
<Field label="Title">
|
||||||
|
<Input name="title" id="title" value={link.title} onChange={onChange} autoFocus={isNew} />
|
||||||
|
</Field>
|
||||||
|
<Field label="Type">
|
||||||
|
<Select inputId="link-type-input" value={link.type} options={linkTypeOptions} onChange={onTypeChange} />
|
||||||
|
</Field>
|
||||||
|
{link.type === 'dashboards' && (
|
||||||
|
<>
|
||||||
|
<Field label="With tags">
|
||||||
|
<TagsInput tags={link.tags} onChange={onTagsChange} />
|
||||||
|
</Field>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{link.type === 'link' && (
|
||||||
|
<>
|
||||||
|
<Field label="URL">
|
||||||
|
<Input name="url" value={link.url} onChange={onChange} />
|
||||||
|
</Field>
|
||||||
|
<Field label="Tooltip">
|
||||||
|
<Input name="tooltip" value={link.tooltip} onChange={onChange} placeholder="Open dashboard" />
|
||||||
|
</Field>
|
||||||
|
<Field label="Icon">
|
||||||
|
<Select value={link.icon} options={linkIconOptions} onChange={onIconChange} />
|
||||||
|
</Field>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<CollapsableSection label="Options" isOpen={true}>
|
||||||
|
{link.type === 'dashboards' && (
|
||||||
|
<Field>
|
||||||
|
<Checkbox label="Show as dropdown" name="asDropdown" value={link.asDropdown} onChange={onChange} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
<Field>
|
||||||
|
<Checkbox label="Include current time range" name="keepTime" value={link.keepTime} onChange={onChange} />
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<Checkbox
|
||||||
|
label="Include current template variable values"
|
||||||
|
name="includeVars"
|
||||||
|
value={link.includeVars}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<Checkbox label="Open link in new tab" name="targetBlank" value={link.targetBlank} onChange={onChange} />
|
||||||
|
</Field>
|
||||||
|
</CollapsableSection>
|
||||||
|
<Button onClick={onGoBack}>Apply</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { DashboardLink } from '@grafana/schema';
|
||||||
|
import { DeleteButton, HorizontalGroup, Icon, IconButton, TagList, useStyles2 } from '@grafana/ui';
|
||||||
|
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||||
|
|
||||||
|
import { ListNewButton } from '../../../dashboard/components/DashboardSettings/ListNewButton';
|
||||||
|
|
||||||
|
interface DashboardLinkListProps {
|
||||||
|
links: DashboardLink[];
|
||||||
|
onNew: () => void;
|
||||||
|
onEdit: (idx: number) => void;
|
||||||
|
onDuplicate: (link: DashboardLink) => void;
|
||||||
|
onDelete: (idx: number) => void;
|
||||||
|
onOrderChange: (idx: number, direction: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DashboardLinkList({
|
||||||
|
links,
|
||||||
|
onNew,
|
||||||
|
onOrderChange,
|
||||||
|
onEdit,
|
||||||
|
onDuplicate,
|
||||||
|
onDelete,
|
||||||
|
}: DashboardLinkListProps) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const isEmptyList = links.length === 0;
|
||||||
|
|
||||||
|
if (isEmptyList) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<EmptyListCTA
|
||||||
|
onClick={onNew}
|
||||||
|
title="There are no dashboard links added yet"
|
||||||
|
buttonIcon="link"
|
||||||
|
buttonTitle="Add dashboard link"
|
||||||
|
infoBoxTitle="What are dashboard links?"
|
||||||
|
infoBox={{
|
||||||
|
__html:
|
||||||
|
'<p>Dashboard Links allow you to place links to other dashboards and web sites directly below the dashboard header.</p>',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<table role="grid" className="filter-table filter-table--hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Info</th>
|
||||||
|
<th colSpan={3} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{links.map((link, idx) => (
|
||||||
|
<tr key={`${link.title}-${idx}`}>
|
||||||
|
<td role="gridcell" className="pointer" onClick={() => onEdit(idx)}>
|
||||||
|
<Icon name="external-link-alt" /> {link.type}
|
||||||
|
</td>
|
||||||
|
<td role="gridcell">
|
||||||
|
<HorizontalGroup>
|
||||||
|
{link.title && <span className={styles.titleWrapper}>{link.title}</span>}
|
||||||
|
{link.type === 'link' && <span className={styles.urlWrapper}>{link.url}</span>}
|
||||||
|
{link.type === 'dashboards' && <TagList tags={link.tags ?? []} />}
|
||||||
|
</HorizontalGroup>
|
||||||
|
</td>
|
||||||
|
<td style={{ width: '1%' }} role="gridcell">
|
||||||
|
{idx !== 0 && (
|
||||||
|
<IconButton name="arrow-up" onClick={() => onOrderChange(idx, -1)} tooltip="Move link up" />
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td style={{ width: '1%' }} role="gridcell">
|
||||||
|
{links.length > 1 && idx !== links.length - 1 ? (
|
||||||
|
<IconButton name="arrow-down" onClick={() => onOrderChange(idx, 1)} tooltip="Move link down" />
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
<td style={{ width: '1%' }} role="gridcell">
|
||||||
|
<IconButton name="copy" onClick={() => onDuplicate(link)} tooltip="Copy link" />
|
||||||
|
</td>
|
||||||
|
<td style={{ width: '1%' }} role="gridcell">
|
||||||
|
<DeleteButton
|
||||||
|
aria-label={`Delete link with title "${link.title}"`}
|
||||||
|
size="sm"
|
||||||
|
onConfirm={() => onDelete(idx)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<ListNewButton onClick={onNew}>New link</ListNewButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = () => ({
|
||||||
|
titleWrapper: css({
|
||||||
|
width: '20vw',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}),
|
||||||
|
urlWrapper: css({
|
||||||
|
width: '40vw',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}),
|
||||||
|
});
|
25
public/app/features/dashboard-scene/settings/links/utils.ts
Normal file
25
public/app/features/dashboard-scene/settings/links/utils.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { IconName } from '@grafana/data';
|
||||||
|
import { DashboardLink } from '@grafana/schema';
|
||||||
|
|
||||||
|
export const NEW_LINK: DashboardLink = {
|
||||||
|
icon: 'external link',
|
||||||
|
title: 'New link',
|
||||||
|
tooltip: '',
|
||||||
|
type: 'dashboards',
|
||||||
|
url: '',
|
||||||
|
asDropdown: false,
|
||||||
|
tags: [],
|
||||||
|
targetBlank: false,
|
||||||
|
keepTime: false,
|
||||||
|
includeVars: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LINK_ICON_MAP: Record<string, IconName | undefined> = {
|
||||||
|
'external link': 'external-link-alt',
|
||||||
|
dashboard: 'apps',
|
||||||
|
question: 'question-circle',
|
||||||
|
info: 'info-circle',
|
||||||
|
bolt: 'bolt',
|
||||||
|
doc: 'file-alt',
|
||||||
|
cloud: 'cloud',
|
||||||
|
};
|
@ -16,6 +16,7 @@ import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
|||||||
import { DashboardControls } from '../scene/DashboardControls';
|
import { DashboardControls } from '../scene/DashboardControls';
|
||||||
import { DashboardLinksControls } from '../scene/DashboardLinksControls';
|
import { DashboardLinksControls } from '../scene/DashboardLinksControls';
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
|
import { NEW_LINK } from '../settings/links/utils';
|
||||||
|
|
||||||
import { DashboardModelCompatibilityWrapper } from './DashboardModelCompatibilityWrapper';
|
import { DashboardModelCompatibilityWrapper } from './DashboardModelCompatibilityWrapper';
|
||||||
|
|
||||||
@ -29,6 +30,7 @@ describe('DashboardModelCompatibilityWrapper', () => {
|
|||||||
expect(wrapper.editable).toBe(false);
|
expect(wrapper.editable).toBe(false);
|
||||||
expect(wrapper.graphTooltip).toBe(DashboardCursorSync.Off);
|
expect(wrapper.graphTooltip).toBe(DashboardCursorSync.Off);
|
||||||
expect(wrapper.tags).toEqual(['hello-tag']);
|
expect(wrapper.tags).toEqual(['hello-tag']);
|
||||||
|
expect(wrapper.links).toEqual([NEW_LINK]);
|
||||||
expect(wrapper.time.from).toBe('now-6h');
|
expect(wrapper.time.from).toBe('now-6h');
|
||||||
expect(wrapper.timezone).toBe('America/New_York');
|
expect(wrapper.timezone).toBe('America/New_York');
|
||||||
expect(wrapper.weekStart).toBe('friday');
|
expect(wrapper.weekStart).toBe('friday');
|
||||||
@ -109,6 +111,7 @@ function setup() {
|
|||||||
title: 'hello',
|
title: 'hello',
|
||||||
description: 'hello description',
|
description: 'hello description',
|
||||||
tags: ['hello-tag'],
|
tags: ['hello-tag'],
|
||||||
|
links: [NEW_LINK],
|
||||||
uid: 'dash-1',
|
uid: 'dash-1',
|
||||||
editable: false,
|
editable: false,
|
||||||
$timeRange: new SceneTimeRange({
|
$timeRange: new SceneTimeRange({
|
||||||
|
@ -82,6 +82,10 @@ export class DashboardModelCompatibilityWrapper {
|
|||||||
return this._scene.state.tags;
|
return this._scene.state.tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get links() {
|
||||||
|
return this._scene.state.links;
|
||||||
|
}
|
||||||
|
|
||||||
public get meta() {
|
public get meta() {
|
||||||
return this._scene.state.meta;
|
return this._scene.state.meta;
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,9 @@ import React, { useState } from 'react';
|
|||||||
import { NavModelItem } from '@grafana/data';
|
import { NavModelItem } from '@grafana/data';
|
||||||
import { config, locationService } from '@grafana/runtime';
|
import { config, locationService } from '@grafana/runtime';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
import { NEW_LINK } from 'app/features/dashboard-scene/settings/links/utils';
|
||||||
|
|
||||||
import { LinkSettingsEdit, LinkSettingsList } from '../LinksSettings';
|
import { LinkSettingsEdit, LinkSettingsList } from '../LinksSettings';
|
||||||
import { newLink } from '../LinksSettings/LinkSettingsEdit';
|
|
||||||
|
|
||||||
import { SettingsPageProps } from './types';
|
import { SettingsPageProps } from './types';
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ export function LinksSettings({ dashboard, sectionNav, editIndex }: SettingsPage
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onNew = () => {
|
const onNew = () => {
|
||||||
dashboard.links = [...dashboard.links, { ...newLink }];
|
dashboard.links = [...dashboard.links, { ...NEW_LINK }];
|
||||||
setIsNew(true);
|
setIsNew(true);
|
||||||
locationService.partial({ editIndex: dashboard.links.length - 1 });
|
locationService.partial({ editIndex: dashboard.links.length - 1 });
|
||||||
};
|
};
|
||||||
|
@ -1,41 +1,11 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
|
||||||
import { DashboardLink } from '@grafana/schema';
|
import { DashboardLink } from '@grafana/schema';
|
||||||
import { CollapsableSection, TagsInput, Select, Field, Input, Checkbox, Button, IconName } from '@grafana/ui';
|
import { DashboardLinkForm } from 'app/features/dashboard-scene/settings/links/DashboardLinkForm';
|
||||||
|
import { NEW_LINK } from 'app/features/dashboard-scene/settings/links/utils';
|
||||||
|
|
||||||
import { DashboardModel } from '../../state/DashboardModel';
|
import { DashboardModel } from '../../state/DashboardModel';
|
||||||
|
|
||||||
export const newLink: DashboardLink = {
|
|
||||||
icon: 'external link',
|
|
||||||
title: 'New link',
|
|
||||||
tooltip: '',
|
|
||||||
type: 'dashboards',
|
|
||||||
url: '',
|
|
||||||
asDropdown: false,
|
|
||||||
tags: [],
|
|
||||||
targetBlank: false,
|
|
||||||
keepTime: false,
|
|
||||||
includeVars: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const linkTypeOptions = [
|
|
||||||
{ value: 'dashboards', label: 'Dashboards' },
|
|
||||||
{ value: 'link', label: 'Link' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const linkIconMap: Record<string, IconName | undefined> = {
|
|
||||||
'external link': 'external-link-alt',
|
|
||||||
dashboard: 'apps',
|
|
||||||
question: 'question-circle',
|
|
||||||
info: 'info-circle',
|
|
||||||
bolt: 'bolt',
|
|
||||||
doc: 'file-alt',
|
|
||||||
cloud: 'cloud',
|
|
||||||
};
|
|
||||||
|
|
||||||
const linkIconOptions = Object.keys(linkIconMap).map((key) => ({ label: key, value: key }));
|
|
||||||
|
|
||||||
type LinkSettingsEditProps = {
|
type LinkSettingsEditProps = {
|
||||||
editLinkIdx: number;
|
editLinkIdx: number;
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
@ -43,7 +13,7 @@ type LinkSettingsEditProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const LinkSettingsEdit = ({ editLinkIdx, dashboard, onGoBack }: LinkSettingsEditProps) => {
|
export const LinkSettingsEdit = ({ editLinkIdx, dashboard, onGoBack }: LinkSettingsEditProps) => {
|
||||||
const [linkSettings, setLinkSettings] = useState(editLinkIdx !== null ? dashboard.links[editLinkIdx] : newLink);
|
const [linkSettings, setLinkSettings] = useState(editLinkIdx !== null ? dashboard.links[editLinkIdx] : NEW_LINK);
|
||||||
|
|
||||||
const onUpdate = (link: DashboardLink) => {
|
const onUpdate = (link: DashboardLink) => {
|
||||||
const links = [...dashboard.links];
|
const links = [...dashboard.links];
|
||||||
@ -52,98 +22,5 @@ export const LinkSettingsEdit = ({ editLinkIdx, dashboard, onGoBack }: LinkSetti
|
|||||||
setLinkSettings(link);
|
setLinkSettings(link);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTagsChange = (tags: string[]) => {
|
return <DashboardLinkForm link={linkSettings} onUpdate={onUpdate} onGoBack={onGoBack} />;
|
||||||
onUpdate({ ...linkSettings, tags: tags });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTypeChange = (selectedItem: SelectableValue) => {
|
|
||||||
const update = { ...linkSettings, type: selectedItem.value };
|
|
||||||
|
|
||||||
// clear props that are no longe revant for this type
|
|
||||||
if (update.type === 'dashboards') {
|
|
||||||
update.url = '';
|
|
||||||
update.tooltip = '';
|
|
||||||
} else {
|
|
||||||
update.tags = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdate(update);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onIconChange = (selectedItem: SelectableValue) => {
|
|
||||||
onUpdate({ ...linkSettings, icon: selectedItem.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onChange = (ev: React.FocusEvent<HTMLInputElement>) => {
|
|
||||||
const target = ev.currentTarget;
|
|
||||||
onUpdate({
|
|
||||||
...linkSettings,
|
|
||||||
[target.name]: target.type === 'checkbox' ? target.checked : target.value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const isNew = linkSettings.title === newLink.title;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ maxWidth: '600px' }}>
|
|
||||||
<Field label="Title">
|
|
||||||
<Input name="title" id="title" value={linkSettings.title} onChange={onChange} autoFocus={isNew} />
|
|
||||||
</Field>
|
|
||||||
<Field label="Type">
|
|
||||||
<Select inputId="link-type-input" value={linkSettings.type} options={linkTypeOptions} onChange={onTypeChange} />
|
|
||||||
</Field>
|
|
||||||
{linkSettings.type === 'dashboards' && (
|
|
||||||
<>
|
|
||||||
<Field label="With tags">
|
|
||||||
<TagsInput tags={linkSettings.tags} onChange={onTagsChange} />
|
|
||||||
</Field>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{linkSettings.type === 'link' && (
|
|
||||||
<>
|
|
||||||
<Field label="URL">
|
|
||||||
<Input name="url" value={linkSettings.url} onChange={onChange} />
|
|
||||||
</Field>
|
|
||||||
<Field label="Tooltip">
|
|
||||||
<Input name="tooltip" value={linkSettings.tooltip} onChange={onChange} placeholder="Open dashboard" />
|
|
||||||
</Field>
|
|
||||||
<Field label="Icon">
|
|
||||||
<Select value={linkSettings.icon} options={linkIconOptions} onChange={onIconChange} />
|
|
||||||
</Field>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<CollapsableSection label="Options" isOpen={true}>
|
|
||||||
{linkSettings.type === 'dashboards' && (
|
|
||||||
<Field>
|
|
||||||
<Checkbox label="Show as dropdown" name="asDropdown" value={linkSettings.asDropdown} onChange={onChange} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
<Field>
|
|
||||||
<Checkbox
|
|
||||||
label="Include current time range"
|
|
||||||
name="keepTime"
|
|
||||||
value={linkSettings.keepTime}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<Checkbox
|
|
||||||
label="Include current template variable values"
|
|
||||||
name="includeVars"
|
|
||||||
value={linkSettings.includeVars}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
<Checkbox
|
|
||||||
label="Open link in new tab"
|
|
||||||
name="targetBlank"
|
|
||||||
value={linkSettings.targetBlank}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</CollapsableSection>
|
|
||||||
<Button onClick={onGoBack}>Apply</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import { css } from '@emotion/css';
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { arrayUtils } from '@grafana/data';
|
import { arrayUtils } from '@grafana/data';
|
||||||
import { DashboardLink } from '@grafana/schema';
|
import { DashboardLink } from '@grafana/schema';
|
||||||
import { DeleteButton, HorizontalGroup, Icon, IconButton, TagList, useStyles2 } from '@grafana/ui';
|
import { DashboardLinkList } from 'app/features/dashboard-scene/settings/links/DashboardLinkList';
|
||||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
|
||||||
|
|
||||||
import { DashboardModel } from '../../state/DashboardModel';
|
import { DashboardModel } from '../../state/DashboardModel';
|
||||||
import { ListNewButton } from '../DashboardSettings/ListNewButton';
|
|
||||||
|
|
||||||
type LinkSettingsListProps = {
|
type LinkSettingsListProps = {
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
@ -15,9 +12,11 @@ type LinkSettingsListProps = {
|
|||||||
onEdit: (idx: number) => void;
|
onEdit: (idx: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used in DashboardSettings to display the list of links.
|
||||||
|
* It updates the DashboardModel instance when links are added, edited, duplicated or deleted.
|
||||||
|
*/
|
||||||
export const LinkSettingsList = ({ dashboard, onNew, onEdit }: LinkSettingsListProps) => {
|
export const LinkSettingsList = ({ dashboard, onNew, onEdit }: LinkSettingsListProps) => {
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
|
|
||||||
const [links, setLinks] = useState(dashboard.links);
|
const [links, setLinks] = useState(dashboard.links);
|
||||||
|
|
||||||
const moveLink = (idx: number, direction: number) => {
|
const moveLink = (idx: number, direction: number) => {
|
||||||
@ -25,7 +24,7 @@ export const LinkSettingsList = ({ dashboard, onNew, onEdit }: LinkSettingsListP
|
|||||||
setLinks(dashboard.links);
|
setLinks(dashboard.links);
|
||||||
};
|
};
|
||||||
|
|
||||||
const duplicateLink = (link: DashboardLink, idx: number) => {
|
const duplicateLink = (link: DashboardLink) => {
|
||||||
dashboard.links = [...links, { ...link }];
|
dashboard.links = [...links, { ...link }];
|
||||||
setLinks(dashboard.links);
|
setLinks(dashboard.links);
|
||||||
};
|
};
|
||||||
@ -35,85 +34,14 @@ export const LinkSettingsList = ({ dashboard, onNew, onEdit }: LinkSettingsListP
|
|||||||
setLinks(dashboard.links);
|
setLinks(dashboard.links);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isEmptyList = dashboard.links.length === 0;
|
|
||||||
|
|
||||||
if (isEmptyList) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<EmptyListCTA
|
|
||||||
onClick={onNew}
|
|
||||||
title="There are no dashboard links added yet"
|
|
||||||
buttonIcon="link"
|
|
||||||
buttonTitle="Add dashboard link"
|
|
||||||
infoBoxTitle="What are dashboard links?"
|
|
||||||
infoBox={{
|
|
||||||
__html:
|
|
||||||
'<p>Dashboard Links allow you to place links to other dashboards and web sites directly below the dashboard header.</p>',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DashboardLinkList
|
||||||
<table role="grid" className="filter-table filter-table--hover">
|
links={links}
|
||||||
<thead>
|
onNew={onNew}
|
||||||
<tr>
|
onEdit={onEdit}
|
||||||
<th>Type</th>
|
onDuplicate={duplicateLink}
|
||||||
<th>Info</th>
|
onDelete={deleteLink}
|
||||||
<th colSpan={3} />
|
onOrderChange={moveLink}
|
||||||
</tr>
|
/>
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{links.map((link, idx) => (
|
|
||||||
<tr key={`${link.title}-${idx}`}>
|
|
||||||
<td role="gridcell" className="pointer" onClick={() => onEdit(idx)}>
|
|
||||||
<Icon name="external-link-alt" /> {link.type}
|
|
||||||
</td>
|
|
||||||
<td role="gridcell">
|
|
||||||
<HorizontalGroup>
|
|
||||||
{link.title && <span className={styles.titleWrapper}>{link.title}</span>}
|
|
||||||
{link.type === 'link' && <span className={styles.urlWrapper}>{link.url}</span>}
|
|
||||||
{link.type === 'dashboards' && <TagList tags={link.tags ?? []} />}
|
|
||||||
</HorizontalGroup>
|
|
||||||
</td>
|
|
||||||
<td style={{ width: '1%' }} role="gridcell">
|
|
||||||
{idx !== 0 && <IconButton name="arrow-up" onClick={() => moveLink(idx, -1)} tooltip="Move link up" />}
|
|
||||||
</td>
|
|
||||||
<td style={{ width: '1%' }} role="gridcell">
|
|
||||||
{links.length > 1 && idx !== links.length - 1 ? (
|
|
||||||
<IconButton name="arrow-down" onClick={() => moveLink(idx, 1)} tooltip="Move link down" />
|
|
||||||
) : null}
|
|
||||||
</td>
|
|
||||||
<td style={{ width: '1%' }} role="gridcell">
|
|
||||||
<IconButton name="copy" onClick={() => duplicateLink(link, idx)} tooltip="Copy link" />
|
|
||||||
</td>
|
|
||||||
<td style={{ width: '1%' }} role="gridcell">
|
|
||||||
<DeleteButton
|
|
||||||
aria-label={`Delete link with title "${link.title}"`}
|
|
||||||
size="sm"
|
|
||||||
onConfirm={() => deleteLink(idx)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<ListNewButton onClick={onNew}>New link</ListNewButton>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = () => ({
|
|
||||||
titleWrapper: css`
|
|
||||||
width: 20vw;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
`,
|
|
||||||
urlWrapper: css`
|
|
||||||
width: 40vw;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
|
@ -6,10 +6,10 @@ import { selectors } from '@grafana/e2e-selectors';
|
|||||||
import { TimeRangeUpdatedEvent } from '@grafana/runtime';
|
import { TimeRangeUpdatedEvent } from '@grafana/runtime';
|
||||||
import { DashboardLink } from '@grafana/schema';
|
import { DashboardLink } from '@grafana/schema';
|
||||||
import { Tooltip, useForceUpdate } from '@grafana/ui';
|
import { Tooltip, useForceUpdate } from '@grafana/ui';
|
||||||
|
import { LINK_ICON_MAP } from 'app/features/dashboard-scene/settings/links/utils';
|
||||||
|
|
||||||
import { getLinkSrv } from '../../../panel/panellinks/link_srv';
|
import { getLinkSrv } from '../../../panel/panellinks/link_srv';
|
||||||
import { DashboardModel } from '../../state';
|
import { DashboardModel } from '../../state';
|
||||||
import { linkIconMap } from '../LinksSettings/LinkSettingsEdit';
|
|
||||||
|
|
||||||
import { DashboardLinkButton, DashboardLinksDashboard } from './DashboardLinksDashboard';
|
import { DashboardLinkButton, DashboardLinksDashboard } from './DashboardLinksDashboard';
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ export const DashboardLinks = ({ dashboard, links }: Props) => {
|
|||||||
return <DashboardLinksDashboard key={key} link={link} linkInfo={linkInfo} dashboardUID={dashboard.uid} />;
|
return <DashboardLinksDashboard key={key} link={link} linkInfo={linkInfo} dashboardUID={dashboard.uid} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const icon = linkIconMap[link.icon];
|
const icon = LINK_ICON_MAP[link.icon];
|
||||||
|
|
||||||
const linkElement = (
|
const linkElement = (
|
||||||
<DashboardLinkButton
|
<DashboardLinkButton
|
||||||
|
Loading…
Reference in New Issue
Block a user