Skip to content
6

VM Table

Virtual-machine instance table with live status dots, size, region, IP and uptime. Self-contained parts, no table primitive required.

Preview

Installation

npx shadcn@latest add https://hirael.com/r/vm-table.json

Code

components/blocks/vm-table.tsx
'use client';

import * as React from 'react';

import { cn } from '@/lib/utils';

export type VmState = 'running' | 'stopped' | 'starting' | 'stopping' | 'error' | 'suspended';

const vmStateMeta: Record<VmState, { dot: string; text: string; label: string; live?: boolean }> = {
  running: { dot: 'bg-success', text: 'text-success', label: 'Running' },
  stopped: {
    dot: 'bg-muted-foreground',
    text: 'text-muted-foreground',
    label: 'Stopped',
  },
  starting: {
    dot: 'bg-info',
    text: 'text-info',
    label: 'Starting',
    live: true,
  },
  stopping: {
    dot: 'bg-warning',
    text: 'text-warning',
    label: 'Stopping',
    live: true,
  },
  error: { dot: 'bg-destructive', text: 'text-destructive', label: 'Error' },
  suspended: {
    dot: 'bg-muted-foreground',
    text: 'text-muted-foreground',
    label: 'Suspended',
  },
};

interface VmTableProps extends React.ComponentProps<'table'> {
  caption?: React.ReactNode;
}

const VmTable = ({ className, caption, children, ...props }: VmTableProps) => {
  return (
    <div data-slot="vm-table-container" className="w-full overflow-x-auto rounded-lg border border-border">
      <table data-slot="vm-table" className={cn('w-full caption-bottom text-sm', className)} {...props}>
        {caption ? <caption className="p-3 text-xs text-muted-foreground">{caption}</caption> : null}
        {children}
      </table>
    </div>
  );
};

const VmTableHeader = ({ className, ...props }: React.ComponentProps<'thead'>) => {
  return (
    <thead
      data-slot="vm-table-header"
      className={cn('border-b border-border bg-muted/40 text-muted-foreground', className)}
      {...props}
    />
  );
};

const VmTableHead = ({ className, ...props }: React.ComponentProps<'th'>) => {
  return (
    <th
      data-slot="vm-table-head"
      className={cn(
        'h-9 px-3 text-start align-middle font-medium whitespace-nowrap',
        'font-mono text-[10px] uppercase tracking-[0.1em]',
        className,
      )}
      {...props}
    />
  );
};

const VmTableBody = ({ className, ...props }: React.ComponentProps<'tbody'>) => {
  return <tbody data-slot="vm-table-body" className={cn('divide-y divide-border', className)} {...props} />;
};

const VmTableRow = ({ className, ...props }: React.ComponentProps<'tr'>) => {
  return <tr data-slot="vm-table-row" className={cn('transition-colors hover:bg-muted/40', className)} {...props} />;
};

const VmTableCell = ({ className, ...props }: React.ComponentProps<'td'>) => {
  return (
    <td data-slot="vm-table-cell" className={cn('px-3 py-2.5 align-middle whitespace-nowrap', className)} {...props} />
  );
};

interface VmTableNameProps extends Omit<React.ComponentProps<'td'>, 'id'> {
  id?: React.ReactNode;
}

const VmTableName = ({ id, className, children, ...props }: VmTableNameProps) => {
  return (
    <td data-slot="vm-table-name" className={cn('px-3 py-2.5 align-middle', className)} {...props}>
      <div className="flex flex-col">
        <span className="font-medium text-foreground">{children}</span>
        {id ? <span className="font-mono text-xs text-muted-foreground">{id}</span> : null}
      </div>
    </td>
  );
};

interface VmStatusProps extends Omit<React.ComponentProps<'span'>, 'children'> {
  state: VmState;
  children?: React.ReactNode;
}

const VmStatus = ({ state, className, children, ...props }: VmStatusProps) => {
  const meta = vmStateMeta[state];
  return (
    <span
      data-slot="vm-status"
      data-state={state}
      className={cn('inline-flex items-center gap-1.5 text-xs font-medium', meta.text, className)}
      {...props}
    >
      <span className="relative flex size-2">
        {meta.live ? (
          <span
            className={cn('absolute inline-flex size-full animate-ping rounded-full opacity-75', meta.dot)}
            aria-hidden
          />
        ) : null}
        <span className={cn('relative inline-flex size-2 rounded-full', meta.dot)} aria-hidden />
      </span>
      {children ?? meta.label}
    </span>
  );
};

export { VmTable, VmTableHeader, VmTableHead, VmTableBody, VmTableRow, VmTableCell, VmTableName, VmStatus };

const VM_ROWS: {
  name: string;
  id: string;
  state: VmState;
  size: string;
  region: string;
  ip: string;
  uptime: string;
}[] = [
  {
    name: 'web-prod-01',
    id: 'i-0a1b2c3d',
    state: 'running',
    size: 'c6i.2xlarge',
    region: 'us-east-1',
    ip: '10.0.1.24',
    uptime: '42d',
  },
  {
    name: 'worker-prod-02',
    id: 'i-0e4f5a6b',
    state: 'running',
    size: 'c6i.xlarge',
    region: 'us-east-1',
    ip: '10.0.1.51',
    uptime: '42d',
  },
  {
    name: 'batch-runner-07',
    id: 'i-0c7d8e9f',
    state: 'starting',
    size: 'm6i.large',
    region: 'eu-west-2',
    ip: '10.1.2.13',
    uptime: '—',
  },
  {
    name: 'db-replica-03',
    id: 'i-0b3c4d5e',
    state: 'error',
    size: 'r6i.4xlarge',
    region: 'eu-west-2',
    ip: '10.1.2.44',
    uptime: '—',
  },
  {
    name: 'sandbox-14',
    id: 'i-0f6a7b8c',
    state: 'stopped',
    size: 't3.medium',
    region: 'ap-south-1',
    ip: '—',
    uptime: '—',
  },
];

const VmTableBlock = () => {
  return (
    <section data-slot="vm-table-block" className="flex w-full justify-center bg-background p-6 sm:p-10">
      <div className="w-full max-w-3xl">
        <VmTable caption="5 instances across 3 regions">
          <VmTableHeader>
            <VmTableRow>
              <VmTableHead>Instance</VmTableHead>
              <VmTableHead>Status</VmTableHead>
              <VmTableHead>Size</VmTableHead>
              <VmTableHead>Region</VmTableHead>
              <VmTableHead>IP</VmTableHead>
              <VmTableHead className="text-end">Uptime</VmTableHead>
            </VmTableRow>
          </VmTableHeader>
          <VmTableBody>
            {VM_ROWS.map((row) => (
              <VmTableRow key={row.id}>
                <VmTableName id={row.id}>{row.name}</VmTableName>
                <VmTableCell>
                  <VmStatus state={row.state} />
                </VmTableCell>
                <VmTableCell className="font-mono text-xs text-muted-foreground">{row.size}</VmTableCell>
                <VmTableCell className="text-muted-foreground">{row.region}</VmTableCell>
                <VmTableCell className="font-mono text-xs text-muted-foreground">{row.ip}</VmTableCell>
                <VmTableCell className="text-end font-mono text-xs text-muted-foreground">{row.uptime}</VmTableCell>
              </VmTableRow>
            ))}
          </VmTableBody>
        </VmTable>
      </div>
    </section>
  );
};

export default VmTableBlock;