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

239 lines
7.1 KiB
TypeScript

import {useRef} from 'react';
import styled from '@emotion/styled';
import {
deleteMonitorEnvironment,
setEnvironmentIsMuted,
updateMonitor,
} from 'sentry/actionCreators/monitors';
import Panel from 'sentry/components/panels/panel';
import {Sticky} from 'sentry/components/sticky';
import {space} from 'sentry/styles/space';
import {setApiQueryData, useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
import useApi from 'sentry/utils/useApi';
import {useDimensions} from 'sentry/utils/useDimensions';
import useOrganization from 'sentry/utils/useOrganization';
import useRouter from 'sentry/utils/useRouter';
import {
GridLineOverlay,
GridLineTimeLabels,
} from 'sentry/views/monitors/components/overviewTimeline/gridLines';
import {SortSelector} from 'sentry/views/monitors/components/overviewTimeline/sortSelector';
import {makeMonitorListQueryKey} from 'sentry/views/monitors/utils';
import type {Monitor} from '../../types';
import {ResolutionSelector} from './resolutionSelector';
import {TimelineTableRow} from './timelineTableRow';
import type {MonitorBucketData, TimeWindow} from './types';
import {getConfigFromTimeRange, getStartFromTimeWindow} from './utils';
interface Props {
monitorList: Monitor[];
}
export function OverviewTimeline({monitorList}: Props) {
const organization = useOrganization();
const api = useApi();
const queryClient = useQueryClient();
const router = useRouter();
const location = router.location;
const timeWindow: TimeWindow = location.query?.timeWindow ?? '24h';
const nowRef = useRef(new Date());
const start = getStartFromTimeWindow(nowRef.current, timeWindow);
const elementRef = useRef<HTMLDivElement>(null);
const {width: timelineWidth} = useDimensions<HTMLDivElement>({elementRef});
const timeWindowConfig = getConfigFromTimeRange(start, nowRef.current, timelineWidth);
const rollup = Math.floor((timeWindowConfig.elapsedMinutes * 60) / timelineWidth);
const monitorStatsQueryKey = `/organizations/${organization.slug}/monitors-stats/`;
const {data: monitorStats, isLoading} = useApiQuery<Record<string, MonitorBucketData>>(
[
monitorStatsQueryKey,
{
query: {
until: Math.floor(nowRef.current.getTime() / 1000),
since: Math.floor(start.getTime() / 1000),
monitor: monitorList.map(m => m.slug),
resolution: `${rollup}s`,
...location.query,
},
},
],
{
staleTime: 0,
enabled: timelineWidth > 0,
}
);
const handleDeleteEnvironment = async (monitor: Monitor, env: string) => {
const success = await deleteMonitorEnvironment(
api,
organization.slug,
monitor.slug,
env
);
if (!success) {
return;
}
const queryKey = makeMonitorListQueryKey(organization, location.query);
setApiQueryData(queryClient, queryKey, (oldMonitorList: Monitor[]) => {
const oldMonitorIdx = oldMonitorList.findIndex(m => m.slug === monitor.slug);
if (oldMonitorIdx < 0) {
return oldMonitorList;
}
const oldMonitor = oldMonitorList[oldMonitorIdx];
const newEnvList = oldMonitor.environments.filter(e => e.name !== env);
const newMonitor = {
...oldMonitor,
environments: newEnvList,
};
return [
...oldMonitorList.slice(0, oldMonitorIdx),
newMonitor,
...oldMonitorList.slice(oldMonitorIdx + 1),
];
});
};
const handleToggleMuteEnvironment = async (
monitor: Monitor,
env: string,
isMuted: boolean
) => {
const resp = await setEnvironmentIsMuted(
api,
organization.slug,
monitor.slug,
env,
isMuted
);
if (resp === null) {
return;
}
const queryKey = makeMonitorListQueryKey(organization, location.query);
setApiQueryData(queryClient, queryKey, (oldMonitorList: Monitor[]) => {
const monitorIdx = oldMonitorList.findIndex(m => m.slug === monitor.slug);
// TODO(davidenwang): in future only change the specifically modified environment for optimistic updates
oldMonitorList[monitorIdx] = resp;
return oldMonitorList;
});
};
const handleToggleStatus = async (monitor: Monitor) => {
const status = monitor.status === 'active' ? 'disabled' : 'active';
const resp = await updateMonitor(api, organization.slug, monitor.slug, {status});
if (resp === null) {
return;
}
const queryKey = makeMonitorListQueryKey(organization, location.query);
setApiQueryData(queryClient, queryKey, (oldMonitorList: Monitor[]) => {
const monitorIdx = oldMonitorList.findIndex(m => m.slug === monitor.slug);
oldMonitorList[monitorIdx] = {...oldMonitorList[monitorIdx], status: resp.status};
return oldMonitorList;
});
};
return (
<MonitorListPanel>
<TimelineWidthTracker ref={elementRef} />
<HeaderControls>
<ResolutionSelector />
<SortSelector />
</HeaderControls>
<StickyGridLineTimeLabels>
<BorderlessGridLineTimeLabels
timeWindowConfig={timeWindowConfig}
start={start}
end={nowRef.current}
width={timelineWidth}
/>
</StickyGridLineTimeLabels>
<GridLineOverlay
stickyCursor
showCursor={!isLoading}
timeWindowConfig={timeWindowConfig}
start={start}
end={nowRef.current}
width={timelineWidth}
/>
{monitorList.map(monitor => (
<TimelineTableRow
key={monitor.id}
monitor={monitor}
timeWindowConfig={timeWindowConfig}
start={start}
bucketedData={monitorStats?.[monitor.slug]}
end={nowRef.current}
width={timelineWidth}
onDeleteEnvironment={env => handleDeleteEnvironment(monitor, env)}
onToggleMuteEnvironment={(env, isMuted) =>
handleToggleMuteEnvironment(monitor, env, isMuted)
}
onToggleStatus={handleToggleStatus}
/>
))}
</MonitorListPanel>
);
}
const MonitorListPanel = styled(Panel)`
display: grid;
grid-template-columns: 350px 135px 1fr;
`;
const HeaderControls = styled(Sticky)`
display: flex;
gap: ${space(0.5)};
z-index: 1;
padding: ${space(1.5)} ${space(2)};
grid-column: 1/3;
background: ${p => p.theme.background};
border-top-left-radius: ${p => p.theme.panelBorderRadius};
box-shadow: 0 1px ${p => p.theme.translucentBorder};
&[data-stuck] {
border-radius: 0;
border-left: 1px solid ${p => p.theme.border};
margin-left: -1px;
}
`;
// We don't need border here because it is already accomplished via box-shadow below
const BorderlessGridLineTimeLabels = styled(GridLineTimeLabels)`
border: none;
`;
const StickyGridLineTimeLabels = styled(Sticky)`
> * {
height: 100%;
}
z-index: 1;
background: ${p => p.theme.background};
border-top-right-radius: ${p => p.theme.panelBorderRadius};
box-shadow: 0 1px ${p => p.theme.translucentBorder};
&[data-stuck] {
border-radius: 0;
border-right: 1px solid ${p => p.theme.border};
margin-right: -1px;
}
`;
const TimelineWidthTracker = styled('div')`
position: absolute;
width: 100%;
grid-row: 1;
grid-column: 3;
`;