diff --git a/static/app/views/dashboards/controls.tsx b/static/app/views/dashboards/controls.tsx
index 16441fd70732cd..af1799e81b08ba 100644
--- a/static/app/views/dashboards/controls.tsx
+++ b/static/app/views/dashboards/controls.tsx
@@ -4,14 +4,14 @@ import styled from '@emotion/styled';
import {updateDashboardFavorite} from 'sentry/actionCreators/dashboards';
import Feature from 'sentry/components/acl/feature';
import FeatureDisabled from 'sentry/components/acl/featureDisabled';
-import Confirm from 'sentry/components/confirm';
+import Confirm, {openConfirmModal} from 'sentry/components/confirm';
import {Button} from 'sentry/components/core/button';
import {ButtonBar} from 'sentry/components/core/button/buttonBar';
import {Tooltip} from 'sentry/components/core/tooltip';
import {DropdownMenu, type MenuItemProps} from 'sentry/components/dropdownMenu';
import {Hovercard} from 'sentry/components/hovercard';
import LoadingIndicator from 'sentry/components/loadingIndicator';
-import {IconAdd, IconDownload, IconEdit, IconStar} from 'sentry/icons';
+import {IconAdd, IconCopy, IconDownload, IconEdit, IconStar} from 'sentry/icons';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Organization} from 'sentry/types/organization';
@@ -19,12 +19,14 @@ import {defined} from 'sentry/utils';
import {trackAnalytics} from 'sentry/utils/analytics';
import {useQueryClient} from 'sentry/utils/queryClient';
import useApi from 'sentry/utils/useApi';
+import {useNavigate} from 'sentry/utils/useNavigate';
import useOrganization from 'sentry/utils/useOrganization';
import {useUser} from 'sentry/utils/useUser';
import {useUserTeams} from 'sentry/utils/useUserTeams';
import {DASHBOARD_SAVING_MESSAGE} from 'sentry/views/dashboards/constants';
import {DashboardCreateLimitWrapper} from 'sentry/views/dashboards/createLimitWrapper';
import EditAccessSelector from 'sentry/views/dashboards/editAccessSelector';
+import {useDuplicatePrebuiltDashboard} from 'sentry/views/dashboards/hooks/useDuplicateDashboard';
import {DataSet} from 'sentry/views/dashboards/widgetBuilder/utils';
import {checkUserHasEditAccess} from './utils/checkUserHasEditAccess';
@@ -84,6 +86,14 @@ function Controls({
const currentUser = useUser();
const {teams: userTeams} = useUserTeams();
const api = useApi();
+ const navigate = useNavigate();
+
+ const {duplicatePrebuiltDashboard, isLoading: isLoadingDuplicatePrebuiltDashboard} =
+ useDuplicatePrebuiltDashboard({
+ onSuccess: (newDashboard: DashboardDetails) => {
+ navigate(`/organizations/${organization.slug}/dashboard/${newDashboard.id}/`);
+ },
+ });
const isPrebuiltDashboard = defined(dashboard.prebuiltId);
@@ -297,7 +307,7 @@ function Controls({
)}
{renderEditButton(hasFeature)}
- {hasFeature && !isPrebuiltDashboard ? (
+ {hasFeature && !isPrebuiltDashboard && (
- ) : null}
+ )}
+ {hasFeature && isPrebuiltDashboard && (
+
+ {({
+ hasReachedDashboardLimit,
+ isLoading: isLoadingDashboardsLimit,
+ limitMessage,
+ }) => {
+ const isLoading =
+ isLoadingDuplicatePrebuiltDashboard || isLoadingDashboardsLimit;
+ return (
+
+
+
+ );
+ }}
+
+ )}
)}
diff --git a/static/app/views/dashboards/hooks/useDuplicateDashboard.tsx b/static/app/views/dashboards/hooks/useDuplicateDashboard.tsx
index f586d39b8714bf..95738738dfbf50 100644
--- a/static/app/views/dashboards/hooks/useDuplicateDashboard.tsx
+++ b/static/app/views/dashboards/hooks/useDuplicateDashboard.tsx
@@ -1,4 +1,4 @@
-import {useCallback} from 'react';
+import {useCallback, useState} from 'react';
import {createDashboard, fetchDashboard} from 'sentry/actionCreators/dashboards';
import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
@@ -6,11 +6,15 @@ import {t} from 'sentry/locale';
import {trackAnalytics} from 'sentry/utils/analytics';
import useApi from 'sentry/utils/useApi';
import useOrganization from 'sentry/utils/useOrganization';
-import type {DashboardListItem} from 'sentry/views/dashboards/types';
+import type {DashboardDetails, DashboardListItem} from 'sentry/views/dashboards/types';
import {cloneDashboard} from 'sentry/views/dashboards/utils';
+import {
+ PREBUILT_DASHBOARDS,
+ type PrebuiltDashboardId,
+} from 'sentry/views/dashboards/utils/prebuiltConfigs';
interface UseDuplicateDashboardProps {
- onSuccess: () => void;
+ onSuccess?: (copiedDashboard: DashboardDetails) => void;
}
export function useDuplicateDashboard({onSuccess}: UseDuplicateDashboardProps) {
@@ -27,13 +31,18 @@ export function useDuplicateDashboard({onSuccess}: UseDuplicateDashboardProps) {
);
const newDashboard = cloneDashboard(dashboardDetail);
newDashboard.widgets.map(widget => (widget.id = undefined));
- await createDashboard(api, organization.slug, newDashboard, true);
+ const copiedDashboard = await createDashboard(
+ api,
+ organization.slug,
+ newDashboard,
+ true
+ );
trackAnalytics('dashboards_manage.duplicate', {
organization,
dashboard_id: parseInt(dashboard.id, 10),
view_type: viewType,
});
- onSuccess();
+ onSuccess?.(copiedDashboard);
addSuccessMessage(t('Dashboard duplicated'));
} catch (e) {
addErrorMessage(t('Error duplicating Dashboard'));
@@ -44,3 +53,42 @@ export function useDuplicateDashboard({onSuccess}: UseDuplicateDashboardProps) {
return duplicateDashboard;
}
+
+export function useDuplicatePrebuiltDashboard({onSuccess}: UseDuplicateDashboardProps) {
+ const api = useApi();
+ const organization = useOrganization();
+ 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'
+ );
+ }
+ const prebuiltDashboard = {id: '-1', ...PREBUILT_DASHBOARDS[prebuiltId]};
+ try {
+ const newDashboard = cloneDashboard(prebuiltDashboard);
+ delete newDashboard.prebuiltId;
+ newDashboard.title = `${newDashboard.title} copy`;
+ newDashboard.widgets.map(widget => (widget.id = undefined));
+ setIsLoading(true);
+ const copiedDashboard = await createDashboard(
+ api,
+ organization.slug,
+ newDashboard,
+ true
+ );
+ onSuccess?.(copiedDashboard);
+ addSuccessMessage(t('Dashboard duplicated'));
+ } catch (e) {
+ addErrorMessage(t('Error duplicating Dashboard'));
+ } finally {
+ setIsLoading(false);
+ }
+ },
+ [api, organization, onSuccess]
+ );
+
+ return {duplicatePrebuiltDashboard, isLoading};
+}