import {useState} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; import Tag from 'sentry/components/badge/tag'; import {Button} from 'sentry/components/button'; import {openConfirmModal} from 'sentry/components/confirm'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; import ActorBadge from 'sentry/components/idBadge/actorBadge'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import Link from 'sentry/components/links/link'; import {IconEllipsis, IconTimer, IconUser} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {fadeIn} from 'sentry/styles/animations'; import {space} from 'sentry/styles/space'; import type {ObjectStatus} from 'sentry/types/core'; import useOrganization from 'sentry/utils/useOrganization'; import {normalizeUrl} from 'sentry/utils/withDomainRequired'; import type {Monitor} from 'sentry/views/monitors/types'; import {scheduleAsText} from 'sentry/views/monitors/utils/scheduleAsText'; import {StatusToggleButton} from '../statusToggleButton'; import {CheckInPlaceholder} from '../timeline/checkInPlaceholder'; import type {CheckInTimelineProps} from '../timeline/checkInTimeline'; import {CheckInTimeline} from '../timeline/checkInTimeline'; import type {MonitorBucket} from '../timeline/types'; import MonitorEnvironmentLabel from './monitorEnvironmentLabel'; interface Props extends Omit { monitor: Monitor; bucketedData?: MonitorBucket[]; onDeleteEnvironment?: (env: string) => Promise; onToggleMuteEnvironment?: (env: string, isMuted: boolean) => Promise; onToggleStatus?: (monitor: Monitor, status: ObjectStatus) => Promise; /** * Whether only one monitor is being rendered in a larger view with this component * turns off things like zebra striping, hover effect, and showing monitor name */ singleMonitorView?: boolean; } const MAX_SHOWN_ENVIRONMENTS = 4; export function OverviewRow({ monitor, bucketedData, singleMonitorView, onDeleteEnvironment, onToggleMuteEnvironment, onToggleStatus, ...timelineProps }: Props) { const organization = useOrganization(); const [isExpanded, setExpanded] = useState( monitor.environments.length <= MAX_SHOWN_ENVIRONMENTS ); const environments = isExpanded ? monitor.environments : monitor.environments.slice(0, MAX_SHOWN_ENVIRONMENTS); const isDisabled = monitor.status === 'disabled'; const monitorDetails = singleMonitorView ? null : ( {monitor.name} {monitor.owner ? ( ) : ( {t('Unassigned')} )} {scheduleAsText(monitor.config)} {monitor.isMuted && {t('Muted')}} {isDisabled && {t('Disabled')}} {onToggleStatus && ( onToggleStatus(monitor, status)} /> )} ); const environmentActionCreators = [ (env: string) => ({ label: t('View Environment'), key: 'view', to: normalizeUrl( `/organizations/${organization.slug}/crons/${monitor.project.slug}/${monitor.slug}/?environment=${env}` ), }), ...(onToggleMuteEnvironment ? [ (env: string, isMuted: boolean) => ({ label: isMuted && !monitor.isMuted ? t('Unmute Environment') : t('Mute Environment'), key: 'mute', details: monitor.isMuted ? t('Monitor is muted') : undefined, disabled: monitor.isMuted, onAction: () => onToggleMuteEnvironment(env, !isMuted), }), ] : []), ...(onDeleteEnvironment ? [ (env: string) => ({ label: t('Delete Environment'), key: 'delete', onAction: () => { openConfirmModal({ onConfirm: () => onDeleteEnvironment(env), header: t('Delete Environment?'), message: tct( 'Are you sure you want to permanently delete the "[envName]" environment?', {envName: env} ), confirmText: t('Delete'), priority: 'danger', }); }, }), ] : []), ]; return ( {monitorDetails} {environments.map(env => { const {name, isMuted} = env; return ( ( } /> )} items={environmentActionCreators.map(actionCreator => actionCreator(name, isMuted) )} /> ); })} {!isExpanded && ( )} {environments.map(({name}) => { return ( {!bucketedData ? ( ) : ( )} ); })} ); } const DetailsLink = styled(Link)` display: block; padding: ${space(3)}; color: ${p => p.theme.textColor}; &:focus-visible { outline: none; } `; const DetailsArea = styled('div')` border-right: 1px solid ${p => p.theme.border}; border-radius: 0; position: relative; `; const DetailsHeadline = styled('div')` display: grid; gap: ${space(1)}; grid-template-columns: 1fr minmax(30px, max-content); `; const DetailsContainer = styled('div')` display: flex; flex-direction: column; gap: ${space(0.5)}; `; const OwnershipDetails = styled('div')` display: flex; gap: ${space(0.75)}; align-items: center; color: ${p => p.theme.subText}; font-size: ${p => p.theme.fontSizeSmall}; `; const UnassignedLabel = styled('div')` display: flex; gap: ${space(0.5)}; align-items: center; `; const MonitorStatuses = styled('div')` display: flex; gap: ${space(0.5)}; `; const Name = styled('h3')` font-size: ${p => p.theme.fontSizeLarge}; word-break: break-word; margin-bottom: ${space(0.5)}; `; const ScheduleDetails = styled('small')` display: flex; gap: ${space(0.5)}; align-items: center; color: ${p => p.theme.subText}; font-size: ${p => p.theme.fontSizeSmall}; `; interface TimelineRowProps { isDisabled?: boolean; singleMonitorView?: boolean; } const TimelineRow = styled('li')` grid-column: 1/-1; display: grid; grid-template-columns: subgrid; ${p => !p.singleMonitorView && css` transition: background 50ms ease-in-out; &:nth-child(odd) { background: ${p.theme.backgroundSecondary}; } &:hover { background: ${p.theme.backgroundTertiary}; } &:has(*:focus-visible) { background: ${p.theme.backgroundTertiary}; } `} /* Disabled monitors become more opaque */ --disabled-opacity: ${p => (p.isDisabled ? '0.6' : 'unset')}; &:last-child { border-bottom-left-radius: ${p => p.theme.borderRadius}; border-bottom-right-radius: ${p => p.theme.borderRadius}; } `; const DetailsActions = styled('div')` position: absolute; top: 0; right: 0; opacity: 0; /* Align to the center of the heading text */ height: calc(${p => p.theme.fontSizeLarge} * ${p => p.theme.text.lineHeightHeading}); margin: ${space(3)}; /* Show when timeline is hovered / focused */ ${TimelineRow}:hover &, ${DetailsLink}:focus-visible + &, &:has(a:focus-visible), &:has(button:focus-visible) { opacity: 1; } `; const MonitorEnvContainer = styled('div')` display: flex; padding: ${space(3)} ${space(2)}; gap: ${space(4)}; flex-direction: column; border-right: 1px solid ${p => p.theme.innerBorder}; text-align: right; `; const EnvRow = styled('div')` display: flex; gap: ${space(0.5)}; justify-content: space-between; align-items: center; height: calc(${p => p.theme.fontSizeLarge} * ${p => p.theme.text.lineHeightHeading}); `; const EnvActionButton = styled(Button)` padding: ${space(0.5)} ${space(1)}; display: none; ${EnvRow}:hover & { display: block; } `; const TimelineContainer = styled('div')` display: flex; padding: ${space(3)} 0; flex-direction: column; gap: ${space(4)}; contain: content; grid-column: 3/-1; `; const TimelineEnvOuterContainer = styled('div')` position: relative; height: calc(${p => p.theme.fontSizeLarge} * ${p => p.theme.text.lineHeightHeading}); opacity: var(--disabled-opacity); `; const TimelineEnvContainer = styled('div')` position: absolute; inset: 0; opacity: 0; animation: ${fadeIn} 1.5s ease-out forwards; contain: content; `;