Scenes/ShareModal: Implement public dashboard tab (#76837)

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
kay delaney 2023-11-14 17:05:24 +00:00 committed by GitHub
parent c506da53f3
commit d0180957d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 520 additions and 124 deletions

View File

@ -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"]

View File

@ -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 = () => {

View File

@ -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,
}),
});

View File

@ -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} />;
}

View File

@ -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}
/>
);
}

View File

@ -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} />
)}
</>
);
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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 },

View File

@ -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;

View File

@ -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;

View File

@ -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),
}),
});

View File

@ -1178,6 +1178,7 @@
"library-panel": "Bibliotheks-Panel",
"link": "Link",
"panel-embed": "",
"public-dashboard": "",
"snapshot": "Schnappschuss"
},
"theme-picker": {

View File

@ -1178,6 +1178,7 @@
"library-panel": "Library panel",
"link": "Link",
"panel-embed": "Embed",
"public-dashboard": "Public Dashboard",
"snapshot": "Snapshot"
},
"theme-picker": {

View File

@ -1184,6 +1184,7 @@
"library-panel": "Panel de librería",
"link": "Enlace",
"panel-embed": "",
"public-dashboard": "",
"snapshot": "Instantánea"
},
"theme-picker": {

View File

@ -1184,6 +1184,7 @@
"library-panel": "Panneau de bibliothèque",
"link": "Lien",
"panel-embed": "",
"public-dashboard": "",
"snapshot": "Instantané"
},
"theme-picker": {

View File

@ -1178,6 +1178,7 @@
"library-panel": "Ŀįþřäřy päʼnęľ",
"link": "Ŀįʼnĸ",
"panel-embed": "Ēmþęđ",
"public-dashboard": "Pūþľįč Đäşĥþőäřđ",
"snapshot": "Ŝʼnäpşĥőŧ"
},
"theme-picker": {

View File

@ -1172,6 +1172,7 @@
"library-panel": "库面板",
"link": "链接",
"panel-embed": "",
"public-dashboard": "",
"snapshot": "快照"
},
"theme-picker": {