mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: de-mui <Skeleton /> component (#24278)
Replaces remaining MUI `<Skeleton />` usages with our shared `Skeleton` component to keep loading states consistent and reduce MUI/Emotion coupling. Also cleans up a few adjacent touched styles while preserving existing behavior. - Replace `@mui/material/Skeleton` with `#/components/Skeleton/Skeleton` - Remove a few MUI-specific skeleton patterns at callsites - De-mui adjacent Emotion styles - Simplify filter layout handling with class-based responsive wrapping
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Skeleton, { type SkeletonProps } from "@mui/material/Skeleton";
|
||||
import type { Breakpoint } from "@mui/system/createTheme";
|
||||
import { ExternalLinkIcon, SlidersHorizontal } from "lucide-react";
|
||||
import { type FC, type ReactNode, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
type ComponentProps,
|
||||
type FC,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
getValidationErrorMessage,
|
||||
hasError,
|
||||
@@ -19,7 +23,9 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "#/components/DropdownMenu/DropdownMenu";
|
||||
import { SearchField } from "#/components/SearchField/SearchField";
|
||||
import { Skeleton, type SkeletonProps } from "#/components/Skeleton/Skeleton";
|
||||
import { useDebouncedFunction } from "#/hooks/debounce";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
type PresetFilter = {
|
||||
name: string;
|
||||
@@ -132,13 +138,9 @@ const stringifyFilter = (filterValue: FilterValues): string => {
|
||||
const BaseSkeleton: FC<SkeletonProps> = ({ children, ...skeletonProps }) => {
|
||||
return (
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={36}
|
||||
{...skeletonProps}
|
||||
css={(theme) => ({
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: "6px",
|
||||
})}
|
||||
className="bg-surface-tertiary rounded-md w-52"
|
||||
>
|
||||
{children}
|
||||
</Skeleton>
|
||||
@@ -149,7 +151,7 @@ export const MenuSkeleton: FC = () => {
|
||||
return <BaseSkeleton className="min-w-[200px] shrink-0" />;
|
||||
};
|
||||
|
||||
type FilterProps = {
|
||||
type FilterProps = ComponentProps<"div"> & {
|
||||
filter: ReturnType<typeof useFilter>;
|
||||
optionsSkeleton: ReactNode;
|
||||
isLoading: boolean;
|
||||
@@ -159,13 +161,6 @@ type FilterProps = {
|
||||
error?: unknown;
|
||||
options?: ReactNode;
|
||||
presets: PresetFilter[];
|
||||
|
||||
/**
|
||||
* The CSS media query breakpoint that defines when the UI will try
|
||||
* displaying all options on one row, regardless of the number of options
|
||||
* present
|
||||
*/
|
||||
singleRowBreakpoint?: Breakpoint;
|
||||
};
|
||||
|
||||
export const Filter: FC<FilterProps> = ({
|
||||
@@ -178,9 +173,9 @@ export const Filter: FC<FilterProps> = ({
|
||||
learnMoreLabel2,
|
||||
learnMoreLink2,
|
||||
presets,
|
||||
singleRowBreakpoint = "lg",
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
// Storing local copy of the filter query so that it can be updated more
|
||||
// aggressively without re-renders rippling out to the rest of the app every
|
||||
// single time. Exists for performance reasons - not really a good way to
|
||||
@@ -205,16 +200,8 @@ export const Filter: FC<FilterProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
flexWrap: "wrap",
|
||||
|
||||
[theme.breakpoints.up(singleRowBreakpoint)]: {
|
||||
flexWrap: "nowrap",
|
||||
},
|
||||
}}
|
||||
className={cn("flex gap-2 flex-wrap lg:flex-nowrap mb-4", className)}
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import type { FC } from "react";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
type PaginationHeaderProps = {
|
||||
paginationUnitLabel: string;
|
||||
@@ -22,23 +22,15 @@ export const PaginationAmount: FC<PaginationHeaderProps> = ({
|
||||
countIsCapped,
|
||||
className,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
flexFlow: "row nowrap",
|
||||
alignItems: "center",
|
||||
margin: 0,
|
||||
fontSize: "13px",
|
||||
color: theme.palette.text.secondary,
|
||||
height: "36px", // The size of a small button
|
||||
"& strong": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
className={className}
|
||||
className={cn(
|
||||
"flex flex-row flex-nowrap items-center m-0",
|
||||
"text-[13px] text-content-secondary",
|
||||
"h-9", // The size of a small button
|
||||
"[&_strong]:text-content-primary",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{totalRecords !== undefined ? (
|
||||
<>
|
||||
|
||||
@@ -18,7 +18,7 @@ const skeletonVariants = cva("bg-surface-tertiary animate-pulse", {
|
||||
},
|
||||
});
|
||||
|
||||
type SkeletonProps = React.ComponentProps<"div"> &
|
||||
export type SkeletonProps = React.ComponentProps<"div"> &
|
||||
VariantProps<typeof skeletonVariants> & {
|
||||
/** Width in pixels (number) or any CSS value (string). */
|
||||
width?: number | string;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import { type FC, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "#/components/DropdownMenu/DropdownMenu";
|
||||
import { Latency } from "#/components/Latency/Latency";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import type { ProxyContextValue } from "#/contexts/ProxyContext";
|
||||
import { useAuthenticated } from "#/hooks/useAuthenticated";
|
||||
import { sortProxiesByLatency } from "./proxyUtils";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import { Container, ExternalLinkIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { useMutation, useQueryClient } from "react-query";
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "#/components/Dialog/Dialog";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -374,18 +374,8 @@ export const AgentDevcontainerCard: FC<AgentDevcontainerCardProps> = ({
|
||||
|
||||
{showSubAgentAppsPlaceholders && (
|
||||
<section className={appsClasses}>
|
||||
<Skeleton
|
||||
width={80}
|
||||
height={32}
|
||||
variant="rectangular"
|
||||
className="rounded"
|
||||
/>
|
||||
<Skeleton
|
||||
width={110}
|
||||
height={32}
|
||||
variant="rectangular"
|
||||
className="rounded"
|
||||
/>
|
||||
<Skeleton width={80} height={32} className="rounded" />
|
||||
<Skeleton width={110} height={32} className="rounded" />
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
type FC,
|
||||
@@ -15,6 +14,7 @@ import type {
|
||||
WorkspaceAgent,
|
||||
WorkspaceAgentMetadata,
|
||||
} from "#/api/typesGenerated";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import { Stack } from "#/components/Stack/Stack";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -141,18 +141,18 @@ const AgentMetadataSkeleton: FC = () => {
|
||||
return (
|
||||
<Stack alignItems="baseline" direction="row" spacing={6}>
|
||||
<div className="leading-relaxed flex flex-col overflow-visible flex-shrink-0">
|
||||
<Skeleton width={40} height={12} variant="text" />
|
||||
<Skeleton width={65} height={14} variant="text" />
|
||||
<Skeleton width={40} height={6} variant="text" />
|
||||
<Skeleton width={65} height={8} variant="text" />
|
||||
</div>
|
||||
|
||||
<div className="leading-relaxed flex flex-col overflow-visible flex-shrink-0">
|
||||
<Skeleton width={40} height={12} variant="text" />
|
||||
<Skeleton width={65} height={14} variant="text" />
|
||||
<Skeleton width={40} height={6} variant="text" />
|
||||
<Skeleton width={65} height={8} variant="text" />
|
||||
</div>
|
||||
|
||||
<div className="leading-relaxed flex flex-col overflow-visible flex-shrink-0">
|
||||
<Skeleton width={40} height={12} variant="text" />
|
||||
<Skeleton width={65} height={14} variant="text" />
|
||||
<Skeleton width={40} height={6} variant="text" />
|
||||
<Skeleton width={65} height={8} variant="text" />
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Collapse from "@mui/material/Collapse";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import {
|
||||
CopyIcon,
|
||||
EllipsisIcon,
|
||||
@@ -37,6 +36,7 @@ import {
|
||||
} from "#/components/DropdownMenu/DropdownMenu";
|
||||
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
|
||||
import type { Line } from "#/components/Logs/LogLine";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
@@ -426,18 +426,8 @@ export const AgentRow: FC<AgentRowProps> = ({
|
||||
|
||||
{agent.status === "connecting" && !isExternalAgent && (
|
||||
<section className="flex flex-wrap gap-4 [&:empty]:hidden">
|
||||
<Skeleton
|
||||
width={80}
|
||||
height={32}
|
||||
variant="rectangular"
|
||||
className="rounded"
|
||||
/>
|
||||
<Skeleton
|
||||
width={110}
|
||||
height={32}
|
||||
variant="rectangular"
|
||||
className="rounded"
|
||||
/>
|
||||
<Skeleton width={80} height={32} className="rounded" />
|
||||
<Skeleton width={110} height={32} className="rounded" />
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import type { WorkspaceBuild } from "#/api/typesGenerated";
|
||||
import { BuildIcon } from "#/components/BuildIcon/BuildIcon";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "#/components/Tooltip/Tooltip";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { createDayString } from "#/utils/createDayString";
|
||||
import {
|
||||
buildReasonLabels,
|
||||
@@ -21,26 +22,20 @@ export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => {
|
||||
const statusType = getDisplayWorkspaceBuildStatus(theme, build).type;
|
||||
|
||||
return (
|
||||
<div css={styles.root}>
|
||||
<div className="flex flex-row items-center gap-3 leading-normal">
|
||||
<BuildIcon
|
||||
transition={build.transition}
|
||||
className="size-4"
|
||||
css={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
color: theme.roles[statusType].fill.solid,
|
||||
}}
|
||||
/>
|
||||
<div className="overflow-hidden">
|
||||
<div className="overflow-hidden flex flex-col">
|
||||
<div
|
||||
css={{
|
||||
color: theme.palette.text.primary,
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
}}
|
||||
className={cn(
|
||||
"text-content-primary text-ellipsis overflow-hidden",
|
||||
"whitespace-nowrap flex items-center gap-1",
|
||||
)}
|
||||
>
|
||||
<span className="capitalize">{build.transition}</span> by{" "}
|
||||
<span className="font-medium">
|
||||
@@ -63,12 +58,7 @@ export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => {
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
<div className="text-xs font-normal text-content-secondary">
|
||||
{createDayString(build.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,22 +68,12 @@ export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => {
|
||||
|
||||
export const WorkspaceBuildDataSkeleton = () => {
|
||||
return (
|
||||
<div css={styles.root}>
|
||||
<div className="flex flex-row items-center gap-3 leading-normal">
|
||||
<Skeleton variant="circular" width={16} height={16} />
|
||||
<div>
|
||||
<Skeleton variant="text" width={94} height={16} />
|
||||
<Skeleton variant="text" width={60} height={14} className="mt-0.5" />
|
||||
<div className="flex flex-col">
|
||||
<Skeleton variant="text" width={94} height={10} />
|
||||
<Skeleton variant="text" width={60} height={8} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
root: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
lineHeight: "1.5",
|
||||
},
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import { saveAs } from "file-saver";
|
||||
import JSZip from "jszip";
|
||||
import { type FC, useEffect, useMemo, useRef, useState } from "react";
|
||||
@@ -13,7 +11,9 @@ import {
|
||||
ConfirmDialog,
|
||||
type ConfirmDialogProps,
|
||||
} from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import { Stack } from "#/components/Stack/Stack";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
type DownloadLogsDialogProps = Pick<
|
||||
ConfirmDialogProps,
|
||||
@@ -34,8 +34,6 @@ export const DownloadLogsDialog: FC<DownloadLogsDialogProps> = ({
|
||||
onClose,
|
||||
download = saveAs,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const buildLogsQuery = useQuery({
|
||||
...buildLogs(workspace),
|
||||
enabled: open,
|
||||
@@ -132,7 +130,7 @@ export const DownloadLogsDialog: FC<DownloadLogsDialogProps> = ({
|
||||
|
||||
downloadTimeoutIdRef.current = window.setTimeout(() => {
|
||||
setIsDownloading(false);
|
||||
}, theme.transitions.duration.leavingScreen);
|
||||
}, 200);
|
||||
} catch (error) {
|
||||
setIsDownloading(false);
|
||||
toast.error(`Error downloading workspace "${workspace.name}" logs.`, {
|
||||
@@ -155,7 +153,7 @@ export const DownloadLogsDialog: FC<DownloadLogsDialogProps> = ({
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<ul css={styles.list}>
|
||||
<ul className="list-none p-0 m-0 flex flex-col gap-2">
|
||||
{allFiles.map((f) => (
|
||||
<DownloadingItem
|
||||
key={f.name}
|
||||
@@ -177,7 +175,6 @@ type DownloadingItemProps = Readonly<{
|
||||
}>;
|
||||
|
||||
const DownloadingItem: FC<DownloadingItemProps> = ({ file, giveUpTimeMs }) => {
|
||||
const theme = useTheme();
|
||||
const [isWaiting, setIsWaiting] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -197,24 +194,34 @@ const DownloadingItem: FC<DownloadingItemProps> = ({ file, giveUpTimeMs }) => {
|
||||
const { baseName, fileExtension } = extractFileNameInfo(file.name);
|
||||
|
||||
return (
|
||||
<li css={styles.listItem}>
|
||||
<li className="w-full flex justify-between items-center gap-x-8">
|
||||
<span
|
||||
css={[
|
||||
styles.listItemPrimary,
|
||||
!isWaiting && { color: theme.palette.text.disabled },
|
||||
]}
|
||||
className={cn(
|
||||
"font-medium text-content-primary",
|
||||
"flex flex-row flex-nowrap gap-x-0 overflow-hidden",
|
||||
!isWaiting && "text-content-disabled",
|
||||
)}
|
||||
>
|
||||
<span css={styles.listItemPrimaryBaseName}>{baseName}</span>
|
||||
<span css={styles.listItemPrimaryFileExtension}>.{fileExtension}</span>
|
||||
<span className="min-w-0 flex-shrink overflow-hidden text-ellipsis">
|
||||
{baseName}
|
||||
</span>
|
||||
<span className="flex-shrink-0">.{fileExtension}</span>
|
||||
</span>
|
||||
|
||||
<span css={styles.listItemSecondary}>
|
||||
<span className="flex-shrink-0 text-sm whitespace-nowrap">
|
||||
{file.blob ? (
|
||||
humanBlobSize(file.blob.size)
|
||||
) : isWaiting ? (
|
||||
<Skeleton variant="text" width={48} height={12} />
|
||||
) : (
|
||||
<p css={styles.notAvailableText}>Not available</p>
|
||||
<p
|
||||
className={cn(
|
||||
"flex flex-row flex-nowrap items-center gap-x-1",
|
||||
"text-content-disabled",
|
||||
)}
|
||||
>
|
||||
Not available
|
||||
</p>
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
@@ -264,56 +271,3 @@ function extractFileNameInfo(filename: string): FileNameInfo {
|
||||
fileExtension: filename.slice(periodIndex + 1),
|
||||
};
|
||||
}
|
||||
|
||||
const styles = {
|
||||
list: {
|
||||
listStyle: "none",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
},
|
||||
|
||||
listItem: {
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
columnGap: "32px",
|
||||
},
|
||||
|
||||
listItemPrimary: (theme) => ({
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.primary,
|
||||
display: "flex",
|
||||
flexFlow: "row nowrap",
|
||||
columnGap: 0,
|
||||
overflow: "hidden",
|
||||
}),
|
||||
|
||||
listItemPrimaryBaseName: {
|
||||
minWidth: 0,
|
||||
flexShrink: 1,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
},
|
||||
|
||||
listItemPrimaryFileExtension: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
|
||||
listItemSecondary: {
|
||||
flexShrink: 0,
|
||||
fontSize: 14,
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
|
||||
notAvailableText: (theme) => ({
|
||||
display: "flex",
|
||||
flexFlow: "row nowrap",
|
||||
alignItems: "center",
|
||||
columnGap: "4px",
|
||||
color: theme.palette.text.disabled,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Link from "@mui/material/Link";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import { InfoIcon, RotateCcwIcon } from "lucide-react";
|
||||
import { type FC, type ReactNode, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
HelpPopoverTitle,
|
||||
HelpPopoverTrigger,
|
||||
} from "#/components/HelpPopover/HelpPopover";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import { linkToTemplate, useLinks } from "#/modules/navigation";
|
||||
import {
|
||||
useWorkspaceUpdate,
|
||||
@@ -40,13 +40,22 @@ export const WorkspaceOutdatedTooltip: FC<WorkspaceOutdatedTooltipProps> = ({
|
||||
{children ? (
|
||||
<HelpPopoverTrigger asChild>
|
||||
<span className="flex items-center gap-1.5 cursor-help">
|
||||
<InfoIcon css={styles.icon} size={14} />
|
||||
<InfoIcon
|
||||
css={(theme) => ({
|
||||
color: theme.roles.notice.outline,
|
||||
})}
|
||||
size={14}
|
||||
/>
|
||||
<span>{children}</span>
|
||||
</span>
|
||||
</HelpPopoverTrigger>
|
||||
) : (
|
||||
<HelpPopoverIconTrigger size="small" hoverEffect={false}>
|
||||
<InfoIcon css={styles.icon} />
|
||||
<InfoIcon
|
||||
css={(theme) => ({
|
||||
color: theme.roles.notice.outline,
|
||||
})}
|
||||
/>
|
||||
<span className="sr-only">Outdated info</span>
|
||||
</HelpPopoverIconTrigger>
|
||||
)}
|
||||
@@ -92,9 +101,11 @@ const WorkspaceOutdatedTooltipContent: FC<TooltipContentProps> = ({
|
||||
This workspace version is outdated and a newer version is available.
|
||||
</HelpPopoverText>
|
||||
|
||||
<div css={styles.container}>
|
||||
<div className="flex flex-col gap-2 py-2 text-[13px]">
|
||||
<div className="leading-[1.6]">
|
||||
<div css={styles.bold}>New version</div>
|
||||
<div className="text-content-primary font-semibold">
|
||||
New version
|
||||
</div>
|
||||
<div>
|
||||
{activeVersion ? (
|
||||
<Link
|
||||
@@ -111,7 +122,7 @@ const WorkspaceOutdatedTooltipContent: FC<TooltipContentProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="leading-[1.6]">
|
||||
<div css={styles.bold}>Message</div>
|
||||
<div className="text-content-primary font-semibold">Message</div>
|
||||
<div>
|
||||
{activeVersion ? (
|
||||
activeVersion.message || "No message"
|
||||
@@ -135,23 +146,3 @@ const WorkspaceOutdatedTooltipContent: FC<TooltipContentProps> = ({
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
icon: (theme) => ({
|
||||
color: theme.roles.notice.outline,
|
||||
}),
|
||||
|
||||
container: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 8,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
fontSize: 13,
|
||||
},
|
||||
|
||||
bold: (theme) => ({
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 600,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Collapse from "@mui/material/Collapse";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import sortBy from "lodash/sortBy";
|
||||
import uniqBy from "lodash/uniqBy";
|
||||
import { type FC, useState } from "react";
|
||||
@@ -10,6 +9,7 @@ import type {
|
||||
} from "#/api/typesGenerated";
|
||||
import { ChevronDownIcon } from "#/components/AnimatedIcons/ChevronDown";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import {
|
||||
calcDuration,
|
||||
formatTime,
|
||||
|
||||
+6
-31
@@ -1,6 +1,5 @@
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import MuiLink from "@mui/material/Link";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import { PlusIcon, RotateCwIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import Confetti from "react-confetti";
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
SettingsHeaderDescription,
|
||||
SettingsHeaderTitle,
|
||||
} from "#/components/SettingsHeader/SettingsHeader";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import { Stack } from "#/components/Stack/Stack";
|
||||
import {
|
||||
@@ -112,9 +112,7 @@ const LicensesSettingsPageView: FC<Props> = ({
|
||||
</Stack>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
{isLoading && (
|
||||
<Skeleton className="rounded" variant="rectangular" height={78} />
|
||||
)}
|
||||
{isLoading && <Skeleton height={78} />}
|
||||
|
||||
{!isLoading && licenses && licenses?.length > 0 && (
|
||||
<Stack spacing={4} className="licenses">
|
||||
@@ -139,13 +137,13 @@ const LicensesSettingsPageView: FC<Props> = ({
|
||||
)}
|
||||
|
||||
{!isLoading && licenses?.length === 0 && (
|
||||
<div css={styles.root}>
|
||||
<div className="min-h-[240px] flex items-center justify-center rounded-lg border border-solid border-border p-12">
|
||||
<Stack alignItems="center" spacing={1}>
|
||||
<Stack alignItems="center" spacing={0.5}>
|
||||
<span css={styles.title}>
|
||||
<span className="text-base">
|
||||
You don't have any licenses!
|
||||
</span>
|
||||
<span css={styles.description}>
|
||||
<span className="text-content-secondary text-center max-w-[464px] mt-2">
|
||||
You're missing out on high availability, RBAC, quotas,
|
||||
and much more. Contact{" "}
|
||||
<MuiLink href="mailto:sales@coder.com">sales</MuiLink> or{" "}
|
||||
@@ -195,27 +193,4 @@ const LicensesSettingsPageView: FC<Props> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
title: {
|
||||
fontSize: 16,
|
||||
},
|
||||
|
||||
root: (theme) => ({
|
||||
minHeight: 240,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
padding: 48,
|
||||
}),
|
||||
|
||||
description: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
textAlign: "center",
|
||||
maxWidth: 464,
|
||||
marginTop: 8,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export default LicensesSettingsPageView;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import { ChevronRightIcon, PlusIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { Link as RouterLink, useNavigate } from "react-router";
|
||||
@@ -12,6 +10,7 @@ import { Button } from "#/components/Button/Button";
|
||||
import { ChooseOne, Cond } from "#/components/Conditionals/ChooseOne";
|
||||
import { EmptyState } from "#/components/EmptyState/EmptyState";
|
||||
import { PaywallPremium } from "#/components/Paywall/PaywallPremium";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -154,7 +153,7 @@ const GroupRow: FC<GroupRowProps> = ({ group }) => {
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div css={styles.arrowCell}>
|
||||
<div className="flex">
|
||||
<ChevronRightIcon className="size-icon-sm" />
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -181,14 +180,3 @@ const TableLoader: FC = () => {
|
||||
</TableLoaderSkeleton>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
arrowRight: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
width: 20,
|
||||
height: 20,
|
||||
}),
|
||||
arrowCell: {
|
||||
display: "flex",
|
||||
},
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import { BellIcon, BellOffIcon } from "lucide-react";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { toast } from "sonner";
|
||||
import { healthSettings, updateHealthSettings } from "#/api/queries/debug";
|
||||
import type { HealthSection } from "#/api/typesGenerated";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
|
||||
export const DismissWarningButton = (props: { healthcheck: HealthSection }) => {
|
||||
@@ -18,14 +18,7 @@ export const DismissWarningButton = (props: { healthcheck: HealthSection }) => {
|
||||
const dismissMutation = useMutation(updateHealthSettings(queryClient));
|
||||
|
||||
if (!healthSettingsQuery.data) {
|
||||
return (
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={36}
|
||||
width={170}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
);
|
||||
return <Skeleton height={36} width={170} className="rounded-lg" />;
|
||||
}
|
||||
|
||||
const { dismissed_healthchecks } = healthSettingsQuery.data;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import { EllipsisVertical, PlusIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { Link as RouterLink, useNavigate } from "react-router";
|
||||
@@ -14,6 +12,7 @@ import {
|
||||
} from "#/components/DropdownMenu/DropdownMenu";
|
||||
import { EmptyState } from "#/components/EmptyState/EmptyState";
|
||||
import { PaywallPremium } from "#/components/Paywall/PaywallPremium";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import { Stack } from "#/components/Stack/Stack";
|
||||
import {
|
||||
Table,
|
||||
@@ -64,8 +63,8 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<span>
|
||||
<h2 css={styles.tableHeader}>Custom Roles</h2>
|
||||
<span css={styles.tableDescription}>
|
||||
<h2 className="mb-0 text-lg">Custom Roles</h2>
|
||||
<span className="text-sm text-content-secondary leading-relaxed">
|
||||
Create custom roles to grant users a tailored set of granular
|
||||
permissions.
|
||||
</span>
|
||||
@@ -88,8 +87,8 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
|
||||
onDeleteRole={onDeleteRole}
|
||||
/>
|
||||
<span>
|
||||
<h2 css={styles.tableHeader}>Built-In Roles</h2>
|
||||
<span css={styles.tableDescription}>
|
||||
<h2 className="mb-0 text-lg">Built-In Roles</h2>
|
||||
<span className="text-sm text-content-secondary leading-relaxed">
|
||||
Built-in roles have predefined permissions. You cannot edit or delete
|
||||
built-in roles.
|
||||
</span>
|
||||
@@ -262,18 +261,3 @@ const TableLoader = () => {
|
||||
</TableLoaderSkeleton>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
secondary: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
}),
|
||||
tableHeader: () => ({
|
||||
marginBottom: 0,
|
||||
fontSize: 18,
|
||||
}),
|
||||
tableDescription: (theme) => ({
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "160%",
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react";
|
||||
import { type FC, useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "react-query";
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "#/components/Popover/Popover";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import { useDebouncedValue } from "#/hooks/debounce";
|
||||
import { useAuthenticated } from "#/hooks/useAuthenticated";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import {
|
||||
@@ -29,12 +27,14 @@ import {
|
||||
import { EmptyState } from "#/components/EmptyState/EmptyState";
|
||||
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
|
||||
import { LastSeen } from "#/components/LastSeen/LastSeen";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import { TableCell, TableRow } from "#/components/Table/Table";
|
||||
import {
|
||||
TableLoaderSkeleton,
|
||||
TableRowSkeleton,
|
||||
} from "#/components/TableLoader/TableLoader";
|
||||
import { AISeatCell } from "#/modules/users/AISeatCell";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { UserRoleCell } from "../../OrganizationSettingsPage/UserTable/UserRoleCell";
|
||||
import { UserGroupsCell } from "./UserGroupsCell";
|
||||
|
||||
@@ -187,10 +187,10 @@ export const UsersTableBody: FC<UsersTableBodyProps> = ({
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
css={[
|
||||
styles.status,
|
||||
user.status === "suspended" && styles.suspended,
|
||||
]}
|
||||
className={cn(
|
||||
"capitalize",
|
||||
user.status === "suspended" && "text-content-secondary",
|
||||
)}
|
||||
>
|
||||
<div>{user.status}</div>
|
||||
{(user.status === "active" || user.status === "dormant") && (
|
||||
@@ -283,27 +283,27 @@ const LoginType: FC<LoginTypeProps> = ({ authMethods, value }) => {
|
||||
|
||||
if (value === "password") {
|
||||
displayName = "Password";
|
||||
icon = <UserLockIcon css={styles.icon} />;
|
||||
icon = <UserLockIcon className="size-icon-xs" />;
|
||||
} else if (value === "none") {
|
||||
displayName = "None";
|
||||
icon = <BanIcon css={styles.icon} />;
|
||||
icon = <BanIcon className="size-icon-xs" />;
|
||||
} else if (value === "github") {
|
||||
displayName = "GitHub";
|
||||
icon = <ExternalImage src="/icon/github.svg" css={styles.icon} />;
|
||||
icon = <ExternalImage src="/icon/github.svg" className="size-icon-xs" />;
|
||||
} else if (value === "token") {
|
||||
displayName = "Token";
|
||||
icon = <KeyIcon css={styles.icon} />;
|
||||
icon = <KeyIcon className="size-icon-xs" />;
|
||||
} else if (value === "oidc") {
|
||||
displayName =
|
||||
authMethods.oidc.signInText === "" ? "OIDC" : authMethods.oidc.signInText;
|
||||
icon =
|
||||
authMethods.oidc.iconUrl === "" ? (
|
||||
<ShieldIcon css={styles.icon} />
|
||||
<ShieldIcon className="size-icon-xs" />
|
||||
) : (
|
||||
<img
|
||||
alt="Open ID Connect icon"
|
||||
src={authMethods.oidc.iconUrl}
|
||||
css={styles.icon}
|
||||
className="size-icon-xs"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -315,18 +315,3 @@ const LoginType: FC<LoginTypeProps> = ({ authMethods, value }) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
icon: {
|
||||
width: 14,
|
||||
height: 14,
|
||||
},
|
||||
|
||||
status: {
|
||||
textTransform: "capitalize",
|
||||
},
|
||||
|
||||
suspended: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import type { FC } from "react";
|
||||
import type { WorkspaceResource } from "#/api/typesGenerated";
|
||||
import {
|
||||
@@ -7,6 +5,7 @@ import {
|
||||
SidebarCaption,
|
||||
SidebarItem,
|
||||
} from "#/components/FullPageLayout/Sidebar";
|
||||
import { Skeleton } from "#/components/Skeleton/Skeleton";
|
||||
import { getResourceIconPath } from "#/utils/workspace";
|
||||
|
||||
type ResourcesSidebarProps = {
|
||||
@@ -22,21 +21,11 @@ export const ResourcesSidebar: FC<ResourcesSidebarProps> = ({
|
||||
isSelected,
|
||||
resources,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
<SidebarCaption>Resources</SidebarCaption>
|
||||
{failed && (
|
||||
<p
|
||||
css={{
|
||||
margin: 0,
|
||||
padding: "0 16px",
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "1.5",
|
||||
}}
|
||||
>
|
||||
<p className="m-0 py-4 text-[13px] text-content-secondary leading-normal">
|
||||
Your workspace build failed, so the necessary resources couldn't
|
||||
be created.
|
||||
</p>
|
||||
@@ -53,7 +42,7 @@ export const ResourcesSidebar: FC<ResourcesSidebarProps> = ({
|
||||
onClick={() => onChange(r)}
|
||||
isActive={isSelected(r)}
|
||||
key={r.id}
|
||||
css={styles.root}
|
||||
className="leading-normal flex items-center gap-3"
|
||||
>
|
||||
<div className="flex items-center justify-center leading-none w-4 h-4 p-0.5">
|
||||
<img
|
||||
@@ -64,9 +53,7 @@ export const ResourcesSidebar: FC<ResourcesSidebarProps> = ({
|
||||
</div>
|
||||
<div className="flex flex-col font-medium">
|
||||
<span>{r.name}</span>
|
||||
<span css={{ fontSize: 12, color: theme.palette.text.secondary }}>
|
||||
{r.type}
|
||||
</span>
|
||||
<span className="text-sm text-content-secondary">{r.type}</span>
|
||||
</div>
|
||||
</SidebarItem>
|
||||
))}
|
||||
@@ -76,7 +63,7 @@ export const ResourcesSidebar: FC<ResourcesSidebarProps> = ({
|
||||
|
||||
const ResourceSidebarItemSkeleton: FC = () => {
|
||||
return (
|
||||
<div css={[styles.root, { pointerEvents: "none" }]}>
|
||||
<div className="leading-normal flex items-center gap-3 pointer-events-none">
|
||||
<Skeleton variant="circular" width={16} height={16} />
|
||||
<div>
|
||||
<Skeleton variant="text" width={94} height={16} />
|
||||
@@ -85,12 +72,3 @@ const ResourceSidebarItemSkeleton: FC = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
root: {
|
||||
lineHeight: "1.5",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
},
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
Reference in New Issue
Block a user