Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions static/app/views/dashboards/controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,7 @@ export function Controls({
'Are you sure you want to duplicate this dashboard?'
),
priority: 'primary',
onConfirm: () =>
duplicatePrebuiltDashboard(dashboard.prebuiltId),
onConfirm: () => duplicatePrebuiltDashboard(dashboard.id),
});
}}
icon={isLoading ? <LoadingIndicator size={14} /> : <IconCopy />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
useDuplicateDashboard,
useDuplicatePrebuiltDashboard,
} from 'sentry/views/dashboards/hooks/useDuplicateDashboard';
import type {DashboardFilters} from 'sentry/views/dashboards/types';
import {WidgetType} from 'sentry/views/dashboards/types';
import {PrebuiltDashboardId} from 'sentry/views/dashboards/utils/prebuiltConfigs';

describe('useDuplicateDashboard', () => {
Expand Down Expand Up @@ -86,13 +88,63 @@ describe('useDuplicatePrebuiltDashboard', () => {
MockApiClient.clearMockResponses();
});

it('resolves and duplicates a prebuilt dashboard', async () => {
it('fetches saved dashboard details and duplicates with saved filters', async () => {
const savedFilters: DashboardFilters = {
globalFilter: [
{
dataset: WidgetType.SPANS,
tag: {key: 'db.normalized_description', name: 'db.normalized_description'},
value: '*billing*',
},
],
};
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/dashboards/55/`,
body: DashboardFixture([], {
id: '55',
prebuiltId: PrebuiltDashboardId.BACKEND_QUERIES_SUMMARY,
filters: savedFilters,
}),
});
const createMock = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/dashboards/`,
method: 'POST',
body: DashboardFixture([], {id: '300'}),
});

const onSuccess = jest.fn();
const {result} = renderHookWithProviders(
() => useDuplicatePrebuiltDashboard({onSuccess}),
{organization}
);

await act(async () => {
await result.current.duplicatePrebuiltDashboard('55');
});

expect(createMock).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
data: expect.objectContaining({filters: savedFilters}),
})
);
expect(onSuccess).toHaveBeenCalledWith(expect.objectContaining({id: '300'}));
});

it('resolves linked dashboard IDs from static config', async () => {
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/dashboards/55/`,
body: DashboardFixture([], {
id: '55',
prebuiltId: PrebuiltDashboardId.WEB_VITALS,
}),
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/dashboards/`,
method: 'GET',
body: [
DashboardFixture([], {
id: '55',
id: '77',
prebuiltId: PrebuiltDashboardId.WEB_VITALS_SUMMARY,
}),
],
Expand All @@ -110,20 +162,20 @@ describe('useDuplicatePrebuiltDashboard', () => {
);

await act(async () => {
await result.current.duplicatePrebuiltDashboard(PrebuiltDashboardId.WEB_VITALS);
await result.current.duplicatePrebuiltDashboard('55');
});

expect(createMock).toHaveBeenCalled();
expect(onSuccess).toHaveBeenCalledWith(expect.objectContaining({id: '300'}));
});

it('throws when no prebuiltId is provided', async () => {
it('throws when no dashboardId is provided', async () => {
const {result} = renderHookWithProviders(() => useDuplicatePrebuiltDashboard({}), {
organization,
});

await expect(result.current.duplicatePrebuiltDashboard(undefined)).rejects.toThrow(
'Prebuilt dashboard ID is required'
'Dashboard ID is required'
);
});
});
24 changes: 18 additions & 6 deletions static/app/views/dashboards/hooks/useDuplicateDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,35 @@ export function useDuplicatePrebuiltDashboard({onSuccess}: UseDuplicateDashboard
const [isLoading, setIsLoading] = useState(false);

const duplicatePrebuiltDashboard = useCallback(
async (prebuiltId?: PrebuiltDashboardId) => {
if (!prebuiltId) {
throw new Error(
'Prebuilt dashboard ID is required to duplicate a prebuilt dashboard'
);
async (dashboardId?: string) => {
if (!dashboardId) {
throw new Error('Dashboard ID is required to duplicate a prebuilt dashboard');
}
try {
setIsLoading(true);

// Fetch the saved dashboard to get the prebuilt ID and saved filters.
// Widgets are not stored for prebuilt dashboards, so we pull those
// from the static config and resolve any linked dashboard placeholders.
const savedDashboard = await fetchDashboard(api, organization.slug, dashboardId);

if (!savedDashboard.prebuiltId) {
throw new Error('Saved dashboard is missing its prebuilt ID');
}

const dashboardDetail = await resolveLinkedDashboardIds({
queryClient,
orgSlug: organization.slug,
dashboard: toPrebuiltDashboardDetails(prebuiltId),
dashboard: toPrebuiltDashboardDetails(savedDashboard.prebuiltId),
});

const newDashboard = cloneDashboard(dashboardDetail);
delete newDashboard.prebuiltId;
newDashboard.title = `${newDashboard.title} copy`;
newDashboard.widgets.map(widget => (widget.id = undefined));
if (savedDashboard.filters !== undefined) {
newDashboard.filters = savedDashboard.filters;
}
const copiedDashboard = await createDashboard(
api,
organization.slug,
Expand Down
Loading