Skip to content

Commit 79846d4

Browse files
committed
refactor SettingsGroup into composable blocks
1 parent 5e58ff9 commit 79846d4

File tree

6 files changed

+128
-174
lines changed

6 files changed

+128
-174
lines changed

app/components/EquivalentCliCommand.tsx

Lines changed: 33 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,37 +13,14 @@ import { Button } from '~/ui/lib/Button'
1313
import { Modal } from '~/ui/lib/Modal'
1414
import useTimeout from '~/ui/lib/use-timeout'
1515

16-
export default function EquivalentCliCommand({ command }: { command: string }) {
16+
export function EquivalentCliCommand({ command }: { command: string }) {
1717
const [isOpen, setIsOpen] = useState(false)
18+
const [hasCopied, setHasCopied] = useState(false)
1819

1920
function handleDismiss() {
2021
setIsOpen(false)
2122
}
2223

23-
return (
24-
<>
25-
<Button variant="ghost" size="sm" className="ml-2" onClick={() => setIsOpen(true)}>
26-
Equivalent CLI Command
27-
</Button>
28-
<EquivalentCliCommandModal
29-
isOpen={isOpen}
30-
handleDismiss={handleDismiss}
31-
command={command}
32-
/>
33-
</>
34-
)
35-
}
36-
37-
export const EquivalentCliCommandModal = ({
38-
command,
39-
isOpen,
40-
handleDismiss,
41-
}: {
42-
command: string
43-
isOpen: boolean
44-
handleDismiss: () => void
45-
}) => {
46-
const [hasCopied, setHasCopied] = useState(false)
4724
useTimeout(() => setHasCopied(false), hasCopied ? 2000 : null)
4825

4926
const handleCopy = () => {
@@ -53,31 +30,36 @@ export const EquivalentCliCommandModal = ({
5330
}
5431

5532
return (
56-
<Modal isOpen={isOpen} onDismiss={handleDismiss} title="CLI command">
57-
<Modal.Section>
58-
<pre className="flex w-full rounded border px-4 py-3 !normal-case !tracking-normal text-mono-md bg-default border-secondary">
59-
<div className="mr-2 select-none text-quaternary">$</div>
60-
{command}
61-
</pre>
62-
</Modal.Section>
63-
<Modal.Footer
64-
onDismiss={handleDismiss}
65-
onAction={handleCopy}
66-
actionText={
67-
<>
68-
{/* use of invisible keeps button the same size in both states */}
69-
<span className={hasCopied ? 'invisible' : ''}>Copy command</span>
70-
<span
71-
className={`absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center ${
72-
hasCopied ? '' : 'invisible'
73-
}`}
74-
>
75-
<Success12Icon className="mr-2 text-accent" />
76-
Copied
77-
</span>
78-
</>
79-
}
80-
/>
81-
</Modal>
33+
<>
34+
<Button variant="ghost" size="sm" className="ml-2" onClick={() => setIsOpen(true)}>
35+
Equivalent CLI Command
36+
</Button>
37+
<Modal isOpen={isOpen} onDismiss={handleDismiss} title="CLI command">
38+
<Modal.Section>
39+
<pre className="flex w-full rounded border px-4 py-3 !normal-case !tracking-normal text-mono-md bg-default border-secondary">
40+
<div className="mr-2 select-none text-quaternary">$</div>
41+
{command}
42+
</pre>
43+
</Modal.Section>
44+
<Modal.Footer
45+
onDismiss={handleDismiss}
46+
onAction={handleCopy}
47+
actionText={
48+
<>
49+
{/* use of invisible keeps button the same size in both states */}
50+
<span className={hasCopied ? 'invisible' : ''}>Copy command</span>
51+
<span
52+
className={`absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center ${
53+
hasCopied ? '' : 'invisible'
54+
}`}
55+
>
56+
<Success12Icon className="mr-2 text-accent" />
57+
Copied
58+
</span>
59+
</>
60+
}
61+
/>
62+
</Modal>
63+
</>
8264
)
8365
}

app/pages/project/instances/instance/SerialConsolePage.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import { Link } from 'react-router-dom'
1111
import { api } from '@oxide/api'
1212
import { PrevArrow12Icon } from '@oxide/design-system/icons/react'
1313

14-
import EquivalentCliCommand from '~/components/EquivalentCliCommand'
14+
import { EquivalentCliCommand } from '~/components/EquivalentCliCommand'
1515
import { useInstanceSelector } from '~/hooks'
1616
import { Badge, type BadgeColor } from '~/ui/lib/Badge'
1717
import { Spinner } from '~/ui/lib/Spinner'
18+
import { cliCmd } from '~/util/cli-cmd'
1819
import { pb } from '~/util/path-builder'
1920

2021
const Terminal = lazy(() => import('~/components/Terminal'))
@@ -114,7 +115,7 @@ export function SerialConsolePage() {
114115
<div className="flex-shrink-0 justify-between overflow-hidden border-t bg-default border-secondary empty:border-t-0">
115116
<div className="gutter flex h-20 items-center justify-between">
116117
<div>
117-
<EquivalentCliCommand command={serialConsoleCliCommand(project, instance)} />
118+
<EquivalentCliCommand command={cliCmd.serialConsole({ project, instance })} />
118119
</div>
119120

120121
<Badge color={statusColor[connectionStatus]}>
@@ -126,13 +127,6 @@ export function SerialConsolePage() {
126127
)
127128
}
128129

129-
export const serialConsoleCliCommand = (
130-
project: string,
131-
instance: string
132-
) => `oxide instance serial console
133-
--project ${project}
134-
--instance ${instance}`
135-
136130
function SerialSkeleton() {
137131
const instanceSelector = useInstanceSelector()
138132

app/pages/project/instances/instance/tabs/ConnectTab.tsx

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,33 @@
66
* Copyright Oxide Computer Company
77
*/
88

9-
import { useState } from 'react'
9+
import { Link } from 'react-router-dom'
1010

11-
import { EquivalentCliCommandModal } from '~/components/EquivalentCliCommand'
11+
import { EquivalentCliCommand } from '~/components/EquivalentCliCommand'
1212
import { useInstanceSelector } from '~/hooks'
13+
import { buttonStyle } from '~/ui/lib/Button'
1314
import { SettingsGroup } from '~/ui/lib/SettingsGroup'
15+
import { cliCmd } from '~/util/cli-cmd'
1416
import { pb } from '~/util/path-builder'
1517

16-
import { serialConsoleCliCommand } from '../SerialConsolePage'
17-
1818
export function ConnectTab() {
1919
const { project, instance } = useInstanceSelector()
2020

21-
const [cliModalOpen, setCliModalOpen] = useState(false)
22-
2321
return (
24-
<>
25-
<EquivalentCliCommandModal
26-
isOpen={cliModalOpen}
27-
handleDismiss={() => setCliModalOpen(false)}
28-
command={serialConsoleCliCommand(project, instance)}
29-
/>
30-
<SettingsGroup
31-
title="Serial Console"
32-
cta={pb.serialConsole({ project, instance })}
33-
ctaText="Connect"
34-
secondaryCta={() => setCliModalOpen(true)}
35-
secondaryCtaText="Equivalent CLI Command"
36-
>
22+
<SettingsGroup.Container>
23+
<SettingsGroup.Body>
24+
<SettingsGroup.Title>Serial console</SettingsGroup.Title>
3725
Connect to your instance&rsquo;s serial console
38-
</SettingsGroup>
39-
</>
26+
</SettingsGroup.Body>
27+
<SettingsGroup.Footer>
28+
<EquivalentCliCommand command={cliCmd.serialConsole({ project, instance })} />
29+
<Link
30+
to={pb.serialConsole({ project, instance })}
31+
className={buttonStyle({ size: 'sm' })}
32+
>
33+
Connect
34+
</Link>
35+
</SettingsGroup.Footer>
36+
</SettingsGroup.Container>
4037
)
4138
}

app/ui/lib/SettingsGroup.stories.tsx

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,40 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8+
import { Link } from 'react-router-dom'
9+
10+
import { Button, buttonStyle } from './Button'
811
import { SettingsGroup } from './SettingsGroup'
912

1013
export const Default = () => (
11-
<SettingsGroup
12-
title="Serial Console"
13-
docs={{ text: 'Serial Console', link: '/' }}
14-
cta="/"
15-
ctaText="Connect"
16-
>
17-
Connect to your instance&rsquo;s serial console
18-
</SettingsGroup>
14+
<SettingsGroup.Container>
15+
<SettingsGroup.Body>
16+
<SettingsGroup.Title>Serial console</SettingsGroup.Title>
17+
Connect to your instance&rsquo;s serial console
18+
</SettingsGroup.Body>
19+
<SettingsGroup.Footer
20+
docsLink={{ text: 'math', href: 'https://en.wikipedia.org/wiki/Mathematics' }}
21+
>
22+
<Link to="/" className={buttonStyle({ size: 'sm' })}>
23+
Connect
24+
</Link>
25+
</SettingsGroup.Footer>
26+
</SettingsGroup.Container>
1927
)
2028

2129
export const WithoutDocs = () => (
22-
<SettingsGroup
23-
title="Serial Console"
24-
cta="/"
25-
ctaText="Connect"
26-
secondaryCta={() => {}}
27-
secondaryCtaText="Secondary"
28-
>
29-
Connect to your instance&rsquo;s serial console
30-
</SettingsGroup>
31-
)
32-
33-
export const FunctionAction = () => (
34-
<SettingsGroup title="Serial Console" cta={() => alert('hi')} ctaText="Connect">
35-
Connect to your instance&rsquo;s serial console
36-
</SettingsGroup>
30+
<SettingsGroup.Container>
31+
<SettingsGroup.Body>
32+
<SettingsGroup.Title>Serial console</SettingsGroup.Title>
33+
Connect to your instance&rsquo;s serial console
34+
</SettingsGroup.Body>
35+
<SettingsGroup.Footer>
36+
<Link to="/" className={buttonStyle({ size: 'sm' })}>
37+
Connect
38+
</Link>
39+
<Button size="sm" variant="secondary" onClick={() => {}}>
40+
Secondary
41+
</Button>
42+
</SettingsGroup.Footer>
43+
</SettingsGroup.Container>
3744
)

app/ui/lib/SettingsGroup.tsx

Lines changed: 32 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -5,82 +5,44 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { Link } from 'react-router-dom'
98

109
import { OpenLink12Icon } from '@oxide/design-system/icons/react'
1110

12-
import { Button, buttonStyle } from '~/ui/lib/Button'
11+
import { classed } from '~/util/classed'
1312

14-
type Props = {
15-
title: string
16-
docs?: {
17-
text: string
18-
link: string
19-
}
20-
children: React.ReactNode
21-
/** String action is a link */
22-
cta: string | (() => void)
23-
ctaText: string
24-
} & (
25-
| { secondaryCta: string | (() => void); secondaryCtaText: string }
26-
| { secondaryCta?: undefined; secondaryCtaText?: undefined }
27-
)
28-
29-
export const SettingsGroup = ({
30-
title,
31-
docs,
32-
children,
33-
cta,
34-
ctaText,
35-
secondaryCta,
36-
secondaryCtaText,
37-
}: Props) => {
38-
return (
39-
<div className="w-full max-w-[660px] rounded-lg border text-sans-md text-secondary border-default">
40-
<div className="p-6">
41-
<div className="mb-1 text-sans-lg text-default">{title}</div>
42-
{children}
43-
</div>
44-
<div className="flex items-center justify-between border-t px-6 py-3 border-default">
45-
{/* div always present to keep the button right-aligned */}
46-
<div className="text-tertiary">
47-
{docs && (
48-
<>
49-
Learn more about{' '}
50-
<a href={docs.link} className="text-accent-secondary hover:text-accent">
51-
{docs.text}
52-
<OpenLink12Icon className="ml-1 align-middle" />
53-
</a>
54-
</>
55-
)}
56-
</div>
13+
type LearnMoreProps = {
14+
href: string
15+
/** Link text */
16+
text: React.ReactNode
17+
}
5718

58-
<div className="flex gap-3">
59-
{secondaryCta &&
60-
(typeof secondaryCta === 'string' ? (
61-
<Link
62-
to={secondaryCta}
63-
className={buttonStyle({ size: 'sm', variant: 'secondary' })}
64-
>
65-
{secondaryCtaText}
66-
</Link>
67-
) : (
68-
<Button size="sm" variant="secondary" onClick={secondaryCta}>
69-
{secondaryCtaText}
70-
</Button>
71-
))}
19+
type FooterProps = {
20+
/** Link text */
21+
children: React.ReactNode
22+
docsLink?: { text: string; href: string }
23+
}
7224

73-
{typeof cta === 'string' ? (
74-
<Link to={cta} className={buttonStyle({ size: 'sm' })}>
75-
{ctaText}
76-
</Link>
77-
) : (
78-
<Button size="sm" onClick={cta}>
79-
{ctaText}
80-
</Button>
81-
)}
82-
</div>
25+
/** Use size=sm on buttons and links! */
26+
export const SettingsGroup = {
27+
Container: classed.div`w-full max-w-[660px] rounded-lg border text-sans-md text-secondary border-default`,
28+
LearnMore: ({ href, text }: LearnMoreProps) => (
29+
<div>
30+
Learn more about{' '}
31+
<a href={href} className="text-accent-secondary hover:text-accent">
32+
{text}
33+
<OpenLink12Icon className="ml-1 align-middle" />
34+
</a>
35+
</div>
36+
),
37+
Body: classed.div`p-6`,
38+
Title: classed.div`mb-1 text-sans-lg text-default`,
39+
Footer: ({ children, docsLink }: FooterProps) => (
40+
<div className="flex items-center justify-between border-t px-6 py-3 border-default">
41+
{/* div always present to keep the buttons right-aligned */}
42+
<div className="text-tertiary">
43+
{docsLink && <SettingsGroup.LearnMore {...docsLink} />}
8344
</div>
45+
<div className="flex gap-3">{children}</div>
8446
</div>
85-
)
47+
),
8648
}

app/util/cli-cmd.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* Copyright Oxide Computer Company
7+
*/
8+
9+
export const cliCmd = {
10+
serialConsole: ({ project, instance }: { project: string; instance: string }) =>
11+
`oxide instance serial console\n--project ${project}\n--instance ${instance}`,
12+
}

0 commit comments

Comments
 (0)