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
1 change: 1 addition & 0 deletions app/api/path-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type NetworkInterface = Merge<Instance, { interface?: string }>
export type Snapshot = Merge<Project, { snapshot?: string }>
export type Vpc = Merge<Project, { vpc?: string }>
export type VpcSubnet = Merge<Vpc, { subnet?: string }>
export type FirewallRule = Merge<Vpc, { rule?: string }>
export type Silo = { silo?: string }
export type IdentityProvider = Merge<Silo, { provider: string }>
export type SystemUpdate = { version: string }
Expand Down
38 changes: 24 additions & 14 deletions app/forms/firewall-rules-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
*
* Copyright Oxide Computer Company
*/
import { useMemo } from 'react'
import { useController, type Control } from 'react-hook-form'
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
import * as R from 'remeda'

import {
apiQueryClient,
firewallRuleGetToPut,
parsePortRange,
useApiMutation,
useApiQueryClient,
usePrefetchedApiQuery,
type ApiError,
type VpcFirewallRule,
type VpcFirewallRuleHostFilter,
Expand All @@ -27,7 +32,8 @@ import { NumberField } from '~/components/form/fields/NumberField'
import { RadioField } from '~/components/form/fields/RadioField'
import { TextField, TextFieldInner } from '~/components/form/fields/TextField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { useForm, useVpcSelector } from '~/hooks'
import { getVpcSelector, useForm, useVpcSelector } from '~/hooks'
import { addToast } from '~/stores/toast'
import { Badge } from '~/ui/lib/Badge'
import { Button } from '~/ui/lib/Button'
import { FormDivider } from '~/ui/lib/Divider'
Expand All @@ -36,6 +42,7 @@ import * as MiniTable from '~/ui/lib/MiniTable'
import { TextInputHint } from '~/ui/lib/TextInput'
import { KEYS } from '~/ui/util/keys'
import { links } from '~/util/links'
import { pb } from '~/util/path-builder'

export type FirewallRuleValues = {
enabled: boolean
Expand Down Expand Up @@ -552,30 +559,33 @@ export const CommonFields = ({ error, control }: CommonFieldsProps) => {
)
}

// TODO: validate priority again
// export const validationSchema = Yup.object({
// priority: Yup.number().integer().min(0).max(65535).required('Required'),
// })

type CreateFirewallRuleFormProps = {
onDismiss: () => void
existingRules: VpcFirewallRule[]
CreateFirewallRuleForm.loader = async ({ params }: LoaderFunctionArgs) => {
await apiQueryClient.prefetchQuery('vpcFirewallRulesView', {
query: getVpcSelector(params),
})
return null
}

export function CreateFirewallRuleForm({
onDismiss,
existingRules,
}: CreateFirewallRuleFormProps) {
export function CreateFirewallRuleForm() {
const vpcSelector = useVpcSelector()
const queryClient = useApiQueryClient()

const navigate = useNavigate()
const onDismiss = () => navigate(pb.vpcFirewallRules(vpcSelector))

const updateRules = useApiMutation('vpcFirewallRulesUpdate', {
onSuccess() {
queryClient.invalidateQueries('vpcFirewallRulesView')
onDismiss()
addToast({ content: 'Your firewall rule has been created' })
navigate(pb.vpcFirewallRules(vpcSelector))
},
})

const { data } = usePrefetchedApiQuery('vpcFirewallRulesView', {
query: vpcSelector,
})
const existingRules = useMemo(() => R.sortBy(data.rules, (r) => r.priority), [data])

const form = useForm({ defaultValues })

return (
Expand Down
52 changes: 40 additions & 12 deletions app/forms/firewall-rules-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,63 @@
*
* Copyright Oxide Computer Company
*/
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'

import {
apiQueryClient,
firewallRuleGetToPut,
useApiMutation,
useApiQueryClient,
type VpcFirewallRule,
usePrefetchedApiQuery,
} from '@oxide/api'

import { trigger404 } from '~/components/ErrorBoundary'
import { SideModalForm } from '~/components/form/SideModalForm'
import { useForm, useVpcSelector } from '~/hooks'
import {
getFirewallRuleSelector,
useFirewallRuleSelector,
useForm,
useVpcSelector,
} from '~/hooks'
import { invariant } from '~/util/invariant'
import { pb } from '~/util/path-builder'

import {
CommonFields,
valuesToRuleUpdate,
type FirewallRuleValues,
} from './firewall-rules-create'

type EditFirewallRuleFormProps = {
onDismiss: () => void
existingRules: VpcFirewallRule[]
originalRule: VpcFirewallRule
EditFirewallRuleForm.loader = async ({ params }: LoaderFunctionArgs) => {
const { project, vpc, rule } = getFirewallRuleSelector(params)

const data = await apiQueryClient.fetchQuery('vpcFirewallRulesView', {
query: { project, vpc },
})

const originalRule = data.rules.find((r) => r.name === rule)
if (!originalRule) throw trigger404

return null
}

export function EditFirewallRuleForm({
onDismiss,
existingRules,
originalRule,
}: EditFirewallRuleFormProps) {
export function EditFirewallRuleForm() {
const { vpc, project, rule } = useFirewallRuleSelector()
const vpcSelector = useVpcSelector()
const queryClient = useApiQueryClient()

const { data } = usePrefetchedApiQuery('vpcFirewallRulesView', {
query: { project, vpc },
})

const originalRule = data.rules.find((r) => r.name === rule)

// we shouldn't hit this because of the trigger404 in the loader
invariant(originalRule, 'Firewall rule must exist')

const navigate = useNavigate()
const onDismiss = () => navigate(pb.vpcFirewallRules(vpcSelector))

const updateRules = useApiMutation('vpcFirewallRulesUpdate', {
onSuccess() {
queryClient.invalidateQueries('vpcFirewallRulesView')
Expand Down Expand Up @@ -72,9 +99,10 @@ export function EditFirewallRuleForm({
onSubmit={(values) => {
// note different filter logic from create: filter out the rule with the
// *original* name because we need to overwrite that rule
const otherRules = existingRules
const otherRules = data.rules
.filter((r) => r.name !== originalRule.name)
.map(firewallRuleGetToPut)

updateRules.mutate({
query: vpcSelector,
body: {
Expand Down
2 changes: 1 addition & 1 deletion app/forms/instance-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export function CreateInstanceForm() {
instance
)
addToast({ content: 'Your instance has been created' })
navigate(pb.instancePage({ project, instance: instance.name }))
navigate(pb.instance({ project, instance: instance.name }))
},
})

Expand Down
12 changes: 7 additions & 5 deletions app/forms/subnet-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
*
* Copyright Oxide Computer Company
*/
import { useNavigate } from 'react-router-dom'

import { useApiMutation, useApiQueryClient, type VpcSubnetCreate } from '@oxide/api'

import { DescriptionField } from '~/components/form/fields/DescriptionField'
Expand All @@ -13,21 +15,21 @@ import { TextField } from '~/components/form/fields/TextField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { useForm, useVpcSelector } from '~/hooks'
import { FormDivider } from '~/ui/lib/Divider'
import { pb } from '~/util/path-builder'

const defaultValues: VpcSubnetCreate = {
name: '',
description: '',
ipv4Block: '',
}

type CreateSubnetFormProps = {
onDismiss: () => void
}

export function CreateSubnetForm({ onDismiss }: CreateSubnetFormProps) {
export function CreateSubnetForm() {
const vpcSelector = useVpcSelector()
const queryClient = useApiQueryClient()

const navigate = useNavigate()
const onDismiss = () => navigate(pb.vpcSubnets(vpcSelector))

const createSubnet = useApiMutation('vpcSubnetCreate', {
onSuccess() {
queryClient.invalidateQueries('vpcSubnetList')
Expand Down
35 changes: 25 additions & 10 deletions app/forms/subnet-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,52 @@
*
* Copyright Oxide Computer Company
*/
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
import * as R from 'remeda'

import {
apiQueryClient,
useApiMutation,
useApiQueryClient,
type VpcSubnet,
usePrefetchedApiQuery,
type VpcSubnetUpdate,
} from '@oxide/api'

import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { NameField } from '~/components/form/fields/NameField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { useForm, useVpcSelector } from '~/hooks'
import { getVpcSubnetSelector, useForm, useVpcSubnetSelector } from '~/hooks'
import { pb } from '~/util/path-builder'

type EditSubnetFormProps = {
onDismiss: () => void
editing: VpcSubnet
EditSubnetForm.loader = async ({ params }: LoaderFunctionArgs) => {
const { project, vpc, subnet } = getVpcSubnetSelector(params)
await apiQueryClient.prefetchQuery('vpcSubnetView', {
query: { project, vpc },
path: { subnet },
})
return null
}

export function EditSubnetForm({ onDismiss, editing }: EditSubnetFormProps) {
const vpcSelector = useVpcSelector()
export function EditSubnetForm() {
const { project, vpc, subnet: subnetName } = useVpcSubnetSelector()
const queryClient = useApiQueryClient()

const navigate = useNavigate()
const onDismiss = () => navigate(pb.vpcSubnets({ project, vpc }))

const { data: subnet } = usePrefetchedApiQuery('vpcSubnetView', {
query: { project, vpc },
path: { subnet: subnetName },
})

const updateSubnet = useApiMutation('vpcSubnetUpdate', {
onSuccess() {
queryClient.invalidateQueries('vpcSubnetList')
onDismiss()
},
})

const defaultValues = R.pick(editing, ['name', 'description']) satisfies VpcSubnetUpdate
const defaultValues = R.pick(subnet, ['name', 'description']) satisfies VpcSubnetUpdate

const form = useForm({ defaultValues })

Expand All @@ -47,8 +62,8 @@ export function EditSubnetForm({ onDismiss, editing }: EditSubnetFormProps) {
onDismiss={onDismiss}
onSubmit={(body) => {
updateSubnet.mutate({
path: { subnet: editing.name },
query: vpcSelector,
path: { subnet: subnet.name },
query: { project, vpc },
body,
})
}}
Expand Down
4 changes: 4 additions & 0 deletions app/hooks/use-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export const getProjectSelector = requireParams('project')
export const getFloatingIpSelector = requireParams('project', 'floatingIp')
export const getInstanceSelector = requireParams('project', 'instance')
export const getVpcSelector = requireParams('project', 'vpc')
export const getFirewallRuleSelector = requireParams('project', 'vpc', 'rule')
export const getVpcSubnetSelector = requireParams('project', 'vpc', 'subnet')
export const getSiloSelector = requireParams('silo')
export const getSiloImageSelector = requireParams('image')
export const getIdpSelector = requireParams('silo', 'provider')
Expand Down Expand Up @@ -77,6 +79,8 @@ export const useProjectSnapshotSelector = () =>
useSelectedParams(getProjectSnapshotSelector)
export const useInstanceSelector = () => useSelectedParams(getInstanceSelector)
export const useVpcSelector = () => useSelectedParams(getVpcSelector)
export const useVpcSubnetSelector = () => useSelectedParams(getVpcSubnetSelector)
export const useFirewallRuleSelector = () => useSelectedParams(getFirewallRuleSelector)
export const useSiloSelector = () => useSelectedParams(getSiloSelector)
export const useSiloImageSelector = () => useSelectedParams(getSiloImageSelector)
export const useIdpSelector = () => useSelectedParams(getIdpSelector)
Expand Down
4 changes: 2 additions & 2 deletions app/pages/project/instances/InstancesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function InstancesPage() {
},
...(instances?.items || []).map((i) => ({
value: i.name,
onSelect: () => navigate(pb.instancePage({ project, instance: i.name })),
onSelect: () => navigate(pb.instance({ project, instance: i.name })),
navGroup: 'Go to instance',
})),
],
Expand All @@ -97,7 +97,7 @@ export function InstancesPage() {
const columns = useMemo(
() => [
colHelper.accessor('name', {
cell: makeLinkCell((instance) => pb.instancePage({ project, instance })),
cell: makeLinkCell((instance) => pb.instance({ project, instance })),
}),
colHelper.accessor((i) => ({ ncpus: i.ncpus, memory: i.memory }), {
header: 'CPU, RAM',
Expand Down
39 changes: 10 additions & 29 deletions app/pages/project/vpcs/VpcPage/VpcPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,27 @@ import type { LoaderFunctionArgs } from 'react-router-dom'
import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api'
import { Networking24Icon } from '@oxide/design-system/icons/react'

import { QueryParamTabs } from '~/components/QueryParamTabs'
import { RouteTabs, Tab } from '~/components/RouteTabs'
import { getVpcSelector, useVpcSelector } from '~/hooks'
import { EmptyCell } from '~/table/cells/EmptyCell'
import { PAGE_SIZE } from '~/table/QueryTable'
import { DateTime } from '~/ui/lib/DateTime'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { Tabs } from '~/ui/lib/Tabs'
import { pb } from '~/util/path-builder'

import { VpcDocsPopover } from '../VpcsPage'
import { VpcFirewallRulesTab } from './tabs/VpcFirewallRulesTab'
import { VpcSubnetsTab } from './tabs/VpcSubnetsTab'

VpcPage.loader = async ({ params }: LoaderFunctionArgs) => {
const { project, vpc } = getVpcSelector(params)
await Promise.all([
apiQueryClient.prefetchQuery('vpcView', { path: { vpc }, query: { project } }),
apiQueryClient.prefetchQuery('vpcFirewallRulesView', {
query: { project, vpc },
}),
apiQueryClient.prefetchQuery('vpcSubnetList', {
query: { project, vpc, limit: PAGE_SIZE },
}),
])
await apiQueryClient.prefetchQuery('vpcView', { path: { vpc }, query: { project } })
return null
}

export function VpcPage() {
const { project, vpc: vpcName } = useVpcSelector()
const vpcSelector = useVpcSelector()
const { data: vpc } = usePrefetchedApiQuery('vpcView', {
path: { vpc: vpcName },
query: { project },
path: { vpc: vpcSelector.vpc },
query: { project: vpcSelector.project },
})

return (
Expand All @@ -67,18 +56,10 @@ export function VpcPage() {
</PropertiesTable>
</PropertiesTable.Group>

<QueryParamTabs className="full-width" defaultValue="firewall-rules">
<Tabs.List>
<Tabs.Trigger value="firewall-rules">Firewall Rules</Tabs.Trigger>
<Tabs.Trigger value="subnets">Subnets</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="firewall-rules">
<VpcFirewallRulesTab />
</Tabs.Content>
<Tabs.Content value="subnets">
<VpcSubnetsTab />
</Tabs.Content>
</QueryParamTabs>
<RouteTabs fullWidth>
<Tab to={pb.vpcFirewallRules(vpcSelector)}>Firewall Rules</Tab>
<Tab to={pb.vpcSubnets(vpcSelector)}>Subnets</Tab>
</RouteTabs>
</>
)
}
Loading