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',
EnableAnnotationsSwitch: 'data-testid public dashboard on off switch for annotations',
SaveConfigButton: 'data-testid public dashboard save config button',
DeleteButton: 'data-testid public dashboard delete button',
CopyUrlInput: 'data-testid public dashboard copy url input',
CopyUrlButton: 'data-testid public dashboard copy url button',
TemplateVariablesWarningAlert: 'data-testid public dashboard disabled template variables alert',

View File

@ -36,7 +36,7 @@ export const publicDashboardApi = createApi({
reducerPath: 'publicDashboardApi',
baseQuery: retry(backendSrvBaseQuery({ baseUrl: '/api/dashboards' }), { maxRetries: 0 }),
tagTypes: ['PublicDashboard', 'AuditTablePublicDashboard'],
keepUnusedDataFor: 0,
refetchOnMountOrArgChange: true,
endpoints: (builder) => ({
getPublicDashboard: builder.query<PublicDashboard, string>({
query: (dashboardUid) => ({
@ -53,7 +53,7 @@ export const publicDashboardApi = createApi({
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 }>({
query: (params) => ({
@ -72,7 +72,7 @@ export const publicDashboardApi = createApi({
publicDashboardEnabled: data.isEnabled,
});
},
invalidatesTags: ['PublicDashboard'],
invalidatesTags: (result, error, { payload }) => [{ type: 'PublicDashboard', id: payload.dashboardUid }],
}),
updatePublicDashboard: builder.mutation<PublicDashboard, { dashboard: DashboardModel; payload: PublicDashboard }>({
query: (params) => ({
@ -92,7 +92,7 @@ export const publicDashboardApi = createApi({
publicDashboardEnabled: data.isEnabled,
});
},
invalidatesTags: ['PublicDashboard'],
invalidatesTags: (result, error, { payload }) => [{ type: 'PublicDashboard', id: payload.dashboardUid }],
}),
listPublicDashboards: builder.query<ListPublicDashboardResponse[], void>({
query: () => ({
@ -100,25 +100,25 @@ export const publicDashboardApi = createApi({
}),
providesTags: ['AuditTablePublicDashboard'],
}),
deletePublicDashboard: builder.mutation<void, { dashboardTitle: string; dashboardUid: string; uid: string }>({
deletePublicDashboard: builder.mutation<void, { dashboard?: DashboardModel; dashboardUid: string; uid: string }>({
query: (params) => ({
url: `/uid/${params.dashboardUid}/public-dashboards/${params.uid}`,
method: 'DELETE',
}),
async onQueryStarted({ dashboardTitle }, { dispatch, queryFulfilled }) {
async onQueryStarted({ dashboard, uid }, { dispatch, queryFulfilled }) {
await queryFulfilled;
dispatch(
notifyApp(
createSuccessNotification(
'Public dashboard deleted',
!!dashboardTitle
? `Public dashboard for ${dashboardTitle} has been deleted`
: `Public dashboard has been deleted`
)
)
);
dispatch(notifyApp(createSuccessNotification('Public dashboard deleted!')));
dashboard?.updateMeta({
hasPublicDashboard: false,
publicDashboardUid: uid,
publicDashboardEnabled: false,
});
},
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')).not.toHaveTextContent('Public dashboard');
});
it('renders share panel when public dashboards feature is enabled', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
@ -110,8 +109,23 @@ describe('SharePublic', () => {
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 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 () => {
expect(mockDashboard.time).toEqual({ from: 'now-6h', to: 'now' });
@ -137,13 +151,15 @@ describe('SharePublic', () => {
mockDashboard.meta.hasPublicDashboard = true;
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.LimitedDSCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableSwitch)).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 () => {
mockDashboard.meta.hasPublicDashboard = true;
@ -154,14 +170,16 @@ describe('SharePublic', () => {
);
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.LimitedDSCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableSwitch)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableAnnotationsSwitch)).toBeDisabled();
expect(screen.getByText('Save public dashboard')).toBeInTheDocument();
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
});
@ -183,7 +201,9 @@ describe('SharePublic - New config setup', () => {
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeEnabled();
expect(screen.getByTestId(selectors.EnableSwitch)).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();
});
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.CostIncreaseCheckbox));
expect(screen.getByText('Create public dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeDisabled();
});
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();
});
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', () => {
@ -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 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();
});
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 () => {
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)).toBeChecked();
});
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 waitForElementToBeRemoved(screen.getByTestId('Spinner'));
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
expect(screen.getByTestId(selectors.WillBePublicCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.LimitedDSCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.CostIncreaseCheckbox)).toBeDisabled();
expect(screen.getByTestId(selectors.EnableSwitch)).toBeEnabled();
expect(screen.getByText('Save public dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.SaveConfigButton)).toBeEnabled();
expect(screen.getByTestId(selectors.DeleteButton)).toBeEnabled();
});
it('when pubdash is enabled, then link url is available', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
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 () => {
@ -274,16 +310,21 @@ describe('SharePublic - Already persisted', () => {
);
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.getByTestId(selectors.EnableAnnotationsSwitch)).not.toBeChecked();
});
it('when pubdash is disabled by the user, then link url is not available', async () => {
await renderSharePublicDashboard({ panel: mockPanel, dashboard: mockDashboard, onDismiss: () => {} });
await waitForElementToBeRemoved(screen.getByTestId('Spinner'));
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
fireEvent.click(screen.getByTestId(selectors.EnableSwitch));
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 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 { selectors as e2eSelectors } from '@grafana/e2e-selectors/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 {
useGetPublicDashboardQuery,
@ -21,22 +34,31 @@ import {
publicDashboardPersisted,
} from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
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 { AccessControlAction } from 'app/types';
import { DashboardMetaChangedEvent } from '../../../../../types/events';
import { ShareModal } from '../ShareModal';
interface Props extends ShareModalTabProps {}
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 selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard;
const styles = useStyles2(getStyles);
const [hasPublicDashboard, setHasPublicDashboard] = useState(props.dashboard.meta.hasPublicDashboard);
const { hasPublicDashboard } = props.dashboard.meta;
const {
isLoading: isFetchingLoading,
isLoading: isGetLoading,
data: publicDashboard,
isError: isFetchingError,
isError: isGetError,
isFetching,
} = useGetPublicDashboardQuery(props.dashboard.uid, {
// if we don't have a public dashboard, don't try to load public dashboard
skip: !hasPublicDashboard,
@ -57,8 +79,12 @@ export const SharePublicDashboard = (props: Props) => {
const [annotationsEnabled, setAnnotationsEnabled] = useState(false);
useEffect(() => {
const eventSubs = new Subscription();
eventSubs.add(props.dashboard.events.subscribe(DashboardMetaChangedEvent, forceUpdate));
reportInteraction('grafana_dashboards_public_share_viewed');
}, []);
return () => eventSubs.unsubscribe();
}, [props.dashboard.events, forceUpdate]);
useEffect(() => {
if (publicDashboardPersisted(publicDashboard)) {
@ -73,19 +99,30 @@ export const SharePublicDashboard = (props: Props) => {
setEnabledSwitch((prevState) => ({ ...prevState, isEnabled: !!publicDashboard?.isEnabled }));
}, [publicDashboard]);
const isLoading = isFetchingLoading || isSaveLoading || isUpdateLoading;
const isLoading = isGetLoading || isSaveLoading || isUpdateLoading;
const hasWritePermissions = contextSrv.hasAccess(AccessControlAction.DashboardsPublicWrite, isOrgAdmin());
const acknowledged = acknowledgements.public && acknowledgements.datasources && acknowledgements.usage;
const isSaveEnabled = useMemo(
const isSaveDisabled = useMemo(
() =>
!hasWritePermissions ||
!acknowledged ||
props.dashboard.hasUnsavedChanges() ||
isLoading ||
isFetchingError ||
isFetching ||
isGetError ||
(!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 () => {
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
if (hasPublicDashboard) {
await updatePublicDashboard(req).unwrap();
setHasPublicDashboard(true);
} else {
await createPublicDashboard(req).unwrap();
setHasPublicDashboard(true);
}
hasPublicDashboard ? updatePublicDashboard(req) : createPublicDashboard(req);
};
const onAcknowledge = (field: string, checked: boolean) => {
setAcknowledgements((prevState) => ({ ...prevState, [field]: checked }));
};
const onDismissDelete = () => {
showModal(ShareModal, {
dashboard: props.dashboard,
onDismiss: hideModal,
activeTab: 'share',
});
};
return (
<>
<HorizontalGroup>
@ -120,7 +158,7 @@ export const SharePublicDashboard = (props: Props) => {
>
Welcome to Grafana public dashboards alpha!
</p>
{isFetchingLoading && <Spinner />}
{(isGetLoading || isFetching) && <Spinner />}
</HorizontalGroup>
<div className={styles.content}>
{dashboardHasTemplateVariables(dashboardVariables) && !publicDashboardPersisted(publicDashboard) ? (
@ -137,9 +175,7 @@ export const SharePublicDashboard = (props: Props) => {
<hr />
<div className={styles.checkboxes}>
<AcknowledgeCheckboxes
disabled={
publicDashboardPersisted(publicDashboard) || !hasWritePermissions || isLoading || isFetchingError
}
disabled={publicDashboardPersisted(publicDashboard) || !hasWritePermissions || isLoading || isGetError}
acknowledgements={acknowledgements}
onAcknowledge={onAcknowledge}
/>
@ -148,7 +184,7 @@ export const SharePublicDashboard = (props: Props) => {
<Configuration
isAnnotationsEnabled={annotationsEnabled}
dashboard={props.dashboard}
disabled={!hasWritePermissions || isLoading || isFetchingError}
disabled={!hasWritePermissions || isLoading || isGetError}
isPubDashEnabled={enabledSwitch.isEnabled}
onToggleEnabled={() =>
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" />
)}
<HorizontalGroup>
<Button disabled={isSaveEnabled} onClick={onSavePublicConfig} data-testid={selectors.SaveConfigButton}>
Save sharing configuration
</Button>
{isSaveLoading && <Spinner />}
<Layout orientation={isDesktop ? 0 : 1}>
<Button disabled={isSaveDisabled} onClick={onSavePublicConfig} data-testid={selectors.SaveConfigButton}>
{hasPublicDashboard ? 'Save public dashboard' : 'Create public dashboard'}
</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>
</>
)}

View File

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

View File

@ -94,7 +94,15 @@ export const PublicDashboardListTable = () => {
<Icon size={responsiveSize} name="cog" />
</LinkButton>
{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>
</td>
@ -134,9 +142,10 @@ function getStyles(theme: GrafanaTheme2, isMobile: boolean) {
orphanedTitle: css`
display: flex;
align-items: center;
gap: ${theme.spacing(1)};
p {
margin: ${theme.spacing(0, 1, 0, 0)};
margin: ${theme.spacing(0)};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;