import styled from '@emotion/styled'; import type {Location} from 'history'; import cloneDeep from 'lodash/cloneDeep'; import { createDashboard, deleteDashboard, fetchDashboard, updateDashboardPermissions, } from 'sentry/actionCreators/dashboards'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import type {Client} from 'sentry/api'; import {ActivityAvatar} from 'sentry/components/activity/item/avatar'; import {Button} from 'sentry/components/button'; import {openConfirmModal} from 'sentry/components/confirm'; import EmptyStateWarning from 'sentry/components/emptyStateWarning'; import GridEditable, { COL_WIDTH_UNDEFINED, type GridColumnOrder, } from 'sentry/components/gridEditable'; import Link from 'sentry/components/links/link'; import TimeSince from 'sentry/components/timeSince'; import {IconCopy, IconDelete} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; import withApi from 'sentry/utils/withApi'; import EditAccessSelector from 'sentry/views/dashboards/editAccessSelector'; import type { DashboardDetails, DashboardListItem, DashboardPermissions, } from 'sentry/views/dashboards/types'; import {cloneDashboard} from '../utils'; type Props = { api: Client; dashboards: DashboardListItem[] | undefined; location: Location; onDashboardsChange: () => void; organization: Organization; isLoading?: boolean; }; enum ResponseKeys { NAME = 'title', WIDGETS = 'widgetDisplay', OWNER = 'createdBy', ACCESS = 'permissions', CREATED = 'dateCreated', } function DashboardTable({ api, organization, location, dashboards, onDashboardsChange, isLoading, }: Props) { const columnOrder = organization.features.includes('dashboards-edit-access') ? [ {key: ResponseKeys.NAME, name: t('Name'), width: COL_WIDTH_UNDEFINED}, {key: ResponseKeys.WIDGETS, name: t('Widgets'), width: COL_WIDTH_UNDEFINED}, {key: ResponseKeys.OWNER, name: t('Owner'), width: COL_WIDTH_UNDEFINED}, {key: ResponseKeys.ACCESS, name: t('Access'), width: COL_WIDTH_UNDEFINED}, {key: ResponseKeys.CREATED, name: t('Created'), width: COL_WIDTH_UNDEFINED}, ] : [ {key: ResponseKeys.NAME, name: t('Name'), width: COL_WIDTH_UNDEFINED}, {key: ResponseKeys.WIDGETS, name: t('Widgets'), width: COL_WIDTH_UNDEFINED}, {key: ResponseKeys.OWNER, name: t('Owner'), width: COL_WIDTH_UNDEFINED}, {key: ResponseKeys.CREATED, name: t('Created'), width: COL_WIDTH_UNDEFINED}, ]; function handleDelete(dashboard: DashboardListItem) { deleteDashboard(api, organization.slug, dashboard.id) .then(() => { trackAnalytics('dashboards_manage.delete', { organization, dashboard_id: parseInt(dashboard.id, 10), view_type: 'table', }); onDashboardsChange(); addSuccessMessage(t('Dashboard deleted')); }) .catch(() => { addErrorMessage(t('Error deleting Dashboard')); }); } async function handleDuplicate(dashboard: DashboardListItem) { try { const dashboardDetail = await fetchDashboard(api, organization.slug, dashboard.id); const newDashboard = cloneDashboard(dashboardDetail); newDashboard.widgets.map(widget => (widget.id = undefined)); await createDashboard(api, organization.slug, newDashboard, true); trackAnalytics('dashboards_manage.duplicate', { organization, dashboard_id: parseInt(dashboard.id, 10), view_type: 'table', }); onDashboardsChange(); addSuccessMessage(t('Dashboard duplicated')); } catch (e) { addErrorMessage(t('Error duplicating Dashboard')); } } // TODO(__SENTRY_USING_REACT_ROUTER_SIX): We can remove this later, react // router 6 handles empty query objects without appending a trailing ? const queryLocation = { ...(location.query && Object.keys(location.query).length > 0 ? {query: location.query} : {}), }; const renderBodyCell = ( column: GridColumnOrder, dataRow: DashboardListItem ) => { if (column.key === ResponseKeys.NAME) { return ( {dataRow[ResponseKeys.NAME]} ); } if (column.key === ResponseKeys.WIDGETS) { return dataRow[ResponseKeys.WIDGETS].length; } if (column.key === ResponseKeys.OWNER) { return dataRow[ResponseKeys.OWNER] ? ( ) : ( ); } if ( column.key === ResponseKeys.ACCESS && organization.features.includes('dashboards-edit-access') ) { /* Handles POST request for Edit Access Selector Changes */ const onChangeEditAccess = (newDashboardPermissions: DashboardPermissions) => { const dashboardCopy = cloneDeep(dataRow); dashboardCopy.permissions = newDashboardPermissions; updateDashboardPermissions(api, organization.slug, dashboardCopy).then( (newDashboard: DashboardDetails) => { onDashboardsChange(); addSuccessMessage(t('Dashboard Edit Access updated.')); return newDashboard; } ); }; return ( ); } if (column.key === ResponseKeys.CREATED) { return ( {dataRow[ResponseKeys.CREATED] ? ( ) : ( )} { e.stopPropagation(); openConfirmModal({ message: t('Are you sure you want to duplicate this dashboard?'), priority: 'primary', onConfirm: () => handleDuplicate(dataRow), }); }} aria-label={t('Duplicate Dashboard')} data-test-id={'dashboard-duplicate'} icon={} size="sm" /> { e.stopPropagation(); openConfirmModal({ message: t('Are you sure you want to delete this dashboard?'), priority: 'danger', onConfirm: () => handleDelete(dataRow), }); }} aria-label={t('Delete Dashboard')} data-test-id={'dashboard-delete'} icon={} size="sm" disabled={dashboards && dashboards.length <= 1} /> ); } return {dataRow[column.key]}; }; return (

{t('Sorry, no Dashboards match your filters.')}

} /> ); } export default withApi(DashboardTable); const DateSelected = styled('div')` font-size: ${p => p.theme.fontSizeMedium}; display: grid; grid-column-gap: ${space(1)}; color: ${p => p.theme.textColor}; ${p => p.theme.overflowEllipsis}; `; const DateStatus = styled('span')` color: ${p => p.theme.textColor}; padding-left: ${space(1)}; `; const DateActionsContainer = styled('div')` display: flex; gap: ${space(4)}; justify-content: space-between; align-items: center; `; const ActionsIconWrapper = styled('div')` display: flex; `; const StyledButton = styled(Button)` border: none; box-shadow: none; `;