-
-
Notifications
You must be signed in to change notification settings - Fork 0
added downtime minutes and availability view #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c1cf472
1976f64
e0647f0
c4dfaa2
d189551
623ac2b
f67cc86
6dcc9fe
a4b68a5
7f808e5
a3a4a7f
cdf5660
7f2cea0
fff5aef
b0ea997
b1e020c
a3e851d
fd1016b
1fd07f0
e2d1cce
69588d3
442bd98
23bdb68
f4eeda6
23f7d8c
6ce9ff9
55ca13a
bd0ef74
42dac68
b7fad46
e75038f
1e7ef04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| interface DowntimeFieldProps { | ||
| isEditing: boolean; | ||
| value: number | null; | ||
| draftValue: number | null; | ||
| onChange: (value: number | null) => void; | ||
| } | ||
|
|
||
| export function DowntimeField({ | ||
| isEditing, | ||
| value, | ||
| draftValue, | ||
| onChange, | ||
| }: DowntimeFieldProps) { | ||
| return ( | ||
| <div className="flex items-center gap-space-md"> | ||
| <div className="text-content-secondary w-20 flex-none text-sm font-medium"> | ||
| Downtime | ||
| </div> | ||
| <div className="flex flex-1 items-center justify-end"> | ||
| {isEditing ? ( | ||
| <div className="flex items-center gap-space-xs"> | ||
| <input | ||
| type="number" | ||
| min="0" | ||
| value={draftValue ?? ''} | ||
| onChange={e => { | ||
| const num = e.target.valueAsNumber; | ||
| onChange(e.target.value === '' ? null : Number.isNaN(num) ? null : num); | ||
| }} | ||
| placeholder="—" | ||
| className="w-20 rounded-radius-sm border border-secondary bg-background-primary px-space-sm py-space-xs text-right text-sm focus:outline-none focus:ring-1" | ||
| /> | ||
| <span className="text-content-secondary text-sm">min</span> | ||
| </div> | ||
| ) : ( | ||
| <span | ||
| className={`text-sm ${value != null ? 'text-content-primary' : 'text-content-tertiary'}`} | ||
| > | ||
| {value != null ? `${value} min` : 'Not set'} | ||
| </span> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import {Link} from '@tanstack/react-router'; | ||
| import {cn} from 'utils/cn'; | ||
|
|
||
| import {getAvailabilityLevel, type AvailabilityLevel} from '../utils'; | ||
|
|
||
| const AVAILABILITY_BG: Record<AvailabilityLevel, string> = { | ||
| success: 'bg-graphics-success-moderate', | ||
| warning: 'bg-graphics-warning-moderate', | ||
| danger: 'bg-graphics-danger-moderate', | ||
| }; | ||
|
|
||
| interface HeatmapBlock { | ||
| label: string; | ||
| availability: number; | ||
| periodStart: string; | ||
| periodEnd: string; | ||
| regionName: string; | ||
| } | ||
|
|
||
| interface HeatmapBarProps { | ||
| blocks: HeatmapBlock[]; | ||
| showEndLabels?: boolean; | ||
| } | ||
|
|
||
| export function HeatmapBar({blocks, showEndLabels}: HeatmapBarProps) { | ||
| return ( | ||
| <div className="flex gap-px overflow-visible"> | ||
| {blocks.map((block, i) => { | ||
| const isFullUptime = block.availability >= 100; | ||
| const displayPct = isFullUptime | ||
| ? '100.00' | ||
| : Math.min(99.99, block.availability).toFixed(2); | ||
| const inner = ( | ||
| <> | ||
| <div | ||
| className={cn( | ||
| 'flex h-8 items-center justify-center sm:transition-opacity sm:group-hover:opacity-80', | ||
| AVAILABILITY_BG[getAvailabilityLevel(block.availability)], | ||
| i === 0 && 'rounded-l-md', | ||
| i === blocks.length - 1 && 'rounded-r-md' | ||
| )} | ||
| > | ||
| <span className="font-mono text-size-sm font-medium text-white drop-shadow-md"> | ||
| {displayPct}% | ||
| </span> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Availability display rounds below 100% to "100.00%"Low Severity
Additional Locations (1) |
||
| </div> | ||
| <div | ||
| className={cn( | ||
| 'pointer-events-none absolute top-full left-1/2 z-10 mt-0.5 -translate-x-1/2 whitespace-nowrap sm:block', | ||
| showEndLabels && i === blocks.length - 1 | ||
| ? 'opacity-100' | ||
| : 'hidden opacity-0 transition-opacity group-hover:opacity-100' | ||
| )} | ||
| > | ||
| <span className="text-size-xs text-content-secondary">{block.label}</span> | ||
| </div> | ||
| </> | ||
| ); | ||
|
|
||
| return isFullUptime ? ( | ||
| <div key={i} className="group relative min-w-0 flex-1"> | ||
| {inner} | ||
| </div> | ||
| ) : ( | ||
| <Link | ||
| key={i} | ||
| className="group relative min-w-0 flex-1" | ||
| to="/" | ||
| search={{ | ||
| affected_region: [block.regionName], | ||
| created_after: block.periodStart, | ||
| created_before: block.periodEnd, | ||
| service_tier: ['T0'], | ||
| status: ['Any'], | ||
| }} | ||
| > | ||
| {inner} | ||
| </Link> | ||
| ); | ||
| })} | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import {Link} from '@tanstack/react-router'; | ||
| import {cn} from 'utils/cn'; | ||
|
|
||
| import {type Period} from '../queries/availabilityQueryOptions'; | ||
|
|
||
| const TABS: {value: Period; label: string}[] = [ | ||
| {value: 'month', label: 'Month'}, | ||
| {value: 'quarter', label: 'Quarter'}, | ||
| {value: 'year', label: 'Year'}, | ||
| ]; | ||
|
|
||
| interface PeriodTabsProps { | ||
| activePeriod: Period; | ||
| } | ||
|
|
||
| export function PeriodTabs({activePeriod}: PeriodTabsProps) { | ||
| return ( | ||
| <div className="gap-space-2xs flex"> | ||
| {TABS.map(tab => ( | ||
| <Link | ||
| key={tab.value} | ||
| to="/availability" | ||
| search={{period: tab.value}} | ||
| preload="intent" | ||
| className={cn( | ||
| 'rounded-radius-sm px-space-lg py-space-sm text-size-sm font-medium transition-colors', | ||
| { | ||
| 'bg-background-primary dark:bg-background-transparent-neutral-muted text-content-headings shadow-sm': | ||
| activePeriod === tab.value, | ||
| 'text-content-secondary hover:text-black dark:hover:text-white': | ||
| activePeriod !== tab.value, | ||
| } | ||
| )} | ||
| > | ||
| {tab.label} | ||
| </Link> | ||
| ))} | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import {Card} from 'components/Card'; | ||
|
|
||
| import {type PeriodData} from '../queries/availabilityQueryOptions'; | ||
|
|
||
| import {HeatmapBar} from './HeatmapBar'; | ||
|
|
||
| interface RegionCardProps { | ||
| regionName: string; | ||
| periods: PeriodData[]; | ||
| showPeriodLabels?: boolean; | ||
| } | ||
|
|
||
| export function RegionCard({regionName, periods, showPeriodLabels}: RegionCardProps) { | ||
| const heatmapBlocks = [...periods].reverse().map(p => { | ||
| const region = p.regions.find(r => r.name === regionName); | ||
| return { | ||
| label: p.label, | ||
| availability: region?.availability_percentage ?? 100, | ||
| periodStart: p.start, | ||
| periodEnd: p.end, | ||
| regionName, | ||
| }; | ||
| }); | ||
|
|
||
| return ( | ||
| <Card className="p-0"> | ||
| <div className="flex flex-col gap-3 px-space-xl pt-3 pb-6"> | ||
| <span className="text-content-headings text-size-lg font-semibold"> | ||
| {regionName} | ||
| </span> | ||
| <HeatmapBar blocks={heatmapBlocks} showEndLabels={showPeriodLabels} /> | ||
| </div> | ||
| </Card> | ||
| ); | ||
| } |


Uh oh!
There was an error while loading. Please reload this page.