mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor(site/src): migrate shared Pill to Badge (#27638)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
## Summary
Deletes the shared `Pill` component (`site/src/components/Pill/`) and
migrates every consumer to `Badge`. Accepts the intentional visual shift
(`rounded-full` → `rounded-md`, slightly different color tokens). No
`ThemeRole` variants were added to `Badge`.
### Key changes
- Added `site/src/components/Badge/themeRoleToBadgeVariant.ts`, a small
mapper used at call sites that receive a `ThemeRole` (e.g.
`StatusBadge`, `TemplateVersionStatusBadge`).
- Exported `BadgeProps` from `Badge.tsx` so the mapper and status
helpers can reference the variant type.
- Replaced `PillSpinner` with `<Spinner loading />`.
- Renamed `StatusPill` → `StatusBadge` (folder, file, and export) and
updated the audit log and connection log imports.
- Extended `Badge.stories.tsx` with a status + spinner icon pattern
(rather than porting the full Pill story).
### Variant mapping applied
| Pill `type` | Badge `variant` |
|---|---|
| `success` | `green` |
| `error` | `destructive` |
| `warning`, `danger` | `warning` |
| `active`, `notice` | `info` |
| `inactive`, `muted`, `info` | `default` |
| `preview` | `purple` |
### Out of scope
The local Health-page `Pill` in `site/src/pages/HealthPage/Content.tsx`
is a separate component with no shared import; it will be migrated in a
follow-up.
## Validation
- `pnpm check` (biome) passes
- `pnpm lint` (biome + tsc + circular-deps + compiler + knip) passes
- `pnpm test:storybook` for Badge, TemplatePageHeader, and AuditLogRow
stories: 30/30 pass
- Grep for `components/Pill` and `PillSpinner` in `site/src` returns
nothing
<details>
<summary>Implementation plan</summary>
# Migrate Pill to Badge
## Goal
Delete `site/src/components/Pill/Pill.tsx` and migrate every shared-Pill
consumer to `site/src/components/Badge/Badge.tsx`. Accept the visual
shift (`rounded-full` → `rounded-md`, slightly different color tokens).
Do not add ThemeRole variants to Badge.
**Out of scope:** the local Health-page `Pill` in
`site/src/pages/HealthPage/Content.tsx` (separate component, no shared
import). Rename/migrate that in a follow-up.
## Variant mapping
Use this fixed mapping at every call site (Pill `type` → Badge
`variant`):
| Pill `type` | Badge `variant` | Notes |
|---|---|---|
| `success` | `green` | |
| `error` | `destructive` | |
| `warning`, `danger` | `warning` | Pill treats danger like orange
warning |
| `active`, `notice` | `info` | sky / pending |
| `inactive`, `muted`, `info` | `default` | Pill `info` is neutral grey;
Badge `info` is sky, so map Pill `info` to `default` |
| `preview` | `purple` | |
Other remaps:
- `icon={node}` → put `node` as the first child of `Badge`
- `size="lg"` → `size="md"` (closest); add a local `className` only if a
specific layout breaks
- `PillSpinner` → `<Spinner loading />` from
`site/src/components/Spinner/Spinner.tsx`
- Prefer existing wrappers when they already exist (e.g.
`DeprecatedBadge` on the template header)
For ThemeRole-driven helpers (`httpStatusColor`, status getters), add a
tiny shared mapper next to Badge
(`site/src/components/Badge/themeRoleToBadgeVariant.ts`). Use it in
`StatusPill` and any site that still receives `ThemeRole` rather than
hardcoding variants twice.
## Call-site work (ordered easy → hard)
1. **Trivial label chips** (import swap + variant map):
TemplatePageHeader (use `DeprecatedBadge`), VersionRow,
ChangeWorkspaceVersionDialog, PermissionPillsList, IdpPillList,
OrganizationPills
2. **Icon-as-child chips**: Provisioner, ProvisionerTag (local
`BooleanPill` becomes a Badge wrapper), TemplateExampleCard (keep
clickable styles via `className`)
3. **Status + spinner**: TemplateVersionStatusBadge (Badge + children
icons; `PillSpinner` → `Spinner`), utils/workspace.tsx (replace
`PillSpinner` imports only)
4. **Wrappers that own ThemeRole**: StatusPill (switch to Badge +
`themeRoleToBadgeVariant(httpStatusColor(...))`; rename to
`StatusBadge`), Notifications (Badge with severity `className`
overrides)
5. **Delete shared Pill**: remove `Pill.tsx`, `Pill.stories.tsx`, and
the `components/Pill` folder; extend `Badge.stories.tsx` only if a
migrated pattern is missing (icon + spinner status)
## Non-goals / constraints
- Do not change Badge's default shape or add `rounded-full`
- Do not broaden Badge's public API with `type` / `icon` props (that
recreates Pill)
- Do not rename IDP/permission "PillsList" filenames unless needed for
imports
- Modify Badge itself only for the new mapper helper (or stories)
## Validation
- `pnpm check` / `pnpm lint` under `site/`
- Storybook smoke on Badge + affected pages
- Grep for `components/Pill` and `PillSpinner` must be empty in
`site/src`
</details>
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { DatabaseIcon, SettingsIcon, TriangleAlertIcon } from "lucide-react";
|
||||
import {
|
||||
CheckIcon,
|
||||
DatabaseIcon,
|
||||
SettingsIcon,
|
||||
TriangleAlertIcon,
|
||||
} from "lucide-react";
|
||||
import { Badges } from "#/components/Badges/Badges";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import { Badge } from "./Badge";
|
||||
|
||||
const meta: Meta<typeof Badge> = {
|
||||
@@ -148,3 +154,22 @@ export const MediumWithIcon: Story = {
|
||||
</Badge>
|
||||
),
|
||||
};
|
||||
|
||||
export const StatusWithIcon: Story = {
|
||||
render: () => (
|
||||
<Badges>
|
||||
<Badge variant="info" size="md" role="status">
|
||||
<Spinner loading />
|
||||
Running
|
||||
</Badge>
|
||||
<Badge variant="green" size="md" role="status">
|
||||
<CheckIcon />
|
||||
Success
|
||||
</Badge>
|
||||
<Badge variant="destructive" size="md" role="status">
|
||||
<TriangleAlertIcon />
|
||||
Failed
|
||||
</Badge>
|
||||
</Badges>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -17,6 +17,8 @@ const badgeVariants = cva(
|
||||
variant: {
|
||||
default:
|
||||
"border-surface-secondary bg-surface-secondary text-content-secondary shadow",
|
||||
outline:
|
||||
"border-border bg-transparent text-content-primary shadow-none",
|
||||
warning:
|
||||
"border-highlight-orange bg-surface-orange text-highlight-orange shadow",
|
||||
destructive:
|
||||
@@ -65,7 +67,7 @@ const badgeVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
type BadgeProps = React.ComponentPropsWithRef<"div"> &
|
||||
export type BadgeProps = React.ComponentPropsWithRef<"div"> &
|
||||
VariantProps<typeof badgeVariants> & {
|
||||
asChild?: boolean;
|
||||
};
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { Pill, PillSpinner } from "./Pill";
|
||||
|
||||
const meta: Meta<typeof Pill> = {
|
||||
title: "components/Pill",
|
||||
component: Pill,
|
||||
args: {
|
||||
children: "Default",
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Pill>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Danger: Story = {
|
||||
args: {
|
||||
children: "Danger",
|
||||
type: "danger",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithError: Story = {
|
||||
args: {
|
||||
children: "Error",
|
||||
type: "error",
|
||||
},
|
||||
};
|
||||
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
children: "Warning",
|
||||
type: "warning",
|
||||
},
|
||||
};
|
||||
|
||||
export const Notice: Story = {
|
||||
args: {
|
||||
children: "Notice",
|
||||
type: "notice",
|
||||
},
|
||||
};
|
||||
|
||||
export const Info: Story = {
|
||||
args: {
|
||||
children: "Information",
|
||||
type: "info",
|
||||
},
|
||||
};
|
||||
|
||||
export const Success: Story = {
|
||||
args: {
|
||||
children: "Success",
|
||||
type: "success",
|
||||
},
|
||||
};
|
||||
|
||||
export const Active: Story = {
|
||||
args: {
|
||||
children: "Active",
|
||||
type: "active",
|
||||
},
|
||||
};
|
||||
|
||||
export const Muted: Story = {
|
||||
args: {
|
||||
children: "Muted",
|
||||
type: "muted" as const,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithIcon: Story = {
|
||||
args: {
|
||||
children: "Information",
|
||||
type: "info",
|
||||
icon: <InfoIcon aria-hidden="true" className="size-icon-sm" />,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithSpinner: Story = {
|
||||
args: {
|
||||
icon: <PillSpinner />,
|
||||
},
|
||||
};
|
||||
@@ -1,103 +0,0 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import type { ThemeRole } from "#/theme/roles";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { Spinner } from "../Spinner/Spinner";
|
||||
|
||||
type PillType = ThemeRole | "muted";
|
||||
|
||||
type PillProps = React.ComponentPropsWithRef<"div"> & {
|
||||
icon?: ReactNode;
|
||||
type?: PillType;
|
||||
size?: "md" | "lg";
|
||||
};
|
||||
|
||||
const pillRoleVariants = cva("text-content-primary", {
|
||||
variants: {
|
||||
type: {
|
||||
error: "border-border-destructive bg-surface-red",
|
||||
warning: "border-border-warning bg-surface-orange",
|
||||
notice: "border-border-pending bg-surface-sky",
|
||||
info: "border-border bg-surface-secondary",
|
||||
success: "border-border-success bg-surface-green",
|
||||
active: "border-border-pending bg-surface-sky",
|
||||
inactive: "border-border bg-surface-secondary",
|
||||
danger: "border-border-warning bg-surface-orange",
|
||||
preview: "border-border-purple bg-surface-purple",
|
||||
muted:
|
||||
"border-border-secondary bg-surface-tertiary text-content-secondary",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
type: "inactive",
|
||||
},
|
||||
});
|
||||
|
||||
const pillLayoutVariants = cva(
|
||||
"inline-flex cursor-default items-center whitespace-nowrap rounded-full border border-solid text-[12px] font-normal leading-none [&_svg]:size-[14px]",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
md: "h-6",
|
||||
lg: "h-[30px]",
|
||||
},
|
||||
withIcon: {
|
||||
true: "",
|
||||
false: "",
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
size: "md",
|
||||
withIcon: false,
|
||||
class: "gap-[5px] px-3",
|
||||
},
|
||||
{
|
||||
size: "md",
|
||||
withIcon: true,
|
||||
class: "gap-[5px] pr-3 pl-[5px]",
|
||||
},
|
||||
{
|
||||
size: "lg",
|
||||
withIcon: false,
|
||||
class: "gap-2.5 px-4",
|
||||
},
|
||||
{
|
||||
size: "lg",
|
||||
withIcon: true,
|
||||
class: "gap-2.5 pr-4 pl-2.5",
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
withIcon: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const PillSpinner: FC = () => {
|
||||
return <Spinner loading />;
|
||||
};
|
||||
|
||||
export const Pill: FC<PillProps> = ({
|
||||
icon,
|
||||
type = "inactive",
|
||||
children,
|
||||
size = "md",
|
||||
className,
|
||||
...divProps
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
pillLayoutVariants({ size, withIcon: Boolean(icon) }),
|
||||
pillRoleVariants({ type }),
|
||||
className,
|
||||
)}
|
||||
{...divProps}
|
||||
>
|
||||
{icon}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { FC } from "react";
|
||||
import { Badge, type BadgeProps } from "#/components/Badge/Badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "#/components/Tooltip/Tooltip";
|
||||
import type { ThemeRole } from "#/theme/roles";
|
||||
import { httpStatusColor } from "#/utils/http";
|
||||
|
||||
function themeRoleToBadgeVariant(
|
||||
role: ThemeRole | "muted",
|
||||
): NonNullable<BadgeProps["variant"]> {
|
||||
switch (role) {
|
||||
case "success":
|
||||
return "green";
|
||||
case "error":
|
||||
return "destructive";
|
||||
case "warning":
|
||||
case "danger":
|
||||
return "warning";
|
||||
case "active":
|
||||
case "notice":
|
||||
return "info";
|
||||
case "preview":
|
||||
return "purple";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
}
|
||||
|
||||
interface StatusBadgeProps {
|
||||
code: number;
|
||||
isHttpCode: boolean;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const StatusBadge: FC<StatusBadgeProps> = ({
|
||||
code,
|
||||
isHttpCode,
|
||||
label,
|
||||
}) => {
|
||||
const role = isHttpCode
|
||||
? httpStatusColor(code)
|
||||
: code === 0
|
||||
? "success"
|
||||
: "error";
|
||||
const badge = (
|
||||
<Badge
|
||||
className="text-[10px] h-5 px-2.5 font-semibold"
|
||||
variant={themeRoleToBadgeVariant(role)}
|
||||
>
|
||||
{code.toString()}
|
||||
</Badge>
|
||||
);
|
||||
if (!label) {
|
||||
return badge;
|
||||
}
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{badge}</TooltipTrigger>
|
||||
<TooltipContent>{label}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { FC } from "react";
|
||||
import { Pill } from "#/components/Pill/Pill";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "#/components/Tooltip/Tooltip";
|
||||
import { httpStatusColor } from "#/utils/http";
|
||||
|
||||
interface StatusPillProps {
|
||||
code: number;
|
||||
isHttpCode: boolean;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const StatusPill: FC<StatusPillProps> = ({
|
||||
code,
|
||||
isHttpCode,
|
||||
label,
|
||||
}) => {
|
||||
const pill = (
|
||||
<Pill
|
||||
className="text-[10px] h-5 px-2.5 font-semibold"
|
||||
type={
|
||||
isHttpCode ? httpStatusColor(code) : code === 0 ? "success" : "error"
|
||||
}
|
||||
>
|
||||
{code.toString()}
|
||||
</Pill>
|
||||
);
|
||||
if (!label) {
|
||||
return pill;
|
||||
}
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{pill}</TooltipTrigger>
|
||||
<TooltipContent>{label}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Building2Icon, UserIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import type { HealthMessage, ProvisionerDaemon } from "#/api/typesGenerated";
|
||||
import { Pill } from "#/components/Pill/Pill";
|
||||
import { Badge } from "#/components/Badge/Badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -52,11 +52,12 @@ export const Provisioner: FC<ProvisionerProps> = ({
|
||||
<div className="ml-auto flex flex-wrap gap-3 justify-end">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Pill size="lg" icon={iconScope}>
|
||||
<Badge size="md">
|
||||
{iconScope}
|
||||
<span className="[&::first-letter]:uppercase">
|
||||
{daemonScope}
|
||||
</span>
|
||||
</Pill>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Scope</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CircleCheckIcon, CircleMinusIcon, TagIcon, XIcon } from "lucide-react";
|
||||
import type { ComponentProps, FC } from "react";
|
||||
import { Badge } from "#/components/Badge/Badge";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { Pill } from "#/components/Pill/Pill";
|
||||
|
||||
const parseBool = (s: string): { valid: boolean; value: boolean } => {
|
||||
switch (s.toLowerCase()) {
|
||||
@@ -46,7 +46,7 @@ export const ProvisionerTag: FC<ProvisionerTagProps> = ({
|
||||
onClick={() => {
|
||||
onDelete(tagName);
|
||||
}}
|
||||
className="size-6"
|
||||
className="size-6 -my-1"
|
||||
>
|
||||
<XIcon className="size-icon-xs" />
|
||||
<span className="sr-only">Delete {tagName}</span>
|
||||
@@ -59,39 +59,33 @@ export const ProvisionerTag: FC<ProvisionerTagProps> = ({
|
||||
return <BooleanPill value={boolValue}>{content}</BooleanPill>;
|
||||
}
|
||||
return (
|
||||
<Pill
|
||||
size="lg"
|
||||
icon={<TagIcon className="size-icon-sm" />}
|
||||
data-testid={`tag-${tagName}`}
|
||||
>
|
||||
<Badge variant="outline" size="md" data-testid={`tag-${tagName}`}>
|
||||
<TagIcon className="size-icon-sm" />
|
||||
{content}
|
||||
</Pill>
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
type BooleanPillProps = Omit<ComponentProps<typeof Pill>, "icon" | "value"> & {
|
||||
type BooleanPillProps = Omit<
|
||||
ComponentProps<typeof Badge>,
|
||||
"variant" | "value"
|
||||
> & {
|
||||
value: boolean;
|
||||
};
|
||||
|
||||
const BooleanPill: FC<BooleanPillProps> = ({
|
||||
value,
|
||||
children,
|
||||
...divProps
|
||||
...badgeProps
|
||||
}) => {
|
||||
return (
|
||||
<Pill
|
||||
type={value ? "active" : "danger"}
|
||||
size="lg"
|
||||
icon={
|
||||
value ? (
|
||||
<CircleCheckIcon className="size-icon-sm text-content-link" />
|
||||
) : (
|
||||
<CircleMinusIcon className="size-icon-sm text-content-warning" />
|
||||
)
|
||||
}
|
||||
{...divProps}
|
||||
>
|
||||
<Badge variant={value ? "info" : "warning"} size="md" {...badgeProps}>
|
||||
{value ? (
|
||||
<CircleCheckIcon className="size-icon-sm text-content-link" />
|
||||
) : (
|
||||
<CircleMinusIcon className="size-icon-sm text-content-warning" />
|
||||
)}
|
||||
{children}
|
||||
</Pill>
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { FC, HTMLAttributes } from "react";
|
||||
import { Link as RouterLink } from "react-router";
|
||||
import type { TemplateExample } from "#/api/typesGenerated";
|
||||
import { Badge } from "#/components/Badge/Badge";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import { Pill } from "#/components/Pill/Pill";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
type TemplateExampleCardProps = HTMLAttributes<HTMLDivElement> & {
|
||||
@@ -37,12 +37,12 @@ export const TemplateExampleCard: FC<TemplateExampleCardProps> = ({
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
{example.tags.map((tag) => (
|
||||
<RouterLink key={tag} to={`/starter-templates?tag=${tag}`}>
|
||||
<Pill
|
||||
type={activeTag === tag ? "active" : undefined}
|
||||
<Badge
|
||||
variant={activeTag === tag ? "info" : "default"}
|
||||
className="cursor-pointer no-underline hover:border-content-primary"
|
||||
>
|
||||
{tag}
|
||||
</Pill>
|
||||
</Badge>
|
||||
</RouterLink>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { TemplateVersion, Workspace } from "#/api/typesGenerated";
|
||||
import { Alert, AlertTitle } from "#/components/Alert/Alert";
|
||||
import { Avatar } from "#/components/Avatar/Avatar";
|
||||
import { AvatarData } from "#/components/Avatar/AvatarData";
|
||||
import { Badge } from "#/components/Badge/Badge";
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxButton,
|
||||
@@ -20,7 +21,6 @@ import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import type { SelectFilterOption } from "#/components/Filter/SelectFilter";
|
||||
import { FormFields } from "#/components/Form/Form";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import { Pill } from "#/components/Pill/Pill";
|
||||
import { TemplateUpdateMessage } from "#/modules/templates/TemplateUpdateMessage";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { createDayString } from "#/utils/createDayString";
|
||||
@@ -140,7 +140,7 @@ export const ChangeWorkspaceVersionDialog: FC<
|
||||
</div>
|
||||
{workspace.template_active_version_id ===
|
||||
option.id && (
|
||||
<Pill type="success">Active</Pill>
|
||||
<Badge variant="green">Active</Badge>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
CollapsibleContent,
|
||||
} from "#/components/Collapsible/Collapsible";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import { StatusPill } from "#/components/StatusPill/StatusPill";
|
||||
import { StatusBadge } from "#/components/StatusBadge/StatusBadge";
|
||||
import { TableCell } from "#/components/Table/Table";
|
||||
import { TimelineEntry } from "#/components/Timeline/TimelineEntry";
|
||||
import {
|
||||
@@ -113,7 +113,7 @@ export const AuditLogRow: FC<AuditLogRowProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<StatusPill isHttpCode code={auditLog.status_code} />
|
||||
<StatusBadge isHttpCode code={auditLog.status_code} />
|
||||
|
||||
{/* With multi-org, there is not enough space so show
|
||||
everything in a tooltip. */}
|
||||
|
||||
@@ -5,7 +5,7 @@ import userAgentParser from "ua-parser-js";
|
||||
import type { ConnectionLog } from "#/api/typesGenerated";
|
||||
import { Avatar } from "#/components/Avatar/Avatar";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import { StatusPill } from "#/components/StatusPill/StatusPill";
|
||||
import { StatusBadge } from "#/components/StatusBadge/StatusBadge";
|
||||
import { TableCell } from "#/components/Table/Table";
|
||||
import { TimelineEntry } from "#/components/Timeline/TimelineEntry";
|
||||
import {
|
||||
@@ -64,7 +64,7 @@ export const ConnectionLogRow: FC<ConnectionLogRowProps> = ({
|
||||
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
{code !== undefined && (
|
||||
<StatusPill
|
||||
<StatusBadge
|
||||
code={code}
|
||||
isHttpCode={isWeb}
|
||||
label={isWeb ? "HTTP Status Code" : "SSH Exit Code"}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FC } from "react";
|
||||
import { Pill } from "#/components/Pill/Pill";
|
||||
import { Badge } from "#/components/Badge/Badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -22,9 +22,12 @@ export const OrganizationPills: FC<OrganizationPillsProps> = ({
|
||||
return (
|
||||
<div className="flex flex-row gap-2">
|
||||
{orgs.length > 0 ? (
|
||||
<Pill type={orgs[0].isUUID ? "error" : "muted"} className="w-fit">
|
||||
<Badge
|
||||
variant={orgs[0].isUUID ? "destructive" : "default"}
|
||||
className="w-fit"
|
||||
>
|
||||
{orgs[0].name}
|
||||
</Pill>
|
||||
</Badge>
|
||||
) : (
|
||||
<p>None</p>
|
||||
)}
|
||||
@@ -42,21 +45,21 @@ const OverflowPillList: FC<OverflowPillProps> = ({ organizations }) => {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Pill type="muted" className="w-fit" data-testid="overflow-pill">
|
||||
<Badge className="w-fit" data-testid="overflow-pill">
|
||||
+{organizations.length}
|
||||
</Pill>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="px-4 py-3 border-surface-quaternary">
|
||||
<ul className="flex flex-col gap-2 list-none my-0 pl-0">
|
||||
{organizations.map((organization) => (
|
||||
<li key={organization.name}>
|
||||
<Pill
|
||||
type={organization.isUUID ? "error" : "muted"}
|
||||
<Badge
|
||||
variant={organization.isUUID ? "destructive" : "default"}
|
||||
className="w-fit"
|
||||
>
|
||||
{organization.name}
|
||||
</Pill>
|
||||
</Badge>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FC } from "react";
|
||||
import type { Permission } from "#/api/typesGenerated";
|
||||
import { Pill } from "#/components/Pill/Pill";
|
||||
import { Badge } from "#/components/Badge/Badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -56,10 +56,10 @@ const PermissionsPill: FC<PermissionPillProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<Pill type="muted" className="w-fit">
|
||||
<Badge className="w-fit">
|
||||
<b>{resource}</b>:{" "}
|
||||
{actions.map((p) => `${p.negate ? "!" : ""}${p.action}`).join(", ")}
|
||||
</Pill>
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -75,13 +75,9 @@ const OverflowPermissionPill: FC<OverflowPermissionPillProps> = ({
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Pill
|
||||
type="muted"
|
||||
className="w-fit"
|
||||
data-testid="overflow-permissions-pill"
|
||||
>
|
||||
<Badge className="w-fit" data-testid="overflow-permissions-pill">
|
||||
+{resources.length} more
|
||||
</Pill>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="px-4 py-3 border-surface-quaternary">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FC } from "react";
|
||||
import { Pill } from "#/components/Pill/Pill";
|
||||
import { Badge } from "#/components/Badge/Badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -15,9 +15,12 @@ export const IdpPillList: FC<PillListProps> = ({ roles }) => {
|
||||
return (
|
||||
<div className="flex flex-row gap-2">
|
||||
{roles.length > 0 ? (
|
||||
<Pill className="w-fit" type={isUUID(roles[0]) ? "error" : "muted"}>
|
||||
<Badge
|
||||
className="w-fit"
|
||||
variant={isUUID(roles[0]) ? "destructive" : "default"}
|
||||
>
|
||||
{roles[0]}
|
||||
</Pill>
|
||||
</Badge>
|
||||
) : (
|
||||
<p>None</p>
|
||||
)}
|
||||
@@ -35,18 +38,21 @@ const OverflowPill: FC<OverflowPillProps> = ({ roles }) => {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Pill type="muted" className="w-fit" data-testid="overflow-pill">
|
||||
<Badge className="w-fit" data-testid="overflow-pill">
|
||||
+{roles.length} more
|
||||
</Pill>
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="px-4 py-3 border-surface-quaternary">
|
||||
<ul className="flex flex-col gap-2 list-none my-0 pl-0">
|
||||
{roles.map((role) => (
|
||||
<li key={role}>
|
||||
<Pill className="w-fit" type={isUUID(role) ? "error" : "muted"}>
|
||||
<Badge
|
||||
className="w-fit"
|
||||
variant={isUUID(role) ? "destructive" : "default"}
|
||||
>
|
||||
{role}
|
||||
</Pill>
|
||||
</Badge>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
TemplateVersion,
|
||||
} from "#/api/typesGenerated";
|
||||
import { Avatar } from "#/components/Avatar/Avatar";
|
||||
import { DeprecatedBadge } from "#/components/Badges/Badges";
|
||||
import { Button, Button as ShadcnButton } from "#/components/Button/Button";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
|
||||
@@ -37,7 +38,6 @@ import {
|
||||
PageHeaderSubtitle,
|
||||
PageHeaderTitle,
|
||||
} from "#/components/PageHeader/PageHeader";
|
||||
import { Pill } from "#/components/Pill/Pill";
|
||||
import { linkToTemplate, useLinks } from "#/modules/navigation";
|
||||
import type { WorkspacePermissions } from "#/modules/permissions/workspaces";
|
||||
import { TemplateStats } from "./TemplateStats";
|
||||
@@ -263,7 +263,7 @@ export const TemplatePageHeader: FC<TemplatePageHeaderProps> = ({
|
||||
? template.display_name
|
||||
: template.name}
|
||||
</PageHeaderTitle>
|
||||
{template.deprecated && <Pill type="warning">Deprecated</Pill>}
|
||||
{template.deprecated && <DeprecatedBadge />}
|
||||
</div>
|
||||
|
||||
{template.deprecation_message !== "" ? (
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { FC } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import type { TemplateVersion } from "#/api/typesGenerated";
|
||||
import { Avatar } from "#/components/Avatar/Avatar";
|
||||
import { Badge } from "#/components/Badge/Badge";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { InfoTooltip } from "#/components/InfoTooltip/InfoTooltip";
|
||||
import { Pill } from "#/components/Pill/Pill";
|
||||
import { TableCell } from "#/components/Table/Table";
|
||||
import { TimelineEntry } from "#/components/Timeline/TimelineEntry";
|
||||
import { useAuthenticated } from "#/hooks/useAuthenticated";
|
||||
@@ -62,34 +62,26 @@ export const VersionRow: FC<VersionRowProps> = ({
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
{isActive && (
|
||||
<Pill role="status" type="success">
|
||||
<Badge role="status" variant="green">
|
||||
Active
|
||||
</Pill>
|
||||
)}
|
||||
{isLatest && (
|
||||
<Pill role="status" type="info">
|
||||
Newest
|
||||
</Pill>
|
||||
</Badge>
|
||||
)}
|
||||
{isLatest && <Badge role="status">Newest</Badge>}
|
||||
{jobStatus === "pending" && (
|
||||
<Pill role="status" type="inactive">
|
||||
Pending…
|
||||
</Pill>
|
||||
<Badge role="status">Pending…</Badge>
|
||||
)}
|
||||
{jobStatus === "running" && (
|
||||
<Pill role="status" type="active">
|
||||
<Badge role="status" variant="info">
|
||||
Building…
|
||||
</Pill>
|
||||
</Badge>
|
||||
)}
|
||||
{(jobStatus === "canceling" || jobStatus === "canceled") && (
|
||||
<Pill role="status" type="inactive">
|
||||
Canceled
|
||||
</Pill>
|
||||
<Badge role="status">Canceled</Badge>
|
||||
)}
|
||||
{jobStatus === "failed" && (
|
||||
<Pill role="status" type="error">
|
||||
<Badge role="status" variant="destructive">
|
||||
Failed
|
||||
</Pill>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{jobStatus === "failed" && onArchiveClick && (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { CheckIcon, CircleAlertIcon, HourglassIcon } from "lucide-react";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import type { TemplateVersion } from "#/api/typesGenerated";
|
||||
import { Pill, PillSpinner } from "#/components/Pill/Pill";
|
||||
import type { ThemeRole } from "#/theme/roles";
|
||||
import { Badge, type BadgeProps } from "#/components/Badge/Badge";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import { getPendingStatusLabel } from "#/utils/provisionerJob";
|
||||
|
||||
interface TemplateVersionStatusBadgeProps {
|
||||
@@ -12,61 +12,57 @@ interface TemplateVersionStatusBadgeProps {
|
||||
export const TemplateVersionStatusBadge: FC<
|
||||
TemplateVersionStatusBadgeProps
|
||||
> = ({ version }) => {
|
||||
const { text, icon, type } = getStatus(version);
|
||||
const { text, icon, variant } = getStatus(version);
|
||||
return (
|
||||
<Pill
|
||||
icon={icon}
|
||||
type={type}
|
||||
title={`Build status is ${text}`}
|
||||
role="status"
|
||||
>
|
||||
<Badge variant={variant} title={`Build status is ${text}`} role="status">
|
||||
{icon}
|
||||
{text}
|
||||
</Pill>
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const getStatus = (
|
||||
version: TemplateVersion,
|
||||
): {
|
||||
type?: ThemeRole;
|
||||
variant: NonNullable<BadgeProps["variant"]>;
|
||||
text: string;
|
||||
icon: ReactNode;
|
||||
} => {
|
||||
switch (version.job.status) {
|
||||
case "running":
|
||||
return {
|
||||
type: "active",
|
||||
variant: "info",
|
||||
text: "Running",
|
||||
icon: <PillSpinner />,
|
||||
icon: <Spinner loading />,
|
||||
};
|
||||
case "pending":
|
||||
return {
|
||||
type: "active",
|
||||
variant: "info",
|
||||
text: getPendingStatusLabel(version.job),
|
||||
icon: <HourglassIcon className="size-icon-sm" />,
|
||||
};
|
||||
case "canceling":
|
||||
return {
|
||||
type: "inactive",
|
||||
variant: "default",
|
||||
text: "Canceling",
|
||||
icon: <PillSpinner />,
|
||||
icon: <Spinner loading />,
|
||||
};
|
||||
case "canceled":
|
||||
return {
|
||||
type: "inactive",
|
||||
variant: "default",
|
||||
text: "Canceled",
|
||||
icon: <CircleAlertIcon className="size-icon-sm" />,
|
||||
};
|
||||
case "unknown":
|
||||
case "failed":
|
||||
return {
|
||||
type: "error",
|
||||
variant: "destructive",
|
||||
text: "Failed",
|
||||
icon: <CircleAlertIcon className="size-icon-sm" />,
|
||||
};
|
||||
case "succeeded":
|
||||
return {
|
||||
type: "success",
|
||||
variant: "green",
|
||||
text: "Success",
|
||||
icon: <CheckIcon className="size-icon-sm" />,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FC, type ReactNode, useState } from "react";
|
||||
import type { AlertProps } from "#/components/Alert/Alert";
|
||||
import { Badge } from "#/components/Badge/Badge";
|
||||
import { Button, type ButtonProps } from "#/components/Button/Button";
|
||||
import { Pill } from "#/components/Pill/Pill";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -118,15 +118,15 @@ const NotificationPill: FC<NotificationPillProps> = ({
|
||||
isOpen,
|
||||
}) => {
|
||||
return (
|
||||
<Pill
|
||||
icon={icon}
|
||||
<Badge
|
||||
className={cn(
|
||||
severityStyles[severity].svgColor,
|
||||
isOpen && severityStyles[severity].border,
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
{items.length}
|
||||
</Pill>
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import semver from "semver";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { PillSpinner } from "#/components/Pill/Pill";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import { getPendingStatusLabel } from "./provisionerJob";
|
||||
|
||||
dayjs.extend(duration);
|
||||
@@ -174,7 +174,7 @@ export const getDisplayWorkspaceStatus = (
|
||||
return {
|
||||
text: "Loading",
|
||||
type: "active",
|
||||
icon: <PillSpinner />,
|
||||
icon: <Spinner loading />,
|
||||
} as const;
|
||||
case "running":
|
||||
return {
|
||||
@@ -186,13 +186,13 @@ export const getDisplayWorkspaceStatus = (
|
||||
return {
|
||||
type: "active",
|
||||
text: "Starting",
|
||||
icon: <PillSpinner />,
|
||||
icon: <Spinner loading />,
|
||||
} as const;
|
||||
case "stopping":
|
||||
return {
|
||||
type: "inactive",
|
||||
text: "Stopping",
|
||||
icon: <PillSpinner />,
|
||||
icon: <Spinner loading />,
|
||||
} as const;
|
||||
case "stopped":
|
||||
return {
|
||||
@@ -204,7 +204,7 @@ export const getDisplayWorkspaceStatus = (
|
||||
return {
|
||||
type: "danger",
|
||||
text: "Deleting",
|
||||
icon: <PillSpinner />,
|
||||
icon: <Spinner loading />,
|
||||
} as const;
|
||||
case "deleted":
|
||||
return {
|
||||
@@ -216,7 +216,7 @@ export const getDisplayWorkspaceStatus = (
|
||||
return {
|
||||
type: "inactive",
|
||||
text: "Canceling",
|
||||
icon: <PillSpinner />,
|
||||
icon: <Spinner loading />,
|
||||
} as const;
|
||||
case "canceled":
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user