PublicDashboards: Add delete public dashboard button in public dashboard modal (#58095)

- Delete public dashboard button added in public dashboard modal
- Delete public dashboard button refactored in order to be used in audit table and public dashboard modal
- Tests added
- RTK Query api modified, in order to keep cached data because of having to show public dashboard modal once delete modal is closed.
- RTK Query specific cached data invalidated for public dashboard
- Save button text changed: Create public dashboard when it was never created. Save public dashboard when there's a public dashboard already created
- Public Dashboard modal subscribed to DashboardModel metadata changes
This commit is contained in:
juanicabanas 2022-11-04 15:08:50 -03:00 committed by GitHub
parent ae30a0688a
commit 8f6cdd4cda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 195 additions and 77 deletions

View File

@ -188,6 +188,7 @@ export const Pages = {
EnableSwitch: 'data-testid public dashboard on off switch', EnableSwitch: 'data-testid public dashboard on off switch',
EnableAnnotationsSwitch: 'data-testid public dashboard on off switch for annotations', EnableAnnotationsSwitch: 'data-testid public dashboard on off switch for annotations',
SaveConfigButton: 'data-testid public dashboard save config button', SaveConfigButton: 'data-testid public dashboard save config button',
DeleteButton: 'data-testid public dashboard delete button',
CopyUrlInput: 'data-testid public dashboard copy url input', CopyUrlInput: 'data-testid public dashboard copy url input',
CopyUrlButton: 'data-testid public dashboard copy url button', CopyUrlButton: 'data-testid public dashboard copy url button',
TemplateVariablesWarningAlert: 'data-testid public dashboard disabled template variables alert', TemplateVariablesWarningAlert: 'data-testid public dashboard disabled template variables alert',

View File

@ -36,7 +36,7 @@ export const publicDashboardApi = createApi({
reducerPath: 'publicDashboardApi', reducerPath: 'publicDashboardApi',
baseQuery: retry(backendSrvBaseQuery({ baseUrl: '/api/dashboards' }), { maxRetries: 0 }), baseQuery: retry(backendSrvBaseQuery({ baseUrl: '/api/dashboards' }), { maxRetries: 0 }),
tagTypes: ['PublicDashboard', 'AuditTablePublicDashboard'], tagTypes: ['PublicDashboard', 'AuditTablePublicDashboard'],
keepUnusedDataFor: 0, refetchOnMountOrArgChange: true,
endpoints: (builder) => ({ endpoints: (builder) => ({
getPublicDashboard: builder.query<PublicDashboard, string>({ getPublicDashboard: builder.query<PublicDashboard, string>({
query: (dashboardUid) => ({ query: (dashboardUid) => ({
@ -53,7 +53,7 @@ export const publicDashboardApi = createApi({
dispatch(notifyApp(createErrorNotification(customError?.error?.data?.message))); dispatch(notifyApp(createErrorNotification(customError?.error?.data?.message)));
} }
}, },
providesTags: ['PublicDashboard'], providesTags: (result, error, dashboardUid) => [{ type: 'PublicDashboard', id: dashboardUid }],
}), }),
createPublicDashboard: builder.mutation<PublicDashboard, { dashboard: DashboardModel; payload: PublicDashboard }>({ createPublicDashboard: builder.mutation<PublicDashboard, { dashboard: DashboardModel; payload: PublicDashboard }>({
query: (params) => ({ query: (params) => ({
@ -72,7 +72,7 @@ export const publicDashboardApi = createApi({
publicDashboardEnabled: data.isEnabled, publicDashboardEnabled: data.isEnabled,
}); });
}, },
invalidatesTags: ['PublicDashboard'], invalidatesTags: (result, error, { payload }) => [{ type: 'PublicDashboard', id: payload.dashboardUid }],
}), }),
updatePublicDashboard: builder.mutation<PublicDashboard, { dashboard: DashboardModel; payload: PublicDashboard }>({ updatePublicDashboard: builder.mutation<PublicDashboard, { dashboard: DashboardModel; payload: PublicDashboard }>({
query: (params) => ({ query: (params) => ({
@ -92,7 +92,7 @@ export const publicDashboardApi = createApi({
publicDashboardEnabled: data.isEnabled, publicDashboardEnabled: data.isEnabled,
}); });
}, },
invalidatesTags: ['PublicDashboard'], invalidatesTags: (result, error, { payload }) => [{ type: 'PublicDashboard', id: payload.dashboardUid }],
}), }),
listPublicDashboards: builder.query<ListPublicDashboardResponse[], void>({ listPublicDashboards: builder.query<ListPublicDashboardResponse[], void>({
query: () => ({ query: () => ({
@ -100,25 +100,25 @@ export const publicDashboardApi = createApi({
}), }),
providesTags: ['AuditTablePublicDashboard'], providesTags: ['AuditTablePublicDashboard'],
}), }),
deletePublicDashboard: builder.mutation<void, { dashboardTitle: string; dashboardUid: string; uid: string }>({ deletePublicDashboard: builder.mutation<void, { dashboard?: DashboardModel; dashboardUid: string; uid: string }>({
query: (params) => ({ query: (params) => ({
url: `/uid/${params.dashboardUid}/public-dashboards/${params.uid}`, url: `/uid/${params.dashboardUid}/public-dashboards/${params.uid}`,
method: 'DELETE', method: 'DELETE',
}), }),
async onQueryStarted({ dashboardTitle }, { dispatch, queryFulfilled }) { async onQueryStarted({ dashboard, uid }, { dispatch, queryFulfilled }) {
await queryFulfilled; await queryFulfilled;
dispatch( dispatch(notifyApp(createSuccessNotification('Public dashboard deleted!')));
notifyApp(
createSuccessNotification( dashboard?.updateMeta({
'Public dashboard deleted', hasPublicDashboard: false,
!!dashboardTitle publicDashboardUid: uid,
? `Public dashboard for ${dashboardTitle} has been deleted` publicDashboardEnabled: false,
: `Public dashboard has been deleted` });
)
)
);
}, },
invalidatesTags: ['AuditTablePublicDashboard'], invalidatesTags: (result, error, { dashboardUid }) => [
{ type: 'PublicDashboard', id: dashboardUid },
'AuditTablePublicDashboard',
],
}), }),
}), }),
}); });

View File

@ -100,7 +100,6 @@ describe('SharePublic', () => {
expect(screen.getByRole('tablist')).toHaveTextContent('Link'); expect(screen.getByRole('tablist')).toHaveTextContent('Link');
expect(screen.getByRole('tablist')).not.toHaveTextContent('Public dashboard'); expect(screen.getByRole('tablist')).not.toHaveTextContent('Public dashboard');
}); });
it('renders share panel when public dashboards feature is enabled', async () => { it('renders share panel when public dashboards feature is enabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
@ -110,8 +109,23 @@ describe('SharePublic', () => {
fireEvent.click(screen.getByText('Public dashboard')); fireEvent.click(screen.getByText('Public dashboard'));
await screen.findByText('Welcome to Grafana public dashboards alpha!'); await screen.findByText('Welcome to Grafana public dashboards alpha!');
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
}); });
it('renders public dashboard modal without delete button because no public dashboard was already created', async () => {
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false);
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
expect(screen.getByRole('tablist')).toHaveTextContent('Link');
expect(screen.getByRole('tablist')).toHaveTextContent('Public dashboard');
fireEvent.click(screen.getByText('Public dashboard'));
await screen.findByText('Welcome to Grafana public dashboards alpha!');
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
});
it('renders default relative time in input', async () => { it('renders default relative time in input', async () => {
expect(mockDashboard.time).toEqual({ from: 'now-6h', to: 'now' }); expect(mockDashboard.time).toEqual({ from: 'now-6h', to: 'now' });
@ -137,13 +151,15 @@ describe('SharePublic', () => {
mockDashboard.meta.hasPublicDashboard = true; mockDashboard.meta.hasPublicDashboard = true;
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
expect(await screen.findByTestId('Spinner')).toBeInTheDocument(); screen.getAllByTestId('Spinner');
expect(screen.getByText('Save public dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled(); expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled(); expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled(); expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableSwitch)).toBeDisabled(); expect(screen.getByTestId(selectors.EnableSwitch)).toBeDisabled();
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled(); expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
}); });
it('when fetch errors happen, then all inputs remain disabled', async () => { it('when fetch errors happen, then all inputs remain disabled', async () => {
mockDashboard.meta.hasPublicDashboard = true; mockDashboard.meta.hasPublicDashboard = true;
@ -154,14 +170,16 @@ describe('SharePublic', () => {
); );
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getByTestId('Spinner')); await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled(); expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled(); expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled(); expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableSwitch)).toBeDisabled(); expect(screen.getByTestId(selectors.EnableSwitch)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeDisabled(); expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeDisabled();
expect(screen.getByText('Save public dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled(); expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
}); });
// test checking if current version of dashboard in state is persisted to db // test checking if current version of dashboard in state is persisted to db
}); });
@ -183,7 +201,9 @@ describe('SharePublic - New config setup', () => {
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeEnabled(); expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeEnabled();
expect(screen.getByTestId(selectors.EnableSwitch)).toBeEnabled(); expect(screen.getByTestId(selectors.EnableSwitch)).toBeEnabled();
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeEnabled(); expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeEnabled();
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled(); expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
}); });
it('when checkboxes are filled, then save button remains disabled', async () => { it('when checkboxes are filled, then save button remains disabled', async () => {
@ -194,6 +214,7 @@ describe('SharePublic - New config setup', () => {
fireEvent.click(screen.getByTestId(selectors.LimitedDSCheckbox)); fireEvent.click(screen.getByTestId(selectors.LimitedDSCheckbox));
fireEvent.click(screen.getByTestId(selectors.CostIncreaseCheckbox)); fireEvent.click(screen.getByTestId(selectors.CostIncreaseCheckbox));
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled(); expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
}); });
it('when checkboxes and switch are filled, then save button is enabled', async () => { it('when checkboxes and switch are filled, then save button is enabled', async () => {
@ -207,6 +228,10 @@ describe('SharePublic - New config setup', () => {
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled(); expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled();
}); });
it('when hasPublicDashboard flag is false, then button text is Create', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
});
}); });
describe('SharePublic - Already persisted', () => { describe('SharePublic - Already persisted', () => {
@ -228,33 +253,44 @@ describe('SharePublic - Already persisted', () => {
); );
}); });
it('when modal is opened, then save button is enabled', async () => { it('when modal is opened, then save button and delete button are enabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getByTestId('Spinner')); await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
expect(screen.getByTestId(selectors.DeleteButton)).toBeEnabled();
expect(screen.getByText('Save public dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled(); expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled();
}); });
it('delete button is not rendered because lack of permissions', async () => {
jest.spyOn(contextSrv, 'hasAccess').mockReturnValue(false);
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
expect(screen.queryByTestId(selectors.DeleteButton)).not.toBeInTheDocument();
});
it('when modal is opened, then annotations toggle is enabled and checked when its enabled in the db', async () => { it('when modal is opened, then annotations toggle is enabled and checked when its enabled in the db', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getByTestId('Spinner')); await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeEnabled(); expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeEnabled();
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeChecked(); expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeChecked();
}); });
it('when fetch is done, then loader spinner is gone, inputs are disabled and save button is enabled', async () => { it('when fetch is done, then loader spinner is gone, inputs are disabled and save button is enabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getByTestId('Spinner')); await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled(); expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled(); expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled(); expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableSwitch)).toBeEnabled(); expect(screen.getByTestId(selectors.EnableSwitch)).toBeEnabled();
expect(screen.getByText('Save public dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled(); expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled();
expect(screen.getByTestId(selectors.DeleteButton)).toBeEnabled();
}); });
it('when pubdash is enabled, then link url is available', async () => { it('when pubdash is enabled, then link url is available', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getByTestId('Spinner')); await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
expect(screen.getByTestId(selectors.CopyUrlInput)).toBeInTheDocument(); expect(screen.getByTestId(selectors.CopyUrlInput)).toBeInTheDocument();
}); });
it('when pubdash is disabled in the db, then link url is not available and annotations toggle is disabled', async () => { it('when pubdash is disabled in the db, then link url is not available and annotations toggle is disabled', async () => {
@ -274,16 +310,21 @@ describe('SharePublic - Already persisted', () => {
); );
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getByTestId('Spinner')); await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
expect(screen.queryByTestId(selectors.CopyUrlInput)).not.toBeInTheDocument(); expect(screen.queryByTestId(selectors.CopyUrlInput)).not.toBeInTheDocument();
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).not.toBeChecked(); expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).not.toBeChecked();
}); });
it('when pubdash is disabled by the user, then link url is not available', async () => { it('when pubdash is disabled by the user, then link url is not available', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} }); await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getByTestId('Spinner')); await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
fireEvent.click(screen.getByTestId(selectors.EnableSwitch)); fireEvent.click(screen.getByTestId(selectors.EnableSwitch));
expect(screen.queryByTestId(selectors.CopyUrlInput)).not.toBeInTheDocument(); expect(screen.queryByTestId(selectors.CopyUrlInput)).not.toBeInTheDocument();
}); });
it('when hasPublicDashboard flag is true, then button text is Save', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
expect(screen.getByText('Save public dashboard')).toBeInTheDocument();
});
}); });

View File

@ -1,10 +1,23 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useContext, useEffect, useMemo, useState } from 'react';
import { Subscription } from 'rxjs';
import { GrafanaTheme2 } from '@grafana/data/src'; import { GrafanaTheme2 } from '@grafana/data/src';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
import { reportInteraction } from '@grafana/runtime/src'; import { reportInteraction } from '@grafana/runtime/src';
import { Alert, Button, ClipboardButton, Field, HorizontalGroup, Input, useStyles2, Spinner } from '@grafana/ui/src'; import {
Alert,
Button,
ClipboardButton,
Field,
HorizontalGroup,
Input,
useStyles2,
Spinner,
ModalsContext,
useForceUpdate,
} from '@grafana/ui/src';
import { Layout } from '@grafana/ui/src/components/Layout/Layout';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { import {
useGetPublicDashboardQuery, useGetPublicDashboardQuery,
@ -21,22 +34,31 @@ import {
publicDashboardPersisted, publicDashboardPersisted,
} from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
import { ShareModalTabProps } from 'app/features/dashboard/components/ShareModal/types'; import { ShareModalTabProps } from 'app/features/dashboard/components/ShareModal/types';
import { useIsDesktop } from 'app/features/dashboard/utils/screen';
import { DeletePublicDashboardButton } from 'app/features/manage-dashboards/components/PublicDashboardListTable/DeletePublicDashboardButton';
import { isOrgAdmin } from 'app/features/plugins/admin/permissions'; import { isOrgAdmin } from 'app/features/plugins/admin/permissions';
import { AccessControlAction } from 'app/types'; import { AccessControlAction } from 'app/types';
import { DashboardMetaChangedEvent } from '../../../../../types/events';
import { ShareModal } from '../ShareModal';
interface Props extends ShareModalTabProps {} interface Props extends ShareModalTabProps {}
export const SharePublicDashboard = (props: Props) => { export const SharePublicDashboard = (props: Props) => {
const forceUpdate = useForceUpdate();
const styles = useStyles2(getStyles);
const { showModal, hideModal } = useContext(ModalsContext);
const isDesktop = useIsDesktop();
const dashboardVariables = props.dashboard.getVariables(); const dashboardVariables = props.dashboard.getVariables();
const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard; const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
const styles = useStyles2(getStyles); const { hasPublicDashboard } = props.dashboard.meta;
const [hasPublicDashboard, setHasPublicDashboard] = useState(props.dashboard.meta.hasPublicDashboard);
const { const {
isLoading: isFetchingLoading, isLoading: isGetLoading,
data: publicDashboard, data: publicDashboard,
isError: isFetchingError, isError: isGetError,
isFetching,
} = useGetPublicDashboardQuery(props.dashboard.uid, { } = useGetPublicDashboardQuery(props.dashboard.uid, {
// if we don't have a public dashboard, don't try to load public dashboard // if we don't have a public dashboard, don't try to load public dashboard
skip: !hasPublicDashboard, skip: !hasPublicDashboard,
@ -57,8 +79,12 @@ export const SharePublicDashboard = (props: Props) => {
const [annotationsEnabled, setAnnotationsEnabled] = useState(false); const [annotationsEnabled, setAnnotationsEnabled] = useState(false);
useEffect(() => { useEffect(() => {
const eventSubs = new Subscription();
eventSubs.add(props.dashboard.events.subscribe(DashboardMetaChangedEvent, forceUpdate));
reportInteraction('grafana_dashboards_public_share_viewed'); reportInteraction('grafana_dashboards_public_share_viewed');
}, []);
return () => eventSubs.unsubscribe();
}, [props.dashboard.events, forceUpdate]);
useEffect(() => { useEffect(() => {
if (publicDashboardPersisted(publicDashboard)) { if (publicDashboardPersisted(publicDashboard)) {
@ -73,19 +99,30 @@ export const SharePublicDashboard = (props: Props) => {
setEnabledSwitch((prevState) => ({ ...prevState, isEnabled: !!publicDashboard?.isEnabled })); setEnabledSwitch((prevState) => ({ ...prevState, isEnabled: !!publicDashboard?.isEnabled }));
}, [publicDashboard]); }, [publicDashboard]);
const isLoading = isFetchingLoading || isSaveLoading || isUpdateLoading; const isLoading = isGetLoading || isSaveLoading || isUpdateLoading;
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin()); const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
const acknowledged = acknowledgements.public && acknowledgements.datasources && acknowledgements.usage; const acknowledged = acknowledgements.public && acknowledgements.datasources && acknowledgements.usage;
const isSaveEnabled = useMemo( const isSaveDisabled = useMemo(
() => () =>
!hasWritePermissions || !hasWritePermissions ||
!acknowledged || !acknowledged ||
props.dashboard.hasUnsavedChanges() || props.dashboard.hasUnsavedChanges() ||
isLoading || isLoading ||
isFetchingError || isFetching ||
isGetError ||
(!publicDashboardPersisted(publicDashboard) && !enabledSwitch.wasTouched), (!publicDashboardPersisted(publicDashboard) && !enabledSwitch.wasTouched),
[hasWritePermissions, acknowledged, props.dashboard, isLoading, isFetchingError, enabledSwitch, publicDashboard] [
hasWritePermissions,
acknowledged,
props.dashboard,
isLoading,
isGetError,
enabledSwitch,
publicDashboard,
isFetching,
]
); );
const isDeleteDisabled = isLoading || isFetching || isGetError;
const onSavePublicConfig = async () => { const onSavePublicConfig = async () => {
reportInteraction('grafana_dashboards_public_create_clicked'); reportInteraction('grafana_dashboards_public_create_clicked');
@ -96,20 +133,21 @@ export const SharePublicDashboard = (props: Props) => {
}; };
// create or update based on whether we have existing uid // create or update based on whether we have existing uid
hasPublicDashboard ? updatePublicDashboard(req) : createPublicDashboard(req);
if (hasPublicDashboard) {
await updatePublicDashboard(req).unwrap();
setHasPublicDashboard(true);
} else {
await createPublicDashboard(req).unwrap();
setHasPublicDashboard(true);
}
}; };
const onAcknowledge = (field: string, checked: boolean) => { const onAcknowledge = (field: string, checked: boolean) => {
setAcknowledgements((prevState) => ({ ...prevState, [field]: checked })); setAcknowledgements((prevState) => ({ ...prevState, [field]: checked }));
}; };
const onDismissDelete = () => {
showModal(ShareModal, {
dashboard: props.dashboard,
onDismiss: hideModal,
activeTab: 'share',
});
};
return ( return (
<> <>
<HorizontalGroup> <HorizontalGroup>
@ -120,7 +158,7 @@ export const SharePublicDashboard = (props: Props) => {
> >
Welcome to Grafana public dashboards alpha! Welcome to Grafana public dashboards alpha!
</p> </p>
{isFetchingLoading && <Spinner />} {(isGetLoading || isFetching) && <Spinner />}
</HorizontalGroup> </HorizontalGroup>
<div className={styles.content}> <div className={styles.content}>
{dashboardHasTemplateVariables(dashboardVariables) && !publicDashboardPersisted(publicDashboard) ? ( {dashboardHasTemplateVariables(dashboardVariables) && !publicDashboardPersisted(publicDashboard) ? (
@ -137,9 +175,7 @@ export const SharePublicDashboard = (props: Props) => {
<hr /> <hr />
<div className={styles.checkboxes}> <div className={styles.checkboxes}>
<AcknowledgeCheckboxes <AcknowledgeCheckboxes
disabled={ disabled={publicDashboardPersisted(publicDashboard) || !hasWritePermissions || isLoading || isGetError}
publicDashboardPersisted(publicDashboard) || !hasWritePermissions || isLoading || isFetchingError
}
acknowledgements={acknowledgements} acknowledgements={acknowledgements}
onAcknowledge={onAcknowledge} onAcknowledge={onAcknowledge}
/> />
@ -148,7 +184,7 @@ export const SharePublicDashboard = (props: Props) => {
<Configuration <Configuration
isAnnotationsEnabled={annotationsEnabled} isAnnotationsEnabled={annotationsEnabled}
dashboard={props.dashboard} dashboard={props.dashboard}
disabled={!hasWritePermissions || isLoading || isFetchingError} disabled={!hasWritePermissions || isLoading || isGetError}
isPubDashEnabled={enabledSwitch.isEnabled} isPubDashEnabled={enabledSwitch.isEnabled}
onToggleEnabled={() => onToggleEnabled={() =>
setEnabledSwitch((prevState) => ({ isEnabled: !prevState.isEnabled, wasTouched: true })) setEnabledSwitch((prevState) => ({ isEnabled: !prevState.isEnabled, wasTouched: true }))
@ -193,10 +229,28 @@ export const SharePublicDashboard = (props: Props) => {
<Alert title="You don't have permissions to create or update a public dashboard" severity="warning" /> <Alert title="You don't have permissions to create or update a public dashboard" severity="warning" />
)} )}
<HorizontalGroup> <HorizontalGroup>
<Button disabled={isSaveEnabled} onClick={onSavePublicConfig} data-testid={selectors.SaveConfigButton}> <Layout orientation={isDesktop ? 0 : 1}>
Save sharing configuration <Button disabled={isSaveDisabled} onClick={onSavePublicConfig} data-testid={selectors.SaveConfigButton}>
</Button> {hasPublicDashboard ? 'Save public dashboard' : 'Create public dashboard'}
{isSaveLoading && <Spinner />} </Button>
{publicDashboard && hasWritePermissions && (
<DeletePublicDashboardButton
disabled={isDeleteDisabled}
data-testid={selectors.DeleteButton}
onDismiss={onDismissDelete}
variant="destructive"
dashboard={props.dashboard}
publicDashboard={{
uid: publicDashboard.uid,
dashboardUid: props.dashboard.uid,
title: props.dashboard.title,
}}
>
Delete public dashboard
</DeletePublicDashboardButton>
)}
</Layout>
{(isSaveLoading || isFetching) && <Spinner />}
</HorizontalGroup> </HorizontalGroup>
</> </>
)} )}

View File

@ -1,47 +1,60 @@
import React from 'react'; import React from 'react';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { Button, ModalsController, ButtonProps } from '@grafana/ui/src';
import { Button, ComponentSize, Icon, ModalsController, Spinner } from '@grafana/ui/src'; import { useDeletePublicDashboardMutation } from 'app/features/dashboard/api/publicDashboardApi';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { useDeletePublicDashboardMutation } from '../../../dashboard/api/publicDashboardApi';
import { ListPublicDashboardResponse } from '../../types';
import { DeletePublicDashboardModal } from './DeletePublicDashboardModal'; import { DeletePublicDashboardModal } from './DeletePublicDashboardModal';
export interface PublicDashboardDeletion {
uid: string;
dashboardUid: string;
title: string;
}
export const DeletePublicDashboardButton = ({ export const DeletePublicDashboardButton = ({
dashboard,
publicDashboard, publicDashboard,
size, loader,
children,
onDismiss,
...rest
}: { }: {
publicDashboard: ListPublicDashboardResponse; dashboard?: DashboardModel;
size: ComponentSize; publicDashboard: PublicDashboardDeletion;
}) => { loader?: JSX.Element;
children: React.ReactNode;
onDismiss?: () => void;
} & ButtonProps) => {
const [deletePublicDashboard, { isLoading }] = useDeletePublicDashboardMutation(); const [deletePublicDashboard, { isLoading }] = useDeletePublicDashboardMutation();
const onDeletePublicDashboardClick = (pd: ListPublicDashboardResponse, onDelete: () => void) => { const onDeletePublicDashboardClick = (pd: PublicDashboardDeletion, onDelete: () => void) => {
deletePublicDashboard({ uid: pd.uid, dashboardUid: pd.dashboardUid, dashboardTitle: pd.title }); deletePublicDashboard({
dashboard,
uid: pd.uid,
dashboardUid: pd.dashboardUid,
});
onDelete(); onDelete();
}; };
const selectors = e2eSelectors.pages.PublicDashboards;
return ( return (
<ModalsController> <ModalsController>
{({ showModal, hideModal }) => ( {({ showModal, hideModal }) => (
<Button <Button
fill="text"
aria-label="Delete public dashboard" aria-label="Delete public dashboard"
title="Delete public dashboard" title="Delete public dashboard"
onClick={() => onClick={() =>
showModal(DeletePublicDashboardModal, { showModal(DeletePublicDashboardModal, {
dashboardTitle: publicDashboard.title, dashboardTitle: publicDashboard.title,
onConfirm: () => onDeletePublicDashboardClick(publicDashboard, hideModal), onConfirm: () => onDeletePublicDashboardClick(publicDashboard, hideModal),
onDismiss: hideModal, onDismiss: () => {
onDismiss ? onDismiss() : hideModal();
},
}) })
} }
data-testid={selectors.ListItem.trashcanButton} {...rest}
size={size}
> >
{isLoading ? <Spinner /> : <Icon size={size} name="trash-alt" />} {isLoading && loader ? loader : children}
</Button> </Button>
)} )}
</ModalsController> </ModalsController>

View File

@ -12,7 +12,7 @@ const Body = ({ title }: { title?: string }) => {
<p className={styles.title}>Do you want to delete this public dashboard?</p> <p className={styles.title}>Do you want to delete this public dashboard?</p>
<p className={styles.description}> <p className={styles.description}>
{title {title
? `This will delete the public dashboard for ${title}. Your dashboard will not be deleted.` ? `This will delete the public dashboard for "${title}". Your dashboard will not be deleted.`
: 'Orphaned public dashboard will be deleted'} : 'Orphaned public dashboard will be deleted'}
</p> </p>
</> </>

View File

@ -94,7 +94,15 @@ export const PublicDashboardListTable = () => {
<Icon size={responsiveSize} name="cog" /> <Icon size={responsiveSize} name="cog" />
</LinkButton> </LinkButton>
{hasWritePermissions && ( {hasWritePermissions && (
<DeletePublicDashboardButton publicDashboard={pd} size={responsiveSize} /> <DeletePublicDashboardButton
variant="primary"
fill="text"
data-testid={selectors.ListItem.trashcanButton}
publicDashboard={pd}
loader={<Spinner />}
>
<Icon size={responsiveSize} name="trash-alt" />
</DeletePublicDashboardButton>
)} )}
</ButtonGroup> </ButtonGroup>
</td> </td>
@ -134,9 +142,10 @@ function getStyles(theme: GrafanaTheme2, isMobile: boolean) {
orphanedTitle: css` orphanedTitle: css`
display: flex; display: flex;
align-items: center; align-items: center;
gap: ${theme.spacing(1)};
p { p {
margin: ${theme.spacing(0, 1, 0, 0)}; margin: ${theme.spacing(0)};
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;