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

Add SSH info to Instance Connect tab #2339

Merged
merged 11 commits into from
Aug 22, 2024
22 changes: 22 additions & 0 deletions app/components/InstanceDocsPopover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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 { Instances16Icon } from '@oxide/design-system/icons/react'

import { docLinks } from '~/util/links'

import { DocsPopover } from './DocsPopover'

export const InstanceDocsPopover = () => (
<DocsPopover
heading="instances"
icon={<Instances16Icon />}
summary="Instances are virtual machines that run on the Oxide platform."
links={[docLinks.instances, docLinks.remoteAccess, docLinks.instanceActions]}
/>
)
12 changes: 3 additions & 9 deletions app/pages/project/instances/InstancesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { useMemo } from 'react'
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'

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

import { DocsPopover } from '~/components/DocsPopover'
import { InstanceDocsPopover } from '~/components/InstanceDocsPopover'
import { RefreshButton } from '~/components/RefreshButton'
import { getProjectSelector, useProjectSelector, useQuickActions } from '~/hooks'
import { InstanceStatusCell } from '~/table/cells/InstanceStatusCell'
Expand All @@ -25,7 +25,6 @@ import { CreateLink } from '~/ui/lib/CreateButton'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
import { TableActions } from '~/ui/lib/Table'
import { docLinks } from '~/util/links'
import { pb } from '~/util/path-builder'

import { useMakeInstanceActions } from './actions'
Expand Down Expand Up @@ -131,12 +130,7 @@ export function InstancesPage() {
<>
<PageHeader>
<PageTitle icon={<Instances24Icon />}>Instances</PageTitle>
<DocsPopover
heading="instances"
icon={<Instances16Icon />}
summary="Instances are virtual machines that run on the Oxide platform."
links={[docLinks.instances, docLinks.instanceActions]}
/>
<InstanceDocsPopover />
</PageHeader>
<TableActions>
<RefreshButton onClick={refetchInstances} />
Expand Down
12 changes: 3 additions & 9 deletions app/pages/project/instances/instance/InstancePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import {
usePrefetchedApiQuery,
type InstanceNetworkInterface,
} from '@oxide/api'
import { Instances16Icon, Instances24Icon } from '@oxide/design-system/icons/react'
import { Instances24Icon } from '@oxide/design-system/icons/react'

import { instanceTransitioning } from '~/api/util'
import { DocsPopover } from '~/components/DocsPopover'
import { ExternalIps } from '~/components/ExternalIps'
import { InstanceDocsPopover } from '~/components/InstanceDocsPopover'
import { MoreActionsMenu } from '~/components/MoreActionsMenu'
import { RefreshButton } from '~/components/RefreshButton'
import { RouteTabs, Tab } from '~/components/RouteTabs'
Expand All @@ -32,7 +32,6 @@ import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { Spinner } from '~/ui/lib/Spinner'
import { Tooltip } from '~/ui/lib/Tooltip'
import { Truncate } from '~/ui/lib/Truncate'
import { docLinks } from '~/util/links'
import { pb } from '~/util/path-builder'

import { useMakeInstanceActions } from '../actions'
Expand Down Expand Up @@ -151,12 +150,7 @@ export function InstancePage() {
<PageHeader>
<PageTitle icon={<Instances24Icon />}>{instance.name}</PageTitle>
<div className="inline-flex gap-2">
<DocsPopover
heading="instances"
icon={<Instances16Icon />}
summary="Instances are virtual machines that run on the Oxide platform."
links={[docLinks.instances, docLinks.instanceActions]}
/>
<InstanceDocsPopover />
<RefreshButton onClick={refreshData} />
<MoreActionsMenu label="Instance actions" actions={actions} />
</div>
Expand Down
89 changes: 71 additions & 18 deletions app/pages/project/instances/instance/tabs/ConnectTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,86 @@
* Copyright Oxide Computer Company
*/

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

import { apiQueryClient, usePrefetchedApiQuery } from '~/api'
import { EquivalentCliCommand } from '~/components/EquivalentCliCommand'
import { useInstanceSelector } from '~/hooks'
import { getInstanceSelector, useInstanceSelector } from '~/hooks'
import { buttonStyle } from '~/ui/lib/Button'
import { SettingsGroup } from '~/ui/lib/SettingsGroup'
import { InlineCode } from '~/ui/lib/InlineCode'
import { LearnMore, SettingsGroup } from '~/ui/lib/SettingsGroup'
import { cliCmd } from '~/util/cli-cmd'
import { links } from '~/util/links'
import { pb } from '~/util/path-builder'

ConnectTab.loader = async ({ params }: LoaderFunctionArgs) => {
const { project, instance } = getInstanceSelector(params)
await apiQueryClient.prefetchQuery('instanceExternalIpList', {
path: { instance },
query: { project },
})
return null
}

export function ConnectTab() {
const { project, instance } = useInstanceSelector()
const { data: externalIps } = usePrefetchedApiQuery('instanceExternalIpList', {
path: { instance },
query: { project },
})
const floatingIp = externalIps.items.find((ip) => ip.kind === 'floating')
const ephemeralIp = externalIps.items.find((ip) => ip.kind === 'ephemeral')
// prefer floating, fall back to ephemeral
const externalIp = floatingIp?.ip || ephemeralIp?.ip

return (
<SettingsGroup.Container>
<SettingsGroup.Body>
<SettingsGroup.Title>Serial console</SettingsGroup.Title>
Connect to your instance&rsquo;s serial console
</SettingsGroup.Body>
<SettingsGroup.Footer>
<EquivalentCliCommand command={cliCmd.serialConsole({ project, instance })} />
<Link
to={pb.serialConsole({ project, instance })}
className={buttonStyle({ size: 'sm' })}
>
Connect
</Link>
</SettingsGroup.Footer>
</SettingsGroup.Container>
<div className="space-y-6">
<SettingsGroup.Container>
<SettingsGroup.Body>
<SettingsGroup.Title>Serial console</SettingsGroup.Title>
Connect to your instance&rsquo;s serial console
</SettingsGroup.Body>
<SettingsGroup.Footer>
<div>
<LearnMore text="Serial Console" href={links.serialConsoleDocs} />
</div>
<div className="flex gap-3">
<EquivalentCliCommand command={cliCmd.serialConsole({ project, instance })} />
<Link
to={pb.serialConsole({ project, instance })}
className={buttonStyle({ size: 'sm' })}
>
Connect
</Link>
</div>
</SettingsGroup.Footer>
</SettingsGroup.Container>
<SettingsGroup.Container>
<SettingsGroup.Body>
<SettingsGroup.Title>SSH</SettingsGroup.Title>
<p>
If your instance allows SSH access, connect with{' '}
<InlineCode>ssh [username]@{externalIp || '[external IP]'}</InlineCode>.
</p>
{!externalIp && (
<p className="mt-2">
This instance has no external IP address. You can add one on the{' '}
<Link
className="link-with-underline"
to={pb.instanceNetworking({ project, instance })}
>
networking
</Link>{' '}
tab.
</p>
)}
</SettingsGroup.Body>
<SettingsGroup.Footer>
<div>
<LearnMore text="SSH" href={links.sshDocs} />
</div>
</SettingsGroup.Footer>
</SettingsGroup.Container>
</div>
)
}
1 change: 1 addition & 0 deletions app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ export const routes = createRoutesFromElements(
<Route
path="connect"
element={<ConnectTab />}
loader={ConnectTab.loader}
handle={{ crumb: 'Connect' }}
/>
</Route>
Expand Down
11 changes: 11 additions & 0 deletions app/ui/lib/InlineCode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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 { classed } from '~/util/classed'

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`
7 changes: 3 additions & 4 deletions app/ui/lib/SettingsGroup.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,16 @@
import { Link } from 'react-router-dom'

import { Button, buttonStyle } from './Button'
import { SettingsGroup } from './SettingsGroup'
import { LearnMore, SettingsGroup } from './SettingsGroup'

export const Default = () => (
<SettingsGroup.Container>
<SettingsGroup.Body>
<SettingsGroup.Title>Serial console</SettingsGroup.Title>
Connect to your instance&rsquo;s serial console
</SettingsGroup.Body>
<SettingsGroup.Footer
docsLink={{ text: 'math', href: 'https://en.wikipedia.org/wiki/Mathematics' }}
>
<SettingsGroup.Footer>
<LearnMore text="math" href="https://en.wikipedia.org/wiki/Mathematics" />
<Link to="/" className={buttonStyle({ size: 'sm' })}>
Connect
</Link>
Expand Down
23 changes: 8 additions & 15 deletions app/ui/lib/SettingsGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,25 @@ import { OpenLink12Icon } from '@oxide/design-system/icons/react'

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

const LearnMore = ({ href, text }: { href: string; text: React.ReactNode }) => (
export const LearnMore = ({ href, text }: { href: string; text: React.ReactNode }) => (
<>
Learn more about{' '}
<a href={href} className="text-accent-secondary hover:text-accent">
<a
href={href}
className="inline-flex items-center text-accent-secondary hover:text-accent"
target="_blank"
rel="noreferrer"
>
{text}
<OpenLink12Icon className="ml-1 align-middle" />
</a>
</>
)

type FooterProps = {
/** Link text */
children: React.ReactNode
docsLink?: { text: string; href: string }
}

/** 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`,
Body: classed.div`p-6`,
Title: classed.div`mb-1 text-sans-lg text-default`,
Footer: ({ children, docsLink }: FooterProps) => (
<div className="flex items-center justify-between border-t px-6 py-3 border-default">
{/* div always present to keep the buttons right-aligned */}
<div className="text-tertiary">{docsLink && <LearnMore {...docsLink} />}</div>
<div className="flex gap-3">{children}</div>
</div>
),
Footer: classed.div`flex items-center justify-between border-t px-6 py-3 border-default h-14`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Classic easier-to-compose-at-the-call-site situation

}
1 change: 1 addition & 0 deletions app/util/classed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const make =

export const classed = {
button: make('button'),
code: make('code'),
div: make('div'),
footer: make('footer'),
h1: make('h1'),
Expand Down
9 changes: 9 additions & 0 deletions app/util/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
*
* Copyright Oxide Computer Company
*/

const remoteAccess = 'https://docs.oxide.computer/guides/remote-access'

export const links = {
accessDocs: 'https://docs.oxide.computer/guides/configuring-access',
cloudInitFormat: 'https://cloudinit.readthedocs.io/en/latest/explanation/format.html',
Expand All @@ -31,6 +34,8 @@ export const links = {
'https://docs.oxide.computer/guides/architecture/service-processors#_server_sled',
snapshotsDocs:
'https://docs.oxide.computer/guides/managing-disks-and-snapshots#_snapshots',
serialConsoleDocs: remoteAccess + '#serial-console',
sshDocs: remoteAccess + '#ssh',
sshKeysDocs: 'https://docs.oxide.computer/guides/user-settings#_ssh_keys',
storageDocs:
'https://docs.oxide.computer/guides/architecture/os-hypervisor-storage#_storage',
Expand Down Expand Up @@ -83,6 +88,10 @@ export const docLinks = {
href: links.quickStart,
linkText: 'Quick Start',
},
remoteAccess: {
href: remoteAccess,
linkText: 'Remote Access',
},
routers: {
href: links.routersDocs,
linkText: 'Custom Routers',
Expand Down
Loading