-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathOxqlMetric.tsx
More file actions
130 lines (115 loc) · 4.68 KB
/
OxqlMetric.tsx
File metadata and controls
130 lines (115 loc) · 4.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright Oxide Computer Company
*/
/*
* OxQL Metrics Schema:
* https://github.com/oxidecomputer/omicron/tree/main/oximeter/oximeter/schema
*/
import { useQuery } from '@tanstack/react-query'
import { Children, useMemo, useState, type ReactNode } from 'react'
import type { LoaderFunctionArgs } from 'react-router'
import { apiq, OXQL_GROUP_BY_ERROR, queryClient } from '@oxide/api'
import { CopyCodeModal } from '~/components/CopyCode'
import { MoreActionsMenu } from '~/components/MoreActionsMenu'
import { getInstanceSelector, useProjectSelector } from '~/hooks/use-params'
import { LearnMore } from '~/ui/lib/CardBlock'
import * as Dropdown from '~/ui/lib/DropdownMenu'
import { classed } from '~/util/classed'
import { links } from '~/util/links'
import { ChartContainer, ChartHeader, TimeSeriesChart } from '../TimeSeriesChart'
import { HighlightedOxqlQuery, toOxqlStr } from './HighlightedOxqlQuery'
import {
composeOxqlData,
getBytesChartProps,
getCountChartProps,
getUtilizationChartProps,
type OxqlQuery,
} from './util'
export async function loader({ params }: LoaderFunctionArgs) {
const { project, instance } = getInstanceSelector(params)
await queryClient.prefetchQuery(
apiq('instanceView', { path: { instance }, query: { project } })
)
return null
}
export type OxqlMetricProps = OxqlQuery & {
title: string
description?: string
unit: 'Bytes' | '%' | 'Count'
}
export function OxqlMetric({ title, description, unit, ...queryObj }: OxqlMetricProps) {
const query = toOxqlStr(queryObj)
const { project } = useProjectSelector()
const {
data: metrics,
error,
isLoading,
} = useQuery(
apiq('timeseriesQuery', { body: { query }, query: { project } })
// avoid graphs flashing blank while loading when you change the time
// { placeholderData: (x) => x }
)
// HACK: omicron has a bug where it blows up on an attempt to group by on
// empty result set because it can't determine whether the data is aligned.
// Most likely it should consider empty data sets trivially aligned and just
// flow the emptiness on through, but in the meantime we have to detect
// this error and pretend it is not an error.
// See https://github.com/oxidecomputer/omicron/issues/7715
const errorMeansEmpty = error?.message === OXQL_GROUP_BY_ERROR
const hasError = !!error && !errorMeansEmpty
const { startTime, endTime } = queryObj
const { chartData, valueCounts } = useMemo(
() => (errorMeansEmpty ? { chartData: [], valueCounts: [] } : composeOxqlData(metrics)),
[metrics, errorMeansEmpty]
)
const { data, label, unitForSet, yAxisTickFormatter } = useMemo(() => {
if (unit === 'Bytes') return getBytesChartProps(chartData)
if (unit === 'Count') return getCountChartProps(chartData)
return getUtilizationChartProps(chartData, valueCounts)
}, [unit, chartData, valueCounts])
const [modalOpen, setModalOpen] = useState(false)
return (
<ChartContainer>
<ChartHeader title={title} label={label} description={description}>
<MoreActionsMenu label="Instance actions" isSmall>
<Dropdown.LinkItem to={links.oxqlSchemaDocs(queryObj.metricName)}>
About this metric
</Dropdown.LinkItem>
<Dropdown.Item onSelect={() => setModalOpen(true)} label="View OxQL query" />
</MoreActionsMenu>
<CopyCodeModal
isOpen={modalOpen}
onDismiss={() => setModalOpen(false)}
code={query}
copyButtonText="Copy query"
modalTitle="OxQL query"
footer={<LearnMore href={links.oxqlDocs} text="OxQL" />}
>
<HighlightedOxqlQuery {...queryObj} />
</CopyCodeModal>
</ChartHeader>
<TimeSeriesChart
title={title}
startTime={startTime}
endTime={endTime}
unit={unitForSet}
data={data}
yAxisTickFormatter={yAxisTickFormatter}
hasError={hasError}
// isLoading only covers first load --- future-proof against the reintroduction of interval refresh
loading={isLoading}
/>
</ChartContainer>
)
}
export const MetricHeader = ({ children }: { children: ReactNode }) => {
// If header has only one child, align it to the end of the container
const justify = Children.count(children) === 1 ? 'justify-end' : 'justify-between'
return <div className={`flex flex-wrap gap-2 ${justify}`}>{children}</div>
}
export const MetricCollection = classed.div`mt-3 flex flex-col gap-4`
export const MetricRow = classed.div`flex w-full flex-col gap-4 @[48rem]:flex-row`