-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(uptime): Adds a simple overview page to the uptime insights tab (#…
…82567) Looks like this <img alt="clipboard.png" width="1710" src="https://i.imgur.com/74rvSUI.jpeg" />
- Loading branch information
1 parent
7727765
commit 8e0a64b
Showing
5 changed files
with
475 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
57 changes: 57 additions & 0 deletions
57
static/app/views/insights/uptime/components/overviewTimeline/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
`; |
166 changes: 166 additions & 0 deletions
166
static/app/views/insights/uptime/components/overviewTimeline/overviewRow.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
Oops, something went wrong.