chore(site): replace CircularProgress with Spinner component (#24276)

This commit is contained in:
Jake Howell
2026-07-15 00:26:43 +10:00
committed by GitHub
parent 57852112a6
commit bf57da58e7
10 changed files with 195 additions and 146 deletions
@@ -1,5 +1,5 @@
import Link from "@mui/material/Link";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Link } from "#/components/Link/Link";
import { FileUpload } from "./FileUpload";
const meta: Meta<typeof FileUpload> = {
@@ -10,8 +10,10 @@ const meta: Meta<typeof FileUpload> = {
description: (
<>
The template has to be a .tar or .zip file. You can also use our{" "}
<Link href="/starter-templates">starter templates</Link> to getting
started with Coder.
<Link href="/starter-templates" showExternalIcon={false}>
starter templates
</Link>{" "}
to getting started with Coder.
</>
),
},
@@ -1,9 +1,9 @@
import CircularProgress from "@mui/material/CircularProgress";
import { CloudUploadIcon, FolderIcon, TrashIcon } from "lucide-react";
import { type DragEvent, type FC, type ReactNode, useRef } from "react";
import { Button } from "#/components/Button/Button";
import { useClickable } from "#/hooks/useClickable";
import { cn } from "#/utils/cn";
import { Spinner } from "../Spinner/Spinner";
interface FileUploadProps {
isUploading: boolean;
@@ -67,7 +67,7 @@ export const FileUpload: FC<FileUploadProps> = ({
<div className="flex flex-col items-center gap-2">
<div className="flex size-16 items-center justify-center">
{isUploading ? (
<CircularProgress size={32} />
<Spinner size="lg" loading />
) : (
<CloudUploadIcon className="size-16" />
)}
@@ -1,5 +1,3 @@
import CircularProgress from "@mui/material/CircularProgress";
import Link from "@mui/material/Link";
import { isAxiosError } from "axios";
import { ExternalLinkIcon } from "lucide-react";
import type { FC } from "react";
@@ -7,6 +5,9 @@ import type { ApiErrorResponse } from "#/api/errors";
import type { ExternalAuthDevice } from "#/api/typesGenerated";
import { Alert, AlertDescription, AlertTitle } from "#/components/Alert/Alert";
import { CopyButton } from "#/components/CopyButton/CopyButton";
import { Link } from "#/components/Link/Link";
import { Loader } from "#/components/Loader/Loader";
import { Spinner } from "../Spinner/Spinner";
interface GitDeviceAuthProps {
externalAuthDevice?: ExternalAuthDevice;
@@ -72,7 +73,7 @@ export const GitDeviceAuth: FC<GitDeviceAuthProps> = ({
}) => {
let status = (
<p className="flex items-center justify-center gap-2 text-content-disabled">
<CircularProgress size={16} color="secondary" data-pixel="ignore" />
<Spinner size="sm" loading />
Checking for authentication...
</p>
);
@@ -125,14 +126,14 @@ export const GitDeviceAuth: FC<GitDeviceAuthProps> = ({
}
if (!externalAuthDevice) {
return <CircularProgress />;
return <Loader />;
}
return (
<div>
<p className="m-0 text-center text-base leading-relaxed text-content-secondary">
Copy your one-time code:&nbsp;
<div className="inline-flex items-center">
<span className="inline-flex items-center">
<span className="font-bold text-content-primary">
{externalAuthDevice.user_code}
</span>
@@ -141,7 +142,7 @@ export const GitDeviceAuth: FC<GitDeviceAuthProps> = ({
text={externalAuthDevice.user_code}
label="Copy user code"
/>
</div>
</span>
<br />
Then open the link below and paste it:
</p>
@@ -151,6 +152,7 @@ export const GitDeviceAuth: FC<GitDeviceAuthProps> = ({
href={externalAuthDevice.verification_uri}
target="_blank"
rel="noreferrer"
showExternalIcon={false}
>
<ExternalLinkIcon className="size-icon-xs" />
Open and Paste
+71 -42
View File
@@ -1,10 +1,8 @@
import { useTheme } from "@emotion/react";
import CircularProgress, {
type CircularProgressProps,
} from "@mui/material/CircularProgress";
import { type FC, type ReactNode, useMemo } from "react";
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";
@@ -14,7 +12,72 @@ type PillProps = React.ComponentPropsWithRef<"div"> & {
size?: "md" | "lg";
};
const PILL_ICON_SIZE = 14;
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: "",
},
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 py-3.5 px-4",
},
{
size: "lg",
withIcon: true,
class: "gap-2.5 py-3.5 pr-4 pl-2.5",
},
],
defaultVariants: {
size: "md",
withIcon: false,
},
},
);
export const PillSpinner: FC = () => {
return <Spinner loading />;
};
export const Pill: FC<PillProps> = ({
icon,
@@ -22,37 +85,15 @@ export const Pill: FC<PillProps> = ({
children,
size = "md",
className,
style,
...divProps
}) => {
const theme = useTheme();
const roleColors = useMemo(() => {
if (type === "muted") {
return undefined;
}
const palette = theme.roles[type];
return {
backgroundColor: palette.background,
borderColor: palette.outline,
color: palette.text,
};
}, [theme, type]);
return (
<div
className={cn(
"inline-flex items-center whitespace-nowrap rounded-full border border-solid",
"font-normal text-xs leading-none cursor-default",
"[&>svg]:size-[14px]",
type === "muted" &&
"bg-surface-tertiary border-border-secondary text-content-secondary",
size === "md" && "h-6 gap-[5px] px-3",
Boolean(icon) && size === "md" && "pl-[5px]",
size === "lg" && "h-[30px] gap-[10px] px-4",
Boolean(icon) && size === "lg" && "pl-[10px]",
pillLayoutVariants({ size, withIcon: Boolean(icon) }),
pillRoleVariants({ type }),
className,
)}
style={{ ...roleColors, ...style }}
{...divProps}
>
{icon}
@@ -60,15 +101,3 @@ export const Pill: FC<PillProps> = ({
</div>
);
};
export const PillSpinner: FC<CircularProgressProps> = (props) => {
const theme = useTheme();
return (
<CircularProgress
size={PILL_ICON_SIZE}
sx={{ "& svg": { transform: "scale(.75)" } }}
style={{ color: theme.experimental.l1.text }}
{...props}
/>
);
};
@@ -38,11 +38,8 @@ export const TemplateExampleCard: FC<TemplateExampleCardProps> = ({
{example.tags.map((tag) => (
<RouterLink key={tag} to={`/starter-templates?tag=${tag}`}>
<Pill
className={cn(
"cursor-pointer border-border",
"hover:border-content-primary",
activeTag === tag && "!border-border-pending !bg-surface-sky",
)}
type={activeTag === tag ? "active" : undefined}
className="cursor-pointer no-underline hover:border-content-primary"
>
{tag}
</Pill>
@@ -1,21 +1,29 @@
import { css } from "@emotion/css";
import Autocomplete from "@mui/material/Autocomplete";
import CircularProgress from "@mui/material/CircularProgress";
import TextField from "@mui/material/TextField";
import { InfoIcon } from "lucide-react";
import { type FC, useState } from "react";
import { type FC, useMemo, useState } from "react";
import { useQuery } from "react-query";
import { templateVersions } from "#/api/queries/templates";
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 {
Combobox,
ComboboxButton,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from "#/components/Combobox/Combobox";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import type { DialogProps } from "#/components/Dialogs/Dialog";
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";
type ChangeWorkspaceVersionDialogProps = DialogProps & {
@@ -31,14 +39,30 @@ export const ChangeWorkspaceVersionDialog: FC<
...templateVersions(workspace.template_id),
select: (data) => [...data].reverse(),
});
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);
const [newVersion, setNewVersion] = useState<TemplateVersion>();
const currentVersion = versions?.find(
(v) => workspace.latest_build.template_version_id === v.id,
);
const [newVersion, setNewVersion] = useState<TemplateVersion>();
const validVersions = versions?.filter((v) => v.job.status === "succeeded");
const selectedVersion = newVersion || currentVersion;
const selectedOption: SelectFilterOption | undefined = useMemo(() => {
if (!selectedVersion) {
return undefined;
}
return {
value: selectedVersion.id,
label: selectedVersion.name,
startIcon: (
<Avatar
size="sm"
src={selectedVersion.created_by.avatar_url}
fallback={selectedVersion.name}
/>
),
};
}, [selectedVersion]);
return (
<ConfirmDialog
{...dialogProps}
@@ -59,74 +83,75 @@ export const ChangeWorkspaceVersionDialog: FC<
{validVersions ? (
<>
<FormFields>
<Autocomplete
disableClearable
options={validVersions}
defaultValue={selectedVersion}
id="template-version-autocomplete"
open={isAutocompleteOpen}
onChange={(_, newTemplateVersion) => {
setNewVersion(newTemplateVersion);
<Combobox
value={selectedVersion?.id}
onValueChange={(id) => {
if (!id) {
// Ignore deselection; a version must
// always be selected.
return;
}
const next = validVersions.find((v) => v.id === id);
setNewVersion(next);
}}
onOpen={() => {
setIsAutocompleteOpen(true);
}}
onClose={() => {
setIsAutocompleteOpen(false);
}}
isOptionEqualToValue={(
option: TemplateVersion,
value: TemplateVersion,
) => option.id === value.id}
getOptionLabel={(option) => option.name}
renderOption={(props, option: TemplateVersion) => (
<li {...props}>
<AvatarData
avatar={
<Avatar
src={option.created_by.avatar_url}
fallback={option.name}
>
<ComboboxTrigger asChild>
<ComboboxButton
id="template-version-autocomplete"
aria-label="Template version"
selectedOption={selectedOption}
placeholder="Template version name"
className="w-full min-w-0 pl-3.5"
/>
</ComboboxTrigger>
<ComboboxContent
className="max-w-none min-w-[min(100%,320px)]"
align="start"
>
<ComboboxInput placeholder="Search versions…" />
<ComboboxList>
{validVersions.map((option) => (
<ComboboxItem
key={option.id}
value={option.id}
keywords={[option.name]}
className={cn(
"px-3 py-2 font-normal",
"data-[selected=true]:bg-surface-tertiary",
)}
>
<AvatarData
avatar={
<Avatar
src={option.created_by.avatar_url}
fallback={option.name}
/>
}
title={
<div className="flex w-full flex-row items-center justify-between gap-2">
<div className="flex flex-row items-center gap-2">
{option.name}
{option.message && (
<InfoIcon
aria-hidden="true"
className="size-icon-xs"
/>
)}
</div>
{workspace.template_active_version_id ===
option.id && (
<Pill type="success">Active</Pill>
)}
</div>
}
subtitle={createDayString(option.created_at)}
/>
}
title={
<div className="flex flex-row justify-between gap-4 w-full">
<div className="flex flex-row items-center gap-2">
{option.name}
{option.message && (
<InfoIcon
aria-hidden="true"
className="size-icon-xs"
/>
)}
</div>
{workspace.template_active_version_id ===
option.id && <Pill type="success">Active</Pill>}
</div>
}
subtitle={createDayString(option.created_at)}
/>
</li>
)}
renderInput={(params) => (
<>
<TextField
{...params}
fullWidth
placeholder="Template version name"
InputProps={{
...params.InputProps,
endAdornment: (
<>
{!versions && <CircularProgress size={16} />}
{params.InputProps.endAdornment}
</>
),
classes: { root: classNames.root },
}}
/>
</>
)}
/>
</ComboboxItem>
))}
</ComboboxList>
<ComboboxEmpty>No template versions found</ComboboxEmpty>
</ComboboxContent>
</Combobox>
</FormFields>
{selectedVersion && (
<>
@@ -151,10 +176,3 @@ export const ChangeWorkspaceVersionDialog: FC<
/>
);
};
const classNames = {
// Same `padding-left` as input
root: css`
padding-left: 14px !important;
`,
};
+2 -2
View File
@@ -1,4 +1,3 @@
import CircularProgress from "@mui/material/CircularProgress";
import kebabCase from "lodash/fp/kebabCase";
import { BellOffIcon, RotateCcwIcon } from "lucide-react";
import { type FC, Suspense } from "react";
@@ -9,6 +8,7 @@ import type { HealthSeverity } from "#/api/typesGenerated";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Button } from "#/components/Button/Button";
import { Loader } from "#/components/Loader/Loader";
import { Spinner } from "#/components/Spinner/Spinner";
import {
Tooltip,
TooltipContent,
@@ -92,7 +92,7 @@ export const HealthLayout: FC = () => {
}}
>
{isRefreshing ? (
<CircularProgress size={16} />
<Spinner size="sm" loading />
) : (
<RotateCcwIcon className="size-5" />
)}
@@ -1,9 +1,9 @@
import CircularProgress from "@mui/material/CircularProgress";
import type { FC } from "react";
import type { GitSSHKey } from "#/api/typesGenerated";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Button } from "#/components/Button/Button";
import { CodeExample } from "#/components/CodeExample/CodeExample";
import { Spinner } from "#/components/Spinner/Spinner";
interface SSHKeysPageViewProps {
isLoading: boolean;
@@ -21,7 +21,7 @@ export const SSHKeysPageView: FC<SSHKeysPageViewProps> = ({
if (isLoading) {
return (
<div className="p-8">
<CircularProgress size={26} />
<Spinner size="lg" loading />
</div>
);
}
@@ -34,11 +34,11 @@ export const SSHKeysPageView: FC<SSHKeysPageViewProps> = ({
{sshKey && (
<>
<p className="leading-relaxed font-normal text-sm text-content-secondary m-0">
<p className="m-0 text-sm text-content-secondary">
The following public key is used to authenticate Git in workspaces.
You may add it to Git services (such as GitHub) that you need to
access from your workspace. Coder configures authentication via{" "}
<code className="bg-surface-quaternary text-xs py-0.5 px-1 text-content-primary rounded-sm">
<code className="rounded-sm border border-border bg-surface-secondary px-1 py-0.5 text-xs text-content-primary">
$GIT_SSH_COMMAND
</code>
.
@@ -17,9 +17,11 @@ export type NotificationItem = {
actions?: ReactNode;
};
type NotificationSeverity = "warning" | "info";
type NotificationsProps = {
items: NotificationItem[];
severity: ThemeRole;
severity: NotificationSeverity;
icon: ReactNode;
};
-1
View File
@@ -172,7 +172,6 @@ export default defineConfig({
"@mui/material/CardActionArea",
"@mui/material/CardContent",
"@mui/material/Checkbox",
"@mui/material/CircularProgress",
"@mui/material/Collapse",
"@mui/material/CssBaseline",
"@mui/material/Dialog",