refactor(site): migrate 39 files from emotion css to tailwind
Migrate all non-MUI emotion files (useTheme + css= prop patterns) to Tailwind utility classes. This covers all 5 'easy' files (static css=, no theme) and 34 'theme' files (useTheme for palette colors, converted to semantic Tailwind tokens). Theme property mappings used: - theme.palette.text.primary → text-content-primary - theme.palette.text.secondary → text-content-secondary - theme.palette.text.disabled → text-content-disabled - theme.palette.divider → border-border-default - theme.palette.background.paper → bg-surface-secondary - theme.palette.background.default → bg-surface-primary - theme.palette.primary.main → text-content-link - theme.roles.active.outline → border-border-pending - theme.roles.danger.outline → border-border-warning Net reduction: ~750 lines removed.
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Avatar as AvatarPrimitive } from "radix-ui";
|
||||
import type { CSSProperties } from "react";
|
||||
import { getExternalImageStylesFromUrl } from "#/theme/externalImages";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
@@ -68,6 +69,9 @@ export const Avatar: React.FC<AvatarProps> = ({
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
// Kept for runtime access to theme.externalImages, which provides
|
||||
// dynamic CSS filter styles based on the current theme mode and URL
|
||||
// parameters. No Tailwind token equivalent exists for this value.
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
@@ -78,7 +82,12 @@ export const Avatar: React.FC<AvatarProps> = ({
|
||||
<AvatarPrimitive.Image
|
||||
src={src}
|
||||
className="aspect-square size-full object-contain"
|
||||
css={getExternalImageStylesFromUrl(theme.externalImages, src)}
|
||||
style={
|
||||
getExternalImageStylesFromUrl(
|
||||
theme.externalImages,
|
||||
src,
|
||||
) as CSSProperties
|
||||
}
|
||||
/>
|
||||
{fallback && (
|
||||
<AvatarPrimitive.Fallback className="flex h-full w-full items-center justify-center rounded-full">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { type CSSObject, useTheme } from "@emotion/react";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { Avatar } from "#/components/Avatar/Avatar";
|
||||
|
||||
@@ -15,20 +14,11 @@ export const AvatarCard: FC<AvatarCardProps> = ({
|
||||
subtitle,
|
||||
maxWidth = "none",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
className="flex flex-row flex-nowrap items-center border border-solid border-border gap-4 p-4 rounded-lg cursor-default"
|
||||
style={{
|
||||
maxWidth: maxWidth === "none" ? undefined : `${maxWidth}px`,
|
||||
display: "flex",
|
||||
flexFlow: "row nowrap",
|
||||
alignItems: "center",
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
gap: "16px",
|
||||
padding: "16px",
|
||||
borderRadius: "8px",
|
||||
cursor: "default",
|
||||
}}
|
||||
>
|
||||
{/**
|
||||
@@ -41,27 +31,13 @@ export const AvatarCard: FC<AvatarCardProps> = ({
|
||||
<h3
|
||||
// Lets users hover over truncated text to see whole thing
|
||||
title={header}
|
||||
css={[
|
||||
theme.typography.body1 as CSSObject,
|
||||
{
|
||||
lineHeight: 1.4,
|
||||
margin: 0,
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
},
|
||||
]}
|
||||
className="text-[1rem] leading-[1.4] m-0 truncate"
|
||||
>
|
||||
{header}
|
||||
</h3>
|
||||
|
||||
{subtitle && (
|
||||
<div
|
||||
css={[
|
||||
theme.typography.body2 as CSSObject,
|
||||
{ color: theme.palette.text.secondary },
|
||||
]}
|
||||
>
|
||||
<div className="text-[14px] leading-[160%] text-content-secondary">
|
||||
{subtitle}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||
import { type FC, useState } from "react";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
@@ -7,7 +6,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "#/components/Tooltip/Tooltip";
|
||||
import { MONOSPACE_FONT_FAMILY } from "#/theme/constants";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { CopyButton } from "../CopyButton/CopyButton";
|
||||
|
||||
interface CodeExampleProps {
|
||||
@@ -52,8 +51,18 @@ export const CodeExample: FC<CodeExampleProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div css={styles.container} className={className}>
|
||||
<code css={[styles.code, secret && styles.secret]}>
|
||||
<div
|
||||
className={cn(
|
||||
"cursor-pointer flex flex-row items-center text-content-primary font-mono text-[14px] rounded-lg p-2 leading-[150%] border border-solid border-border hover:bg-surface-tertiary",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<code
|
||||
className={cn(
|
||||
"px-2 grow break-all",
|
||||
secret && "[-webkit-text-security:disc]",
|
||||
)}
|
||||
>
|
||||
{secret ? (
|
||||
<>
|
||||
{/*
|
||||
@@ -99,33 +108,3 @@ export const CodeExample: FC<CodeExampleProps> = ({
|
||||
function obfuscateText(text: string): string {
|
||||
return new Array(text.length).fill("*").join("");
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: (theme) => ({
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
color: theme.experimental.l1.text,
|
||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
||||
fontSize: 14,
|
||||
borderRadius: 8,
|
||||
padding: 8,
|
||||
lineHeight: "150%",
|
||||
border: `1px solid ${theme.experimental.l1.outline}`,
|
||||
|
||||
"&:hover": {
|
||||
backgroundColor: theme.experimental.l2.hover.background,
|
||||
},
|
||||
}),
|
||||
|
||||
code: {
|
||||
padding: "0 8px",
|
||||
flexGrow: 1,
|
||||
wordBreak: "break-all",
|
||||
},
|
||||
|
||||
secret: {
|
||||
"-webkit-text-security": "disc", // also supported by firefox
|
||||
},
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import type { CSSProperties } from "react";
|
||||
import { getExternalImageStylesFromUrl } from "#/theme/externalImages";
|
||||
|
||||
export const ExternalImage: React.FC<React.ComponentPropsWithRef<"img">> = ({
|
||||
...props
|
||||
}) => {
|
||||
// Kept for runtime access to theme.externalImages, which provides
|
||||
// dynamic CSS filter styles based on the current theme mode and URL
|
||||
// parameters. No Tailwind token equivalent exists for this value.
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
// biome-ignore lint/a11y/useAltText: alt should be passed in as a prop
|
||||
<img
|
||||
css={getExternalImageStylesFromUrl(theme.externalImages, props.src)}
|
||||
{...props}
|
||||
style={{
|
||||
...props.style,
|
||||
...(getExternalImageStylesFromUrl(
|
||||
theme.externalImages,
|
||||
props.src,
|
||||
) as CSSProperties),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import {
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
@@ -22,21 +21,17 @@ type FormProps = HTMLProps<HTMLFormElement> & {
|
||||
};
|
||||
|
||||
export const Form: FC<FormProps> = ({ direction, ...formProps }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<FormContext.Provider value={{ direction }}>
|
||||
<form
|
||||
{...formProps}
|
||||
css={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: direction === "horizontal" ? 80 : 40,
|
||||
|
||||
[theme.breakpoints.down("md")]: {
|
||||
gap: 64,
|
||||
},
|
||||
}}
|
||||
className={cn(
|
||||
"flex flex-col gap-16",
|
||||
direction === "horizontal"
|
||||
? "min-[900px]:gap-20"
|
||||
: "min-[900px]:gap-10",
|
||||
formProps.className,
|
||||
)}
|
||||
/>
|
||||
</FormContext.Provider>
|
||||
);
|
||||
@@ -92,27 +87,35 @@ export const FormSection: FC<FormSectionProps> = ({
|
||||
return (
|
||||
<section
|
||||
ref={ref}
|
||||
css={[
|
||||
styles.formSection,
|
||||
direction === "horizontal" && styles.formSectionHorizontal,
|
||||
]}
|
||||
className={classes.root}
|
||||
className={cn(
|
||||
"flex items-start flex-col gap-4 min-[1200px]:gap-6",
|
||||
direction === "horizontal" &&
|
||||
"min-[1200px]:flex-row min-[1200px]:gap-[120px]",
|
||||
classes.root,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
css={[
|
||||
styles.formSectionInfo,
|
||||
direction === "horizontal" && styles.formSectionInfoHorizontal,
|
||||
]}
|
||||
className={classes.sectionInfo}
|
||||
className={cn(
|
||||
"w-full shrink-0 top-6",
|
||||
direction === "horizontal" && "max-w-[312px] min-[1200px]:sticky",
|
||||
classes.sectionInfo,
|
||||
)}
|
||||
>
|
||||
<header className="flex items-center gap-4">
|
||||
<h2 css={styles.formSectionInfoTitle} className={classes.infoTitle}>
|
||||
<h2
|
||||
className={cn(
|
||||
"text-[20px] text-content-primary font-medium m-0 mb-2 flex flex-row items-center gap-3",
|
||||
classes.infoTitle,
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
{alpha && <AlphaBadge />}
|
||||
{deprecated && <DeprecatedBadge />}
|
||||
</header>
|
||||
<div css={styles.formSectionInfoDescription}>{description}</div>
|
||||
<div className="text-[14px] text-content-secondary leading-[160%] m-0">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
@@ -126,68 +129,11 @@ export const FormFields: FC<ComponentProps<typeof Stack>> = (props) => {
|
||||
direction="column"
|
||||
spacing={3}
|
||||
{...props}
|
||||
css={styles.formSectionFields}
|
||||
className={cn("w-full", props.className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
formSection: (theme) => ({
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
flexDirection: "column",
|
||||
gap: 24,
|
||||
|
||||
[theme.breakpoints.down("lg")]: {
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
},
|
||||
}),
|
||||
formSectionHorizontal: {
|
||||
flexDirection: "row",
|
||||
gap: 120,
|
||||
},
|
||||
formSectionInfo: (theme) => ({
|
||||
width: "100%",
|
||||
flexShrink: 0,
|
||||
top: 24,
|
||||
|
||||
[theme.breakpoints.down("md")]: {
|
||||
width: "100%",
|
||||
position: "initial" as const,
|
||||
},
|
||||
}),
|
||||
formSectionInfoHorizontal: (theme) => ({
|
||||
maxWidth: 312,
|
||||
|
||||
[theme.breakpoints.up("lg")]: {
|
||||
position: "sticky",
|
||||
},
|
||||
}),
|
||||
formSectionInfoTitle: (theme) => ({
|
||||
fontSize: 20,
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 500,
|
||||
margin: 0,
|
||||
marginBottom: 8,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
}),
|
||||
|
||||
formSectionInfoDescription: (theme) => ({
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "160%",
|
||||
margin: 0,
|
||||
}),
|
||||
|
||||
formSectionFields: {
|
||||
width: "100%",
|
||||
},
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export const FormFooter: FC<HTMLProps<HTMLDivElement>> = ({
|
||||
className,
|
||||
...props
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import {
|
||||
cloneElement,
|
||||
type FC,
|
||||
@@ -51,10 +50,9 @@ export const TopbarData: FC<HTMLAttributes<HTMLDivElement>> = (props) => {
|
||||
|
||||
export const TopbarDivider: FC<
|
||||
Omit<HTMLAttributes<HTMLSpanElement>, "children">
|
||||
> = (props) => {
|
||||
const theme = useTheme();
|
||||
> = ({ className, ...props }) => {
|
||||
return (
|
||||
<span {...props} css={{ color: theme.palette.divider }}>
|
||||
<span {...props} className={cn("text-border", className)}>
|
||||
/
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { CSSObject } from "@emotion/react";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
type StackProps = React.ComponentPropsWithRef<"div"> & {
|
||||
className?: string;
|
||||
direction?: "column" | "row";
|
||||
spacing?: number;
|
||||
alignItems?: CSSObject["alignItems"];
|
||||
justifyContent?: CSSObject["justifyContent"];
|
||||
wrap?: CSSObject["flexWrap"];
|
||||
alignItems?: React.CSSProperties["alignItems"];
|
||||
justifyContent?: React.CSSProperties["justifyContent"];
|
||||
wrap?: React.CSSProperties["flexWrap"];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -26,14 +26,14 @@ export const Stack: React.FC<StackProps> = (props) => {
|
||||
return (
|
||||
<div
|
||||
{...divProps}
|
||||
css={{
|
||||
display: "flex",
|
||||
className={cn("flex max-w-full", divProps.className)}
|
||||
style={{
|
||||
...divProps.style,
|
||||
flexDirection: direction,
|
||||
gap: spacing * 8,
|
||||
alignItems: alignItems,
|
||||
justifyContent: justifyContent,
|
||||
alignItems,
|
||||
justifyContent,
|
||||
flexWrap: wrap,
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Building2Icon, UserIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import type { HealthMessage, ProvisionerDaemon } from "#/api/typesGenerated";
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "#/components/Tooltip/Tooltip";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { createDayString } from "#/utils/createDayString";
|
||||
import { ProvisionerTag } from "./ProvisionerTag";
|
||||
|
||||
@@ -20,7 +20,6 @@ export const Provisioner: FC<ProvisionerProps> = ({
|
||||
provisioner,
|
||||
warnings,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const daemonScope = provisioner.tags.scope || "organization";
|
||||
const iconScope =
|
||||
daemonScope === "organization" ? (
|
||||
@@ -36,20 +35,16 @@ export const Provisioner: FC<ProvisionerProps> = ({
|
||||
return (
|
||||
<div
|
||||
key={provisioner.name}
|
||||
css={[
|
||||
{
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
fontSize: 14,
|
||||
},
|
||||
isWarning && { borderColor: theme.palette.warning.light },
|
||||
]}
|
||||
className={cn(
|
||||
"rounded-lg border border-solid border-border text-sm",
|
||||
isWarning && "border-border-warning",
|
||||
)}
|
||||
>
|
||||
<header className="p-6 flex items-center justify-between gap-6">
|
||||
<div className="flex items-center gap-6 object-fill">
|
||||
<div className="leading-[160%]">
|
||||
<h4 className="font-medium m-0">{provisioner.name}</h4>
|
||||
<span css={{ color: theme.palette.text.secondary }}>
|
||||
<span className="text-content-secondary">
|
||||
<code>{provisioner.version}</code>
|
||||
</span>
|
||||
</div>
|
||||
@@ -71,17 +66,7 @@ export const Provisioner: FC<ProvisionerProps> = ({
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
css={{
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "8px 24px",
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
<div className="border-solid border-0 border-t border-border flex items-center justify-between py-2 px-6 text-xs text-content-secondary">
|
||||
{warnings && warnings.length > 0 ? (
|
||||
<div className="flex flex-col">
|
||||
{warnings.map((warning) => (
|
||||
@@ -92,7 +77,7 @@ export const Provisioner: FC<ProvisionerProps> = ({
|
||||
<span>No warnings</span>
|
||||
)}
|
||||
{provisioner.last_seen_at && (
|
||||
<span css={{ color: theme.roles.info.text }} data-chromatic="ignore">
|
||||
<span className="text-content-primary" data-chromatic="ignore">
|
||||
Last seen {createDayString(provisioner.last_seen_at)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import { CircleCheckIcon, CircleMinusIcon, TagIcon, XIcon } from "lucide-react";
|
||||
import type { ComponentProps, FC } from "react";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
@@ -85,9 +84,9 @@ const BooleanPill: FC<BooleanPillProps> = ({
|
||||
size="lg"
|
||||
icon={
|
||||
value ? (
|
||||
<CircleCheckIcon css={styles.truePill} className="size-icon-sm" />
|
||||
<CircleCheckIcon className="text-border-pending size-icon-sm" />
|
||||
) : (
|
||||
<CircleMinusIcon css={styles.falsePill} className="size-icon-sm" />
|
||||
<CircleMinusIcon className="text-border-warning size-icon-sm" />
|
||||
)
|
||||
}
|
||||
{...divProps}
|
||||
@@ -96,12 +95,3 @@ const BooleanPill: FC<BooleanPillProps> = ({
|
||||
</Pill>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
truePill: (theme) => ({
|
||||
color: theme.roles.active.outline,
|
||||
}),
|
||||
falsePill: (theme) => ({
|
||||
color: theme.roles.danger.outline,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import type { FC } from "react";
|
||||
import type { WorkspaceAgent } from "#/api/typesGenerated";
|
||||
import { TerminalIcon } from "#/components/Icons/TerminalIcon";
|
||||
import { VSCodeIcon } from "#/components/Icons/VSCodeIcon";
|
||||
import { Stack } from "#/components/Stack/Stack";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { DisplayAppNameMap } from "./AppLink/AppLink";
|
||||
import { AppPreview } from "./AppLink/AppPreview";
|
||||
import { BaseIcon } from "./AppLink/BaseIcon";
|
||||
@@ -27,52 +27,42 @@ export const AgentRowPreview: FC<AgentRowPreviewProps> = ({
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
css={styles.agentRow}
|
||||
className="py-4 px-8 bg-surface-secondary text-[16px] relative [&:not(:last-child)]:pb-0 after:content-[''] after:absolute after:top-0 after:left-[43px] after:h-full after:w-0.5 after:bg-border"
|
||||
>
|
||||
<Stack direction="row" alignItems="baseline">
|
||||
<div css={styles.agentStatusWrapper}>
|
||||
<div css={styles.agentStatusPreview} />
|
||||
<div className="flex w-6 justify-center shrink-0">
|
||||
<div className="w-2.5 h-2.5 border-2 border-solid border-content-secondary rounded-full relative z-[1] bg-surface-secondary" />
|
||||
</div>
|
||||
<Stack
|
||||
alignItems="baseline"
|
||||
direction="row"
|
||||
spacing={4}
|
||||
css={styles.agentData}
|
||||
className="text-[14px] text-content-secondary max-[900px]:gap-4 max-[900px]:flex-wrap"
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="baseline"
|
||||
spacing={1}
|
||||
css={[
|
||||
styles.noShrink,
|
||||
styles.agentDataItem,
|
||||
(theme) => ({
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
minWidth: alignValues ? 240 : undefined,
|
||||
},
|
||||
}),
|
||||
]}
|
||||
className={cn(
|
||||
"shrink-0 max-[900px]:flex-col max-[900px]:items-start max-[900px]:gap-2 max-[900px]:w-fit",
|
||||
alignValues && "min-[600px]:min-w-[240px]",
|
||||
)}
|
||||
>
|
||||
<span>Agent:</span>
|
||||
<span css={styles.agentDataValue}>{agent.name}</span>
|
||||
<span className="text-content-primary">{agent.name}</span>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="baseline"
|
||||
spacing={1}
|
||||
css={[
|
||||
styles.noShrink,
|
||||
styles.agentDataItem,
|
||||
(theme) => ({
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
minWidth: alignValues ? 100 : undefined,
|
||||
},
|
||||
}),
|
||||
]}
|
||||
className={cn(
|
||||
"shrink-0 max-[900px]:flex-col max-[900px]:items-start max-[900px]:gap-2 max-[900px]:w-fit",
|
||||
alignValues && "min-[600px]:min-w-[100px]",
|
||||
)}
|
||||
>
|
||||
<span>OS:</span>
|
||||
<span css={[styles.agentDataValue, styles.agentOS]}>
|
||||
<span className="text-content-primary capitalize text-[14px]">
|
||||
{agent.operating_system}
|
||||
</span>
|
||||
</Stack>
|
||||
@@ -81,7 +71,7 @@ export const AgentRowPreview: FC<AgentRowPreviewProps> = ({
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={1}
|
||||
css={styles.agentDataItem}
|
||||
className="max-[900px]:flex-col max-[900px]:items-start max-[900px]:gap-2 max-[900px]:w-fit"
|
||||
>
|
||||
<span>Apps:</span>
|
||||
<Stack
|
||||
@@ -128,7 +118,7 @@ export const AgentRowPreview: FC<AgentRowPreviewProps> = ({
|
||||
)
|
||||
)}
|
||||
{agent.apps.length === 0 && agent.display_apps.length === 0 && (
|
||||
<span css={styles.agentDataValue}>None</span>
|
||||
<span className="text-content-primary">None</span>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -137,79 +127,3 @@ export const AgentRowPreview: FC<AgentRowPreviewProps> = ({
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
agentRow: (theme) => ({
|
||||
padding: "16px 32px",
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
fontSize: 16,
|
||||
position: "relative",
|
||||
|
||||
"&:not(:last-child)": {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
|
||||
"&:after": {
|
||||
content: "''",
|
||||
height: "100%",
|
||||
width: 2,
|
||||
backgroundColor: theme.palette.divider,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 43,
|
||||
},
|
||||
}),
|
||||
|
||||
agentStatusWrapper: {
|
||||
width: 24,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
},
|
||||
|
||||
agentStatusPreview: (theme) => ({
|
||||
width: 10,
|
||||
height: 10,
|
||||
border: `2px solid ${theme.palette.text.secondary}`,
|
||||
borderRadius: "100%",
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
background: theme.palette.background.paper,
|
||||
}),
|
||||
|
||||
agentName: {
|
||||
fontWeight: 600,
|
||||
},
|
||||
|
||||
agentOS: {
|
||||
textTransform: "capitalize",
|
||||
fontSize: 14,
|
||||
},
|
||||
|
||||
agentData: (theme) => ({
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
|
||||
[theme.breakpoints.down("md")]: {
|
||||
gap: 16,
|
||||
flexWrap: "wrap",
|
||||
},
|
||||
}),
|
||||
|
||||
agentDataValue: (theme) => ({
|
||||
color: theme.palette.text.primary,
|
||||
}),
|
||||
|
||||
noShrink: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
|
||||
agentDataItem: (theme) => ({
|
||||
[theme.breakpoints.down("md")]: {
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
gap: 8,
|
||||
width: "fit-content",
|
||||
},
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import { Children, type FC, type JSX, useState } from "react";
|
||||
import type { WorkspaceAgent, WorkspaceResource } from "#/api/typesGenerated";
|
||||
import { ChevronDownIcon } from "#/components/AnimatedIcons/ChevronDown";
|
||||
@@ -14,66 +13,6 @@ import {
|
||||
import { ResourceAvatar } from "./ResourceAvatar";
|
||||
import { SensitiveValue } from "./SensitiveValue";
|
||||
|
||||
const styles = {
|
||||
resourceCard: (theme) => ({
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
background: theme.palette.background.default,
|
||||
|
||||
"&:not(:last-child)": {
|
||||
borderBottom: 0,
|
||||
},
|
||||
|
||||
"&:first-of-type": {
|
||||
borderTopLeftRadius: 8,
|
||||
borderTopRightRadius: 8,
|
||||
},
|
||||
|
||||
"&:last-child": {
|
||||
borderBottomLeftRadius: 8,
|
||||
borderBottomRightRadius: 8,
|
||||
},
|
||||
}),
|
||||
|
||||
resourceCardProfile: {
|
||||
flexShrink: 0,
|
||||
width: "fit-content",
|
||||
minWidth: 220,
|
||||
},
|
||||
|
||||
resourceCardHeader: (theme) => ({
|
||||
padding: "24px 32px",
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
|
||||
"&:last-child": {
|
||||
borderBottom: 0,
|
||||
},
|
||||
|
||||
[theme.breakpoints.down("md")]: {
|
||||
width: "100%",
|
||||
overflow: "scroll",
|
||||
},
|
||||
}),
|
||||
|
||||
metadata: () => ({
|
||||
lineHeight: "1.5",
|
||||
fontSize: 14,
|
||||
}),
|
||||
|
||||
metadataLabel: (theme) => ({
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
}),
|
||||
|
||||
metadataValue: () => ({
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
interface ResourceCardProps {
|
||||
resource: WorkspaceResource;
|
||||
agentRow: (agent: WorkspaceAgent) => JSX.Element;
|
||||
@@ -96,20 +35,29 @@ export const ResourceCard: FC<ResourceCardProps> = ({ resource, agentRow }) => {
|
||||
const gridWidth = mLength === 1 ? 1 : 4;
|
||||
|
||||
return (
|
||||
<div key={resource.id} css={styles.resourceCard} className="resource-card">
|
||||
<div
|
||||
key={resource.id}
|
||||
className="resource-card border border-solid border-border bg-surface-primary [&:not(:last-child)]:border-b-0 first-of-type:rounded-t-[8px] last:rounded-b-[8px]"
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
css={styles.resourceCardHeader}
|
||||
className="py-6 px-8 border-0 border-b border-solid border-border last:border-b-0 max-[900px]:w-full max-[900px]:overflow-scroll"
|
||||
spacing={10}
|
||||
>
|
||||
<Stack direction="row" spacing={1} css={styles.resourceCardProfile}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
className="shrink-0 w-fit min-w-[220px]"
|
||||
>
|
||||
<div>
|
||||
<ResourceAvatar resource={resource} />
|
||||
</div>
|
||||
<div css={styles.metadata}>
|
||||
<div css={styles.metadataLabel}>{resource.type}</div>
|
||||
<div css={styles.metadataValue}>{resource.name}</div>
|
||||
<div className="text-[14px] leading-normal">
|
||||
<div className="text-[12px] text-content-secondary truncate">
|
||||
{resource.type}
|
||||
</div>
|
||||
<div className="truncate">{resource.name}</div>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
@@ -120,18 +68,20 @@ export const ResourceCard: FC<ResourceCardProps> = ({ resource, agentRow }) => {
|
||||
}}
|
||||
>
|
||||
{resource.daily_cost > 0 && (
|
||||
<div css={styles.metadata}>
|
||||
<div css={styles.metadataLabel}>
|
||||
<div className="text-[14px] leading-normal">
|
||||
<div className="text-[12px] text-content-secondary truncate">
|
||||
<b>Daily cost</b>
|
||||
</div>
|
||||
<div css={styles.metadataValue}>{resource.daily_cost}</div>
|
||||
<div className="truncate">{resource.daily_cost}</div>
|
||||
</div>
|
||||
)}
|
||||
{visibleMetadata.map((meta) => {
|
||||
return (
|
||||
<div css={styles.metadata} key={meta.key}>
|
||||
<div css={styles.metadataLabel}>{meta.key}</div>
|
||||
<div css={styles.metadataValue}>
|
||||
<div className="text-[14px] leading-normal" key={meta.key}>
|
||||
<div className="text-[12px] text-content-secondary truncate">
|
||||
{meta.key}
|
||||
</div>
|
||||
<div className="truncate">
|
||||
{meta.sensitive ? (
|
||||
<SensitiveValue value={meta.value} />
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import type { FC } from "react";
|
||||
import { MemoizedMarkdown } from "#/components/Markdown/Markdown";
|
||||
|
||||
@@ -10,35 +9,15 @@ export const TemplateUpdateMessage: FC<TemplateUpdateMessageProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<MemoizedMarkdown css={styles.versionMessage}>{children}</MemoizedMarkdown>
|
||||
<MemoizedMarkdown
|
||||
className={[
|
||||
"text-[14px] leading-[1.2]",
|
||||
"[&_:is(h1,h2,h3,h4,h5,h6)]:m-0 [&_:is(h1,h2,h3,h4,h5,h6)]:mb-[0.75em]",
|
||||
"[&_h1]:text-[1.2em] [&_h2]:text-[1.15em] [&_h3]:text-[1.1em]",
|
||||
"[&_h4]:text-[1.05em] [&_h5]:text-[1em] [&_h6]:text-[0.95em]",
|
||||
].join(" ")}
|
||||
>
|
||||
{children}
|
||||
</MemoizedMarkdown>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
versionMessage: {
|
||||
fontSize: 14,
|
||||
lineHeight: 1.2,
|
||||
|
||||
"& h1, & h2, & h3, & h4, & h5, & h6": {
|
||||
margin: "0 0 0.75em",
|
||||
},
|
||||
"& h1": {
|
||||
fontSize: "1.2em",
|
||||
},
|
||||
"& h2": {
|
||||
fontSize: "1.15em",
|
||||
},
|
||||
"& h3": {
|
||||
fontSize: "1.1em",
|
||||
},
|
||||
"& h4": {
|
||||
fontSize: "1.05em",
|
||||
},
|
||||
"& h5": {
|
||||
fontSize: "1em",
|
||||
},
|
||||
"& h6": {
|
||||
fontSize: "0.95em",
|
||||
},
|
||||
},
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import type { FC, HTMLProps } from "react";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
@@ -48,7 +47,16 @@ export const YAxisHeader: FC<HTMLProps<HTMLSpanElement>> = (props) => {
|
||||
};
|
||||
|
||||
export const YAxisLabels: FC<HTMLProps<HTMLUListElement>> = (props) => {
|
||||
return <ul css={styles.labels} {...props} />;
|
||||
return (
|
||||
<ul
|
||||
{...props}
|
||||
className={cn(
|
||||
"m-0 list-none flex flex-col text-right",
|
||||
"gap-[var(--x-axis-rows-gap)] p-[var(--section-padding)]",
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type YAxisLabelProps = Omit<HTMLProps<HTMLLIElement>, "id"> & {
|
||||
@@ -57,32 +65,16 @@ type YAxisLabelProps = Omit<HTMLProps<HTMLLIElement>, "id"> & {
|
||||
|
||||
export const YAxisLabel: FC<YAxisLabelProps> = ({ id, ...props }) => {
|
||||
return (
|
||||
<li {...props} css={styles.label} id={encodeURIComponent(id)}>
|
||||
<li
|
||||
{...props}
|
||||
className={cn(
|
||||
"flex items-center",
|
||||
"[&>*]:block [&>*]:w-full [&>*]:truncate",
|
||||
props.className,
|
||||
)}
|
||||
id={encodeURIComponent(id)}
|
||||
>
|
||||
<span>{props.children}</span>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
labels: {
|
||||
margin: 0,
|
||||
listStyle: "none",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "var(--x-axis-rows-gap)",
|
||||
textAlign: "right",
|
||||
padding: "var(--section-padding)",
|
||||
},
|
||||
label: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
|
||||
"& > *": {
|
||||
display: "block",
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
},
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import type { FC } from "react";
|
||||
import type { AuditDiff } from "#/api/typesGenerated";
|
||||
import { MONOSPACE_FONT_FAMILY } from "#/theme/constants";
|
||||
import colors from "#/theme/tailwindColors";
|
||||
|
||||
const getDiffValue = (value: unknown): string => {
|
||||
if (typeof value === "string") {
|
||||
@@ -50,29 +47,33 @@ export const AuditLogDiff: FC<AuditLogDiffProps> = ({ diff }) => {
|
||||
const diffEntries = Object.entries(diff);
|
||||
|
||||
return (
|
||||
<div css={styles.diff}>
|
||||
<div css={[styles.diffColumn, styles.diffOld]}>
|
||||
<div className="flex items-start text-sm border-t border-border font-mono relative z-[2]">
|
||||
<div className="flex-1 pt-4 pb-5 pr-4 leading-[160%] self-stretch [overflow-wrap:anywhere] bg-red-950 text-red-50">
|
||||
{diffEntries.map(([attrName, valueDiff], index) => (
|
||||
<div key={attrName} css={styles.diffRow}>
|
||||
<div css={styles.diffLine}>{index + 1}</div>
|
||||
<div css={styles.diffIcon}>-</div>
|
||||
<div key={attrName} className="flex items-baseline">
|
||||
<div className="opacity-50 w-12 text-right shrink-0">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="w-8 text-center text-base shrink-0">-</div>
|
||||
<div>
|
||||
{attrName}:{" "}
|
||||
<span css={[styles.diffValue, styles.diffValueOld]}>
|
||||
<span className="p-px rounded bg-red-800">
|
||||
{valueDiff.secret ? "••••••••" : getDiffValue(valueDiff.old)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div css={[styles.diffColumn, styles.diffNew]}>
|
||||
<div className="flex-1 pt-4 pb-5 pr-4 leading-[160%] self-stretch [overflow-wrap:anywhere] bg-green-950 text-green-50">
|
||||
{diffEntries.map(([attrName, valueDiff], index) => (
|
||||
<div key={attrName} css={styles.diffRow}>
|
||||
<div css={styles.diffLine}>{index + 1}</div>
|
||||
<div css={styles.diffIcon}>+</div>
|
||||
<div key={attrName} className="flex items-baseline">
|
||||
<div className="opacity-50 w-12 text-right shrink-0">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="w-8 text-center text-base shrink-0">+</div>
|
||||
<div>
|
||||
{attrName}:{" "}
|
||||
<span css={[styles.diffValue, styles.diffValueNew]}>
|
||||
<span className="p-px rounded bg-green-800">
|
||||
{valueDiff.secret ? "••••••••" : getDiffValue(valueDiff.new)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -82,67 +83,3 @@ export const AuditLogDiff: FC<AuditLogDiffProps> = ({ diff }) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
diff: (theme) => ({
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
fontSize: theme.typography.body2.fontSize,
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
||||
position: "relative",
|
||||
zIndex: 2,
|
||||
}),
|
||||
|
||||
diffColumn: {
|
||||
flex: 1,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 20,
|
||||
paddingRight: 16,
|
||||
lineHeight: "160%",
|
||||
alignSelf: "stretch",
|
||||
overflowWrap: "anywhere",
|
||||
},
|
||||
|
||||
diffOld: {
|
||||
backgroundColor: colors.red[950],
|
||||
color: colors.red[50],
|
||||
},
|
||||
|
||||
diffRow: {
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
},
|
||||
|
||||
diffLine: {
|
||||
opacity: 0.5,
|
||||
width: 48,
|
||||
textAlign: "right",
|
||||
flexShrink: 0,
|
||||
},
|
||||
|
||||
diffIcon: (theme) => ({
|
||||
width: 32,
|
||||
textAlign: "center",
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
flexShrink: 0,
|
||||
}),
|
||||
|
||||
diffNew: {
|
||||
backgroundColor: colors.green[950],
|
||||
color: colors.green[50],
|
||||
},
|
||||
|
||||
diffValue: {
|
||||
padding: 1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
|
||||
diffValueOld: {
|
||||
backgroundColor: colors.red[800],
|
||||
},
|
||||
|
||||
diffValueNew: {
|
||||
backgroundColor: colors.green[800],
|
||||
},
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import type { FC } from "react";
|
||||
import { Link as RouterLink } from "react-router";
|
||||
import { CodeExample } from "#/components/CodeExample/CodeExample";
|
||||
@@ -10,10 +9,10 @@ type CliInstallPageViewProps = {
|
||||
|
||||
export const CliInstallPageView: FC<CliInstallPageViewProps> = ({ origin }) => {
|
||||
return (
|
||||
<div css={styles.container}>
|
||||
<div className="flex-1 h-screen flex flex-col justify-center items-center w-[480px] mx-auto">
|
||||
<Welcome>Install the Coder CLI</Welcome>
|
||||
|
||||
<p css={styles.instructions}>
|
||||
<p className="text-[16px] text-content-secondary pb-2 text-center leading-[1.4]">
|
||||
Copy the command below and{" "}
|
||||
<strong className="block">paste it in your terminal.</strong>
|
||||
</p>
|
||||
@@ -25,56 +24,16 @@ export const CliInstallPageView: FC<CliInstallPageViewProps> = ({ origin }) => {
|
||||
/>
|
||||
|
||||
<div className="pt-4">
|
||||
<RouterLink to="/workspaces" css={styles.backLink}>
|
||||
<RouterLink
|
||||
to="/workspaces"
|
||||
className="block text-center text-content-primary underline underline-offset-[3px] decoration-[hsla(0,0%,100%,0.7)] pt-4 pb-4 hover:no-underline"
|
||||
>
|
||||
Go to workspaces
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div css={styles.copyright}>
|
||||
<div className="text-[12px] text-content-secondary mt-6">
|
||||
© {new Date().getFullYear()} Coder Technologies, Inc.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
flex: 1,
|
||||
// Fallback to 100vh
|
||||
height: ["100vh", "-webkit-fill-available"],
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: 480,
|
||||
margin: "auto",
|
||||
},
|
||||
|
||||
instructions: (theme) => ({
|
||||
fontSize: 16,
|
||||
color: theme.palette.text.secondary,
|
||||
paddingBottom: 8,
|
||||
textAlign: "center",
|
||||
lineHeight: 1.4,
|
||||
}),
|
||||
|
||||
backLink: (theme) => ({
|
||||
display: "block",
|
||||
textAlign: "center",
|
||||
color: theme.palette.text.primary,
|
||||
textDecoration: "underline",
|
||||
textUnderlineOffset: 3,
|
||||
textDecorationColor: "hsla(0deg, 0%, 100%, 0.7)",
|
||||
paddingTop: 16,
|
||||
paddingBottom: 16,
|
||||
|
||||
"&:hover": {
|
||||
textDecoration: "none",
|
||||
},
|
||||
}),
|
||||
|
||||
copyright: (theme) => ({
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
marginTop: 24,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import type { FC } from "react";
|
||||
import { Link, useSearchParams } from "react-router";
|
||||
import type { TemplateExample } from "#/api/typesGenerated";
|
||||
import { Stack } from "#/components/Stack/Stack";
|
||||
import { TemplateExampleCard } from "#/modules/templates/TemplateExampleCard/TemplateExampleCard";
|
||||
import { cn } from "#/utils/cn";
|
||||
import type { StarterTemplatesByTag } from "#/utils/starterTemplates";
|
||||
|
||||
const getTagLabel = (tag: string) => {
|
||||
@@ -67,13 +67,20 @@ export const StarterTemplates: FC<StarterTemplatesProps> = ({
|
||||
<Stack direction="row" spacing={4} alignItems="flex-start">
|
||||
{starterTemplatesByTag && tags && (
|
||||
<Stack className="w-[202px] shrink-0 sticky">
|
||||
<h2 css={styles.sectionTitle}>Choose a starter template</h2>
|
||||
<span css={styles.filterCaption}>Filter</span>
|
||||
<h2 className="text-content-primary text-base font-normal m-0">
|
||||
Choose a starter template
|
||||
</h2>
|
||||
<span className="uppercase font-semibold text-xs text-content-secondary tracking-[0.1em]">
|
||||
Filter
|
||||
</span>
|
||||
{tags.map((tag) => (
|
||||
<Link
|
||||
key={tag}
|
||||
to={`?tag=${tag}`}
|
||||
css={[styles.tagLink, tag === activeTag && styles.tagLinkActive]}
|
||||
className={cn(
|
||||
"text-content-secondary no-underline text-sm capitalize hover:text-content-primary",
|
||||
tag === activeTag && "text-content-primary font-semibold",
|
||||
)}
|
||||
>
|
||||
{getTagLabel(tag)} ({starterTemplatesByTag[tag].length})
|
||||
</Link>
|
||||
@@ -84,9 +91,7 @@ export const StarterTemplates: FC<StarterTemplatesProps> = ({
|
||||
<div className="flex flex-wrap gap-8 h-max">
|
||||
{visibleTemplates?.map((example) => (
|
||||
<TemplateExampleCard
|
||||
css={(theme) => ({
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
})}
|
||||
className="bg-surface-secondary"
|
||||
example={example}
|
||||
key={example.id}
|
||||
activeTag={activeTag}
|
||||
@@ -96,36 +101,3 @@ export const StarterTemplates: FC<StarterTemplatesProps> = ({
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
filterCaption: (theme) => ({
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 600,
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
letterSpacing: "0.1em",
|
||||
}),
|
||||
|
||||
tagLink: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
textDecoration: "none",
|
||||
fontSize: 14,
|
||||
textTransform: "capitalize",
|
||||
|
||||
"&:hover": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}),
|
||||
|
||||
tagLinkActive: (theme) => ({
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 600,
|
||||
}),
|
||||
|
||||
sectionTitle: (theme) => ({
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: 16,
|
||||
fontWeight: 400,
|
||||
margin: 0,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
+1
-14
@@ -1,4 +1,3 @@
|
||||
import { css } from "@emotion/react";
|
||||
import type { FC } from "react";
|
||||
import type {
|
||||
DeploymentValues,
|
||||
@@ -60,19 +59,7 @@ export const ExternalAuthSettingsPageView: FC<
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
css={css`
|
||||
& td {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
& td:last-child,
|
||||
& th:last-child {
|
||||
padding-left: 32px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Table className="[&_td]:py-6 [&_td:last-child]:pl-8 [&_th:last-child]:pl-8">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-1/3">ID</TableHead>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import type { FC } from "react";
|
||||
import { useMutation } from "react-query";
|
||||
import { toast } from "sonner";
|
||||
@@ -23,18 +22,10 @@ export const Troubleshooting: FC<TroubleshootingProps> = ({
|
||||
}),
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
css={{
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "160%",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
Send a test notification to troubleshoot your notification settings.
|
||||
<div className="text-[14px] text-content-secondary leading-[160%] mb-4">
|
||||
Send a test notification to troubleshoot your notification settings.{" "}
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { css, type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import { WrenchIcon } from "lucide-react";
|
||||
import type { FC, HTMLAttributes, PropsWithChildren } from "react";
|
||||
import { DisabledBadge, EnabledBadge } from "#/components/Badges/Badges";
|
||||
import { MONOSPACE_FONT_FAMILY } from "#/theme/constants";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
export const OptionName: FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
@@ -22,7 +21,6 @@ interface OptionValueProps {
|
||||
|
||||
export const OptionValue: FC<OptionValueProps> = (props) => {
|
||||
const { children: value } = props;
|
||||
const theme = useTheme();
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return (
|
||||
@@ -34,7 +32,7 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
|
||||
|
||||
if (typeof value === "number") {
|
||||
return (
|
||||
<span css={styles.option} className="option-value-number">
|
||||
<span className="option-value-number font-mono text-[14px] [overflow-wrap:anywhere] select-all [&_ul]:p-4">
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
@@ -42,7 +40,7 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
|
||||
|
||||
if (!value || value.length === 0) {
|
||||
return (
|
||||
<span css={styles.option} className="option-value-empty">
|
||||
<span className="option-value-empty font-mono text-[14px] [overflow-wrap:anywhere] select-all [&_ul]:p-4">
|
||||
Not set
|
||||
</span>
|
||||
);
|
||||
@@ -50,7 +48,7 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
|
||||
|
||||
if (typeof value === "string") {
|
||||
return (
|
||||
<span css={styles.option} className="option-value-string">
|
||||
<span className="option-value-string font-mono text-[14px] [overflow-wrap:anywhere] select-all [&_ul]:p-4">
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
@@ -64,16 +62,12 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
|
||||
.map(([option, isEnabled]) => (
|
||||
<li
|
||||
key={option}
|
||||
css={[
|
||||
styles.option,
|
||||
!isEnabled && {
|
||||
marginLeft: 32,
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
]}
|
||||
className={`option-array-item-${option} ${
|
||||
isEnabled ? "option-enabled" : "option-disabled"
|
||||
}`}
|
||||
className={cn(
|
||||
"font-mono text-[14px] [overflow-wrap:anywhere] select-all [&_ul]:p-4",
|
||||
!isEnabled && "ml-8 text-content-disabled",
|
||||
`option-array-item-${option}`,
|
||||
isEnabled ? "option-enabled" : "option-disabled",
|
||||
)}
|
||||
>
|
||||
<div className="inline-flex items-center">
|
||||
{isEnabled && <WrenchIcon className="size-4 mx-2" />}
|
||||
@@ -89,7 +83,10 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
|
||||
return (
|
||||
<ul className="option-array list-inside">
|
||||
{value.map((item) => (
|
||||
<li key={item} css={styles.option}>
|
||||
<li
|
||||
key={item}
|
||||
className="font-mono text-[14px] [overflow-wrap:anywhere] select-all [&_ul]:p-4"
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
@@ -98,7 +95,7 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<span css={styles.option} className="option-value-json">
|
||||
<span className="option-value-json font-mono text-[14px] [overflow-wrap:anywhere] select-all [&_ul]:p-4">
|
||||
{JSON.stringify(value)}
|
||||
</span>
|
||||
);
|
||||
@@ -107,66 +104,35 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
|
||||
type OptionConfigProps = HTMLAttributes<HTMLDivElement> & { isSource: boolean };
|
||||
|
||||
// OptionConfig takes a isSource bool to indicate if the Option is the source of the configured value.
|
||||
export const OptionConfig: FC<OptionConfigProps> = ({ isSource, ...attrs }) => {
|
||||
export const OptionConfig: FC<OptionConfigProps> = ({
|
||||
isSource,
|
||||
className,
|
||||
...attrs
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
{...attrs}
|
||||
css={[styles.configOption, isSource && styles.sourceConfigOption]}
|
||||
className={cn(
|
||||
"font-mono text-[13px] font-semibold bg-surface-secondary inline-flex items-center rounded p-1.5 leading-none gap-1.5 border border-solid border-border",
|
||||
isSource &&
|
||||
"border-border-pending [&_.OptionConfigFlag]:bg-content-link",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const OptionConfigFlag: FC<HTMLAttributes<HTMLDivElement>> = (props) => {
|
||||
const theme = useTheme();
|
||||
|
||||
export const OptionConfigFlag: FC<HTMLAttributes<HTMLDivElement>> = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className="OptionConfigFlag"
|
||||
css={{
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
display: "block",
|
||||
backgroundColor: theme.palette.divider,
|
||||
lineHeight: 1,
|
||||
padding: "2px 4px",
|
||||
borderRadius: 1,
|
||||
}}
|
||||
className={cn(
|
||||
"OptionConfigFlag text-[10px] font-semibold block bg-border leading-none px-1 py-0.5 rounded-[1px]",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
configOption: (theme) => ({
|
||||
fontSize: 13,
|
||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
||||
fontWeight: 600,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
borderRadius: 4,
|
||||
padding: 6,
|
||||
lineHeight: 1,
|
||||
gap: 6,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
}),
|
||||
|
||||
sourceConfigOption: (theme) => ({
|
||||
border: `1px solid ${theme.roles.active.fill.outline}`,
|
||||
|
||||
"& .OptionConfigFlag": {
|
||||
background: theme.roles.active.fill.solid,
|
||||
},
|
||||
}),
|
||||
|
||||
option: css`
|
||||
font-size: 14px;
|
||||
font-family: ${MONOSPACE_FONT_FAMILY};
|
||||
overflow-wrap: anywhere;
|
||||
user-select: all;
|
||||
|
||||
& ul {
|
||||
padding: 16px;
|
||||
}
|
||||
`,
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import { EllipsisVertical, UserPlusIcon } from "lucide-react";
|
||||
import { type FC, useState } from "react";
|
||||
import { useMutation, useQueryClient } from "react-query";
|
||||
@@ -35,6 +34,7 @@ import {
|
||||
TableRow,
|
||||
} from "#/components/Table/Table";
|
||||
import { isEveryoneGroup } from "#/modules/groups";
|
||||
import { cn } from "#/utils/cn";
|
||||
import type { GroupPageOutletContext } from "./GroupPage";
|
||||
|
||||
const GroupMembersPage: FC = () => {
|
||||
@@ -213,9 +213,12 @@ const GroupMemberRow: FC<GroupMemberRowProps> = ({
|
||||
</TableCell>
|
||||
<TableCell
|
||||
width="40%"
|
||||
css={[styles.status, member.status === "suspended" && styles.suspended]}
|
||||
className={cn(
|
||||
"capitalize",
|
||||
member.status === "suspended" && "text-content-secondary",
|
||||
)}
|
||||
>
|
||||
<div>{member.status}</div>
|
||||
<div>{member.status}</div>{" "}
|
||||
<LastSeen at={member.last_seen_at} className="text-xs" />
|
||||
</TableCell>
|
||||
<TableCell width="1%">
|
||||
@@ -243,13 +246,4 @@ const GroupMemberRow: FC<GroupMemberRowProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
status: {
|
||||
textTransform: "capitalize",
|
||||
},
|
||||
suspended: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export default GroupMembersPage;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import {
|
||||
CircleAlertIcon,
|
||||
CircleCheckIcon,
|
||||
@@ -14,8 +13,8 @@ import {
|
||||
} from "react";
|
||||
import type { HealthCode, HealthSeverity } from "#/api/typesGenerated";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { docs } from "#/utils/docs";
|
||||
import { healthyColor } from "./healthyColor";
|
||||
|
||||
const CONTENT_PADDING = 36;
|
||||
|
||||
@@ -44,11 +43,18 @@ interface HealthIconProps {
|
||||
}
|
||||
|
||||
export const HealthIcon: FC<HealthIconProps> = ({ size, severity }) => {
|
||||
const theme = useTheme();
|
||||
const color = healthyColor(theme, severity);
|
||||
const Icon = severity === "error" ? CircleAlertIcon : CircleCheckIcon;
|
||||
|
||||
return <Icon css={{ width: size, height: size, color }} />;
|
||||
return (
|
||||
<Icon
|
||||
className={cn(
|
||||
severity === "ok" && "text-content-success",
|
||||
severity === "warning" && "text-content-warning",
|
||||
severity === "error" && "text-content-destructive",
|
||||
)}
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface HealthyDotProps {
|
||||
@@ -56,16 +62,14 @@ interface HealthyDotProps {
|
||||
}
|
||||
|
||||
export const HealthyDot: FC<HealthyDotProps> = ({ severity }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 9999,
|
||||
backgroundColor: healthyColor(theme, severity),
|
||||
}}
|
||||
className={cn(
|
||||
"size-2 rounded-full",
|
||||
severity === "ok" && "bg-content-success",
|
||||
severity === "warning" && "bg-content-warning",
|
||||
severity === "error" && "bg-content-destructive",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -93,30 +97,13 @@ export const GridData: FC<HTMLAttributes<HTMLDivElement>> = (props) => {
|
||||
};
|
||||
|
||||
export const GridDataLabel: FC<HTMLAttributes<HTMLSpanElement>> = (props) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<span
|
||||
css={{
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
<span className="text-sm font-medium text-content-secondary" {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
export const GridDataValue: FC<HTMLAttributes<HTMLSpanElement>> = (props) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<span
|
||||
css={{
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return <span className="text-sm text-content-primary" {...props} />;
|
||||
};
|
||||
|
||||
export const SectionLabel: FC<HTMLAttributes<HTMLHeadingElement>> = (props) => {
|
||||
@@ -130,22 +117,9 @@ type PillProps = React.ComponentPropsWithRef<"div"> & {
|
||||
};
|
||||
|
||||
export const Pill: React.FC<PillProps> = ({ icon, children, ...divProps }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
height: 32,
|
||||
borderRadius: 9999,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
padding: 8,
|
||||
gap: 8,
|
||||
cursor: "default",
|
||||
}}
|
||||
className="inline-flex items-center h-8 rounded-full border border-border text-xs font-medium p-2 gap-2 cursor-default"
|
||||
{...divProps}
|
||||
>
|
||||
{cloneElement(icon, { className: "size-[14px]" })}
|
||||
@@ -178,16 +152,13 @@ export const BooleanPill: FC<BooleanPillProps> = ({
|
||||
children,
|
||||
...divProps
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const color = value ? theme.roles.success.outline : theme.roles.error.outline;
|
||||
|
||||
return (
|
||||
<Pill
|
||||
icon={
|
||||
value ? (
|
||||
<CircleCheckIcon css={{ color }} className="size-icon-sm" />
|
||||
<CircleCheckIcon className="size-icon-sm text-content-success" />
|
||||
) : (
|
||||
<CircleMinusIcon css={{ color }} className="size-icon-sm" />
|
||||
<CircleMinusIcon className="size-icon-sm text-content-destructive" />
|
||||
)
|
||||
}
|
||||
{...divProps}
|
||||
@@ -200,20 +171,9 @@ export const BooleanPill: FC<BooleanPillProps> = ({
|
||||
type LogsProps = HTMLAttributes<HTMLDivElement> & { lines: readonly string[] };
|
||||
|
||||
export const Logs: FC<LogsProps> = ({ lines, ...divProps }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: 13,
|
||||
lineHeight: "160%",
|
||||
padding: 24,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
overflowX: "auto",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-all",
|
||||
}}
|
||||
className="font-mono text-[13px] leading-[160%] p-6 bg-surface-secondary overflow-x-auto whitespace-pre-wrap break-all"
|
||||
{...divProps}
|
||||
>
|
||||
{lines.map((line, index) => (
|
||||
@@ -222,9 +182,7 @@ export const Logs: FC<LogsProps> = ({ lines, ...divProps }) => {
|
||||
</span>
|
||||
))}
|
||||
{lines.length === 0 && (
|
||||
<span css={{ color: theme.palette.text.secondary }}>
|
||||
No logs available
|
||||
</span>
|
||||
<span className="text-content-secondary">No logs available</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { CodeIcon } from "lucide-react";
|
||||
import { useOutletContext } from "react-router";
|
||||
import type { HealthcheckReport } from "#/api/typesGenerated";
|
||||
@@ -8,7 +7,6 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "#/components/Tooltip/Tooltip";
|
||||
import { MONOSPACE_FONT_FAMILY } from "#/theme/constants";
|
||||
import { pageTitle } from "#/utils/page";
|
||||
import {
|
||||
Header,
|
||||
@@ -23,7 +21,6 @@ import { DismissWarningButton } from "./DismissWarningButton";
|
||||
const WebsocketPage = () => {
|
||||
const healthStatus = useOutletContext<HealthcheckReport>();
|
||||
const { websocket } = healthStatus;
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -65,22 +62,11 @@ const WebsocketPage = () => {
|
||||
|
||||
<section>
|
||||
<SectionLabel>Body</SectionLabel>
|
||||
<div
|
||||
css={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 8,
|
||||
fontSize: 14,
|
||||
padding: 24,
|
||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
||||
}}
|
||||
>
|
||||
<div className="bg-surface-secondary border border-border rounded-lg text-sm p-6 font-mono">
|
||||
{websocket.body !== "" ? (
|
||||
websocket.body
|
||||
) : (
|
||||
<span css={{ color: theme.palette.text.secondary }}>
|
||||
No body message
|
||||
</span>
|
||||
<span className="text-content-secondary">No body message</span>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { GlobeIcon, HashIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { useOutletContext } from "react-router";
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "#/components/Tooltip/Tooltip";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { createDayString } from "#/utils/createDayString";
|
||||
import { pageTitle } from "#/utils/page";
|
||||
import {
|
||||
@@ -26,7 +26,6 @@ const WorkspaceProxyPage: FC = () => {
|
||||
const healthStatus = useOutletContext<HealthcheckReport>();
|
||||
const { workspace_proxy } = healthStatus;
|
||||
const { regions } = workspace_proxy.workspace_proxies;
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -66,15 +65,10 @@ const WorkspaceProxyPage: FC = () => {
|
||||
return (
|
||||
<div
|
||||
key={region.id}
|
||||
css={{
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${
|
||||
region.healthy
|
||||
? theme.palette.divider
|
||||
: theme.palette.warning.light
|
||||
}`,
|
||||
fontSize: 14,
|
||||
}}
|
||||
className={cn(
|
||||
"rounded-lg border text-sm",
|
||||
region.healthy ? "border-border" : "border-border-warning",
|
||||
)}
|
||||
>
|
||||
<header className="p-6 flex items-center justify-between gap-6">
|
||||
<div className="flex items-center gap-6">
|
||||
@@ -87,7 +81,7 @@ const WorkspaceProxyPage: FC = () => {
|
||||
</div>
|
||||
<div className="leading-[160%]">
|
||||
<h4 className="font-medium m-0">{region.display_name}</h4>
|
||||
<span css={{ color: theme.palette.text.secondary }}>
|
||||
<span className="text-content-secondary">
|
||||
{region.version}
|
||||
</span>
|
||||
</div>
|
||||
@@ -132,17 +126,7 @@ const WorkspaceProxyPage: FC = () => {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
css={{
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "8px 24px",
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
<div className="border-t border-border flex items-center justify-between py-2 px-6 text-xs text-content-secondary">
|
||||
{region.status?.status === "unregistered" ? (
|
||||
<span>Has not connected yet</span>
|
||||
) : warnings.length === 0 && errors.length === 0 ? (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import type { FC } from "react";
|
||||
import type { ApiErrorResponse } from "#/api/errors";
|
||||
import type { ExternalAuthDevice } from "#/api/typesGenerated";
|
||||
@@ -36,7 +35,7 @@ const LoginOAuthDevicePageView: FC<LoginOAuthDevicePageViewProps> = ({
|
||||
<SignInLayout>
|
||||
<Welcome>You've authenticated with GitHub!</Welcome>
|
||||
|
||||
<p css={styles.text}>
|
||||
<p className="text-base text-content-secondary text-center leading-[160%] m-0">
|
||||
If you're not redirected automatically,{" "}
|
||||
<a href={redirectUrl}>click here</a>.
|
||||
</p>
|
||||
@@ -45,13 +44,3 @@ const LoginOAuthDevicePageView: FC<LoginOAuthDevicePageViewProps> = ({
|
||||
};
|
||||
|
||||
export default LoginOAuthDevicePageView;
|
||||
|
||||
const styles = {
|
||||
text: (theme) => ({
|
||||
fontSize: 16,
|
||||
color: theme.palette.text.secondary,
|
||||
textAlign: "center",
|
||||
lineHeight: "160%",
|
||||
margin: 0,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import type { FC, HTMLAttributes, ReactNode } from "react";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
export const HorizontalContainer: FC<HTMLAttributes<HTMLDivElement>> = ({
|
||||
className,
|
||||
...attrs
|
||||
}) => {
|
||||
return <div css={styles.horizontalContainer} {...attrs} />;
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-col gap-16 md:gap-20", className)}
|
||||
{...attrs}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface HorizontalSectionProps
|
||||
@@ -17,71 +24,27 @@ export const HorizontalSection: FC<HorizontalSectionProps> = ({
|
||||
children,
|
||||
title,
|
||||
description,
|
||||
className,
|
||||
...attrs
|
||||
}) => {
|
||||
return (
|
||||
<section css={styles.formSection} {...attrs}>
|
||||
<div css={styles.formSectionInfo}>
|
||||
<h2 css={styles.formSectionInfoTitle}>{title}</h2>
|
||||
<div css={styles.formSectionInfoDescription}>{description}</div>
|
||||
<section
|
||||
className={cn(
|
||||
"flex flex-col gap-4 lg:flex-row lg:gap-[120px]",
|
||||
className,
|
||||
)}
|
||||
{...attrs}
|
||||
>
|
||||
<div className="w-full shrink-0 top-6 max-w-[312px] md:sticky">
|
||||
<h2 className="text-[20px] text-content-primary font-normal m-0 mb-2 flex flex-row items-center gap-3">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="text-[14px] text-content-secondary leading-[160%] m-0">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
horizontalContainer: (theme) => ({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 80,
|
||||
|
||||
[theme.breakpoints.down("md")]: {
|
||||
gap: 64,
|
||||
},
|
||||
}),
|
||||
|
||||
formSection: (theme) => ({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: 120,
|
||||
|
||||
[theme.breakpoints.down("lg")]: {
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
},
|
||||
}),
|
||||
|
||||
formSectionInfo: (theme) => ({
|
||||
width: "100%",
|
||||
flexShrink: 0,
|
||||
top: 24,
|
||||
maxWidth: 312,
|
||||
position: "sticky",
|
||||
|
||||
[theme.breakpoints.down("md")]: {
|
||||
width: "100%",
|
||||
position: "initial",
|
||||
},
|
||||
}),
|
||||
|
||||
formSectionInfoTitle: (theme) => ({
|
||||
fontSize: 20,
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 400,
|
||||
margin: 0,
|
||||
marginBottom: 8,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
}),
|
||||
|
||||
formSectionInfoDescription: (theme) => ({
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "160%",
|
||||
margin: 0,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
* went with a simpler design. If we decide we really do need to display the
|
||||
* users like that, though, know that it will be painful
|
||||
*/
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import type { FC } from "react";
|
||||
import type { LoginType, SlimRole } from "#/api/typesGenerated";
|
||||
import { Pill } from "#/components/Pill/Pill";
|
||||
@@ -78,12 +77,12 @@ export const UserRoleCell: FC<UserRoleCellProps> = ({
|
||||
)}
|
||||
|
||||
<Pill
|
||||
css={
|
||||
className={
|
||||
hasOwnerRole
|
||||
? styles.ownerRoleBadge
|
||||
? "bg-surface-sky border-border-pending"
|
||||
: mainDisplayRole.global
|
||||
? styles.globalRoleBadge
|
||||
: styles.roleBadge
|
||||
? "bg-surface-sky border-border-pending"
|
||||
: "bg-surface-tertiary border-border"
|
||||
}
|
||||
>
|
||||
{mainDisplayRole.global ? (
|
||||
@@ -111,18 +110,11 @@ type OverflowRolePillProps = {
|
||||
};
|
||||
|
||||
const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<Pill
|
||||
css={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderColor: theme.palette.divider,
|
||||
}}
|
||||
>
|
||||
<Pill className="bg-surface-secondary border-border">
|
||||
+{roles.length} more
|
||||
</Pill>
|
||||
</TooltipTrigger>
|
||||
@@ -131,7 +123,11 @@ const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
|
||||
{roles.map((role) => (
|
||||
<Pill
|
||||
key={role.name}
|
||||
css={role.global ? styles.globalRoleBadge : styles.roleBadge}
|
||||
className={
|
||||
role.global
|
||||
? "bg-surface-sky border-border-pending"
|
||||
: "bg-surface-tertiary border-border"
|
||||
}
|
||||
>
|
||||
{role.global ? (
|
||||
<span title="This user has this role for all organizations.">
|
||||
@@ -148,21 +144,6 @@ const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
globalRoleBadge: (theme) => ({
|
||||
backgroundColor: theme.roles.active.background,
|
||||
borderColor: theme.roles.active.outline,
|
||||
}),
|
||||
ownerRoleBadge: (theme) => ({
|
||||
backgroundColor: theme.roles.notice.background,
|
||||
borderColor: theme.roles.notice.outline,
|
||||
}),
|
||||
roleBadge: (theme) => ({
|
||||
backgroundColor: theme.experimental.l2.background,
|
||||
borderColor: theme.experimental.l2.outline,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
const fallbackRole: TieredSlimRole = {
|
||||
name: "member",
|
||||
display_name: "Member",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { ExternalLinkIcon, PlusIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { Link } from "react-router";
|
||||
@@ -25,8 +24,6 @@ export const StarterTemplatePageView: FC<StarterTemplatePageViewProps> = ({
|
||||
starterTemplate,
|
||||
error,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Margins>
|
||||
@@ -73,14 +70,11 @@ export const StarterTemplatePageView: FC<StarterTemplatePageViewProps> = ({
|
||||
</PageHeader>
|
||||
|
||||
<div
|
||||
css={{
|
||||
background: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
className="bg-surface-secondary border border-solid border-border rounded-lg"
|
||||
id="readme"
|
||||
>
|
||||
<div className="px-10 pt-10 pb-16 max-w-[800px] mx-auto">
|
||||
{" "}
|
||||
<MemoizedMarkdown>{starterTemplate.markdown}</MemoizedMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { CSSObject, Interpolation, Theme } from "@emotion/react";
|
||||
import type { FC } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import type { TemplateVersion } from "#/api/typesGenerated";
|
||||
@@ -40,11 +39,11 @@ export const VersionRow: FC<VersionRowProps> = ({
|
||||
{...clickableProps}
|
||||
className={clickableProps.className}
|
||||
>
|
||||
<TableCell css={styles.versionCell}>
|
||||
<TableCell className="!p-0 relative border-b-0">
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
css={styles.versionWrapper}
|
||||
className="px-8 py-4"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Stack direction="row" alignItems="center">
|
||||
@@ -53,7 +52,7 @@ export const VersionRow: FC<VersionRowProps> = ({
|
||||
src={version.created_by.avatar_url}
|
||||
/>
|
||||
<Stack
|
||||
css={styles.versionSummary}
|
||||
className="text-base [font-family:inherit]"
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={1}
|
||||
@@ -67,7 +66,7 @@ export const VersionRow: FC<VersionRowProps> = ({
|
||||
<InfoTooltip title="Message" message={version.message} />
|
||||
)}
|
||||
|
||||
<span css={styles.versionTime}>
|
||||
<span className="text-content-secondary text-[12px]">
|
||||
{new Date(version.created_at).toLocaleTimeString()}
|
||||
</span>
|
||||
</Stack>
|
||||
@@ -138,25 +137,3 @@ export const VersionRow: FC<VersionRowProps> = ({
|
||||
</TimelineEntry>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
versionWrapper: {
|
||||
padding: "16px 32px",
|
||||
},
|
||||
|
||||
versionCell: {
|
||||
padding: "0 !important",
|
||||
position: "relative",
|
||||
borderBottom: 0,
|
||||
},
|
||||
|
||||
versionSummary: (theme) => ({
|
||||
...(theme.typography.body1 as CSSObject),
|
||||
fontFamily: "inherit",
|
||||
}),
|
||||
|
||||
versionTime: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 12,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import { ArrowRightIcon, PlusIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { Link as RouterLink, useNavigate } from "react-router";
|
||||
@@ -42,6 +41,7 @@ import {
|
||||
import { useClickableTableRow } from "#/hooks/useClickableTableRow";
|
||||
import { linkToTemplate, useLinks } from "#/modules/navigation";
|
||||
import type { WorkspacePermissions } from "#/modules/permissions/workspaces";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { createDayString } from "#/utils/createDayString";
|
||||
import { docs } from "#/utils/docs";
|
||||
import {
|
||||
@@ -143,7 +143,10 @@ const TemplateRow: FC<TemplateRowProps> = ({
|
||||
key={template.id}
|
||||
data-testid={`template-${template.id}`}
|
||||
{...clickableRow}
|
||||
css={styles.tableRow}
|
||||
className={cn(
|
||||
clickableRow.className,
|
||||
"[&:hover_.actionButton]:text-content-primary [&:hover_.actionButton]:border-border-hover",
|
||||
)}
|
||||
>
|
||||
<TableCell>
|
||||
<AvatarData
|
||||
@@ -160,7 +163,7 @@ const TemplateRow: FC<TemplateRowProps> = ({
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell css={styles.secondary}>
|
||||
<TableCell className="text-content-secondary">
|
||||
{showOrganizations ? (
|
||||
<AvatarData
|
||||
title={template.organization_display_name}
|
||||
@@ -172,15 +175,15 @@ const TemplateRow: FC<TemplateRowProps> = ({
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell css={styles.secondary}>
|
||||
<TableCell className="text-content-secondary">
|
||||
{formatTemplateBuildTime(template.build_time_stats.start.P50)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell data-chromatic="ignore" css={styles.secondary}>
|
||||
<TableCell data-chromatic="ignore" className="text-content-secondary">
|
||||
{createDayString(template.updated_at)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell css={styles.actionCell}>
|
||||
<TableCell className="whitespace-nowrap">
|
||||
<TemplateActions
|
||||
template={template}
|
||||
workspacePermissions={workspacePermissions}
|
||||
@@ -308,41 +311,3 @@ const TableLoader: FC = () => {
|
||||
</TableLoaderSkeleton>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
templateIconWrapper: {
|
||||
// Same size then the avatar component
|
||||
width: 36,
|
||||
height: 36,
|
||||
padding: 2,
|
||||
|
||||
"& img": {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
actionCell: {
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
cellPrimaryLine: (theme) => ({
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 600,
|
||||
}),
|
||||
cellSecondaryLine: (theme) => ({
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "150%",
|
||||
}),
|
||||
secondary: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
}),
|
||||
tableRow: (theme) => ({
|
||||
"&:hover .actionButton": {
|
||||
color: theme.experimental.l2.hover.text,
|
||||
borderColor: theme.experimental.l2.hover.outline,
|
||||
},
|
||||
}),
|
||||
actionButton: (theme) => ({
|
||||
transition: "none",
|
||||
color: theme.palette.text.primary,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -160,16 +160,8 @@ const TerminalPage: FC = () => {
|
||||
</div>
|
||||
|
||||
{latency && isDebugging && (
|
||||
<span
|
||||
css={{
|
||||
position: "absolute",
|
||||
bottom: 24,
|
||||
right: 24,
|
||||
color: theme.palette.text.disabled,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
Latency: {latency.latencyMS.toFixed(0)}ms
|
||||
<span className="absolute bottom-6 right-6 text-content-disabled text-[14px]">
|
||||
Latency: {latency.latencyMS.toFixed(0)}ms{" "}
|
||||
</span>
|
||||
)}
|
||||
</ThemeOverride>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { EllipsisVertical, RefreshCcwIcon } from "lucide-react";
|
||||
import { type FC, useCallback, useEffect, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
@@ -114,7 +113,6 @@ const ExternalAuthRow: FC<ExternalAuthRowProps> = ({
|
||||
onUnlinkExternalAuth,
|
||||
onValidateExternalAuth,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const name = app.display_name || app.id || app.type;
|
||||
const authURL = `/external-auth/${app.id}`;
|
||||
|
||||
@@ -152,11 +150,7 @@ const ExternalAuthRow: FC<ExternalAuthRowProps> = ({
|
||||
|
||||
{link?.validate_error && (
|
||||
<span>
|
||||
<span
|
||||
css={{ paddingLeft: "1em", color: theme.palette.error.light }}
|
||||
>
|
||||
Error:{" "}
|
||||
</span>
|
||||
<span className="pl-[1em] text-content-destructive">Error: </span>
|
||||
{link?.validate_error}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import {
|
||||
FeatureStageBadge,
|
||||
@@ -36,7 +35,7 @@ export const Section: FC<SectionProps> = ({
|
||||
<section className={className} id={id} data-testid={id}>
|
||||
<div className={layout === "fluid" ? "max-w-full" : "max-w-[500px]"}>
|
||||
{(title || description) && (
|
||||
<div css={styles.header}>
|
||||
<div className="mb-6 flex flex-row justify-between">
|
||||
<div>
|
||||
{title && (
|
||||
<Stack direction="row" alignItems="center">
|
||||
@@ -51,10 +50,14 @@ export const Section: FC<SectionProps> = ({
|
||||
</Stack>
|
||||
)}
|
||||
{description && typeof description === "string" && (
|
||||
<p css={styles.description}>{description}</p>
|
||||
<p className="text-content-secondary text-base m-0 mt-1 leading-[140%]">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
{description && typeof description !== "string" && (
|
||||
<div css={styles.description}>{description}</div>
|
||||
<div className="text-content-secondary text-base m-0 mt-1 leading-[140%]">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{toolbar && <div>{toolbar}</div>}
|
||||
@@ -66,19 +69,3 @@ export const Section: FC<SectionProps> = ({
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
header: {
|
||||
marginBottom: 24,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
description: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 16,
|
||||
margin: 0,
|
||||
marginTop: 4,
|
||||
lineHeight: "140%",
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { css, type Interpolation, type Theme } from "@emotion/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { type FC, useState } from "react";
|
||||
import { Link as RouterLink } from "react-router";
|
||||
@@ -34,7 +33,7 @@ const TokensPage: FC = () => {
|
||||
<>
|
||||
<Section
|
||||
title="Tokens"
|
||||
css={styles.section}
|
||||
className="[&_code]:bg-border [&_code]:text-[12px] [&_code]:px-1 [&_code]:py-0.5 [&_code]:text-content-primary [&_code]:rounded-[2px]"
|
||||
description={
|
||||
<>
|
||||
Tokens are used to authenticate with the Coder API. You can create a
|
||||
@@ -75,16 +74,4 @@ const TokenActions: FC = () => (
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const styles = {
|
||||
section: (theme) => css`
|
||||
& code {
|
||||
background: ${theme.palette.divider};
|
||||
font-size: 12px;
|
||||
padding: 2px 4px;
|
||||
color: ${theme.palette.text.primary};
|
||||
border-radius: 2px;
|
||||
}
|
||||
`,
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export default TokensPage;
|
||||
|
||||
@@ -29,25 +29,12 @@ export const SidebarItem: FC<SidebarItemProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
css={(theme) => ({
|
||||
background: active ? theme.experimental.l2.background : "none",
|
||||
border: "none",
|
||||
fontSize: 14,
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
padding: "0 24px",
|
||||
cursor: "pointer",
|
||||
pointerEvents: active ? "none" : "auto",
|
||||
color: active
|
||||
? theme.palette.text.primary
|
||||
: theme.palette.text.secondary,
|
||||
"&:hover": {
|
||||
background: theme.palette.action.hover,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
})}
|
||||
className={cn(
|
||||
"border-none text-sm w-full text-left px-6 py-2.5 cursor-pointer",
|
||||
active
|
||||
? "bg-surface-secondary text-content-primary pointer-events-none"
|
||||
: "bg-transparent text-content-secondary hover:bg-surface-tertiary hover:text-content-primary",
|
||||
)}
|
||||
{...attrs}
|
||||
>
|
||||
{children}
|
||||
@@ -61,14 +48,7 @@ export const SidebarCaption: FC<HTMLAttributes<HTMLDivElement>> = ({
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
css={(theme) => ({
|
||||
fontSize: 10,
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.secondary,
|
||||
padding: "12px 24px",
|
||||
letterSpacing: "0.5px",
|
||||
})}
|
||||
className="text-2xs uppercase font-medium text-content-secondary py-3 px-6 tracking-[0.5px]"
|
||||
{...attrs}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import { Children, type FC, type HTMLAttributes } from "react";
|
||||
import type { WorkspaceResource } from "#/api/typesGenerated";
|
||||
import { CopyableValue } from "#/components/CopyableValue/CopyableValue";
|
||||
@@ -28,11 +27,14 @@ export const ResourceMetadata: FC<ResourceMetadataProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<header css={styles.root} {...headerProps}>
|
||||
<header
|
||||
className="p-6 flex flex-wrap gap-12 gap-y-6 mb-6 text-sm"
|
||||
{...headerProps}
|
||||
>
|
||||
{metadata.map((meta) => {
|
||||
return (
|
||||
<div css={styles.item} key={meta.key}>
|
||||
<div css={styles.value}>
|
||||
<div className="leading-normal" key={meta.key}>
|
||||
<div className="truncate">
|
||||
{meta.sensitive ? (
|
||||
<SensitiveValue value={meta.value} />
|
||||
) : (
|
||||
@@ -59,40 +61,12 @@ export const ResourceMetadata: FC<ResourceMetadataProps> = ({
|
||||
</MemoizedInlineMarkdown>
|
||||
)}
|
||||
</div>
|
||||
<div css={styles.label}>{meta.key}</div>
|
||||
<div className="text-[13px] text-content-secondary truncate">
|
||||
{meta.key}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
root: () => ({
|
||||
padding: 24,
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 48,
|
||||
rowGap: 24,
|
||||
marginBottom: 24,
|
||||
fontSize: 14,
|
||||
}),
|
||||
|
||||
item: {
|
||||
lineHeight: "1.5",
|
||||
},
|
||||
|
||||
label: (theme) => ({
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
}),
|
||||
|
||||
value: {
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import { type FC, type ReactNode, useState } from "react";
|
||||
import type { AlertProps } from "#/components/Alert/Alert";
|
||||
import { Button, type ButtonProps } from "#/components/Button/Button";
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "#/components/Tooltip/Tooltip";
|
||||
import type { ThemeRole } from "#/theme/roles";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
export type NotificationItem = {
|
||||
title: string;
|
||||
@@ -24,22 +24,61 @@ type NotificationsProps = {
|
||||
icon: ReactNode;
|
||||
};
|
||||
|
||||
// Maps a ThemeRole severity to Tailwind classes for the role's outline
|
||||
// color. These are the closest semantic matches available in the design
|
||||
// token system.
|
||||
const severityStyles: Record<ThemeRole, { svgColor: string; border: string }> =
|
||||
{
|
||||
error: {
|
||||
svgColor: "[&_svg]:text-border-destructive",
|
||||
border: "border-border-destructive",
|
||||
},
|
||||
warning: {
|
||||
svgColor: "[&_svg]:text-border-warning",
|
||||
border: "border-border-warning",
|
||||
},
|
||||
notice: {
|
||||
svgColor: "[&_svg]:text-border-pending",
|
||||
border: "border-border-pending",
|
||||
},
|
||||
info: {
|
||||
svgColor: "[&_svg]:text-content-secondary",
|
||||
border: "border-border",
|
||||
},
|
||||
success: {
|
||||
svgColor: "[&_svg]:text-border-success",
|
||||
border: "border-border-success",
|
||||
},
|
||||
active: {
|
||||
svgColor: "[&_svg]:text-border-pending",
|
||||
border: "border-border-pending",
|
||||
},
|
||||
inactive: {
|
||||
svgColor: "[&_svg]:text-content-disabled",
|
||||
border: "border-border",
|
||||
},
|
||||
danger: {
|
||||
svgColor: "[&_svg]:text-border-warning",
|
||||
border: "border-border-warning",
|
||||
},
|
||||
preview: {
|
||||
svgColor: "[&_svg]:text-border-purple",
|
||||
border: "border-border-purple",
|
||||
},
|
||||
};
|
||||
|
||||
export const Notifications: FC<NotificationsProps> = ({
|
||||
items,
|
||||
severity,
|
||||
icon,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip open={isOpen} onOpenChange={setIsOpen} delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
css={styles.pillContainer}
|
||||
data-testid={`${severity}-notifications`}
|
||||
>
|
||||
<div className="py-2" data-testid={`${severity}-notifications`}>
|
||||
<NotificationPill
|
||||
items={items}
|
||||
severity={severity}
|
||||
@@ -51,10 +90,10 @@ export const Notifications: FC<NotificationsProps> = ({
|
||||
<TooltipContent
|
||||
align="end"
|
||||
collisionPadding={16}
|
||||
className="max-w-[400px] p-0 bg-surface-secondary border-surface-quaternary text-sm text-content-primary"
|
||||
style={{
|
||||
borderColor: theme.roles[severity].outline,
|
||||
}}
|
||||
className={cn(
|
||||
"max-w-[400px] p-0 bg-surface-secondary text-sm text-content-primary",
|
||||
severityStyles[severity].border,
|
||||
)}
|
||||
>
|
||||
{items.map((n) => (
|
||||
<NotificationItem notification={n} key={n.title} />
|
||||
@@ -78,10 +117,10 @@ const NotificationPill: FC<NotificationPillProps> = ({
|
||||
return (
|
||||
<Pill
|
||||
icon={icon}
|
||||
css={(theme) => ({
|
||||
"& svg": { color: theme.roles[severity].outline },
|
||||
borderColor: isTooltipOpen ? theme.roles[severity].outline : undefined,
|
||||
})}
|
||||
className={cn(
|
||||
severityStyles[severity].svgColor,
|
||||
isTooltipOpen && severityStyles[severity].border,
|
||||
)}
|
||||
>
|
||||
{items.length}
|
||||
</Pill>
|
||||
@@ -94,10 +133,12 @@ interface NotificationItemProps {
|
||||
|
||||
const NotificationItem: FC<NotificationItemProps> = ({ notification }) => {
|
||||
return (
|
||||
<article css={styles.notificationItem}>
|
||||
<article className="p-5 leading-normal border-solid border-0 border-t border-border first-of-type:border-t-0">
|
||||
<h4 className="m-0 font-medium">{notification.title}</h4>
|
||||
{notification.detail && (
|
||||
<p css={styles.notificationDetail}>{notification.detail}</p>
|
||||
<p className="m-0 text-content-secondary leading-[1.6] block mt-2">
|
||||
{notification.detail}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-2 flex items-center gap-1">{notification.actions}</div>
|
||||
</article>
|
||||
@@ -107,26 +148,3 @@ const NotificationItem: FC<NotificationItemProps> = ({ notification }) => {
|
||||
export const NotificationActionButton: FC<ButtonProps> = (props) => {
|
||||
return <Button variant="default" size="sm" {...props} />;
|
||||
};
|
||||
|
||||
const styles = {
|
||||
// Adds some spacing from the Tooltip content
|
||||
pillContainer: {
|
||||
padding: "8px 0",
|
||||
},
|
||||
notificationItem: (theme) => ({
|
||||
padding: 20,
|
||||
lineHeight: "1.5",
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
|
||||
"&:first-of-type": {
|
||||
borderTop: 0,
|
||||
},
|
||||
}),
|
||||
notificationDetail: (theme) => ({
|
||||
margin: 0,
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: 1.6,
|
||||
display: "block",
|
||||
marginTop: 8,
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { InfoIcon, TriangleAlertIcon } from "lucide-react";
|
||||
@@ -242,7 +241,7 @@ export const WorkspaceNotifications: FC<WorkspaceNotificationsProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div css={styles.notificationsGroup}>
|
||||
<div className="flex items-center gap-3">
|
||||
{infoNotifications.length > 0 && (
|
||||
<Notifications
|
||||
items={infoNotifications}
|
||||
@@ -262,14 +261,6 @@ export const WorkspaceNotifications: FC<WorkspaceNotificationsProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const styles = {
|
||||
notificationsGroup: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
},
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export const findTroubleshootingURL = (
|
||||
workspaceBuild: WorkspaceBuild,
|
||||
): string | undefined => {
|
||||
|
||||
+1
-4
@@ -185,10 +185,7 @@ export const WorkspaceParametersPageView: FC<
|
||||
</a>
|
||||
</Button>
|
||||
}
|
||||
css={(theme) => ({
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 8,
|
||||
})}
|
||||
className="border border-solid border-border rounded-lg"
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
|
||||
@@ -197,10 +197,7 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
|
||||
|
||||
{pageNumberIsInvalid ? (
|
||||
<EmptyState
|
||||
css={(theme) => ({
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
})}
|
||||
className="border border-solid border-border rounded-lg"
|
||||
message="Page not found"
|
||||
description="The page you are trying to access does not exist."
|
||||
cta={
|
||||
|
||||
Reference in New Issue
Block a user