Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,30 @@
*
* Copyright Oxide Computer Company
*/
import { useState } from 'react'
import { useState, type ReactNode } from 'react'

import { Success12Icon } from '@oxide/design-system/icons/react'

import { Button } from '~/ui/lib/Button'
import { Modal } from '~/ui/lib/Modal'
import { useTimeout } from '~/ui/lib/use-timeout'

export function EquivalentCliCommand({ command }: { command: string }) {
type CopyCodeProps = {
code: string
modalButtonText: string
copyButtonText: string
modalTitle: string
/** rendered code */
children?: ReactNode
}

export function CopyCode({
code,
modalButtonText,
copyButtonText,
modalTitle,
children,
}: CopyCodeProps) {
const [isOpen, setIsOpen] = useState(false)
const [hasCopied, setHasCopied] = useState(false)

Expand All @@ -24,30 +39,28 @@ export function EquivalentCliCommand({ command }: { command: string }) {
useTimeout(() => setHasCopied(false), hasCopied ? 2000 : null)

const handleCopy = () => {
window.navigator.clipboard.writeText(command).then(() => {
window.navigator.clipboard.writeText(code).then(() => {
setHasCopied(true)
})
}

return (
<>
<Button variant="ghost" size="sm" className="ml-2" onClick={() => setIsOpen(true)}>
Equivalent CLI Command
{modalButtonText}
</Button>
<Modal isOpen={isOpen} onDismiss={handleDismiss} title="CLI command">
<Modal isOpen={isOpen} onDismiss={handleDismiss} title={modalTitle} width="free">
<Modal.Section>
<pre className="flex w-full rounded border px-4 py-3 !normal-case !tracking-normal text-mono-md bg-default border-secondary">
<div className="mr-2 select-none text-tertiary">$</div>
{command}
{children}
</pre>
</Modal.Section>
<Modal.Footer
onDismiss={handleDismiss}
onAction={handleCopy}
actionText={
<>
{/* use of invisible keeps button the same size in both states */}
<span className={hasCopied ? 'invisible' : ''}>Copy command</span>
<span className={hasCopied ? 'invisible' : ''}>{copyButtonText}</span>
<span
className={`absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center ${
hasCopied ? '' : 'invisible'
Expand All @@ -63,3 +76,25 @@ export function EquivalentCliCommand({ command }: { command: string }) {
</>
)
}

type EquivProps = { project: string; instance: string }

export function EquivalentCliCommand({ project, instance }: EquivProps) {
const cmdParts = [
'oxide instance serial console',
`--project ${project}`,
`--instance ${instance}`,
]

return (
<CopyCode
code={cmdParts.join(' ')}
modalButtonText="Equivalent CLI Command"
copyButtonText="Copy command"
modalTitle="CLI command"
>
<div className="mr-2 select-none text-tertiary">$</div>
{cmdParts.join(' \\\n')}
</CopyCode>
)
}
2 changes: 1 addition & 1 deletion app/components/form/FullPageForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ const ConfirmNavigation = ({ blocker }: { blocker: Blocker }) => (
isOpen={blocker.state === 'blocked'}
onDismiss={() => blocker.reset?.()}
title="Confirm navigation"
narrow
width="narrow"
>
<Modal.Section>
Are you sure you want to leave this page?
Expand Down
2 changes: 1 addition & 1 deletion app/components/form/SideModalForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export function SideModalForm<TFieldValues extends FieldValues>({
isOpen
onDismiss={() => setShowNavGuard(false)}
title="Confirm navigation"
narrow
width="narrow"
overlay={false}
>
<Modal.Section>
Expand Down
5 changes: 2 additions & 3 deletions app/pages/project/instances/instance/SerialConsolePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@ import {
} from '@oxide/api'
import { PrevArrow12Icon } from '@oxide/design-system/icons/react'

import { EquivalentCliCommand } from '~/components/EquivalentCliCommand'
import { EquivalentCliCommand } from '~/components/CopyCode'
import { InstanceStateBadge } from '~/components/StateBadge'
import { getInstanceSelector, useInstanceSelector } from '~/hooks/use-params'
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'))
Expand Down Expand Up @@ -159,7 +158,7 @@ export function Component() {
<div className="shrink-0 justify-between overflow-hidden border-t bg-default border-secondary empty:border-t-0">
<div className="gutter flex h-20 items-center justify-between">
<div>
<EquivalentCliCommand command={cliCmd.serialConsole({ project, instance })} />
<EquivalentCliCommand project={project} instance={instance} />
</div>

<Badge color={statusColor[connectionStatus]}>
Expand Down
5 changes: 2 additions & 3 deletions app/pages/project/instances/instance/tabs/ConnectTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@
import { Link, type LoaderFunctionArgs } from 'react-router'

import { apiQueryClient, usePrefetchedApiQuery } from '~/api'
import { EquivalentCliCommand } from '~/components/EquivalentCliCommand'
import { EquivalentCliCommand } from '~/components/CopyCode'
import { getInstanceSelector, useInstanceSelector } from '~/hooks/use-params'
import { buttonStyle } from '~/ui/lib/Button'
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'

Expand Down Expand Up @@ -51,7 +50,7 @@ export function Component() {
<LearnMore text="Serial Console" href={links.serialConsoleDocs} />
</div>
<div className="flex gap-3">
<EquivalentCliCommand command={cliCmd.serialConsole({ project, instance })} />
<EquivalentCliCommand project={project} instance={instance} />
<Link
to={pb.serialConsole({ project, instance })}
className={buttonStyle({ size: 'sm' })}
Expand Down
18 changes: 13 additions & 5 deletions app/ui/lib/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,21 @@ import { Button } from './Button'
import { DialogOverlay } from './DialogOverlay'
import { ModalContext } from './modal-context'

type Width = 'narrow' | 'medium' | 'free'

const widthClass: Record<Width, string> = {
narrow: 'w-full max-w-[24rem]',
medium: 'w-full max-w-[28rem]',
free: 'min-w-[24rem] max-w-[48rem]', // give it a big max just to be safe
}

export type ModalProps = {
title: string
isOpen: boolean
children?: React.ReactNode
onDismiss: () => void
/** Default false. Only needed in a couple of spots. */
narrow?: true
/** Default medium. Only needed in a couple of spots. */
width?: Width
/** Default true. We only need to hide it for the rare case of modal on top of modal. */
overlay?: boolean
}
Expand All @@ -38,7 +46,7 @@ export function Modal({
onDismiss,
title,
isOpen,
narrow,
width = 'medium',
overlay = true,
}: ModalProps) {
return (
Expand Down Expand Up @@ -66,8 +74,8 @@ export function Modal({
animate={{ x: '-50%', y: '-50%' }}
transition={{ type: 'spring', duration: 0.3, bounce: 0 }}
className={cn(
'pointer-events-auto fixed left-1/2 top-[min(50%,500px)] z-modal m-0 flex max-h-[min(800px,80vh)] w-full flex-col justify-between rounded-lg border p-0 bg-raise border-secondary elevation-2',
narrow ? 'max-w-[24rem]' : 'max-w-[28rem]'
'pointer-events-auto fixed left-1/2 top-[min(50%,500px)] z-modal m-0 flex max-h-[min(800px,80vh)] flex-col justify-between rounded-lg border p-0 bg-raise border-secondary elevation-2',
widthClass[width]
)}
>
<Dialog.Title className="border-b px-4 py-4 text-sans-semi-lg bg-secondary border-b-secondary">
Expand Down