import {Fragment, useEffect, useState} from 'react'; import styled from '@emotion/styled'; import type {Location} from 'history'; import isEqual from 'lodash/isEqual'; import { createDashboard, deleteDashboard, fetchDashboard, } from 'sentry/actionCreators/dashboards'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import type {Client} from 'sentry/api'; import {Button} from 'sentry/components/button'; import {openConfirmModal} from 'sentry/components/confirm'; import type {MenuItemProps} from 'sentry/components/dropdownMenu'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; import EmptyStateWarning from 'sentry/components/emptyStateWarning'; import Placeholder from 'sentry/components/placeholder'; import TimeSince from 'sentry/components/timeSince'; import {IconEllipsis} from 'sentry/icons'; import {t, tn} 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 { DASHBOARD_CARD_GRID_PADDING, MINIMUM_DASHBOARD_CARD_WIDTH, } from 'sentry/views/dashboards/manage/settings'; import type {DashboardListItem} from 'sentry/views/dashboards/types'; import {cloneDashboard} from '../utils'; import DashboardCard from './dashboardCard'; import GridPreview from './gridPreview'; type Props = { api: Client; columnCount: number; dashboards: DashboardListItem[] | undefined; location: Location; onDashboardsChange: () => void; organization: Organization; rowCount: number; isLoading?: boolean; }; function DashboardGrid({ api, organization, location, dashboards, onDashboardsChange, rowCount, columnCount, isLoading, }: Props) { // this acts as a cache for the dashboards being passed in. It preserves the previously populated dashboard list // to be able to show the 'previous' dashboards on resize const [currentDashboards, setCurrentDashboards] = useState< DashboardListItem[] | undefined >(dashboards); useEffect(() => { if (dashboards?.length) { setCurrentDashboards(dashboards); } }, [dashboards]); function handleDelete(dashboard: DashboardListItem) { deleteDashboard(api, organization.slug, dashboard.id) .then(() => { trackAnalytics('dashboards_manage.delete', { organization, dashboard_id: parseInt(dashboard.id, 10), }); 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), }); onDashboardsChange(); addSuccessMessage(t('Dashboard duplicated')); } catch (e) { addErrorMessage(t('Error duplicating Dashboard')); } } function renderDropdownMenu(dashboard: DashboardListItem) { const menuItems: MenuItemProps[] = [ { key: 'dashboard-duplicate', label: t('Duplicate'), onAction: () => handleDuplicate(dashboard), }, { key: 'dashboard-delete', label: t('Delete'), priority: 'danger', onAction: () => { openConfirmModal({ message: t('Are you sure you want to delete this dashboard?'), priority: 'danger', onConfirm: () => handleDelete(dashboard), }); }, }, ]; return ( ( { e.stopPropagation(); e.preventDefault(); triggerProps.onClick?.(e); }} icon={} /> )} position="bottom-end" disabledKeys={dashboards && dashboards.length <= 1 ? ['dashboard-delete'] : []} offset={4} /> ); } function renderGridPreview(dashboard) { return ; } // 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} : {}), }; function renderMiniDashboards() { // on pagination, render no dashboards to show placeholders while loading if ( rowCount * columnCount === currentDashboards?.length && !isEqual(currentDashboards, dashboards) ) { return []; } return currentDashboards?.slice(0, rowCount * columnCount).map((dashboard, index) => { return ( : undefined } createdBy={dashboard.createdBy} renderWidgets={() => renderGridPreview(dashboard)} renderContextMenu={() => renderDropdownMenu(dashboard)} /> ); }); } function renderDashboardGrid() { if (!dashboards?.length && !isLoading) { return (

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

); } const gridIsBeingResized = rowCount * columnCount !== currentDashboards?.length; // finds number of dashboards (cached or not) based on if the screen is being resized or not const numDashboards = gridIsBeingResized ? currentDashboards?.length ?? 0 : dashboards?.length ?? 0; return ( {renderMiniDashboards()} {isLoading && rowCount * columnCount > numDashboards && new Array(rowCount * columnCount - numDashboards) .fill(0) .map((_, index) => )} ); } return {renderDashboardGrid()}; } const DashboardGridContainer = styled('div')<{columns: number; rows: number}>` display: grid; grid-template-columns: repeat( ${props => props.columns}, minmax(${MINIMUM_DASHBOARD_CARD_WIDTH}px, 1fr) ); grid-template-rows: repeat(${props => props.rows}, max-content); gap: ${DASHBOARD_CARD_GRID_PADDING}px; `; const DropdownTrigger = styled(Button)` transform: translateX(${space(1)}); `; export default withApi(DashboardGrid);