Files
2024-02-15 17:01:50 +00:00

343 lines
9.6 KiB
TypeScript

import {useState} from 'react';
import {Link} from 'react-router';
import {css} from '@emotion/react';
import styled from '@emotion/styled';
import {Button} from 'sentry/components/button';
import {openConfirmModal} from 'sentry/components/confirm';
import {DropdownMenu} from 'sentry/components/dropdownMenu';
import Tag from 'sentry/components/tag';
import {Tooltip} from 'sentry/components/tooltip';
import {IconEllipsis} 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';
import useOrganization from 'sentry/utils/useOrganization';
import {StatusToggleButton} from 'sentry/views/monitors/components/statusToggleButton';
import type {Monitor} from 'sentry/views/monitors/types';
import {MonitorStatus} from 'sentry/views/monitors/types';
import {scheduleAsText} from 'sentry/views/monitors/utils';
import {statusIconColorMap} from 'sentry/views/monitors/utils/constants';
import type {CheckInTimelineProps} from './checkInTimeline';
import {CheckInTimeline} from './checkInTimeline';
import {TimelinePlaceholder} from './timelinePlaceholder';
import type {MonitorBucket} from './types';
interface Props extends Omit<CheckInTimelineProps, 'bucketedData' | 'environment'> {
monitor: Monitor;
bucketedData?: MonitorBucket[];
onDeleteEnvironment?: (env: string) => Promise<void>;
onToggleMuteEnvironment?: (env: string, isMuted: boolean) => Promise<void>;
onToggleStatus?: (monitor: Monitor, status: ObjectStatus) => Promise<void>;
/**
* 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 TimelineTableRow({
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 : (
<DetailsArea>
<DetailsLink to={`/organizations/${organization.slug}/crons/${monitor.slug}/`}>
<DetailsHeadline>
<Name>{monitor.name}</Name>
{isDisabled && <Tag>{t('Disabled')}</Tag>}
</DetailsHeadline>
<Schedule>{scheduleAsText(monitor.config)}</Schedule>
</DetailsLink>
<DetailsActions>
{onToggleStatus && (
<StatusToggleButton
monitor={monitor}
size="xs"
onToggleStatus={status => onToggleStatus(monitor, status)}
/>
)}
</DetailsActions>
</DetailsArea>
);
const environmentActionCreators = [
(env: string) => ({
label: t('View Environment'),
key: 'view',
to: `/organizations/${organization.slug}/crons/${monitor.slug}/?environment=${env}`,
}),
...(onToggleMuteEnvironment
? [
(env: string, isMuted: boolean) => ({
label: isMuted ? t('Unmute Environment') : t('Mute Environment'),
key: 'mute',
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 (
<TimelineRow
key={monitor.id}
isDisabled={isDisabled}
singleMonitorView={singleMonitorView}
>
{monitorDetails}
<MonitorEnvContainer>
{environments.map(({name, status, isMuted}) => {
const envStatus = monitor.isMuted || isMuted ? MonitorStatus.DISABLED : status;
const {label, icon} = statusIconColorMap[envStatus];
return (
<EnvRow key={name}>
<DropdownMenu
size="sm"
trigger={triggerProps => (
<EnvActionButton
{...triggerProps}
aria-label={t('Monitor environment actions')}
size="xs"
icon={<IconEllipsis />}
/>
)}
items={environmentActionCreators.map(actionCreator =>
actionCreator(name, isMuted)
)}
/>
<EnvWithStatus>
<MonitorEnvLabel status={envStatus}>{name}</MonitorEnvLabel>
<Tooltip title={label} skipWrapper>
{icon}
</Tooltip>
</EnvWithStatus>
</EnvRow>
);
})}
{!isExpanded && (
<Button size="xs" onClick={() => setExpanded(true)}>
{tct('Show [num] More', {
num: monitor.environments.length - MAX_SHOWN_ENVIRONMENTS,
})}
</Button>
)}
</MonitorEnvContainer>
<TimelineContainer>
{environments.map(({name}) => {
return (
<TimelineEnvOuterContainer key={name}>
{!bucketedData ? (
<TimelinePlaceholder />
) : (
<TimelineEnvContainer>
<CheckInTimeline
{...timelineProps}
bucketedData={bucketedData}
environment={name}
/>
</TimelineEnvContainer>
)}
</TimelineEnvOuterContainer>
);
})}
</TimelineContainer>
</TimelineRow>
);
}
const DetailsLink = styled(Link)`
display: block;
padding: ${space(3)};
color: ${p => p.theme.textColor};
`;
const DetailsArea = styled('div')`
border-right: 1px solid ${p => p.theme.border};
border-radius: 0;
position: relative;
`;
const DetailsActions = styled('div')`
position: absolute;
top: 0;
right: 0;
display: none;
align-items: center;
/* Align to the center of the heading text */
height: calc(${p => p.theme.fontSizeLarge} * ${p => p.theme.text.lineHeightHeading});
margin: ${space(3)};
`;
const DetailsHeadline = styled('div')`
display: grid;
gap: ${space(1)};
/* We always leave at least enough room for the status toggle button */
grid-template-columns: 1fr minmax(30px, max-content);
`;
const Name = styled('h3')`
font-size: ${p => p.theme.fontSizeLarge};
margin-bottom: ${space(0.25)};
word-break: break-word;
`;
const Schedule = styled('small')`
color: ${p => p.theme.subText};
font-size: ${p => p.theme.fontSizeSmall};
`;
interface TimelineRowProps {
isDisabled?: boolean;
singleMonitorView?: boolean;
}
const TimelineRow = styled('div')<TimelineRowProps>`
display: contents;
${p =>
!p.singleMonitorView &&
css`
&:nth-child(odd) > * {
background: ${p.theme.backgroundSecondary};
}
&:hover > * {
background: ${p.theme.backgroundTertiary};
}
`}
/* Show detail actions on hover */
&:hover ${DetailsActions} {
display: flex;
}
/* Hide trailing items on hover */
&:hover ${DetailsHeadline} ${Tag} {
visibility: hidden;
}
/* Disabled monitors become more opaque */
--disabled-opacity: ${p => (p.isDisabled ? '0.6' : 'unset')};
&:last-child > *:first-child {
border-bottom-left-radius: ${p => p.theme.borderRadius};
}
&:last-child > *:last-child {
border-bottom-right-radius: ${p => p.theme.borderRadius};
}
> * {
transition: background 50ms ease-in-out;
}
`;
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 EnvActionButton = styled(Button)`
padding: ${space(0.5)} ${space(1)};
display: none;
`;
const EnvRow = styled('div')`
&:hover ${EnvActionButton} {
display: block;
}
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 EnvWithStatus = styled('div')`
display: grid;
grid-template-columns: 1fr max-content;
gap: ${space(0.5)};
align-items: center;
opacity: var(--disabled-opacity);
`;
const MonitorEnvLabel = styled('div')<{status: MonitorStatus}>`
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
min-width: 0;
color: ${p => p.theme[statusIconColorMap[p.status].color]};
opacity: var(--disabled-opacity);
`;
const TimelineContainer = styled('div')`
display: flex;
padding: ${space(3)} 0;
flex-direction: column;
gap: ${space(4)};
contain: content;
`;
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;
`;