mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Scenes/ShareModal: Implement public dashboard tab (#76837)
Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
parent
c506da53f3
commit
d0180957d1
@ -3186,10 +3186,6 @@ exports[`better eslint`] = {
|
|||||||
"public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/UnsupportedDataSourcesAlert.tsx:5381": [
|
"public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ModalAlerts/UnsupportedDataSourcesAlert.tsx:5381": [
|
||||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
[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": [
|
"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.", "0"],
|
||||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
[0, 0, 0, "Styles should be written using objects.", "1"]
|
||||||
|
@ -13,6 +13,7 @@ import { ShareExportTab } from './ShareExportTab';
|
|||||||
import { ShareLinkTab } from './ShareLinkTab';
|
import { ShareLinkTab } from './ShareLinkTab';
|
||||||
import { SharePanelEmbedTab } from './SharePanelEmbedTab';
|
import { SharePanelEmbedTab } from './SharePanelEmbedTab';
|
||||||
import { ShareSnapshotTab } from './ShareSnapshotTab';
|
import { ShareSnapshotTab } from './ShareSnapshotTab';
|
||||||
|
import { SharePublicDashboardTab } from './public-dashboards/SharePublicDashboardTab';
|
||||||
import { ModalSceneObjectLike, SceneShareTab } from './types';
|
import { ModalSceneObjectLike, SceneShareTab } from './types';
|
||||||
|
|
||||||
interface ShareModalState extends SceneObjectState {
|
interface ShareModalState extends SceneObjectState {
|
||||||
@ -28,10 +29,10 @@ interface ShareModalState extends SceneObjectState {
|
|||||||
export class ShareModal extends SceneObjectBase<ShareModalState> implements ModalSceneObjectLike {
|
export class ShareModal extends SceneObjectBase<ShareModalState> implements ModalSceneObjectLike {
|
||||||
static Component = SharePanelModalRenderer;
|
static Component = SharePanelModalRenderer;
|
||||||
|
|
||||||
constructor(state: Omit<ShareModalState, 'activeTab'>) {
|
constructor(state: Omit<ShareModalState, 'activeTab'> & { activeTab?: string }) {
|
||||||
super({
|
super({
|
||||||
...state,
|
|
||||||
activeTab: 'Link',
|
activeTab: 'Link',
|
||||||
|
...state,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addActivationHandler(() => this.buildTabs());
|
this.addActivationHandler(() => this.buildTabs());
|
||||||
@ -50,6 +51,10 @@ export class ShareModal extends SceneObjectBase<ShareModalState> implements Moda
|
|||||||
tabs.push(new ShareSnapshotTab({ panelRef, dashboardRef, modalRef: this.getRef() }));
|
tabs.push(new ShareSnapshotTab({ panelRef, dashboardRef, modalRef: this.getRef() }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Boolean(config.featureToggles['publicDashboards'])) {
|
||||||
|
tabs.push(new SharePublicDashboardTab({ dashboardRef, modalRef: this.getRef() }));
|
||||||
|
}
|
||||||
|
|
||||||
if (panelRef) {
|
if (panelRef) {
|
||||||
tabs.push(new SharePanelEmbedTab({ panelRef, dashboardRef }));
|
tabs.push(new SharePanelEmbedTab({ panelRef, dashboardRef }));
|
||||||
}
|
}
|
||||||
@ -74,14 +79,6 @@ export class ShareModal extends SceneObjectBase<ShareModalState> implements Moda
|
|||||||
// });
|
// });
|
||||||
// tabs.push(...customDashboardTabs);
|
// tabs.push(...customDashboardTabs);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// if (Boolean(config.featureToggles['publicDashboards'])) {
|
|
||||||
// tabs.push({
|
|
||||||
// label: 'Public dashboard',
|
|
||||||
// value: shareDashboardType.publicDashboard,
|
|
||||||
// component: SharePublicDashboard,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onDismiss = () => {
|
onDismiss = () => {
|
||||||
|
@ -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,
|
||||||
|
}),
|
||||||
|
});
|
@ -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} />;
|
||||||
|
}
|
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -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} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -11,6 +11,7 @@ import {
|
|||||||
SessionUser,
|
SessionUser,
|
||||||
} from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
|
} from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
|
||||||
import { DashboardModel } from 'app/features/dashboard/state';
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
|
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
|
||||||
import {
|
import {
|
||||||
PublicDashboardListWithPagination,
|
PublicDashboardListWithPagination,
|
||||||
PublicDashboardListWithPaginationResponse,
|
PublicDashboardListWithPaginationResponse,
|
||||||
@ -69,40 +70,61 @@ export const publicDashboardApi = createApi({
|
|||||||
}),
|
}),
|
||||||
createPublicDashboard: builder.mutation<
|
createPublicDashboard: builder.mutation<
|
||||||
PublicDashboard,
|
PublicDashboard,
|
||||||
{ dashboard: DashboardModel; payload: Partial<PublicDashboardSettings> }
|
{ dashboard: DashboardModel | DashboardScene; payload: Partial<PublicDashboardSettings> }
|
||||||
>({
|
>({
|
||||||
query: (params) => ({
|
query: (params) => {
|
||||||
url: `/dashboards/uid/${params.dashboard.uid}/public-dashboards`,
|
const dashUid = params.dashboard instanceof DashboardScene ? params.dashboard.state.uid : params.dashboard.uid;
|
||||||
method: 'POST',
|
return {
|
||||||
data: params.payload,
|
url: `/dashboards/uid/${dashUid}/public-dashboards`,
|
||||||
}),
|
method: 'POST',
|
||||||
|
data: params.payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
async onQueryStarted({ dashboard, payload }, { dispatch, queryFulfilled }) {
|
async onQueryStarted({ dashboard, payload }, { dispatch, queryFulfilled }) {
|
||||||
const { data } = await queryFulfilled;
|
const { data } = await queryFulfilled;
|
||||||
dispatch(notifyApp(createSuccessNotification('Dashboard is public!')));
|
dispatch(notifyApp(createSuccessNotification('Dashboard is public!')));
|
||||||
|
|
||||||
// Update runtime meta flag
|
if (dashboard instanceof DashboardScene) {
|
||||||
dashboard.updateMeta({
|
dashboard.setState({
|
||||||
publicDashboardUid: data.uid,
|
meta: { ...dashboard.state.meta, publicDashboardEnabled: data.isEnabled, publicDashboardUid: data.uid },
|
||||||
publicDashboardEnabled: data.isEnabled,
|
});
|
||||||
});
|
} else {
|
||||||
|
// Update runtime meta flag
|
||||||
|
dashboard.updateMeta({
|
||||||
|
publicDashboardUid: data.uid,
|
||||||
|
publicDashboardEnabled: data.isEnabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
invalidatesTags: (result, error, { dashboard }) => [{ type: 'PublicDashboard', id: dashboard.uid }],
|
invalidatesTags: (result, error, { dashboard }) => [
|
||||||
|
{ type: 'PublicDashboard', id: dashboard instanceof DashboardScene ? dashboard.state.uid : dashboard.uid },
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
updatePublicDashboard: builder.mutation<
|
updatePublicDashboard: builder.mutation<
|
||||||
PublicDashboard,
|
PublicDashboard,
|
||||||
{ dashboard: Partial<DashboardModel>; payload: Partial<PublicDashboard> }
|
{
|
||||||
|
dashboard: (Pick<DashboardModel, 'uid'> & Partial<Pick<DashboardModel, 'updateMeta'>>) | DashboardScene;
|
||||||
|
payload: Partial<PublicDashboard>;
|
||||||
|
}
|
||||||
>({
|
>({
|
||||||
query: (params) => ({
|
query: ({ payload, dashboard }) => {
|
||||||
url: `/dashboards/uid/${params.dashboard.uid}/public-dashboards/${params.payload.uid}`,
|
const dashUid = dashboard instanceof DashboardScene ? dashboard.state.uid : dashboard.uid;
|
||||||
method: 'PATCH',
|
return {
|
||||||
data: params.payload,
|
url: `/dashboards/uid/${dashUid}/public-dashboards/${payload.uid}`,
|
||||||
}),
|
method: 'PATCH',
|
||||||
async onQueryStarted({ dashboard, payload }, { dispatch, queryFulfilled }) {
|
data: payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async onQueryStarted({ dashboard }, { dispatch, queryFulfilled }) {
|
||||||
const { data } = await queryFulfilled;
|
const { data } = await queryFulfilled;
|
||||||
dispatch(notifyApp(createSuccessNotification('Public dashboard updated!')));
|
dispatch(notifyApp(createSuccessNotification('Public dashboard updated!')));
|
||||||
|
|
||||||
if (dashboard.updateMeta) {
|
if (dashboard instanceof DashboardScene) {
|
||||||
dashboard.updateMeta({
|
dashboard.setState({
|
||||||
|
meta: { ...dashboard.state.meta, publicDashboardEnabled: data.isEnabled, publicDashboardUid: data.uid },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dashboard.updateMeta?.({
|
||||||
publicDashboardUid: data.uid,
|
publicDashboardUid: data.uid,
|
||||||
publicDashboardEnabled: data.isEnabled,
|
publicDashboardEnabled: data.isEnabled,
|
||||||
});
|
});
|
||||||
@ -150,7 +172,10 @@ export const publicDashboardApi = createApi({
|
|||||||
}),
|
}),
|
||||||
providesTags: ['AuditTablePublicDashboard'],
|
providesTags: ['AuditTablePublicDashboard'],
|
||||||
}),
|
}),
|
||||||
deletePublicDashboard: builder.mutation<void, { dashboard?: DashboardModel; dashboardUid: string; uid: string }>({
|
deletePublicDashboard: builder.mutation<
|
||||||
|
void,
|
||||||
|
{ dashboard?: DashboardModel | DashboardScene; dashboardUid: string; uid: string }
|
||||||
|
>({
|
||||||
query: (params) => ({
|
query: (params) => ({
|
||||||
url: `/dashboards/uid/${params.dashboardUid}/public-dashboards/${params.uid}`,
|
url: `/dashboards/uid/${params.dashboardUid}/public-dashboards/${params.uid}`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@ -159,10 +184,16 @@ export const publicDashboardApi = createApi({
|
|||||||
await queryFulfilled;
|
await queryFulfilled;
|
||||||
dispatch(notifyApp(createSuccessNotification('Public dashboard deleted!')));
|
dispatch(notifyApp(createSuccessNotification('Public dashboard deleted!')));
|
||||||
|
|
||||||
dashboard?.updateMeta({
|
if (dashboard instanceof DashboardScene) {
|
||||||
publicDashboardUid: uid,
|
dashboard.setState({
|
||||||
publicDashboardEnabled: false,
|
meta: { ...dashboard.state.meta, publicDashboardUid: uid, publicDashboardEnabled: false },
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
dashboard?.updateMeta({
|
||||||
|
publicDashboardUid: uid,
|
||||||
|
publicDashboardEnabled: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
invalidatesTags: (result, error, { dashboardUid }) => [
|
invalidatesTags: (result, error, { dashboardUid }) => [
|
||||||
{ type: 'PublicDashboard', id: dashboardUid },
|
{ type: 'PublicDashboard', id: dashboardUid },
|
||||||
|
@ -1,27 +1,33 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { useContext } from 'react';
|
import React from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data/src';
|
import { GrafanaTheme2, TimeRange } from '@grafana/data/src';
|
||||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||||
import { config, featureEnabled } from '@grafana/runtime/src';
|
import { config, featureEnabled } from '@grafana/runtime/src';
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
ClipboardButton,
|
ClipboardButton,
|
||||||
Field,
|
Field,
|
||||||
HorizontalGroup,
|
HorizontalGroup,
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
ModalsContext,
|
ModalsController,
|
||||||
Switch,
|
Switch,
|
||||||
useStyles2,
|
useStyles2,
|
||||||
} from '@grafana/ui/src';
|
} from '@grafana/ui/src';
|
||||||
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
|
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
|
||||||
|
import {
|
||||||
|
useDeletePublicDashboardMutation,
|
||||||
|
useUpdatePublicDashboardMutation,
|
||||||
|
} from 'app/features/dashboard/api/publicDashboardApi';
|
||||||
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
|
import { getTimeRange } from 'app/features/dashboard/utils/timeRange';
|
||||||
|
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
|
||||||
|
import { DeletePublicDashboardModal } from 'app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardModal';
|
||||||
|
|
||||||
import { contextSrv } from '../../../../../../core/services/context_srv';
|
import { contextSrv } from '../../../../../../core/services/context_srv';
|
||||||
import { AccessControlAction, useSelector } from '../../../../../../types';
|
import { AccessControlAction, useSelector } from '../../../../../../types';
|
||||||
import { DeletePublicDashboardButton } from '../../../../../manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardButton';
|
|
||||||
import { useGetPublicDashboardQuery, useUpdatePublicDashboardMutation } from '../../../../api/publicDashboardApi';
|
|
||||||
import { useIsDesktop } from '../../../../utils/screen';
|
import { useIsDesktop } from '../../../../utils/screen';
|
||||||
import { ShareModal } from '../../ShareModal';
|
import { ShareModal } from '../../ShareModal';
|
||||||
import { trackDashboardSharingActionPerType } from '../../analytics';
|
import { trackDashboardSharingActionPerType } from '../../analytics';
|
||||||
@ -30,8 +36,11 @@ import { NoUpsertPermissionsAlert } from '../ModalAlerts/NoUpsertPermissionsAler
|
|||||||
import { SaveDashboardChangesAlert } from '../ModalAlerts/SaveDashboardChangesAlert';
|
import { SaveDashboardChangesAlert } from '../ModalAlerts/SaveDashboardChangesAlert';
|
||||||
import { UnsupportedDataSourcesAlert } from '../ModalAlerts/UnsupportedDataSourcesAlert';
|
import { UnsupportedDataSourcesAlert } from '../ModalAlerts/UnsupportedDataSourcesAlert';
|
||||||
import { UnsupportedTemplateVariablesAlert } from '../ModalAlerts/UnsupportedTemplateVariablesAlert';
|
import { UnsupportedTemplateVariablesAlert } from '../ModalAlerts/UnsupportedTemplateVariablesAlert';
|
||||||
import { dashboardHasTemplateVariables, generatePublicDashboardUrl } from '../SharePublicDashboardUtils';
|
import {
|
||||||
import { useGetUnsupportedDataSources } from '../useGetUnsupportedDataSources';
|
dashboardHasTemplateVariables,
|
||||||
|
generatePublicDashboardUrl,
|
||||||
|
PublicDashboard,
|
||||||
|
} from '../SharePublicDashboardUtils';
|
||||||
|
|
||||||
import { Configuration } from './Configuration';
|
import { Configuration } from './Configuration';
|
||||||
import { EmailSharingConfiguration } from './EmailSharingConfiguration';
|
import { EmailSharingConfiguration } from './EmailSharingConfiguration';
|
||||||
@ -46,25 +55,33 @@ export interface ConfigPublicDashboardForm {
|
|||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfigPublicDashboard = () => {
|
interface Props {
|
||||||
|
unsupportedDatasources?: string[];
|
||||||
|
showSaveChangesAlert?: boolean;
|
||||||
|
publicDashboard?: PublicDashboard;
|
||||||
|
hasTemplateVariables?: boolean;
|
||||||
|
timeRange: TimeRange;
|
||||||
|
onRevoke: () => void;
|
||||||
|
dashboard: DashboardModel | DashboardScene;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfigPublicDashboardBase({
|
||||||
|
onRevoke,
|
||||||
|
timeRange,
|
||||||
|
hasTemplateVariables = false,
|
||||||
|
showSaveChangesAlert = false,
|
||||||
|
unsupportedDatasources = [],
|
||||||
|
publicDashboard,
|
||||||
|
dashboard,
|
||||||
|
}: Props) {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const isDesktop = useIsDesktop();
|
const isDesktop = useIsDesktop();
|
||||||
const { showModal, hideModal } = useContext(ModalsContext);
|
|
||||||
|
|
||||||
|
const [update, { isLoading }] = useUpdatePublicDashboardMutation();
|
||||||
const hasWritePermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPublicWrite);
|
const hasWritePermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPublicWrite);
|
||||||
|
const disableInputs = !hasWritePermissions || isLoading;
|
||||||
const hasEmailSharingEnabled =
|
const hasEmailSharingEnabled =
|
||||||
!!config.featureToggles.publicDashboardsEmailSharing && featureEnabled('publicDashboardsEmailSharing');
|
!!config.featureToggles.publicDashboardsEmailSharing && featureEnabled('publicDashboardsEmailSharing');
|
||||||
const dashboardState = useSelector((store) => store.dashboard);
|
|
||||||
const dashboard = dashboardState.getModel()!;
|
|
||||||
const dashboardVariables = dashboard.getVariables();
|
|
||||||
|
|
||||||
const { unsupportedDataSources } = useGetUnsupportedDataSources(dashboard);
|
|
||||||
|
|
||||||
const { data: publicDashboard, isFetching: isGetLoading } = useGetPublicDashboardQuery(dashboard.uid);
|
|
||||||
const [update, { isLoading: isUpdateLoading }] = useUpdatePublicDashboardMutation();
|
|
||||||
const isDataLoading = isUpdateLoading || isGetLoading;
|
|
||||||
const disableInputs = !hasWritePermissions || isDataLoading;
|
|
||||||
const timeRange = getTimeRange(dashboard.getDefaultTime(), dashboard);
|
|
||||||
|
|
||||||
const { handleSubmit, setValue, register } = useForm<ConfigPublicDashboardForm>({
|
const { handleSubmit, setValue, register } = useForm<ConfigPublicDashboardForm>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -74,33 +91,23 @@ const ConfigPublicDashboard = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onUpdate = async (values: ConfigPublicDashboardForm) => {
|
const onPublicDashboardUpdate = async (values: ConfigPublicDashboardForm) => {
|
||||||
const { isAnnotationsEnabled, isTimeSelectionEnabled, isPaused } = values;
|
const { isAnnotationsEnabled, isTimeSelectionEnabled, isPaused } = values;
|
||||||
|
|
||||||
const req = {
|
update({
|
||||||
dashboard,
|
dashboard: dashboard,
|
||||||
payload: {
|
payload: {
|
||||||
...publicDashboard!,
|
...publicDashboard!,
|
||||||
annotationsEnabled: isAnnotationsEnabled,
|
annotationsEnabled: isAnnotationsEnabled,
|
||||||
timeSelectionEnabled: isTimeSelectionEnabled,
|
timeSelectionEnabled: isTimeSelectionEnabled,
|
||||||
isEnabled: !isPaused,
|
isEnabled: !isPaused,
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
update(req);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChange = async (name: keyof ConfigPublicDashboardForm, value: boolean) => {
|
const onChange = async (name: keyof ConfigPublicDashboardForm, value: boolean) => {
|
||||||
setValue(name, value);
|
setValue(name, value);
|
||||||
await handleSubmit((data) => onUpdate(data))();
|
await handleSubmit((data) => onPublicDashboardUpdate(data))();
|
||||||
};
|
|
||||||
|
|
||||||
const onDismissDelete = () => {
|
|
||||||
showModal(ShareModal, {
|
|
||||||
dashboard,
|
|
||||||
onDismiss: hideModal,
|
|
||||||
activeTab: shareDashboardType.publicDashboard,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function onCopyURL() {
|
function onCopyURL() {
|
||||||
@ -109,11 +116,11 @@ const ConfigPublicDashboard = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.configContainer}>
|
<div className={styles.configContainer}>
|
||||||
{hasWritePermissions && dashboard.hasUnsavedChanges() && <SaveDashboardChangesAlert />}
|
{showSaveChangesAlert && <SaveDashboardChangesAlert />}
|
||||||
{!hasWritePermissions && <NoUpsertPermissionsAlert mode="edit" />}
|
{!hasWritePermissions && <NoUpsertPermissionsAlert mode="edit" />}
|
||||||
{dashboardHasTemplateVariables(dashboardVariables) && <UnsupportedTemplateVariablesAlert />}
|
{hasTemplateVariables && <UnsupportedTemplateVariablesAlert />}
|
||||||
{!!unsupportedDataSources.length && (
|
{unsupportedDatasources.length > 0 && (
|
||||||
<UnsupportedDataSourcesAlert unsupportedDataSources={unsupportedDataSources.join(', ')} />
|
<UnsupportedDataSourcesAlert unsupportedDataSources={unsupportedDatasources.join(', ')} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasEmailSharingEnabled && <EmailSharingConfiguration />}
|
{hasEmailSharingEnabled && <EmailSharingConfiguration />}
|
||||||
@ -168,7 +175,7 @@ const ConfigPublicDashboard = () => {
|
|||||||
headerElement={({ className }) => (
|
headerElement={({ className }) => (
|
||||||
<SettingsSummary
|
<SettingsSummary
|
||||||
className={className}
|
className={className}
|
||||||
isDataLoading={isDataLoading}
|
isDataLoading={isLoading}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
timeSelectionEnabled={publicDashboard?.timeSelectionEnabled}
|
timeSelectionEnabled={publicDashboard?.timeSelectionEnabled}
|
||||||
annotationsEnabled={publicDashboard?.annotationsEnabled}
|
annotationsEnabled={publicDashboard?.annotationsEnabled}
|
||||||
@ -186,27 +193,73 @@ const ConfigPublicDashboard = () => {
|
|||||||
align={isDesktop ? 'center' : 'normal'}
|
align={isDesktop ? 'center' : 'normal'}
|
||||||
>
|
>
|
||||||
<HorizontalGroup justify="flex-end">
|
<HorizontalGroup justify="flex-end">
|
||||||
<DeletePublicDashboardButton
|
<Button
|
||||||
|
aria-label="Revoke public URL"
|
||||||
|
title="Revoke public URL"
|
||||||
|
onClick={onRevoke}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={disableInputs}
|
disabled={disableInputs}
|
||||||
data-testid={selectors.DeleteButton}
|
data-testid={selectors.DeleteButton}
|
||||||
onDismiss={onDismissDelete}
|
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
fill="outline"
|
fill="outline"
|
||||||
dashboard={dashboard}
|
|
||||||
publicDashboard={{
|
|
||||||
uid: publicDashboard!.uid,
|
|
||||||
dashboardUid: dashboard.uid,
|
|
||||||
title: dashboard.title,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Revoke public URL
|
Revoke public URL
|
||||||
</DeletePublicDashboardButton>
|
</Button>
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
interface ConfigPublicDashboardProps {
|
||||||
|
publicDashboard: PublicDashboard;
|
||||||
|
unsupportedDatasources: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfigPublicDashboard({ publicDashboard, unsupportedDatasources }: ConfigPublicDashboardProps) {
|
||||||
|
const dashboardState = useSelector((store) => store.dashboard);
|
||||||
|
const dashboard = dashboardState.getModel()!;
|
||||||
|
const timeRange = getTimeRange(dashboard.getDefaultTime(), dashboard);
|
||||||
|
const hasWritePermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPublicWrite);
|
||||||
|
const hasTemplateVariables = dashboardHasTemplateVariables(dashboard.getVariables());
|
||||||
|
const [deletePublicDashboard] = useDeletePublicDashboardMutation();
|
||||||
|
const onDeletePublicDashboardClick = (onDelete: () => void) => {
|
||||||
|
deletePublicDashboard({
|
||||||
|
dashboard,
|
||||||
|
uid: publicDashboard!.uid,
|
||||||
|
dashboardUid: dashboard.uid,
|
||||||
|
});
|
||||||
|
onDelete();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalsController>
|
||||||
|
{({ showModal, hideModal }) => (
|
||||||
|
<ConfigPublicDashboardBase
|
||||||
|
publicDashboard={publicDashboard}
|
||||||
|
dashboard={dashboard}
|
||||||
|
unsupportedDatasources={unsupportedDatasources}
|
||||||
|
timeRange={timeRange}
|
||||||
|
showSaveChangesAlert={hasWritePermissions && dashboard.hasUnsavedChanges()}
|
||||||
|
hasTemplateVariables={hasTemplateVariables}
|
||||||
|
onRevoke={() => {
|
||||||
|
showModal(DeletePublicDashboardModal, {
|
||||||
|
dashboardTitle: dashboard.title,
|
||||||
|
onConfirm: () => onDeletePublicDashboardClick(hideModal),
|
||||||
|
onDismiss: () => {
|
||||||
|
showModal(ShareModal, {
|
||||||
|
dashboard,
|
||||||
|
onDismiss: hideModal,
|
||||||
|
activeTab: shareDashboardType.publicDashboard,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ModalsController>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
configContainer: css`
|
configContainer: css`
|
||||||
@ -225,5 +278,3 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ConfigPublicDashboard;
|
|
||||||
|
@ -5,10 +5,12 @@ import { FormState, UseFormRegister } from 'react-hook-form';
|
|||||||
import { GrafanaTheme2 } from '@grafana/data/src';
|
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||||
import { Button, Form, Spinner, useStyles2 } from '@grafana/ui/src';
|
import { Button, Form, Spinner, useStyles2 } from '@grafana/ui/src';
|
||||||
|
import { useCreatePublicDashboardMutation } from 'app/features/dashboard/api/publicDashboardApi';
|
||||||
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
|
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
|
||||||
|
|
||||||
import { contextSrv } from '../../../../../../core/services/context_srv';
|
import { contextSrv } from '../../../../../../core/services/context_srv';
|
||||||
import { AccessControlAction, useSelector } from '../../../../../../types';
|
import { AccessControlAction, useSelector } from '../../../../../../types';
|
||||||
import { useCreatePublicDashboardMutation } from '../../../../api/publicDashboardApi';
|
|
||||||
import { trackDashboardSharingActionPerType } from '../../analytics';
|
import { trackDashboardSharingActionPerType } from '../../analytics';
|
||||||
import { shareDashboardType } from '../../utils';
|
import { shareDashboardType } from '../../utils';
|
||||||
import { NoUpsertPermissionsAlert } from '../ModalAlerts/NoUpsertPermissionsAlert';
|
import { NoUpsertPermissionsAlert } from '../ModalAlerts/NoUpsertPermissionsAlert';
|
||||||
@ -27,22 +29,29 @@ export type SharePublicDashboardAcknowledgmentInputs = {
|
|||||||
usageAcknowledgment: boolean;
|
usageAcknowledgment: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CreatePublicDashboard = ({ isError }: { isError: boolean }) => {
|
interface CreatePublicDashboarBaseProps {
|
||||||
|
unsupportedDatasources?: string[];
|
||||||
|
unsupportedTemplateVariables?: boolean;
|
||||||
|
dashboard: DashboardModel | DashboardScene;
|
||||||
|
hasError?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreatePublicDashboardBase = ({
|
||||||
|
unsupportedDatasources = [],
|
||||||
|
unsupportedTemplateVariables = false,
|
||||||
|
dashboard,
|
||||||
|
hasError = false,
|
||||||
|
}: CreatePublicDashboarBaseProps) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const hasWritePermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPublicWrite);
|
const hasWritePermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPublicWrite);
|
||||||
const dashboardState = useSelector((store) => store.dashboard);
|
const [createPublicDashboard, { isLoading, isError }] = useCreatePublicDashboardMutation();
|
||||||
const dashboard = dashboardState.getModel()!;
|
const onCreate = () => {
|
||||||
|
|
||||||
const { unsupportedDataSources } = useGetUnsupportedDataSources(dashboard);
|
|
||||||
const [createPublicDashboard, { isLoading: isSaveLoading }] = useCreatePublicDashboardMutation();
|
|
||||||
|
|
||||||
const disableInputs = !hasWritePermissions || isSaveLoading || isError;
|
|
||||||
|
|
||||||
const onCreate = async () => {
|
|
||||||
trackDashboardSharingActionPerType('generate_public_url', shareDashboardType.publicDashboard);
|
|
||||||
createPublicDashboard({ dashboard, payload: { isEnabled: true } });
|
createPublicDashboard({ dashboard, payload: { isEnabled: true } });
|
||||||
|
trackDashboardSharingActionPerType('generate_public_url', shareDashboardType.publicDashboard);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const disableInputs = !hasWritePermissions || isLoading || isError || hasError;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div>
|
<div>
|
||||||
@ -52,10 +61,10 @@ const CreatePublicDashboard = ({ isError }: { isError: boolean }) => {
|
|||||||
|
|
||||||
{!hasWritePermissions && <NoUpsertPermissionsAlert mode="create" />}
|
{!hasWritePermissions && <NoUpsertPermissionsAlert mode="create" />}
|
||||||
|
|
||||||
{dashboardHasTemplateVariables(dashboard.getVariables()) && <UnsupportedTemplateVariablesAlert />}
|
{unsupportedTemplateVariables && <UnsupportedTemplateVariablesAlert />}
|
||||||
|
|
||||||
{!!unsupportedDataSources.length && (
|
{unsupportedDatasources.length > 0 && (
|
||||||
<UnsupportedDataSourcesAlert unsupportedDataSources={unsupportedDataSources.join(', ')} />
|
<UnsupportedDataSourcesAlert unsupportedDataSources={unsupportedDatasources.join(', ')} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form onSubmit={onCreate} validateOn="onChange" maxWidth="none">
|
<Form onSubmit={onCreate} validateOn="onChange" maxWidth="none">
|
||||||
@ -72,7 +81,7 @@ const CreatePublicDashboard = ({ isError }: { isError: boolean }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
<Button type="submit" disabled={disableInputs || !isValid} data-testid={selectors.CreateButton}>
|
<Button type="submit" disabled={disableInputs || !isValid} data-testid={selectors.CreateButton}>
|
||||||
Generate public URL {isSaveLoading && <Spinner className={styles.loadingSpinner} />}
|
Generate public URL {isLoading && <Spinner className={styles.loadingSpinner} />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -82,6 +91,22 @@ const CreatePublicDashboard = ({ isError }: { isError: boolean }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function CreatePublicDashboard({ hasError }: { hasError?: boolean }) {
|
||||||
|
const dashboardState = useSelector((store) => store.dashboard);
|
||||||
|
const dashboard = dashboardState.getModel()!;
|
||||||
|
const { unsupportedDataSources } = useGetUnsupportedDataSources(dashboard);
|
||||||
|
const hasTemplateVariables = dashboardHasTemplateVariables(dashboard.getVariables());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CreatePublicDashboardBase
|
||||||
|
dashboard={dashboard}
|
||||||
|
unsupportedDatasources={unsupportedDataSources}
|
||||||
|
unsupportedTemplateVariables={hasTemplateVariables}
|
||||||
|
hasError={hasError}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
container: css`
|
container: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -107,5 +132,3 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
margin-left: ${theme.spacing(1)};
|
margin-left: ${theme.spacing(1)};
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default CreatePublicDashboard;
|
|
||||||
|
@ -6,14 +6,17 @@ import { Spinner, useStyles2 } from '@grafana/ui/src';
|
|||||||
import { useGetPublicDashboardQuery } from 'app/features/dashboard/api/publicDashboardApi';
|
import { useGetPublicDashboardQuery } from 'app/features/dashboard/api/publicDashboardApi';
|
||||||
import { publicDashboardPersisted } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
|
import { publicDashboardPersisted } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
|
||||||
import { ShareModalTabProps } from 'app/features/dashboard/components/ShareModal/types';
|
import { ShareModalTabProps } from 'app/features/dashboard/components/ShareModal/types';
|
||||||
|
import { useSelector } from 'app/types';
|
||||||
|
|
||||||
import { HorizontalGroup } from '../../../../plugins/admin/components/HorizontalGroup';
|
import { HorizontalGroup } from '../../../../plugins/admin/components/HorizontalGroup';
|
||||||
|
|
||||||
import ConfigPublicDashboard from './ConfigPublicDashboard/ConfigPublicDashboard';
|
import { ConfigPublicDashboard } from './ConfigPublicDashboard/ConfigPublicDashboard';
|
||||||
import CreatePublicDashboard from './CreatePublicDashboard/CreatePublicDashboard';
|
import { CreatePublicDashboard } from './CreatePublicDashboard/CreatePublicDashboard';
|
||||||
|
import { useGetUnsupportedDataSources } from './useGetUnsupportedDataSources';
|
||||||
|
|
||||||
interface Props extends ShareModalTabProps {}
|
interface Props extends ShareModalTabProps {}
|
||||||
|
|
||||||
const Loader = () => {
|
export const Loader = () => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -28,28 +31,31 @@ const Loader = () => {
|
|||||||
|
|
||||||
export const SharePublicDashboard = (props: Props) => {
|
export const SharePublicDashboard = (props: Props) => {
|
||||||
const { data: publicDashboard, isLoading, isError } = useGetPublicDashboardQuery(props.dashboard.uid);
|
const { data: publicDashboard, isLoading, isError } = useGetPublicDashboardQuery(props.dashboard.uid);
|
||||||
|
const dashboardState = useSelector((store) => store.dashboard);
|
||||||
|
const dashboard = dashboardState.getModel()!;
|
||||||
|
const { unsupportedDataSources } = useGetUnsupportedDataSources(dashboard);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader />
|
<Loader />
|
||||||
) : !publicDashboardPersisted(publicDashboard) ? (
|
) : !publicDashboardPersisted(publicDashboard) ? (
|
||||||
<CreatePublicDashboard isError={isError} />
|
<CreatePublicDashboard hasError={isError} />
|
||||||
) : (
|
) : (
|
||||||
<ConfigPublicDashboard />
|
<ConfigPublicDashboard publicDashboard={publicDashboard!} unsupportedDatasources={unsupportedDataSources} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
loadingContainer: css`
|
loadingContainer: css({
|
||||||
height: 280px;
|
height: '280px',
|
||||||
align-items: center;
|
alignItems: 'center',
|
||||||
justify-content: center;
|
justifyContent: 'center',
|
||||||
gap: ${theme.spacing(1)};
|
gap: theme.spacing(1),
|
||||||
`,
|
}),
|
||||||
spinner: css`
|
spinner: css({
|
||||||
margin-bottom: ${theme.spacing(0)};
|
marginBottom: theme.spacing(0),
|
||||||
`,
|
}),
|
||||||
});
|
});
|
||||||
|
@ -1178,6 +1178,7 @@
|
|||||||
"library-panel": "Bibliotheks-Panel",
|
"library-panel": "Bibliotheks-Panel",
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
"panel-embed": "",
|
"panel-embed": "",
|
||||||
|
"public-dashboard": "",
|
||||||
"snapshot": "Schnappschuss"
|
"snapshot": "Schnappschuss"
|
||||||
},
|
},
|
||||||
"theme-picker": {
|
"theme-picker": {
|
||||||
|
@ -1178,6 +1178,7 @@
|
|||||||
"library-panel": "Library panel",
|
"library-panel": "Library panel",
|
||||||
"link": "Link",
|
"link": "Link",
|
||||||
"panel-embed": "Embed",
|
"panel-embed": "Embed",
|
||||||
|
"public-dashboard": "Public Dashboard",
|
||||||
"snapshot": "Snapshot"
|
"snapshot": "Snapshot"
|
||||||
},
|
},
|
||||||
"theme-picker": {
|
"theme-picker": {
|
||||||
|
@ -1184,6 +1184,7 @@
|
|||||||
"library-panel": "Panel de librería",
|
"library-panel": "Panel de librería",
|
||||||
"link": "Enlace",
|
"link": "Enlace",
|
||||||
"panel-embed": "",
|
"panel-embed": "",
|
||||||
|
"public-dashboard": "",
|
||||||
"snapshot": "Instantánea"
|
"snapshot": "Instantánea"
|
||||||
},
|
},
|
||||||
"theme-picker": {
|
"theme-picker": {
|
||||||
|
@ -1184,6 +1184,7 @@
|
|||||||
"library-panel": "Panneau de bibliothèque",
|
"library-panel": "Panneau de bibliothèque",
|
||||||
"link": "Lien",
|
"link": "Lien",
|
||||||
"panel-embed": "",
|
"panel-embed": "",
|
||||||
|
"public-dashboard": "",
|
||||||
"snapshot": "Instantané"
|
"snapshot": "Instantané"
|
||||||
},
|
},
|
||||||
"theme-picker": {
|
"theme-picker": {
|
||||||
|
@ -1178,6 +1178,7 @@
|
|||||||
"library-panel": "Ŀįþřäřy päʼnęľ",
|
"library-panel": "Ŀįþřäřy päʼnęľ",
|
||||||
"link": "Ŀįʼnĸ",
|
"link": "Ŀįʼnĸ",
|
||||||
"panel-embed": "Ēmþęđ",
|
"panel-embed": "Ēmþęđ",
|
||||||
|
"public-dashboard": "Pūþľįč Đäşĥþőäřđ",
|
||||||
"snapshot": "Ŝʼnäpşĥőŧ"
|
"snapshot": "Ŝʼnäpşĥőŧ"
|
||||||
},
|
},
|
||||||
"theme-picker": {
|
"theme-picker": {
|
||||||
|
@ -1172,6 +1172,7 @@
|
|||||||
"library-panel": "库面板",
|
"library-panel": "库面板",
|
||||||
"link": "链接",
|
"link": "链接",
|
||||||
"panel-embed": "",
|
"panel-embed": "",
|
||||||
|
"public-dashboard": "",
|
||||||
"snapshot": "快照"
|
"snapshot": "快照"
|
||||||
},
|
},
|
||||||
"theme-picker": {
|
"theme-picker": {
|
||||||
|
Loading…
Reference in New Issue
Block a user