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
59 changes: 55 additions & 4 deletions static/app/views/dashboards/controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,29 @@ 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';
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';
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -297,7 +307,7 @@ function Controls({
</Tooltip>
)}
{renderEditButton(hasFeature)}
{hasFeature && !isPrebuiltDashboard ? (
{hasFeature && !isPrebuiltDashboard && (
<Tooltip
title={tooltipMessage}
disabled={!widgetLimitReached && hasEditAccess}
Expand All @@ -316,7 +326,48 @@ function Controls({
position="bottom-end"
/>
</Tooltip>
) : null}
)}
{hasFeature && isPrebuiltDashboard && (
<DashboardCreateLimitWrapper>
{({
hasReachedDashboardLimit,
isLoading: isLoadingDashboardsLimit,
limitMessage,
}) => {
const isLoading =
isLoadingDuplicatePrebuiltDashboard || isLoadingDashboardsLimit;
return (
<Tooltip
title={t('Duplicate Dashboard')}
disabled={isLoading || hasReachedDashboardLimit}
>
<Button
data-test-id="dashboard-duplicate"
aria-label={t('duplicate-dashboard')}
onClick={e => {
e.preventDefault();
openConfirmModal({
message: t(
'Are you sure you want to duplicate this dashboard?'
),
priority: 'primary',
onConfirm: () =>
duplicatePrebuiltDashboard(dashboard.prebuiltId),
});
}}
icon={isLoading ? <LoadingIndicator size={14} /> : <IconCopy />}
disabled={isLoading || hasReachedDashboardLimit}
title={limitMessage}
priority="default"
size="sm"
>
{t('Duplicate Dashboard')}
</Button>
</Tooltip>
);
}}
</DashboardCreateLimitWrapper>
)}
</Fragment>
)}
</DashboardEditFeature>
Expand Down
58 changes: 53 additions & 5 deletions static/app/views/dashboards/hooks/useDuplicateDashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import {useCallback} from 'react';
import {useCallback, useState} from 'react';

import {createDashboard, fetchDashboard} from 'sentry/actionCreators/dashboards';
import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
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) {
Expand All @@ -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'));
Expand All @@ -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};
}
Loading