Siteplane UI

Resource management

Filter and sort projects, inspect a project, and confirm archiving.

Markdown

Projects

Find a project, review its details, or archive it.

ProjectOwnerStatusActions
Customer researchSofia NguyenArchived
Mobile applicationBen CarterActive
Website redesignMara EllisActive
"use client";

import {
  AlertDialog,
  AlertDialogClose,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogPopup,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/siteplane/components/alert-dialog";
import { Badge } from "@/siteplane/components/badge";
import { Button } from "@/siteplane/components/button";
import {
  DataList,
  DataListItem,
  DataListLabel,
  DataListValue,
} from "@/siteplane/components/data-list";
import {
  DetailDrawer,
  DetailDrawerBody,
  DetailDrawerClose,
  DetailDrawerDescription,
  DetailDrawerFooter,
  DetailDrawerHeader,
  DetailDrawerPopup,
  DetailDrawerTitle,
} from "@/siteplane/components/detail-drawer";
import {
  FilterBar,
  FilterBarControls,
  FilterBarFilters,
  FilterBarSearch,
} from "@/siteplane/components/filter-bar";
import { Input } from "@/siteplane/components/input";
import {
  Select,
  SelectItem,
  SelectPopup,
  SelectTrigger,
  SelectValue,
} from "@/siteplane/components/select";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
  type SortDescriptor,
} from "@/siteplane/components/table";
import { useState } from "react";

const statuses = [
  { value: "All", label: "All statuses" },
  { value: "Active", label: "Active" },
  { value: "Archived", label: "Archived" },
];

export default function Example() {
  const [projects, setProjects] = useState([
    {
      id: "website",
      name: "Website redesign",
      owner: "Mara Ellis",
      status: "Active",
    },
    {
      id: "mobile",
      name: "Mobile application",
      owner: "Ben Carter",
      status: "Active",
    },
    {
      id: "research",
      name: "Customer research",
      owner: "Sofia Nguyen",
      status: "Archived",
    },
  ]);
  const [query, setQuery] = useState("");
  const [status, setStatus] = useState("All");
  const [sort, setSort] = useState<SortDescriptor>({
    column: "name",
    direction: "ascending",
  });
  const [activeId, setActiveId] = useState<string>();
  const [open, setOpen] = useState(false);
  const [confirm, setConfirm] = useState(false);
  const active = projects.find((project) => project.id === activeId);
  const visible = projects
    .filter(
      (project) =>
        project.name.toLowerCase().includes(query.toLowerCase()) &&
        (status === "All" || project.status === status),
    )
    .sort((a, b) => {
      const key = sort.column === "owner" ? "owner" : "name";
      return (
        a[key].localeCompare(b[key]) * (sort.direction === "ascending" ? 1 : -1)
      );
    });
  return (
    <div className="grid w-full min-w-0 gap-4">
      <div>
        <h2 className="text-lg font-semibold">Projects</h2>
        <p className="text-sm text-foreground">
          Find a project, review its details, or archive it.
        </p>
      </div>
      <FilterBar aria-label="Project filters">
        <FilterBarControls>
          <FilterBarSearch>
            <Input
              aria-label="Search projects"
              type="search"
              placeholder="Search projects…"
              value={query}
              onChange={(event) => setQuery(event.target.value)}
            />
          </FilterBarSearch>
          <FilterBarFilters>
            <Select
              items={statuses}
              value={status}
              onValueChange={(value) => {
                if (value) setStatus(value);
              }}
            >
              <SelectTrigger aria-label="Filter by status">
                <SelectValue />
              </SelectTrigger>
              <SelectPopup>
                {statuses.map((item) => (
                  <SelectItem key={item.value} value={item.value}>
                    {item.label}
                  </SelectItem>
                ))}
              </SelectPopup>
            </Select>
          </FilterBarFilters>
        </FilterBarControls>
      </FilterBar>
      <Table aria-label="Projects" sortDescriptor={sort} onSortChange={setSort}>
        <TableHeader>
          <TableHead id="name" isRowHeader allowsSorting>
            Project
          </TableHead>
          <TableHead id="owner" allowsSorting>
            Owner
          </TableHead>
          <TableHead id="status">Status</TableHead>
          <TableHead id="actions">Actions</TableHead>
        </TableHeader>
        <TableBody renderEmptyState={() => "No projects match your filters."}>
          {visible.map((project) => (
            <TableRow id={project.id} key={project.id} textValue={project.name}>
              <TableCell>{project.name}</TableCell>
              <TableCell>{project.owner}</TableCell>
              <TableCell>
                <Badge
                  variant={
                    project.status === "Active" ? "success" : "secondary"
                  }
                >
                  {project.status}
                </Badge>
              </TableCell>
              <TableCell>
                <Button
                  variant="ghost"
                  size="sm"
                  aria-label={`View ${project.name}`}
                  onClick={() => {
                    setActiveId(project.id);
                    setOpen(true);
                  }}
                >
                  View
                </Button>
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
      <DetailDrawer open={open} onOpenChange={setOpen}>
        <DetailDrawerPopup>
          <DetailDrawerHeader>
            <DetailDrawerTitle>
              {active?.name ?? "Project details"}
            </DetailDrawerTitle>
            <DetailDrawerDescription>
              Owner and current status.
            </DetailDrawerDescription>
          </DetailDrawerHeader>
          <DetailDrawerBody>
            <DataList orientation="vertical">
              <DataListItem>
                <DataListLabel>Owner</DataListLabel>
                <DataListValue>{active?.owner}</DataListValue>
              </DataListItem>
              <DataListItem>
                <DataListLabel>Status</DataListLabel>
                <DataListValue>{active?.status}</DataListValue>
              </DataListItem>
            </DataList>
          </DetailDrawerBody>
          <DetailDrawerFooter>
            <DetailDrawerClose render={<Button variant="ghost" />}>
              Close
            </DetailDrawerClose>
            <AlertDialog open={confirm} onOpenChange={setConfirm}>
              <AlertDialogTrigger
                render={
                  <Button
                    variant="destructive"
                    disabled={active?.status !== "Active"}
                  />
                }
              >
                Archive project
              </AlertDialogTrigger>
              <AlertDialogPopup>
                <AlertDialogHeader>
                  <AlertDialogTitle>Archive {active?.name}?</AlertDialogTitle>
                  <AlertDialogDescription>
                    Archived projects no longer appear under Active. You can
                    still find them under Archived.
                  </AlertDialogDescription>
                </AlertDialogHeader>
                <AlertDialogFooter>
                  <AlertDialogClose render={<Button variant="ghost" />}>
                    Cancel
                  </AlertDialogClose>
                  <Button
                    variant="destructive"
                    onClick={() => {
                      setProjects((current) =>
                        current.map((project) =>
                          project.id === activeId
                            ? { ...project, status: "Archived" }
                            : project,
                        ),
                      );
                      setConfirm(false);
                      setOpen(false);
                    }}
                  >
                    Archive
                  </Button>
                </AlertDialogFooter>
              </AlertDialogPopup>
            </AlertDialog>
          </DetailDrawerFooter>
        </DetailDrawerPopup>
      </DetailDrawer>
    </div>
  );
}

Installation

Complete one-time setup, then install:

pnpm dlx shadcn@4.18.0 add @siteplane/resource-management

Files

The command installs src/components/resource-management.tsx and its component dependencies. Render its default export in your application. The registry item and the Code panel contain the same source.

Try the workflow

Search by name or filter by status, sort by project or owner, and open a project with View. Archive project opens a confirmation inside the drawer; Archive updates the local data, Cancel leaves the project unchanged. The Archived filter shows archived projects, and an unmatched search shows the empty state.

Composition and adaptation

FilterBar, Input and Select filter a Table; DetailDrawer presents a project with a DataList of its owner and status, and AlertDialog confirms archiving.

AreaDetails
Dataprojects contains stable IDs; query, status and sort define the visible rows.
CallbacksTable onSortChange updates sorting; View sets activeId; dialog callbacks own open state; Archive updates the selected project.
Included statesFiltered and empty results, project details, confirmation and cancel, and the archived status in the table.
Connect to your productReplace the local data and archive update with your service; define pagination, errors and permissions there. The local archive is synchronous; a service version should keep errors visible and close the drawer only after success.