mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
212 lines
6.7 KiB
TypeScript
212 lines
6.7 KiB
TypeScript
import {Fragment} from 'react';
|
|
import {css} from '@emotion/react';
|
|
import styled from '@emotion/styled';
|
|
import moment from 'moment-timezone';
|
|
|
|
import CollapsePanel from 'sentry/components/collapsePanel';
|
|
import DateTime from 'sentry/components/dateTime';
|
|
import Duration from 'sentry/components/duration';
|
|
import Link from 'sentry/components/links/link';
|
|
import PanelTable from 'sentry/components/panels/panelTable';
|
|
import StatusIndicator from 'sentry/components/statusIndicator';
|
|
import {t, tn} from 'sentry/locale';
|
|
import {space} from 'sentry/styles/space';
|
|
import type {Organization} from 'sentry/types';
|
|
import {getDuration} from 'sentry/utils/formatters';
|
|
import getDynamicText from 'sentry/utils/getDynamicText';
|
|
import {capitalize} from 'sentry/utils/string/capitalize';
|
|
import useOrganization from 'sentry/utils/useOrganization';
|
|
import {COMPARISON_DELTA_OPTIONS} from 'sentry/views/alerts/rules/metric/constants';
|
|
import {AlertRuleThresholdType} from 'sentry/views/alerts/rules/metric/types';
|
|
import type {ActivityType, Incident} from 'sentry/views/alerts/types';
|
|
import {IncidentActivityType, IncidentStatus} from 'sentry/views/alerts/types';
|
|
import {alertDetailsLink} from 'sentry/views/alerts/utils';
|
|
import {AlertWizardAlertNames} from 'sentry/views/alerts/wizard/options';
|
|
import {getAlertTypeFromAggregateDataset} from 'sentry/views/alerts/wizard/utils';
|
|
|
|
const COLLAPSE_COUNT = 3;
|
|
|
|
type MetricAlertActivityProps = {
|
|
incident: Incident;
|
|
organization: Organization;
|
|
};
|
|
|
|
function MetricAlertActivity({organization, incident}: MetricAlertActivityProps) {
|
|
const activities = (incident.activities ?? []).filter(
|
|
activity => activity.type === IncidentActivityType.STATUS_CHANGE
|
|
);
|
|
const criticalActivity = activities.find(
|
|
activity => activity.value === `${IncidentStatus.CRITICAL}`
|
|
);
|
|
const warningActivity = activities.find(
|
|
activity => activity.value === `${IncidentStatus.WARNING}`
|
|
);
|
|
|
|
const triggeredActivity: ActivityType = criticalActivity
|
|
? criticalActivity!
|
|
: warningActivity!;
|
|
const isCritical = Number(triggeredActivity.value) === IncidentStatus.CRITICAL;
|
|
|
|
// Find duration by looking at the difference between the previous and current activity timestamp
|
|
const nextActivity = activities.find(
|
|
({previousValue}) => previousValue === triggeredActivity.value
|
|
);
|
|
const activityDuration = (
|
|
nextActivity ? moment(nextActivity.dateCreated) : moment()
|
|
).diff(moment(triggeredActivity.dateCreated), 'milliseconds');
|
|
|
|
const triggerLabel = isCritical ? 'critical' : 'warning';
|
|
const curentTrigger = incident.alertRule.triggers.find(
|
|
trigger => trigger.label === triggerLabel
|
|
);
|
|
const timeWindow = getDuration(incident.alertRule.timeWindow * 60);
|
|
const alertName = capitalize(
|
|
AlertWizardAlertNames[getAlertTypeFromAggregateDataset(incident.alertRule)]
|
|
);
|
|
|
|
return (
|
|
<Fragment>
|
|
<Cell>
|
|
{triggeredActivity.value && (
|
|
<StatusIndicator
|
|
status={isCritical ? 'error' : 'warning'}
|
|
tooltipTitle={t('Status: %s', isCritical ? t('Critical') : t('Warning'))}
|
|
/>
|
|
)}
|
|
<Link
|
|
to={{
|
|
pathname: alertDetailsLink(organization, incident),
|
|
query: {alert: incident.identifier},
|
|
}}
|
|
>
|
|
#{incident.identifier}
|
|
</Link>
|
|
</Cell>
|
|
<Cell>
|
|
{incident.alertRule.comparisonDelta ? (
|
|
<Fragment>
|
|
{alertName} {curentTrigger?.alertThreshold}%
|
|
{t(
|
|
' %s in %s compared to the ',
|
|
incident.alertRule.thresholdType === AlertRuleThresholdType.ABOVE
|
|
? t('higher')
|
|
: t('lower'),
|
|
timeWindow
|
|
)}
|
|
{COMPARISON_DELTA_OPTIONS.find(
|
|
({value}) => value === incident.alertRule.comparisonDelta
|
|
)?.label ?? COMPARISON_DELTA_OPTIONS[0].label}
|
|
</Fragment>
|
|
) : (
|
|
<Fragment>
|
|
{alertName}{' '}
|
|
{incident.alertRule.thresholdType === AlertRuleThresholdType.ABOVE
|
|
? t('above')
|
|
: t('below')}{' '}
|
|
{curentTrigger?.alertThreshold} {t('in')} {timeWindow}
|
|
</Fragment>
|
|
)}
|
|
</Cell>
|
|
<Cell>
|
|
{activityDuration &&
|
|
getDynamicText({
|
|
value: <Duration abbreviation seconds={activityDuration / 1000} />,
|
|
fixed: '30s',
|
|
})}
|
|
</Cell>
|
|
<Cell>
|
|
<StyledDateTime
|
|
date={getDynamicText({
|
|
value: incident.dateCreated,
|
|
fixed: 'Mar 4, 2022 10:44:13 AM UTC',
|
|
})}
|
|
year
|
|
seconds
|
|
timeZone
|
|
/>
|
|
</Cell>
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
type Props = {
|
|
incidents?: Incident[];
|
|
};
|
|
|
|
function MetricHistory({incidents}: Props) {
|
|
const organization = useOrganization();
|
|
const filteredIncidents = (incidents ?? []).filter(
|
|
incident => incident.activities?.length
|
|
);
|
|
const numOfIncidents = filteredIncidents.length;
|
|
|
|
return (
|
|
<CollapsePanel
|
|
items={numOfIncidents}
|
|
collapseCount={COLLAPSE_COUNT}
|
|
disableBorder={false}
|
|
buttonTitle={tn('Hidden Alert', 'Hidden Alerts', numOfIncidents - COLLAPSE_COUNT)}
|
|
>
|
|
{({isExpanded, showMoreButton}) => (
|
|
<div>
|
|
<StyledPanelTable
|
|
headers={[t('Alert'), t('Reason'), t('Duration'), t('Date Triggered')]}
|
|
isEmpty={!numOfIncidents}
|
|
emptyMessage={t('No alerts triggered during this time.')}
|
|
expanded={numOfIncidents <= COLLAPSE_COUNT || isExpanded}
|
|
>
|
|
{filteredIncidents.map((incident, idx) => {
|
|
if (idx >= COLLAPSE_COUNT && !isExpanded) {
|
|
return null;
|
|
}
|
|
return (
|
|
<MetricAlertActivity
|
|
key={idx}
|
|
incident={incident}
|
|
organization={organization}
|
|
/>
|
|
);
|
|
})}
|
|
</StyledPanelTable>
|
|
{showMoreButton}
|
|
</div>
|
|
)}
|
|
</CollapsePanel>
|
|
);
|
|
}
|
|
|
|
export default MetricHistory;
|
|
|
|
const StyledPanelTable = styled(PanelTable)<{expanded: boolean; isEmpty: boolean}>`
|
|
grid-template-columns: max-content 1fr repeat(2, max-content);
|
|
|
|
& > div {
|
|
padding: ${space(1)} ${space(2)};
|
|
}
|
|
|
|
div:last-of-type {
|
|
padding: ${p => p.isEmpty && `48px ${space(1)}`};
|
|
}
|
|
|
|
${p =>
|
|
!p.expanded &&
|
|
css`
|
|
margin-bottom: 0px;
|
|
border-bottom-left-radius: 0px;
|
|
border-bottom-right-radius: 0px;
|
|
border-bottom: none;
|
|
`}
|
|
`;
|
|
|
|
const StyledDateTime = styled(DateTime)`
|
|
color: ${p => p.theme.gray300};
|
|
`;
|
|
|
|
const Cell = styled('div')`
|
|
display: flex;
|
|
align-items: center;
|
|
white-space: nowrap;
|
|
font-size: ${p => p.theme.fontSizeMedium};
|
|
padding: ${space(1)};
|
|
`;
|