diff --git a/.betterer.results b/.betterer.results index 8ca72ebce09..ac417c4cb5c 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"] diff --git a/public/app/features/dashboard-scene/sharing/ShareModal.tsx b/public/app/features/dashboard-scene/sharing/ShareModal.tsx index 7bdef7e27c9..afda14680b4 100644 --- a/public/app/features/dashboard-scene/sharing/ShareModal.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareModal.tsx @@ -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 implements ModalSceneObjectLike { static Component = SharePanelModalRenderer; - constructor(state: Omit) { + constructor(state: Omit & { activeTab?: string }) { super({ - ...state, activeTab: 'Link', + ...state, }); this.addActivationHandler(() => this.buildTabs()); @@ -50,6 +51,10 @@ export class ShareModal extends SceneObjectBase 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 implements Moda // }); // tabs.push(...customDashboardTabs); // } - - // if (Boolean(config.featureToggles['publicDashboards'])) { - // tabs.push({ - // label: 'Public dashboard', - // value: shareDashboardType.publicDashboard, - // component: SharePublicDashboard, - // }); - // } } onDismiss = () => { diff --git a/public/app/features/dashboard-scene/sharing/public-dashboards/ConfigPublicDashboard.tsx b/public/app/features/dashboard-scene/sharing/public-dashboards/ConfigPublicDashboard.tsx new file mode 100644 index 00000000000..c4dc496a989 --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/public-dashboards/ConfigPublicDashboard.tsx @@ -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 { + 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 ( + { + dashboard.showModal( + new ConfirmModal({ + isOpen: true, + title: 'Revoke public URL', + icon: 'trash-alt', + confirmText: 'Revoke public URL', + body: ( +

+ Are you sure you want to revoke this URL? The dashboard will no longer be public. +

+ ), + 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, + }), +}); diff --git a/public/app/features/dashboard-scene/sharing/public-dashboards/ConfirmModal.tsx b/public/app/features/dashboard-scene/sharing/public-dashboards/ConfirmModal.tsx new file mode 100644 index 00000000000..38c28676733 --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/public-dashboards/ConfirmModal.tsx @@ -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 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) { + const props = model.useState(); + return ; +} diff --git a/public/app/features/dashboard-scene/sharing/public-dashboards/CreatePublicDashboard.tsx b/public/app/features/dashboard-scene/sharing/public-dashboards/CreatePublicDashboard.tsx new file mode 100644 index 00000000000..5b01c56aa72 --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/public-dashboards/CreatePublicDashboard.tsx @@ -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) { + const { dashboardRef } = model.useState(); + const dashboard = dashboardRef.resolve(); + const unsupportedDataSources = useUnsupportedDatasources(dashboard); + const hasTemplateVariables = (dashboard.state.$variables?.state.variables.length ?? 0) > 0; + + return ( + + ); +} diff --git a/public/app/features/dashboard-scene/sharing/public-dashboards/SharePublicDashboardTab.tsx b/public/app/features/dashboard-scene/sharing/public-dashboards/SharePublicDashboardTab.tsx new file mode 100644 index 00000000000..e34123410cc --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/public-dashboards/SharePublicDashboardTab.tsx @@ -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; +} + +export class SharePublicDashboardTab extends SceneObjectBase { + static Component = SharePublicDashboardTabRenderer; + + public getTabLabel() { + return t('share-modal.tab-title.public-dashboard', 'Public Dashboard'); + } +} + +function SharePublicDashboardTabRenderer({ model }: SceneComponentProps) { + const { data: publicDashboard, isLoading: isGetLoading } = useGetPublicDashboardQuery( + model.state.dashboardRef.resolve().state.uid! + ); + + return ( + <> + {isGetLoading ? ( + + ) : !publicDashboardPersisted(publicDashboard) ? ( + + ) : ( + + )} + + ); +} diff --git a/public/app/features/dashboard-scene/sharing/public-dashboards/hooks.ts b/public/app/features/dashboard-scene/sharing/public-dashboards/hooks.ts new file mode 100644 index 00000000000..1d05385958a --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/public-dashboards/hooks.ts @@ -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; +} diff --git a/public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts b/public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts new file mode 100644 index 00000000000..763469c786c --- /dev/null +++ b/public/app/features/dashboard-scene/sharing/public-dashboards/utils.ts @@ -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 => { + let unsupportedDS = new Set(); + + 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(); + + 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(); + 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); +} diff --git a/public/app/features/dashboard-scene/sharing/utils.ts b/public/app/features/dashboard-scene/sharing/utils.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/public/app/features/dashboard/api/publicDashboardApi.ts b/public/app/features/dashboard/api/publicDashboardApi.ts index c1d31e70c81..e3c05f3ddf0 100644 --- a/public/app/features/dashboard/api/publicDashboardApi.ts +++ b/public/app/features/dashboard/api/publicDashboardApi.ts @@ -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 } + { dashboard: DashboardModel | DashboardScene; payload: Partial } >({ - 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; payload: Partial } + { + dashboard: (Pick & Partial>) | DashboardScene; + payload: Partial; + } >({ - 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({ + 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 }, diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx index 8def97feb8c..21d4a0b5542 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/ConfigPublicDashboard/ConfigPublicDashboard.tsx @@ -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({ 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 (
- {hasWritePermissions && dashboard.hasUnsavedChanges() && } + {showSaveChangesAlert && } {!hasWritePermissions && } - {dashboardHasTemplateVariables(dashboardVariables) && } - {!!unsupportedDataSources.length && ( - + {hasTemplateVariables && } + {unsupportedDatasources.length > 0 && ( + )} {hasEmailSharingEnabled && } @@ -168,7 +175,7 @@ const ConfigPublicDashboard = () => { headerElement={({ className }) => ( { align={isDesktop ? 'center' : 'normal'} > - Revoke public URL - +
); -}; +} + +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 ( + + {({ showModal, hideModal }) => ( + { + showModal(DeletePublicDashboardModal, { + dashboardTitle: dashboard.title, + onConfirm: () => onDeletePublicDashboardClick(hideModal), + onDismiss: () => { + showModal(ShareModal, { + dashboard, + onDismiss: hideModal, + activeTab: shareDashboardType.publicDashboard, + }); + }, + }); + }} + /> + )} + + ); +} const getStyles = (theme: GrafanaTheme2) => ({ configContainer: css` @@ -225,5 +278,3 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: 'inline-block', }), }); - -export default ConfigPublicDashboard; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard.tsx index 6cee539c193..f2e0c21f8dc 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/CreatePublicDashboard/CreatePublicDashboard.tsx @@ -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 (
@@ -52,10 +61,10 @@ const CreatePublicDashboard = ({ isError }: { isError: boolean }) => { {!hasWritePermissions && } - {dashboardHasTemplateVariables(dashboard.getVariables()) && } + {unsupportedTemplateVariables && } - {!!unsupportedDataSources.length && ( - + {unsupportedDatasources.length > 0 && ( + )}
@@ -72,7 +81,7 @@ const CreatePublicDashboard = ({ isError }: { isError: boolean }) => {
@@ -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 ( + + ); +} + const getStyles = (theme: GrafanaTheme2) => ({ container: css` display: flex; @@ -107,5 +132,3 @@ const getStyles = (theme: GrafanaTheme2) => ({ margin-left: ${theme.spacing(1)}; `, }); - -export default CreatePublicDashboard; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx index 746885fa058..36cbd0d3b96 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx @@ -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 ? ( ) : !publicDashboardPersisted(publicDashboard) ? ( - + ) : ( - + )} ); }; 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), + }), }); diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index fd58b58a87d..15aaf80a51c 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -1178,6 +1178,7 @@ "library-panel": "Bibliotheks-Panel", "link": "Link", "panel-embed": "", + "public-dashboard": "", "snapshot": "Schnappschuss" }, "theme-picker": { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 63035cceb67..db5fa55c05e 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1178,6 +1178,7 @@ "library-panel": "Library panel", "link": "Link", "panel-embed": "Embed", + "public-dashboard": "Public Dashboard", "snapshot": "Snapshot" }, "theme-picker": { diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 77397a91d1b..c2723074f05 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -1184,6 +1184,7 @@ "library-panel": "Panel de librería", "link": "Enlace", "panel-embed": "", + "public-dashboard": "", "snapshot": "Instantánea" }, "theme-picker": { diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 9b70f53c66f..fc648590c5f 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -1184,6 +1184,7 @@ "library-panel": "Panneau de bibliothèque", "link": "Lien", "panel-embed": "", + "public-dashboard": "", "snapshot": "Instantané" }, "theme-picker": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 32a321220db..bff14de3d53 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1178,6 +1178,7 @@ "library-panel": "Ŀįþřäřy päʼnęľ", "link": "Ŀįʼnĸ", "panel-embed": "Ēmþęđ", + "public-dashboard": "Pūþľįč Đäşĥþőäřđ", "snapshot": "Ŝʼnäpşĥőŧ" }, "theme-picker": { diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 3cb01978a3b..fb8d15866dc 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -1172,6 +1172,7 @@ "library-panel": "库面板", "link": "链接", "panel-embed": "", + "public-dashboard": "", "snapshot": "快照" }, "theme-picker": {