Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
warnOnUnsupportedTypeScriptVersion: false,
// this config is needed for type aware lint rules
project: true,
tsconfigRootDir: __dirname,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended-type-checked',
'plugin:@typescript-eslint/strict',
'plugin:@typescript-eslint/stylistic',
'plugin:jsx-a11y/recommended',
Expand Down Expand Up @@ -45,6 +49,18 @@ module.exports = {
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],

// disabling the type-aware rules we don't like
// https://typescript-eslint.io/getting-started/typed-linting/
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-misused-promises': 'off',
'@typescript-eslint/unbound-method': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',

eqeqeq: ['error', 'always', { null: 'ignore' }],
'import/no-default-export': 'error',
'import/no-unresolved': 'off', // plugin doesn't know anything
Expand Down
4 changes: 2 additions & 2 deletions app/components/form/SideModalForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import { useEffect, useId, type ReactNode } from 'react'
import type { FieldValues, UseFormReturn } from 'react-hook-form'
import { useNavigationType } from 'react-router-dom'
import { NavigationType, useNavigationType } from 'react-router-dom'

import type { ApiError } from '@oxide/api'

Expand Down Expand Up @@ -57,7 +57,7 @@ type SideModalFormProps<TFieldValues extends FieldValues> = {
* any way to distinguish between fresh pageload and back/forward.
*/
export function useShouldAnimateModal() {
return useNavigationType() === 'PUSH'
return useNavigationType() === NavigationType.Push
}

export function SideModalForm<TFieldValues extends FieldValues>({
Expand Down
4 changes: 2 additions & 2 deletions app/components/form/fields/DateTimeRangePicker.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe.skip('custom mode', () => {
expect(screen.getByRole('button', { name: 'Load' })).toHaveClass('visually-disabled')
})

it('clicking load after changing date changes range', async () => {
it('clicking load after changing date changes range', () => {
const { setRange } = renderLastDay()

// expect(screen.getByLabelText('Start time')).toHaveValue(dateForInput(subDays(now, 1)))
Expand Down Expand Up @@ -125,7 +125,7 @@ describe.skip('custom mode', () => {
})
})

it('clicking reset after changing inputs resets inputs', async () => {
it('clicking reset after changing inputs resets inputs', () => {
const { setRange } = renderLastDay()

// expect(screen.getByLabelText('Start time')).toHaveValue(dateForInput(subDays(now, 1)))
Expand Down
2 changes: 1 addition & 1 deletion app/components/form/fields/ListboxField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export type ListboxFieldProps<
className?: string
label?: string
required?: boolean
description?: string | React.ReactNode | React.ReactNode
description?: string | React.ReactNode
tooltipText?: string
control: Control<TFieldValues>
disabled?: boolean
Expand Down
2 changes: 1 addition & 1 deletion app/forms/image-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ export function CreateImageSideModalForm() {
// coordinating when to cleanup, we make cleanup idempotent by having it check
// whether it has already been run, or more concretely before each action,
// check whether it needs to be done
async function closeModal() {
function closeModal() {
if (allDone) {
backToImages()
return
Expand Down
2 changes: 1 addition & 1 deletion app/forms/vpc-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function EditVpcSideModalForm() {
const onDismiss = () => navigate(pb.vpcs({ project }))

const editVpc = useApiMutation('vpcUpdate', {
async onSuccess(vpc) {
onSuccess(vpc) {
queryClient.invalidateQueries('vpcList')
queryClient.setQueryData(
'vpcView',
Expand Down
2 changes: 1 addition & 1 deletion app/pages/SiloAccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export function SiloAccessPage() {
doDelete: () =>
updatePolicy.mutateAsync({
// we know policy is there, otherwise there's no row to display
body: deleteRole(row.id, siloPolicy!),
body: deleteRole(row.id, siloPolicy),
}),
label: (
<span>
Expand Down
2 changes: 1 addition & 1 deletion app/pages/project/access/ProjectAccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export function ProjectAccessPage() {
updatePolicy.mutateAsync({
path: { project },
// we know policy is there, otherwise there's no row to display
body: deleteRole(row.id, projectPolicy!),
body: deleteRole(row.id, projectPolicy),
}),
// TODO: explain that this will not affect the role inherited from
// the silo or roles inherited from group membership. Ideally we'd
Expand Down
12 changes: 3 additions & 9 deletions app/pages/project/instances/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,10 @@ export const useMakeInstanceActions = (
onActivate() {
confirmAction({
actionType: 'danger',
doAction: async () =>
stopInstance.mutate(instanceParams, {
doAction: () =>
stopInstance.mutateAsync(instanceParams, {
onSuccess: () =>
addToast({ title: `Stopping instance '${instance.name}'` }),
onError: (error) =>
addToast({
variant: 'error',
title: `Error stopping instance '${instance.name}'`,
content: error.message,
}),
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is interesting! What happened here is that the new eslint rule made me remove the async because there was no await, which allowed TS to see that the function was not returning a promise like it was supposed to (I guess every async function returns Promise<_> automatically), so that errored, alerting me to the fact that this is set up wrong. It was working fine because of the manual toast, but the errorTitle bit below now covers that because the error handled inside confirmAction.

This is kind of a confusing pattern, but I think the idea was to ensure that there is always some error toast. On unconfirmed actions like instance start, there's nothing forcing you to handle the error, so if you forget to define onError, it does nothing on error, which is annoying. The problem now is that on ones wrapped in confirmAction, you don't see an onError, so maybe you think that's the normal way, but for non-confirmed actions you need to define it.

}),
modalTitle: 'Confirm stop instance',
modalContent: (
Expand All @@ -89,7 +83,7 @@ export const useMakeInstanceActions = (
freed.
</p>
),
errorTitle: `Could not stop ${instance.name}`,
errorTitle: `Error stopping ${instance.name}`,
})
},
disabled: !instanceCan.stop(instance) && (
Expand Down
4 changes: 3 additions & 1 deletion app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ const staticColumns = [
cell: (info) => {
const { hosts, ports, protocols } = info.getValue()
const children = [
...(hosts || []).map((tv, i) => <TypeValueCell key={`${tv}-${i}`} {...tv} />),
...(hosts || []).map((tv, i) => (
<TypeValueCell key={`host-${tv.type}-${tv.value}-${i}`} {...tv} />
)),
...(protocols || []).map((p, i) => <Badge key={`${p}-${i}`}>{p}</Badge>),
...(ports || []).map((p, i) => (
<TypeValueCell key={`port-${p}-${i}`} type="Port" value={p} />
Expand Down
2 changes: 1 addition & 1 deletion app/ui/lib/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
return (
<Wrap
when={isDisabled && disabledReason}
with={<Tooltip content={disabledReason!} ref={ref} />}
with={<Tooltip content={disabledReason} ref={ref} />}
>
<button
className={cn(buttonStyle({ size, variant }), className, {
Expand Down
2 changes: 1 addition & 1 deletion app/util/file.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { describe, expect, test } from 'vitest'

import { readBlobAsBase64 } from './file'

describe('readBlobAsBase64', async () => {
describe('readBlobAsBase64', () => {
test('works with zeros', async () => {
const blob = new Blob([Buffer.alloc(10)])
const text = await readBlobAsBase64(blob)
Expand Down
2 changes: 2 additions & 0 deletions app/util/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export function percentage<T extends number | bigint>(top: T, bottom: T): number
// be like 10^20 bigger than bottom. In any case, the nice thing is it seems
// JS runtimes will not overflow when Number is given a huge arg, they just
// convert to a huge number with reduced precision.
// type assertion is necessary according to TS. bug in ts-eslint
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
return Number(((top as bigint) * 10_000n) / (bottom as bigint)) / 100
}

Expand Down
4 changes: 2 additions & 2 deletions mock-api/msw/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ export const handlers = makeHandlers({
const instances = db.instances.filter((i) => i.project_id === project.id)
return paginated(query, instances)
},
async instanceCreate({ body, query }) {
instanceCreate({ body, query }) {
const project = lookup.project(query)

if (body.name === 'no-default-pool') {
Expand Down Expand Up @@ -646,7 +646,7 @@ export const handlers = makeHandlers({
return json(instance, { status: 202 })
},
ipPoolList: ({ query }) => paginated(query, db.ipPools),
async ipPoolUtilizationView({ path }) {
ipPoolUtilizationView({ path }) {
const pool = lookup.ipPool(path)
const ranges = db.ipPoolRanges
.filter((r) => r.ip_pool_id === pool.id)
Expand Down
9 changes: 7 additions & 2 deletions mock-api/msw/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,16 @@ export const errIfExists = <T extends Record<string, unknown>>(
Object.entries(match).every(([key, value]) => item[key] === value)
)
) {
const name = 'name' in match ? match.name : 'id' in match ? match.id : '<resource>'
const name =
'name' in match && match.name
? match.name
: 'id' in match && match.id
? match.id
: '<resource>'
throw json(
{
error_code: 'ObjectAlreadyExists',
message: `already exists: ${resourceLabel} "${name}"`,
message: `already exists: ${resourceLabel} "${name.toString()}"`,
},
{ status: 400 }
)
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"test": "vitest",
"e2e": "playwright test",
"e2ec": "playwright test --project=chrome",
"lint": "eslint --ext .js,.ts,.tsx,.json .",
"lint-fast": "eslint --cache --fix --ext .js,.ts,.tsx,.json .",
"lint": "eslint --ext .js,.ts,.tsx app test mock-api",
"lint-fast": "npm run lint -- --cache",
"fmt": "prettier --cache --write . && npm run lint -- --fix",
"openapi-gen-ts": "openapi-gen-ts",
"prettier": "prettier",
Expand Down Expand Up @@ -140,6 +140,6 @@
]
},
"lint-staged": {
"*.{js,ts,tsx,json}": "eslint --cache --fix"
"*.{js,ts,tsx}": "eslint --cache --fix"
}
}
4 changes: 2 additions & 2 deletions test/e2e/images.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ test('can copy an image ID to clipboard', async ({ page, browserName }) => {

await page.goto('/images')
await clickRowAction(page, 'ubuntu-22-04', 'Copy ID')
await expect(await clipboardText(page)).toEqual('ae46ddf5-a8d5-40fa-bcda-fcac606e3f9b')
expect(await clipboardText(page)).toEqual('ae46ddf5-a8d5-40fa-bcda-fcac606e3f9b')

await page.goto('/projects/mock-project/images')
await clickRowAction(page, 'image-4', 'Copy ID')
await expect(await clipboardText(page)).toEqual('d150b87d-eb20-49d2-8b56-ff5564670e8c')
expect(await clipboardText(page)).toEqual('d150b87d-eb20-49d2-8b56-ff5564670e8c')
})

test('can demote an image from silo', async ({ page }) => {
Expand Down
5 changes: 4 additions & 1 deletion test/e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import { MSW_USER_COOKIE } from '../../mock-api/msw/util'

export * from '@playwright/test'

export async function forEach(loc: Locator, fn: (loc0: Locator, i: number) => void) {
export async function forEach(
loc: Locator,
fn: (loc0: Locator, i: number) => Promise<void>
) {
const count = await loc.count()
for (let i = 0; i < count; i++) {
await fn(loc.nth(i), i)
Expand Down