mirror of
https://github.com/grafana/grafana.git
synced 2024-11-29 04:04:00 -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": [
|
||||
[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"]
|
||||
|
@ -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 {
|
||||
@ -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());
|
||||
@ -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 }));
|
||||
}
|
||||
@ -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 = () => {
|
||||
|
@ -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,
|
||||
} from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
|
||||
import {
|
||||
PublicDashboardListWithPagination,
|
||||
PublicDashboardListWithPaginationResponse,
|
||||
@ -69,40 +70,61 @@ export const publicDashboardApi = createApi({
|
||||
}),
|
||||
createPublicDashboard: builder.mutation<
|
||||
PublicDashboard,
|
||||
{ dashboard: DashboardModel; payload: Partial<PublicDashboardSettings> }
|
||||
{ dashboard: DashboardModel | DashboardScene; payload: Partial<PublicDashboardSettings> }
|
||||
>({
|
||||
query: (params) => ({
|
||||
url: `/dashboards/uid/${params.dashboard.uid}/public-dashboards`,
|
||||
method: 'POST',
|
||||
data: params.payload,
|
||||
}),
|
||||
query: (params) => {
|
||||
const dashUid = params.dashboard instanceof DashboardScene ? params.dashboard.state.uid : params.dashboard.uid;
|
||||
return {
|
||||
url: `/dashboards/uid/${dashUid}/public-dashboards`,
|
||||
method: 'POST',
|
||||
data: params.payload,
|
||||
};
|
||||
},
|
||||
async onQueryStarted({ dashboard, payload }, { dispatch, queryFulfilled }) {
|
||||
const { data } = await queryFulfilled;
|
||||
dispatch(notifyApp(createSuccessNotification('Dashboard is public!')));
|
||||
|
||||
// Update runtime meta flag
|
||||
dashboard.updateMeta({
|
||||
publicDashboardUid: data.uid,
|
||||
publicDashboardEnabled: data.isEnabled,
|
||||
});
|
||||
if (dashboard instanceof DashboardScene) {
|
||||
dashboard.setState({
|
||||
meta: { ...dashboard.state.meta, publicDashboardEnabled: data.isEnabled, publicDashboardUid: data.uid },
|
||||
});
|
||||
} 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<
|
||||
PublicDashboard,
|
||||
{ dashboard: Partial<DashboardModel>; payload: Partial<PublicDashboard> }
|
||||
{
|
||||
dashboard: (Pick<DashboardModel, 'uid'> & Partial<Pick<DashboardModel, 'updateMeta'>>) | DashboardScene;
|
||||
payload: Partial<PublicDashboard>;
|
||||
}
|
||||
>({
|
||||
query: (params) => ({
|
||||
url: `/dashboards/uid/${params.dashboard.uid}/public-dashboards/${params.payload.uid}`,
|
||||
method: 'PATCH',
|
||||
data: params.payload,
|
||||
}),
|
||||
async onQueryStarted({ dashboard, payload }, { dispatch, queryFulfilled }) {
|
||||
query: ({ payload, dashboard }) => {
|
||||
const dashUid = dashboard instanceof DashboardScene ? dashboard.state.uid : dashboard.uid;
|
||||
return {
|
||||
url: `/dashboards/uid/${dashUid}/public-dashboards/${payload.uid}`,
|
||||
method: 'PATCH',
|
||||
data: payload,
|
||||
};
|
||||
},
|
||||
async onQueryStarted({ dashboard }, { dispatch, queryFulfilled }) {
|
||||
const { data } = await queryFulfilled;
|
||||
dispatch(notifyApp(createSuccessNotification('Public dashboard updated!')));
|
||||
|
||||
if (dashboard.updateMeta) {
|
||||
dashboard.updateMeta({
|
||||
if (dashboard instanceof DashboardScene) {
|
||||
dashboard.setState({
|
||||
meta: { ...dashboard.state.meta, publicDashboardEnabled: data.isEnabled, publicDashboardUid: data.uid },
|
||||
});
|
||||
} else {
|
||||
dashboard.updateMeta?.({
|
||||
publicDashboardUid: data.uid,
|
||||
publicDashboardEnabled: data.isEnabled,
|
||||
});
|
||||
@ -150,7 +172,10 @@ export const publicDashboardApi = createApi({
|
||||
}),
|
||||
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) => ({
|
||||
url: `/dashboards/uid/${params.dashboardUid}/public-dashboards/${params.uid}`,
|
||||
method: 'DELETE',
|
||||
@ -159,10 +184,16 @@ export const publicDashboardApi = createApi({
|
||||
await queryFulfilled;
|
||||
dispatch(notifyApp(createSuccessNotification('Public dashboard deleted!')));
|
||||
|
||||
dashboard?.updateMeta({
|
||||
publicDashboardUid: uid,
|
||||
publicDashboardEnabled: false,
|
||||
});
|
||||
if (dashboard instanceof DashboardScene) {
|
||||
dashboard.setState({
|
||||
meta: { ...dashboard.state.meta, publicDashboardUid: uid, publicDashboardEnabled: false },
|
||||
});
|
||||
} else {
|
||||
dashboard?.updateMeta({
|
||||
publicDashboardUid: uid,
|
||||
publicDashboardEnabled: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
invalidatesTags: (result, error, { dashboardUid }) => [
|
||||
{ type: 'PublicDashboard', id: dashboardUid },
|
||||
|
@ -1,27 +1,33 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useContext } from 'react';
|
||||
import React from 'react';
|
||||
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 { config, featureEnabled } from '@grafana/runtime/src';
|
||||
import {
|
||||
Button,
|
||||
ClipboardButton,
|
||||
Field,
|
||||
HorizontalGroup,
|
||||
Input,
|
||||
Label,
|
||||
ModalsContext,
|
||||
ModalsController,
|
||||
Switch,
|
||||
useStyles2,
|
||||
} from '@grafana/ui/src';
|
||||
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 { 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 { AccessControlAction, useSelector } from '../../../../../../types';
|
||||
import { DeletePublicDashboardButton } from '../../../../../manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardButton';
|
||||
import { useGetPublicDashboardQuery, useUpdatePublicDashboardMutation } from '../../../../api/publicDashboardApi';
|
||||
import { useIsDesktop } from '../../../../utils/screen';
|
||||
import { ShareModal } from '../../ShareModal';
|
||||
import { trackDashboardSharingActionPerType } from '../../analytics';
|
||||
@ -30,8 +36,11 @@ import { NoUpsertPermissionsAlert } from '../ModalAlerts/NoUpsertPermissionsAler
|
||||
import { SaveDashboardChangesAlert } from '../ModalAlerts/SaveDashboardChangesAlert';
|
||||
import { UnsupportedDataSourcesAlert } from '../ModalAlerts/UnsupportedDataSourcesAlert';
|
||||
import { UnsupportedTemplateVariablesAlert } from '../ModalAlerts/UnsupportedTemplateVariablesAlert';
|
||||
import { dashboardHasTemplateVariables, generatePublicDashboardUrl } from '../SharePublicDashboardUtils';
|
||||
import { useGetUnsupportedDataSources } from '../useGetUnsupportedDataSources';
|
||||
import {
|
||||
dashboardHasTemplateVariables,
|
||||
generatePublicDashboardUrl,
|
||||
PublicDashboard,
|
||||
} from '../SharePublicDashboardUtils';
|
||||
|
||||
import { Configuration } from './Configuration';
|
||||
import { EmailSharingConfiguration } from './EmailSharingConfiguration';
|
||||
@ -46,25 +55,33 @@ export interface ConfigPublicDashboardForm {
|
||||
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 isDesktop = useIsDesktop();
|
||||
const { showModal, hideModal } = useContext(ModalsContext);
|
||||
|
||||
const [update, { isLoading }] = useUpdatePublicDashboardMutation();
|
||||
const hasWritePermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPublicWrite);
|
||||
const disableInputs = !hasWritePermissions || isLoading;
|
||||
const hasEmailSharingEnabled =
|
||||
!!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>({
|
||||
defaultValues: {
|
||||
@ -74,33 +91,23 @@ const ConfigPublicDashboard = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const onUpdate = async (values: ConfigPublicDashboardForm) => {
|
||||
const onPublicDashboardUpdate = async (values: ConfigPublicDashboardForm) => {
|
||||
const { isAnnotationsEnabled, isTimeSelectionEnabled, isPaused } = values;
|
||||
|
||||
const req = {
|
||||
dashboard,
|
||||
update({
|
||||
dashboard: dashboard,
|
||||
payload: {
|
||||
...publicDashboard!,
|
||||
annotationsEnabled: isAnnotationsEnabled,
|
||||
timeSelectionEnabled: isTimeSelectionEnabled,
|
||||
isEnabled: !isPaused,
|
||||
},
|
||||
};
|
||||
|
||||
update(req);
|
||||
});
|
||||
};
|
||||
|
||||
const onChange = async (name: keyof ConfigPublicDashboardForm, value: boolean) => {
|
||||
setValue(name, value);
|
||||
await handleSubmit((data) => onUpdate(data))();
|
||||
};
|
||||
|
||||
const onDismissDelete = () => {
|
||||
showModal(ShareModal, {
|
||||
dashboard,
|
||||
onDismiss: hideModal,
|
||||
activeTab: shareDashboardType.publicDashboard,
|
||||
});
|
||||
await handleSubmit((data) => onPublicDashboardUpdate(data))();
|
||||
};
|
||||
|
||||
function onCopyURL() {
|
||||
@ -109,11 +116,11 @@ const ConfigPublicDashboard = () => {
|
||||
|
||||
return (
|
||||
<div className={styles.configContainer}>
|
||||
{hasWritePermissions && dashboard.hasUnsavedChanges() && <SaveDashboardChangesAlert />}
|
||||
{showSaveChangesAlert && <SaveDashboardChangesAlert />}
|
||||
{!hasWritePermissions && <NoUpsertPermissionsAlert mode="edit" />}
|
||||
{dashboardHasTemplateVariables(dashboardVariables) && <UnsupportedTemplateVariablesAlert />}
|
||||
{!!unsupportedDataSources.length && (
|
||||
<UnsupportedDataSourcesAlert unsupportedDataSources={unsupportedDataSources.join(', ')} />
|
||||
{hasTemplateVariables && <UnsupportedTemplateVariablesAlert />}
|
||||
{unsupportedDatasources.length > 0 && (
|
||||
<UnsupportedDataSourcesAlert unsupportedDataSources={unsupportedDatasources.join(', ')} />
|
||||
)}
|
||||
|
||||
{hasEmailSharingEnabled && <EmailSharingConfiguration />}
|
||||
@ -168,7 +175,7 @@ const ConfigPublicDashboard = () => {
|
||||
headerElement={({ className }) => (
|
||||
<SettingsSummary
|
||||
className={className}
|
||||
isDataLoading={isDataLoading}
|
||||
isDataLoading={isLoading}
|
||||
timeRange={timeRange}
|
||||
timeSelectionEnabled={publicDashboard?.timeSelectionEnabled}
|
||||
annotationsEnabled={publicDashboard?.annotationsEnabled}
|
||||
@ -186,27 +193,73 @@ const ConfigPublicDashboard = () => {
|
||||
align={isDesktop ? 'center' : 'normal'}
|
||||
>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<DeletePublicDashboardButton
|
||||
<Button
|
||||
aria-label="Revoke public URL"
|
||||
title="Revoke public URL"
|
||||
onClick={onRevoke}
|
||||
type="button"
|
||||
disabled={disableInputs}
|
||||
data-testid={selectors.DeleteButton}
|
||||
onDismiss={onDismissDelete}
|
||||
variant="destructive"
|
||||
fill="outline"
|
||||
dashboard={dashboard}
|
||||
publicDashboard={{
|
||||
uid: publicDashboard!.uid,
|
||||
dashboardUid: dashboard.uid,
|
||||
title: dashboard.title,
|
||||
}}
|
||||
>
|
||||
Revoke public URL
|
||||
</DeletePublicDashboardButton>
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</Layout>
|
||||
</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) => ({
|
||||
configContainer: css`
|
||||
@ -225,5 +278,3 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
display: 'inline-block',
|
||||
}),
|
||||
});
|
||||
|
||||
export default ConfigPublicDashboard;
|
||||
|
@ -5,10 +5,12 @@ import { FormState, UseFormRegister } from 'react-hook-form';
|
||||
import { GrafanaTheme2 } from '@grafana/data/src';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/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 { AccessControlAction, useSelector } from '../../../../../../types';
|
||||
import { useCreatePublicDashboardMutation } from '../../../../api/publicDashboardApi';
|
||||
import { trackDashboardSharingActionPerType } from '../../analytics';
|
||||
import { shareDashboardType } from '../../utils';
|
||||
import { NoUpsertPermissionsAlert } from '../ModalAlerts/NoUpsertPermissionsAlert';
|
||||
@ -27,22 +29,29 @@ export type SharePublicDashboardAcknowledgmentInputs = {
|
||||
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 hasWritePermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPublicWrite);
|
||||
const dashboardState = useSelector((store) => store.dashboard);
|
||||
const dashboard = dashboardState.getModel()!;
|
||||
|
||||
const { unsupportedDataSources } = useGetUnsupportedDataSources(dashboard);
|
||||
const [createPublicDashboard, { isLoading: isSaveLoading }] = useCreatePublicDashboardMutation();
|
||||
|
||||
const disableInputs = !hasWritePermissions || isSaveLoading || isError;
|
||||
|
||||
const onCreate = async () => {
|
||||
trackDashboardSharingActionPerType('generate_public_url', shareDashboardType.publicDashboard);
|
||||
const [createPublicDashboard, { isLoading, isError }] = useCreatePublicDashboardMutation();
|
||||
const onCreate = () => {
|
||||
createPublicDashboard({ dashboard, payload: { isEnabled: true } });
|
||||
trackDashboardSharingActionPerType('generate_public_url', shareDashboardType.publicDashboard);
|
||||
};
|
||||
|
||||
const disableInputs = !hasWritePermissions || isLoading || isError || hasError;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div>
|
||||
@ -52,10 +61,10 @@ const CreatePublicDashboard = ({ isError }: { isError: boolean }) => {
|
||||
|
||||
{!hasWritePermissions && <NoUpsertPermissionsAlert mode="create" />}
|
||||
|
||||
{dashboardHasTemplateVariables(dashboard.getVariables()) && <UnsupportedTemplateVariablesAlert />}
|
||||
{unsupportedTemplateVariables && <UnsupportedTemplateVariablesAlert />}
|
||||
|
||||
{!!unsupportedDataSources.length && (
|
||||
<UnsupportedDataSourcesAlert unsupportedDataSources={unsupportedDataSources.join(', ')} />
|
||||
{unsupportedDatasources.length > 0 && (
|
||||
<UnsupportedDataSourcesAlert unsupportedDataSources={unsupportedDatasources.join(', ')} />
|
||||
)}
|
||||
|
||||
<Form onSubmit={onCreate} validateOn="onChange" maxWidth="none">
|
||||
@ -72,7 +81,7 @@ const CreatePublicDashboard = ({ isError }: { isError: boolean }) => {
|
||||
</div>
|
||||
<div className={styles.buttonContainer}>
|
||||
<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>
|
||||
</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) => ({
|
||||
container: css`
|
||||
display: flex;
|
||||
@ -107,5 +132,3 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
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 { publicDashboardPersisted } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
|
||||
import { ShareModalTabProps } from 'app/features/dashboard/components/ShareModal/types';
|
||||
import { useSelector } from 'app/types';
|
||||
|
||||
import { HorizontalGroup } from '../../../../plugins/admin/components/HorizontalGroup';
|
||||
|
||||
import ConfigPublicDashboard from './ConfigPublicDashboard/ConfigPublicDashboard';
|
||||
import CreatePublicDashboard from './CreatePublicDashboard/CreatePublicDashboard';
|
||||
import { ConfigPublicDashboard } from './ConfigPublicDashboard/ConfigPublicDashboard';
|
||||
import { CreatePublicDashboard } from './CreatePublicDashboard/CreatePublicDashboard';
|
||||
import { useGetUnsupportedDataSources } from './useGetUnsupportedDataSources';
|
||||
|
||||
interface Props extends ShareModalTabProps {}
|
||||
|
||||
const Loader = () => {
|
||||
export const Loader = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
@ -28,28 +31,31 @@ const Loader = () => {
|
||||
|
||||
export const SharePublicDashboard = (props: Props) => {
|
||||
const { data: publicDashboard, isLoading, isError } = useGetPublicDashboardQuery(props.dashboard.uid);
|
||||
const dashboardState = useSelector((store) => store.dashboard);
|
||||
const dashboard = dashboardState.getModel()!;
|
||||
const { unsupportedDataSources } = useGetUnsupportedDataSources(dashboard);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Loader />
|
||||
) : !publicDashboardPersisted(publicDashboard) ? (
|
||||
<CreatePublicDashboard isError={isError} />
|
||||
<CreatePublicDashboard hasError={isError} />
|
||||
) : (
|
||||
<ConfigPublicDashboard />
|
||||
<ConfigPublicDashboard publicDashboard={publicDashboard!} unsupportedDatasources={unsupportedDataSources} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
loadingContainer: css`
|
||||
height: 280px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: ${theme.spacing(1)};
|
||||
`,
|
||||
spinner: css`
|
||||
margin-bottom: ${theme.spacing(0)};
|
||||
`,
|
||||
loadingContainer: css({
|
||||
height: '280px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
spinner: css({
|
||||
marginBottom: theme.spacing(0),
|
||||
}),
|
||||
});
|
||||
|
@ -1178,6 +1178,7 @@
|
||||
"library-panel": "Bibliotheks-Panel",
|
||||
"link": "Link",
|
||||
"panel-embed": "",
|
||||
"public-dashboard": "",
|
||||
"snapshot": "Schnappschuss"
|
||||
},
|
||||
"theme-picker": {
|
||||
|
@ -1178,6 +1178,7 @@
|
||||
"library-panel": "Library panel",
|
||||
"link": "Link",
|
||||
"panel-embed": "Embed",
|
||||
"public-dashboard": "Public Dashboard",
|
||||
"snapshot": "Snapshot"
|
||||
},
|
||||
"theme-picker": {
|
||||
|
@ -1184,6 +1184,7 @@
|
||||
"library-panel": "Panel de librería",
|
||||
"link": "Enlace",
|
||||
"panel-embed": "",
|
||||
"public-dashboard": "",
|
||||
"snapshot": "Instantánea"
|
||||
},
|
||||
"theme-picker": {
|
||||
|
@ -1184,6 +1184,7 @@
|
||||
"library-panel": "Panneau de bibliothèque",
|
||||
"link": "Lien",
|
||||
"panel-embed": "",
|
||||
"public-dashboard": "",
|
||||
"snapshot": "Instantané"
|
||||
},
|
||||
"theme-picker": {
|
||||
|
@ -1178,6 +1178,7 @@
|
||||
"library-panel": "Ŀįþřäřy päʼnęľ",
|
||||
"link": "Ŀįʼnĸ",
|
||||
"panel-embed": "Ēmþęđ",
|
||||
"public-dashboard": "Pūþľįč Đäşĥþőäřđ",
|
||||
"snapshot": "Ŝʼnäpşĥőŧ"
|
||||
},
|
||||
"theme-picker": {
|
||||
|
@ -1172,6 +1172,7 @@
|
||||
"library-panel": "库面板",
|
||||
"link": "链接",
|
||||
"panel-embed": "",
|
||||
"public-dashboard": "",
|
||||
"snapshot": "快照"
|
||||
},
|
||||
"theme-picker": {
|
||||
|
Loading…
Reference in New Issue
Block a user