From 8f7736c612a7224f45f7502f0339241893fc0e34 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Wed, 20 Dec 2023 14:26:17 +0000 Subject: [PATCH 01/19] First pass at custom SSH key UI --- app/components/form/fields/SshKeysField.tsx | 122 ++++++++++++++++++ app/components/form/index.ts | 3 +- app/forms/instance-create.tsx | 76 +---------- libs/api-mocks/sshKeys.ts | 11 +- 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 +- 8 files changed, 237 insertions(+), 76 deletions(-) create mode 100644 app/components/form/fields/SshKeysField.tsx create mode 100644 libs/ui/lib/checkbox-group/CheckboxGroup.stories.tsx create mode 100644 libs/ui/lib/checkbox-group/CheckboxGroup.tsx diff --git a/app/components/form/fields/SshKeysField.tsx b/app/components/form/fields/SshKeysField.tsx new file mode 100644 index 0000000000..b2abd7ce0c --- /dev/null +++ b/app/components/form/fields/SshKeysField.tsx @@ -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 }) { + const keys = usePrefetchedApiQuery('currentUserSshKeyList', {}).data?.items || [] + const [newSshKey, setNewSshKey] = useState(false) + + const { + field: { value, onChange }, + } = useController({ control, name: 'publicKeys' }) + + return ( +
+
+ SSH keys + + SSH keys can be added and removed in your user settings + +
+ + {keys.length > 0 ? ( + + If your image supports the cidata volume and{' '} + + cloud-init + + , the keys above will be added to your instance. Keys are added when the + instance is created and are not updated after instance launch. + + } + /> + ) : ( +
+ } + title="No SSH keys" + body="You need to add a SSH key to be able to see it here" + /> +
+ )} + + {[ + ...keys.map((key) => ( + { + const { checked } = e.target + let newValue = [...value] + 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)} + > + {key.name} + + )), + { + setNewSshKey(e.target.checked) + }} + > + New SSH Key +
+ One-off key saved to this instance and not added to your profile’s SSH keys +
+ {newSshKey && ( + { + 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) + }} + /> + )} +
, + ]} +
+
+ ) +} diff --git a/app/components/form/index.ts b/app/components/form/index.ts index 0fa6f0b108..0ccec5c161 100644 --- a/app/components/form/index.ts +++ b/app/components/form/index.ts @@ -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' diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx index 2389e133a8..1b2e073934 100644 --- a/app/forms/instance-create.tsx +++ b/app/forms/instance-create.tsx @@ -21,19 +21,15 @@ import { } 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, @@ -46,6 +42,7 @@ import { NameField, NetworkInterfaceField, RadioFieldDyn, + SshKeysField, TextField, type DiskTableItem, } from 'app/components/form' @@ -83,6 +80,8 @@ const baseDefaultValues: InstanceCreateInput = { disks: [], networkInterfaces: { type: 'default' }, + publicKeys: [], + start: true, } @@ -188,6 +187,7 @@ export function CreateInstanceForm() { externalIps: [{ type: 'ephemeral' }], start: values.start, networkInterfaces: values.networkInterfaces, + publicKeys: values.publicKeys, }, }) }} @@ -365,7 +365,7 @@ export function CreateInstanceForm() { Authentication - + Networking @@ -386,70 +386,6 @@ export function CreateInstanceForm() { ) } -const SshKeysTable = () => { - const keys = usePrefetchedApiQuery('currentUserSshKeyList', {}).data?.items || [] - - return ( -
-
- SSH keys - - SSH keys can be added and removed in your user settings - -
- - {keys.length > 0 ? ( - - - - Name - Created - - - - {keys.map((key) => ( - - - - - - {formatDateTime(key.timeCreated)} - - - ))} - -
- ) : ( -
- } - title="No SSH keys" - body="You need to add a SSH key to be able to see it here" - /> -
- )} - - - If your image supports the cidata volume and{' '} - - cloud-init - - , the keys above will be added to your instance. Keys are added when the - instance is created and are not updated after instance launch. - - } - /> -
- ) -} - const renderLargeRadioCards = (category: string) => { return PRESETS.filter((option) => option.category === category).map((option) => ( diff --git a/libs/api-mocks/sshKeys.ts b/libs/api-mocks/sshKeys.ts index 56d5a35e1d..f5c72fef4c 100644 --- a/libs/api-mocks/sshKeys.ts +++ b/libs/api-mocks/sshKeys.ts @@ -17,7 +17,16 @@ export const sshKeys: Json[] = [ 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, }, ] diff --git a/libs/ui/index.ts b/libs/ui/index.ts index e70fb970a0..cce0799923 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) => ( -