From 5e58ff95f56b41d9cb95bb1cbf54bb4013f81992 Mon Sep 17 00:00:00 2001 From: Benjamin Leonard Date: Thu, 7 Mar 2024 12:37:48 +0000 Subject: [PATCH 1/3] Add CLI command button to settings group --- app/components/EquivalentCliCommand.tsx | 82 +++++++++++-------- .../instances/instance/SerialConsolePage.tsx | 13 +-- .../instances/instance/tabs/ConnectTab.tsx | 30 +++++-- app/ui/lib/SettingsGroup.stories.tsx | 8 +- app/ui/lib/SettingsGroup.tsx | 49 ++++++++--- 5 files changed, 126 insertions(+), 56 deletions(-) diff --git a/app/components/EquivalentCliCommand.tsx b/app/components/EquivalentCliCommand.tsx index 9c0f8697b4..8afd302376 100644 --- a/app/components/EquivalentCliCommand.tsx +++ b/app/components/EquivalentCliCommand.tsx @@ -15,12 +15,35 @@ import useTimeout from '~/ui/lib/use-timeout' export default function EquivalentCliCommand({ command }: { command: string }) { const [isOpen, setIsOpen] = useState(false) - const [hasCopied, setHasCopied] = useState(false) function handleDismiss() { setIsOpen(false) } + return ( + <> + + + + ) +} + +export const EquivalentCliCommandModal = ({ + command, + isOpen, + handleDismiss, +}: { + command: string + isOpen: boolean + handleDismiss: () => void +}) => { + const [hasCopied, setHasCopied] = useState(false) useTimeout(() => setHasCopied(false), hasCopied ? 2000 : null) const handleCopy = () => { @@ -30,36 +53,31 @@ export default function EquivalentCliCommand({ command }: { command: string }) { } return ( - <> - - - -
-            
$
- {command} -
-
- - {/* use of invisible keeps button the same size in both states */} - Copy command - - - Copied - - - } - /> -
- + + +
+          
$
+ {command} +
+
+ + {/* use of invisible keeps button the same size in both states */} + Copy command + + + Copied + + + } + /> +
) } diff --git a/app/pages/project/instances/instance/SerialConsolePage.tsx b/app/pages/project/instances/instance/SerialConsolePage.tsx index 6f6262ad18..fc5e18676c 100644 --- a/app/pages/project/instances/instance/SerialConsolePage.tsx +++ b/app/pages/project/instances/instance/SerialConsolePage.tsx @@ -95,10 +95,6 @@ export function SerialConsolePage() { } }, []) - const command = `oxide instance serial console - --project ${project} - --instance ${instance}` - return (
- +
@@ -130,6 +126,13 @@ export function SerialConsolePage() { ) } +export const serialConsoleCliCommand = ( + project: string, + instance: string +) => `oxide instance serial console +--project ${project} +--instance ${instance}` + function SerialSkeleton() { const instanceSelector = useInstanceSelector() diff --git a/app/pages/project/instances/instance/tabs/ConnectTab.tsx b/app/pages/project/instances/instance/tabs/ConnectTab.tsx index 71e5611398..555e8b9f7f 100644 --- a/app/pages/project/instances/instance/tabs/ConnectTab.tsx +++ b/app/pages/project/instances/instance/tabs/ConnectTab.tsx @@ -6,20 +6,36 @@ * Copyright Oxide Computer Company */ +import { useState } from 'react' + +import { EquivalentCliCommandModal } from '~/components/EquivalentCliCommand' import { useInstanceSelector } from '~/hooks' import { SettingsGroup } from '~/ui/lib/SettingsGroup' import { pb } from '~/util/path-builder' +import { serialConsoleCliCommand } from '../SerialConsolePage' + export function ConnectTab() { const { project, instance } = useInstanceSelector() + const [cliModalOpen, setCliModalOpen] = useState(false) + return ( - - Connect to your instance’s serial console - + <> + setCliModalOpen(false)} + command={serialConsoleCliCommand(project, instance)} + /> + setCliModalOpen(true)} + secondaryCtaText="Equivalent CLI Command" + > + Connect to your instance’s serial console + + ) } diff --git a/app/ui/lib/SettingsGroup.stories.tsx b/app/ui/lib/SettingsGroup.stories.tsx index 65bb11c009..64adfc53da 100644 --- a/app/ui/lib/SettingsGroup.stories.tsx +++ b/app/ui/lib/SettingsGroup.stories.tsx @@ -19,7 +19,13 @@ export const Default = () => ( ) export const WithoutDocs = () => ( - + {}} + secondaryCtaText="Secondary" + > Connect to your instance’s serial console ) diff --git a/app/ui/lib/SettingsGroup.tsx b/app/ui/lib/SettingsGroup.tsx index 9915718c02..14517669ad 100644 --- a/app/ui/lib/SettingsGroup.tsx +++ b/app/ui/lib/SettingsGroup.tsx @@ -21,9 +21,20 @@ type Props = { /** String action is a link */ cta: string | (() => void) ctaText: string -} +} & ( + | { secondaryCta: string | (() => void); secondaryCtaText: string } + | { secondaryCta?: undefined; secondaryCtaText?: undefined } +) -export const SettingsGroup = ({ title, docs, children, cta, ctaText }: Props) => { +export const SettingsGroup = ({ + title, + docs, + children, + cta, + ctaText, + secondaryCta, + secondaryCtaText, +}: Props) => { return (
@@ -44,15 +55,31 @@ export const SettingsGroup = ({ title, docs, children, cta, ctaText }: Props) => )}
- {typeof cta === 'string' ? ( - - {ctaText} - - ) : ( - - )} +
+ {secondaryCta && + (typeof secondaryCta === 'string' ? ( + + {secondaryCtaText} + + ) : ( + + ))} + + {typeof cta === 'string' ? ( + + {ctaText} + + ) : ( + + )} +
) From 79846d46c9d3d9d035196c95faf459674059bc81 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 8 Mar 2024 15:48:09 -0600 Subject: [PATCH 2/3] refactor SettingsGroup into composable blocks --- app/components/EquivalentCliCommand.tsx | 84 ++++++--------- .../instances/instance/SerialConsolePage.tsx | 12 +-- .../instances/instance/tabs/ConnectTab.tsx | 39 ++++--- app/ui/lib/SettingsGroup.stories.tsx | 53 +++++---- app/ui/lib/SettingsGroup.tsx | 102 ++++++------------ app/util/cli-cmd.ts | 12 +++ 6 files changed, 128 insertions(+), 174 deletions(-) create mode 100644 app/util/cli-cmd.ts diff --git a/app/components/EquivalentCliCommand.tsx b/app/components/EquivalentCliCommand.tsx index 8afd302376..2c16564f0e 100644 --- a/app/components/EquivalentCliCommand.tsx +++ b/app/components/EquivalentCliCommand.tsx @@ -13,37 +13,14 @@ import { Button } from '~/ui/lib/Button' import { Modal } from '~/ui/lib/Modal' import useTimeout from '~/ui/lib/use-timeout' -export default function EquivalentCliCommand({ command }: { command: string }) { +export function EquivalentCliCommand({ command }: { command: string }) { const [isOpen, setIsOpen] = useState(false) + const [hasCopied, setHasCopied] = useState(false) function handleDismiss() { setIsOpen(false) } - return ( - <> - - - - ) -} - -export const EquivalentCliCommandModal = ({ - command, - isOpen, - handleDismiss, -}: { - command: string - isOpen: boolean - handleDismiss: () => void -}) => { - const [hasCopied, setHasCopied] = useState(false) useTimeout(() => setHasCopied(false), hasCopied ? 2000 : null) const handleCopy = () => { @@ -53,31 +30,36 @@ export const EquivalentCliCommandModal = ({ } return ( - - -
-          
$
- {command} -
-
- - {/* use of invisible keeps button the same size in both states */} - Copy command - - - Copied - - - } - /> -
+ <> + + + +
+            
$
+ {command} +
+
+ + {/* use of invisible keeps button the same size in both states */} + Copy command + + + Copied + + + } + /> +
+ ) } diff --git a/app/pages/project/instances/instance/SerialConsolePage.tsx b/app/pages/project/instances/instance/SerialConsolePage.tsx index fc5e18676c..9d80e0b4d6 100644 --- a/app/pages/project/instances/instance/SerialConsolePage.tsx +++ b/app/pages/project/instances/instance/SerialConsolePage.tsx @@ -11,10 +11,11 @@ import { Link } from 'react-router-dom' import { api } from '@oxide/api' import { PrevArrow12Icon } from '@oxide/design-system/icons/react' -import EquivalentCliCommand from '~/components/EquivalentCliCommand' +import { EquivalentCliCommand } from '~/components/EquivalentCliCommand' import { useInstanceSelector } from '~/hooks' import { Badge, type BadgeColor } from '~/ui/lib/Badge' import { Spinner } from '~/ui/lib/Spinner' +import { cliCmd } from '~/util/cli-cmd' import { pb } from '~/util/path-builder' const Terminal = lazy(() => import('~/components/Terminal')) @@ -114,7 +115,7 @@ export function SerialConsolePage() {
- +
@@ -126,13 +127,6 @@ export function SerialConsolePage() { ) } -export const serialConsoleCliCommand = ( - project: string, - instance: string -) => `oxide instance serial console ---project ${project} ---instance ${instance}` - function SerialSkeleton() { const instanceSelector = useInstanceSelector() diff --git a/app/pages/project/instances/instance/tabs/ConnectTab.tsx b/app/pages/project/instances/instance/tabs/ConnectTab.tsx index 555e8b9f7f..89141139d0 100644 --- a/app/pages/project/instances/instance/tabs/ConnectTab.tsx +++ b/app/pages/project/instances/instance/tabs/ConnectTab.tsx @@ -6,36 +6,33 @@ * Copyright Oxide Computer Company */ -import { useState } from 'react' +import { Link } from 'react-router-dom' -import { EquivalentCliCommandModal } from '~/components/EquivalentCliCommand' +import { EquivalentCliCommand } from '~/components/EquivalentCliCommand' import { useInstanceSelector } from '~/hooks' +import { buttonStyle } from '~/ui/lib/Button' import { SettingsGroup } from '~/ui/lib/SettingsGroup' +import { cliCmd } from '~/util/cli-cmd' import { pb } from '~/util/path-builder' -import { serialConsoleCliCommand } from '../SerialConsolePage' - export function ConnectTab() { const { project, instance } = useInstanceSelector() - const [cliModalOpen, setCliModalOpen] = useState(false) - return ( - <> - setCliModalOpen(false)} - command={serialConsoleCliCommand(project, instance)} - /> - setCliModalOpen(true)} - secondaryCtaText="Equivalent CLI Command" - > + + + Serial console Connect to your instance’s serial console - - + + + + + Connect + + + ) } diff --git a/app/ui/lib/SettingsGroup.stories.tsx b/app/ui/lib/SettingsGroup.stories.tsx index 64adfc53da..1ac409a2df 100644 --- a/app/ui/lib/SettingsGroup.stories.tsx +++ b/app/ui/lib/SettingsGroup.stories.tsx @@ -5,33 +5,40 @@ * * Copyright Oxide Computer Company */ +import { Link } from 'react-router-dom' + +import { Button, buttonStyle } from './Button' import { SettingsGroup } from './SettingsGroup' export const Default = () => ( - - Connect to your instance’s serial console - + + + Serial console + Connect to your instance’s serial console + + + + Connect + + + ) export const WithoutDocs = () => ( - {}} - secondaryCtaText="Secondary" - > - Connect to your instance’s serial console - -) - -export const FunctionAction = () => ( - alert('hi')} ctaText="Connect"> - Connect to your instance’s serial console - + + + Serial console + Connect to your instance’s serial console + + + + Connect + + + + ) diff --git a/app/ui/lib/SettingsGroup.tsx b/app/ui/lib/SettingsGroup.tsx index 14517669ad..9dd48a8f9a 100644 --- a/app/ui/lib/SettingsGroup.tsx +++ b/app/ui/lib/SettingsGroup.tsx @@ -5,82 +5,44 @@ * * Copyright Oxide Computer Company */ -import { Link } from 'react-router-dom' import { OpenLink12Icon } from '@oxide/design-system/icons/react' -import { Button, buttonStyle } from '~/ui/lib/Button' +import { classed } from '~/util/classed' -type Props = { - title: string - docs?: { - text: string - link: string - } - children: React.ReactNode - /** String action is a link */ - cta: string | (() => void) - ctaText: string -} & ( - | { secondaryCta: string | (() => void); secondaryCtaText: string } - | { secondaryCta?: undefined; secondaryCtaText?: undefined } -) - -export const SettingsGroup = ({ - title, - docs, - children, - cta, - ctaText, - secondaryCta, - secondaryCtaText, -}: Props) => { - return ( -
-
-
{title}
- {children} -
-
- {/* div always present to keep the button right-aligned */} -
- {docs && ( - <> - Learn more about{' '} - - {docs.text} - - - - )} -
+type LearnMoreProps = { + href: string + /** Link text */ + text: React.ReactNode +} -
- {secondaryCta && - (typeof secondaryCta === 'string' ? ( - - {secondaryCtaText} - - ) : ( - - ))} +type FooterProps = { + /** Link text */ + children: React.ReactNode + docsLink?: { text: string; href: string } +} - {typeof cta === 'string' ? ( - - {ctaText} - - ) : ( - - )} -
+/** Use size=sm on buttons and links! */ +export const SettingsGroup = { + Container: classed.div`w-full max-w-[660px] rounded-lg border text-sans-md text-secondary border-default`, + LearnMore: ({ href, text }: LearnMoreProps) => ( +
+ Learn more about{' '} + + {text} + + +
+ ), + Body: classed.div`p-6`, + Title: classed.div`mb-1 text-sans-lg text-default`, + Footer: ({ children, docsLink }: FooterProps) => ( +
+ {/* div always present to keep the buttons right-aligned */} +
+ {docsLink && }
+
{children}
- ) + ), } diff --git a/app/util/cli-cmd.ts b/app/util/cli-cmd.ts new file mode 100644 index 0000000000..d4154c394e --- /dev/null +++ b/app/util/cli-cmd.ts @@ -0,0 +1,12 @@ +/* + * 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 + */ + +export const cliCmd = { + serialConsole: ({ project, instance }: { project: string; instance: string }) => + `oxide instance serial console\n--project ${project}\n--instance ${instance}`, +} From 27ee9a10116c99788f5ab604fa861ef759c34012 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 8 Mar 2024 16:01:57 -0600 Subject: [PATCH 3/3] don't need to export LearnMore --- app/ui/lib/SettingsGroup.tsx | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/app/ui/lib/SettingsGroup.tsx b/app/ui/lib/SettingsGroup.tsx index 9dd48a8f9a..62e8d943ae 100644 --- a/app/ui/lib/SettingsGroup.tsx +++ b/app/ui/lib/SettingsGroup.tsx @@ -10,11 +10,15 @@ import { OpenLink12Icon } from '@oxide/design-system/icons/react' import { classed } from '~/util/classed' -type LearnMoreProps = { - href: string - /** Link text */ - text: React.ReactNode -} +const LearnMore = ({ href, text }: { href: string; text: React.ReactNode }) => ( + <> + Learn more about{' '} + + {text} + + + +) type FooterProps = { /** Link text */ @@ -25,23 +29,12 @@ type FooterProps = { /** Use size=sm on buttons and links! */ export const SettingsGroup = { Container: classed.div`w-full max-w-[660px] rounded-lg border text-sans-md text-secondary border-default`, - LearnMore: ({ href, text }: LearnMoreProps) => ( -
- Learn more about{' '} - - {text} - - -
- ), Body: classed.div`p-6`, Title: classed.div`mb-1 text-sans-lg text-default`, Footer: ({ children, docsLink }: FooterProps) => (
{/* div always present to keep the buttons right-aligned */} -
- {docsLink && } -
+
{docsLink && }
{children}
),