DashboardScene: Share library panel (#78421)

* DashboardScene: Share library panel

* Add menu item to create library panel

* Test update
This commit is contained in:
Dominik Prokop 2023-11-23 12:30:25 +01:00 committed by GitHub
parent 91a5c3803c
commit 58a0ff7459
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 119 additions and 31 deletions

View File

@ -38,7 +38,7 @@ describe('panelMenuBehavior', () => {
await new Promise((r) => setTimeout(r, 1));
expect(menu.state.items?.length).toBe(5);
expect(menu.state.items?.length).toBe(6);
// verify view panel url keeps url params and adds viewPanel=<panel-key>
expect(menu.state.items?.[0].href).toBe('/scenes/dashboard/dash-1?from=now-5m&to=now&viewPanel=panel-12');
// verify edit url keeps url time range

View File

@ -12,6 +12,7 @@ import { getDashboardUrl, getInspectUrl, getViewPanelUrl, tryGetExploreUrlForPan
import { getPanelIdForVizPanel } from '../utils/utils';
import { DashboardScene } from './DashboardScene';
import { LibraryVizPanel } from './LibraryVizPanel';
import { VizPanelLinks } from './PanelLinks';
/**
@ -24,6 +25,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
const panel = menu.parent as VizPanel;
const location = locationService.getLocation();
const items: PanelMenuItem[] = [];
const moreSubMenu: PanelMenuItem[] = [];
const panelId = getPanelIdForVizPanel(panel);
const dashboard = panel.getRoot();
@ -63,6 +65,25 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
shortcut: 'p s',
});
if (panel instanceof LibraryVizPanel) {
// TODO: Implement unlinking library panel
} else {
moreSubMenu.push({
text: t('panel.header-menu.create-library-panel', `Create library panel`),
iconClassName: 'share-alt',
onClick: () => {
reportInteraction('dashboards_panelheader_menu', { item: 'createLibraryPanel' });
dashboard.showModal(
new ShareModal({
panelRef: panel.getRef(),
dashboardRef: dashboard.getRef(),
activeTab: 'Library panel',
})
);
},
});
}
if (config.featureToggles.datatrails) {
addDataTrailPanelAction(dashboard, panel, items);
}
@ -87,6 +108,18 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
href: getInspectUrl(panel),
});
if (moreSubMenu.length) {
items.push({
type: 'submenu',
text: t('panel.header-menu.more', `More...`),
iconClassName: 'cube',
subMenu: moreSubMenu,
onClick: (e) => {
e.preventDefault();
},
});
}
menu.setState({ items });
};

View File

@ -25,6 +25,7 @@ interface ShareExportTabState extends SceneShareTabState {
}
export class ShareExportTab extends SceneObjectBase<ShareExportTabState> {
public tabId = 'Export';
static Component = ShareExportTabRenderer;
private _exporter = new DashboardExporter();

View File

@ -0,0 +1,58 @@
import React from 'react';
import { SceneComponentProps, SceneGridItem, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes';
import { t } from 'app/core/internationalization';
import { ShareLibraryPanel } from 'app/features/dashboard/components/ShareModal/ShareLibraryPanel';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { DashboardScene } from '../scene/DashboardScene';
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
import { gridItemToPanel, transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
import { SceneShareTabState } from './types';
export interface ShareLibraryPanelTabState extends SceneShareTabState {
panelRef?: SceneObjectRef<VizPanel>;
dashboardRef: SceneObjectRef<DashboardScene>;
}
export class ShareLibraryPanelTab extends SceneObjectBase<ShareLibraryPanelTabState> {
public tabId = 'Library panel';
static Component = ShareLibraryPanelTabRenderer;
public getTabLabel() {
return t('share-modal.tab-title.library-panel', 'Library panel');
}
}
function ShareLibraryPanelTabRenderer({ model }: SceneComponentProps<ShareLibraryPanelTab>) {
const { panelRef, dashboardRef, modalRef } = model.useState();
if (!panelRef) {
return null;
}
const vizPanel = panelRef.resolve();
if (vizPanel.parent instanceof SceneGridItem || vizPanel.parent instanceof PanelRepeaterGridItem) {
const dashboardScene = dashboardRef.resolve();
const panelJson = gridItemToPanel(vizPanel.parent);
const panelModel = new PanelModel(panelJson);
const dashboardJson = transformSceneToSaveModel(dashboardScene);
const dashboardModel = new DashboardModel(dashboardJson);
return (
<ShareLibraryPanel
initialFolderUid={dashboardScene.state.meta.folderUid}
dashboard={dashboardModel}
panel={panelModel}
onDismiss={() => {
modalRef?.resolve().onDismiss();
}}
/>
);
}
return null;
}

View File

@ -30,6 +30,8 @@ interface ShareOptions {
}
export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> {
public tabId = 'Link';
static Component = ShareLinkTabRenderer;
constructor(state: Omit<ShareLinkTabState, keyof ShareOptions>) {

View File

@ -10,6 +10,7 @@ import { DashboardScene } from '../scene/DashboardScene';
import { getDashboardSceneFor } from '../utils/utils';
import { ShareExportTab } from './ShareExportTab';
import { ShareLibraryPanelTab } from './ShareLibraryPanelTab';
import { ShareLinkTab } from './ShareLinkTab';
import { SharePanelEmbedTab } from './SharePanelEmbedTab';
import { ShareSnapshotTab } from './ShareSnapshotTab';
@ -51,34 +52,19 @@ export class ShareModal extends SceneObjectBase<ShareModalState> implements Moda
tabs.push(new ShareSnapshotTab({ panelRef, dashboardRef, modalRef: this.getRef() }));
}
if (panelRef) {
tabs.push(new SharePanelEmbedTab({ panelRef, dashboardRef }));
if (panelRef.resolve() instanceof VizPanel) {
tabs.push(new ShareLibraryPanelTab({ panelRef, dashboardRef, modalRef: this.getRef() }));
}
}
if (Boolean(config.featureToggles['publicDashboards'])) {
tabs.push(new SharePublicDashboardTab({ dashboardRef, modalRef: this.getRef() }));
}
if (panelRef) {
tabs.push(new SharePanelEmbedTab({ panelRef, dashboardRef }));
}
this.setState({ tabs });
// if (panel) {
// const embedLabel = t('share-modal.tab-title.embed', 'Embed');
// tabs.push({ label: embedLabel, value: shareDashboardType.embed, component: ShareEmbed });
// if (!isPanelModelLibraryPanel(panel)) {
// const libraryPanelLabel = t('share-modal.tab-title.library-panel', 'Library panel');
// tabs.push({ label: libraryPanelLabel, value: shareDashboardType.libraryPanel, component: ShareLibraryPanel });
// }
// tabs.push(...customPanelTabs);
// } else {
// const exportLabel = t('share-modal.tab-title.export', 'Export');
// tabs.push({
// label: exportLabel,
// value: shareDashboardType.export,
// component: ShareExport,
// });
// tabs.push(...customDashboardTabs);
// }
}
onDismiss = () => {
@ -101,7 +87,7 @@ function SharePanelModalRenderer({ model }: SceneComponentProps<ShareModal>) {
const modalTabs = tabs?.map((tab) => ({
label: tab.getTabLabel(),
value: tab.getTabLabel(),
value: tab.tabId,
}));
const header = (
@ -114,7 +100,7 @@ function SharePanelModalRenderer({ model }: SceneComponentProps<ShareModal>) {
/>
);
const currentTab = tabs.find((t) => t.getTabLabel() === activeTab);
const currentTab = tabs.find((t) => t.tabId === activeTab);
return (
<Modal isOpen={true} title={header} onDismiss={model.onDismiss}>

View File

@ -18,6 +18,7 @@ export interface SharePanelEmbedTabState extends SceneShareTabState {
}
export class SharePanelEmbedTab extends SceneObjectBase<SharePanelEmbedTabState> {
public tabId = 'Embed';
static Component = SharePanelEmbedTabRenderer;
public constructor(state: SharePanelEmbedTabState) {

View File

@ -50,6 +50,7 @@ export interface ShareSnapshotTabState extends SceneShareTabState {
}
export class ShareSnapshotTab extends SceneObjectBase<ShareSnapshotTabState> {
public tabId = 'Snapshot';
static Component = ShareSnapshoTabRenderer;
public constructor(state: ShareSnapshotTabState) {

View File

@ -17,6 +17,7 @@ export interface SharePublicDashboardTabState extends SceneShareTabState {
}
export class SharePublicDashboardTab extends SceneObjectBase<SharePublicDashboardTabState> {
public tabId = 'Public dashboard';
static Component = SharePublicDashboardTabRenderer;
public getTabLabel() {

View File

@ -10,4 +10,5 @@ export interface SceneShareTabState extends SceneObjectState {
export interface SceneShareTab<T extends SceneShareTabState = SceneShareTabState> extends SceneObject<T> {
getTabLabel(): string;
tabId: string;
}

View File

@ -24,7 +24,7 @@ export const ShareLibraryPanel = ({ panel, initialFolderUid, onDismiss }: Props)
<p className="share-modal-info-text">
<Trans i18nKey="share-modal.library.info">Create library panel.</Trans>
</p>
<AddLibraryPanelContents panel={panel} initialFolderUid={initialFolderUid} onDismiss={onDismiss!} />
<AddLibraryPanelContents panel={panel} initialFolderUid={initialFolderUid} onDismiss={onDismiss} />
</>
);
};

View File

@ -12,7 +12,7 @@ import { LibraryElementDTO } from '../../types';
import { usePanelSave } from '../../utils/usePanelSave';
interface AddLibraryPanelContentsProps {
onDismiss: () => void;
onDismiss?: () => void;
panel: PanelModel;
initialFolderUid?: string;
}
@ -23,20 +23,23 @@ export const AddLibraryPanelContents = ({ panel, initialFolderUid, onDismiss }:
const [debouncedPanelName, setDebouncedPanelName] = useState(panel.title);
const [waiting, setWaiting] = useState(false);
console.log('folderUid', folderUid);
useEffect(() => setWaiting(true), [panelName]);
useDebounce(() => setDebouncedPanelName(panelName), 350, [panelName]);
const { saveLibraryPanel } = usePanelSave();
const onCreate = useCallback(() => {
panel.libraryPanel = { uid: '', name: panelName };
saveLibraryPanel(panel, folderUid!).then((res: LibraryElementDTO | FetchError) => {
if (!isFetchError(res)) {
onDismiss();
onDismiss?.();
} else {
panel.libraryPanel = undefined;
}
});
}, [panel, panelName, folderUid, onDismiss, saveLibraryPanel]);
const isValidName = useAsync(async () => {
try {
return !(await getLibraryPanelByName(panelName)).some((lp) => lp.folderUid === folderUid);
@ -50,6 +53,7 @@ export const AddLibraryPanelContents = ({ panel, initialFolderUid, onDismiss }:
}
}, [debouncedPanelName, folderUid]);
console.log('isValidName:', isValidName);
const invalidInput =
!isValidName?.value && isValidName.value !== undefined && panelName === debouncedPanelName && !waiting;

View File

@ -3303,7 +3303,7 @@ __metadata:
linkType: soft
"@grafana/scenes@npm:1.24.1":
version: 1.24.1
version: 0.0.0-use.local
resolution: "@grafana/scenes@npm:1.24.1"
dependencies:
"@grafana/e2e-selectors": "npm:10.0.2"
@ -3318,7 +3318,7 @@ __metadata:
"@grafana/ui": 10.0.3
checksum: 38967dd3977a9b9feb4c295da58bb378d2f9c810c43b34c67c65858782b9593421dfb58d29bc3fa0e86fc63f9af37f7a381ac958ef43bf38d6443a0a7e4de059
languageName: node
linkType: hard
linkType: soft
"@grafana/schema@npm:10.3.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema":
version: 0.0.0-use.local