Skip to content

Commit

Permalink
feat(uptime): Adds a simple overview page to the uptime insights tab (#…
Browse files Browse the repository at this point in the history
…82567)

Looks like this

<img alt="clipboard.png" width="1710"
src="https://i.imgur.com/74rvSUI.jpeg" />
  • Loading branch information
evanpurkhiser authored Jan 2, 2025
1 parent 7727765 commit 8e0a64b
Show file tree
Hide file tree
Showing 5 changed files with 475 additions and 0 deletions.
5 changes: 5 additions & 0 deletions static/app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1737,6 +1737,11 @@ function buildRoutes() {
)}
/>
</Route>
<Route path={`${MODULE_BASE_URLS[ModuleName.UPTIME]}/`}>
<IndexRoute
component={make(() => import('sentry/views/insights/uptime/views/overview'))}
/>
</Route>
<Route path={`${MODULE_BASE_URLS[ModuleName.AI]}/`}>
<IndexRoute
component={make(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import styled from '@emotion/styled';

import Panel from 'sentry/components/panels/panel';
import {Sticky} from 'sentry/components/sticky';
import type {UptimeAlert} from 'sentry/views/alerts/types';

import {OverviewRow} from './overviewRow';

interface Props {
uptimeAlerts: UptimeAlert[];
}

export function OverviewTimeline({uptimeAlerts}: Props) {
return (
<MonitorListPanel role="region">
<Header />
<UptimeAlertRow>
{uptimeAlerts.map(uptimeAlert => (
<OverviewRow key={uptimeAlert.id} uptimeAlert={uptimeAlert} />
))}
</UptimeAlertRow>
</MonitorListPanel>
);
}

const Header = styled(Sticky)`
display: grid;
grid-column: 1/-1;
grid-template-columns: subgrid;
z-index: 1;
background: ${p => p.theme.background};
border-top-left-radius: ${p => p.theme.panelBorderRadius};
border-top-right-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};
border-right: 1px solid ${p => p.theme.border};
margin: 0 -1px;
}
`;

const MonitorListPanel = styled(Panel)`
display: grid;
grid-template-columns: 350px 1fr max-content;
`;

const UptimeAlertRow = styled('ul')`
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1;
list-style: none;
padding: 0;
margin: 0;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import {css} from '@emotion/react';
import styled from '@emotion/styled';
import pick from 'lodash/pick';

import ActorBadge from 'sentry/components/idBadge/actorBadge';
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
import Link from 'sentry/components/links/link';
import {IconTimer, IconUser} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import getDuration from 'sentry/utils/duration/getDuration';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
import useProjectFromSlug from 'sentry/utils/useProjectFromSlug';
import type {UptimeAlert} from 'sentry/views/alerts/types';

interface Props {
uptimeAlert: UptimeAlert;
}

export function OverviewRow({uptimeAlert}: Props) {
const organization = useOrganization();
const project = useProjectFromSlug({
organization,
projectSlug: uptimeAlert.projectSlug,
});

const location = useLocation();
const query = pick(location.query, ['start', 'end', 'statsPeriod', 'environment']);

return (
<TimelineRow key={uptimeAlert.id}>
<DetailsArea>
<DetailsLink
to={{
pathname: `/organizations/${organization.slug}/alerts/rules/uptime/${uptimeAlert.projectSlug}/${uptimeAlert.id}/details/`,
query,
}}
>
<DetailsHeadline>
<Name>{uptimeAlert.name}</Name>
</DetailsHeadline>
<DetailsContainer>
<OwnershipDetails>
{project && <ProjectBadge project={project} avatarSize={12} disableLink />}
{uptimeAlert.owner ? (
<ActorBadge actor={uptimeAlert.owner} avatarSize={12} />
) : (
<UnassignedLabel>
<IconUser size="xs" />
{t('Unassigned')}
</UnassignedLabel>
)}
</OwnershipDetails>
<ScheduleDetails>
<IconTimer size="xs" />
{t('Checked every %s', getDuration(uptimeAlert.intervalSeconds))}
</ScheduleDetails>
</DetailsContainer>
</DetailsLink>
</DetailsArea>
<TimelineContainer />
</TimelineRow>
);
}

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 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')<TimelineRowProps>`
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 TimelineContainer = styled('div')`
display: flex;
padding: ${space(3)} 0;
flex-direction: column;
gap: ${space(4)};
contain: content;
grid-column: 2/-1;
`;
84 changes: 84 additions & 0 deletions static/app/views/insights/uptime/views/overview.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {ProjectFixture} from 'sentry-fixture/project';
import {TeamFixture} from 'sentry-fixture/team';
import {UptimeRuleFixture} from 'sentry-fixture/uptimeRule';

import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen} from 'sentry-test/reactTestingLibrary';

import OrganizationStore from 'sentry/stores/organizationStore';
import {useNavigate} from 'sentry/utils/useNavigate';
import usePageFilters from 'sentry/utils/usePageFilters';
import UptimeOverview from 'sentry/views/insights/uptime/views/overview';

jest.mock('sentry/utils/usePageFilters');

jest.mock('sentry/utils/useNavigate', () => ({
useNavigate: jest.fn(),
}));

const mockUseNavigate = jest.mocked(useNavigate);
const mockNavigate = jest.fn();
mockUseNavigate.mockReturnValue(mockNavigate);

describe('Uptime Overview', function () {
const project = ProjectFixture();
const team = TeamFixture();

jest.mocked(usePageFilters).mockReturnValue({
isReady: true,
desyncedFilters: new Set(),
pinnedFilters: new Set(),
shouldPersist: true,
selection: {
datetime: {
period: '10d',
start: null,
end: null,
utc: false,
},
environments: [],
projects: [],
},
});

beforeEach(function () {
OrganizationStore.init();
MockApiClient.clearMockResponses();
MockApiClient.addMockResponse({
url: '/organizations/org-slug/uptime/',
body: [
UptimeRuleFixture({
name: 'Test Monitor',
projectSlug: project.slug,
owner: undefined,
}),
],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/projects/',
body: [project],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/members/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/teams/',
body: [team],
});
});

it('renders', async function () {
const {organization, router} = initializeOrg({
organization: {features: ['insights-initial-modules', 'insights-uptime']},
});
OrganizationStore.onUpdate(organization);

render(<UptimeOverview />, {organization, router});
expect(
await screen.findByRole('heading', {name: 'Uptime Monitors'})
).toBeInTheDocument();

expect(screen.getByRole('heading', {name: 'Test Monitor'})).toBeInTheDocument();
});
});
Loading

0 comments on commit 8e0a64b

Please sign in to comment.