Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
0e8f468
Add Floating IP mock API calls
charliepark Feb 5, 2024
458187d
Working on networking / routes for pages and tabs
charliepark Feb 5, 2024
6bd6c14
Adjusting from VPC base to Floating IP implementation
charliepark Feb 6, 2024
187e04b
Adjust vertical spacing on tabbed pages with table action buttons
charliepark Feb 6, 2024
f12e50c
Add Floating IP to mock API calls
charliepark Feb 6, 2024
6722664
Update columns
charliepark Feb 6, 2024
a5ea03a
Add form for Floating IP create
charliepark Feb 7, 2024
bdad833
Update actions list
charliepark Feb 7, 2024
717b967
Get routing on floatingIpView working, though will remove it soon
charliepark Feb 7, 2024
86b7fcb
Get attached instance name into Floating IPs table
charliepark Feb 8, 2024
4a4d813
Get pools loading in Floating IP create form
charliepark Feb 8, 2024
6b00cd2
Add IP Address field validation
charliepark Feb 9, 2024
2342a61
Update formatting on dropdown
charliepark Feb 9, 2024
442f05e
Merge branch 'main' into floating-ips
charliepark Feb 9, 2024
bb587e3
Refactor validation
charliepark Feb 9, 2024
b1c45ab
Pass address to ip
charliepark Feb 9, 2024
d37b9a7
Default to vpcs page
charliepark Feb 9, 2024
401fa4b
Proper logic for disabling detach/delete actions
charliepark Feb 10, 2024
591f10b
Enable deleting of Floating IPs
charliepark Feb 10, 2024
61262c6
FloatingIp sidebar loading, but not saving yet
charliepark Feb 12, 2024
ec0e9fc
Merge branch 'main' into floating-ips
charliepark Feb 12, 2024
22b5927
Refactor
charliepark Feb 12, 2024
0887393
working form in side modal to attach to instances
charliepark Feb 12, 2024
7bf2bdc
Cleaning up Floating IPs page
charliepark Feb 13, 2024
3036579
Merge branch 'main' into floating-ips
charliepark Feb 14, 2024
89e5125
Detaching via modal works
charliepark Feb 14, 2024
0662b94
Refactor
charliepark Feb 14, 2024
6cd3883
Attach Floating IP to Instance working
charliepark Feb 14, 2024
421a144
Spoof ip
charliepark Feb 14, 2024
70c80f5
Resolve TS issues
charliepark Feb 14, 2024
7f97a4c
Merge main
charliepark Feb 14, 2024
0ebb041
Remove js files accidentally included in branch
charliepark Feb 15, 2024
0504fb3
Refactor deletion flow
charliepark Feb 15, 2024
999c3ac
Refactor form
charliepark Feb 15, 2024
e6a5acd
Merge branch 'main' into floating-ips
charliepark Feb 15, 2024
b57a539
Fix import to align with new rule
charliepark Feb 15, 2024
2dc8bd7
Moving Floating IPs to project resource level
charliepark Feb 15, 2024
87a3a0d
Clean up unneeded files
charliepark Feb 15, 2024
a76767b
Refactor - Networking page not needed at all
charliepark Feb 15, 2024
7e1d683
Clean up redirect reference
charliepark Feb 15, 2024
f70c63f
Consistency on route naming
charliepark Feb 15, 2024
7a8a31f
One more consistency fix
charliepark Feb 15, 2024
052ec9c
Make the linter happy
charliepark Feb 16, 2024
b7bdc0e
Update routes so Floating IP page doesn't disappear when using create…
charliepark Feb 16, 2024
a47d556
Fix e2e test
charliepark Feb 16, 2024
b744d63
Update libs/api-mocks/msw/handlers.ts
charliepark Feb 16, 2024
433d6a1
Add InstanceLinkCell component
charliepark Feb 16, 2024
af2741d
Move detach flow to action menu
charliepark Feb 16, 2024
08c654d
Refactor loader to get instance list loading appropriately
charliepark Feb 16, 2024
5756309
Remove unneeded file
charliepark Feb 16, 2024
0df20f1
Refactor props for floating ip create form
charliepark Feb 16, 2024
c7af4dc
Align path for floatingIpNew with standard syntax
charliepark Feb 16, 2024
b700959
Use floatingIp lookup
charliepark Feb 16, 2024
10bce52
Make sure an already-attached floating IP can't be attached again
charliepark Feb 16, 2024
77246eb
Update type on Floating IP Create - address value, and prevent spaces…
charliepark Feb 16, 2024
c9134af
Refactor forms and types
charliepark Feb 16, 2024
c148a98
Refactor
charliepark Feb 16, 2024
dae7785
Accordion to hide advanced settings; refactorable
charliepark Feb 20, 2024
704eade
add e2e test for creating a floating ip; attach / detach still in pro…
charliepark Feb 21, 2024
3773c54
Attacah/Detach test passes
charliepark Feb 21, 2024
12c2441
Extract simplified AccordionItem component
charliepark Feb 21, 2024
d508956
Pull advanced props over to AccordionItem so we can reuse it wherever…
charliepark Feb 21, 2024
8b0b87e
Updated test with helper utils
charliepark Feb 21, 2024
7868586
Revert Playwright utils
charliepark Feb 22, 2024
3195eb1
Last util cleanup
charliepark Feb 22, 2024
0f9dd2f
Fix Playwright test
charliepark Feb 22, 2024
1f3bf52
Add info box to attach modal; update action menu to have either attac…
charliepark Feb 22, 2024
2a17795
No period for single-line Message
charliepark Feb 22, 2024
353ee84
Refactor useState flow to more intelligently use null
charliepark Feb 23, 2024
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
46 changes: 46 additions & 0 deletions app/components/AccordionItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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 * as Accordion from '@radix-ui/react-accordion'
import cn from 'classnames'
import { useEffect, useRef } from 'react'

import { DirectionRightIcon } from '@oxide/design-system/icons/react'

type AccordionItemProps = {
children: React.ReactNode
isOpen: boolean
label: string
value: string
}

export const AccordionItem = ({ children, isOpen, label, value }: AccordionItemProps) => {
const contentRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (isOpen && contentRef.current) {
contentRef.current.scrollIntoView({ behavior: 'smooth' })
}
}, [isOpen])

return (
<Accordion.Item value={value}>
<Accordion.Header className="max-w-lg">
<Accordion.Trigger className="group flex w-full items-center justify-between border-t pt-2 text-sans-xl border-secondary [&>svg]:data-[state=open]:rotate-90">
<div className="text-secondary">{label}</div>
<DirectionRightIcon className="transition-all text-secondary" />
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content
ref={contentRef}
forceMount
className={cn('ox-accordion-content overflow-hidden py-8', { hidden: !isOpen })}
>
{children}
</Accordion.Content>
</Accordion.Item>
)
}
147 changes: 147 additions & 0 deletions app/forms/floating-ip-create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* 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 * as Accordion from '@radix-ui/react-accordion'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import type { SetRequired } from 'type-fest'

import {
apiQueryClient,
useApiMutation,
useApiQueryClient,
usePrefetchedApiQuery,
type FloatingIpCreate,
type SiloIpPool,
} from '@oxide/api'
import { Badge, Message } from '@oxide/ui'
import { validateIp } from '@oxide/util'

import { AccordionItem } from 'app/components/AccordionItem'
import {
DescriptionField,
ListboxField,
NameField,
SideModalForm,
TextField,
} from 'app/components/form'
import { useForm, useProjectSelector, useToast } from 'app/hooks'
import { pb } from 'app/util/path-builder'

CreateFloatingIpSideModalForm.loader = async () => {
await apiQueryClient.prefetchQuery('projectIpPoolList', {
query: { limit: 1000 },
})
return null
}

const toListboxItem = (p: SiloIpPool) => {
if (!p.isDefault) {
return { value: p.name, label: p.name }
}
// For the default pool, add a label to the dropdown
return {
value: p.name,
labelString: p.name,
label: (
<>
{p.name}{' '}
<Badge className="ml-1" color="neutral">
default
</Badge>
</>
),
}
}

const defaultValues: SetRequired<FloatingIpCreate, 'address'> = {
name: '',
description: '',
pool: undefined,
address: '',
}

export function CreateFloatingIpSideModalForm() {
// Fetch 1000 to we can be sure to get them all.
const { data: allPools } = usePrefetchedApiQuery('projectIpPoolList', {
query: { limit: 1000 },
})

const queryClient = useApiQueryClient()
const projectSelector = useProjectSelector()
const addToast = useToast()
const navigate = useNavigate()

const createFloatingIp = useApiMutation('floatingIpCreate', {
onSuccess() {
queryClient.invalidateQueries('floatingIpList')
addToast({ content: 'Your Floating IP has been created' })
navigate(pb.floatingIps(projectSelector))
},
})

const form = useForm({ defaultValues })
const isPoolSelected = !!form.watch('pool')

const [openItems, setOpenItems] = useState<string[]>([])

return (
<SideModalForm
id="create-floating-ip-form"
title="Create Floating IP"
form={form}
onDismiss={() => navigate(pb.floatingIps(projectSelector))}
onSubmit={({ address, ...rest }) => {
createFloatingIp.mutate({
query: projectSelector,
// if address is '', evaluate as false and send as undefined
body: { address: address || undefined, ...rest },
})
}}
loading={createFloatingIp.isPending}
submitError={createFloatingIp.error}
>
<NameField name="name" control={form.control} />
<DescriptionField name="description" control={form.control} />

<Accordion.Root
type="multiple"
className="mt-12 max-w-lg"
value={openItems}
onValueChange={setOpenItems}
>
<AccordionItem
isOpen={openItems.includes('advanced')}
label="Advanced"
value="advanced"
>
<Message
variant="info"
content="If you don’t specify a pool, the default will be used"
/>

<ListboxField
name="pool"
items={allPools.items.map((p) => toListboxItem(p))}
label="IP pool"
control={form.control}
placeholder="Select pool"
/>
<TextField
name="address"
control={form.control}
disabled={!isPoolSelected}
transform={(v) => v.replace(/\s/g, '')}
validate={(ip) =>
ip && !validateIp(ip).valid ? 'Not a valid IP address' : true
}
/>
</AccordionItem>
</Accordion.Root>
</SideModalForm>
)
}
40 changes: 2 additions & 38 deletions app/forms/instance-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
* Copyright Oxide Computer Company
*/
import * as Accordion from '@radix-ui/react-accordion'
import cn from 'classnames'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useWatch, type Control } from 'react-hook-form'
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
import type { SetRequired } from 'type-fest'
Expand All @@ -23,7 +22,6 @@ import {
type InstanceCreate,
} from '@oxide/api'
import {
DirectionRightIcon,
EmptyMessage,
FormDivider,
Images16Icon,
Expand All @@ -35,6 +33,7 @@ import {
} from '@oxide/ui'
import { GiB, invariant } from '@oxide/util'

import { AccordionItem } from 'app/components/AccordionItem'
import {
CheckboxField,
DescriptionField,
Expand Down Expand Up @@ -487,41 +486,6 @@ const AdvancedAccordion = ({
)
}

type AccordionItemProps = {
value: string
isOpen: boolean
label: string
children: React.ReactNode
}

function AccordionItem({ value, label, children, isOpen }: AccordionItemProps) {
const contentRef = useRef<HTMLDivElement>(null)

useEffect(() => {
if (isOpen && contentRef.current) {
contentRef.current.scrollIntoView({ behavior: 'smooth' })
}
}, [isOpen])

return (
<Accordion.Item value={value}>
<Accordion.Header className="max-w-lg">
<Accordion.Trigger className="group flex w-full items-center justify-between border-t py-2 text-sans-xl border-secondary [&>svg]:data-[state=open]:rotate-90">
<div className="text-secondary">{label}</div>
<DirectionRightIcon className="transition-all text-secondary" />
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content
ref={contentRef}
forceMount
className={cn('ox-accordion-content overflow-hidden py-8', { hidden: !isOpen })}
>
{children}
</Accordion.Content>
</Accordion.Item>
)
}

const renderLargeRadioCards = (category: string) => {
return PRESETS.filter((option) => option.category === category).map((option) => (
<RadioCard key={option.id} value={option.id}>
Expand Down
2 changes: 2 additions & 0 deletions app/hooks/use-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const requireParams =
}

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 getSiloSelector = requireParams('silo')
Expand Down Expand Up @@ -69,6 +70,7 @@ function useSelectedParams<T>(getSelector: (params: AllParams) => T) {
// params are present. Only the specified keys end up in the result object, but
// we do not error if there are other params present in the query string.

export const useFloatingIpSelector = () => useSelectedParams(getFloatingIpSelector)
export const useProjectSelector = () => useSelectedParams(getProjectSelector)
export const useProjectImageSelector = () => useSelectedParams(getProjectImageSelector)
export const useProjectSnapshotSelector = () =>
Expand Down
9 changes: 7 additions & 2 deletions app/layouts/ProjectLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
Folder16Icon,
Images16Icon,
Instances16Icon,
IpGlobal16Icon,
Networking16Icon,
Snapshots16Icon,
Storage16Icon,
Expand Down Expand Up @@ -67,7 +68,8 @@ function ProjectLayout({ overrideContentPane }: ProjectLayoutProps) {
{ value: 'Disks', path: pb.disks(projectSelector) },
{ value: 'Snapshots', path: pb.snapshots(projectSelector) },
{ value: 'Images', path: pb.projectImages(projectSelector) },
{ value: 'Networking', path: pb.vpcs(projectSelector) },
{ value: 'VPCs', path: pb.vpcs(projectSelector) },
{ value: 'Floating IPs', path: pb.floatingIps(projectSelector) },
{ value: 'Access & IAM', path: pb.projectAccess(projectSelector) },
]
// filter out the entry for the path we're currently on
Expand Down Expand Up @@ -111,7 +113,10 @@ function ProjectLayout({ overrideContentPane }: ProjectLayoutProps) {
<Images16Icon title="images" /> Images
</NavLinkItem>
<NavLinkItem to={pb.vpcs(projectSelector)}>
<Networking16Icon /> Networking
<Networking16Icon /> VPCs
</NavLinkItem>
<NavLinkItem to={pb.floatingIps(projectSelector)}>
<IpGlobal16Icon /> Floating IPs
</NavLinkItem>
<NavLinkItem to={pb.projectAccess(projectSelector)}>
<Access16Icon title="Access & IAM" /> Access &amp; IAM
Expand Down
24 changes: 2 additions & 22 deletions app/pages/project/disks/DisksPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ import {
diskCan,
genName,
useApiMutation,
useApiQuery,
useApiQueryClient,
type Disk,
} from '@oxide/api'
import {
DateCell,
LinkCell,
InstanceLinkCell,
SizeCell,
SkeletonCell,
useQueryTable,
type MenuAction,
} from '@oxide/table'
Expand All @@ -40,24 +38,6 @@ import { pb } from 'app/util/path-builder'

import { fancifyStates } from '../instances/instance/tabs/common'

function InstanceNameFromId({ value: instanceId }: { value: string | null }) {
const { project } = useProjectSelector()
const { data: instance } = useApiQuery(
'instanceView',
{ path: { instance: instanceId! } },
{ enabled: !!instanceId }
)

if (!instanceId) return null
if (!instance) return <SkeletonCell />

return (
<LinkCell to={pb.instancePage({ project, instance: instance.name })}>
{instance.name}
</LinkCell>
)
}

const EmptyState = () => (
<EmptyMessage
icon={<Storage24Icon />}
Expand Down Expand Up @@ -157,7 +137,7 @@ export function DisksPage() {
// whether it has an instance field
'instance' in disk.state ? disk.state.instance : null
}
cell={InstanceNameFromId}
cell={InstanceLinkCell}
/>
<Column header="Size" accessor="size" cell={SizeCell} />
<Column
Expand Down
Loading