Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Instance custom SSH key select #1867

Merged
merged 21 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8f7736c
First pass at custom SSH key UI
benjaminleonard Dec 20, 2023
57853f2
Merge branch 'main' into ssh-keys-select
zephraph Jan 24, 2024
045d454
Add generated API updates for ssh keys in instance create
zephraph Jan 24, 2024
48d8652
Add and use `CheckboxGroupField`
benjaminleonard Jan 24, 2024
acead2c
Add keys from instance form and select all checkbox
benjaminleonard Jan 25, 2024
8d7a7be
Add error for number of keys
benjaminleonard Jan 25, 2024
5362e6d
All keys selected by default
benjaminleonard Jan 25, 2024
a3bc5b5
Fix test
benjaminleonard Jan 25, 2024
f6e9a2c
ssh keys checkboxes mostly work with plain CheckboxField
david-crespo Jan 26, 2024
fa4bed7
too many keys error now displays as before
david-crespo Jan 26, 2024
3c557f0
don't set default list of keys to []
david-crespo Jan 26, 2024
a012b14
eliminate sshKeys: undefined possibility
david-crespo Jan 26, 2024
ae3f211
do validation through rules: validate
david-crespo Jan 26, 2024
6a94dcd
move static message out for readability
david-crespo Jan 26, 2024
631c4b4
put back the checkbox checked thing and remove extra mock ssh keys
david-crespo Jan 26, 2024
f915d89
e2e test for adding ssh key from instance create
david-crespo Jan 26, 2024
6cd64d7
Update `MAX_KEYS_PER_INSTANCE` to match new value
benjaminleonard Jan 29, 2024
c8f1e25
Update generated API
benjaminleonard Jan 31, 2024
62cc8b7
Merge branch 'main' into ssh-keys-select
benjaminleonard Jan 31, 2024
8534ba8
Add `instanceSshPublicKeyList` to handlers
benjaminleonard Jan 31, 2024
48e07ef
Update API after rename to `sshPublicKeys`
benjaminleonard Jan 31, 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
122 changes: 122 additions & 0 deletions app/components/form/fields/SshKeysField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* 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 { useState } from 'react'
import { useController, type Control } from 'react-hook-form'

import { usePrefetchedApiQuery } from '@oxide/api'
import {
Checkbox,
CheckboxGroup,
EmptyMessage,
FieldLabel,
Key16Icon,
Message,
TextInput,
TextInputHint,
} from '@oxide/ui'

import type { InstanceCreateInput } from 'app/forms/instance-create'

export function SshKeysField({ control }: { control: Control<InstanceCreateInput> }) {
const keys = usePrefetchedApiQuery('currentUserSshKeyList', {}).data?.items || []
const [newSshKey, setNewSshKey] = useState(false)

const {
field: { value, onChange },
} = useController({ control, name: 'publicKeys' })

Check failure on line 31 in app/components/form/fields/SshKeysField.tsx

View workflow job for this annotation

GitHub Actions / ci

Type '"publicKeys"' is not assignable to type '"name" | "image" | "start" | "description" | "hostname" | "memory" | "ncpus" | "disks" | "externalIps" | "networkInterfaces" | "userData" | "presetId" | "bootDiskName" | "bootDiskSize" | ... 21 more ... | `networkInterfaces.params.${number}.vpcName`'.

Check failure on line 31 in app/components/form/fields/SshKeysField.tsx

View workflow job for this annotation

GitHub Actions / ci

Type '"publicKeys"' is not assignable to type '"name" | "image" | "start" | "description" | "hostname" | "memory" | "ncpus" | "disks" | "externalIps" | "networkInterfaces" | "userData" | "presetId" | "bootDiskName" | "bootDiskSize" | ... 21 more ... | `networkInterfaces.params.${number}.vpcName`'.

return (
<div className="max-w-lg">
<div className="mb-2">
<FieldLabel id="ssh-keys-label">SSH keys</FieldLabel>
<TextInputHint id="ssh-keys-label-help-text">
SSH keys can be added and removed in your user settings
</TextInputHint>
</div>

{keys.length > 0 ? (
<Message
variant="notice"
content={
<>
If your image supports the cidata volume and{' '}
<a
target="_blank"
href="https://cloudinit.readthedocs.io/en/latest/"
rel="noreferrer"
>
cloud-init
</a>
, the keys above will be added to your instance. Keys are added when the
instance is created and are not updated after instance launch.
</>
}
/>
) : (
<div className="mb-4 flex max-w-lg items-center justify-center rounded-lg border p-6 border-default">
<EmptyMessage
icon={<Key16Icon />}
title="No SSH keys"
body="You need to add a SSH key to be able to see it here"
/>
</div>
)}
<CheckboxGroup name="ssh-keys" column className="mt-4">
{[
...keys.map((key) => (
<Checkbox
key={key.id}
value={key.id}
onChange={(e) => {
benjaminleonard marked this conversation as resolved.
Show resolved Hide resolved
const { checked } = e.target
let newValue = [...value]

Check failure on line 77 in app/components/form/fields/SshKeysField.tsx

View workflow job for this annotation

GitHub Actions / ci

Type 'string | number | boolean | { blockSize: BlockSize; type: "blank"; } | { snapshotId: string; type: "snapshot"; } | { imageId: string; type: "image"; } | { blockSize: BlockSize; type: "importing_blocks"; } | ... 11 more ... | undefined' must have a '[Symbol.iterator]()' method that returns an iterator.

Check failure on line 77 in app/components/form/fields/SshKeysField.tsx

View workflow job for this annotation

GitHub Actions / ci

Type 'string | number | boolean | { blockSize: BlockSize; type: "blank"; } | { snapshotId: string; type: "snapshot"; } | { imageId: string; type: "image"; } | { blockSize: BlockSize; type: "importing_blocks"; } | ... 11 more ... | undefined' must have a '[Symbol.iterator]()' method that returns an iterator.
if (checked) {
newValue.push({ type: 'user-key', key: key.id })
} else {
newValue = newValue.filter((k) => 'key' in k && k.key !== key.id)
}
onChange(newValue)
}}
checked={value.some((k) => 'key' in k && k.key === key.id)}

Check failure on line 85 in app/components/form/fields/SshKeysField.tsx

View workflow job for this annotation

GitHub Actions / ci

'value' is possibly 'undefined'.

Check failure on line 85 in app/components/form/fields/SshKeysField.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'some' does not exist on type 'string | number | boolean | { blockSize: BlockSize; type: "blank"; } | { snapshotId: string; type: "snapshot"; } | { imageId: string; type: "image"; } | { blockSize: BlockSize; type: "importing_blocks"; } | ... 10 more ... | DiskTableItem[]'.

Check failure on line 85 in app/components/form/fields/SshKeysField.tsx

View workflow job for this annotation

GitHub Actions / ci

Parameter 'k' implicitly has an 'any' type.

Check failure on line 85 in app/components/form/fields/SshKeysField.tsx

View workflow job for this annotation

GitHub Actions / ci

'value' is possibly 'undefined'.

Check failure on line 85 in app/components/form/fields/SshKeysField.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'some' does not exist on type 'string | number | boolean | { blockSize: BlockSize; type: "blank"; } | { snapshotId: string; type: "snapshot"; } | { imageId: string; type: "image"; } | { blockSize: BlockSize; type: "importing_blocks"; } | ... 10 more ... | DiskTableItem[]'.

Check failure on line 85 in app/components/form/fields/SshKeysField.tsx

View workflow job for this annotation

GitHub Actions / ci

Parameter 'k' implicitly has an 'any' type.
>
{key.name}
</Checkbox>
)),
<Checkbox
key="new"
value="New SSH Key"
checked={newSshKey}
onChange={(e) => {
setNewSshKey(e.target.checked)
}}
>
New SSH Key
<div className="mt-1 text-sans-sm text-tertiary">
One-off key saved to this instance and not added to your profile’s SSH keys
</div>
{newSshKey && (
<TextInput
as="textarea"
rows={5}
placeholder="Enter your SSH key"
className="mt-2"
onChange={(e) => {
const val = e.target.value
let newValue = [...value]
newValue = newValue.filter((k) => 'key' in k && k.type !== 'string')
newValue.push({ type: 'string', key: val })
onChange(newValue)
}}
/>
)}
</Checkbox>,
]}
</CheckboxGroup>
</div>
)
}
3 changes: 2 additions & 1 deletion app/components/form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ export * from './fields/DateTimeRangePicker'
export * from './fields/DescriptionField'
export * from './fields/DiskSizeField'
export * from './fields/DisksTableField'
export * from './fields/FileField'
export * from './fields/ImageSelectField'
export * from './fields/ListboxField'
export * from './fields/NameField'
export * from './fields/NetworkInterfaceField'
export * from './fields/RadioField'
export * from './fields/SshKeysField'
export * from './fields/SubnetListbox'
export * from './fields/TextField'
export * from './fields/TlsCertsField'
export * from './fields/FileField'
76 changes: 6 additions & 70 deletions app/forms/instance-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,15 @@
} from '@oxide/api'
import {
EmptyMessage,
FieldLabel,
FormDivider,
Images16Icon,
Instances24Icon,
Key16Icon,
Message,
RadioCard,
Table,
Tabs,
TextInputHint,
Truncate,
} from '@oxide/ui'
import { formatDateTime, GiB, invariant } from '@oxide/util'
import { GiB, invariant } from '@oxide/util'

import {
CheckboxField,
Expand All @@ -46,6 +42,7 @@
NameField,
NetworkInterfaceField,
RadioFieldDyn,
SshKeysField,
TextField,
type DiskTableItem,
} from 'app/components/form'
Expand Down Expand Up @@ -83,6 +80,8 @@
disks: [],
networkInterfaces: { type: 'default' },

publicKeys: [],

Check failure on line 83 in app/forms/instance-create.tsx

View workflow job for this annotation

GitHub Actions / ci

Type '{ name: string; description: string; presetId: "general-xs"; memory: number; ncpus: number; hostname: string; bootDiskName: string; bootDiskSize: number; image: string; disks: never[]; networkInterfaces: { ...; }; publicKeys: never[]; start: true; }' is not assignable to type 'InstanceCreateInput'.

Check failure on line 83 in app/forms/instance-create.tsx

View workflow job for this annotation

GitHub Actions / ci

Type '{ name: string; description: string; presetId: "general-xs"; memory: number; ncpus: number; hostname: string; bootDiskName: string; bootDiskSize: number; image: string; disks: never[]; networkInterfaces: { ...; }; publicKeys: never[]; start: true; }' is not assignable to type 'InstanceCreateInput'.

start: true,
}

Expand Down Expand Up @@ -188,6 +187,7 @@
externalIps: [{ type: 'ephemeral' }],
start: values.start,
networkInterfaces: values.networkInterfaces,
publicKeys: values.publicKeys,

Check failure on line 190 in app/forms/instance-create.tsx

View workflow job for this annotation

GitHub Actions / ci

Type '{ name: string; hostname: string; description: string; memory: number; ncpus: number; disks: DiskTableItem[]; externalIps: { type: "ephemeral"; }[]; start: boolean | undefined; networkInterfaces: InstanceNetworkInterfaceAttachment; publicKeys: any; }' is not assignable to type 'InstanceCreate'.

Check failure on line 190 in app/forms/instance-create.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'publicKeys' does not exist on type 'InstanceCreateInput'.

Check failure on line 190 in app/forms/instance-create.tsx

View workflow job for this annotation

GitHub Actions / ci

Type '{ name: string; hostname: string; description: string; memory: number; ncpus: number; disks: DiskTableItem[]; externalIps: { type: "ephemeral"; }[]; start: boolean | undefined; networkInterfaces: InstanceNetworkInterfaceAttachment; publicKeys: any; }' is not assignable to type 'InstanceCreate'.

Check failure on line 190 in app/forms/instance-create.tsx

View workflow job for this annotation

GitHub Actions / ci

Property 'publicKeys' does not exist on type 'InstanceCreateInput'.
},
})
}}
Expand Down Expand Up @@ -365,7 +365,7 @@
<FormDivider />
<Form.Heading id="authentication">Authentication</Form.Heading>

<SshKeysTable />
<SshKeysField control={control} />

<FormDivider />
<Form.Heading id="networking">Networking</Form.Heading>
Expand All @@ -386,70 +386,6 @@
)
}

const SshKeysTable = () => {
const keys = usePrefetchedApiQuery('currentUserSshKeyList', {}).data?.items || []

return (
<div className="max-w-lg">
<div className="mb-2">
<FieldLabel id="ssh-keys-label">SSH keys</FieldLabel>
<TextInputHint id="ssh-keys-label-help-text">
SSH keys can be added and removed in your user settings
</TextInputHint>
</div>

{keys.length > 0 ? (
<Table className="w-full">
<Table.Header>
<Table.HeaderRow>
<Table.HeadCell>Name</Table.HeadCell>
<Table.HeadCell>Created</Table.HeadCell>
</Table.HeaderRow>
</Table.Header>
<Table.Body>
{keys.map((key) => (
<Table.Row key={key.id}>
<Table.Cell height="auto">
<Truncate text={key.name} maxLength={28} />
</Table.Cell>
<Table.Cell height="auto" className="text-secondary">
{formatDateTime(key.timeCreated)}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
) : (
<div className="mb-4 flex max-w-lg items-center justify-center rounded-lg border p-6 border-default">
<EmptyMessage
icon={<Key16Icon />}
title="No SSH keys"
body="You need to add a SSH key to be able to see it here"
/>
</div>
)}

<Message
variant="notice"
content={
<>
If your image supports the cidata volume and{' '}
<a
target="_blank"
href="https://cloudinit.readthedocs.io/en/latest/"
rel="noreferrer"
>
cloud-init
</a>
, the keys above will be added to your instance. Keys are added when the
instance is created and are not updated after instance launch.
</>
}
/>
</div>
)
}

const renderLargeRadioCards = (category: string) => {
return PRESETS.filter((option) => option.category === category).map((option) => (
<RadioCard key={option.id} value={option.id}>
Expand Down
11 changes: 10 additions & 1 deletion libs/api-mocks/sshKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,16 @@ export const sshKeys: Json<SshKey>[] = [
description: 'For use on personal projects',
time_created: new Date().toISOString(),
time_modified: new Date().toISOString(),
public_key: 'aslsddlfkjsdlfkjsdlfkjsdflkjsdlfkjsdlfkjsdlfkjsd',
public_key: 'aslsddlfkjsdlfkjsdlfkjsdlfkjsdflkjsdlfkjsdlfkjsd',
silo_user_id: user1.id,
},
{
id: 'b2c3d4e5-6f7g-8h9i-0j1k-2l3m4n5o6p7q',
name: 'mac-mini',
description: '',
time_created: new Date().toISOString(),
time_modified: new Date().toISOString(),
public_key: 'aslsddlfkjsdlfkjsdlfkjsdlfkjsdflkjsdlfkjsdlfkjsd',
silo_user_id: user1.id,
},
]
1 change: 1 addition & 0 deletions libs/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
40 changes: 40 additions & 0 deletions libs/ui/lib/checkbox-group/CheckboxGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => (
<CheckboxGroup name="group1">
<Checkbox value="notify">Comments</Checkbox>
<Checkbox value="do-not-notify">Nothing</Checkbox>
</CheckboxGroup>
)

export const DefaultColumn = () => (
<CheckboxGroup name="group2" column>
<Checkbox value="50">50 GB</Checkbox>
<Checkbox value="100">100 GB</Checkbox>
<Checkbox value="200">200 GB</Checkbox>
<Checkbox value="300">300 GB</Checkbox>
<Checkbox value="400">400 GB</Checkbox>
<Checkbox value="500">500 GB</Checkbox>
<Checkbox value="600">600 GB</Checkbox>
</CheckboxGroup>
)

export const Disabled = () => (
<CheckboxGroup name="group4" disabled>
<Checkbox value="50">50 GB</Checkbox>
<Checkbox value="100">100 GB</Checkbox>
<Checkbox value="200">200 GB</Checkbox>
<Checkbox value="300">300 GB</Checkbox>
<Checkbox value="400">400 GB</Checkbox>
<Checkbox value="500">500 GB</Checkbox>
<Checkbox value="600">600 GB</Checkbox>
</CheckboxGroup>
)
52 changes: 52 additions & 0 deletions libs/ui/lib/checkbox-group/CheckboxGroup.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>) => void
}

export const CheckboxGroup = ({
name,
defaultChecked,
children,
required,
disabled,
column,
className,
onChange,
...props
}: CheckboxGroupProps) => (
<div
className={cn('flex', column ? 'flex-col space-y-2' : 'flex-wrap gap-5', className)}
role="group"
onChange={onChange}
{...props}
>
{React.Children.map(children, (checkbox) =>
React.cloneElement(checkbox, {
name,
required,
disabled,
defaultChecked: defaultChecked?.includes(checkbox.props.value),
})
)}
</div>
)
Loading
Loading