# Resource management

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

Resource management

[Complete TSX file](/examples/resource-management/default.tsx)

```tsx
"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](/docs/installation/), then install:

```bash
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](/registry/resource-management.json) 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.

| Area                    | Details                                                                                                                                                                                                                               |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Data                    | `projects` contains stable IDs; `query`, `status` and `sort` define the visible rows.                                                                                                                                                 |
| Callbacks               | Table `onSortChange` updates sorting; View sets `activeId`; dialog callbacks own open state; Archive updates the selected project.                                                                                                    |
| Included states         | Filtered and empty results, project details, confirmation and cancel, and the archived status in the table.                                                                                                                           |
| Connect to your product | Replace 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. |
