Skip to content

Commit 3263678

Browse files
charlieparkbenjaminleonarddavid-crespo
authored
Instance Metrics using OxQL (#2654)
* Add stub of Disk Metrics using OxQL * Refactoring; getting chart working; needs better default situation * refacotrs; remove old DiskMetrics; add writes and flushes charts * initial stub for CPU metrics * file reorg * More CPU metrics, though we mgiht need to rethink the long-term plan * working group_by * Updates to routes to handle sub-tabs * Dropdown working on CPU charts * cleanup * more work on charts; networking * Standardize wrapper components * Reorder charts a bit * getting side tabs into place * Update side tab CSS * Consolidate SideTabs into legacy tabs, using props to control layout * refactoring; getting rollups working for disks and network interfaces * Pass pre-formed query string to metric component * Move date selector up a level, using useContext * Update routes in path-builder test * Small refactor to align approach to useState and dropdowns for network and disk metrics tabs * Add static values for metrics for testing and mock service worker * Removes TS guard that was a bit onerous; relying on casting now, though, which isn't great * Updated mock data for disks * small refactor before integrating Ben's PR * Refactoring chart logic * Add tests for OxQL charts * Better handle cumulative_u64 data with initial sum value * Instance metrics design tweaks (#2676) Co-authored-by: David Crespo <[email protected]> Co-authored-by: Charlie Park <[email protected]> * a little code cleanup * make getOxqlQuery args more generic and structured * view/copy oxql modal * inline oxql query modal, remove comment about showing query * NonEmptyArray whaaaaaaat * highlight oxql * Add 'More about OxQL queries' button/link to modal * test for rendered oxql in modal * Better link style for OxQL docs * slightly smaller text * clean up my weird half-finished metrics props change * CopyCode footer * handle no nics case on network metrics * small aria label fix * Add restriction to only turn on query reloading once initial data have succcessfully loaded in * Simplify CPU utilization tab * Metrics more actions (#2700) * OxQL metrics more actions * take CopyCodeModal refactor further, fix motion import * move oxql schema docs thing into links file --------- Co-authored-by: David Crespo <[email protected]> * tweak more actions menu copy one more time * Dynamic chart Y axis width (#2697) * Dynamic Y axis width * Remove spacing and make tick size/margin explicit * Need spacing on old cards * Updates, and better logic on utilization chart; still accounting for cpus count * Updates to incorporate nCPUs in utilization calculation * small refactor * Updated test for utilization * Move OxqlMetric files to own component directory * A few more tests * update import * tests are easier to make sense of when you can see all the data at once * Default to single state on CPU utiization tab; offer 'total' option * Update metrics schema URL * Metrics error & loading states (#2698) * Move some loaders to parent component * Update dropdown to cap at 24 hours and handle minimum mean_within * Use seconds when determining durations * remove intervalPicker until OxQL is faster * Less twitchy datepicker wrap * clean up chart loading states * init MetricsContext with null instead of dummy values * Update mock numbers so CPU utilization range is normal * use lazy imports in the routes * blarg lint * Clean up CPU charts * revert CpuStateMetric component * utils file tweaks, abstract slightly less * replace getUnit with explicit unit prop * use date-fns * use delay function for sleeps --------- Co-authored-by: Benjamin Leonard <[email protected]> Co-authored-by: David Crespo <[email protected]>
1 parent a72b460 commit 3263678

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2340
-519
lines changed

app/api/__tests__/safety.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ it('mock-api is only referenced in test files', () => {
5151
"app/main.tsx",
5252
"app/msw-mock-api.ts",
5353
"docs/mock-api-differences.md",
54+
"mock-api/msw/util.ts",
5455
"package.json",
5556
"test/e2e/utils.ts",
5657
"test/unit/server.ts",

app/api/util.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ export type ChartDatum = {
229229
// we're doing the x axis as timestamp ms instead of Date primarily to make
230230
// type=number work
231231
timestamp: number
232-
value: number
232+
value: number | null
233233
}
234234

235235
/** fill in data points at start and end of range */

app/components/CopyCode.tsx

Lines changed: 64 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8+
import * as m from 'motion/react-m'
89
import { useState, type ReactNode } from 'react'
910

1011
import { Success12Icon } from '@oxide/design-system/icons/react'
@@ -13,29 +14,27 @@ import { Button } from '~/ui/lib/Button'
1314
import { Modal } from '~/ui/lib/Modal'
1415
import { useTimeout } from '~/ui/lib/use-timeout'
1516

16-
type CopyCodeProps = {
17+
type CopyCodeModalProps = {
1718
code: string
18-
modalButtonText: string
1919
copyButtonText: string
2020
modalTitle: string
21+
footer?: ReactNode
2122
/** rendered code */
2223
children?: ReactNode
24+
isOpen: boolean
25+
onDismiss: () => void
2326
}
2427

25-
export function CopyCode({
28+
export function CopyCodeModal({
29+
isOpen,
30+
onDismiss,
2631
code,
27-
modalButtonText,
2832
copyButtonText,
2933
modalTitle,
3034
children,
31-
}: CopyCodeProps) {
32-
const [isOpen, setIsOpen] = useState(false)
35+
footer,
36+
}: CopyCodeModalProps) {
3337
const [hasCopied, setHasCopied] = useState(false)
34-
35-
function handleDismiss() {
36-
setIsOpen(false)
37-
}
38-
3938
useTimeout(() => setHasCopied(false), hasCopied ? 2000 : null)
4039

4140
const handleCopy = () => {
@@ -45,35 +44,44 @@ export function CopyCode({
4544
}
4645

4746
return (
48-
<>
49-
<Button variant="ghost" size="sm" className="ml-2" onClick={() => setIsOpen(true)}>
50-
{modalButtonText}
51-
</Button>
52-
<Modal isOpen={isOpen} onDismiss={handleDismiss} title={modalTitle} width="free">
53-
<Modal.Section>
54-
<pre className="flex w-full rounded border px-4 py-3 !normal-case !tracking-normal text-mono-md bg-default border-secondary">
55-
{children}
56-
</pre>
57-
</Modal.Section>
58-
<Modal.Footer
59-
onDismiss={handleDismiss}
60-
onAction={handleCopy}
61-
actionText={
62-
<>
63-
<span className={hasCopied ? 'invisible' : ''}>{copyButtonText}</span>
64-
<span
65-
className={`absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center ${
66-
hasCopied ? '' : 'invisible'
67-
}`}
47+
<Modal isOpen={isOpen} onDismiss={onDismiss} title={modalTitle} width="free">
48+
<Modal.Section>
49+
<pre className="w-full rounded border px-4 py-3 !normal-case !tracking-normal text-mono-md bg-default border-secondary">
50+
{children}
51+
</pre>
52+
</Modal.Section>
53+
<Modal.Footer
54+
onDismiss={onDismiss}
55+
onAction={handleCopy}
56+
actionText={
57+
<>
58+
<m.span
59+
className="flex items-center"
60+
animate={{
61+
opacity: hasCopied ? 0 : 1,
62+
y: hasCopied ? 25 : 0,
63+
}}
64+
transition={{ type: 'spring', duration: 0.3, bounce: 0 }}
65+
>
66+
{copyButtonText}
67+
</m.span>
68+
69+
{hasCopied && (
70+
<m.span
71+
animate={{ opacity: 1, y: '-50%', x: '-50%' }}
72+
initial={{ opacity: 0, y: 'calc(-50% - 25px)', x: '-50%' }}
73+
transition={{ type: 'spring', duration: 0.3, bounce: 0 }}
74+
className="absolute left-1/2 top-1/2 flex items-center"
6875
>
69-
<Success12Icon className="mr-2 text-accent" />
70-
Copied
71-
</span>
72-
</>
73-
}
74-
/>
75-
</Modal>
76-
</>
76+
<Success12Icon className="text-accent" />
77+
</m.span>
78+
)}
79+
</>
80+
}
81+
>
82+
{footer}
83+
</Modal.Footer>
84+
</Modal>
7785
)
7886
}
7987

@@ -86,15 +94,23 @@ export function EquivalentCliCommand({ project, instance }: EquivProps) {
8694
`--instance ${instance}`,
8795
]
8896

97+
const [isOpen, setIsOpen] = useState(false)
98+
8999
return (
90-
<CopyCode
91-
code={cmdParts.join(' ')}
92-
modalButtonText="Equivalent CLI Command"
93-
copyButtonText="Copy command"
94-
modalTitle="CLI command"
95-
>
96-
<div className="mr-2 select-none text-tertiary">$</div>
97-
{cmdParts.join(' \\\n')}
98-
</CopyCode>
100+
<>
101+
<Button variant="ghost" size="sm" className="ml-2" onClick={() => setIsOpen(true)}>
102+
Equivalent CLI Command
103+
</Button>
104+
<CopyCodeModal
105+
code={cmdParts.join(' ')}
106+
copyButtonText="Copy command"
107+
modalTitle="CLI command"
108+
isOpen={isOpen}
109+
onDismiss={() => setIsOpen(false)}
110+
>
111+
<span className="mr-2 select-none text-tertiary">$</span>
112+
{cmdParts.join(' \\\n ')}
113+
</CopyCodeModal>
114+
</>
99115
)
100116
}

app/components/MoreActionsMenu.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8+
import cn from 'classnames'
9+
810
import { More12Icon } from '@oxide/design-system/icons/react'
911

1012
import type { MenuAction } from '~/table/columns/action-col'
@@ -16,13 +18,21 @@ interface MoreActionsMenuProps {
1618
/** The accessible name for the menu button */
1719
label: string
1820
actions: MenuAction[]
21+
isSmall?: boolean
1922
}
20-
export const MoreActionsMenu = ({ actions, label }: MoreActionsMenuProps) => {
23+
export const MoreActionsMenu = ({
24+
actions,
25+
label,
26+
isSmall = false,
27+
}: MoreActionsMenuProps) => {
2128
return (
2229
<DropdownMenu.Root>
2330
<DropdownMenu.Trigger
2431
aria-label={label}
25-
className="flex h-8 w-8 items-center justify-center rounded border border-default hover:bg-tertiary"
32+
className={cn(
33+
'flex items-center justify-center rounded border border-default hover:bg-tertiary',
34+
isSmall ? 'h-6 w-6' : 'h-8 w-8'
35+
)}
2636
>
2737
<More12Icon />
2838
</DropdownMenu.Trigger>

app/components/RefetchIntervalPicker.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,19 @@ type Props = {
3737
enabled: boolean
3838
isLoading: boolean
3939
fn: () => void
40+
showLastFetched?: boolean
41+
className?: string
42+
isSlim?: boolean
4043
}
4144

42-
export function useIntervalPicker({ enabled, isLoading, fn }: Props) {
45+
export function useIntervalPicker({
46+
enabled,
47+
isLoading,
48+
fn,
49+
showLastFetched = false,
50+
className,
51+
isSlim = false,
52+
}: Props) {
4353
const [intervalPreset, setIntervalPreset] = useState<IntervalPreset>('10s')
4454

4555
const [lastFetched, setLastFetched] = useState(new Date())
@@ -53,11 +63,13 @@ export function useIntervalPicker({ enabled, isLoading, fn }: Props) {
5363
return {
5464
intervalMs: (enabled && intervalPresets[intervalPreset]) || undefined,
5565
intervalPicker: (
56-
<div className="mb-12 flex items-center justify-between">
57-
<div className="hidden items-center gap-2 text-right text-mono-sm text-tertiary lg+:flex">
58-
<Time16Icon className="text-quaternary" /> Refreshed{' '}
59-
{toLocaleTimeString(lastFetched)}
60-
</div>
66+
<div className={cn('flex items-center justify-between', className)}>
67+
{showLastFetched && (
68+
<div className="hidden items-center gap-2 text-right text-mono-sm text-tertiary lg+:flex">
69+
<Time16Icon className="text-quaternary" /> Refreshed{' '}
70+
{toLocaleTimeString(lastFetched)}
71+
</div>
72+
)}
6173
<div className="flex">
6274
<button
6375
type="button"
@@ -75,10 +87,11 @@ export function useIntervalPicker({ enabled, isLoading, fn }: Props) {
7587
</button>
7688
<Listbox
7789
selected={enabled ? intervalPreset : 'Off'}
78-
className="w-24 [&_button]:!rounded-l-none"
90+
className={cn('[&_button]:!rounded-l-none', isSlim ? '' : 'w-24')}
7991
items={intervalItems}
8092
onChange={setIntervalPreset}
8193
disabled={!enabled}
94+
hideSelected={isSlim}
8295
/>
8396
</div>
8497
</div>

app/components/RouteTabs.tsx

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,35 @@ const selectTab = (e: React.KeyboardEvent<HTMLDivElement>) => {
3838
export interface RouteTabsProps {
3939
children: ReactNode
4040
fullWidth?: boolean
41+
sideTabs?: boolean
42+
tabListClassName?: string
4143
}
42-
export function RouteTabs({ children, fullWidth }: RouteTabsProps) {
44+
/** Tabbed views, controlling both the layout and functioning of tabs and the panel contents.
45+
* sideTabs: Whether the tabs are displayed on the side of the panel. Default is false.
46+
*/
47+
export function RouteTabs({
48+
children,
49+
fullWidth,
50+
sideTabs = false,
51+
tabListClassName,
52+
}: RouteTabsProps) {
53+
const wrapperClasses = sideTabs
54+
? 'ox-side-tabs flex'
55+
: cn('ox-tabs', { 'full-width': fullWidth })
56+
const tabListClasses = sideTabs ? 'ox-side-tabs-list' : 'ox-tabs-list'
57+
const panelClasses = cn('ox-tabs-panel @container', { 'ml-5 flex-grow': sideTabs })
4358
return (
44-
<div className={cn('ox-tabs', { 'full-width': fullWidth })}>
59+
<div className={wrapperClasses}>
4560
{/* eslint-disable-next-line jsx-a11y/interactive-supports-focus */}
46-
<div role="tablist" className="ox-tabs-list" onKeyDown={selectTab}>
61+
<div
62+
role="tablist"
63+
className={cn(tabListClasses, tabListClassName)}
64+
onKeyDown={selectTab}
65+
>
4766
{children}
4867
</div>
4968
{/* TODO: Add aria-describedby for active tab */}
50-
<div className="ox-tabs-panel" role="tabpanel" tabIndex={0}>
69+
<div className={panelClasses} role="tabpanel" tabIndex={0}>
5170
<Outlet />
5271
</div>
5372
</div>
@@ -57,14 +76,16 @@ export function RouteTabs({ children, fullWidth }: RouteTabsProps) {
5776
export interface TabProps {
5877
to: string
5978
children: ReactNode
79+
sideTab?: boolean
6080
}
61-
export const Tab = ({ to, children }: TabProps) => {
81+
export const Tab = ({ to, children, sideTab = false }: TabProps) => {
6282
const isActive = useIsActivePath({ to })
83+
const baseClass = sideTab ? 'ox-side-tab' : 'ox-tab'
6384
return (
6485
<Link
6586
role="tab"
6687
to={to}
67-
className={cn('ox-tab', { 'is-selected': isActive })}
88+
className={cn(baseClass, { 'is-selected': isActive })}
6889
tabIndex={isActive ? 0 : -1}
6990
aria-selected={isActive}
7091
>

app/components/SystemMetric.tsx

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import React, { Suspense, useMemo, useRef } from 'react'
8+
import { useMemo, useRef } from 'react'
99

1010
import {
1111
synthesizeData,
@@ -16,7 +16,7 @@ import {
1616

1717
import { Spinner } from '~/ui/lib/Spinner'
1818

19-
const TimeSeriesChart = React.lazy(() => import('./TimeSeriesChart'))
19+
import { TimeSeriesChart } from './TimeSeriesChart'
2020

2121
// The difference between system metric and silo metric is
2222
// 1. different endpoints
@@ -99,20 +99,18 @@ export function SiloMetric({
9999
{(inRange.isPending || beforeStart.isPending) && <Spinner />}
100100
</h2>
101101
{/* TODO: proper skeleton for empty chart */}
102-
<Suspense fallback={<div />}>
103-
<div className="mt-3 h-[300px]">
104-
<TimeSeriesChart
105-
data={data}
106-
title={title}
107-
width={480}
108-
height={240}
109-
interpolation="stepAfter"
110-
startTime={startTime}
111-
endTime={endTime}
112-
unit={unit !== 'count' ? unit : undefined}
113-
/>
114-
</div>
115-
</Suspense>
102+
<div className="mt-3 h-[300px]">
103+
<TimeSeriesChart
104+
data={data}
105+
title={title}
106+
width={480}
107+
height={240}
108+
interpolation="stepAfter"
109+
startTime={startTime}
110+
endTime={endTime}
111+
unit={unit !== 'count' ? unit : undefined}
112+
/>
113+
</div>
116114
</div>
117115
)
118116
}
@@ -177,20 +175,18 @@ export function SystemMetric({
177175
{(inRange.isPending || beforeStart.isPending) && <Spinner />}
178176
</h2>
179177
{/* TODO: proper skeleton for empty chart */}
180-
<Suspense fallback={<div />}>
181-
<div className="mt-3 h-[300px]">
182-
<TimeSeriesChart
183-
data={data}
184-
title={title}
185-
width={480}
186-
height={240}
187-
interpolation="stepAfter"
188-
startTime={startTime}
189-
endTime={endTime}
190-
unit={unit !== 'count' ? unit : undefined}
191-
/>
192-
</div>
193-
</Suspense>
178+
<div className="mt-3 h-[300px]">
179+
<TimeSeriesChart
180+
data={data}
181+
title={title}
182+
width={480}
183+
height={240}
184+
interpolation="stepAfter"
185+
startTime={startTime}
186+
endTime={endTime}
187+
unit={unit !== 'count' ? unit : undefined}
188+
/>
189+
</div>
194190
</div>
195191
)
196192
}

0 commit comments

Comments
 (0)