Skip to content
6

Resource Status

Statuspage-style panel: an overall banner over a list of resources, each with an operational / degraded / outage / maintenance state and uptime.

Preview

Installation

npx shadcn@latest add https://hirael.com/r/resource-status.json

Code

components/blocks/resource-status.tsx
'use client';

import * as React from 'react';

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

export type ResourceState = 'operational' | 'degraded' | 'partial-outage' | 'major-outage' | 'maintenance';

const stateDot: Record<ResourceState, string> = {
  operational: 'bg-success',
  degraded: 'bg-warning',
  'partial-outage': 'bg-warning',
  'major-outage': 'bg-destructive',
  maintenance: 'bg-info',
};

const stateText: Record<ResourceState, string> = {
  operational: 'text-success',
  degraded: 'text-warning',
  'partial-outage': 'text-warning',
  'major-outage': 'text-destructive',
  maintenance: 'text-info',
};

const stateLabel: Record<ResourceState, string> = {
  operational: 'Operational',
  degraded: 'Degraded',
  'partial-outage': 'Partial outage',
  'major-outage': 'Major outage',
  maintenance: 'Maintenance',
};

type ResourceStatusProps = React.ComponentProps<'div'>;

const ResourceStatus = ({ className, ...props }: ResourceStatusProps) => {
  return (
    <div
      data-slot="resource-status"
      className={cn('overflow-hidden rounded-lg border border-border bg-card text-card-foreground', className)}
      {...props}
    />
  );
};

interface ResourceStatusBannerProps extends React.ComponentProps<'div'> {
  state: ResourceState;
}

const ResourceStatusBanner = ({ state, className, children, ...props }: ResourceStatusBannerProps) => {
  return (
    <div
      data-slot="resource-status-banner"
      data-state={state}
      className={cn('flex items-center gap-3 border-b border-border px-4 py-3', className)}
      {...props}
    >
      <ResourceStatusDot state={state} />
      <span className={cn('text-sm font-medium', stateText[state])}>{children ?? stateLabel[state]}</span>
    </div>
  );
};

type ResourceStatusListProps = React.ComponentProps<'ul'>;

const ResourceStatusList = ({ className, ...props }: ResourceStatusListProps) => {
  return <ul data-slot="resource-status-list" className={cn('divide-y divide-border', className)} {...props} />;
};

interface ResourceStatusItemProps extends React.ComponentProps<'li'> {
  name: React.ReactNode;
  state: ResourceState;
  description?: React.ReactNode;
  uptime?: React.ReactNode;
}

const ResourceStatusItem = ({
  name,
  state,
  description,
  uptime,
  className,
  children,
  ...props
}: ResourceStatusItemProps) => {
  return (
    <li
      data-slot="resource-status-item"
      data-state={state}
      className={cn('flex items-center justify-between gap-4 px-4 py-3', className)}
      {...props}
    >
      <div className="flex min-w-0 flex-col gap-0.5">
        <span className="truncate text-sm font-medium text-foreground">{name}</span>
        {description ? <span className="truncate text-xs text-muted-foreground">{description}</span> : null}
      </div>
      <div className="flex shrink-0 items-center gap-3">
        {uptime ? <span className="font-mono text-xs text-muted-foreground">{uptime}</span> : null}
        {children ?? <ResourceStatusIndicator state={state} />}
      </div>
    </li>
  );
};

interface ResourceStatusDotProps extends React.ComponentProps<'span'> {
  state: ResourceState;
  /** Pulse the dot to signal a live/active condition. */
  pulse?: boolean;
}

const ResourceStatusDot = ({ state, pulse, className, ...props }: ResourceStatusDotProps) => {
  return (
    <span data-slot="resource-status-dot" className={cn('relative flex size-2.5 shrink-0', className)} {...props}>
      {pulse ? (
        <span
          className={cn('absolute inline-flex size-full animate-ping rounded-full opacity-75', stateDot[state])}
          aria-hidden
        />
      ) : null}
      <span className={cn('relative inline-flex size-2.5 rounded-full', stateDot[state])} aria-hidden />
    </span>
  );
};

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

const ResourceStatusIndicator = ({ state, className, children, ...props }: ResourceStatusIndicatorProps) => {
  return (
    <span
      data-slot="resource-status-indicator"
      data-state={state}
      className={cn('inline-flex items-center gap-1.5 text-xs font-medium', stateText[state], className)}
      {...props}
    >
      <ResourceStatusDot state={state} />
      {children ?? stateLabel[state]}
    </span>
  );
};

export {
  ResourceStatus,
  ResourceStatusBanner,
  ResourceStatusList,
  ResourceStatusItem,
  ResourceStatusDot,
  ResourceStatusIndicator,
};

const ResourceStatusBlock = () => {
  return (
    <section data-slot="resource-status-block" className="flex w-full justify-center bg-background p-6 sm:p-10">
      <ResourceStatus className="w-full max-w-lg">
        <ResourceStatusBanner state="degraded">Some systems degraded</ResourceStatusBanner>
        <ResourceStatusList>
          <ResourceStatusItem name="API" description="REST + GraphQL" state="operational" uptime="99.98%" />
          <ResourceStatusItem name="Dashboard" description="app.example.com" state="operational" uptime="99.95%" />
          <ResourceStatusItem
            name="Webhooks"
            description="Elevated delivery latency"
            state="degraded"
            uptime="99.10%"
          />
          <ResourceStatusItem
            name="Background jobs"
            description="Queue backed up"
            state="partial-outage"
            uptime="97.40%"
          />
          <ResourceStatusItem
            name="Object storage"
            description="Scheduled migration"
            state="maintenance"
            uptime="99.99%"
          />
        </ResourceStatusList>
      </ResourceStatus>
    </section>
  );
};

export default ResourceStatusBlock;