Skip to content

Commit

Permalink
Scenes/ShareModal: Implement public dashboard tab (grafana#76837)
Browse files Browse the repository at this point in the history
Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
  • Loading branch information
kaydelaney and dprokop authored Nov 14, 2023
1 parent c506da5 commit d018095
Show file tree
Hide file tree
Showing 19 changed files with 520 additions and 124 deletions.
4 changes: 0 additions & 4 deletions .betterer.results
Original file line number Diff line number Diff line change
Expand Up @@ -3186,10 +3186,6 @@ exports[`better eslint`] = {
"public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/UnsupportedDataSourcesAlert.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.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/SubMenu/AnnotationPicker.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
Expand Down
17 changes: 7 additions & 10 deletions public/app/features/dashboard-scene/sharing/ShareModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ShareExportTab } from './ShareExportTab';
import { ShareLinkTab } from './ShareLinkTab';
import { SharePanelEmbedTab } from './SharePanelEmbedTab';
import { ShareSnapshotTab } from './ShareSnapshotTab';
import { SharePublicDashboardTab } from './public-dashboards/SharePublicDashboardTab';
import { ModalSceneObjectLike, SceneShareTab } from './types';

interface ShareModalState extends SceneObjectState {
Expand All @@ -28,10 +29,10 @@ interface ShareModalState extends SceneObjectState {
export class ShareModal extends SceneObjectBase<ShareModalState> implements ModalSceneObjectLike {
static Component = SharePanelModalRenderer;

constructor(state: Omit<ShareModalState, 'activeTab'>) {
constructor(state: Omit<ShareModalState, 'activeTab'> & { activeTab?: string }) {
super({
...state,
activeTab: 'Link',
...state,
});

this.addActivationHandler(() => this.buildTabs());
Expand All @@ -50,6 +51,10 @@ export class ShareModal extends SceneObjectBase<ShareModalState> implements Moda
tabs.push(new ShareSnapshotTab({ 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 }));
}
Expand All @@ -74,14 +79,6 @@ export class ShareModal extends SceneObjectBase<ShareModalState> implements Moda
// });
// tabs.push(...customDashboardTabs);
// }

// if (Boolean(config.featureToggles['publicDashboards'])) {
// tabs.push({
// label: 'Public dashboard',
// value: shareDashboardType.publicDashboard,
// component: SharePublicDashboard,
// });
// }
}

onDismiss = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { css } from '@emotion/css';
import React from 'react';

import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
import { useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { useDeletePublicDashboardMutation } from 'app/features/dashboard/api/publicDashboardApi';
import { ConfigPublicDashboardBase } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard';
import { PublicDashboard } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
import { AccessControlAction } from 'app/types';

import { ShareModal } from '../ShareModal';

import { ConfirmModal } from './ConfirmModal';
import { SharePublicDashboardTab } from './SharePublicDashboardTab';
import { useUnsupportedDatasources } from './hooks';

interface Props extends SceneComponentProps<SharePublicDashboardTab> {
publicDashboard?: PublicDashboard;
isGetLoading?: boolean;
}

export function ConfigPublicDashboard({ model, publicDashboard, isGetLoading }: Props) {
const styles = useStyles2(getStyles);

const hasWritePermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPublicWrite);
const { dashboardRef } = model.useState();
const dashboard = dashboardRef.resolve();
const { isDirty } = dashboard.useState();
const [deletePublicDashboard] = useDeletePublicDashboardMutation();
const hasTemplateVariables = (dashboard.state.$variables?.state.variables.length ?? 0) > 0;
const unsupportedDataSources = useUnsupportedDatasources(dashboard);
const timeRangeState = sceneGraph.getTimeRange(model);
const timeRange = timeRangeState.useState();

return (
<ConfigPublicDashboardBase
dashboard={dashboard}
publicDashboard={publicDashboard}
unsupportedDatasources={unsupportedDataSources}
onRevoke={() => {
dashboard.showModal(
new ConfirmModal({
isOpen: true,
title: 'Revoke public URL',
icon: 'trash-alt',
confirmText: 'Revoke public URL',
body: (
<p className={styles.description}>
Are you sure you want to revoke this URL? The dashboard will no longer be public.
</p>
),
onDismiss: () => {
dashboard.showModal(new ShareModal({ dashboardRef, activeTab: 'Public Dashboard' }));
},
onConfirm: () => {
deletePublicDashboard({ dashboard, dashboardUid: dashboard.state.uid!, uid: publicDashboard!.uid });
dashboard.closeModal();
},
})
);
}}
timeRange={timeRange.value}
showSaveChangesAlert={hasWritePermissions && isDirty}
hasTemplateVariables={hasTemplateVariables}
/>
);
}

const getStyles = (theme: GrafanaTheme2) => ({
description: css({
fontSize: theme.typography.body.fontSize,
}),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';

import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { ConfirmModal as ConfirmModalComponent, ConfirmModalProps } from '@grafana/ui';

import { ModalSceneObjectLike } from '../types';

interface ConfirmModalState extends ConfirmModalProps, SceneObjectState {}

export class ConfirmModal extends SceneObjectBase<ConfirmModalState> implements ModalSceneObjectLike {
static Component = ConfirmModalRenderer;

constructor(state: ConfirmModalState) {
super({
confirmVariant: 'destructive',
dismissText: 'Cancel',
dismissVariant: 'secondary',
icon: 'exclamation-triangle',
confirmButtonVariant: 'destructive',
...state,
});
}

onDismiss() {}
}

function ConfirmModalRenderer({ model }: SceneComponentProps<ConfirmModal>) {
const props = model.useState();
return <ConfirmModalComponent {...props} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';

import { SceneComponentProps } from '@grafana/scenes';
import { CreatePublicDashboardBase } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard';

import { SharePublicDashboardTab } from './SharePublicDashboardTab';
import { useUnsupportedDatasources } from './hooks';

export function CreatePublicDashboard({ model }: SceneComponentProps<SharePublicDashboardTab>) {
const { dashboardRef } = model.useState();
const dashboard = dashboardRef.resolve();
const unsupportedDataSources = useUnsupportedDatasources(dashboard);
const hasTemplateVariables = (dashboard.state.$variables?.state.variables.length ?? 0) > 0;

return (
<CreatePublicDashboardBase
dashboard={dashboard}
unsupportedDatasources={unsupportedDataSources}
unsupportedTemplateVariables={hasTemplateVariables}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';

import { SceneComponentProps, SceneObjectBase, SceneObjectRef } from '@grafana/scenes';
import { t } from 'app/core/internationalization';
import { useGetPublicDashboardQuery } from 'app/features/dashboard/api/publicDashboardApi';
import { Loader } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard';
import { publicDashboardPersisted } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';

import { DashboardScene } from '../../scene/DashboardScene';
import { SceneShareTabState } from '../types';

import { ConfigPublicDashboard } from './ConfigPublicDashboard';
import { CreatePublicDashboard } from './CreatePublicDashboard';

export interface SharePublicDashboardTabState extends SceneShareTabState {
dashboardRef: SceneObjectRef<DashboardScene>;
}

export class SharePublicDashboardTab extends SceneObjectBase<SharePublicDashboardTabState> {
static Component = SharePublicDashboardTabRenderer;

public getTabLabel() {
return t('share-modal.tab-title.public-dashboard', 'Public Dashboard');
}
}

function SharePublicDashboardTabRenderer({ model }: SceneComponentProps<SharePublicDashboardTab>) {
const { data: publicDashboard, isLoading: isGetLoading } = useGetPublicDashboardQuery(
model.state.dashboardRef.resolve().state.uid!
);

return (
<>
{isGetLoading ? (
<Loader />
) : !publicDashboardPersisted(publicDashboard) ? (
<CreatePublicDashboard model={model} />
) : (
<ConfigPublicDashboard model={model} publicDashboard={publicDashboard} isGetLoading={isGetLoading} />
)}
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useAsync } from 'react-use';

import { DashboardScene } from '../../scene/DashboardScene';

import { getPanelDatasourceTypes, getUnsupportedDashboardDatasources } from './utils';

export function useUnsupportedDatasources(dashboard: DashboardScene) {
const { value: unsupportedDataSources } = useAsync(async () => {
const types = getPanelDatasourceTypes(dashboard);
return getUnsupportedDashboardDatasources(types);
}, []);

return unsupportedDataSources;
}
102 changes: 102 additions & 0 deletions public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { DataSourceWithBackend } from '@grafana/runtime';
import {
SceneGridItemLike,
VizPanel,
SceneGridItem,
SceneQueryRunner,
SceneDataTransformer,
SceneGridLayout,
SceneGridRow,
} from '@grafana/scenes';
import { supportedDatasources } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SupportedPubdashDatasources';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';

import { DashboardScene } from '../../scene/DashboardScene';
import { LibraryVizPanel } from '../../scene/LibraryVizPanel';
import { PanelRepeaterGridItem } from '../../scene/PanelRepeaterGridItem';

export const getUnsupportedDashboardDatasources = async (types: string[]): Promise<string[]> => {
let unsupportedDS = new Set<string>();

for (const type of types) {
if (!supportedDatasources.has(type)) {
unsupportedDS.add(type);
} else {
const ds = await getDatasourceSrv().get(type);
if (!(ds instanceof DataSourceWithBackend)) {
unsupportedDS.add(type);
}
}
}

return Array.from(unsupportedDS);
};

export function getPanelDatasourceTypes(scene: DashboardScene): string[] {
const types = new Set<string>();

const body = scene.state.body;
if (!(body instanceof SceneGridLayout)) {
return [];
}

for (const child of body.state.children) {
if (child instanceof SceneGridItem) {
const ts = panelDatasourceTypes(child);
for (const t of ts) {
types.add(t);
}
}

if (child instanceof SceneGridRow) {
const ts = rowTypes(child);
for (const t of ts) {
types.add(t);
}
}
}

return Array.from(types).sort();
}

function rowTypes(gridRow: SceneGridRow) {
const types = new Set(gridRow.state.children.map((c) => panelDatasourceTypes(c)).flat());
return types;
}

function panelDatasourceTypes(gridItem: SceneGridItemLike) {
let vizPanel: VizPanel | undefined;
if (gridItem instanceof SceneGridItem) {
if (gridItem.state.body instanceof LibraryVizPanel) {
vizPanel = gridItem.state.body.state.panel;
} else if (gridItem.state.body instanceof VizPanel) {
vizPanel = gridItem.state.body;
} else {
throw new Error('SceneGridItem body expected to be VizPanel');
}
} else if (gridItem instanceof PanelRepeaterGridItem) {
vizPanel = gridItem.state.source;
}

if (!vizPanel) {
throw new Error('Unsupported grid item type');
}
const dataProvider = vizPanel.state.$data;
const types = new Set<string>();
if (dataProvider instanceof SceneQueryRunner) {
for (const q of dataProvider.state.queries) {
types.add(q.datasource?.type ?? '');
}
}

if (dataProvider instanceof SceneDataTransformer) {
const panelData = dataProvider.state.$data;
if (panelData instanceof SceneQueryRunner) {
for (const q of panelData.state.queries) {
types.add(q.datasource?.type ?? '');
}
}
}

return Array.from(types);
}
Empty file.
Loading

0 comments on commit d018095

Please sign in to comment.