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
2 changes: 1 addition & 1 deletion app/components/ImageDetailSideModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function ImageDetailSideModal({
>
<PropertiesTable>
<PropertiesTable.IdRow id={image.id} />
<PropertiesTable.DescriptionRow description={image.description} sideModal />
<PropertiesTable.DescriptionRow description={image.description} />
<PropertiesTable.Row label="Visibility">{visibility}</PropertiesTable.Row>
<PropertiesTable.Row label="OS">{image.os}</PropertiesTable.Row>
<PropertiesTable.Row label="Version">{image.version}</PropertiesTable.Row>
Expand Down
2 changes: 1 addition & 1 deletion app/components/IpPoolDetailSideModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function IpPoolDetailSideModal({ pool, onDismiss }: IpPoolDetailSideModal
>
<PropertiesTable>
<PropertiesTable.IdRow id={pool.id} />
<PropertiesTable.DescriptionRow description={pool.description} sideModal />
<PropertiesTable.DescriptionRow description={pool.description} />
<PropertiesTable.Row label="IP version">
<IpVersionBadge ipVersion={pool.ipVersion} />
</PropertiesTable.Row>
Expand Down
2 changes: 1 addition & 1 deletion app/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Sidebar.Nav = ({ children, heading }: SidebarNav) => (
<div className="mx-3 my-4 space-y-1">
{heading && (
<div className="text-mono-sm text-tertiary mb-2">
<Truncate text={heading} maxLength={24} />
<Truncate text={heading} />
</div>
)}
<nav aria-label="Sidebar navigation">
Expand Down
2 changes: 1 addition & 1 deletion app/components/SnapshotDetailSideModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function SnapshotDetailSideModal({
>
<PropertiesTable>
<PropertiesTable.IdRow id={snapshot.id} />
<PropertiesTable.DescriptionRow description={snapshot.description} sideModal />
<PropertiesTable.DescriptionRow description={snapshot.description} />
<PropertiesTable.Row label="State">
<SnapshotStateBadge state={snapshot.state} />
</PropertiesTable.Row>
Expand Down
2 changes: 1 addition & 1 deletion app/pages/project/disks/DiskDetailSideModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export function DiskDetailSideModal({
>
<PropertiesTable>
<PropertiesTable.IdRow id={disk.id} />
<PropertiesTable.DescriptionRow description={disk.description} sideModal />
<PropertiesTable.DescriptionRow description={disk.description} />
<PropertiesTable.SizeRow bytes={disk.size} />
<PropertiesTable.Row label="State">
<DiskStateBadge state={disk.state.state} />
Expand Down
2 changes: 1 addition & 1 deletion app/pages/project/instances/NetworkingTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const VpcNameFromId = ({ value }: { value: string }) => {
q(api.vpcView, { path: { vpc: value } }, { throwOnError: false })
)

if (isError) return <Truncate text={value} maxLength={32} />
if (isError) return <Truncate text={value} position="middle" className="max-w-48" />
if (!vpc) return <SkeletonCell />
return <LinkCell to={pb.vpc({ project, vpc: vpc.name })}>{vpc.name}</LinkCell>
}
Expand Down
10 changes: 2 additions & 8 deletions app/pages/project/vpcs/internet-gateway-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,7 @@ export default function EditInternetGatewayForm() {
<PropertiesTable key={internetGateway.id}>
<PropertiesTable.IdRow id={internetGateway.id} />
<PropertiesTable.Row label="Name">{internetGateway.name}</PropertiesTable.Row>
<PropertiesTable.DescriptionRow
description={internetGateway.description}
sideModal
/>
<PropertiesTable.DescriptionRow description={internetGateway.description} />
</PropertiesTable>
<FormDivider />

Expand All @@ -141,10 +138,7 @@ export default function EditInternetGatewayForm() {
<PropertiesTable.Row label="Name">
{gatewayIpAddress.name}
</PropertiesTable.Row>
<PropertiesTable.DescriptionRow
description={gatewayIpAddress.description}
sideModal
/>
<PropertiesTable.DescriptionRow description={gatewayIpAddress.description} />
<PropertiesTable.Row label="IP Address">
<CopyableIp ip={gatewayIpAddress.address} />
</PropertiesTable.Row>
Expand Down
10 changes: 9 additions & 1 deletion app/pages/settings/AccessTokensPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { DateTime } from '~/ui/lib/DateTime'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
import { TipIcon } from '~/ui/lib/TipIcon'
import { Truncate } from '~/ui/lib/Truncate'
import { docLinks } from '~/util/links'
import { pb } from '~/util/path-builder'

Expand Down Expand Up @@ -78,7 +79,14 @@ export default function AccessTokensPage() {
</TipIcon>
</>
),
cell: (info) => <span>{info.getValue()}</span>,
cell: (info) => (
<Truncate
text={info.getValue()}
position="middle"
className="max-w-48"
hasCopyButton
/>
),
}),
colHelper.accessor('timeCreated', Columns.timeCreated),
colHelper.accessor('timeExpires', {
Expand Down
12 changes: 11 additions & 1 deletion app/pages/system/inventory/DisksTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Badge, type BadgeColor } from '@oxide/design-system/ui'

import { useQueryTable } from '~/table/QueryTable'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { Truncate } from '~/ui/lib/Truncate'

const POLICY_KIND_BADGE_COLORS: Record<PhysicalDiskPolicy['kind'], BadgeColor> = {
in_service: 'default',
Expand Down Expand Up @@ -50,7 +51,16 @@ export const handle = { crumb: 'Disks' }

const colHelper = createColumnHelper<PhysicalDisk>()
const staticCols = [
colHelper.accessor('id', {}),
colHelper.accessor('id', {
cell: (info) => (
<Truncate
text={info.getValue()}
position="middle"
className="max-w-48"
hasCopyButton
/>
),
}),
colHelper.accessor((d) => (d.formFactor === 'u2' ? 'U.2' : 'M.2'), {
header: 'Form factor',
cell: (info) => <Badge>{info.getValue()}</Badge>,
Expand Down
9 changes: 7 additions & 2 deletions app/pages/system/inventory/SledsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import { createColumnHelper } from '@tanstack/react-table'
import { api, getListQFn, queryClient, type Sled } from '@oxide/api'
import { Servers24Icon } from '@oxide/design-system/icons/react'

import { makeLinkCell } from '~/table/cells/LinkCell'
import { LinkCell } from '~/table/cells/LinkCell'
import { useQueryTable } from '~/table/QueryTable'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { Truncate } from '~/ui/lib/Truncate'
import { pb } from '~/util/path-builder'

import { ProvisionPolicyBadge, SledKindBadge, SledStateBadge } from './sled/SledBadges'
Expand All @@ -29,7 +30,11 @@ export const handle = { crumb: 'Sleds' }
const colHelper = createColumnHelper<Sled>()
const staticCols = [
colHelper.accessor('id', {
cell: makeLinkCell((sledId) => pb.sledInstances({ sledId })),
cell: (info) => (
<LinkCell to={pb.sledInstances({ sledId: info.getValue() })}>
<Truncate text={info.getValue()} position="middle" className="max-w-48" />
</LinkCell>
),
}),
// TODO: colHelper.accessor('baseboard.serviceAddress', { header: 'service address' }),
colHelper.group({
Expand Down
8 changes: 2 additions & 6 deletions app/pages/system/inventory/sled/SledPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@ export default function SledPage() {
</PageHeader>

<PropertiesTable columns={2} className="-mt-8 mb-8">
<PropertiesTable.Row label="sled id">
<span className="text-default">{sled.id}</span>
</PropertiesTable.Row>
<PropertiesTable.IdRow label="sled id" id={sled.id} />
<PropertiesTable.Row label="policy kind">
<SledKindBadge policy={sled.policy} />
</PropertiesTable.Row>
Expand All @@ -68,9 +66,7 @@ export default function SledPage() {
<PropertiesTable.Row label="usable hardware threads">
<span className="text-default">{sled.usableHardwareThreads}</span>
</PropertiesTable.Row>
<PropertiesTable.Row label="rack id">
<span className="text-default">{sled.rackId}</span>
</PropertiesTable.Row>
<PropertiesTable.IdRow label="rack id" id={sled.rackId} />
<PropertiesTable.SizeRow
label="usable physical ram"
bytes={sled.usablePhysicalRam}
Expand Down
4 changes: 3 additions & 1 deletion app/pages/system/silos/SiloScimTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ const colHelper = createColumnHelper<ScimClientBearerToken>()
const staticColumns = [
colHelper.accessor('id', {
header: 'ID',
cell: (info) => <Truncate text={info.getValue()} position="middle" maxLength={18} />,
cell: (info) => (
<Truncate text={info.getValue()} position="middle" className="max-w-48" />
),
}),
colHelper.accessor('timeCreated', Columns.timeCreated),
colHelper.accessor('timeExpires', {
Expand Down
10 changes: 2 additions & 8 deletions app/table/cells/DescriptionCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,5 @@
import { EmptyCell } from '~/table/cells/EmptyCell'
import { Truncate } from '~/ui/lib/Truncate'

export type Props = { text?: string; maxLength?: number; sideModal?: boolean }

export const DescriptionCell = ({ text, maxLength, sideModal }: Props) =>
text ? (
<Truncate text={text} maxLength={maxLength ?? (sideModal ? 20 : 48)} />
) : (
<EmptyCell />
)
export const DescriptionCell = ({ text }: { text?: string }) =>
text ? <Truncate text={text} className="max-w-96" /> : <EmptyCell />
2 changes: 1 addition & 1 deletion app/table/cells/SubnetNameCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const SubnetNameFromId = ({ subnetId }: { subnetId: string }) => {
const { data: subnet, isError } = useQuery(
q(api.vpcSubnetView, { path: { subnet: subnetId } }, { throwOnError: false })
)
if (isError) return <Truncate text={subnetId} maxLength={32} />
if (isError) return <Truncate text={subnetId} position="middle" className="max-w-48" />
if (!subnet) return <SkeletonCell /> // loading
return <span className="text-default">{subnet.name}</span>
}
10 changes: 2 additions & 8 deletions app/table/columns/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
import type { InstanceState } from '~/api'
import { InstanceStateBadge } from '~/components/StateBadge'
import { DescriptionCell } from '~/table/cells/DescriptionCell'
import { CopyToClipboard } from '~/ui/lib/CopyToClipboard'
import { DateTime } from '~/ui/lib/DateTime'
import { Truncate } from '~/ui/lib/Truncate'
import { Size } from '~/ui/lib/ValueUnit'

// the full type of the info arg is CellContext<Row, Item> from RT, but in these
Expand All @@ -22,14 +22,8 @@ function dateCell(info: Info<Date>) {
}

function idCell(info: Info<string>) {
const text = info.getValue()
return (
<div className="flex items-center gap-0.5 overflow-hidden">
{text}
<div className="flex items-center p-0.5">
<CopyToClipboard text={text} />
</div>
</div>
<Truncate text={info.getValue()} position="middle" className="max-w-48" hasCopyButton />
)
}

Expand Down
12 changes: 7 additions & 5 deletions app/ui/lib/FileInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function FileInput({
}

return (
<label className={cn(className, 'group relative block')}>
<label className={cn(className, 'group relative block contain-[inline-size]')}>
<input
ref={mergeRefs([inputRef, ref])}
type="file"
Expand Down Expand Up @@ -95,11 +95,13 @@ export function FileInput({
>
<Document16Icon className="h-4 w-4" />
</div>
<div className="text-sans-md flex h-8 items-center">
<div className="text-sans-md flex h-8 max-w-full items-center">
{file && !dragOver ? (
<div className="text-raise flex items-center">
<Truncate text={file.name} maxLength={32} position="middle" />
<span className="text-tertiary ml-1">({formatBytes(file.size).label})</span>
<div className="text-raise flex min-w-0 items-center">
<Truncate text={file.name} position="middle" />
<span className="text-tertiary ml-1 whitespace-nowrap">
({formatBytes(file.size).label})
</span>
<button
type="button"
onClick={handleResetInput}
Expand Down
12 changes: 3 additions & 9 deletions app/ui/lib/PropertiesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,13 @@ PropertiesTable.Row = ({ label, children }: PropertiesTableRowProps) => (

PropertiesTable.IdRow = ({ id, label = 'ID' }: { id?: string | null; label?: string }) => (
<PropertiesTable.Row label={label}>
{id ? <Truncate text={id} maxLength={32} hasCopyButton /> : <EmptyCell />}
{id ? <Truncate text={id} position="middle" hasCopyButton /> : <EmptyCell />}
</PropertiesTable.Row>
)

PropertiesTable.DescriptionRow = ({
description,
sideModal,
}: {
description: string
sideModal?: boolean
}) => (
PropertiesTable.DescriptionRow = ({ description }: { description: string }) => (
<PropertiesTable.Row label="Description">
<DescriptionCell text={description} sideModal={sideModal} />
<DescriptionCell text={description} />
</PropertiesTable.Row>
)

Expand Down
2 changes: 1 addition & 1 deletion app/ui/lib/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const Toast = ({
className="text-mono-sm text-accent-secondary hover:text-accent mt-3 block"
to={cta.link}
>
<Truncate text={cta.text} maxLength={36} />
<Truncate text={cta.text} />
</Link>
)}
</div>
Expand Down
64 changes: 64 additions & 0 deletions app/ui/lib/Truncate.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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 { render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'

import { Truncate } from './Truncate'

const measureText = vi.fn((text: string) => ({ width: text.length }))

Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {
configurable: true,
value: () => ({ font: '', letterSpacing: '', measureText }),
})

afterEach(() => {
vi.restoreAllMocks()
measureText.mockClear()
})

describe('Truncate', () => {
it('preserves complete Unicode characters when truncating in the middle', () => {
vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(6)
vi.spyOn(HTMLElement.prototype, 'scrollWidth', 'get').mockReturnValue(16)
const text = '😀'.repeat(8)

render(<Truncate text={text} position="middle" />)

const displayedText = screen
.getByLabelText(text)
.querySelector('.absolute')?.textContent
expect(displayedText).toBeDefined()
expect(hasUnpairedSurrogate(displayedText ?? '')).toBe(false)
})

it('truncates whenever the rendered text is wider than its container', () => {
vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockReturnValue(9)
vi.spyOn(HTMLElement.prototype, 'scrollWidth', 'get').mockReturnValue(10)
const text = 'abcdefghij'

render(<Truncate text={text} position="middle" />)

expect(screen.getByLabelText(text).querySelector('.absolute')).not.toBeNull()
})
})

function hasUnpairedSurrogate(text: string) {
for (let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i)
if (code >= 0xd800 && code <= 0xdbff) {
const next = text.charCodeAt(i + 1)
if (next < 0xdc00 || next > 0xdfff) return true
i++
} else if (code >= 0xdc00 && code <= 0xdfff) {
return true
}
}
return false
}
Loading
Loading