From 8c48bf49ee481b90a5c0d340a1d5cca64598f01f Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Mon, 15 Jan 2024 17:45:42 +0000 Subject: [PATCH 01/19] Table improvements - Make name clickable - Add direction column - Improve action casing --- .../VpcPage/tabs/VpcFirewallRulesTab.tsx | 32 +++++++++++++++++-- libs/util/str.ts | 7 ++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx b/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx index 6682bbc99d..2defd01dd9 100644 --- a/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx +++ b/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx @@ -24,6 +24,7 @@ import { useReactTable, } from '@oxide/table' import { Button, EmptyMessage, TableEmptyBox } from '@oxide/ui' +import { titleCase } from '@oxide/util' import { CreateFirewallRuleForm } from 'app/forms/firewall-rules-create' import { EditFirewallRuleForm } from 'app/forms/firewall-rules-edit' @@ -32,10 +33,17 @@ import { confirmDelete } from 'app/stores/confirm-delete' const colHelper = createColumnHelper() +const directionMap = { + inbound: 'Incoming', + outbound: 'Outgoing', +} + /** columns that don't depend on anything in `render` */ const staticColumns = [ - colHelper.accessor('name', { header: 'Name' }), - colHelper.accessor('action', { header: 'Action' }), + colHelper.accessor('action', { + header: 'Action', + cell: (info) =>
{titleCase(info.getValue())}
, + }), colHelper.accessor('targets', { header: 'Targets', cell: (info) => , @@ -48,6 +56,14 @@ const staticColumns = [ header: 'Status', cell: (info) => , }), + colHelper.accessor('direction', { + header: 'Direction', + cell: (info) => ( +
+ {directionMap[info.getValue()] || info.getValue()} +
+ ), + }), colHelper.accessor('timeCreated', { id: 'created', header: 'Created', @@ -74,6 +90,18 @@ export const VpcFirewallRulesTab = () => { // the whole thing can't be static because the action depends on setEditing const columns = useMemo(() => { return [ + colHelper.accessor('name', { + header: 'Name', + cell: (info) => ( + <> + - +
+ + +
+ + +
- - - - Type - Name - - - - - {targets.value.map((t) => ( - - {/* TODO: should be the pretty type label, not the type key */} - {t.type} - {t.value} - - { - targets.onChange( - targets.value.filter( - (t1) => t1.value !== t.value || t1.type !== t.type - ) - ) - }} - /> - - - ))} - -
+ {!!targets.value.length && ( + + + Type + Name + {/* For remove button */} + + + + {targets.value.map((t, index) => ( + + + {t.type} + + {t.value} + + + + + ))} + + + )} @@ -279,128 +287,141 @@ export const CommonFields = ({ error, control }: CommonFieldsProps) => { required control={hostForm.control} /> - {/* For everything but IP this is a name, but for IP it's an IP. + +
+ {/* For everything but IP this is a name, but for IP it's an IP. So we should probably have the label on this field change when the host type changes. Also need to confirm that it's just an IP and not a block. */} - - -
- - + + +
+ + +
- - - - Type - Value - - - - - {hosts.value.map((h) => ( - - {/* TODO: should be the pretty type label, not the type key */} - {h.type} - {h.value} - - { - hosts.onChange( - hosts.value.filter((h1) => h1.value !== h.value && h1.type !== h.type) - ) - }} - /> - - - ))} - -
+ {!!hosts.value.length && ( + + + Type + Value + {/* For remove button */} + + + + {hosts.value.map((h, index) => ( + + + {h.type} + + {h.value} + + + + + ))} + + + )} - -
- - +
+ +
+ + +
- - - - Range - - - - - {ports.value.map((p) => ( - - {/* TODO: should be the pretty type label, not the type key */} - {p} - - { - ports.onChange(ports.value.filter((p1) => p1 !== p)) - }} - /> - - - ))} - -
+ + {!!ports.value.length && ( + + + Range + {/* For remove button */} + + + + {ports.value.map((p) => ( + + {p} + + + + + ))} + + + )} From c13cfec7e68efd953c2efc2fe47af36f4fe4819f Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 16 Jan 2024 09:18:02 +0000 Subject: [PATCH 03/19] Use same labels as API --- app/forms/firewall-rules-create.tsx | 4 ++-- .../networking/VpcPage/tabs/VpcFirewallRulesTab.tsx | 11 +---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/app/forms/firewall-rules-create.tsx b/app/forms/firewall-rules-create.tsx index 430e05266d..c017a39d36 100644 --- a/app/forms/firewall-rules-create.tsx +++ b/app/forms/firewall-rules-create.tsx @@ -173,8 +173,8 @@ export const CommonFields = ({ error, control }: CommonFieldsProps) => { column control={control} items={[ - { value: 'inbound', label: 'Incoming' }, - { value: 'outbound', label: 'Outgoing' }, + { value: 'inbound', label: 'Inbound' }, + { value: 'outbound', label: 'Outbound' }, ]} /> diff --git a/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx b/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx index 2defd01dd9..4849d6ce8c 100644 --- a/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx +++ b/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx @@ -33,11 +33,6 @@ import { confirmDelete } from 'app/stores/confirm-delete' const colHelper = createColumnHelper() -const directionMap = { - inbound: 'Incoming', - outbound: 'Outgoing', -} - /** columns that don't depend on anything in `render` */ const staticColumns = [ colHelper.accessor('action', { @@ -58,11 +53,7 @@ const staticColumns = [ }), colHelper.accessor('direction', { header: 'Direction', - cell: (info) => ( -
- {directionMap[info.getValue()] || info.getValue()} -
- ), + cell: (info) =>
{titleCase(info.getValue())}
, }), colHelper.accessor('timeCreated', { id: 'created', From 2984d45de75a0858f70e2f07b6fe9c4e41097433 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 16 Jan 2024 09:19:06 +0000 Subject: [PATCH 04/19] Test titleCase --- libs/util/str.spec.ts | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/libs/util/str.spec.ts b/libs/util/str.spec.ts index 4a4dd3d079..81059cb945 100644 --- a/libs/util/str.spec.ts +++ b/libs/util/str.spec.ts @@ -7,7 +7,7 @@ */ import { describe, expect, it } from 'vitest' -import { camelCase, capitalize, commaSeries, kebabCase } from './str' +import { camelCase, capitalize, commaSeries, kebabCase, titleCase } from './str' describe('capitalize', () => { it('capitalizes the first letter', () => { @@ -46,3 +46,38 @@ it('commaSeries', () => { expect(commaSeries(['a', 'b'], 'or')).toBe('a or b') expect(commaSeries(['a', 'b', 'c'], 'or')).toBe('a, b, or c') }) + +describe('titleCase', () => { + it('converts single words to title case', () => { + expect(titleCase('hello')).toBe('Hello') + }) + + it('converts multiple words to title case', () => { + expect(titleCase('hello world')).toBe('Hello World') + }) + + it('handles mixed case input correctly', () => { + expect(titleCase('hElLo WoRlD')).toBe('Hello World') + }) + + it('works correctly with strings containing punctuation', () => { + expect(titleCase('hello, world!')).toBe('Hello, World!') + }) + + // lol this title doesn't match the assert + it('retains existing capitalization of non-initial letters', () => { + expect(titleCase('hElLo wOrLd')).toBe('Hello World') + }) + + it('works correctly with empty strings', () => { + expect(titleCase('')).toBe('') + }) + + it('handles strings with only one character', () => { + expect(titleCase('a')).toBe('A') + }) + + it('doesn’t modify non-letter characters', () => { + expect(titleCase('123 abc')).toBe('123 Abc') + }) +}) From 6789fc8fc304454def228bc3f5aeb4b66146a7b6 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Tue, 16 Jan 2024 09:54:09 +0000 Subject: [PATCH 05/19] Checkout `CheckboxGroup` from `ssh-key-select` --- libs/ui/index.ts | 1 + .../checkbox-group/CheckboxGroup.stories.tsx | 40 ++++++++++++++ libs/ui/lib/checkbox-group/CheckboxGroup.tsx | 52 +++++++++++++++++++ libs/ui/lib/checkbox/Checkbox.tsx | 8 +-- 4 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 libs/ui/lib/checkbox-group/CheckboxGroup.stories.tsx create mode 100644 libs/ui/lib/checkbox-group/CheckboxGroup.tsx diff --git a/libs/ui/index.ts b/libs/ui/index.ts index 1cc0d384aa..e7906fbd6c 100644 --- a/libs/ui/index.ts +++ b/libs/ui/index.ts @@ -18,6 +18,7 @@ export * from './lib/avatar/Avatar' export * from './lib/badge/Badge' export * from './lib/button/Button' export * from './lib/checkbox/Checkbox' +export * from './lib/checkbox-group/CheckboxGroup' export * from './lib/copy-to-clipboard/CopyToClipboard' export * from './lib/date-picker/DateRangePicker' export * from './lib/divider/Divider' diff --git a/libs/ui/lib/checkbox-group/CheckboxGroup.stories.tsx b/libs/ui/lib/checkbox-group/CheckboxGroup.stories.tsx new file mode 100644 index 0000000000..bd9f46ed7f --- /dev/null +++ b/libs/ui/lib/checkbox-group/CheckboxGroup.stories.tsx @@ -0,0 +1,40 @@ +/* + * 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 + */ +import { Checkbox } from '../checkbox/Checkbox' +import { CheckboxGroup } from './CheckboxGroup' + +export const Default = () => ( + + Comments + Nothing + +) + +export const DefaultColumn = () => ( + + 50 GB + 100 GB + 200 GB + 300 GB + 400 GB + 500 GB + 600 GB + +) + +export const Disabled = () => ( + + 50 GB + 100 GB + 200 GB + 300 GB + 400 GB + 500 GB + 600 GB + +) diff --git a/libs/ui/lib/checkbox-group/CheckboxGroup.tsx b/libs/ui/lib/checkbox-group/CheckboxGroup.tsx new file mode 100644 index 0000000000..466d0bbd54 --- /dev/null +++ b/libs/ui/lib/checkbox-group/CheckboxGroup.tsx @@ -0,0 +1,52 @@ +/* + * 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 + */ +import cn from 'classnames' +import React from 'react' + +import { classed } from '@oxide/util' + +export const CheckboxGroupHint = classed.p`text-base text-secondary text-sans-sm max-w-3xl` + +export type CheckboxGroupProps = { + name: string + children: React.ReactElement | React.ReactElement[] + required?: boolean + disabled?: boolean + column?: boolean + className?: string + defaultChecked?: string[] + onChange?: (event: React.ChangeEvent) => void +} + +export const CheckboxGroup = ({ + name, + defaultChecked, + children, + required, + disabled, + column, + className, + onChange, + ...props +}: CheckboxGroupProps) => ( +
+ {React.Children.map(children, (checkbox) => + React.cloneElement(checkbox, { + name, + required, + disabled, + defaultChecked: defaultChecked?.includes(checkbox.props.value), + }) + )} +
+) diff --git a/libs/ui/lib/checkbox/Checkbox.tsx b/libs/ui/lib/checkbox/Checkbox.tsx index 50a633ee04..8e617a6093 100644 --- a/libs/ui/lib/checkbox/Checkbox.tsx +++ b/libs/ui/lib/checkbox/Checkbox.tsx @@ -11,7 +11,7 @@ import { Checkmark12Icon } from '@oxide/design-system/icons/react' import { classed } from '@oxide/util' const Check = () => ( - + ) const Indeterminate = classed.div`absolute w-2 h-0.5 left-1 top-[7px] bg-accent pointer-events-none` @@ -20,7 +20,7 @@ const inputStyle = ` appearance-none border border-default bg-default h-4 w-4 rounded-sm absolute left-0 outline-none disabled:cursor-not-allowed hover:border-hover hover:cursor-pointer - checked:bg-accent-secondary checked:border-accent-secondary checked:hover:border-accent + checked:bg-accent-secondary checked:border-accent-secondary checked:hover:border-accent [&:checked+svg]:block indeterminate:bg-accent-secondary indeterminate:border-accent hover:indeterminate:bg-accent-secondary-hover ` @@ -43,7 +43,7 @@ export const Checkbox = ({ className, ...inputProps }: CheckboxProps) => ( -
- {rules.length > 0 || isLoading ? : emptyState} + {rules.length > 0 ?
: emptyState} ) } From 16deaacfb1052dc46a6ef82f302c87e5f55f04db Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Mon, 29 Jan 2024 18:46:44 +0000 Subject: [PATCH 13/19] Blocking out routes (incomplete) --- app/forms/firewall-rules-edit.tsx | 46 +++++++++++++------ app/hooks/use-params.ts | 3 ++ .../project/networking/VpcPage/VpcPage.tsx | 26 ++++------- .../VpcPage/tabs/VpcFirewallRulesTab.tsx | 34 +++++++------- app/routes.tsx | 22 ++++++++- app/util/path-builder.ts | 2 + 6 files changed, 84 insertions(+), 49 deletions(-) diff --git a/app/forms/firewall-rules-edit.tsx b/app/forms/firewall-rules-edit.tsx index a523397433..94e8348ed4 100644 --- a/app/forms/firewall-rules-edit.tsx +++ b/app/forms/firewall-rules-edit.tsx @@ -5,15 +5,21 @@ * * Copyright Oxide Computer Company */ +import { useMemo } from 'react' +import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' + import { + apiQueryClient, firewallRuleGetToPut, useApiMutation, useApiQueryClient, - type VpcFirewallRule, + usePrefetchedApiQuery, } from '@oxide/api' +import { invariant } from '@oxide/util' import { SideModalForm } from 'app/components/form' -import { useForm, useVpcSelector } from 'app/hooks' +import { getVpcSelector, useForm, useVpcFirewallRuleSelector } from 'app/hooks' +import { pb } from 'app/util/path-builder' import { CommonFields, @@ -21,18 +27,32 @@ import { type FirewallRuleValues, } from './firewall-rules-create' -type EditFirewallRuleFormProps = { - onDismiss: () => void - existingRules: VpcFirewallRule[] - originalRule: VpcFirewallRule +EditFirewallRuleForm.loader = async ({ params }: LoaderFunctionArgs) => { + const { project, vpc } = getVpcSelector(params) + await apiQueryClient.prefetchQuery('vpcFirewallRulesView', { + query: { project, vpc }, + }) + return null } -export function EditFirewallRuleForm({ - onDismiss, - existingRules, - originalRule, -}: EditFirewallRuleFormProps) { - const vpcSelector = useVpcSelector() +export function EditFirewallRuleForm() { + const navigate = useNavigate() + + const { project, vpc, firewallRule } = useVpcFirewallRuleSelector() + const { data } = usePrefetchedApiQuery('vpcFirewallRulesView', { + query: { project, vpc }, + }) + + const [existingRules, originalRule] = useMemo(() => { + const rules = data?.rules || [] + + return [rules, rules.find((rule) => rule.name === firewallRule)] + }, [data, firewallRule]) + + invariant(originalRule, 'Firewall rule must exist') + + const onDismiss = () => navigate(pb.vpcFirewallRules({ project, vpc })) + const queryClient = useApiQueryClient() const updateRules = useApiMutation('vpcFirewallRulesUpdate', { @@ -76,7 +96,7 @@ export function EditFirewallRuleForm({ .filter((r) => r.name !== originalRule.name) .map(firewallRuleGetToPut) updateRules.mutate({ - query: vpcSelector, + query: { project, vpc }, body: { rules: [...otherRules, valuesToRuleUpdate(values)], }, diff --git a/app/hooks/use-params.ts b/app/hooks/use-params.ts index 21fb0da773..10e53bbd26 100644 --- a/app/hooks/use-params.ts +++ b/app/hooks/use-params.ts @@ -35,6 +35,7 @@ export const requireParams = export const getProjectSelector = requireParams('project') export const getInstanceSelector = requireParams('project', 'instance') export const getVpcSelector = requireParams('project', 'vpc') +export const getVpcFirewallRuleSelector = requireParams('project', 'vpc', 'firewallRule') export const getSiloSelector = requireParams('silo') export const getSiloImageSelector = requireParams('image') export const getIdpSelector = requireParams('silo', 'provider') @@ -74,6 +75,8 @@ export const useProjectSnapshotSelector = () => useSelectedParams(getProjectSnapshotSelector) export const useInstanceSelector = () => useSelectedParams(getInstanceSelector) export const useVpcSelector = () => useSelectedParams(getVpcSelector) +export const useVpcFirewallRuleSelector = () => + useSelectedParams(getVpcFirewallRuleSelector) export const useSiloSelector = () => useSelectedParams(getSiloSelector) export const useSiloImageSelector = () => useSelectedParams(getSiloImageSelector) export const useIdpSelector = () => useSelectedParams(getIdpSelector) diff --git a/app/pages/project/networking/VpcPage/VpcPage.tsx b/app/pages/project/networking/VpcPage/VpcPage.tsx index 1b0fefb6a7..b9c8af7145 100644 --- a/app/pages/project/networking/VpcPage/VpcPage.tsx +++ b/app/pages/project/networking/VpcPage/VpcPage.tsx @@ -8,14 +8,12 @@ import type { LoaderFunctionArgs } from 'react-router-dom' import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' -import { Networking24Icon, PageHeader, PageTitle, PropertiesTable, Tabs } from '@oxide/ui' +import { Networking24Icon, PageHeader, PageTitle, PropertiesTable } from '@oxide/ui' import { formatDateTime } from '@oxide/util' -import { QueryParamTabs } from 'app/components/QueryParamTabs' +import { RouteTabs, Tab } from 'app/components/RouteTabs' import { getVpcSelector, useVpcSelector } from 'app/hooks' - -import { VpcFirewallRulesTab } from './tabs/VpcFirewallRulesTab' -import { VpcSubnetsTab } from './tabs/VpcSubnetsTab' +import { pb } from 'app/util/path-builder' VpcPage.loader = async ({ params }: LoaderFunctionArgs) => { const { project, vpc } = getVpcSelector(params) @@ -58,18 +56,12 @@ export function VpcPage() { - - - Subnets - Firewall Rules - - - - - - - - + + Subnets + + Firewall Rules + + ) } diff --git a/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx b/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx index 67f925f532..efb96a9132 100644 --- a/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx +++ b/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx @@ -27,7 +27,6 @@ import { Button, EmptyMessage, TableEmptyBox } from '@oxide/ui' import { sortBy, titleCase } from '@oxide/util' import { CreateFirewallRuleForm } from 'app/forms/firewall-rules-create' -import { EditFirewallRuleForm } from 'app/forms/firewall-rules-edit' import { useVpcSelector } from 'app/hooks' import { confirmDelete } from 'app/stores/confirm-delete' @@ -76,7 +75,6 @@ export const VpcFirewallRulesTab = () => { const rules = useMemo(() => sortBy(data.rules, (r) => r.priority), [data]) const [createModalOpen, setCreateModalOpen] = useState(false) - const [editing, setEditing] = useState(null) const updateRules = useApiMutation('vpcFirewallRulesUpdate', { onSuccess() { @@ -87,21 +85,21 @@ export const VpcFirewallRulesTab = () => { // the whole thing can't be static because the action depends on setEditing const columns = useMemo(() => { return [ - colHelper.accessor('name', { - header: 'Name', - cell: (info) => ( - <> -
: emptyState} diff --git a/app/routes.tsx b/app/routes.tsx index afe77dfdac..d9b8379389 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -10,6 +10,7 @@ import { createRoutesFromElements, Navigate, Route } from 'react-router-dom' import { RouterDataErrorBoundary } from './components/ErrorBoundary' import { NotFound } from './components/ErrorPage' import { CreateDiskSideModalForm } from './forms/disk-create' +import { EditFirewallRuleForm } from './forms/firewall-rules-edit' import { CreateIdpSideModalForm } from './forms/idp/create' import { EditIdpSideModalForm } from './forms/idp/edit' import { @@ -55,6 +56,8 @@ import { ConnectTab } from './pages/project/instances/instance/tabs/ConnectTab' import { MetricsTab } from './pages/project/instances/instance/tabs/MetricsTab' import { NetworkingTab } from './pages/project/instances/instance/tabs/NetworkingTab' import { StorageTab } from './pages/project/instances/instance/tabs/StorageTab' +import { VpcFirewallRulesTab } from './pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab' +import { VpcSubnetsTab } from './pages/project/networking/VpcPage/tabs/VpcSubnetsTab' import ProjectsPage from './pages/ProjectsPage' import { ProfilePage } from './pages/settings/ProfilePage' import { SSHKeysPage } from './pages/settings/SSHKeysPage' @@ -306,7 +309,24 @@ export const routes = createRoutesFromElements( element={} loader={VpcPage.loader} handle={{ crumb: vpcCrumb }} - /> + > + } + handle={{ crumb: 'Subnets' }} + /> + } + handle={{ crumb: 'Subnets' }} + /> + } + loader={EditFirewallRuleForm.loader} + handle={{ crumb: 'Edit Firewall Rule' }} + /> + } loader={DisksPage.loader}> diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 2c88e678ce..b0ade01f1d 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -65,6 +65,8 @@ export const pb = { vpcs: (params: Project) => `${pb.project(params)}/vpcs`, vpc: (params: Vpc) => `${pb.vpcs(params)}/${params.vpc}`, vpcEdit: (params: Vpc) => `${pb.vpc(params)}/edit`, + vpcSubnets: (params: Vpc) => `${pb.vpc(params)}/subnets`, + vpcFirewallRules: (params: Vpc) => `${pb.vpc(params)}/firewall-rules`, siloUtilization: () => '/utilization', siloAccess: () => '/access', From 36ad38ff5cf7ba69825b69ec51b8714479a9817c Mon Sep 17 00:00:00 2001 From: David Crespo Date: Mon, 29 Jan 2024 13:26:35 -0600 Subject: [PATCH 14/19] Revert "Blocking out routes (incomplete)" This reverts commit 16deaacfb1052dc46a6ef82f302c87e5f55f04db. --- app/forms/firewall-rules-edit.tsx | 46 ++++++------------- app/hooks/use-params.ts | 3 -- .../project/networking/VpcPage/VpcPage.tsx | 26 +++++++---- .../VpcPage/tabs/VpcFirewallRulesTab.tsx | 34 +++++++------- app/routes.tsx | 22 +-------- app/util/path-builder.ts | 2 - 6 files changed, 49 insertions(+), 84 deletions(-) diff --git a/app/forms/firewall-rules-edit.tsx b/app/forms/firewall-rules-edit.tsx index 94e8348ed4..a523397433 100644 --- a/app/forms/firewall-rules-edit.tsx +++ b/app/forms/firewall-rules-edit.tsx @@ -5,21 +5,15 @@ * * Copyright Oxide Computer Company */ -import { useMemo } from 'react' -import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' - import { - apiQueryClient, firewallRuleGetToPut, useApiMutation, useApiQueryClient, - usePrefetchedApiQuery, + type VpcFirewallRule, } from '@oxide/api' -import { invariant } from '@oxide/util' import { SideModalForm } from 'app/components/form' -import { getVpcSelector, useForm, useVpcFirewallRuleSelector } from 'app/hooks' -import { pb } from 'app/util/path-builder' +import { useForm, useVpcSelector } from 'app/hooks' import { CommonFields, @@ -27,32 +21,18 @@ import { type FirewallRuleValues, } from './firewall-rules-create' -EditFirewallRuleForm.loader = async ({ params }: LoaderFunctionArgs) => { - const { project, vpc } = getVpcSelector(params) - await apiQueryClient.prefetchQuery('vpcFirewallRulesView', { - query: { project, vpc }, - }) - return null +type EditFirewallRuleFormProps = { + onDismiss: () => void + existingRules: VpcFirewallRule[] + originalRule: VpcFirewallRule } -export function EditFirewallRuleForm() { - const navigate = useNavigate() - - const { project, vpc, firewallRule } = useVpcFirewallRuleSelector() - const { data } = usePrefetchedApiQuery('vpcFirewallRulesView', { - query: { project, vpc }, - }) - - const [existingRules, originalRule] = useMemo(() => { - const rules = data?.rules || [] - - return [rules, rules.find((rule) => rule.name === firewallRule)] - }, [data, firewallRule]) - - invariant(originalRule, 'Firewall rule must exist') - - const onDismiss = () => navigate(pb.vpcFirewallRules({ project, vpc })) - +export function EditFirewallRuleForm({ + onDismiss, + existingRules, + originalRule, +}: EditFirewallRuleFormProps) { + const vpcSelector = useVpcSelector() const queryClient = useApiQueryClient() const updateRules = useApiMutation('vpcFirewallRulesUpdate', { @@ -96,7 +76,7 @@ export function EditFirewallRuleForm() { .filter((r) => r.name !== originalRule.name) .map(firewallRuleGetToPut) updateRules.mutate({ - query: { project, vpc }, + query: vpcSelector, body: { rules: [...otherRules, valuesToRuleUpdate(values)], }, diff --git a/app/hooks/use-params.ts b/app/hooks/use-params.ts index 10e53bbd26..21fb0da773 100644 --- a/app/hooks/use-params.ts +++ b/app/hooks/use-params.ts @@ -35,7 +35,6 @@ export const requireParams = export const getProjectSelector = requireParams('project') export const getInstanceSelector = requireParams('project', 'instance') export const getVpcSelector = requireParams('project', 'vpc') -export const getVpcFirewallRuleSelector = requireParams('project', 'vpc', 'firewallRule') export const getSiloSelector = requireParams('silo') export const getSiloImageSelector = requireParams('image') export const getIdpSelector = requireParams('silo', 'provider') @@ -75,8 +74,6 @@ export const useProjectSnapshotSelector = () => useSelectedParams(getProjectSnapshotSelector) export const useInstanceSelector = () => useSelectedParams(getInstanceSelector) export const useVpcSelector = () => useSelectedParams(getVpcSelector) -export const useVpcFirewallRuleSelector = () => - useSelectedParams(getVpcFirewallRuleSelector) export const useSiloSelector = () => useSelectedParams(getSiloSelector) export const useSiloImageSelector = () => useSelectedParams(getSiloImageSelector) export const useIdpSelector = () => useSelectedParams(getIdpSelector) diff --git a/app/pages/project/networking/VpcPage/VpcPage.tsx b/app/pages/project/networking/VpcPage/VpcPage.tsx index b9c8af7145..1b0fefb6a7 100644 --- a/app/pages/project/networking/VpcPage/VpcPage.tsx +++ b/app/pages/project/networking/VpcPage/VpcPage.tsx @@ -8,12 +8,14 @@ import type { LoaderFunctionArgs } from 'react-router-dom' import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' -import { Networking24Icon, PageHeader, PageTitle, PropertiesTable } from '@oxide/ui' +import { Networking24Icon, PageHeader, PageTitle, PropertiesTable, Tabs } from '@oxide/ui' import { formatDateTime } from '@oxide/util' -import { RouteTabs, Tab } from 'app/components/RouteTabs' +import { QueryParamTabs } from 'app/components/QueryParamTabs' import { getVpcSelector, useVpcSelector } from 'app/hooks' -import { pb } from 'app/util/path-builder' + +import { VpcFirewallRulesTab } from './tabs/VpcFirewallRulesTab' +import { VpcSubnetsTab } from './tabs/VpcSubnetsTab' VpcPage.loader = async ({ params }: LoaderFunctionArgs) => { const { project, vpc } = getVpcSelector(params) @@ -56,12 +58,18 @@ export function VpcPage() { - - Subnets - - Firewall Rules - - + + + Subnets + Firewall Rules + + + + + + + + ) } diff --git a/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx b/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx index efb96a9132..67f925f532 100644 --- a/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx +++ b/app/pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab.tsx @@ -27,6 +27,7 @@ import { Button, EmptyMessage, TableEmptyBox } from '@oxide/ui' import { sortBy, titleCase } from '@oxide/util' import { CreateFirewallRuleForm } from 'app/forms/firewall-rules-create' +import { EditFirewallRuleForm } from 'app/forms/firewall-rules-edit' import { useVpcSelector } from 'app/hooks' import { confirmDelete } from 'app/stores/confirm-delete' @@ -75,6 +76,7 @@ export const VpcFirewallRulesTab = () => { const rules = useMemo(() => sortBy(data.rules, (r) => r.priority), [data]) const [createModalOpen, setCreateModalOpen] = useState(false) + const [editing, setEditing] = useState(null) const updateRules = useApiMutation('vpcFirewallRulesUpdate', { onSuccess() { @@ -85,21 +87,21 @@ export const VpcFirewallRulesTab = () => { // the whole thing can't be static because the action depends on setEditing const columns = useMemo(() => { return [ - // colHelper.accessor('name', { - // header: 'Name', - // cell: (info) => ( - // <> - //
: emptyState} diff --git a/app/routes.tsx b/app/routes.tsx index d9b8379389..afe77dfdac 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -10,7 +10,6 @@ import { createRoutesFromElements, Navigate, Route } from 'react-router-dom' import { RouterDataErrorBoundary } from './components/ErrorBoundary' import { NotFound } from './components/ErrorPage' import { CreateDiskSideModalForm } from './forms/disk-create' -import { EditFirewallRuleForm } from './forms/firewall-rules-edit' import { CreateIdpSideModalForm } from './forms/idp/create' import { EditIdpSideModalForm } from './forms/idp/edit' import { @@ -56,8 +55,6 @@ import { ConnectTab } from './pages/project/instances/instance/tabs/ConnectTab' import { MetricsTab } from './pages/project/instances/instance/tabs/MetricsTab' import { NetworkingTab } from './pages/project/instances/instance/tabs/NetworkingTab' import { StorageTab } from './pages/project/instances/instance/tabs/StorageTab' -import { VpcFirewallRulesTab } from './pages/project/networking/VpcPage/tabs/VpcFirewallRulesTab' -import { VpcSubnetsTab } from './pages/project/networking/VpcPage/tabs/VpcSubnetsTab' import ProjectsPage from './pages/ProjectsPage' import { ProfilePage } from './pages/settings/ProfilePage' import { SSHKeysPage } from './pages/settings/SSHKeysPage' @@ -309,24 +306,7 @@ export const routes = createRoutesFromElements( element={} loader={VpcPage.loader} handle={{ crumb: vpcCrumb }} - > - } - handle={{ crumb: 'Subnets' }} - /> - } - handle={{ crumb: 'Subnets' }} - /> - } - loader={EditFirewallRuleForm.loader} - handle={{ crumb: 'Edit Firewall Rule' }} - /> - + /> } loader={DisksPage.loader}> diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index b0ade01f1d..2c88e678ce 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -65,8 +65,6 @@ export const pb = { vpcs: (params: Project) => `${pb.project(params)}/vpcs`, vpc: (params: Vpc) => `${pb.vpcs(params)}/${params.vpc}`, vpcEdit: (params: Vpc) => `${pb.vpc(params)}/edit`, - vpcSubnets: (params: Vpc) => `${pb.vpc(params)}/subnets`, - vpcFirewallRules: (params: Vpc) => `${pb.vpc(params)}/firewall-rules`, siloUtilization: () => '/utilization', siloAccess: () => '/access', From fa9c1e8600594e79a3756b8a1d0bf24a7e0faf6c Mon Sep 17 00:00:00 2001 From: David Crespo Date: Mon, 29 Jan 2024 13:28:13 -0600 Subject: [PATCH 15/19] remove redundant/incorrect test from chatgpt --- libs/util/str.spec.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/libs/util/str.spec.ts b/libs/util/str.spec.ts index 81059cb945..3a4a8a28e2 100644 --- a/libs/util/str.spec.ts +++ b/libs/util/str.spec.ts @@ -64,11 +64,6 @@ describe('titleCase', () => { expect(titleCase('hello, world!')).toBe('Hello, World!') }) - // lol this title doesn't match the assert - it('retains existing capitalization of non-initial letters', () => { - expect(titleCase('hElLo wOrLd')).toBe('Hello World') - }) - it('works correctly with empty strings', () => { expect(titleCase('')).toBe('') }) From fae42ca3df4868698c17d6490576e50125345e90 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Mon, 29 Jan 2024 13:29:11 -0600 Subject: [PATCH 16/19] undo checkbox changes --- libs/ui/lib/checkbox/Checkbox.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/ui/lib/checkbox/Checkbox.tsx b/libs/ui/lib/checkbox/Checkbox.tsx index 8e617a6093..50a633ee04 100644 --- a/libs/ui/lib/checkbox/Checkbox.tsx +++ b/libs/ui/lib/checkbox/Checkbox.tsx @@ -11,7 +11,7 @@ import { Checkmark12Icon } from '@oxide/design-system/icons/react' import { classed } from '@oxide/util' const Check = () => ( - + ) const Indeterminate = classed.div`absolute w-2 h-0.5 left-1 top-[7px] bg-accent pointer-events-none` @@ -20,7 +20,7 @@ const inputStyle = ` appearance-none border border-default bg-default h-4 w-4 rounded-sm absolute left-0 outline-none disabled:cursor-not-allowed hover:border-hover hover:cursor-pointer - checked:bg-accent-secondary checked:border-accent-secondary checked:hover:border-accent [&:checked+svg]:block + checked:bg-accent-secondary checked:border-accent-secondary checked:hover:border-accent indeterminate:bg-accent-secondary indeterminate:border-accent hover:indeterminate:bg-accent-secondary-hover ` @@ -43,7 +43,7 @@ export const Checkbox = ({ className, ...inputProps }: CheckboxProps) => ( -