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.", "6"],
|
||||
[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": [
|
||||
[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": [
|
||||
[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": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[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 { DashboardLink } from '@grafana/schema';
|
||||
import { Tooltip } from '@grafana/ui';
|
||||
import { linkIconMap } from 'app/features/dashboard/components/LinksSettings/LinkSettingsEdit';
|
||||
import {
|
||||
DashboardLinkButton,
|
||||
DashboardLinksDashboard,
|
||||
} from 'app/features/dashboard/components/SubMenu/DashboardLinksDashboard';
|
||||
import { getLinkSrv } from 'app/features/panel/panellinks/link_srv';
|
||||
|
||||
import { LINK_ICON_MAP } from '../settings/links/utils';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
interface DashboardLinksControlsState extends SceneObjectState {}
|
||||
@ -37,7 +37,7 @@ function DashboardLinksControlsRenderer({ model }: SceneComponentProps<Dashboard
|
||||
return <DashboardLinksDashboard key={key} link={link} linkInfo={linkInfo} dashboardUID={uid} />;
|
||||
}
|
||||
|
||||
const icon = linkIconMap[link.icon];
|
||||
const icon = LINK_ICON_MAP[link.icon];
|
||||
|
||||
const linkElement = (
|
||||
<DashboardLinkButton
|
||||
|
@ -61,6 +61,7 @@ describe('DashboardScene', () => {
|
||||
${'description'} | ${'new description'}
|
||||
${'tags'} | ${['tag3', 'tag4']}
|
||||
${'editable'} | ${false}
|
||||
${'links'} | ${[]}
|
||||
`(
|
||||
'A change to $prop should set isDirty true',
|
||||
({ prop, value }: { prop: keyof DashboardSceneState; value: any }) => {
|
||||
|
@ -38,7 +38,7 @@ import { DashboardSceneUrlSync } from './DashboardSceneUrlSync';
|
||||
import { ViewPanelScene } from './ViewPanelScene';
|
||||
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 {
|
||||
/** The title */
|
||||
@ -48,7 +48,7 @@ export interface DashboardSceneState extends SceneObjectState {
|
||||
/** Tags */
|
||||
tags?: string[];
|
||||
/** Links */
|
||||
links?: DashboardLink[];
|
||||
links: DashboardLink[];
|
||||
/** Is editable */
|
||||
editable?: boolean;
|
||||
/** A uid when saved */
|
||||
@ -109,6 +109,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
meta: {},
|
||||
editable: true,
|
||||
body: state.body ?? new SceneFlexLayout({ children: [] }),
|
||||
links: state.links ?? [],
|
||||
...state,
|
||||
});
|
||||
|
||||
|
@ -335,7 +335,20 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
|
||||
"fiscalYearStartMonth": 1,
|
||||
"graphTooltip": 0,
|
||||
"id": 1351,
|
||||
"links": [],
|
||||
"links": [
|
||||
{
|
||||
"asDropdown": false,
|
||||
"icon": "external link",
|
||||
"includeVars": false,
|
||||
"keepTime": false,
|
||||
"tags": [],
|
||||
"targetBlank": false,
|
||||
"title": "Link 1",
|
||||
"tooltip": "",
|
||||
"type": "dashboards",
|
||||
"url": "",
|
||||
},
|
||||
],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
|
@ -45,6 +45,7 @@ import { DashboardControls } from '../scene/DashboardControls';
|
||||
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
|
||||
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||
import { NEW_LINK } from '../settings/links/utils';
|
||||
import { getQueryRunnerFor } from '../utils/utils';
|
||||
|
||||
import dashboard_to_load1 from './testfiles/dashboard_to_load1.json';
|
||||
@ -71,6 +72,7 @@ describe('transformSaveModelToScene', () => {
|
||||
...defaultTimePickerConfig,
|
||||
hidden: true,
|
||||
},
|
||||
links: [{ ...NEW_LINK, title: 'Link 1' }],
|
||||
templating: {
|
||||
list: [
|
||||
{
|
||||
@ -110,6 +112,8 @@ describe('transformSaveModelToScene', () => {
|
||||
|
||||
expect(scene.state.title).toBe('test');
|
||||
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.fiscalYearStartMonth).toEqual(2);
|
||||
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 { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||
import { NEW_LINK } from '../settings/links/utils';
|
||||
import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-utils';
|
||||
import { getVizPanelKeyForPanelId } from '../utils/utils';
|
||||
|
||||
@ -185,6 +186,7 @@ describe('transformSceneToSaveModel', () => {
|
||||
time_options: ['5m', '15m', '30m'],
|
||||
hidden: true,
|
||||
},
|
||||
links: [{ ...NEW_LINK, title: 'Link 1' }],
|
||||
};
|
||||
const scene = transformSaveModelToScene({ dashboard: dashboardWithCustomSettings as any, meta: {} });
|
||||
const saveModel = transformSceneToSaveModel(scene);
|
||||
@ -817,15 +819,14 @@ describe('transformSceneToSaveModel', () => {
|
||||
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 () => {
|
||||
// const scene = transformSaveModelToScene({ dashboard: snapshotableDashboardJson as any, meta: {} });
|
||||
// activateFullSceneTree(scene);
|
||||
// const snapshot = transformSceneToSaveModel(scene, true);
|
||||
// expect(snapshot.links?.length).toBe(1);
|
||||
// const result = trimDashboardForSnapshot('Snap title', getTimeRange({ from: 'now-6h', to: 'now' }), snapshot);
|
||||
// expect(result.links?.length).toBe(0);
|
||||
// });
|
||||
it('should remove links', async () => {
|
||||
const scene = transformSaveModelToScene({ dashboard: snapshotableDashboardJson as any, meta: {} });
|
||||
activateFullSceneTree(scene);
|
||||
const snapshot = transformSceneToSaveModel(scene, true);
|
||||
expect(snapshot.links?.length).toBe(1);
|
||||
const result = trimDashboardForSnapshot('Snap title', getTimeRange({ from: 'now-6h', to: 'now' }), snapshot);
|
||||
expect(result.links?.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -138,6 +138,7 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa
|
||||
fiscalYearStartMonth: timeRange.fiscalYearStartMonth,
|
||||
weekStart: timeRange.weekStart,
|
||||
tags: state.tags,
|
||||
links: state.links,
|
||||
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 { NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { NavModel, NavModelItem, PageLayoutType, arrayUtils } from '@grafana/data';
|
||||
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
|
||||
import { DashboardLink } from '@grafana/schema';
|
||||
import { Link } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
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 { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync';
|
||||
@ -24,49 +25,106 @@ export class DashboardLinksEditView extends SceneObjectBase<DashboardLinksEditVi
|
||||
public getUrlKey(): string {
|
||||
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>) {
|
||||
const { editIndex } = model.useState();
|
||||
const dashboard = getDashboardSceneFor(model);
|
||||
const links = dashboard.state.links || [];
|
||||
const { links, overlay } = dashboard.useState();
|
||||
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
||||
const linkToEdit = editIndex !== undefined ? links[editIndex] : undefined;
|
||||
|
||||
if (editIndex !== undefined) {
|
||||
const link = links[editIndex];
|
||||
if (link) {
|
||||
return <EditLinkView pageNav={pageNav} navModel={navModel} link={link} dashboard={dashboard} />;
|
||||
}
|
||||
if (linkToEdit) {
|
||||
return (
|
||||
<EditLinkView
|
||||
pageNav={pageNav}
|
||||
navModel={navModel}
|
||||
link={linkToEdit}
|
||||
dashboard={dashboard}
|
||||
onChange={model.onUpdateLink}
|
||||
onGoBack={model.onGoBack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
|
||||
<NavToolbarActions dashboard={dashboard} />
|
||||
{links.map((link, i) => (
|
||||
<Link
|
||||
key={`${link.title}-${i}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
locationService.partial({ editIndex: i });
|
||||
}}
|
||||
>
|
||||
{link.title}
|
||||
</Link>
|
||||
))}
|
||||
<DashboardLinkList
|
||||
links={links}
|
||||
onNew={model.onNewLink}
|
||||
onEdit={model.onEdit}
|
||||
onDelete={model.onDelete}
|
||||
onDuplicate={model.onDuplicate}
|
||||
onOrderChange={model.onOrderChange}
|
||||
/>
|
||||
{overlay && <overlay.Component model={overlay} />}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
interface EditLinkViewProps {
|
||||
link: DashboardLink;
|
||||
link?: DashboardLink;
|
||||
pageNav: NavModelItem;
|
||||
navModel: NavModel;
|
||||
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)!;
|
||||
parentTab.parentItem = pageNav;
|
||||
const { overlay } = dashboard.useState();
|
||||
|
||||
const editLinkPageNav = {
|
||||
text: 'Edit link',
|
||||
@ -76,7 +134,8 @@ function EditLinkView({ pageNav, link, navModel, dashboard }: EditLinkViewProps)
|
||||
return (
|
||||
<Page navModel={navModel} pageNav={editLinkPageNav} layout={PageLayoutType.Standard}>
|
||||
<NavToolbarActions dashboard={dashboard} />
|
||||
{JSON.stringify(link)}
|
||||
<DashboardLinkForm link={link!} onUpdate={onChange} onGoBack={onGoBack} />
|
||||
{overlay && <overlay.Component model={overlay} />}
|
||||
</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 { DashboardLinksControls } from '../scene/DashboardLinksControls';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { NEW_LINK } from '../settings/links/utils';
|
||||
|
||||
import { DashboardModelCompatibilityWrapper } from './DashboardModelCompatibilityWrapper';
|
||||
|
||||
@ -29,6 +30,7 @@ describe('DashboardModelCompatibilityWrapper', () => {
|
||||
expect(wrapper.editable).toBe(false);
|
||||
expect(wrapper.graphTooltip).toBe(DashboardCursorSync.Off);
|
||||
expect(wrapper.tags).toEqual(['hello-tag']);
|
||||
expect(wrapper.links).toEqual([NEW_LINK]);
|
||||
expect(wrapper.time.from).toBe('now-6h');
|
||||
expect(wrapper.timezone).toBe('America/New_York');
|
||||
expect(wrapper.weekStart).toBe('friday');
|
||||
@ -109,6 +111,7 @@ function setup() {
|
||||
title: 'hello',
|
||||
description: 'hello description',
|
||||
tags: ['hello-tag'],
|
||||
links: [NEW_LINK],
|
||||
uid: 'dash-1',
|
||||
editable: false,
|
||||
$timeRange: new SceneTimeRange({
|
||||
|
@ -82,6 +82,10 @@ export class DashboardModelCompatibilityWrapper {
|
||||
return this._scene.state.tags;
|
||||
}
|
||||
|
||||
public get links() {
|
||||
return this._scene.state.links;
|
||||
}
|
||||
|
||||
public get meta() {
|
||||
return this._scene.state.meta;
|
||||
}
|
||||
|
@ -3,9 +3,9 @@ import React, { useState } from 'react';
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
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 { newLink } from '../LinksSettings/LinkSettingsEdit';
|
||||
|
||||
import { SettingsPageProps } from './types';
|
||||
|
||||
@ -20,7 +20,7 @@ export function LinksSettings({ dashboard, sectionNav, editIndex }: SettingsPage
|
||||
};
|
||||
|
||||
const onNew = () => {
|
||||
dashboard.links = [...dashboard.links, { ...newLink }];
|
||||
dashboard.links = [...dashboard.links, { ...NEW_LINK }];
|
||||
setIsNew(true);
|
||||
locationService.partial({ editIndex: dashboard.links.length - 1 });
|
||||
};
|
||||
|
@ -1,41 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
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';
|
||||
|
||||
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 = {
|
||||
editLinkIdx: number;
|
||||
dashboard: DashboardModel;
|
||||
@ -43,7 +13,7 @@ type 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 links = [...dashboard.links];
|
||||
@ -52,98 +22,5 @@ export const LinkSettingsEdit = ({ editLinkIdx, dashboard, onGoBack }: LinkSetti
|
||||
setLinkSettings(link);
|
||||
};
|
||||
|
||||
const onTagsChange = (tags: string[]) => {
|
||||
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>
|
||||
);
|
||||
return <DashboardLinkForm link={linkSettings} onUpdate={onUpdate} onGoBack={onGoBack} />;
|
||||
};
|
||||
|
@ -1,13 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { arrayUtils } from '@grafana/data';
|
||||
import { DashboardLink } from '@grafana/schema';
|
||||
import { DeleteButton, HorizontalGroup, Icon, IconButton, TagList, useStyles2 } from '@grafana/ui';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { DashboardLinkList } from 'app/features/dashboard-scene/settings/links/DashboardLinkList';
|
||||
|
||||
import { DashboardModel } from '../../state/DashboardModel';
|
||||
import { ListNewButton } from '../DashboardSettings/ListNewButton';
|
||||
|
||||
type LinkSettingsListProps = {
|
||||
dashboard: DashboardModel;
|
||||
@ -15,9 +12,11 @@ type LinkSettingsListProps = {
|
||||
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) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const [links, setLinks] = useState(dashboard.links);
|
||||
|
||||
const moveLink = (idx: number, direction: number) => {
|
||||
@ -25,7 +24,7 @@ export const LinkSettingsList = ({ dashboard, onNew, onEdit }: LinkSettingsListP
|
||||
setLinks(dashboard.links);
|
||||
};
|
||||
|
||||
const duplicateLink = (link: DashboardLink, idx: number) => {
|
||||
const duplicateLink = (link: DashboardLink) => {
|
||||
dashboard.links = [...links, { ...link }];
|
||||
setLinks(dashboard.links);
|
||||
};
|
||||
@ -35,85 +34,14 @@ export const LinkSettingsList = ({ dashboard, onNew, onEdit }: LinkSettingsListP
|
||||
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 (
|
||||
<>
|
||||
<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={() => 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>
|
||||
</>
|
||||
<DashboardLinkList
|
||||
links={links}
|
||||
onNew={onNew}
|
||||
onEdit={onEdit}
|
||||
onDuplicate={duplicateLink}
|
||||
onDelete={deleteLink}
|
||||
onOrderChange={moveLink}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 { DashboardLink } from '@grafana/schema';
|
||||
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 { DashboardModel } from '../../state';
|
||||
import { linkIconMap } from '../LinksSettings/LinkSettingsEdit';
|
||||
|
||||
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} />;
|
||||
}
|
||||
|
||||
const icon = linkIconMap[link.icon];
|
||||
const icon = LINK_ICON_MAP[link.icon];
|
||||
|
||||
const linkElement = (
|
||||
<DashboardLinkButton
|
||||
|
Loading…
Reference in New Issue
Block a user