Skip to content

Commit 1bb9270

Browse files
Add SSH info to Instance Connect tab (#2339)
* Add box to Connect page with SSH info * Remove CopyToClipboard; refactor * Copy tweak * Update size; remove unnecessary copy * tweak for more cardiness * fix ladle type error * Placeholder copy update; still in flux * add correct link to new remote access guide * extract InlineCode, tweak styling, marginally gooder copy --------- Co-authored-by: David Crespo <david.crespo@oxidecomputer.com>
1 parent 8dcddce commit 1bb9270

File tree

10 files changed

+132
-55
lines changed

10 files changed

+132
-55
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
import { Instances16Icon } from '@oxide/design-system/icons/react'
10+
11+
import { docLinks } from '~/util/links'
12+
13+
import { DocsPopover } from './DocsPopover'
14+
15+
export const InstanceDocsPopover = () => (
16+
<DocsPopover
17+
heading="instances"
18+
icon={<Instances16Icon />}
19+
summary="Instances are virtual machines that run on the Oxide platform."
20+
links={[docLinks.instances, docLinks.remoteAccess, docLinks.instanceActions]}
21+
/>
22+
)

app/pages/project/instances/InstancesPage.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import { useMemo } from 'react'
1111
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
1212

1313
import { apiQueryClient, usePrefetchedApiQuery, type Instance } from '@oxide/api'
14-
import { Instances16Icon, Instances24Icon } from '@oxide/design-system/icons/react'
14+
import { Instances24Icon } from '@oxide/design-system/icons/react'
1515

16-
import { DocsPopover } from '~/components/DocsPopover'
16+
import { InstanceDocsPopover } from '~/components/InstanceDocsPopover'
1717
import { RefreshButton } from '~/components/RefreshButton'
1818
import { getProjectSelector, useProjectSelector, useQuickActions } from '~/hooks'
1919
import { InstanceStatusCell } from '~/table/cells/InstanceStatusCell'
@@ -25,7 +25,6 @@ import { CreateLink } from '~/ui/lib/CreateButton'
2525
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
2626
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
2727
import { TableActions } from '~/ui/lib/Table'
28-
import { docLinks } from '~/util/links'
2928
import { pb } from '~/util/path-builder'
3029

3130
import { useMakeInstanceActions } from './actions'
@@ -131,12 +130,7 @@ export function InstancesPage() {
131130
<>
132131
<PageHeader>
133132
<PageTitle icon={<Instances24Icon />}>Instances</PageTitle>
134-
<DocsPopover
135-
heading="instances"
136-
icon={<Instances16Icon />}
137-
summary="Instances are virtual machines that run on the Oxide platform."
138-
links={[docLinks.instances, docLinks.instanceActions]}
139-
/>
133+
<InstanceDocsPopover />
140134
</PageHeader>
141135
<TableActions>
142136
<RefreshButton onClick={refetchInstances} />

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

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import {
1515
usePrefetchedApiQuery,
1616
type InstanceNetworkInterface,
1717
} from '@oxide/api'
18-
import { Instances16Icon, Instances24Icon } from '@oxide/design-system/icons/react'
18+
import { Instances24Icon } from '@oxide/design-system/icons/react'
1919

2020
import { instanceTransitioning } from '~/api/util'
21-
import { DocsPopover } from '~/components/DocsPopover'
2221
import { ExternalIps } from '~/components/ExternalIps'
22+
import { InstanceDocsPopover } from '~/components/InstanceDocsPopover'
2323
import { MoreActionsMenu } from '~/components/MoreActionsMenu'
2424
import { RefreshButton } from '~/components/RefreshButton'
2525
import { RouteTabs, Tab } from '~/components/RouteTabs'
@@ -32,7 +32,6 @@ import { PropertiesTable } from '~/ui/lib/PropertiesTable'
3232
import { Spinner } from '~/ui/lib/Spinner'
3333
import { Tooltip } from '~/ui/lib/Tooltip'
3434
import { Truncate } from '~/ui/lib/Truncate'
35-
import { docLinks } from '~/util/links'
3635
import { pb } from '~/util/path-builder'
3736

3837
import { useMakeInstanceActions } from '../actions'
@@ -151,12 +150,7 @@ export function InstancePage() {
151150
<PageHeader>
152151
<PageTitle icon={<Instances24Icon />}>{instance.name}</PageTitle>
153152
<div className="inline-flex gap-2">
154-
<DocsPopover
155-
heading="instances"
156-
icon={<Instances16Icon />}
157-
summary="Instances are virtual machines that run on the Oxide platform."
158-
links={[docLinks.instances, docLinks.instanceActions]}
159-
/>
153+
<InstanceDocsPopover />
160154
<RefreshButton onClick={refreshData} />
161155
<MoreActionsMenu label="Instance actions" actions={actions} />
162156
</div>

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

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

9-
import { Link } from 'react-router-dom'
9+
import { Link, type LoaderFunctionArgs } from 'react-router-dom'
1010

11+
import { apiQueryClient, usePrefetchedApiQuery } from '~/api'
1112
import { EquivalentCliCommand } from '~/components/EquivalentCliCommand'
12-
import { useInstanceSelector } from '~/hooks'
13+
import { getInstanceSelector, useInstanceSelector } from '~/hooks'
1314
import { buttonStyle } from '~/ui/lib/Button'
14-
import { SettingsGroup } from '~/ui/lib/SettingsGroup'
15+
import { InlineCode } from '~/ui/lib/InlineCode'
16+
import { LearnMore, SettingsGroup } from '~/ui/lib/SettingsGroup'
1517
import { cliCmd } from '~/util/cli-cmd'
18+
import { links } from '~/util/links'
1619
import { pb } from '~/util/path-builder'
1720

21+
ConnectTab.loader = async ({ params }: LoaderFunctionArgs) => {
22+
const { project, instance } = getInstanceSelector(params)
23+
await apiQueryClient.prefetchQuery('instanceExternalIpList', {
24+
path: { instance },
25+
query: { project },
26+
})
27+
return null
28+
}
29+
1830
export function ConnectTab() {
1931
const { project, instance } = useInstanceSelector()
32+
const { data: externalIps } = usePrefetchedApiQuery('instanceExternalIpList', {
33+
path: { instance },
34+
query: { project },
35+
})
36+
const floatingIp = externalIps.items.find((ip) => ip.kind === 'floating')
37+
const ephemeralIp = externalIps.items.find((ip) => ip.kind === 'ephemeral')
38+
// prefer floating, fall back to ephemeral
39+
const externalIp = floatingIp?.ip || ephemeralIp?.ip
2040

2141
return (
22-
<SettingsGroup.Container>
23-
<SettingsGroup.Body>
24-
<SettingsGroup.Title>Serial console</SettingsGroup.Title>
25-
Connect to your instance&rsquo;s serial console
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>
42+
<div className="space-y-6">
43+
<SettingsGroup.Container>
44+
<SettingsGroup.Body>
45+
<SettingsGroup.Title>Serial console</SettingsGroup.Title>
46+
Connect to your instance&rsquo;s serial console
47+
</SettingsGroup.Body>
48+
<SettingsGroup.Footer>
49+
<div>
50+
<LearnMore text="Serial Console" href={links.serialConsoleDocs} />
51+
</div>
52+
<div className="flex gap-3">
53+
<EquivalentCliCommand command={cliCmd.serialConsole({ project, instance })} />
54+
<Link
55+
to={pb.serialConsole({ project, instance })}
56+
className={buttonStyle({ size: 'sm' })}
57+
>
58+
Connect
59+
</Link>
60+
</div>
61+
</SettingsGroup.Footer>
62+
</SettingsGroup.Container>
63+
<SettingsGroup.Container>
64+
<SettingsGroup.Body>
65+
<SettingsGroup.Title>SSH</SettingsGroup.Title>
66+
<p>
67+
If your instance allows SSH access, connect with{' '}
68+
<InlineCode>ssh [username]@{externalIp || '[external IP]'}</InlineCode>.
69+
</p>
70+
{!externalIp && (
71+
<p className="mt-2">
72+
This instance has no external IP address. You can add one on the{' '}
73+
<Link
74+
className="link-with-underline"
75+
to={pb.instanceNetworking({ project, instance })}
76+
>
77+
networking
78+
</Link>{' '}
79+
tab.
80+
</p>
81+
)}
82+
</SettingsGroup.Body>
83+
<SettingsGroup.Footer>
84+
<div>
85+
<LearnMore text="SSH" href={links.sshDocs} />
86+
</div>
87+
</SettingsGroup.Footer>
88+
</SettingsGroup.Container>
89+
</div>
3790
)
3891
}

app/routes.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ export const routes = createRoutesFromElements(
334334
<Route
335335
path="connect"
336336
element={<ConnectTab />}
337+
loader={ConnectTab.loader}
337338
handle={{ crumb: 'Connect' }}
338339
/>
339340
</Route>

app/ui/lib/InlineCode.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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+
import { classed } from '~/util/classed'
10+
11+
export const InlineCode = classed.code`whitespace-nowrap rounded-sm px-[3px] py-[1px] text-mono-sm !normal-case bg-raise border border-secondary mx-px`

app/ui/lib/SettingsGroup.stories.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,16 @@
88
import { Link } from 'react-router-dom'
99

1010
import { Button, buttonStyle } from './Button'
11-
import { SettingsGroup } from './SettingsGroup'
11+
import { LearnMore, SettingsGroup } from './SettingsGroup'
1212

1313
export const Default = () => (
1414
<SettingsGroup.Container>
1515
<SettingsGroup.Body>
1616
<SettingsGroup.Title>Serial console</SettingsGroup.Title>
1717
Connect to your instance&rsquo;s serial console
1818
</SettingsGroup.Body>
19-
<SettingsGroup.Footer
20-
docsLink={{ text: 'math', href: 'https://en.wikipedia.org/wiki/Mathematics' }}
21-
>
19+
<SettingsGroup.Footer>
20+
<LearnMore text="math" href="https://en.wikipedia.org/wiki/Mathematics" />
2221
<Link to="/" className={buttonStyle({ size: 'sm' })}>
2322
Connect
2423
</Link>

app/ui/lib/SettingsGroup.tsx

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,25 @@ import { OpenLink12Icon } from '@oxide/design-system/icons/react'
1010

1111
import { classed } from '~/util/classed'
1212

13-
const LearnMore = ({ href, text }: { href: string; text: React.ReactNode }) => (
13+
export const LearnMore = ({ href, text }: { href: string; text: React.ReactNode }) => (
1414
<>
1515
Learn more about{' '}
16-
<a href={href} className="text-accent-secondary hover:text-accent">
16+
<a
17+
href={href}
18+
className="inline-flex items-center text-accent-secondary hover:text-accent"
19+
target="_blank"
20+
rel="noreferrer"
21+
>
1722
{text}
1823
<OpenLink12Icon className="ml-1 align-middle" />
1924
</a>
2025
</>
2126
)
2227

23-
type FooterProps = {
24-
/** Link text */
25-
children: React.ReactNode
26-
docsLink?: { text: string; href: string }
27-
}
28-
2928
/** Use size=sm on buttons and links! */
3029
export const SettingsGroup = {
3130
Container: classed.div`w-full max-w-[660px] rounded-lg border text-sans-md text-secondary border-default`,
3231
Body: classed.div`p-6`,
3332
Title: classed.div`mb-1 text-sans-lg text-default`,
34-
Footer: ({ children, docsLink }: FooterProps) => (
35-
<div className="flex items-center justify-between border-t px-6 py-3 border-default">
36-
{/* div always present to keep the buttons right-aligned */}
37-
<div className="text-tertiary">{docsLink && <LearnMore {...docsLink} />}</div>
38-
<div className="flex gap-3">{children}</div>
39-
</div>
40-
),
33+
Footer: classed.div`flex items-center justify-between border-t px-6 py-3 border-default h-14`,
4134
}

app/util/classed.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const make =
3131

3232
export const classed = {
3333
button: make('button'),
34+
code: make('code'),
3435
div: make('div'),
3536
footer: make('footer'),
3637
h1: make('h1'),

app/util/links.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8+
9+
const remoteAccess = 'https://docs.oxide.computer/guides/remote-access'
10+
811
export const links = {
912
accessDocs: 'https://docs.oxide.computer/guides/configuring-access',
1013
cloudInitFormat: 'https://cloudinit.readthedocs.io/en/latest/explanation/format.html',
@@ -31,6 +34,8 @@ export const links = {
3134
'https://docs.oxide.computer/guides/architecture/service-processors#_server_sled',
3235
snapshotsDocs:
3336
'https://docs.oxide.computer/guides/managing-disks-and-snapshots#_snapshots',
37+
serialConsoleDocs: remoteAccess + '#serial-console',
38+
sshDocs: remoteAccess + '#ssh',
3439
sshKeysDocs: 'https://docs.oxide.computer/guides/user-settings#_ssh_keys',
3540
storageDocs:
3641
'https://docs.oxide.computer/guides/architecture/os-hypervisor-storage#_storage',
@@ -83,6 +88,10 @@ export const docLinks = {
8388
href: links.quickStart,
8489
linkText: 'Quick Start',
8590
},
91+
remoteAccess: {
92+
href: remoteAccess,
93+
linkText: 'Remote Access',
94+
},
8695
routers: {
8796
href: links.routersDocs,
8897
linkText: 'Custom Routers',

0 commit comments

Comments
 (0)