Skip to content

Instance Metrics using OxQL #2654

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

Merged
merged 82 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
2c3d3bf
Add stub of Disk Metrics using OxQL
charliepark Jan 17, 2025
b7f8d9f
Merge branch 'main' into oxql_disk_metrics
charliepark Jan 23, 2025
b313aac
Refactoring; getting chart working; needs better default situation
charliepark Jan 27, 2025
4c61baf
refacotrs; remove old DiskMetrics; add writes and flushes charts
charliepark Jan 29, 2025
3a94b31
initial stub for CPU metrics
charliepark Jan 29, 2025
503a47e
file reorg
charliepark Jan 29, 2025
1bd508d
More CPU metrics, though we mgiht need to rethink the long-term plan
charliepark Jan 30, 2025
82368ed
working group_by
charliepark Jan 30, 2025
b7267fb
Updates to routes to handle sub-tabs
charliepark Jan 30, 2025
8745bec
Dropdown working on CPU charts
charliepark Jan 30, 2025
3faf22b
cleanup
charliepark Jan 30, 2025
67eb979
more work on charts; networking
charliepark Jan 31, 2025
26dfd35
Standardize wrapper components
charliepark Jan 31, 2025
4723da2
Reorder charts a bit
charliepark Jan 31, 2025
40bd0c5
getting side tabs into place
charliepark Jan 31, 2025
d332180
Update side tab CSS
charliepark Jan 31, 2025
77bd509
Consolidate SideTabs into legacy tabs, using props to control layout
charliepark Feb 1, 2025
e3e2382
refactoring; getting rollups working for disks and network interfaces
charliepark Feb 4, 2025
450fc98
Pass pre-formed query string to metric component
charliepark Feb 4, 2025
36de1bc
Move date selector up a level, using useContext
charliepark Feb 5, 2025
dd96051
Update routes in path-builder test
charliepark Feb 5, 2025
4ad4271
Small refactor to align approach to useState and dropdowns for networ…
charliepark Feb 5, 2025
8d80808
Add static values for metrics for testing and mock service worker
charliepark Feb 6, 2025
dae5172
Merge branch 'main' into oxql_disk_metrics
charliepark Feb 7, 2025
51ef360
Removes TS guard that was a bit onerous; relying on casting now, thou…
charliepark Feb 7, 2025
0ad871e
Updated mock data for disks
charliepark Feb 7, 2025
6fd2397
small refactor before integrating Ben's PR
charliepark Feb 7, 2025
c673784
Refactoring chart logic
charliepark Feb 11, 2025
2c4d130
Add tests for OxQL charts
charliepark Feb 11, 2025
ef48320
Better handle cumulative_u64 data with initial sum value
charliepark Feb 11, 2025
d35afe4
Merge branch 'main' into oxql_disk_metrics
charliepark Feb 12, 2025
bb8d32b
Instance metrics design tweaks (#2676)
benjaminleonard Feb 12, 2025
bfc9714
a little code cleanup
david-crespo Feb 13, 2025
c143e71
make getOxqlQuery args more generic and structured
david-crespo Feb 13, 2025
83be393
merge main
david-crespo Feb 13, 2025
1437239
view/copy oxql modal
david-crespo Feb 13, 2025
520ee8b
inline oxql query modal, remove comment about showing query
david-crespo Feb 13, 2025
d1f8e85
NonEmptyArray whaaaaaaat
david-crespo Feb 13, 2025
a7ba787
highlight oxql
david-crespo Feb 13, 2025
b518077
Add 'More about OxQL queries' button/link to modal
charliepark Feb 14, 2025
17446b0
test for rendered oxql in modal
david-crespo Feb 14, 2025
3d31bd2
Better link style for OxQL docs
charliepark Feb 14, 2025
4614ac8
slightly smaller text
charliepark Feb 14, 2025
aefe263
clean up my weird half-finished metrics props change
david-crespo Feb 14, 2025
d476b3a
CopyCode footer
david-crespo Feb 14, 2025
e8710a8
handle no nics case on network metrics
david-crespo Feb 14, 2025
56502be
small aria label fix
charliepark Feb 14, 2025
7016bee
Add restriction to only turn on query reloading once initial data hav…
charliepark Feb 15, 2025
dc8e38e
Simplify CPU utilization tab
charliepark Feb 18, 2025
8f4aa9b
Metrics more actions (#2700)
benjaminleonard Feb 18, 2025
919ca2b
tweak more actions menu copy one more time
david-crespo Feb 18, 2025
82096ba
Dynamic chart Y axis width (#2697)
benjaminleonard Feb 18, 2025
9141b3f
merge main (react 19)
david-crespo Feb 18, 2025
7661b10
Updates, and better logic on utilization chart; still accounting for …
charliepark Feb 20, 2025
65f0f43
Merge branch 'main' into oxql_disk_metrics
charliepark Feb 20, 2025
d552feb
Updates to incorporate nCPUs in utilization calculation
charliepark Feb 21, 2025
2ee92f0
small refactor
charliepark Feb 21, 2025
8cc6018
Updated test for utilization
charliepark Feb 21, 2025
8a1e80f
Move OxqlMetric files to own component directory
charliepark Feb 22, 2025
9c5aae3
A few more tests
charliepark Feb 22, 2025
1a8d551
update import
charliepark Feb 22, 2025
3758390
tests are easier to make sense of when you can see all the data at once
charliepark Feb 22, 2025
1cf9bca
Default to single state on CPU utiization tab; offer 'total' option
charliepark Feb 24, 2025
ed338f9
Update metrics schema URL
benjaminleonard Feb 25, 2025
bc8853c
Metrics error & loading states (#2698)
benjaminleonard Feb 25, 2025
cf3faf8
Move some loaders to parent component
charliepark Feb 25, 2025
fa081a7
Update dropdown to cap at 24 hours and handle minimum mean_within
charliepark Feb 26, 2025
7375ac0
Use seconds when determining durations
charliepark Feb 26, 2025
6fa81f4
remove intervalPicker until OxQL is faster
charliepark Feb 26, 2025
8c537dd
Less twitchy datepicker wrap
benjaminleonard Feb 26, 2025
8a0ca56
clean up chart loading states
david-crespo Feb 26, 2025
0e87365
init MetricsContext with null instead of dummy values
david-crespo Feb 26, 2025
f3207fb
Update mock numbers so CPU utilization range is normal
charliepark Feb 26, 2025
2f38adc
use lazy imports in the routes
david-crespo Feb 26, 2025
13e82c0
blarg lint
david-crespo Feb 27, 2025
42b22d3
Clean up CPU charts
charliepark Feb 27, 2025
47da2bf
revert CpuStateMetric component
charliepark Feb 27, 2025
e16b1d7
merge main
david-crespo Feb 27, 2025
f448a0a
utils file tweaks, abstract slightly less
david-crespo Feb 27, 2025
fa375f9
replace getUnit with explicit unit prop
david-crespo Feb 27, 2025
3263861
use date-fns
david-crespo Feb 27, 2025
d5ceef4
use delay function for sleeps
david-crespo Feb 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/api/__tests__/safety.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ it('mock-api is only referenced in test files', () => {
"app/main.tsx",
"app/msw-mock-api.ts",
"docs/mock-api-differences.md",
"mock-api/msw/util.ts",
"package.json",
"test/e2e/utils.ts",
"test/unit/server.ts",
Expand Down
2 changes: 1 addition & 1 deletion app/api/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export type ChartDatum = {
// we're doing the x axis as timestamp ms instead of Date primarily to make
// type=number work
timestamp: number
value: number
value: number | null
}

/** fill in data points at start and end of range */
Expand Down
112 changes: 64 additions & 48 deletions app/components/CopyCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*
* Copyright Oxide Computer Company
*/
import * as m from 'motion/react-m'
import { useState, type ReactNode } from 'react'

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

type CopyCodeProps = {
type CopyCodeModalProps = {
code: string
modalButtonText: string
copyButtonText: string
modalTitle: string
footer?: ReactNode
/** rendered code */
children?: ReactNode
isOpen: boolean
onDismiss: () => void
}

export function CopyCode({
export function CopyCodeModal({
isOpen,
onDismiss,
code,
modalButtonText,
copyButtonText,
modalTitle,
children,
}: CopyCodeProps) {
const [isOpen, setIsOpen] = useState(false)
footer,
}: CopyCodeModalProps) {
const [hasCopied, setHasCopied] = useState(false)

function handleDismiss() {
setIsOpen(false)
}

useTimeout(() => setHasCopied(false), hasCopied ? 2000 : null)

const handleCopy = () => {
Expand All @@ -45,35 +44,44 @@ export function CopyCode({
}

return (
<>
<Button variant="ghost" size="sm" className="ml-2" onClick={() => setIsOpen(true)}>
{modalButtonText}
</Button>
<Modal isOpen={isOpen} onDismiss={handleDismiss} title={modalTitle} width="free">
<Modal.Section>
<pre className="flex w-full rounded border px-4 py-3 !normal-case !tracking-normal text-mono-md bg-default border-secondary">
{children}
</pre>
</Modal.Section>
<Modal.Footer
onDismiss={handleDismiss}
onAction={handleCopy}
actionText={
<>
<span className={hasCopied ? 'invisible' : ''}>{copyButtonText}</span>
<span
className={`absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center ${
hasCopied ? '' : 'invisible'
}`}
<Modal isOpen={isOpen} onDismiss={onDismiss} title={modalTitle} width="free">
<Modal.Section>
<pre className="w-full rounded border px-4 py-3 !normal-case !tracking-normal text-mono-md bg-default border-secondary">
{children}
</pre>
</Modal.Section>
<Modal.Footer
onDismiss={onDismiss}
onAction={handleCopy}
actionText={
<>
<m.span
className="flex items-center"
animate={{
opacity: hasCopied ? 0 : 1,
y: hasCopied ? 25 : 0,
}}
transition={{ type: 'spring', duration: 0.3, bounce: 0 }}
>
{copyButtonText}
</m.span>

{hasCopied && (
<m.span
animate={{ opacity: 1, y: '-50%', x: '-50%' }}
initial={{ opacity: 0, y: 'calc(-50% - 25px)', x: '-50%' }}
transition={{ type: 'spring', duration: 0.3, bounce: 0 }}
className="absolute left-1/2 top-1/2 flex items-center"
>
<Success12Icon className="mr-2 text-accent" />
Copied
</span>
</>
}
/>
</Modal>
</>
<Success12Icon className="text-accent" />
</m.span>
)}
</>
}
>
{footer}
</Modal.Footer>
</Modal>
)
}

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

const [isOpen, setIsOpen] = useState(false)

return (
<CopyCode
code={cmdParts.join(' ')}
modalButtonText="Equivalent CLI Command"
copyButtonText="Copy command"
modalTitle="CLI command"
>
<div className="mr-2 select-none text-tertiary">$</div>
{cmdParts.join(' \\\n')}
</CopyCode>
<>
<Button variant="ghost" size="sm" className="ml-2" onClick={() => setIsOpen(true)}>
Equivalent CLI Command
</Button>
<CopyCodeModal
code={cmdParts.join(' ')}
copyButtonText="Copy command"
modalTitle="CLI command"
isOpen={isOpen}
onDismiss={() => setIsOpen(false)}
>
<span className="mr-2 select-none text-tertiary">$</span>
{cmdParts.join(' \\\n ')}
</CopyCodeModal>
</>
)
}
14 changes: 12 additions & 2 deletions app/components/MoreActionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
*
* Copyright Oxide Computer Company
*/
import cn from 'classnames'

import { More12Icon } from '@oxide/design-system/icons/react'

import type { MenuAction } from '~/table/columns/action-col'
Expand All @@ -16,13 +18,21 @@ interface MoreActionsMenuProps {
/** The accessible name for the menu button */
label: string
actions: MenuAction[]
isSmall?: boolean
}
export const MoreActionsMenu = ({ actions, label }: MoreActionsMenuProps) => {
export const MoreActionsMenu = ({
actions,
label,
isSmall = false,
}: MoreActionsMenuProps) => {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger
aria-label={label}
className="flex h-8 w-8 items-center justify-center rounded border border-default hover:bg-tertiary"
className={cn(
'flex items-center justify-center rounded border border-default hover:bg-tertiary',
isSmall ? 'h-6 w-6' : 'h-8 w-8'
)}
>
<More12Icon />
</DropdownMenu.Trigger>
Expand Down
27 changes: 20 additions & 7 deletions app/components/RefetchIntervalPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,19 @@ type Props = {
enabled: boolean
isLoading: boolean
fn: () => void
showLastFetched?: boolean
className?: string
isSlim?: boolean
}

export function useIntervalPicker({ enabled, isLoading, fn }: Props) {
export function useIntervalPicker({
enabled,
isLoading,
fn,
showLastFetched = false,
className,
isSlim = false,
}: Props) {
const [intervalPreset, setIntervalPreset] = useState<IntervalPreset>('10s')

const [lastFetched, setLastFetched] = useState(new Date())
Expand All @@ -53,11 +63,13 @@ export function useIntervalPicker({ enabled, isLoading, fn }: Props) {
return {
intervalMs: (enabled && intervalPresets[intervalPreset]) || undefined,
intervalPicker: (
<div className="mb-12 flex items-center justify-between">
<div className="hidden items-center gap-2 text-right text-mono-sm text-tertiary lg+:flex">
<Time16Icon className="text-quaternary" /> Refreshed{' '}
{toLocaleTimeString(lastFetched)}
</div>
<div className={cn('flex items-center justify-between', className)}>
{showLastFetched && (
<div className="hidden items-center gap-2 text-right text-mono-sm text-tertiary lg+:flex">
<Time16Icon className="text-quaternary" /> Refreshed{' '}
{toLocaleTimeString(lastFetched)}
</div>
)}
<div className="flex">
<button
type="button"
Expand All @@ -75,10 +87,11 @@ export function useIntervalPicker({ enabled, isLoading, fn }: Props) {
</button>
<Listbox
selected={enabled ? intervalPreset : 'Off'}
className="w-24 [&_button]:!rounded-l-none"
className={cn('[&_button]:!rounded-l-none', isSlim ? '' : 'w-24')}
items={intervalItems}
onChange={setIntervalPreset}
disabled={!enabled}
hideSelected={isSlim}
/>
</div>
</div>
Expand Down
33 changes: 27 additions & 6 deletions app/components/RouteTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,35 @@ const selectTab = (e: React.KeyboardEvent<HTMLDivElement>) => {
export interface RouteTabsProps {
children: ReactNode
fullWidth?: boolean
sideTabs?: boolean
tabListClassName?: string
}
export function RouteTabs({ children, fullWidth }: RouteTabsProps) {
/** Tabbed views, controlling both the layout and functioning of tabs and the panel contents.
* sideTabs: Whether the tabs are displayed on the side of the panel. Default is false.
*/
export function RouteTabs({
children,
fullWidth,
sideTabs = false,
tabListClassName,
}: RouteTabsProps) {
const wrapperClasses = sideTabs
? 'ox-side-tabs flex'
: cn('ox-tabs', { 'full-width': fullWidth })
const tabListClasses = sideTabs ? 'ox-side-tabs-list' : 'ox-tabs-list'
const panelClasses = cn('ox-tabs-panel @container', { 'ml-5 flex-grow': sideTabs })
return (
<div className={cn('ox-tabs', { 'full-width': fullWidth })}>
<div className={wrapperClasses}>
{/* eslint-disable-next-line jsx-a11y/interactive-supports-focus */}
<div role="tablist" className="ox-tabs-list" onKeyDown={selectTab}>
<div
role="tablist"
className={cn(tabListClasses, tabListClassName)}
onKeyDown={selectTab}
>
{children}
</div>
{/* TODO: Add aria-describedby for active tab */}
<div className="ox-tabs-panel" role="tabpanel" tabIndex={0}>
<div className={panelClasses} role="tabpanel" tabIndex={0}>
<Outlet />
</div>
</div>
Expand All @@ -57,14 +76,16 @@ export function RouteTabs({ children, fullWidth }: RouteTabsProps) {
export interface TabProps {
to: string
children: ReactNode
sideTab?: boolean
}
export const Tab = ({ to, children }: TabProps) => {
export const Tab = ({ to, children, sideTab = false }: TabProps) => {
const isActive = useIsActivePath({ to })
const baseClass = sideTab ? 'ox-side-tab' : 'ox-tab'
return (
<Link
role="tab"
to={to}
className={cn('ox-tab', { 'is-selected': isActive })}
className={cn(baseClass, { 'is-selected': isActive })}
tabIndex={isActive ? 0 : -1}
aria-selected={isActive}
>
Expand Down
56 changes: 26 additions & 30 deletions app/components/SystemMetric.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*
* Copyright Oxide Computer Company
*/
import React, { Suspense, useMemo, useRef } from 'react'
import { useMemo, useRef } from 'react'

import {
synthesizeData,
Expand All @@ -16,7 +16,7 @@ import {

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

const TimeSeriesChart = React.lazy(() => import('./TimeSeriesChart'))
import { TimeSeriesChart } from './TimeSeriesChart'

// The difference between system metric and silo metric is
// 1. different endpoints
Expand Down Expand Up @@ -99,20 +99,18 @@ export function SiloMetric({
{(inRange.isPending || beforeStart.isPending) && <Spinner />}
</h2>
{/* TODO: proper skeleton for empty chart */}
<Suspense fallback={<div />}>
<div className="mt-3 h-[300px]">
<TimeSeriesChart
data={data}
title={title}
width={480}
height={240}
interpolation="stepAfter"
startTime={startTime}
endTime={endTime}
unit={unit !== 'count' ? unit : undefined}
/>
</div>
</Suspense>
<div className="mt-3 h-[300px]">
<TimeSeriesChart
data={data}
title={title}
width={480}
height={240}
interpolation="stepAfter"
startTime={startTime}
endTime={endTime}
unit={unit !== 'count' ? unit : undefined}
/>
</div>
</div>
)
}
Expand Down Expand Up @@ -177,20 +175,18 @@ export function SystemMetric({
{(inRange.isPending || beforeStart.isPending) && <Spinner />}
</h2>
{/* TODO: proper skeleton for empty chart */}
<Suspense fallback={<div />}>
<div className="mt-3 h-[300px]">
<TimeSeriesChart
data={data}
title={title}
width={480}
height={240}
interpolation="stepAfter"
startTime={startTime}
endTime={endTime}
unit={unit !== 'count' ? unit : undefined}
/>
</div>
</Suspense>
<div className="mt-3 h-[300px]">
<TimeSeriesChart
data={data}
title={title}
width={480}
height={240}
interpolation="stepAfter"
startTime={startTime}
endTime={endTime}
unit={unit !== 'count' ? unit : undefined}
/>
</div>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got rid of the Suspense here in 2f38adc because we're relying on RR's lazy loading instead of doing it manually.

</div>
)
}
Loading
Loading