Skip to content

Commit c773c3a

Browse files
Merge branch 'main' into instance-auto-restart
2 parents a599745 + 7a8615a commit c773c3a

33 files changed

+587
-556
lines changed

.eslintrc.cjs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ module.exports = {
7070
'import/no-default-export': 'error',
7171
'import/no-unresolved': 'off', // plugin doesn't know anything
7272
'jsx-a11y/label-has-associated-control': [2, { controlComponents: ['button'] }],
73+
// only worry about console.log
74+
'no-console': ['error', { allow: ['warn', 'error', 'info', 'table'] }],
7375
'no-param-reassign': 'error',
7476
'no-restricted-imports': [
7577
'error',
@@ -112,11 +114,18 @@ module.exports = {
112114
},
113115
{
114116
files: ['*.e2e.ts'],
115-
extends: ['plugin:playwright/playwright-test'],
117+
extends: ['plugin:playwright/recommended'],
116118
rules: {
117119
'playwright/expect-expect': [
118120
'warn',
119-
{ assertFunctionNames: ['expectVisible', 'expectRowVisible', 'expectOptions'] },
121+
{
122+
assertFunctionNames: [
123+
'expectVisible',
124+
'expectRowVisible',
125+
'expectOptions',
126+
'expectRowMenuStaysOpen',
127+
],
128+
},
120129
],
121130
'playwright/no-force-option': 'off',
122131
},

.github/workflows/reformatter.yaml

Lines changed: 0 additions & 30 deletions
This file was deleted.

.oxlintrc.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
"plugins": ["react", "react-hooks", "unicorn", "typescript", "oxc"],
44
"rules": {
55
"react-hooks/exhaustive-deps": "error",
6+
// only worry about console.log
7+
"no-console": ["error", { "allow": ["warn", "error", "info", "table"] }],
68
// turning this off because it's more sensitive than eslint currently
79
// "react-hooks/rules-of-hooks": "error",
810
"no-unused-vars": [
@@ -13,5 +15,6 @@
1315
"caughtErrorsIgnorePattern": "^_"
1416
}
1517
]
16-
}
18+
},
19+
"ignorePatterns": ["dist/", "node_modules/"]
1720
}

app/api/hooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ export const wrapQueryClient = <A extends ApiClient>(api: A, queryClient: QueryC
346346
// directly without further processing
347347
throw data
348348
}
349-
console.log(options.explanation)
349+
console.info(options.explanation)
350350
return { type: 'error' as const, data }
351351
}),
352352
...options,

app/api/window.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function handleResult<T>(result: ApiResult<T>): T {
3434
}
3535

3636
function logHeading(s: string) {
37-
console.log(`%c${s}`, 'font-size: 16px; font-weight: bold;')
37+
console.info(`%c${s}`, 'font-size: 16px; font-weight: bold;')
3838
}
3939

4040
if (typeof window !== 'undefined') {

app/components/RoundedSector.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8+
import { useReducedMotion } from 'motion/react'
89
import { useEffect, useMemo, useState } from 'react'
910

10-
import { useReducedMotion } from '~/hooks/use-reduce-motion'
11-
1211
export function RoundedSector({
1312
angle,
1413
size,

app/components/TimeSeriesChart.tsx

Lines changed: 55 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -154,59 +154,60 @@ export default function TimeSeriesChart({
154154
const data = useMemo(() => rawData || [], [rawData])
155155

156156
return (
157-
<ResponsiveContainer width="100%" height={300}>
158-
<AreaChart
159-
width={width}
160-
height={height}
161-
data={data}
162-
margin={{ top: 0, right: 8, bottom: 16, left: 0 }}
163-
className={cn(className, 'rounded-lg border border-default')}
164-
>
165-
<CartesianGrid stroke={GRID_GRAY} vertical={false} />
166-
<XAxis
167-
axisLine={{ stroke: GRID_GRAY }}
168-
tickLine={{ stroke: GRID_GRAY }}
169-
// TODO: show full given date range in the chart even if the data doesn't fill the range
170-
domain={['auto', 'auto']}
171-
dataKey="timestamp"
172-
interval="preserveStart"
173-
scale="time"
174-
// TODO: use Date directly as x-axis values
175-
type="number"
176-
name="Time"
177-
ticks={getTicks(data, 5)}
178-
tickFormatter={isSameDay(startTime, endTime) ? shortTime : shortDateTime}
179-
tick={textMonoMd}
180-
tickMargin={8}
181-
/>
182-
<YAxis
183-
axisLine={{ stroke: GRID_GRAY }}
184-
tickLine={{ stroke: GRID_GRAY }}
185-
orientation="right"
186-
tick={textMonoMd}
187-
tickMargin={8}
188-
tickFormatter={yAxisTickFormatter}
189-
padding={{ top: 32 }}
190-
{...yTicks}
191-
/>
192-
{/* TODO: stop tooltip being focused by default on pageload if nothing else has been clicked */}
193-
<Tooltip
194-
isAnimationActive={false}
195-
content={(props: TooltipProps<number, string>) => renderTooltip(props, unit)}
196-
cursor={{ stroke: CURSOR_GRAY, strokeDasharray: '3,3' }}
197-
wrapperStyle={{ outline: 'none' }}
198-
/>
199-
<Area
200-
dataKey="value"
201-
name={title}
202-
type={interpolation}
203-
stroke={GREEN_600}
204-
fill={GREEN_400}
205-
isAnimationActive={false}
206-
dot={false}
207-
activeDot={{ fill: GREEN_800, r: 3, strokeWidth: 0 }}
208-
/>
209-
</AreaChart>
210-
</ResponsiveContainer>
157+
<div className="h-[300px] w-full">
158+
<ResponsiveContainer className={cn(className, 'rounded-lg border border-default')}>
159+
<AreaChart
160+
width={width}
161+
height={height}
162+
data={data}
163+
margin={{ top: 0, right: 8, bottom: 16, left: 0 }}
164+
>
165+
<CartesianGrid stroke={GRID_GRAY} vertical={false} />
166+
<XAxis
167+
axisLine={{ stroke: GRID_GRAY }}
168+
tickLine={{ stroke: GRID_GRAY }}
169+
// TODO: show full given date range in the chart even if the data doesn't fill the range
170+
domain={['auto', 'auto']}
171+
dataKey="timestamp"
172+
interval="preserveStart"
173+
scale="time"
174+
// TODO: use Date directly as x-axis values
175+
type="number"
176+
name="Time"
177+
ticks={getTicks(data, 5)}
178+
tickFormatter={isSameDay(startTime, endTime) ? shortTime : shortDateTime}
179+
tick={textMonoMd}
180+
tickMargin={8}
181+
/>
182+
<YAxis
183+
axisLine={{ stroke: GRID_GRAY }}
184+
tickLine={{ stroke: GRID_GRAY }}
185+
orientation="right"
186+
tick={textMonoMd}
187+
tickMargin={8}
188+
tickFormatter={yAxisTickFormatter}
189+
padding={{ top: 32 }}
190+
{...yTicks}
191+
/>
192+
{/* TODO: stop tooltip being focused by default on pageload if nothing else has been clicked */}
193+
<Tooltip
194+
isAnimationActive={false}
195+
content={(props: TooltipProps<number, string>) => renderTooltip(props, unit)}
196+
cursor={{ stroke: CURSOR_GRAY, strokeDasharray: '3,3' }}
197+
wrapperStyle={{ outline: 'none' }}
198+
/>
199+
<Area
200+
dataKey="value"
201+
name={title}
202+
type={interpolation}
203+
stroke={GREEN_600}
204+
fill={GREEN_400}
205+
isAnimationActive={false}
206+
dot={false}
207+
activeDot={{ fill: GREEN_800, r: 3, strokeWidth: 0 }}
208+
/>
209+
</AreaChart>
210+
</ResponsiveContainer>
211+
</div>
211212
)
212213
}

app/components/ToastStack.tsx

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,39 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { animated, useTransition } from '@react-spring/web'
8+
import { AnimatePresence } from 'motion/react'
9+
import * as m from 'motion/react-m'
910

1011
import { removeToast, useToastStore } from '~/stores/toast'
1112
import { Toast } from '~/ui/lib/Toast'
1213

1314
export function ToastStack() {
1415
const toasts = useToastStore((state) => state.toasts)
1516

16-
const transition = useTransition(toasts, {
17-
keys: (toast) => toast.id,
18-
from: { opacity: 0, y: 10, scale: 95 },
19-
enter: { opacity: 1, y: 0, scale: 100 },
20-
leave: { opacity: 0, y: 10, scale: 95 },
21-
config: { duration: 100 },
22-
})
23-
2417
return (
2518
<div
2619
className="pointer-events-auto fixed bottom-4 left-4 z-toast flex flex-col items-end space-y-2"
2720
data-testid="Toasts"
2821
>
29-
{transition((style, item) => (
30-
<animated.div
31-
style={{
32-
opacity: style.opacity,
33-
y: style.y,
34-
transform: style.scale.to((val) => `scale(${val}%, ${val}%)`),
35-
}}
36-
>
37-
<Toast
38-
key={item.id}
39-
{...item.options}
40-
onClose={() => {
41-
removeToast(item.id)
42-
item.options.onClose?.()
43-
}}
44-
/>
45-
</animated.div>
46-
))}
22+
<AnimatePresence>
23+
{toasts.map((toast) => (
24+
<m.div
25+
key={toast.id}
26+
initial={{ opacity: 0, y: 20, scale: 0.95 }}
27+
animate={{ opacity: 1, y: 0, scale: 1 }}
28+
exit={{ opacity: 0, y: 20, scale: 0.95 }}
29+
transition={{ type: 'spring', duration: 0.2, bounce: 0 }}
30+
>
31+
<Toast
32+
{...toast.options}
33+
onClose={() => {
34+
removeToast(toast.id)
35+
toast.options.onClose?.()
36+
}}
37+
/>
38+
</m.div>
39+
))}
40+
</AnimatePresence>
4741
</div>
4842
)
4943
}

app/forms/floating-ip-edit.tsx

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,41 @@
66
* Copyright Oxide Computer Company
77
*/
88
import { useForm } from 'react-hook-form'
9-
import { useNavigate, type LoaderFunctionArgs } from 'react-router'
9+
import { Link, useNavigate, type LoaderFunctionArgs } from 'react-router'
1010

11-
import { apiq, queryClient, useApiMutation, usePrefetchedApiQuery } from '@oxide/api'
11+
import {
12+
apiq,
13+
getListQFn,
14+
queryClient,
15+
useApiMutation,
16+
usePrefetchedQuery,
17+
} from '@oxide/api'
1218

1319
import { DescriptionField } from '~/components/form/fields/DescriptionField'
1420
import { NameField } from '~/components/form/fields/NameField'
1521
import { SideModalForm } from '~/components/form/SideModalForm'
1622
import { HL } from '~/components/HL'
1723
import { getFloatingIpSelector, useFloatingIpSelector } from '~/hooks/use-params'
1824
import { addToast } from '~/stores/toast'
25+
import { EmptyCell } from '~/table/cells/EmptyCell'
26+
import { IpPoolCell } from '~/table/cells/IpPoolCell'
27+
import { CopyableIp } from '~/ui/lib/CopyableIp'
28+
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
29+
import { ALL_ISH } from '~/util/consts'
1930
import type * as PP from '~/util/path-params'
2031
import { pb } from 'app/util/path-builder'
2132

2233
const floatingIpView = ({ project, floatingIp }: PP.FloatingIp) =>
2334
apiq('floatingIpView', { path: { floatingIp }, query: { project } })
35+
const instanceList = (project: string) =>
36+
getListQFn('instanceList', { query: { project, limit: ALL_ISH } })
2437

2538
EditFloatingIpSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
2639
const selector = getFloatingIpSelector(params)
27-
await queryClient.prefetchQuery(floatingIpView(selector))
40+
await Promise.all([
41+
queryClient.fetchQuery(floatingIpView(selector)),
42+
queryClient.fetchQuery(instanceList(selector.project).optionsFn()),
43+
])
2844
return null
2945
}
3046

@@ -35,10 +51,11 @@ export function EditFloatingIpSideModalForm() {
3551

3652
const onDismiss = () => navigate(pb.floatingIps({ project: floatingIpSelector.project }))
3753

38-
const { data: floatingIp } = usePrefetchedApiQuery('floatingIpView', {
39-
path: { floatingIp: floatingIpSelector.floatingIp },
40-
query: { project: floatingIpSelector.project },
41-
})
54+
const { data: floatingIp } = usePrefetchedQuery(floatingIpView(floatingIpSelector))
55+
const { data: instances } = usePrefetchedQuery(
56+
instanceList(floatingIpSelector.project).optionsFn()
57+
)
58+
const instanceName = instances.items.find((i) => i.id === floatingIp.instanceId)?.name
4259

4360
const editFloatingIp = useApiMutation('floatingIpUpdate', {
4461
onSuccess(_floatingIp) {
@@ -49,7 +66,6 @@ export function EditFloatingIpSideModalForm() {
4966
})
5067

5168
const form = useForm({ defaultValues: floatingIp })
52-
5369
return (
5470
<SideModalForm
5571
form={form}
@@ -66,6 +82,32 @@ export function EditFloatingIpSideModalForm() {
6682
loading={editFloatingIp.isPending}
6783
submitError={editFloatingIp.error}
6884
>
85+
<PropertiesTable>
86+
<PropertiesTable.IdRow id={floatingIp.id} />
87+
<PropertiesTable.DateRow label="Created" date={floatingIp.timeCreated} />
88+
<PropertiesTable.DateRow label="Updated" date={floatingIp.timeModified} />
89+
<PropertiesTable.Row label="IP Address">
90+
<CopyableIp ip={floatingIp.ip} isLinked={false} />
91+
</PropertiesTable.Row>
92+
<PropertiesTable.Row label="IP Pool">
93+
<IpPoolCell ipPoolId={floatingIp.ipPoolId} />
94+
</PropertiesTable.Row>
95+
<PropertiesTable.Row label="Instance">
96+
{instanceName ? (
97+
<Link
98+
to={pb.instanceNetworking({
99+
project: floatingIpSelector.project,
100+
instance: instanceName,
101+
})}
102+
className="link-with-underline group text-sans-md"
103+
>
104+
{instanceName}
105+
</Link>
106+
) : (
107+
<EmptyCell />
108+
)}
109+
</PropertiesTable.Row>
110+
</PropertiesTable>
69111
<NameField name="name" control={form.control} />
70112
<DescriptionField name="description" control={form.control} />
71113
</SideModalForm>

0 commit comments

Comments
 (0)