refactor: remove usage of <Box> and sx (#10702)

This commit is contained in:
Kayla Washburn
2023-12-04 12:09:04 -07:00
committed by GitHub
parent 1e6ea6133c
commit 185400db11
84 changed files with 1440 additions and 1521 deletions
+2
View File
@@ -142,6 +142,8 @@ rules:
- name: "@mui/material/Typography"
message:
"You should use the native HTML elements as span, p, h1, h2, h3..."
- name: "@mui/material/Box"
message: "You should use a <div> instead"
no-unused-vars: "off"
"object-curly-spacing": "off"
react-hooks/exhaustive-deps: warn
-1
View File
@@ -40,7 +40,6 @@
"@mui/icons-material": "5.14.0",
"@mui/lab": "5.0.0-alpha.129",
"@mui/material": "5.14.0",
"@mui/styles": "5.14.0",
"@mui/system": "5.14.0",
"@mui/utils": "5.14.11",
"@vitejs/plugin-react": "4.1.0",
+1 -108
View File
@@ -45,9 +45,6 @@ dependencies:
'@mui/material':
specifier: 5.14.0
version: 5.14.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@mui/styles':
specifier: 5.14.0
version: 5.14.0(@types/react@18.2.6)(react@18.2.0)
'@mui/system':
specifier: 5.14.0
version: 5.14.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0)
@@ -3096,37 +3093,6 @@ packages:
react: 18.2.0
dev: false
/@mui/styles@5.14.0(@types/react@18.2.6)(react@18.2.0):
resolution: {integrity: sha512-+nXPk/7qhlJn2QGSBlB42gM9G4shLDEAfkTqjUoCDsS3qPo7ZlpM2X5SgnCNoYhXCn820R0YxaJYd19rmC3FSA==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.6
'@emotion/hash': 0.9.1
'@mui/private-theming': 5.13.7(@types/react@18.2.6)(react@18.2.0)
'@mui/types': 7.2.4(@types/react@18.2.6)
'@mui/utils': 5.14.11(@types/react@18.2.6)(react@18.2.0)
'@types/react': 18.2.6
clsx: 1.2.1
csstype: 3.1.2
hoist-non-react-statics: 3.3.2
jss: 10.10.0
jss-plugin-camel-case: 10.10.0
jss-plugin-default-unit: 10.10.0
jss-plugin-global: 10.10.0
jss-plugin-nested: 10.10.0
jss-plugin-props-sort: 10.10.0
jss-plugin-rule-value-function: 10.10.0
jss-plugin-vendor-prefixer: 10.10.0
prop-types: 15.8.1
react: 18.2.0
dev: false
/@mui/system@5.14.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0):
resolution: {integrity: sha512-0HZGkX8miJbiNw+rjlZ9l0Cfkz1bSqfSHQH0EH9J+nx0aAm5cBleg9piOlLdCNIWGgecCqsw4x62erGrGjjcJg==}
engines: {node: '>=12.0.0'}
@@ -7218,13 +7184,6 @@ packages:
source-map: 0.6.1
dev: false
/css-vendor@2.0.8:
resolution: {integrity: sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==}
dependencies:
'@babel/runtime': 7.23.2
is-in-browser: 1.1.3
dev: false
/css.escape@1.5.1:
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
dev: true
@@ -9252,10 +9211,6 @@ packages:
resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==}
dev: false
/is-in-browser@1.1.3:
resolution: {integrity: sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==}
dev: false
/is-interactive@1.0.0:
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
engines: {node: '>=8'}
@@ -10174,68 +10129,6 @@ packages:
graceful-fs: 4.2.11
dev: true
/jss-plugin-camel-case@10.10.0:
resolution: {integrity: sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==}
dependencies:
'@babel/runtime': 7.23.2
hyphenate-style-name: 1.0.4
jss: 10.10.0
dev: false
/jss-plugin-default-unit@10.10.0:
resolution: {integrity: sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==}
dependencies:
'@babel/runtime': 7.23.2
jss: 10.10.0
dev: false
/jss-plugin-global@10.10.0:
resolution: {integrity: sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==}
dependencies:
'@babel/runtime': 7.23.2
jss: 10.10.0
dev: false
/jss-plugin-nested@10.10.0:
resolution: {integrity: sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==}
dependencies:
'@babel/runtime': 7.23.2
jss: 10.10.0
tiny-warning: 1.0.3
dev: false
/jss-plugin-props-sort@10.10.0:
resolution: {integrity: sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==}
dependencies:
'@babel/runtime': 7.23.2
jss: 10.10.0
dev: false
/jss-plugin-rule-value-function@10.10.0:
resolution: {integrity: sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==}
dependencies:
'@babel/runtime': 7.23.2
jss: 10.10.0
tiny-warning: 1.0.3
dev: false
/jss-plugin-vendor-prefixer@10.10.0:
resolution: {integrity: sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==}
dependencies:
'@babel/runtime': 7.23.2
css-vendor: 2.0.8
jss: 10.10.0
dev: false
/jss@10.10.0:
resolution: {integrity: sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==}
dependencies:
'@babel/runtime': 7.23.2
csstype: 3.1.2
is-in-browser: 1.1.3
tiny-warning: 1.0.3
dev: false
/jsx-ast-utils@3.3.4:
resolution: {integrity: sha512-fX2TVdCViod6HwKEtSWGHs57oFhVfCMwieb9PuRDgjDPh5XeqJiHFFFJCHxU5cnTc3Bu/GRL+kPiFmw8XWOfKw==}
engines: {node: '>=4.0'}
@@ -14244,7 +14137,7 @@ packages:
peerDependencies:
eslint: '>=7'
meow: ^9.0.0
optionator: 0.9.3
optionator: ^0.9.1
stylelint: '>=13'
typescript: '*'
vite: '>=2.0.0'
+2 -5
View File
@@ -1,8 +1,5 @@
import type { DefaultTheme as MuiTheme } from "@mui/system";
import type { NewTheme } from "theme/experimental";
import type { Theme as MuiTheme } from "@mui/material/styles";
declare module "@emotion/react" {
interface Theme extends MuiTheme {
experimental: NewTheme;
}
interface Theme extends MuiTheme {}
}
+1 -9
View File
@@ -1,14 +1,6 @@
import type {
PaletteColor,
PaletteColorOptions,
Theme,
} from "@mui/material/styles";
import type { PaletteColor, PaletteColorOptions } from "@mui/material/styles";
import type { NewTheme } from "theme/experimental";
declare module "@mui/styles/defaultTheme" {
interface DefaultTheme extends Theme {}
}
declare module "@mui/material/styles" {
interface Theme {
experimental: NewTheme;
+6 -4
View File
@@ -48,12 +48,14 @@ export const ThemeProviders: FC<PropsWithChildren> = ({ children }) => {
);
};
export const AppProviders = ({
children,
queryClient = defaultQueryClient,
}: {
interface AppProvidersProps {
children: ReactNode;
queryClient?: QueryClient;
}
export const AppProviders: FC<AppProvidersProps> = ({
children,
queryClient = defaultQueryClient,
}) => {
return (
<HelmetProvider>
@@ -1,6 +1,3 @@
import Box from "@mui/material/Box";
import { Theme } from "@mui/material/styles";
import useTheme from "@mui/styles/useTheme";
import {
CategoryScale,
Chart as ChartJS,
@@ -22,7 +19,8 @@ import {
HelpTooltipText,
} from "components/HelpTooltip/HelpTooltip";
import dayjs from "dayjs";
import { FC } from "react";
import { useTheme } from "@emotion/react";
import { type FC } from "react";
import { Line } from "react-chartjs-2";
import annotationPlugin from "chartjs-plugin-annotation";
@@ -42,7 +40,7 @@ ChartJS.register(
const USER_LIMIT_DISPLAY_THRESHOLD = 60;
export interface ActiveUserChartProps {
data: { date: string; amount: number }[];
data: Array<{ date: string; amount: number }>;
interval: "day" | "week";
userLimit: number | undefined;
}
@@ -52,7 +50,7 @@ export const ActiveUserChart: FC<ActiveUserChartProps> = ({
interval,
userLimit,
}) => {
const theme: Theme = useTheme();
const theme = useTheme();
const labels = data.map((val) => dayjs(val.date).format("YYYY-MM-DD"));
const chartData = data.map((val) => val.amount);
@@ -137,9 +135,9 @@ export const ActiveUserChart: FC<ActiveUserChartProps> = ({
);
};
export const ActiveUsersTitle = () => {
export const ActiveUsersTitle: FC = () => {
return (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<div css={{ display: "flex", alignItems: "center", gap: 8 }}>
Active Users
<HelpTooltip size="small">
<HelpTooltipTitle>How do we calculate active users?</HelpTooltipTitle>
@@ -148,7 +146,7 @@ export const ActiveUsersTitle = () => {
considered an active user. e.g. apps, web terminal, SSH
</HelpTooltipText>
</HelpTooltip>
</Box>
</div>
);
};
+14 -10
View File
@@ -1,9 +1,15 @@
import { useState, FC, ReactNode } from "react";
import {
useState,
type FC,
type ReactNode,
type PropsWithChildren,
} from "react";
import Collapse from "@mui/material/Collapse";
// eslint-disable-next-line no-restricted-imports -- It is the base component
import MuiAlert, { AlertProps as MuiAlertProps } from "@mui/material/Alert";
import MuiAlert, {
type AlertProps as MuiAlertProps,
} from "@mui/material/Alert";
import Button from "@mui/material/Button";
import Box from "@mui/material/Box";
export type AlertProps = MuiAlertProps & {
actions?: ReactNode;
@@ -32,7 +38,7 @@ export const Alert: FC<AlertProps> = ({
<Collapse in>
<MuiAlert
{...alertProps}
sx={{ textAlign: "left", ...alertProps.sx }}
css={{ textAlign: "left" }}
severity={severity}
action={
<>
@@ -62,15 +68,13 @@ export const Alert: FC<AlertProps> = ({
);
};
export const AlertDetail = ({ children }: { children: ReactNode }) => {
export const AlertDetail: FC<PropsWithChildren> = ({ children }) => {
return (
<Box
component="span"
color={(theme) => theme.palette.text.secondary}
fontSize={13}
<span
css={(theme) => ({ color: theme.palette.text.secondary, fontSize: 13 })}
data-chromatic="ignore"
>
{children}
</Box>
</span>
);
};
+8 -5
View File
@@ -1,9 +1,10 @@
// This is the only place MuiAvatar can be used
// eslint-disable-next-line no-restricted-imports -- Read above
import MuiAvatar, { AvatarProps as MuiAvatarProps } from "@mui/material/Avatar";
import { FC, useId } from "react";
import MuiAvatar, {
type AvatarProps as MuiAvatarProps,
} from "@mui/material/Avatar";
import { type FC, useId } from "react";
import { css, type Interpolation, type Theme } from "@emotion/react";
import { Box } from "@mui/system";
import { visuallyHidden } from "@mui/utils";
export type AvatarProps = MuiAvatarProps & {
@@ -78,6 +79,8 @@ export const AvatarIcon: FC<AvatarIconProps> = ({ src, alt }) => {
const hookId = useId();
const avatarId = `${hookId}-avatar`;
// We use a `visuallyHidden` element instead of setting `alt` to avoid
// splatting the text out on the screen if the image fails to load.
return (
<>
<img
@@ -86,9 +89,9 @@ export const AvatarIcon: FC<AvatarIconProps> = ({ src, alt }) => {
css={{ maxWidth: "50%" }}
aria-labelledby={avatarId}
/>
<Box id={avatarId} sx={visuallyHidden}>
<div id={avatarId} css={{ ...visuallyHidden }}>
{alt}
</Box>
</div>
</>
);
};
@@ -1,18 +1,17 @@
import { DeploymentBanner } from "./DeploymentBanner/DeploymentBanner";
import Snackbar from "@mui/material/Snackbar";
import Link from "@mui/material/Link";
import Button from "@mui/material/Button";
import InfoOutlined from "@mui/icons-material/InfoOutlined";
import { type FC, type HTMLAttributes, Suspense } from "react";
import { Outlet } from "react-router-dom";
import { LicenseBanner } from "components/Dashboard/LicenseBanner/LicenseBanner";
import { Loader } from "components/Loader/Loader";
import { ServiceBanner } from "components/Dashboard/ServiceBanner/ServiceBanner";
import { usePermissions } from "hooks/usePermissions";
import { FC, Suspense } from "react";
import { Outlet } from "react-router-dom";
import { dashboardContentBottomPadding } from "theme/constants";
import { Navbar } from "./Navbar/Navbar";
import Snackbar from "@mui/material/Snackbar";
import Link from "@mui/material/Link";
import Box, { BoxProps } from "@mui/material/Box";
import InfoOutlined from "@mui/icons-material/InfoOutlined";
import Button from "@mui/material/Button";
import { docs } from "utils/docs";
import { Navbar } from "./Navbar/Navbar";
import { DeploymentBanner } from "./DeploymentBanner/DeploymentBanner";
import { useUpdateCheck } from "./useUpdateCheck";
export const DashboardLayout: FC = () => {
@@ -74,21 +73,21 @@ export const DashboardLayout: FC = () => {
}),
}}
message={
<Box display="flex" gap={2}>
<div css={{ display: "flex", gap: 16 }}>
<InfoOutlined
sx={(theme) => ({
css={(theme) => ({
fontSize: 16,
height: 20, // 20 is the height of the text line so we can align them
color: theme.palette.info.light,
})}
/>
<Box>
<p>
Coder {updateCheck.data?.version} is now available. View the{" "}
<Link href={updateCheck.data?.url}>release notes</Link> and{" "}
<Link href={docs("/admin/upgrade")}>upgrade instructions</Link>{" "}
for more information.
</Box>
</Box>
</p>
</div>
}
action={
<Button variant="text" size="small" onClick={updateCheck.dismiss}>
@@ -101,12 +100,14 @@ export const DashboardLayout: FC = () => {
);
};
export const DashboardFullPage = (props: BoxProps) => {
export const DashboardFullPage: FC<HTMLAttributes<HTMLDivElement>> = ({
children,
...attrs
}) => {
return (
<Box
{...props}
sx={{
...props.sx,
<div
{...attrs}
css={{
marginBottom: `-${dashboardContentBottomPadding}px`,
flex: 1,
display: "flex",
@@ -114,6 +115,8 @@ export const DashboardFullPage = (props: BoxProps) => {
flexBasis: 0,
minHeight: "100%",
}}
/>
>
{children}
</div>
);
};
@@ -2,7 +2,6 @@ import Drawer from "@mui/material/Drawer";
import IconButton from "@mui/material/IconButton";
import Divider from "@mui/material/Divider";
import Skeleton from "@mui/material/Skeleton";
import Box from "@mui/material/Box";
import Menu from "@mui/material/Menu";
import Button from "@mui/material/Button";
import MenuItem from "@mui/material/MenuItem";
@@ -45,81 +44,6 @@ export const Language = {
deployment: "Deployment",
};
const styles = {
desktopNavItems: (theme) => css`
display: none;
${theme.breakpoints.up("md")} {
display: flex;
}
`,
mobileMenuButton: (theme) => css`
${theme.breakpoints.up("md")} {
display: none;
}
`,
wrapper: (theme) => css`
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
${theme.breakpoints.up("md")} {
justify-content: flex-start;
}
`,
drawerHeader: {
padding: 16,
paddingTop: 32,
paddingBottom: 32,
},
logo: (theme) => css`
align-items: center;
display: flex;
height: ${navHeight}px;
color: ${theme.palette.text.primary};
padding: 16px;
// svg is for the Coder logo, img is for custom images
& svg,
& img {
height: 100%;
object-fit: contain;
}
`,
drawerLogo: {
padding: 0,
maxHeight: 40,
},
item: {
padding: 0,
},
link: (theme) => css`
align-items: center;
color: ${colors.gray[6]};
display: flex;
flex: 1;
font-size: 16px;
padding: 12px 16px;
text-decoration: none;
transition: background-color 0.15s ease-in-out;
&.active {
color: ${theme.palette.text.primary};
font-weight: 500;
}
&:hover {
background-color: ${theme.palette.action.hover};
}
${theme.breakpoints.up("md")} {
height: ${navHeight}px;
padding: 0 24px;
}
`,
} satisfies Record<string, Interpolation<Theme>>;
interface NavItemsProps {
children?: ReactNode;
className?: string;
@@ -128,9 +52,12 @@ interface NavItemsProps {
canViewAllUsers: boolean;
}
const NavItems: React.FC<NavItemsProps> = (props) => {
const { className, canViewAuditLog, canViewDeployment, canViewAllUsers } =
props;
const NavItems: FC<NavItemsProps> = ({
className,
canViewAuditLog,
canViewDeployment,
canViewAllUsers,
}) => {
const location = useLocation();
const theme = useTheme();
@@ -181,15 +108,16 @@ export const NavbarView: FC<NavbarViewProps> = ({
canViewAllUsers,
proxyContextValue,
}) => {
const theme = useTheme();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
return (
<nav
css={(theme) => ({
css={{
height: navHeight,
borderBottom: `1px solid ${theme.palette.divider}`,
backgroundColor: theme.palette.background.paper,
})}
}}
>
<div css={styles.wrapper}>
<IconButton
@@ -241,13 +169,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
canViewAllUsers={canViewAllUsers}
/>
<Box
display="flex"
marginLeft={{ md: "auto" }}
gap={2}
alignItems="center"
paddingRight={2}
>
<div css={styles.navMenus}>
{proxyContextValue && (
<ProxyMenu proxyContextValue={proxyContextValue} />
)}
@@ -259,15 +181,18 @@ export const NavbarView: FC<NavbarViewProps> = ({
onSignOut={onSignOut}
/>
)}
</Box>
</div>
</div>
</nav>
);
};
const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
proxyContextValue,
}) => {
interface ProxyMenuProps {
proxyContextValue: ProxyContextValue;
}
const ProxyMenu: FC<ProxyMenuProps> = ({ proxyContextValue }) => {
const theme = useTheme();
const buttonRef = useRef<HTMLButtonElement>(null);
const [isOpen, setIsOpen] = useState(false);
const [refetchDate, setRefetchDate] = useState<Date>();
@@ -305,7 +230,7 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
<Skeleton
width="110px"
height={BUTTON_SM_HEIGHT}
sx={{ borderRadius: "9999px", transform: "none" }}
css={{ borderRadius: "9999px", transform: "none" }}
/>
);
}
@@ -317,28 +242,29 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
onClick={() => setIsOpen(true)}
size="small"
endIcon={<KeyboardArrowDownOutlined />}
sx={{
css={{
borderRadius: "999px",
"& .MuiSvgIcon-root": { fontSize: 14 },
}}
>
{selectedProxy ? (
<Box display="flex" gap={1} alignItems="center">
<Box width={16} height={16} lineHeight={0}>
<Box
component="img"
<div css={{ display: "flex", gap: 8, alignItems: "center" }}>
<div css={{ width: 16, height: 16, lineHeight: 0 }}>
<img
src={selectedProxy.icon_url}
alt=""
sx={{ objectFit: "contain" }}
width="100%"
height="100%"
css={{
objectFit: "contain",
width: "100%",
height: "100%",
}}
/>
</Box>
</div>
<ProxyStatusLatency
latency={latencies?.[selectedProxy.id]?.latencyMS}
isLoading={proxyLatencyLoading(selectedProxy)}
/>
</Box>
</div>
) : (
"Select Proxy"
)}
@@ -348,13 +274,13 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
anchorEl={buttonRef.current}
onClick={closeMenu}
onClose={closeMenu}
sx={{ "& .MuiMenu-paper": { py: 1 } }}
css={{ "& .MuiMenu-paper": { paddingTop: 8, paddingBottom: 8 } }}
>
<Box
sx={{
w: "100%",
<div
css={{
width: "100%",
fontSize: 14,
padding: 2,
padding: 16,
maxWidth: "320px",
lineHeight: "140%",
}}
@@ -383,8 +309,8 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
manually selected, otherwise the default primary region will be
used.
</p>
</Box>
<Divider sx={{ borderColor: (theme) => theme.palette.divider }} />
</div>
<Divider css={{ borderColor: theme.palette.divider }} />
{proxyContextValue.proxies
?.sort((a, b) => {
const latencyA = latencies?.[a.id]?.latencyMS ?? Infinity;
@@ -405,33 +331,39 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
}}
key={proxy.id}
selected={proxy.id === selectedProxy?.id}
sx={{
fontSize: 14,
}}
css={{ fontSize: 14 }}
>
<Box display="flex" gap={3} alignItems="center" width="100%">
<Box width={14} height={14} lineHeight={0}>
<Box
component="img"
<div
css={{
display: "flex",
gap: 24,
alignItems: "center",
width: "100%",
}}
>
<div css={{ width: 14, height: 14, lineHeight: 0 }}>
<img
src={proxy.icon_url}
alt=""
sx={{ objectFit: "contain" }}
width="100%"
height="100%"
css={{
objectFit: "contain",
width: "100%",
height: "100%",
}}
/>
</Box>
</div>
{proxy.display_name}
<ProxyStatusLatency
latency={latencies?.[proxy.id]?.latencyMS}
isLoading={proxyLatencyLoading(proxy)}
/>
</Box>
</div>
</MenuItem>
))}
<Divider sx={{ borderColor: (theme) => theme.palette.divider }} />
<Divider css={{ borderColor: theme.palette.divider }} />
{Boolean(permissions.editWorkspaceProxies) && (
<MenuItem
sx={{ fontSize: 14 }}
css={{ fontSize: 14 }}
onClick={() => {
navigate("deployment/workspace-proxies");
}}
@@ -440,7 +372,7 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
</MenuItem>
)}
<MenuItem
sx={{ fontSize: 14 }}
css={{ fontSize: 14 }}
onClick={(e) => {
// Stop the menu from closing
e.stopPropagation();
@@ -455,3 +387,85 @@ const ProxyMenu: FC<{ proxyContextValue: ProxyContextValue }> = ({
</>
);
};
const styles = {
desktopNavItems: (theme) => css`
display: none;
${theme.breakpoints.up("md")} {
display: flex;
}
`,
mobileMenuButton: (theme) => css`
${theme.breakpoints.up("md")} {
display: none;
}
`,
navMenus: (theme) => ({
display: "flex",
gap: 16,
alignItems: "center",
paddingRight: 16,
[theme.breakpoints.up("md")]: {
marginLeft: "auto",
},
}),
wrapper: (theme) => css`
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
${theme.breakpoints.up("md")} {
justify-content: flex-start;
}
`,
drawerHeader: {
padding: 16,
paddingTop: 32,
paddingBottom: 32,
},
logo: (theme) => css`
align-items: center;
display: flex;
height: ${navHeight}px;
color: ${theme.palette.text.primary};
padding: 16px;
// svg is for the Coder logo, img is for custom images
& svg,
& img {
height: 100%;
object-fit: contain;
}
`,
drawerLogo: {
padding: 0,
maxHeight: 40,
},
link: (theme) => css`
align-items: center;
color: ${colors.gray[6]};
display: flex;
flex: 1;
font-size: 16px;
padding: 12px 16px;
text-decoration: none;
transition: background-color 0.15s ease-in-out;
&.active {
color: ${theme.palette.text.primary};
font-weight: 500;
}
&:hover {
background-color: ${theme.palette.action.hover};
}
${theme.breakpoints.up("md")} {
height: ${navHeight}px;
padding: 0 24px;
}
`,
} satisfies Record<string, Interpolation<Theme>>;
@@ -57,7 +57,7 @@ export const UserDropdown: FC<PropsWithChildren<UserDropdownProps>> = ({
>
<Badge overlap="circular">
<UserAvatar
sx={{
css={{
width: BUTTON_SM_HEIGHT,
height: BUTTON_SM_HEIGHT,
fontSize: 16,
@@ -1,14 +1,14 @@
import { act, renderHook, waitFor } from "@testing-library/react";
import { useUpdateCheck } from "./useUpdateCheck";
import { QueryClient, QueryClientProvider } from "react-query";
import { ReactNode } from "react";
import { type FC, type PropsWithChildren } from "react";
import { rest } from "msw";
import { MockUpdateCheck } from "testHelpers/entities";
import { server } from "testHelpers/server";
const createWrapper = () => {
const createWrapper = (): FC<PropsWithChildren> => {
const queryClient = new QueryClient();
return ({ children }: { children: ReactNode }) => (
return ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
@@ -1,18 +1,14 @@
import Box, { type BoxProps } from "@mui/material/Box";
import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined";
import { css, useTheme } from "@emotion/react";
import type { PropsWithChildren, FC } from "react";
import type { HTMLAttributes, PropsWithChildren, FC } from "react";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { DisabledBadge, EnabledBadge } from "../Badges/Badges";
export const OptionName: FC<PropsWithChildren> = (props) => {
const { children } = props;
export const OptionName: FC<PropsWithChildren> = ({ children }) => {
return <span css={{ display: "block" }}>{children}</span>;
};
export const OptionDescription: FC<PropsWithChildren> = (props) => {
const { children } = props;
export const OptionDescription: FC<PropsWithChildren> = ({ children }) => {
const theme = useTheme();
return (
@@ -37,31 +33,20 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
const { children: value } = props;
const theme = useTheme();
const optionStyles = css`
font-size: 14px;
font-family: ${MONOSPACE_FONT_FAMILY};
overflow-wrap: anywhere;
user-select: all;
& ul {
padding: 16px;
}
`;
if (typeof value === "boolean") {
return value ? <EnabledBadge /> : <DisabledBadge />;
}
if (typeof value === "number") {
return <span css={optionStyles}>{value}</span>;
return <span css={styles.option}>{value}</span>;
}
if (!value || value.length === 0) {
return <span css={optionStyles}>Not set</span>;
return <span css={styles.option}>Not set</span>;
}
if (typeof value === "string") {
return <span css={optionStyles}>{value}</span>;
return <span css={styles.option}>{value}</span>;
}
if (typeof value === "object" && !Array.isArray(value)) {
@@ -73,31 +58,31 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
<li
key={option}
css={[
optionStyles,
styles.option,
!isEnabled && {
marginLeft: 32,
color: theme.palette.text.disabled,
},
]}
>
<Box
sx={{
<div
css={{
display: "inline-flex",
alignItems: "center",
}}
>
{isEnabled && (
<CheckCircleOutlined
sx={{
css={(theme) => ({
width: 16,
height: 16,
color: (theme) => theme.palette.success.light,
color: theme.palette.success.light,
margin: "0 8px",
}}
})}
/>
)}
{option}
</Box>
</div>
</li>
))}
</ul>
@@ -108,7 +93,7 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
return (
<ul css={{ listStylePosition: "inside" }}>
{value.map((item) => (
<li key={item} css={optionStyles}>
<li key={item} css={styles.option}>
{item}
</li>
))}
@@ -116,63 +101,83 @@ export const OptionValue: FC<OptionValueProps> = (props) => {
);
}
return <span css={optionStyles}>{JSON.stringify(value)}</span>;
return <span css={styles.option}>{JSON.stringify(value)}</span>;
};
interface OptionConfigProps extends BoxProps {
interface OptionConfigProps extends HTMLAttributes<HTMLDivElement> {
source?: boolean;
}
// OptionalConfig takes a source bool to indicate if the Option is the source of the configured value.
export const OptionConfig = (props: OptionConfigProps) => {
const { source, sx, ...attrs } = props;
// OptionConfig takes a source bool to indicate if the Option is the source of the configured value.
export const OptionConfig: FC<OptionConfigProps> = ({
children,
source,
...attrs
}) => {
const theme = useTheme();
const borderColor = source ? undefined : theme.palette.divider;
return (
<Box
<div
{...attrs}
sx={{
css={{
fontSize: 13,
fontFamily: MONOSPACE_FONT_FAMILY,
fontWeight: 600,
backgroundColor: (theme) =>
source ? theme.palette.primary.dark : theme.palette.background.paper,
backgroundColor: source
? theme.palette.primary.dark
: theme.palette.background.paper,
display: "inline-flex",
alignItems: "center",
borderRadius: 0.25,
borderRadius: 2,
padding: "0 8px",
border: `1px solid ${borderColor}`,
...sx,
}}
/>
>
{children}
</div>
);
};
interface OptionConfigFlagProps extends BoxProps {
interface OptionConfigFlagProps extends HTMLAttributes<HTMLDivElement> {
source?: boolean;
}
export const OptionConfigFlag = (props: OptionConfigFlagProps) => {
const { children, source, sx, ...attrs } = props;
export const OptionConfigFlag: FC<OptionConfigFlagProps> = ({
children,
source,
...attrs
}) => {
const theme = useTheme();
return (
<Box
<div
{...attrs}
sx={{
css={{
fontSize: 10,
fontWeight: 600,
margin: "0 6px 0 -4px",
display: "block",
backgroundColor: (theme) =>
source ? "rgba(0, 0, 0, 0.7)" : theme.palette.divider,
backgroundColor: source ? "rgba(0, 0, 0, 0.7)" : theme.palette.divider,
lineHeight: 1,
padding: "2px 4px",
borderRadius: 0.25,
...sx,
borderRadius: 2,
}}
>
{children}
</Box>
</div>
);
};
const styles = {
option: css`
font-size: 14px;
font-family: ${MONOSPACE_FONT_FAMILY};
overflow-wrap: anywhere;
user-select: all;
& ul {
padding: 16px;
}
`,
};
@@ -5,7 +5,6 @@ import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import { type FC } from "react";
import Box from "@mui/material/Box";
import { css } from "@emotion/react";
import type { ClibaseOption } from "api/typesGenerated";
import {
@@ -17,10 +16,12 @@ import {
} from "components/DeploySettingsLayout/Option";
import { optionValue } from "./optionValue";
const OptionsTable: FC<{
interface OptionsTableProps {
options: ClibaseOption[];
additionalValues?: string[];
}> = ({ options, additionalValues }) => {
}
const OptionsTable: FC<OptionsTableProps> = ({ options, additionalValues }) => {
if (options.length === 0) {
return <p>No options to configure</p>;
}
@@ -60,12 +61,12 @@ const OptionsTable: FC<{
<TableCell>
<OptionName>{option.name}</OptionName>
<OptionDescription>{option.description}</OptionDescription>
<Box
sx={{
marginTop: 3,
<div
css={{
marginTop: 24,
display: "flex",
flexWrap: "wrap",
gap: 1,
gap: 8,
}}
>
{option.flag && (
@@ -92,7 +93,7 @@ const OptionsTable: FC<{
{option.yaml}
</OptionConfig>
)}
</Box>
</div>
</TableCell>
<TableCell>
+12 -14
View File
@@ -1,11 +1,10 @@
import Box from "@mui/material/Box";
import type { FC, ReactNode } from "react";
export interface EmptyStateProps {
/** Text Message to display, placed inside Typography component */
message: string;
/** Longer optional description to display below the message */
description?: string | React.ReactNode;
description?: string | ReactNode;
cta?: ReactNode;
className?: string;
image?: ReactNode;
@@ -15,17 +14,16 @@ export interface EmptyStateProps {
* Component to place on screens or in lists that have no content. Optionally
* provide a button that would allow the user to return from where they were,
* or to add an item that they currently have none of.
*
* EmptyState's props extend the [Material UI Box component](https://material-ui.com/components/box/)
* that you can directly pass props through to to customize the shape and layout of it.
*/
export const EmptyState: FC<React.PropsWithChildren<EmptyStateProps>> = (
props,
) => {
const { message, description, cta, image, ...boxProps } = props;
export const EmptyState: FC<EmptyStateProps> = ({
message,
description,
cta,
image,
...attrs
}) => {
return (
<Box
<div
css={{
overflow: "hidden",
display: "flex",
@@ -37,9 +35,9 @@ export const EmptyState: FC<React.PropsWithChildren<EmptyStateProps>> = (
padding: "80px 40px",
position: "relative",
}}
{...boxProps}
{...attrs}
>
<h5 css={{ fontSize: 24, fontWeight: 400, margin: 0 }}>{message}</h5>
<h5 css={{ fontSize: 24, fontWeight: 500, margin: 0 }}>{message}</h5>
{description && (
<p
css={(theme) => ({
@@ -55,6 +53,6 @@ export const EmptyState: FC<React.PropsWithChildren<EmptyStateProps>> = (
)}
{cta && <div css={{ marginTop: 24 }}>{cta}</div>}
{image}
</Box>
</div>
);
};
+11 -7
View File
@@ -1,3 +1,4 @@
import { type FC } from "react";
import { useMe } from "hooks";
import { BaseOption } from "./options";
import { getUsers } from "api/api";
@@ -67,7 +68,11 @@ export const useUserFilterMenu = ({
export type UserFilterMenu = ReturnType<typeof useUserFilterMenu>;
export const UserMenu = (menu: UserFilterMenu) => {
interface UserMenuProps {
menu: UserFilterMenu;
}
export const UserMenu: FC<UserMenuProps> = ({ menu }) => {
return (
<FilterSearchMenu
id="users-menu"
@@ -85,13 +90,12 @@ export const UserMenu = (menu: UserFilterMenu) => {
);
};
const UserOptionItem = ({
option,
isSelected,
}: {
interface UserOptionItemProps {
option: UserOption;
isSelected?: boolean;
}) => {
}
const UserOptionItem: FC<UserOptionItemProps> = ({ option, isSelected }) => {
return (
<OptionItem
option={option}
@@ -100,7 +104,7 @@ const UserOptionItem = ({
<UserAvatar
username={option.label}
avatarURL={option.avatarUrl}
sx={{ width: 16, height: 16, fontSize: 8 }}
css={{ width: 16, height: 16, fontSize: 8 }}
/>
}
/>
+154 -122
View File
@@ -1,31 +1,37 @@
import { ReactNode, forwardRef, useEffect, useRef, useState } from "react";
import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import KeyboardArrowDown from "@mui/icons-material/KeyboardArrowDown";
import Button, { ButtonProps } from "@mui/material/Button";
import Menu, { MenuProps } from "@mui/material/Menu";
import Button, { type ButtonProps } from "@mui/material/Button";
import Menu, { type MenuProps } from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import SearchOutlined from "@mui/icons-material/SearchOutlined";
import InputAdornment from "@mui/material/InputAdornment";
import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip";
import CloseOutlined from "@mui/icons-material/CloseOutlined";
import { useSearchParams } from "react-router-dom";
import Skeleton, { SkeletonProps } from "@mui/material/Skeleton";
import Skeleton, { type SkeletonProps } from "@mui/material/Skeleton";
import MenuList from "@mui/material/MenuList";
import Divider from "@mui/material/Divider";
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
import CheckOutlined from "@mui/icons-material/CheckOutlined";
import CloseOutlined from "@mui/icons-material/CloseOutlined";
import KeyboardArrowDown from "@mui/icons-material/KeyboardArrowDown";
import SearchOutlined from "@mui/icons-material/SearchOutlined";
import { useTheme } from "@emotion/react";
import {
type FC,
type ReactNode,
forwardRef,
useEffect,
useRef,
useState,
} from "react";
import { useSearchParams } from "react-router-dom";
import {
getValidationErrorMessage,
hasError,
isApiValidationError,
} from "api/errors";
import { useFilterMenu } from "./menu";
import { BaseOption } from "./options";
import MenuList from "@mui/material/MenuList";
import { Loader } from "components/Loader/Loader";
import Divider from "@mui/material/Divider";
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
import { useDebouncedFunction } from "hooks/debounce";
import { useFilterMenu } from "./menu";
import type { BaseOption } from "./options";
export type PresetFilter = {
name: string;
@@ -118,25 +124,29 @@ const stringifyFilter = (filterValue: FilterValues): string => {
return result.trim();
};
const BaseSkeleton = (props: SkeletonProps) => {
const BaseSkeleton: FC<SkeletonProps> = ({ children, ...skeletonProps }) => {
return (
<Skeleton
variant="rectangular"
height={36}
{...props}
sx={{
bgcolor: (theme) => theme.palette.background.paper,
{...skeletonProps}
css={(theme) => ({
backgroundColor: theme.palette.background.paper,
borderRadius: "6px",
...props.sx,
}}
/>
})}
>
{children}
</Skeleton>
);
};
export const SearchFieldSkeleton = () => <BaseSkeleton width="100%" />;
export const MenuSkeleton = () => (
<BaseSkeleton sx={{ minWidth: 200, flexShrink: 0 }} />
);
export const SearchFieldSkeleton: FC = () => {
return <BaseSkeleton width="100%" />;
};
export const MenuSkeleton: FC = () => {
return <BaseSkeleton css={{ minWidth: 200, flexShrink: 0 }} />;
};
type FilterProps = {
filter: ReturnType<typeof useFilter>;
@@ -150,7 +160,7 @@ type FilterProps = {
presets: PresetFilter[];
};
export const Filter = ({
export const Filter: FC<FilterProps> = ({
filter,
isLoading,
error,
@@ -160,7 +170,8 @@ export const Filter = ({
learnMoreLabel2,
learnMoreLink2,
presets,
}: FilterProps) => {
}) => {
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
@@ -185,19 +196,23 @@ export const Filter = ({
const hasFilterQuery = filter.query !== "";
return (
<Box
sx={{
<div
css={{
display: "flex",
flexWrap: ["wrap", undefined, "nowrap"],
gap: 1,
mb: 2,
gap: 8,
marginBottom: 16,
flexWrap: "nowrap",
[theme.breakpoints.down("md")]: {
flexWrap: "wrap",
},
}}
>
{isLoading ? (
skeleton
) : (
<>
<Box sx={{ display: "flex", width: "100%" }}>
<div css={{ display: "flex", width: "100%" }}>
<PresetMenu
onSelect={(query) => filter.update(query)}
presets={presets}
@@ -238,7 +253,7 @@ export const Filter = ({
zIndex: 2,
},
"& input::placeholder": {
color: (theme) => theme.palette.text.secondary,
color: theme.palette.text.secondary,
},
"& .MuiInputAdornment-root": {
marginLeft: 0,
@@ -250,9 +265,9 @@ export const Filter = ({
startAdornment: (
<InputAdornment position="start">
<SearchOutlined
sx={{
css={{
fontSize: 14,
color: (theme) => theme.palette.text.secondary,
color: theme.palette.text.secondary,
}}
/>
</InputAdornment>
@@ -266,43 +281,46 @@ export const Filter = ({
filter.update("");
}}
>
<CloseOutlined sx={{ fontSize: 14 }} />
<CloseOutlined css={{ fontSize: 14 }} />
</IconButton>
</Tooltip>
</InputAdornment>
),
}}
/>
</Box>
</div>
{options}
</>
)}
</Box>
</div>
);
};
const PresetMenu = ({
presets,
learnMoreLink,
learnMoreLabel2,
learnMoreLink2,
onSelect,
}: {
interface PresetMenuProps {
presets: PresetFilter[];
learnMoreLink: string;
learnMoreLabel2?: string;
learnMoreLink2?: string;
onSelect: (query: string) => void;
}
const PresetMenu: FC<PresetMenuProps> = ({
presets,
learnMoreLink,
learnMoreLabel2,
learnMoreLink2,
onSelect,
}) => {
const [isOpen, setIsOpen] = useState(false);
const anchorRef = useRef<HTMLButtonElement>(null);
const theme = useTheme();
return (
<>
<Button
onClick={() => setIsOpen(true)}
ref={anchorRef}
sx={{
css={{
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
flexShrink: 0,
@@ -325,11 +343,11 @@ const PresetMenu = ({
vertical: "top",
horizontal: "left",
}}
sx={{ "& .MuiMenu-paper": { py: 1 } }}
css={{ "& .MuiMenu-paper": { paddingTop: 8, paddingBottom: 8 } }}
>
{presets.map((presetFilter) => (
<MenuItem
sx={{ fontSize: 14 }}
css={{ fontSize: 14 }}
key={presetFilter.name}
onClick={() => {
onSelect(presetFilter.query);
@@ -339,17 +357,17 @@ const PresetMenu = ({
{presetFilter.name}
</MenuItem>
))}
<Divider sx={{ borderColor: (theme) => theme.palette.divider }} />
<Divider css={{ borderColor: theme.palette.divider }} />
<MenuItem
component="a"
href={learnMoreLink}
target="_blank"
sx={{ fontSize: 13, fontWeight: 500 }}
css={{ fontSize: 13, fontWeight: 500 }}
onClick={() => {
setIsOpen(false);
}}
>
<OpenInNewOutlined sx={{ fontSize: "14px !important" }} />
<OpenInNewOutlined css={{ fontSize: "14px !important" }} />
View advanced filtering
</MenuItem>
{learnMoreLink2 && learnMoreLabel2 && (
@@ -357,12 +375,12 @@ const PresetMenu = ({
component="a"
href={learnMoreLink2}
target="_blank"
sx={{ fontSize: 13, fontWeight: 500 }}
css={{ fontSize: 13, fontWeight: 500 }}
onClick={() => {
setIsOpen(false);
}}
>
<OpenInNewOutlined sx={{ fontSize: "14px !important" }} />
<OpenInNewOutlined css={{ fontSize: "14px !important" }} />
{learnMoreLabel2}
</MenuItem>
)}
@@ -371,17 +389,17 @@ const PresetMenu = ({
);
};
export const FilterMenu = <TOption extends BaseOption>({
id,
menu,
label,
children,
}: {
interface FilterMenuProps<TOption extends BaseOption> {
menu: ReturnType<typeof useFilterMenu<TOption>>;
label: ReactNode;
id: string;
children: (values: { option: TOption; isSelected: boolean }) => ReactNode;
}) => {
}
export const FilterMenu = <TOption extends BaseOption>(
props: FilterMenuProps<TOption>,
) => {
const { id, menu, label, children } = props;
const buttonRef = useRef<HTMLButtonElement>(null);
const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -394,7 +412,7 @@ export const FilterMenu = <TOption extends BaseOption>({
<MenuButton
ref={buttonRef}
onClick={() => setIsMenuOpen(true)}
sx={{ minWidth: 200 }}
css={{ minWidth: 200 }}
>
{label}
</MenuButton>
@@ -403,7 +421,7 @@ export const FilterMenu = <TOption extends BaseOption>({
anchorEl={buttonRef.current}
open={isMenuOpen}
onClose={handleClose}
sx={{ "& .MuiPaper-root": { minWidth: 200 } }}
css={{ "& .MuiPaper-root": { minWidth: 200 } }}
// Disabled this so when we clear the filter and do some sorting in the
// search items it does not look strange. Github removes exit transitions
// on their filters as well.
@@ -432,17 +450,19 @@ export const FilterMenu = <TOption extends BaseOption>({
);
};
interface FilterSearchMenuProps<TOption extends BaseOption> {
menu: ReturnType<typeof useFilterMenu<TOption>>;
label: ReactNode;
id: string;
children: (values: { option: TOption; isSelected: boolean }) => ReactNode;
}
export const FilterSearchMenu = <TOption extends BaseOption>({
id,
menu,
label,
children,
}: {
menu: ReturnType<typeof useFilterMenu<TOption>>;
label: ReactNode;
id: string;
children: (values: { option: TOption; isSelected: boolean }) => ReactNode;
}) => {
}: FilterSearchMenuProps<TOption>) => {
const buttonRef = useRef<HTMLButtonElement>(null);
const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -455,7 +475,7 @@ export const FilterSearchMenu = <TOption extends BaseOption>({
<MenuButton
ref={buttonRef}
onClick={() => setIsMenuOpen(true)}
sx={{ minWidth: 200 }}
css={{ minWidth: 200 }}
>
{label}
</MenuButton>
@@ -493,57 +513,70 @@ type OptionItemProps = {
isSelected?: boolean;
};
export const OptionItem = ({ option, left, isSelected }: OptionItemProps) => {
export const OptionItem: FC<OptionItemProps> = ({
option,
left,
isSelected,
}) => {
return (
<Box
display="flex"
alignItems="center"
gap={2}
fontSize={14}
overflow="hidden"
width="100%"
<div
css={{
display: "flex",
alignItems: "center",
gap: 16,
fontSize: 4,
overflow: "hidden",
width: "100%",
}}
>
{left}
<Box component="span" overflow="hidden" textOverflow="ellipsis">
<span css={{ overflow: "hidden", textOverflow: "ellipsis" }}>
{option.label}
</Box>
</span>
{isSelected && (
<CheckOutlined sx={{ width: 16, height: 16, marginLeft: "auto" }} />
<CheckOutlined css={{ width: 16, height: 16, marginLeft: "auto" }} />
)}
</Box>
</div>
);
};
const MenuButton = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
const { children, ...attrs } = props;
return (
<Button
ref={ref}
endIcon={<KeyboardArrowDown />}
{...props}
sx={{
css={{
borderRadius: "6px",
justifyContent: "space-between",
lineHeight: "120%",
...props.sx,
}}
/>
{...attrs}
>
{children}
</Button>
);
});
function SearchMenu<TOption extends { label: string; value: string }>({
interface SearchMenuProps<TOption extends BaseOption>
extends Pick<MenuProps, "anchorEl" | "open" | "onClose" | "id"> {
options?: TOption[];
renderOption: (option: TOption) => ReactNode;
query: string;
onQueryChange: (query: string) => void;
}
function SearchMenu<TOption extends BaseOption>({
options,
renderOption,
query,
onQueryChange,
...menuProps
}: Pick<MenuProps, "anchorEl" | "open" | "onClose" | "id"> & {
options?: TOption[];
renderOption: (option: TOption) => ReactNode;
query: string;
onQueryChange: (query: string) => void;
}) {
}: SearchMenuProps<TOption>) {
const menuListRef = useRef<HTMLUListElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
const theme = useTheme();
return (
<Menu
@@ -552,7 +585,7 @@ function SearchMenu<TOption extends { label: string; value: string }>({
menuProps.onClose && menuProps.onClose(event, reason);
onQueryChange("");
}}
sx={{
css={{
"& .MuiPaper-root": {
width: 320,
paddingY: 0,
@@ -565,33 +598,31 @@ function SearchMenu<TOption extends { label: string; value: string }>({
enter: 250,
exit: 0,
}}
onKeyDown={(e) => {
e.stopPropagation();
if (e.key === "ArrowDown" && menuListRef.current) {
const firstItem = menuListRef.current.firstChild as HTMLElement;
firstItem.focus();
}
}}
>
<Box
component="li"
sx={{
<li
css={{
display: "flex",
alignItems: "center",
paddingLeft: 2,
paddingLeft: 16,
height: 40,
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
}}
onKeyDown={(e) => {
e.stopPropagation();
if (e.key === "ArrowDown" && menuListRef.current) {
const firstItem = menuListRef.current.firstChild as HTMLElement;
firstItem.focus();
}
borderBottom: `1px solid ${theme.palette.divider}`,
}}
>
<SearchOutlined
sx={{
css={{
fontSize: 14,
color: (theme) => theme.palette.text.secondary,
color: theme.palette.text.secondary,
}}
/>
<Box
<input
tabIndex={-1}
component="input"
type="text"
placeholder="Search..."
autoFocus
@@ -600,21 +631,21 @@ function SearchMenu<TOption extends { label: string; value: string }>({
onChange={(e) => {
onQueryChange(e.target.value);
}}
sx={{
css={{
height: "100%",
border: 0,
background: "none",
width: "100%",
marginLeft: 2,
marginLeft: 16,
outline: 0,
"&::placeholder": {
color: (theme) => theme.palette.text.secondary,
color: theme.palette.text.secondary,
},
}}
/>
</Box>
</li>
<Box component="li" sx={{ maxHeight: 480, overflowY: "auto" }}>
<li css={{ maxHeight: 480, overflowY: "auto" }}>
<MenuList
ref={menuListRef}
onKeyDown={(e) => {
@@ -629,22 +660,23 @@ function SearchMenu<TOption extends { label: string; value: string }>({
options.length > 0 ? (
options.map(renderOption)
) : (
<Box
sx={{
<div
css={{
fontSize: 13,
color: (theme) => theme.palette.text.secondary,
color: theme.palette.text.secondary,
textAlign: "center",
py: 1,
paddingTop: 8,
paddingBottom: 8,
}}
>
No results
</Box>
</div>
)
) : (
<Loader size={14} />
)}
</MenuList>
</Box>
</li>
</Menu>
);
}
@@ -45,12 +45,12 @@ export const GlobalSnackbar: FC = () => {
autoHideDuration={notification.msgType === MsgType.Error ? 22000 : 6000}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
message={
<div css={styles.messageWrapper}>
<div css={{ display: "flex" }}>
{notification.msgType === MsgType.Error && (
<ErrorIcon css={styles.errorIcon} />
)}
<div css={styles.message}>
<div css={{ maxWidth: 670 }}>
<span css={styles.messageTitle}>{notification.msg}</span>
{notification.additionalMsgs &&
@@ -64,30 +64,13 @@ export const GlobalSnackbar: FC = () => {
);
};
const styles = {
list: {
paddingLeft: 0,
},
messageWrapper: {
display: "flex",
},
message: {
maxWidth: 670,
},
messageTitle: {
fontSize: 14,
fontWeight: 600,
},
messageSubtitle: {
marginTop: 12,
},
errorIcon: (theme) => ({
color: theme.palette.error.contrastText,
marginRight: 16,
}),
} satisfies Record<string, Interpolation<Theme>>;
interface AdditionalMessageDisplayProps {
message: AdditionalMessage;
}
function AdditionalMessageDisplay({ message }: { message: AdditionalMessage }) {
const AdditionalMessageDisplay: FC<AdditionalMessageDisplayProps> = ({
message,
}) => {
if (isNotificationText(message)) {
return <span css={styles.messageSubtitle}>{message}</span>;
}
@@ -102,7 +85,7 @@ function AdditionalMessageDisplay({ message }: { message: AdditionalMessage }) {
if (isNotificationList(message)) {
return (
<ul css={styles.list}>
<ul css={{ paddingLeft: 0 }}>
{message.map((item, idx) => (
<li key={idx}>
<span css={styles.messageSubtitle}>{item}</span>
@@ -113,4 +96,18 @@ function AdditionalMessageDisplay({ message }: { message: AdditionalMessage }) {
}
return null;
}
};
const styles = {
messageTitle: {
fontSize: 14,
fontWeight: 600,
},
messageSubtitle: {
marginTop: 12,
},
errorIcon: (theme) => ({
color: theme.palette.error.contrastText,
marginRight: 16,
}),
} satisfies Record<string, Interpolation<Theme>>;
+8 -12
View File
@@ -1,11 +1,12 @@
import Box, { type BoxProps } from "@mui/material/Box";
import { useTheme } from "@emotion/react";
import dayjs from "dayjs";
import { type FC, type HTMLAttributes } from "react";
export const LastSeen = ({
value,
...boxProps
}: { value: string } & BoxProps) => {
interface LastSeenProps extends HTMLAttributes<HTMLSpanElement> {
value: string;
}
export const LastSeen: FC<LastSeenProps> = ({ value, ...attrs }) => {
const theme = useTheme();
const t = dayjs(value);
const now = dayjs();
@@ -29,13 +30,8 @@ export const LastSeen = ({
}
return (
<Box
component="span"
data-chromatic="ignore"
{...boxProps}
sx={{ color, ...boxProps.sx }}
>
<span data-chromatic="ignore" css={{ color }} {...attrs}>
{message}
</Box>
</span>
);
};
+16 -14
View File
@@ -1,22 +1,24 @@
import Box, { BoxProps } from "@mui/material/Box";
import CircularProgress from "@mui/material/CircularProgress";
import { FC } from "react";
import { type FC, type HTMLAttributes } from "react";
export const Loader: FC<{ size?: number } & BoxProps> = ({
size = 26,
...boxProps
}) => {
interface LoaderProps extends HTMLAttributes<HTMLDivElement> {
size?: number;
}
export const Loader: FC<LoaderProps> = ({ size = 26, ...attrs }) => {
return (
<Box
p={4}
width="100%"
display="flex"
alignItems="center"
justifyContent="center"
<div
css={{
padding: 4,
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
data-testid="loader"
{...boxProps}
{...attrs}
>
<CircularProgress size={size} />
</Box>
</div>
);
};
+15 -12
View File
@@ -1,18 +1,21 @@
/**
* @file Provides reusable vertical overflow behavior.
*/
import { type ReactNode } from "react";
import { type SystemStyleObject } from "@mui/system";
import Box from "@mui/system/Box";
import { type FC, type ReactNode } from "react";
type Props = {
children: ReactNode;
type OverflowYProps = {
children?: ReactNode;
className?: string;
height?: number;
maxHeight?: number;
sx?: SystemStyleObject;
};
export function OverflowY({ children, height, maxHeight, sx }: Props) {
export const OverflowY: FC<OverflowYProps> = ({
children,
height,
maxHeight,
...attrs
}) => {
const computedHeight = height === undefined ? "100%" : `${height}px`;
// Doing Math.max check to catch cases where height is accidentally larger
@@ -23,17 +26,17 @@ export function OverflowY({ children, height, maxHeight, sx }: Props) {
: `${Math.max(height ?? 0, maxHeight)}px`;
return (
<Box
sx={{
<div
css={{
width: "100%",
height: computedHeight,
maxHeight: computedMaxHeight,
overflowY: "auto",
flexShrink: 1,
...sx,
}}
{...attrs}
>
{children}
</Box>
</div>
);
}
};
+6 -17
View File
@@ -1,21 +1,19 @@
import Box from "@mui/material/Box";
import { type FC, type ReactNode } from "react";
import { type Interpolation, type Theme } from "@emotion/react";
import { EnterpriseBadge } from "components/Badges/Badges";
import { Stack } from "components/Stack/Stack";
export interface PaywallProps {
children?: ReactNode;
message: string;
description?: string | React.ReactNode;
description?: string | ReactNode;
cta?: ReactNode;
}
export const Paywall: FC<React.PropsWithChildren<PaywallProps>> = (props) => {
const { message, description, cta } = props;
export const Paywall: FC<PaywallProps> = ({ message, description, cta }) => {
return (
<Box css={styles.root}>
<div css={styles.header}>
<div css={styles.root}>
<div css={{ marginBottom: 24 }}>
<Stack direction="row" alignItems="center" justifyContent="center">
<h5 css={styles.title}>{message}</h5>
<EnterpriseBadge />
@@ -24,7 +22,7 @@ export const Paywall: FC<React.PropsWithChildren<PaywallProps>> = (props) => {
{description && <p css={styles.description}>{description}</p>}
</div>
{cta}
</Box>
</div>
);
};
@@ -41,9 +39,6 @@ const styles = {
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8,
}),
header: {
marginBottom: 24,
},
title: {
fontWeight: 600,
fontFamily: "inherit",
@@ -58,10 +53,4 @@ const styles = {
color: theme.palette.text.secondary,
fontSize: 14,
}),
enterpriseChip: (theme) => ({
background: theme.palette.success.dark,
color: theme.palette.success.contrastText,
border: `1px solid ${theme.palette.success.light}`,
fontSize: 13,
}),
} satisfies Record<string, Interpolation<Theme>>;
+25 -15
View File
@@ -1,6 +1,7 @@
import {
ReactElement,
ReactNode,
type FC,
type ReactElement,
type ReactNode,
cloneElement,
createContext,
useContext,
@@ -38,13 +39,19 @@ const PopoverContext = createContext<PopoverContextValue | undefined>(
undefined,
);
export const Popover = (props: {
interface PopoverProps {
children: ReactNode | ((popover: PopoverContextValue) => ReactNode); // Allows inline usage
mode?: TriggerMode;
isDefaultOpen?: boolean;
}
export const Popover: FC<PopoverProps> = ({
children,
mode,
isDefaultOpen,
}) => {
const hookId = useId();
const [isOpen, setIsOpen] = useState(props.isDefaultOpen ?? false);
const [isOpen, setIsOpen] = useState(isDefaultOpen ?? false);
const triggerRef = useRef<HTMLElement>(null);
const value: PopoverContextValue = {
@@ -52,14 +59,12 @@ export const Popover = (props: {
setIsOpen,
triggerRef,
id: `${hookId}-popover`,
mode: props.mode ?? "click",
mode: mode ?? "click",
};
return (
<PopoverContext.Provider value={value}>
{typeof props.children === "function"
? props.children(value)
: props.children}
{typeof children === "function" ? children(value) : children}
</PopoverContext.Provider>
);
};
@@ -102,14 +107,19 @@ export const PopoverTrigger = (props: { children: TriggerElement }) => {
type Horizontal = "left" | "right";
export const PopoverContent = (
props: Omit<MuiPopoverProps, "open" | "onClose" | "anchorEl"> & {
horizontal?: Horizontal;
},
) => {
type PopoverContentProps = Omit<
MuiPopoverProps,
"open" | "onClose" | "anchorEl"
> & {
horizontal?: Horizontal;
};
export const PopoverContent: FC<PopoverContentProps> = ({
horizontal = "left",
...popoverProps
}) => {
const popover = usePopover();
const [isReady, setIsReady] = useState(false);
const horizontal = props.horizontal ?? "left";
const hoverMode = popover.mode === "hover";
// This is a hack to make sure the popover is not rendered until the trigger
@@ -143,7 +153,7 @@ export const PopoverContent = (
}}
{...horizontalProps(horizontal)}
{...modeProps(popover)}
{...props}
{...popoverProps}
id={popover.id}
open={popover.isOpen}
onClose={() => popover.setIsOpen(false)}
@@ -1,15 +1,19 @@
import { useTheme } from "@mui/material/styles";
import HelpOutline from "@mui/icons-material/HelpOutline";
import Box from "@mui/material/Box";
import Tooltip from "@mui/material/Tooltip";
import { FC } from "react";
import { type FC } from "react";
import { getLatencyColor } from "utils/latency";
import CircularProgress from "@mui/material/CircularProgress";
export const ProxyStatusLatency: FC<{
interface ProxyStatusLatencyProps {
latency?: number;
isLoading?: boolean;
}> = ({ latency, isLoading }) => {
}
export const ProxyStatusLatency: FC<ProxyStatusLatencyProps> = ({
latency,
isLoading,
}) => {
const theme = useTheme();
const color = getLatencyColor(theme, latency);
@@ -18,7 +22,7 @@ export const ProxyStatusLatency: FC<{
<Tooltip title="Loading latency...">
<CircularProgress
size={14}
sx={{
css={{
// Always use the no latency color for loading.
color: getLatencyColor(theme, undefined),
marginLeft: "auto",
@@ -32,8 +36,8 @@ export const ProxyStatusLatency: FC<{
return (
<Tooltip title="Latency not available">
<HelpOutline
sx={{
ml: "auto",
css={{
marginLeft: "auto",
fontSize: "14px !important",
color,
}}
@@ -43,8 +47,8 @@ export const ProxyStatusLatency: FC<{
}
return (
<Box sx={{ color, fontSize: 13, marginLeft: "auto" }}>
<div css={{ color, fontSize: 13, marginLeft: "auto" }}>
{latency.toFixed(0)}ms
</Box>
</div>
);
};
+14 -7
View File
@@ -1,28 +1,35 @@
import Button, { ButtonProps } from "@mui/material/Button";
import Button, { type ButtonProps } from "@mui/material/Button";
import { useTheme } from "@emotion/react";
import { forwardRef } from "react";
// eslint-disable-next-line react/display-name -- Name is inferred from variable name
export const AgentButton = forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => {
const { children, ...buttonProps } = props;
const theme = useTheme();
return (
<Button
color="neutral"
{...props}
{...buttonProps}
ref={ref}
sx={{
backgroundColor: (theme) => theme.palette.background.default,
css={{
backgroundColor: theme.palette.background.default,
"&:hover": {
backgroundColor: (theme) => theme.palette.background.paper,
backgroundColor: theme.palette.background.paper,
},
// Making them smaller since those icons don't have a padding around them
"& .MuiButton-startIcon": {
width: 12,
height: 12,
"& svg": { width: "100%", height: "100%" },
},
...props.sx,
}}
/>
>
{children}
</Button>
);
},
);
+38 -21
View File
@@ -1,29 +1,34 @@
import dayjs from "dayjs";
import Skeleton from "@mui/material/Skeleton";
import Tooltip from "@mui/material/Tooltip";
import { type Interpolation, type Theme } from "@emotion/react";
import {
createContext,
type FC,
type HTMLAttributes,
useContext,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import { watchAgentMetadata } from "api/api";
import type {
WorkspaceAgent,
WorkspaceAgentMetadata,
} from "api/typesGenerated";
import { Stack } from "components/Stack/Stack";
import dayjs from "dayjs";
import {
createContext,
FC,
useContext,
useEffect,
useRef,
useState,
} from "react";
import Skeleton from "@mui/material/Skeleton";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import Tooltip from "@mui/material/Tooltip";
import Box, { BoxProps } from "@mui/material/Box";
import { type Interpolation, type Theme } from "@emotion/react";
type ItemStatus = "stale" | "valid" | "loading";
export const WatchAgentMetadataContext = createContext(watchAgentMetadata);
const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {
interface MetadataItemProps {
item: WorkspaceAgentMetadata;
}
const MetadataItem: FC<MetadataItemProps> = ({ item }) => {
if (item.result === undefined) {
throw new Error("Metadata item result is undefined");
}
@@ -79,7 +84,7 @@ const MetadataItem: FC<{ item: WorkspaceAgentMetadata }> = ({ item }) => {
return (
<div css={styles.metadata}>
<div css={styles.metadataLabel}>{item.description.display_name}</div>
<Box>{value}</Box>
<div>{value}</div>
</div>
);
};
@@ -106,10 +111,15 @@ export const AgentMetadataView: FC<AgentMetadataViewProps> = ({ metadata }) => {
);
};
export const AgentMetadata: FC<{
interface AgentMetadataProps {
agent: WorkspaceAgent;
storybookMetadata?: WorkspaceAgentMetadata[];
}> = ({ agent, storybookMetadata }) => {
}
export const AgentMetadata: FC<AgentMetadataProps> = ({
agent,
storybookMetadata,
}) => {
const [metadata, setMetadata] = useState<
WorkspaceAgentMetadata[] | undefined
>(undefined);
@@ -182,10 +192,13 @@ export const AgentMetadataSkeleton: FC = () => {
);
};
const StaticWidth = (props: BoxProps) => {
const StaticWidth: FC<HTMLAttributes<HTMLDivElement>> = ({
children,
...attrs
}) => {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
useLayoutEffect(() => {
// Ignore this in storybook
if (!ref.current || process.env.STORYBOOK === "true") {
return;
@@ -196,9 +209,13 @@ const StaticWidth = (props: BoxProps) => {
const autoWidth = ref.current.getBoundingClientRect().width;
ref.current.style.width =
autoWidth > currentWidth ? `${autoWidth}px` : `${currentWidth}px`;
}, [props.children]);
}, [children]);
return <Box {...props} ref={ref} />;
return (
<div ref={ref} {...attrs}>
{children}
</div>
);
};
// These are more or less copied from
@@ -1,4 +1,3 @@
import Box from "@mui/material/Box";
import { PortForwardPopoverView } from "./PortForwardButton";
import type { Meta, StoryObj } from "@storybook/react";
import {
@@ -11,16 +10,16 @@ const meta: Meta<typeof PortForwardPopoverView> = {
component: PortForwardPopoverView,
decorators: [
(Story) => (
<Box
sx={{
<div
css={(theme) => ({
width: 304,
border: (theme) => `1px solid ${theme.palette.divider}`,
borderRadius: 1,
backgroundColor: (theme) => theme.palette.background.paper,
}}
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8,
backgroundColor: theme.palette.background.paper,
})}
>
<Story />
</Box>
</div>
),
],
args: {
@@ -155,13 +155,13 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
/>
<Button
type="submit"
size="small"
variant="text"
css={{
paddingLeft: 12,
paddingRight: 12,
minWidth: 0,
}}
size="small"
>
<OpenInNewOutlined
css={{
@@ -50,7 +50,7 @@ export const VSCodeDesktopButton: FC<
<ButtonGroup
ref={menuAnchorRef}
variant="outlined"
sx={{
css={{
// Workaround to make the border transitions smoothly on button groups
"& > button:hover + button": {
borderLeft: "1px solid #FFF",
@@ -74,9 +74,9 @@ export const VSCodeDesktopButton: FC<
onClick={() => {
setIsVariantMenuOpen(true);
}}
sx={{ px: 0 }}
css={{ paddingLeft: 0, paddingRight: 0 }}
>
<KeyboardArrowDownIcon sx={{ fontSize: 16 }} />
<KeyboardArrowDownIcon css={{ fontSize: 16 }} />
</AgentButton>
</ButtonGroup>
@@ -84,28 +84,28 @@ export const VSCodeDesktopButton: FC<
open={isVariantMenuOpen}
anchorEl={menuAnchorRef.current}
onClose={() => setIsVariantMenuOpen(false)}
sx={{
css={{
"& .MuiMenu-paper": {
width: menuAnchorRef.current?.clientWidth,
},
}}
>
<MenuItem
sx={{ fontSize: 14 }}
css={{ fontSize: 14 }}
onClick={() => {
selectVariant("vscode");
}}
>
<VSCodeIcon sx={{ width: 12, height: 12 }} />
<VSCodeIcon css={{ width: 12, height: 12 }} />
VS Code Desktop
</MenuItem>
<MenuItem
sx={{ fontSize: 14 }}
css={{ fontSize: 14 }}
onClick={() => {
selectVariant("vscode-insiders");
}}
>
<VSCodeInsidersIcon sx={{ width: 12, height: 12 }} />
<VSCodeInsidersIcon css={{ width: 12, height: 12 }} />
VS Code Insiders
</MenuItem>
</Menu>
@@ -1,4 +1,3 @@
import Box from "@mui/material/Box";
import FormControlLabel from "@mui/material/FormControlLabel";
import Radio from "@mui/material/Radio";
import RadioGroup from "@mui/material/RadioGroup";
@@ -154,9 +153,9 @@ export const RichParameterInput: FC<RichParameterInputProps> = ({
data-testid={`parameter-field-${parameter.name}`}
>
<ParameterLabel parameter={parameter} />
<Box sx={{ display: "flex", flexDirection: "column" }}>
<div css={{ display: "flex", flexDirection: "column" }}>
<RichParameterField {...fieldProps} size={size} parameter={parameter} />
</Box>
</div>
</Stack>
);
};
@@ -235,7 +234,7 @@ const RichParameterField: React.FC<RichParameterInputProps> = ({
</MemoizedMarkdown>
}
>
<Box>{option.name}</Box>
<div>{option.name}</div>
</Tooltip>
) : (
<>
+18 -12
View File
@@ -1,22 +1,28 @@
import { type CSSObject, type Interpolation, type Theme } from "@emotion/react";
import Box from "@mui/material/Box";
import { ReactNode, type ComponentProps, type FC } from "react";
import { type FC, type HTMLAttributes, type ReactNode } from "react";
export const Stats: FC<ComponentProps<typeof Box>> = (props) => {
return <Box {...props} css={styles.stats} />;
export const Stats: FC<HTMLAttributes<HTMLDivElement>> = ({
children,
...attrs
}) => {
return (
<div css={styles.stats} {...attrs}>
{children}
</div>
);
};
export const StatsItem: FC<
{
label: string;
value: ReactNode;
} & ComponentProps<typeof Box>
> = ({ label, value, ...divProps }) => {
interface StatsItemProps extends HTMLAttributes<HTMLDivElement> {
label: string;
value: ReactNode;
}
export const StatsItem: FC<StatsItemProps> = ({ label, value, ...attrs }) => {
return (
<Box {...divProps} css={styles.statItem}>
<div css={styles.statItem} {...attrs}>
<span css={styles.statsLabel}>{label}:</span>
<span css={styles.statsValue}>{value}</span>
</Box>
</div>
);
};
@@ -1,14 +1,17 @@
import TableCell from "@mui/material/TableCell";
import TableRow from "@mui/material/TableRow";
import { FC } from "react";
import { EmptyState, EmptyStateProps } from "components/EmptyState/EmptyState";
import { type FC } from "react";
import {
EmptyState,
type EmptyStateProps,
} from "components/EmptyState/EmptyState";
export type TableEmptyProps = EmptyStateProps;
export const TableEmpty: FC<TableEmptyProps> = (props) => {
return (
<TableRow>
<TableCell colSpan={999} sx={{ padding: "0 !important" }}>
<TableCell colSpan={999} css={{ padding: "0 !important" }}>
<EmptyState {...props} />
</TableCell>
</TableRow>
@@ -13,12 +13,14 @@ export const TableLoader: FC = () => {
);
};
export const TableLoaderSkeleton = ({
interface TableLoaderSkeletonProps {
rows?: number;
children?: ReactNode;
}
export const TableLoaderSkeleton: FC<TableLoaderSkeletonProps> = ({
rows = 4,
children,
}: {
rows?: number;
children: ReactNode;
}) => {
if (!isValidElement(children)) {
throw new Error(
@@ -34,6 +36,13 @@ export const TableLoaderSkeleton = ({
);
};
export const TableRowSkeleton = (props: TableRowProps) => {
return <TableRow role="progressbar" data-testid="loader" {...props} />;
export const TableRowSkeleton: FC<TableRowProps> = ({
children,
...rowProps
}) => {
return (
<TableRow role="progressbar" data-testid="loader" {...rowProps}>
{children}
</TableRow>
);
};
+8 -2
View File
@@ -29,7 +29,11 @@ interface TabLinkProps extends NavLinkProps {
className?: string;
}
export const TabLink: FC<TabLinkProps> = ({ className, ...linkProps }) => {
export const TabLink: FC<TabLinkProps> = ({
className,
children,
...linkProps
}) => {
const tabLink = useClassName(classNames.tabLink, []);
const activeTabLink = useClassName(classNames.activeTabLink, []);
@@ -39,7 +43,9 @@ export const TabLink: FC<TabLinkProps> = ({ className, ...linkProps }) => {
cx([tabLink, isActive && activeTabLink, className])
}
{...linkProps}
/>
>
{children}
</NavLink>
);
};
@@ -1,10 +1,9 @@
import dayjs from "dayjs";
import { type ComponentProps, type FC, Fragment } from "react";
import { type FC, Fragment, type HTMLAttributes } from "react";
import type { ProvisionerJobLog } from "api/typesGenerated";
import { BODY_FONT_FAMILY, MONOSPACE_FONT_FAMILY } from "theme/constants";
import { Logs } from "./Logs";
import Box from "@mui/material/Box";
import { type Interpolation, type Theme } from "@emotion/react";
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
const Language = {
seconds: "seconds",
@@ -38,30 +37,30 @@ const getStageDurationInSeconds = (logs: ProvisionerJobLog[]) => {
return completedAt.diff(startedAt, "seconds");
};
export type WorkspaceBuildLogsProps = {
logs: ProvisionerJobLog[];
sticky?: boolean;
interface WorkspaceBuildLogsProps extends HTMLAttributes<HTMLDivElement> {
hideTimestamps?: boolean;
} & ComponentProps<typeof Box>;
sticky?: boolean;
logs: ProvisionerJobLog[];
}
export const WorkspaceBuildLogs: FC<WorkspaceBuildLogsProps> = ({
hideTimestamps,
sticky,
logs,
...boxProps
...attrs
}) => {
const theme = useTheme();
const groupedLogsByStage = groupLogsByStage(logs);
const stages = Object.keys(groupedLogsByStage);
return (
<Box
{...boxProps}
sx={{
border: (theme) => `1px solid ${theme.palette.divider}`,
borderRadius: 1,
<div
css={{
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8,
fontFamily: MONOSPACE_FONT_FAMILY,
...boxProps.sx,
}}
{...attrs}
>
{stages.map((stage) => {
const logs = groupedLogsByStage[stage];
@@ -92,7 +91,7 @@ export const WorkspaceBuildLogs: FC<WorkspaceBuildLogsProps> = ({
</Fragment>
);
})}
</Box>
</div>
);
};
@@ -1,26 +1,29 @@
import { Workspace } from "api/typesGenerated";
import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider";
import { Alert } from "components/Alert/Alert";
import { formatDistanceToNow } from "date-fns";
import Link from "@mui/material/Link";
import { type FC } from "react";
import { Link as RouterLink } from "react-router-dom";
import type { Workspace } from "api/typesGenerated";
import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider";
import { Alert } from "components/Alert/Alert";
export enum Count {
Singular,
Multiple,
}
export const DormantWorkspaceBanner = ({
workspaces,
onDismiss,
shouldRedisplayBanner,
count = Count.Singular,
}: {
interface DormantWorkspaceBannerProps {
workspaces?: Workspace[];
onDismiss: () => void;
shouldRedisplayBanner: boolean;
count?: Count;
}): JSX.Element | null => {
}
export const DormantWorkspaceBanner: FC<DormantWorkspaceBannerProps> = ({
workspaces,
onDismiss,
shouldRedisplayBanner,
count = Count.Singular,
}) => {
const experimentEnabled = useIsWorkspaceActionsEnabled();
if (!workspaces) {
+7 -8
View File
@@ -1,3 +1,5 @@
import capitalize from "lodash/capitalize";
import { type FC } from "react";
import { AuditActions, ResourceTypes } from "api/typesGenerated";
import { UserFilterMenu, UserMenu } from "components/Filter/UserFilter";
import {
@@ -10,7 +12,6 @@ import {
} from "components/Filter/filter";
import { UseFilterMenuOptions, useFilterMenu } from "components/Filter/menu";
import { BaseOption } from "components/Filter/options";
import capitalize from "lodash/capitalize";
import { docs } from "utils/docs";
const PRESET_FILTERS = [
@@ -30,11 +31,7 @@ const PRESET_FILTERS = [
},
];
export const AuditFilter = ({
filter,
error,
menus,
}: {
interface AuditFilterProps {
filter: ReturnType<typeof useFilter>;
error?: unknown;
menus: {
@@ -42,7 +39,9 @@ export const AuditFilter = ({
action: ActionFilterMenu;
resourceType: ResourceTypeFilterMenu;
};
}) => {
}
export const AuditFilter: FC<AuditFilterProps> = ({ filter, error, menus }) => {
return (
<Filter
learnMoreLink={docs("/admin/audit-logs#filtering-logs")}
@@ -54,7 +53,7 @@ export const AuditFilter = ({
<>
<ResourceTypeMenu {...menus.resourceType} />
<ActionMenu {...menus.action} />
<UserMenu {...menus.user} />
<UserMenu menu={menus.user} />
</>
}
skeleton={
@@ -112,7 +112,7 @@ export const CreateUserForm: FC<
return (
<FullPageForm title="Create user">
{isApiError(error) && !hasApiFieldErrors(error) && (
<ErrorAlert error={error} sx={{ mb: 4 }} />
<ErrorAlert error={error} css={{ marginBottom: 32 }} />
)}
<form onSubmit={form.handleSubmit} autoComplete="off">
<Stack spacing={2.5}>
@@ -8,7 +8,7 @@ import { renderHookWithAuth } from "testHelpers/renderHelpers";
function render(workspace?: Workspace) {
return renderHookWithAuth(
({ workspace }: { workspace?: Workspace }) => {
({ workspace }) => {
return useWorkspaceDuplication(workspace);
},
{
@@ -1,5 +1,5 @@
import Box from "@mui/material/Box";
import {
import { type FC } from "react";
import type {
ClibaseOption,
DAUsResponse,
Entitlements,
@@ -24,13 +24,14 @@ export type GeneralSettingsPageViewProps = {
entitlements: Entitlements | undefined;
safeExperiments: Experiments | undefined;
};
export const GeneralSettingsPageView = ({
export const GeneralSettingsPageView: FC<GeneralSettingsPageViewProps> = ({
deploymentOptions,
deploymentDAUs,
deploymentDAUsError,
entitlements,
safeExperiments,
}: GeneralSettingsPageViewProps): JSX.Element => {
}) => {
return (
<>
<Header
@@ -43,7 +44,7 @@ export const GeneralSettingsPageView = ({
<ErrorAlert error={deploymentDAUsError} />
)}
{deploymentDAUs && (
<Box height={200} sx={{ mb: 3 }}>
<div css={{ marginBottom: 24, height: 200 }}>
<ChartSection title={<ActiveUsersTitle />}>
<ActiveUserChart
data={deploymentDAUs.entries}
@@ -55,7 +56,7 @@ export const GeneralSettingsPageView = ({
}
/>
</ChartSection>
</Box>
</div>
)}
<OptionsTable
options={useDeploymentOptions(
@@ -10,7 +10,6 @@ import { useParams, useSearchParams } from "react-router-dom";
import ExternalAuthPageView from "./ExternalAuthPageView";
import { ApiErrorResponse } from "api/errors";
import { isAxiosError } from "axios";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import { SignInLayout } from "components/SignInLayout/SignInLayout";
import { Welcome } from "components/Welcome/Welcome";
@@ -88,12 +87,12 @@ const ExternalAuthPage: FC = () => {
return (
<SignInLayout>
<Welcome message="Failed to validate oauth access token" />
<Box textAlign="center">
<p css={{ textAlign: "center" }}>
Attempted to validate the user&apos;s oauth access token from the
authentication flow. This situation may occur as a result of an
external authentication provider misconfiguration. Verify the
external authentication validation URL is accurately configured.
</Box>
</p>
<br />
<Button
onClick={() => {
+2 -3
View File
@@ -41,7 +41,6 @@ import {
} from "api/queries/groups";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { getErrorMessage } from "api/errors";
import Box from "@mui/material/Box";
import { LastSeen } from "components/LastSeen/LastSeen";
import { type Interpolation, type Theme } from "@emotion/react";
import LoadingButton from "@mui/lab/LoadingButton";
@@ -282,8 +281,8 @@ const GroupMemberRow = (props: {
width="40%"
css={[styles.status, member.status === "suspended" && styles.suspended]}
>
<Box>{member.status}</Box>
<LastSeen value={member.last_seen_at} sx={{ fontSize: 12 }} />
<div>{member.status}</div>
<LastSeen value={member.last_seen_at} css={{ fontSize: 12 }} />
</TableCell>
<TableCell width="1%">
{canUpdate && (
+3 -4
View File
@@ -27,7 +27,6 @@ import type { Group } from "api/typesGenerated";
import { GroupAvatar } from "components/GroupAvatar/GroupAvatar";
import { docs } from "utils/docs";
import Skeleton from "@mui/material/Skeleton";
import { Box } from "@mui/system";
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton";
export type GroupsPageViewProps = {
@@ -156,7 +155,7 @@ export const GroupsPageView: FC<GroupsPageViewProps> = ({
<AvatarGroup
max={10}
total={group.members.length}
sx={{ justifyContent: "flex-end" }}
css={{ justifyContent: "flex-end" }}
>
{group.members.map((member) => (
<UserAvatar
@@ -192,9 +191,9 @@ const TableLoader = () => {
<TableLoaderSkeleton>
<TableRowSkeleton>
<TableCell>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<div css={{ display: "flex", alignItems: "center", gap: 8 }}>
<AvatarDataSkeleton />
</Box>
</div>
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
+59 -48
View File
@@ -1,21 +1,22 @@
import Box from "@mui/material/Box";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { getHealth } from "api/api";
import { Loader } from "components/Loader/Loader";
import { useTab } from "hooks";
import { Helmet } from "react-helmet-async";
import { pageTitle } from "utils/page";
import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined";
import ErrorOutline from "@mui/icons-material/ErrorOutline";
import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter";
import { createDayString } from "utils/createDayString";
import { DashboardFullPage } from "components/Dashboard/DashboardLayout";
import ReplayIcon from "@mui/icons-material/Replay";
import { health, refreshHealth } from "api/queries/debug";
import { useTheme } from "@mui/material/styles";
import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip";
import CircularProgress from "@mui/material/CircularProgress";
import { type Interpolation, type Theme } from "@emotion/react";
import { type FC } from "react";
import { Helmet } from "react-helmet-async";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { getHealth } from "api/api";
import { health, refreshHealth } from "api/queries/debug";
import { useTab } from "hooks";
import { createDayString } from "utils/createDayString";
import { pageTitle } from "utils/page";
import { DashboardFullPage } from "components/Dashboard/DashboardLayout";
import { Loader } from "components/Loader/Loader";
import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter";
const sections = {
derp: "DERP",
@@ -56,23 +57,25 @@ export default function HealthPage() {
);
}
export function HealthPageView({
healthStatus,
tab,
forceRefresh,
isRefreshing,
}: {
interface HealthPageViewProps {
healthStatus: Awaited<ReturnType<typeof getHealth>>;
tab: ReturnType<typeof useTab>;
forceRefresh: () => void;
isRefreshing: boolean;
}) {
}
export const HealthPageView: FC<HealthPageViewProps> = ({
healthStatus,
tab,
forceRefresh,
isRefreshing,
}) => {
const theme = useTheme();
return (
<DashboardFullPage>
<Box
sx={{
<div
css={{
display: "flex",
flexBasis: 0,
flex: 1,
@@ -200,29 +203,10 @@ export function HealthPageView({
onClick={() => {
tab.set(key);
}}
css={{
background: isActive
? theme.palette.action.hover
: "none",
border: "none",
fontSize: 14,
width: "100%",
display: "flex",
alignItems: "center",
gap: 12,
textAlign: "left",
height: 36,
padding: "0 24px",
cursor: "pointer",
pointerEvents: isActive ? "none" : "auto",
color: isActive
? theme.palette.text.primary
: theme.palette.text.secondary,
"&:hover": {
background: theme.palette.action.hover,
color: theme.palette.text.primary,
},
}}
css={[
styles.sectionLink,
isActive && styles.activeSectionLink,
]}
>
{isHealthy ? (
isWarning ? (
@@ -257,8 +241,7 @@ export function HealthPageView({
})}
</nav>
</div>
{/* 62px - navbar and 36px - the bottom bar */}
<Box sx={{ overflowY: "auto", width: "100%" }} data-chromatic="ignore">
<div css={{ overflowY: "auto", width: "100%" }} data-chromatic="ignore">
<SyntaxHighlighter
language="json"
editorProps={{ height: "100%" }}
@@ -268,8 +251,36 @@ export function HealthPageView({
2,
)}
/>
</Box>
</Box>
</div>
</div>
</DashboardFullPage>
);
}
};
const styles = {
sectionLink: (theme) => ({
border: "none",
fontSize: 14,
width: "100%",
display: "flex",
alignItems: "center",
gap: 12,
textAlign: "left",
height: 36,
padding: "0 24px",
cursor: "pointer",
background: "none",
color: theme.palette.text.secondary,
"&:hover": {
background: theme.palette.action.hover,
color: theme.palette.text.primary,
},
}),
activeSectionLink: (theme) => ({
background: theme.palette.action.hover,
pointerEvents: "none",
color: theme.palette.text.primary,
}),
} satisfies Record<string, Interpolation<Theme>>;
+4 -5
View File
@@ -2,7 +2,6 @@ import TextField from "@mui/material/TextField";
import InputAdornment from "@mui/material/InputAdornment";
import Tooltip from "@mui/material/Tooltip";
import IconButton from "@mui/material/IconButton";
import Box from "@mui/material/Box";
import Link from "@mui/material/Link";
import SearchIcon from "@mui/icons-material/SearchOutlined";
import ClearIcon from "@mui/icons-material/CloseOutlined";
@@ -80,7 +79,7 @@ export const IconsPage: FC = () => {
<Tooltip
placement="bottom-end"
title={
<Box
<p
css={{
padding: 8,
fontSize: 13,
@@ -91,7 +90,7 @@ export const IconsPage: FC = () => {
public GitHub repository. Just keep in mind that it should be
relevant to many Coder users, and redistributable under a
permissive license.
</Box>
</p>
}
>
<Link href="https://github.com/coder/coder/tree/main/site/static/icon">
@@ -126,7 +125,7 @@ export const IconsPage: FC = () => {
startAdornment: (
<InputAdornment position="start">
<SearchIcon
sx={{
css={{
fontSize: 14,
color: theme.palette.text.secondary,
}}
@@ -140,7 +139,7 @@ export const IconsPage: FC = () => {
size="small"
onClick={() => setSearchInputText("")}
>
<ClearIcon sx={{ fontSize: 14 }} />
<ClearIcon css={{ fontSize: 14 }} />
</IconButton>
</Tooltip>
</InputAdornment>
+11 -12
View File
@@ -1,30 +1,29 @@
import Button from "@mui/material/Button";
import GitHubIcon from "@mui/icons-material/GitHub";
import KeyIcon from "@mui/icons-material/VpnKey";
import Box from "@mui/material/Box";
import { useId, type FC } from "react";
import { type FC, useId } from "react";
import { Language } from "./SignInForm";
import { type AuthMethods } from "api/typesGenerated";
import type { AuthMethods } from "api/typesGenerated";
import { visuallyHidden } from "@mui/utils";
const iconStyles = {
width: 16,
height: 16,
};
type OAuthSignInFormProps = {
isSigningIn: boolean;
redirectTo: string;
authMethods?: AuthMethods;
};
const iconStyles = {
width: 16,
height: 16,
};
export const OAuthSignInForm: FC<OAuthSignInFormProps> = ({
isSigningIn,
redirectTo,
authMethods,
}) => {
return (
<Box display="grid" gap="16px">
<div css={{ display: "grid", gap: "16px" }}>
{authMethods?.github.enabled && (
<Button
component="a"
@@ -64,7 +63,7 @@ export const OAuthSignInForm: FC<OAuthSignInFormProps> = ({
{authMethods.oidc.signInText || Language.oidcSignIn}
</Button>
)}
</Box>
</div>
);
};
@@ -72,7 +71,7 @@ type OidcIconProps = {
iconUrl: string;
};
function OidcIcon({ iconUrl }: OidcIconProps) {
const OidcIcon: FC<OidcIconProps> = ({ iconUrl }) => {
const hookId = useId();
const oidcId = `${hookId}-oidc`;
@@ -87,4 +86,4 @@ function OidcIcon({ iconUrl }: OidcIconProps) {
</div>
</>
);
}
};
@@ -1,6 +1,5 @@
import CheckOutlined from "@mui/icons-material/CheckOutlined";
import FileCopyOutlined from "@mui/icons-material/FileCopyOutlined";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import FormControlLabel from "@mui/material/FormControlLabel";
import Radio from "@mui/material/Radio";
@@ -101,8 +100,8 @@ export const TemplateEmbedPageView: FC<{
{!buttonValues || !templateParameters ? (
<Loader />
) : (
<Box display="flex" alignItems="flex-start" gap={6}>
<Box flex={1} maxWidth={400}>
<div css={{ display: "flex", alignItems: "flex-start", gap: 48 }}>
<div css={{ flex: 1, maxWidth: 400 }}>
<VerticalForm>
<FormSection
title="Creation mode"
@@ -143,37 +142,39 @@ export const TemplateEmbedPageView: FC<{
</>
)}
</VerticalForm>
</Box>
<Box
display="flex"
height={{
</div>
<div
css={(theme) => ({
// 80px for padding, 36px is for the status bar. We want to use `vh`
// so that it will be relative to the screen and not the parent layout.
height: "calc(100vh - (80px + 36px))",
top: 40,
position: "sticky",
}}
p={8}
flex={1}
alignItems="center"
justifyContent="center"
borderRadius={1}
bgcolor="background.paper"
border={(theme) => `1px solid ${theme.palette.divider}`}
display: "flex",
padding: 64,
flex: 1,
alignItems: "center",
justifyContent: "center",
borderRadius: 8,
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
})}
>
<img src="/open-in-coder.svg" alt="Open in Coder button" />
<Box
p={2}
py={6}
display="flex"
justifyContent="center"
position="absolute"
bottom={0}
left={0}
width="100%"
<div
css={{
padding: "48px 16px",
position: "absolute",
bottom: 0,
left: 0,
display: "flex",
justifyContent: "center",
width: "100%",
}}
>
<Button
sx={{ borderRadius: 999 }}
css={{ borderRadius: 999 }}
startIcon={
clipboard.isCopied ? <CheckOutlined /> : <FileCopyOutlined />
}
@@ -183,9 +184,9 @@ export const TemplateEmbedPageView: FC<{
>
Copy button code
</Button>
</Box>
</Box>
</Box>
</div>
</div>
</div>
)}
</>
);
@@ -1,10 +1,9 @@
import CheckOutlined from "@mui/icons-material/CheckOutlined";
import ExpandMoreOutlined from "@mui/icons-material/ExpandMoreOutlined";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import { useState, useRef } from "react";
import { type FC, useState, useRef } from "react";
export const insightsIntervals = {
day: {
@@ -17,13 +16,12 @@ export const insightsIntervals = {
export type InsightsInterval = keyof typeof insightsIntervals;
export const IntervalMenu = ({
value,
onChange,
}: {
interface IntervalMenuProps {
value: InsightsInterval;
onChange: (value: InsightsInterval) => void;
}) => {
}
export const IntervalMenu: FC<IntervalMenuProps> = ({ value, onChange }) => {
const anchorRef = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
@@ -72,11 +70,11 @@ export const IntervalMenu = ({
}}
>
{label}
<Box css={{ width: 16, height: 16 }}>
<div css={{ width: 16, height: 16 }}>
{value === interval && (
<CheckOutlined css={{ width: 16, height: 16 }} />
)}
</Box>
</div>
</MenuItem>
);
})}
@@ -1,10 +1,9 @@
import CheckOutlined from "@mui/icons-material/CheckOutlined";
import ExpandMoreOutlined from "@mui/icons-material/ExpandMoreOutlined";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import { useState, useRef } from "react";
import { type FC, useState, useRef } from "react";
import { DateRangeValue } from "./DateRange";
import { differenceInWeeks } from "date-fns";
import { lastWeeks } from "./utils";
@@ -13,13 +12,12 @@ import { lastWeeks } from "./utils";
// 6 months.
export const numberOfWeeksOptions = [4, 12, 24] as const;
export const WeekPicker = ({
value,
onChange,
}: {
interface WeekPickerProps {
value: DateRangeValue;
onChange: (value: DateRangeValue) => void;
}) => {
}
export const WeekPicker: FC<WeekPickerProps> = ({ value, onChange }) => {
const anchorRef = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
const numberOfWeeks = differenceInWeeks(value.endDate, value.startDate);
@@ -71,11 +69,11 @@ export const WeekPicker = ({
}}
>
Last {option} weeks
<Box css={{ width: 16, height: 16 }}>
<div css={{ width: 16, height: 16 }}>
{numberOfWeeks === option && (
<CheckOutlined css={{ width: 16, height: 16 }} />
)}
</Box>
</div>
</MenuItem>
);
})}
@@ -1,4 +1,3 @@
import Box from "@mui/material/Box";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
@@ -6,7 +5,7 @@ import TableContainer from "@mui/material/TableContainer";
import TableRow from "@mui/material/TableRow";
import { Timeline } from "components/Timeline/Timeline";
import { type FC } from "react";
import * as TypesGen from "api/typesGenerated";
import type * as TypesGen from "api/typesGenerated";
import { EmptyState } from "components/EmptyState/EmptyState";
import { TableLoader } from "components/TableLoader/TableLoader";
import { VersionRow } from "./VersionRow";
@@ -20,15 +19,17 @@ export const Language = {
export interface VersionsTableProps {
activeVersionId: string;
versions?: TypesGen.TemplateVersion[];
onPromoteClick?: (templateVersionId: string) => void;
onArchiveClick?: (templateVersionId: string) => void;
versions?: TypesGen.TemplateVersion[];
}
export const VersionsTable: FC<VersionsTableProps> = (props) => {
const { versions, onArchiveClick, onPromoteClick, activeVersionId } = props;
export const VersionsTable: FC<VersionsTableProps> = ({
activeVersionId,
versions,
onArchiveClick,
onPromoteClick,
}) => {
const latestVersionId = versions?.reduce(
(latestSoFar, against) => {
if (against.job.status !== "succeeded") {
@@ -73,9 +74,9 @@ export const VersionsTable: FC<VersionsTableProps> = (props) => {
{versions && versions.length === 0 && (
<TableRow>
<TableCell colSpan={999}>
<Box p={4}>
<div css={{ padding: 32 }}>
<EmptyState message={Language.emptyMessage} />
</Box>
</div>
</TableCell>
</TableRow>
)}
@@ -1,8 +1,7 @@
import CircularProgress from "@mui/material/CircularProgress";
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import { type ChangeEvent, useState } from "react";
import { type ChangeEvent, type FC, useState } from "react";
import { css } from "@emotion/react";
import type { Group, User } from "api/typesGenerated";
import { AvatarData } from "components/AvatarData/AvatarData";
@@ -21,13 +20,13 @@ export type UserOrGroupAutocompleteProps = {
exclude: UserOrGroupAutocompleteValue[];
};
export const UserOrGroupAutocomplete: React.FC<
UserOrGroupAutocompleteProps
> = ({ value, onChange, templateID, exclude }) => {
const [autoComplete, setAutoComplete] = useState<{
value: string;
open: boolean;
}>({
export const UserOrGroupAutocomplete: FC<UserOrGroupAutocompleteProps> = ({
value,
onChange,
templateID,
exclude,
}) => {
const [autoComplete, setAutoComplete] = useState({
value: "",
open: false,
});
@@ -90,7 +89,7 @@ export const UserOrGroupAutocomplete: React.FC<
const isOptionGroup = isGroup(option);
return (
<Box component="li" {...props}>
<li {...props}>
<AvatarData
title={
isOptionGroup
@@ -100,7 +99,7 @@ export const UserOrGroupAutocomplete: React.FC<
subtitle={isOptionGroup ? getGroupSubtitle(option) : option.email}
src={option.avatar_url}
/>
</Box>
</li>
);
}}
options={options}
@@ -1,5 +1,6 @@
import { Template } from "api/typesGenerated";
import { TemplateAutostopRequirementDaysValue } from "utils/schedule";
import { type FC } from "react";
import type { Template } from "api/typesGenerated";
import type { TemplateAutostopRequirementDaysValue } from "utils/schedule";
const autostopRequirementDescriptions = {
off: "Workspaces are not required to stop periodically.",
@@ -26,21 +27,24 @@ export const convertAutostopRequirementDaysValue = (
return "off";
};
export const AutostopRequirementDaysHelperText = ({
days = "off",
}: {
interface AutostopRequirementDaysHelperTextProps {
days: TemplateAutostopRequirementDaysValue;
}) => {
}
export const AutostopRequirementDaysHelperText: FC<
AutostopRequirementDaysHelperTextProps
> = ({ days = "off" }) => {
return <span>{autostopRequirementDescriptions[days]}</span>;
};
export const AutostopRequirementWeeksHelperText = ({
days,
weeks,
}: {
interface AutostopRequirementWeeksHelperTextProps {
days: TemplateAutostopRequirementDaysValue;
weeks: number;
}) => {
}
export const AutostopRequirementWeeksHelperText: FC<
AutostopRequirementWeeksHelperTextProps
> = ({ days, weeks }) => {
// Disabled
if (days !== "saturday" && days !== "sunday") {
return (
@@ -71,9 +71,7 @@ export const ScheduleDialog: FC<PropsWithChildren<ScheduleDialogProps>> = ({
<div css={styles.dialogDescription}>{`
This change will result in ${inactiveWorkspacesToGoDormant} workspaces being immediately transitioned to the dormant state and ${inactiveWorkspacesToGoDormantInWeek} over the next seven days. To prevent this, do you want to reset the inactivity period for all template workspaces?`}</div>
<FormControlLabel
sx={{
marginTop: 2,
}}
css={{ marginTop: 16 }}
control={
<Checkbox
size="small"
@@ -90,15 +88,17 @@ export const ScheduleDialog: FC<PropsWithChildren<ScheduleDialogProps>> = ({
{showDeletionWarning && (
<>
<h4>{"Dormancy Auto-Deletion"}</h4>
<h4>Dormancy Auto-Deletion</h4>
<Stack direction="row" spacing={5}>
<div
css={styles.dialogDescription}
>{`This change will result in ${dormantWorkspacesToBeDeleted} workspaces being immediately deleted and ${dormantWorkspacesToBeDeletedInWeek} over the next 7 days. To prevent this, do you want to reset the dormancy period for all template workspaces?`}</div>
<p css={styles.dialogDescription}>
This change will result in {dormantWorkspacesToBeDeleted}{" "}
workspaces being immediately deleted and{" "}
{dormantWorkspacesToBeDeletedInWeek} over the next 7 days. To
prevent this, do you want to reset the dormancy period for all
template workspaces?
</p>
<FormControlLabel
sx={{
marginTop: 2,
}}
css={{ marginTop: 16 }}
control={
<Checkbox
size="small"
@@ -1,8 +1,8 @@
import Button, { ButtonProps } from "@mui/material/Button";
import Button, { type ButtonProps } from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip";
import CreateIcon from "@mui/icons-material/AddOutlined";
import {
import type {
ProvisionerJobLog,
Template,
TemplateVersion,
@@ -47,6 +47,7 @@ import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { Loader } from "components/Loader/Loader";
type Tab = "logs" | "resources" | undefined; // Undefined is to hide the tab
export interface TemplateVersionEditorProps {
template: Template;
templateVersion: TemplateVersion;
@@ -537,13 +538,11 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
<div>
<Alert
severity="error"
sx={{
css={{
borderRadius: 0,
border: 0,
borderBottom: (theme) =>
`1px solid ${theme.palette.divider}`,
borderLeft: (theme) =>
`2px solid ${theme.palette.error.main}`,
borderBottom: `1px solid ${theme.palette.divider}`,
borderLeft: `2px solid ${theme.palette.error.main}`,
}}
>
<AlertTitle>Error during the build</AlertTitle>
@@ -645,17 +644,19 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
);
};
const TopbarButton = (props: ButtonProps) => {
const TopbarButton: FC<ButtonProps> = ({ children, ...buttonProps }) => {
return (
<Button
{...props}
{...buttonProps}
css={{
height: 28,
fontSize: 13,
borderRadius: 4,
padding: "0 12px",
}}
/>
>
{children}
</Button>
);
};
@@ -42,7 +42,6 @@ import { Avatar } from "components/Avatar/Avatar";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { docs } from "utils/docs";
import Skeleton from "@mui/material/Skeleton";
import { Box } from "@mui/system";
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton";
import { Pill } from "components/Pill/Pill";
@@ -62,7 +61,7 @@ export const Language = {
templateTooltipLink: "Manage templates",
};
const TemplateHelpTooltip: React.FC = () => {
const TemplateHelpTooltip: FC = () => {
return (
<HelpTooltip>
<HelpTooltipTitle>{Language.templateTooltipTitle}</HelpTooltipTitle>
@@ -76,7 +75,11 @@ const TemplateHelpTooltip: React.FC = () => {
);
};
const TemplateRow: FC<{ template: Template }> = ({ template }) => {
interface TemplateRowProps {
template: Template;
}
const TemplateRow: FC<TemplateRowProps> = ({ template }) => {
const templatePageLink = `/templates/${template.name}`;
const hasIcon = template.icon && template.icon !== "";
const navigate = useNavigate();
@@ -226,14 +229,14 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = ({
);
};
const TableLoader = () => {
const TableLoader: FC = () => {
return (
<TableLoaderSkeleton>
<TableRowSkeleton>
<TableCell>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<div css={{ display: "flex", alignItems: "center", gap: 8 }}>
<AvatarDataSkeleton />
</Box>
</div>
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
+12 -13
View File
@@ -1,10 +1,10 @@
import Button from "@mui/material/Button";
import Link from "@mui/material/Link";
import { Alert, AlertProps } from "components/Alert/Alert";
import { useState } from "react";
import { type FC, useState } from "react";
import { docs } from "utils/docs";
export const ErrorScriptAlert = () => {
export const ErrorScriptAlert: FC = () => {
return (
<TerminalAlert
severity="warning"
@@ -42,7 +42,7 @@ export const ErrorScriptAlert = () => {
);
};
export const LoadingScriptsAlert = () => {
export const LoadingScriptsAlert: FC = () => {
return (
<TerminalAlert
dismissible
@@ -64,7 +64,7 @@ export const LoadingScriptsAlert = () => {
);
};
export const LoadedScriptsAlert = () => {
export const LoadedScriptsAlert: FC = () => {
return (
<TerminalAlert
severity="success"
@@ -87,25 +87,24 @@ export const LoadedScriptsAlert = () => {
);
};
const TerminalAlert = (props: AlertProps) => {
const TerminalAlert: FC<AlertProps> = (props) => {
return (
<Alert
{...props}
sx={{
css={(theme) => ({
borderRadius: 0,
borderWidth: 0,
borderBottomWidth: 1,
borderBottomColor: (theme) => theme.palette.divider,
backgroundColor: (theme) => theme.palette.background.paper,
borderLeft: (theme) =>
`3px solid ${theme.palette[props.severity!].light}`,
borderBottomColor: theme.palette.divider,
backgroundColor: theme.palette.background.paper,
borderLeft: `3px solid ${theme.palette[props.severity!].light}`,
marginBottom: 1,
}}
})}
/>
);
};
export const DisconnectedAlert = (props: AlertProps) => {
export const DisconnectedAlert: FC<AlertProps> = (props) => {
return (
<TerminalAlert
{...props}
@@ -117,7 +116,7 @@ export const DisconnectedAlert = (props: AlertProps) => {
);
};
const RefreshSessionButton = () => {
const RefreshSessionButton: FC = () => {
const [isRefreshing, setIsRefreshing] = useState(false);
return (
+37 -32
View File
@@ -14,7 +14,6 @@ import "xterm/css/xterm.css";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { pageTitle } from "utils/page";
import { useProxy } from "contexts/ProxyContext";
import Box from "@mui/material/Box";
import { useDashboard } from "components/Dashboard/DashboardProvider";
import type { Region } from "api/typesGenerated";
import { getLatencyColor } from "utils/latency";
@@ -309,7 +308,7 @@ const TerminalPage: FC = () => {
: ""}
</title>
</Helmet>
<Box display="flex" flexDirection="column" height="100vh">
<div css={{ display: "flex", flexDirection: "column", height: "100vh" }}>
{lifecycleState === "start_error" && <ErrorScriptAlert />}
{lifecycleState === "starting" && <LoadingScriptsAlert />}
{lifecycleState === "ready" &&
@@ -321,31 +320,35 @@ const TerminalPage: FC = () => {
latency && (
<BottomBar proxy={selectedProxy} latency={latency.latencyMS} />
)}
</Box>
</div>
</>
);
};
const BottomBar = ({ proxy, latency }: { proxy: Region; latency?: number }) => {
interface BottomBarProps {
proxy: Region;
latency?: number;
}
const BottomBar: FC<BottomBarProps> = ({ proxy, latency }) => {
const theme = useTheme();
const color = getLatencyColor(theme, latency);
return (
<Box
sx={{
<div
css={{
padding: "0 16px",
background: (theme) => theme.palette.background.paper,
background: theme.palette.background.paper,
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
fontSize: 12,
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
borderTop: `1px solid ${theme.palette.divider}`,
}}
>
<Popover mode="hover">
<PopoverTrigger>
<Box
component="button"
<button
aria-label="Terminal latency"
aria-haspopup="true"
css={{
@@ -358,22 +361,22 @@ const BottomBar = ({ proxy, latency }: { proxy: Region; latency?: number }) => {
padding: 8,
}}
>
<Box
sx={{
<div
css={{
height: 6,
width: 6,
backgroundColor: color,
border: 0,
borderRadius: 9999,
borderRadius: 3,
}}
/>
<ProxyStatusLatency latency={latency} />
</Box>
</button>
</PopoverTrigger>
<PopoverContent
id="latency-popover"
disableRestoreFocus
sx={{
css={{
pointerEvents: "none",
"& .MuiPaper-root": {
padding: "8px 16px",
@@ -388,36 +391,38 @@ const BottomBar = ({ proxy, latency }: { proxy: Region; latency?: number }) => {
horizontal: "right",
}}
>
<Box
sx={{
<div
css={{
fontSize: 13,
color: (theme) => theme.palette.text.secondary,
color: theme.palette.text.secondary,
fontWeight: 500,
}}
>
Selected proxy
</Box>
<Box
sx={{ fontSize: 14, display: "flex", gap: 3, alignItems: "center" }}
</div>
<div
css={{
fontSize: 14,
display: "flex",
gap: 3,
alignItems: "center",
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Box width={12} height={12} lineHeight={0}>
<Box
component="img"
<div css={{ display: "flex", alignItems: "center", gap: 1 }}>
<div css={{ width: 12, height: 12, lineHeight: 0 }}>
<img
src={proxy.icon_url}
alt=""
sx={{ objectFit: "contain" }}
width="100%"
height="100%"
css={{ objectFit: "contain", width: "100%", height: "100%" }}
/>
</Box>
</div>
{proxy.display_name}
</Box>
</div>
<ProxyStatusLatency latency={latency} />
</Box>
</div>
</PopoverContent>
</Popover>
</Box>
</div>
);
};
@@ -1,139 +0,0 @@
import { AlertColor } from "@mui/material/Alert/Alert";
import Button from "@mui/material/Button";
import Link from "@mui/material/Link";
import { Alert } from "components/Alert/Alert";
import { ReactNode } from "react";
import { docs } from "utils/docs";
export type TerminalPageAlertType = "error" | "starting" | "success";
type MapAlertTypeToComponent = {
[key in TerminalPageAlertType]: {
severity: AlertColor;
children: ReactNode | undefined;
};
};
const mapAlertTypeToText: MapAlertTypeToComponent = {
error: {
severity: "warning",
children: (
<>
The workspace{" "}
<Link
title="startup script has exited with an error"
href={docs(
"/templates/troubleshooting#startup-script-exited-with-an-error",
)}
target="_blank"
rel="noreferrer"
>
startup script has exited with an error
</Link>
, we recommend reloading this session and{" "}
<Link
title=" debugging the startup script"
href={docs("/templates/troubleshooting#debugging-the-startup-script")}
target="_blank"
rel="noreferrer"
>
debugging the startup script
</Link>{" "}
because{" "}
<Link
title="your workspace may be incomplete."
href={docs(
"/templates/troubleshooting#your-workspace-may-be-incomplete",
)}
target="_blank"
rel="noreferrer"
>
your workspace may be incomplete.
</Link>{" "}
</>
),
},
starting: {
severity: "info",
children: (
<>
Startup script is still running. You can continue using this terminal,
but{" "}
<Link
title="your workspace may be incomplete."
href={docs(
"/templates/troubleshooting#your-workspace-may-be-incomplete",
)}
target="_blank"
rel="noreferrer"
>
{" "}
your workspace may be incomplete.
</Link>
</>
),
},
success: {
severity: "success",
children: (
<>
Startup script has completed successfully. The workspace is ready but
this{" "}
<Link
title="session was started before the startup script finished"
href={docs(
"/templates/troubleshooting#your-workspace-may-be-incomplete",
)}
target="_blank"
rel="noreferrer"
>
session was started before the startup script finished.
</Link>{" "}
To ensure your shell environment is up-to-date, we recommend reloading
this session.
</>
),
},
};
export default ({
alertType,
onDismiss,
}: {
alertType: TerminalPageAlertType;
onDismiss: () => void;
}) => {
const severity = mapAlertTypeToText[alertType].severity;
return (
<Alert
severity={severity}
sx={{
borderRadius: 0,
borderWidth: 0,
borderBottomWidth: 1,
borderBottomColor: (theme) => theme.palette.divider,
backgroundColor: (theme) => theme.palette.background.paper,
borderLeft: (theme) => `3px solid ${theme.palette[severity].light}`,
marginBottom: 1,
}}
onDismiss={onDismiss}
dismissible
actions={[
<Button
key="refresh-session"
size="small"
variant="text"
onClick={() => {
// By redirecting the user without the session in the URL we
// create a new one
window.location.href = window.location.pathname;
}}
>
Refresh session
</Button>,
]}
>
{mapAlertTypeToText[alertType].children}
</Alert>
);
};
@@ -1,16 +1,15 @@
import { Stack } from "@mui/system";
import { type FC } from "react";
import { useQuery } from "react-query";
import { useOrganizationId } from "hooks";
import { useMe } from "hooks/useMe";
import { usePermissions } from "hooks/usePermissions";
import { useQuery } from "react-query";
import { groupsForUser } from "api/queries/groups";
import { useOrganizationId } from "hooks";
import { useAuth } from "components/AuthProvider/AuthProvider";
import { Section } from "components/SettingsLayout/Section";
import { useDashboard } from "components/Dashboard/DashboardProvider";
import { Stack } from "@mui/system";
import { AccountUserGroups } from "./AccountUserGroups";
import { AccountForm } from "./AccountForm";
import { Section } from "components/SettingsLayout/Section";
export const AccountPage: FC = () => {
const me = useMe();
@@ -38,7 +37,6 @@ export const AccountPage: FC = () => {
/>
</Section>
{/* Has <Section> embedded inside because its description is dynamic */}
{hasGroupsFeature && (
<AccountUserGroups
groups={groupsQuery.data}
@@ -1,12 +1,12 @@
import Grid from "@mui/material/Grid";
import { useTheme } from "@emotion/react";
import { type FC } from "react";
import { isApiError } from "api/errors";
import { type Group } from "api/typesGenerated";
import type { Group } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { AvatarCard } from "components/AvatarCard/AvatarCard";
import { Loader } from "components/Loader/Loader";
import { Section } from "components/SettingsLayout/Section";
import Grid from "@mui/material/Grid";
type AccountGroupsProps = {
groups: readonly Group[] | undefined;
@@ -14,11 +14,11 @@ type AccountGroupsProps = {
loading: boolean;
};
export function AccountUserGroups({
export const AccountUserGroups: FC<AccountGroupsProps> = ({
groups,
error,
loading,
}: AccountGroupsProps) {
}) => {
const theme = useTheme();
return (
@@ -71,4 +71,4 @@ export function AccountUserGroups({
</div>
</Section>
);
}
};
@@ -1,4 +1,3 @@
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import CircularProgress from "@mui/material/CircularProgress";
import { type FC, type PropsWithChildren } from "react";
@@ -25,9 +24,9 @@ export const SSHKeysPageView: FC<PropsWithChildren<SSHKeysPageViewProps>> = ({
if (isLoading) {
return (
<Box p={4}>
<div css={{ padding: 32 }}>
<CircularProgress size={26} />
</Box>
</div>
);
}
@@ -57,16 +57,18 @@ export const SecurityPage: FC = () => {
);
};
export const SecurityPageView = ({
security,
oidc,
}: {
interface SecurityPageViewProps {
security: {
form: ComponentProps<typeof SecurityForm>;
};
oidc?: {
section: ComponentProps<typeof SingleSignOnSection>;
};
}
export const SecurityPageView: FC<SecurityPageViewProps> = ({
security,
oidc,
}) => {
return (
<Stack spacing={6}>
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { type FC, useState } from "react";
import { Section } from "components/SettingsLayout/Section";
import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box";
import GitHubIcon from "@mui/icons-material/GitHub";
import KeyIcon from "@mui/icons-material/VpnKey";
import Button from "@mui/material/Button";
@@ -99,7 +99,7 @@ export const useSingleSignOnSection = () => {
};
};
function SSOEmptyState() {
const SSOEmptyState: FC = () => {
return (
<EmptyState
css={(theme) => ({
@@ -117,14 +117,14 @@ function SSOEmptyState() {
}
/>
);
}
};
type SingleSignOnSectionProps = ReturnType<typeof useSingleSignOnSection> & {
authMethods: AuthMethods;
userLoginType: UserLoginType;
};
export const SingleSignOnSection = ({
export const SingleSignOnSection: FC<SingleSignOnSectionProps> = ({
authMethods,
userLoginType,
openConfirmation,
@@ -133,11 +133,12 @@ export const SingleSignOnSection = ({
isUpdating,
isConfirming,
error,
}: SingleSignOnSectionProps) => {
}) => {
const theme = useTheme();
const authList = Object.values(
authMethods,
) as (typeof authMethods)[keyof typeof authMethods][];
const noSsoEnabled = !authList.some((method) => method.enabled);
return (
@@ -147,7 +148,7 @@ export const SingleSignOnSection = ({
title="Single Sign On"
description="Authenticate in Coder using one-click"
>
<Box display="grid" gap="16px">
<div css={{ display: "grid", gap: "16px" }}>
{userLoginType.login_type === "password" ? (
<>
{authMethods.github.enabled && (
@@ -155,7 +156,7 @@ export const SingleSignOnSection = ({
size="large"
fullWidth
disabled={isUpdating}
startIcon={<GitHubIcon sx={{ width: 16, height: 16 }} />}
startIcon={<GitHubIcon css={{ width: 16, height: 16 }} />}
onClick={() => openConfirmation("github")}
>
GitHub
@@ -177,21 +178,21 @@ export const SingleSignOnSection = ({
{noSsoEnabled && <SSOEmptyState />}
</>
) : (
<Box
sx={{
background: (theme) => theme.palette.background.paper,
borderRadius: 1,
border: (theme) => `1px solid ${theme.palette.divider}`,
padding: 2,
<div
css={{
background: theme.palette.background.paper,
borderRadius: 8,
border: `1px solid ${theme.palette.divider}`,
padding: 16,
display: "flex",
gap: 2,
gap: 16,
alignItems: "center",
fontSize: 14,
}}
>
<CheckCircleOutlined
sx={{
color: (theme) => theme.palette.success.light,
css={{
color: theme.palette.success.light,
fontSize: 16,
}}
/>
@@ -203,16 +204,16 @@ export const SingleSignOnSection = ({
: getOIDCLabel(authMethods.oidc)}
</strong>
</span>
<Box sx={{ ml: "auto", lineHeight: 1 }}>
<div css={{ marginLeft: "auto", lineHeight: 1 }}>
{userLoginType.login_type === "github" ? (
<GitHubIcon sx={{ width: 16, height: 16 }} />
<GitHubIcon css={{ width: 16, height: 16 }} />
) : (
<OIDCIcon oidcAuth={authMethods.oidc} />
)}
</Box>
</Box>
</div>
</div>
)}
</Box>
</div>
</Section>
<ConfirmLoginTypeChangeModal
@@ -226,17 +227,20 @@ export const SingleSignOnSection = ({
);
};
const OIDCIcon = ({ oidcAuth }: { oidcAuth: OIDCAuthMethod }) => {
interface OIDCIconProps {
oidcAuth: OIDCAuthMethod;
}
const OIDCIcon: FC<OIDCIconProps> = ({ oidcAuth }) => {
if (!oidcAuth.iconUrl) {
return <KeyIcon sx={{ width: 16, height: 16 }} />;
return <KeyIcon css={{ width: 16, height: 16 }} />;
}
return (
<Box
component="img"
<img
alt="Open ID Connect icon"
src={oidcAuth.iconUrl}
sx={{ width: 16, height: 16 }}
css={{ width: 16, height: 16 }}
/>
);
};
@@ -245,18 +249,20 @@ const getOIDCLabel = (oidcAuth: OIDCAuthMethod) => {
return oidcAuth.signInText || "OpenID Connect";
};
const ConfirmLoginTypeChangeModal = ({
open,
loading,
error,
onClose,
onConfirm,
}: {
interface ConfirmLoginTypeChangeModalProps {
open: boolean;
loading: boolean;
error: unknown;
onClose: () => void;
onConfirm: (password: string) => void;
}
const ConfirmLoginTypeChangeModal: FC<ConfirmLoginTypeChangeModalProps> = ({
open,
loading,
error,
onClose,
onConfirm,
}) => {
const [password, setPassword] = useState("");
@@ -1,4 +1,3 @@
import { useTheme } from "@mui/styles";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
@@ -11,9 +10,10 @@ import { TableEmpty } from "components/TableEmpty/TableEmpty";
import { TableLoader } from "components/TableLoader/TableLoader";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import dayjs from "dayjs";
import { FC } from "react";
import { useTheme } from "@emotion/react";
import { type FC, type ReactNode } from "react";
import IconButton from "@mui/material/IconButton/IconButton";
import { APIKeyWithOwner } from "api/typesGenerated";
import type { APIKeyWithOwner } from "api/typesGenerated";
import relativeTime from "dayjs/plugin/relativeTime";
import { ErrorAlert } from "components/Alert/ErrorAlert";
@@ -32,11 +32,10 @@ export interface TokensPageViewProps {
hasLoaded: boolean;
onDelete: (token: APIKeyWithOwner) => void;
deleteTokenError?: unknown;
children?: ReactNode;
}
export const TokensPageView: FC<
React.PropsWithChildren<TokensPageViewProps>
> = ({
export const TokensPageView: FC<TokensPageViewProps> = ({
tokens,
getTokensError,
isLoading,
@@ -3,6 +3,7 @@ import { AvatarData } from "components/AvatarData/AvatarData";
import { Avatar } from "components/Avatar/Avatar";
import TableCell from "@mui/material/TableCell";
import TableRow from "@mui/material/TableRow";
import { useTheme } from "@emotion/react";
import type { FC, ReactNode } from "react";
import {
HealthyBadge,
@@ -12,12 +13,15 @@ import {
} from "components/Badges/Badges";
import type { ProxyLatencyReport } from "contexts/useProxyLatency";
import { getLatencyColor } from "utils/latency";
import Box from "@mui/material/Box";
export const ProxyRow: FC<{
interface ProxyRowProps {
latency?: ProxyLatencyReport;
proxy: Region;
}> = ({ proxy, latency }) => {
}
export const ProxyRow: FC<ProxyRowProps> = ({ proxy, latency }) => {
const theme = useTheme();
// If we have a more specific proxy status, use that.
// All users can see healthy/unhealthy, some can see more.
let statusBadge = <ProxyStatus proxy={proxy} />;
@@ -56,16 +60,15 @@ export const ProxyRow: FC<{
/>
</TableCell>
<TableCell sx={{ fontSize: 14 }}>{proxy.path_app_url}</TableCell>
<TableCell sx={{ fontSize: 14 }}>{statusBadge}</TableCell>
<TableCell css={{ fontSize: 14 }}>{proxy.path_app_url}</TableCell>
<TableCell css={{ fontSize: 14 }}>{statusBadge}</TableCell>
<TableCell
sx={{
css={{
fontSize: 14,
textAlign: "right",
color: (theme) =>
latency
? getLatencyColor(theme, latency.latencyMS)
: theme.palette.text.secondary,
color: latency
? getLatencyColor(theme, latency.latencyMS)
: theme.palette.text.secondary,
}}
>
{latency ? `${latency.latencyMS.toFixed(0)} ms` : "Not available"}
@@ -75,7 +78,7 @@ export const ProxyRow: FC<{
<TableRow>
<TableCell
colSpan={4}
sx={{ padding: "0px !important", borderBottom: 0 }}
css={{ padding: "0 !important", borderBottom: 0 }}
>
<ProxyMessagesRow proxy={proxy as WorkspaceProxy} />
</TableCell>
@@ -85,30 +88,22 @@ export const ProxyRow: FC<{
);
};
const ProxyMessagesRow: FC<{
interface ProxyMessagesRowProps {
proxy: WorkspaceProxy;
}> = ({ proxy }) => {
}
const ProxyMessagesRow: FC<ProxyMessagesRowProps> = ({ proxy }) => {
const theme = useTheme();
return (
<>
<ProxyMessagesList
title={
<Box
component="span"
sx={{ color: (theme) => theme.palette.error.light }}
>
Errors
</Box>
}
title={<span css={{ color: theme.palette.error.light }}>Errors</span>}
messages={proxy.status?.report?.errors}
/>
<ProxyMessagesList
title={
<Box
component="span"
sx={{ color: (theme) => theme.palette.warning.light }}
>
Warnings
</Box>
<span css={{ color: theme.palette.warning.light }}>Warnings</span>
}
messages={proxy.status?.report?.warnings}
/>
@@ -116,53 +111,58 @@ const ProxyMessagesRow: FC<{
);
};
const ProxyMessagesList: FC<{
interface ProxyMessagesListProps {
title: ReactNode;
messages?: string[];
}> = ({ title, messages }) => {
}
const ProxyMessagesList: FC<ProxyMessagesListProps> = ({ title, messages }) => {
const theme = useTheme();
if (!messages) {
return <></>;
}
return (
<Box
sx={{
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
backgroundColor: (theme) => theme.palette.background.default,
p: "16px 24px",
<div
css={{
borderBottom: `1px solid ${theme.palette.divider}`,
backgroundColor: theme.palette.background.default,
padding: "16px 24px",
}}
>
<Box
<div
id="nested-list-subheader"
sx={{
mb: 0.5,
css={{
marginBottom: 4,
fontSize: 13,
fontWeight: 600,
}}
>
{title}
</Box>
</div>
{messages.map((error, index) => (
<Box
component="pre"
key={"message" + index}
sx={{
<pre
key={index}
css={{
margin: "0 0 8px",
fontSize: 14,
whiteSpace: "pre-wrap",
}}
>
{error}
</Box>
</pre>
))}
</Box>
</div>
);
};
// DetailedProxyStatus allows a more precise status to be displayed.
const DetailedProxyStatus: FC<{
interface DetailedProxyStatusProps {
proxy: WorkspaceProxy;
}> = ({ proxy }) => {
}
// DetailedProxyStatus allows a more precise status to be displayed.
const DetailedProxyStatus: FC<DetailedProxyStatusProps> = ({ proxy }) => {
if (!proxy.status) {
// If the status is null/undefined/not provided, just go with the boolean "healthy" value.
return <ProxyStatus proxy={proxy} />;
@@ -187,10 +187,12 @@ const DetailedProxyStatus: FC<{
}
};
// ProxyStatus will only show "healthy" or "not healthy" status.
const ProxyStatus: FC<{
interface ProxyStatusProps {
proxy: Region;
}> = ({ proxy }) => {
}
// ProxyStatus will only show "healthy" or "not healthy" status.
const ProxyStatus: FC<ProxyStatusProps> = ({ proxy }) => {
let icon = <NotHealthyBadge />;
if (proxy.healthy) {
icon = <HealthyBadge derpOnly={false} />;
@@ -47,7 +47,7 @@ export const WorkspaceProxyView: FC<
<TableCell width="40%">Proxy</TableCell>
<TableCell width="30%">URL</TableCell>
<TableCell width="10%">Status</TableCell>
<TableCell width="20%" sx={{ textAlign: "right" }}>
<TableCell width="20%" css={{ textAlign: "right" }}>
Latency
</TableCell>
</TableRow>
+33 -25
View File
@@ -1,6 +1,5 @@
import { FC } from "react";
import Box from "@mui/material/Box";
import { Palette, PaletteColor } from "@mui/material/styles";
import { useTheme } from "@emotion/react";
import { type FC } from "react";
import {
Filter,
FilterMenu,
@@ -9,8 +8,12 @@ import {
SearchFieldSkeleton,
useFilter,
} from "components/Filter/filter";
import { BaseOption } from "components/Filter/options";
import { UseFilterMenuOptions, useFilterMenu } from "components/Filter/menu";
import type { BaseOption } from "components/Filter/options";
import {
type UseFilterMenuOptions,
useFilterMenu,
} from "components/Filter/menu";
import type { ThemeRole } from "theme/experimental";
import { docs } from "utils/docs";
const userFilterQuery = {
@@ -19,7 +22,7 @@ const userFilterQuery = {
};
type StatusOption = BaseOption & {
color: string;
color: ThemeRole;
};
export const useStatusFilterMenu = ({
@@ -28,7 +31,7 @@ export const useStatusFilterMenu = ({
}: Pick<UseFilterMenuOptions<StatusOption>, "value" | "onChange">) => {
const statusOptions: StatusOption[] = [
{ value: "active", label: "Active", color: "success" },
{ value: "dormant", label: "Dormant", color: "secondary" },
{ value: "dormant", label: "Dormant", color: "notice" },
{ value: "suspended", label: "Suspended", color: "warning" },
];
return useFilterMenu({
@@ -48,17 +51,15 @@ const PRESET_FILTERS = [
{ query: userFilterQuery.all, name: "All users" },
];
export const UsersFilter = ({
filter,
error,
menus,
}: {
interface UsersFilterProps {
filter: ReturnType<typeof useFilter>;
error?: unknown;
menus: {
status: StatusFilterMenu;
};
}) => {
}
export const UsersFilter: FC<UsersFilterProps> = ({ filter, error, menus }) => {
return (
<Filter
presets={PRESET_FILTERS}
@@ -97,12 +98,14 @@ const StatusMenu = (menu: StatusFilterMenu) => {
);
};
const StatusOptionItem = ({
option,
isSelected,
}: {
interface StatusOptionItemProps {
option: StatusOption;
isSelected?: boolean;
}
const StatusOptionItem: FC<StatusOptionItemProps> = ({
option,
isSelected,
}) => {
return (
<OptionItem
@@ -113,15 +116,20 @@ const StatusOptionItem = ({
);
};
const StatusIndicator: FC<{ option: StatusOption }> = ({ option }) => {
interface StatusIndicatorProps {
option: StatusOption;
}
const StatusIndicator: FC<StatusIndicatorProps> = ({ option }) => {
const theme = useTheme();
return (
<Box
height={8}
width={8}
borderRadius={9999}
sx={{
backgroundColor: (theme) =>
(theme.palette[option.color as keyof Palette] as PaletteColor).light,
<div
css={{
height: 8,
width: 8,
borderRadius: 4,
backgroundColor: theme.experimental.roles[option.color].fill,
}}
/>
);
@@ -51,7 +51,7 @@ export function UserGroupsCell({ userGroups }: GroupsCellProps) {
css={{ columnGap: 8, alignItems: "center" }}
>
<GroupIcon
sx={{
css={{
width: "1rem",
height: "1rem",
opacity: userGroups.length > 0 ? 0.8 : 0.5,
@@ -1,12 +1,18 @@
import Box from "@mui/material/Box";
import TableCell from "@mui/material/TableCell";
import TableRow from "@mui/material/TableRow";
import Skeleton from "@mui/material/Skeleton";
import Divider from "@mui/material/Divider";
import HideSourceOutlined from "@mui/icons-material/HideSourceOutlined";
import KeyOutlined from "@mui/icons-material/KeyOutlined";
import GitHub from "@mui/icons-material/GitHub";
import PasswordOutlined from "@mui/icons-material/PasswordOutlined";
import ShieldOutlined from "@mui/icons-material/ShieldOutlined";
import { type Interpolation, type Theme } from "@emotion/react";
import { type FC } from "react";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import type * as TypesGen from "api/typesGenerated";
import { type GroupsByUserId } from "api/queries/groups";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { AvatarData } from "components/AvatarData/AvatarData";
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton";
@@ -16,15 +22,7 @@ import {
TableRowSkeleton,
} from "components/TableLoader/TableLoader";
import { EnterpriseBadge } from "components/Badges/Badges";
import HideSourceOutlined from "@mui/icons-material/HideSourceOutlined";
import KeyOutlined from "@mui/icons-material/KeyOutlined";
import GitHub from "@mui/icons-material/GitHub";
import PasswordOutlined from "@mui/icons-material/PasswordOutlined";
import ShieldOutlined from "@mui/icons-material/ShieldOutlined";
import { LastSeen } from "components/LastSeen/LastSeen";
import { UserRoleCell } from "./UserRoleCell";
import { type GroupsByUserId } from "api/queries/groups";
import { UserGroupsCell } from "./UserGroupsCell";
import {
MoreMenu,
MoreMenuTrigger,
@@ -32,7 +30,8 @@ import {
MoreMenuItem,
ThreeDotsButton,
} from "components/MoreMenu/MoreMenu";
import Divider from "@mui/material/Divider";
import { UserRoleCell } from "./UserRoleCell";
import { UserGroupsCell } from "./UserGroupsCell";
dayjs.extend(relativeTime);
@@ -91,9 +90,9 @@ export const UsersTableBody: FC<
<TableLoaderSkeleton>
<TableRowSkeleton>
<TableCell>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<div css={{ display: "flex", alignItems: "center", gap: 8 }}>
<AvatarDataSkeleton />
</Box>
</div>
</TableCell>
<TableCell>
@@ -126,9 +125,9 @@ export const UsersTableBody: FC<
<Cond condition={isNonInitialPage}>
<TableRow>
<TableCell colSpan={999}>
<Box p={4}>
<div css={{ padding: 32 }}>
<EmptyState message="No users found on this page" />
</Box>
</div>
</TableCell>
</TableRow>
</Cond>
@@ -136,9 +135,9 @@ export const UsersTableBody: FC<
<Cond>
<TableRow>
<TableCell colSpan={999}>
<Box p={4}>
<div css={{ padding: 32 }}>
<EmptyState message="No users found" />
</Box>
</div>
</TableCell>
</TableRow>
</Cond>
@@ -177,8 +176,8 @@ export const UsersTableBody: FC<
user.status === "suspended" && styles.suspended,
]}
>
<Box>{user.status}</Box>
<LastSeen value={user.last_seen_at} sx={{ fontSize: 12 }} />
<div>{user.status}</div>
<LastSeen value={user.last_seen_at} css={{ fontSize: 12 }} />
</TableCell>
{canEditUsers && (
@@ -237,57 +236,60 @@ export const UsersTableBody: FC<
);
};
const LoginType = ({
authMethods,
value,
}: {
interface LoginTypeProps {
authMethods: TypesGen.AuthMethods;
value: TypesGen.LoginType;
}) => {
}
const LoginType: FC<LoginTypeProps> = ({ authMethods, value }) => {
let displayName: string = value;
let icon = <></>;
const iconStyles = { width: 14, height: 14 };
if (value === "password") {
displayName = "Password";
icon = <PasswordOutlined sx={iconStyles} />;
icon = <PasswordOutlined css={styles.icon} />;
} else if (value === "none") {
displayName = "None";
icon = <HideSourceOutlined sx={iconStyles} />;
icon = <HideSourceOutlined css={styles.icon} />;
} else if (value === "github") {
displayName = "GitHub";
icon = <GitHub sx={iconStyles} />;
icon = <GitHub css={styles.icon} />;
} else if (value === "token") {
displayName = "Token";
icon = <KeyOutlined sx={iconStyles} />;
icon = <KeyOutlined css={styles.icon} />;
} else if (value === "oidc") {
displayName =
authMethods.oidc.signInText === "" ? "OIDC" : authMethods.oidc.signInText;
icon =
authMethods.oidc.iconUrl === "" ? (
<ShieldOutlined sx={iconStyles} />
<ShieldOutlined css={styles.icon} />
) : (
<Box
component="img"
<img
alt="Open ID Connect icon"
src={authMethods.oidc.iconUrl}
sx={iconStyles}
css={styles.icon}
/>
);
}
return (
<Box sx={{ display: "flex", alignItems: "center", gap: 1, fontSize: 14 }}>
<div css={{ display: "flex", alignItems: "center", gap: 8, fontSize: 14 }}>
{icon}
{displayName}
</Box>
</div>
);
};
const styles = {
icon: {
width: 14,
height: 14,
},
status: {
textTransform: "capitalize",
},
suspended: (theme) => ({
color: theme.palette.text.secondary,
}),
+11 -11
View File
@@ -1,31 +1,31 @@
import Box from "@mui/material/Box";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableRow from "@mui/material/TableRow";
import { Timeline } from "components/Timeline/Timeline";
import { FC } from "react";
import * as TypesGen from "api/typesGenerated";
import { EmptyState } from "components/EmptyState/EmptyState";
import { TableLoader } from "components/TableLoader/TableLoader";
import { BuildRow } from "./BuildRow";
import { Stack } from "components/Stack/Stack";
import LoadingButton from "@mui/lab/LoadingButton";
import ArrowDownwardOutlined from "@mui/icons-material/ArrowDownwardOutlined";
import { type FC, type ReactNode } from "react";
import type * as TypesGen from "api/typesGenerated";
import { EmptyState } from "components/EmptyState/EmptyState";
import { TableLoader } from "components/TableLoader/TableLoader";
import { Timeline } from "components/Timeline/Timeline";
import { Stack } from "components/Stack/Stack";
import { BuildRow } from "./BuildRow";
export const Language = {
emptyMessage: "No builds found",
};
export interface BuildsTableProps {
children?: ReactNode;
builds: TypesGen.WorkspaceBuild[] | undefined;
onLoadMoreBuilds: () => void;
isLoadingMoreBuilds: boolean;
hasMoreBuilds: boolean;
}
export const BuildsTable: FC<React.PropsWithChildren<BuildsTableProps>> = ({
export const BuildsTable: FC<BuildsTableProps> = ({
builds,
onLoadMoreBuilds,
isLoadingMoreBuilds,
@@ -49,9 +49,9 @@ export const BuildsTable: FC<React.PropsWithChildren<BuildsTableProps>> = ({
{builds && builds.length === 0 && (
<TableRow>
<TableCell colSpan={999}>
<Box p={4}>
<div css={{ padding: 32 }}>
<EmptyState message={Language.emptyMessage} />
</Box>
</div>
</TableCell>
</TableRow>
)}
@@ -1,9 +1,10 @@
import ExpandMoreOutlined from "@mui/icons-material/ExpandMoreOutlined";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import { useTheme } from "@emotion/react";
import { type FC } from "react";
import { useQuery } from "react-query";
import { getWorkspaceParameters } from "api/api";
import {
import type {
TemplateVersionParameter,
Workspace,
WorkspaceBuildParameter,
@@ -28,14 +29,16 @@ import {
usePopover,
} from "components/Popover/Popover";
export const BuildParametersPopover = ({
workspace,
disabled,
onSubmit,
}: {
interface BuildParametersPopoverProps {
workspace: Workspace;
disabled?: boolean;
onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void;
}
export const BuildParametersPopover: FC<BuildParametersPopoverProps> = ({
workspace,
disabled,
onSubmit,
}) => {
const { data: parameters } = useQuery({
queryKey: ["workspace", workspace.id, "parameters"],
@@ -52,9 +55,9 @@ export const BuildParametersPopover = ({
data-testid="build-parameters-button"
disabled={disabled}
color="neutral"
sx={{ px: 0 }}
css={{ paddingLeft: 0, paddingRight: 0 }}
>
<ExpandMoreOutlined sx={{ fontSize: 16 }} />
<ExpandMoreOutlined css={{ fontSize: 16 }} />
</Button>
</PopoverTrigger>
<PopoverContent
@@ -71,15 +74,18 @@ export const BuildParametersPopover = ({
);
};
const BuildParametersPopoverContent = ({
onSubmit,
ephemeralParameters,
buildParameters,
}: {
onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void;
interface BuildParametersPopoverContentProps {
ephemeralParameters?: TemplateVersionParameter[];
buildParameters?: WorkspaceBuildParameter[];
onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void;
}
const BuildParametersPopoverContent: FC<BuildParametersPopoverContentProps> = ({
ephemeralParameters,
buildParameters,
onSubmit,
}) => {
const theme = useTheme();
const popover = usePopover();
return (
@@ -87,19 +93,19 @@ const BuildParametersPopoverContent = ({
{buildParameters && ephemeralParameters ? (
ephemeralParameters.length > 0 ? (
<>
<Box
sx={{
color: (theme) => theme.palette.text.secondary,
p: 2.5,
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
<div
css={{
color: theme.palette.text.secondary,
padding: 20,
borderBottom: `1px solid ${theme.palette.divider}`,
}}
>
<HelpTooltipTitle>Build Options</HelpTooltipTitle>
<HelpTooltipText>
These parameters only apply for a single workspace start.
</HelpTooltipText>
</Box>
<Box sx={{ p: 2.5 }}>
</div>
<div css={{ padding: 20 }}>
<Form
onSubmit={(buildParameters) => {
onSubmit(buildParameters);
@@ -108,14 +114,14 @@ const BuildParametersPopoverContent = ({
ephemeralParameters={ephemeralParameters}
buildParameters={buildParameters}
/>
</Box>
</div>
</>
) : (
<Box
sx={{
color: (theme) => theme.palette.text.secondary,
p: 2.5,
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
<div
css={{
color: theme.palette.text.secondary,
padding: 20,
borderBottom: `1px solid ${theme.palette.divider}`,
}}
>
<HelpTooltipTitle>Build Options</HelpTooltipTitle>
@@ -129,7 +135,7 @@ const BuildParametersPopoverContent = ({
Read the docs
</HelpTooltipLink>
</HelpTooltipLinksGroup>
</Box>
</div>
)
) : (
<Loader />
@@ -138,14 +144,16 @@ const BuildParametersPopoverContent = ({
);
};
const Form = ({
ephemeralParameters,
buildParameters,
onSubmit,
}: {
interface FormProps {
ephemeralParameters: TemplateVersionParameter[];
buildParameters: WorkspaceBuildParameter[];
onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void;
}
const Form: FC<FormProps> = ({
ephemeralParameters,
buildParameters,
onSubmit,
}) => {
const form = useFormik({
initialValues: {
@@ -180,17 +188,17 @@ const Form = ({
);
})}
</FormFields>
<Box sx={{ py: 3, pb: 1 }}>
<div css={{ paddingTop: "24px", paddingBottom: "8px" }}>
<Button
data-testid="build-parameters-submit"
type="submit"
variant="contained"
color="primary"
sx={{ width: "100%" }}
css={{ width: "100%" }}
>
Build workspace
</Button>
</Box>
</div>
</form>
);
};
@@ -1,7 +1,3 @@
import { type FC } from "react";
import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated";
import { BuildParametersPopover } from "./BuildParametersPopover";
import Tooltip from "@mui/material/Tooltip";
import Button from "@mui/material/Button";
import LoadingButton from "@mui/lab/LoadingButton";
@@ -15,15 +11,18 @@ import OutlinedBlockIcon from "@mui/icons-material/BlockOutlined";
import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew";
import RetryIcon from "@mui/icons-material/BuildOutlined";
import RetryDebugIcon from "@mui/icons-material/BugReportOutlined";
import { type FC } from "react";
import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated";
import { BuildParametersPopover } from "./BuildParametersPopover";
interface WorkspaceActionProps {
interface ActionButtonProps {
loading?: boolean;
handleAction: () => void;
handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void;
disabled?: boolean;
tooltipText?: string;
}
export const UpdateButton: FC<WorkspaceActionProps> = ({
export const UpdateButton: FC<ActionButtonProps> = ({
handleAction,
loading,
}) => {
@@ -33,14 +32,14 @@ export const UpdateButton: FC<WorkspaceActionProps> = ({
loadingPosition="start"
data-testid="workspace-update-button"
startIcon={<CloudQueueIcon />}
onClick={handleAction}
onClick={() => handleAction()}
>
{loading ? <>Updating&hellip;</> : <>Update&hellip;</>}
</LoadingButton>
);
};
export const ActivateButton: FC<WorkspaceActionProps> = ({
export const ActivateButton: FC<ActionButtonProps> = ({
handleAction,
loading,
}) => {
@@ -49,19 +48,24 @@ export const ActivateButton: FC<WorkspaceActionProps> = ({
loading={loading}
loadingPosition="start"
startIcon={<PowerSettingsNewIcon />}
onClick={handleAction}
onClick={() => handleAction()}
>
{loading ? <>Activating&hellip;</> : "Activate"}
</LoadingButton>
);
};
export const StartButton: FC<
Omit<WorkspaceActionProps, "handleAction"> & {
workspace: Workspace;
handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void;
}
> = ({ handleAction, workspace, loading, disabled, tooltipText }) => {
interface ActionButtonPropsWithWorkspace extends ActionButtonProps {
workspace: Workspace;
}
export const StartButton: FC<ActionButtonPropsWithWorkspace> = ({
handleAction,
workspace,
loading,
disabled,
tooltipText,
}) => {
const buttonContent = (
<ButtonGroup
variant="outlined"
@@ -97,7 +101,7 @@ export const StartButton: FC<
);
};
export const StopButton: FC<WorkspaceActionProps> = ({
export const StopButton: FC<ActionButtonProps> = ({
handleAction,
loading,
}) => {
@@ -106,7 +110,7 @@ export const StopButton: FC<WorkspaceActionProps> = ({
loading={loading}
loadingPosition="start"
startIcon={<CropSquareIcon />}
onClick={handleAction}
onClick={() => handleAction()}
data-testid="workspace-stop-button"
>
{loading ? <>Stopping&hellip;</> : "Stop"}
@@ -114,16 +118,17 @@ export const StopButton: FC<WorkspaceActionProps> = ({
);
};
export const RestartButton: FC<
Omit<WorkspaceActionProps, "handleAction"> & {
workspace: Workspace;
handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void;
}
> = ({ handleAction, loading, workspace, disabled, tooltipText }) => {
export const RestartButton: FC<ActionButtonPropsWithWorkspace> = ({
handleAction,
loading,
workspace,
disabled,
tooltipText,
}) => {
const buttonContent = (
<ButtonGroup
variant="outlined"
sx={{
css={{
// Workaround to make the border transitions smoothly on button groups
"& > button:hover + button": {
borderLeft: "1px solid #FFF",
@@ -156,19 +161,19 @@ export const RestartButton: FC<
);
};
export const CancelButton: FC<WorkspaceActionProps> = ({ handleAction }) => {
export const CancelButton: FC<ActionButtonProps> = ({ handleAction }) => {
return (
<Button startIcon={<BlockIcon />} onClick={handleAction}>
<Button startIcon={<BlockIcon />} onClick={() => handleAction()}>
Cancel
</Button>
);
};
interface DisabledProps {
interface DisabledButtonProps {
label: string;
}
export const DisabledButton: FC<DisabledProps> = ({ label }) => {
export const DisabledButton: FC<DisabledButtonProps> = ({ label }) => {
return (
<Button startIcon={<OutlinedBlockIcon />} disabled>
{label}
@@ -188,18 +193,18 @@ export const ActionLoadingButton: FC<LoadingProps> = ({ label }) => {
);
};
type DebugButtonProps = Omit<WorkspaceActionProps, "loading"> & {
type RetryButtonProps = Omit<ActionButtonProps, "loading"> & {
debug?: boolean;
};
export const RetryButton = ({
export const RetryButton: FC<RetryButtonProps> = ({
handleAction,
debug = false,
}: DebugButtonProps) => {
}) => {
return (
<Button
startIcon={debug ? <RetryDebugIcon /> : <RetryIcon />}
onClick={handleAction}
onClick={() => handleAction()}
>
Retry{debug && " (Debug)"}
</Button>
@@ -1,15 +1,18 @@
import Box from "@mui/material/Box";
import { ProvisionerJobLog } from "api/typesGenerated";
import { useTheme } from "@emotion/react";
import { type FC, useRef, useEffect } from "react";
import type { ProvisionerJobLog } from "api/typesGenerated";
import { Loader } from "components/Loader/Loader";
import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs";
import { useRef, useEffect } from "react";
export const WorkspaceBuildLogsSection = ({
interface WorkspaceBuildLogsSectionProps {
logs?: ProvisionerJobLog[];
}
export const WorkspaceBuildLogsSection: FC<WorkspaceBuildLogsSectionProps> = ({
logs,
}: {
logs: ProvisionerJobLog[] | undefined;
}) => {
const scrollRef = useRef<HTMLDivElement>(null);
const theme = useTheme();
useEffect(() => {
// Auto scrolling makes hard to snapshot test using Chromatic
@@ -24,14 +27,15 @@ export const WorkspaceBuildLogsSection = ({
}, [logs]);
return (
<Box
sx={(theme) => ({
borderRadius: 1,
<div
css={{
borderRadius: 8,
border: `1px solid ${theme.palette.divider}`,
})}
overflow: "hidden",
}}
>
<Box
sx={(theme) => ({
<header
css={{
background: theme.palette.background.paper,
borderBottom: `1px solid ${theme.palette.divider}`,
padding: "8px 8px 8px 24px",
@@ -40,26 +44,20 @@ export const WorkspaceBuildLogsSection = ({
display: "flex",
alignItems: "center",
borderRadius: "8px 8px 0 0",
})}
}}
>
Build logs
</Box>
<Box
ref={scrollRef}
sx={() => ({
height: "400px",
overflowY: "auto",
})}
>
</header>
<div ref={scrollRef} css={{ height: "400px", overflowY: "auto" }}>
{logs ? (
<WorkspaceBuildLogs
sticky
logs={logs}
sx={{ border: 0, borderRadius: 0 }}
css={{ border: 0, borderRadius: 0 }}
/>
) : (
<Box
sx={{
<div
css={{
display: "flex",
alignItems: "center",
justifyContent: "center",
@@ -68,9 +66,9 @@ export const WorkspaceBuildLogsSection = ({
}}
>
<Loader />
</Box>
</div>
)}
</Box>
</Box>
</div>
</div>
);
};
@@ -103,7 +103,10 @@ export const WorkspacePage: FC = () => {
if (pageError) {
return (
<Margins>
<ErrorAlert error={pageError} sx={{ my: 2 }} />
<ErrorAlert
error={pageError}
css={{ marginTop: 16, marginBottom: 16 }}
/>
</Margins>
);
}
@@ -10,7 +10,7 @@ import {
} from "./WorkspaceParametersForm";
import { useNavigate } from "react-router-dom";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
import { FC } from "react";
import { type FC } from "react";
import { isApiValidationError } from "api/errors";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { WorkspaceBuildParameter } from "api/typesGenerated";
@@ -19,7 +19,7 @@ import Button from "@mui/material/Button";
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
import { docs } from "utils/docs";
const WorkspaceParametersPage = () => {
const WorkspaceParametersPage: FC = () => {
const workspace = useWorkspaceSettings();
const parameters = useQuery({
queryKey: ["workspace", workspace.id, "parameters"],
@@ -79,16 +79,12 @@ export const WorkspaceParametersPageView: FC<
> = ({ data, submitError, isSubmitting, onSubmit, onCancel }) => {
return (
<>
<PageHeader
css={{
paddingTop: 0,
}}
>
<PageHeader css={{ paddingTop: 0 }}>
<PageHeaderTitle>Workspace parameters</PageHeaderTitle>
</PageHeader>
{submitError && !isApiValidationError(submitError) && (
<ErrorAlert error={submitError} sx={{ mb: 6 }} />
<ErrorAlert error={submitError} css={{ marginBottom: 48 }} />
)}
{data ? (
+18 -16
View File
@@ -1,16 +1,18 @@
import { useTheme } from "@emotion/react";
import TextField from "@mui/material/TextField";
import { Box } from "@mui/system";
import { deleteWorkspace, startWorkspace, stopWorkspace } from "api/api";
import { Workspace } from "api/typesGenerated";
import type { Workspace } from "api/typesGenerated";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { displayError } from "components/GlobalSnackbar/utils";
import { useState } from "react";
import { type FC, useState } from "react";
import { useMutation } from "react-query";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
export const useBatchActions = (options: {
interface UseBatchActionsProps {
onSuccess: () => Promise<void>;
}) => {
}
export function useBatchActions(options: UseBatchActionsProps) {
const { onSuccess } = options;
const startAllMutation = useMutation({
@@ -56,7 +58,7 @@ export const useBatchActions = (options: {
stopAllMutation.isLoading ||
deleteAllMutation.isLoading,
};
};
}
type BatchDeleteConfirmationProps = {
checkedWorkspaces: Workspace[];
@@ -66,10 +68,11 @@ type BatchDeleteConfirmationProps = {
onConfirm: () => void;
};
export const BatchDeleteConfirmation = (
props: BatchDeleteConfirmationProps,
export const BatchDeleteConfirmation: FC<BatchDeleteConfirmationProps> = (
props,
) => {
const { checkedWorkspaces, open, onClose, onConfirm, isLoading } = props;
const theme = useTheme();
const [confirmation, setConfirmation] = useState({ value: "", error: false });
const confirmDeletion = () => {
@@ -103,21 +106,20 @@ export const BatchDeleteConfirmation = (
confirmDeletion();
}}
>
<Box>
<div>
Deleting these workspaces is irreversible! Are you sure you want to
proceed? Type{" "}
<Box
component="code"
sx={{
<code
css={{
fontFamily: MONOSPACE_FONT_FAMILY,
color: (theme) => theme.palette.text.primary,
color: theme.palette.text.primary,
fontWeight: 600,
}}
>
`DELETE`
</Box>{" "}
</code>{" "}
to confirm.
</Box>
</div>
<TextField
value={confirmation.value}
required
@@ -127,7 +129,7 @@ export const BatchDeleteConfirmation = (
"aria-label": "Type DELETE to confirm",
}}
placeholder="Type DELETE to confirm"
sx={{ mt: 2 }}
css={{ marginTop: 16 }}
onChange={(e) => {
const value = e.currentTarget?.value;
setConfirmation((c) => ({ ...c, value }));
@@ -1,11 +1,10 @@
import { type PropsWithChildren, type ReactNode, useState } from "react";
import { type FC, type ReactNode, useState } from "react";
import { type Template } from "api/typesGenerated";
import { type UseQueryResult } from "react-query";
import {
Link as RouterLink,
LinkProps as RouterLinkProps,
} from "react-router-dom";
import Box from "@mui/system/Box";
import Button from "@mui/material/Button";
import Link from "@mui/material/Link";
import AddIcon from "@mui/icons-material/AddOutlined";
@@ -25,16 +24,17 @@ const ICON_SIZE = 18;
type TemplatesQuery = UseQueryResult<Template[]>;
type WorkspacesButtonProps = PropsWithChildren<{
interface WorkspacesButtonProps {
children?: ReactNode;
templatesFetchStatus: TemplatesQuery["status"];
templates: TemplatesQuery["data"];
}>;
}
export function WorkspacesButton({
export const WorkspacesButton: FC<WorkspacesButtonProps> = ({
children,
templatesFetchStatus,
templates,
}: WorkspacesButtonProps) {
}) => {
// Dataset should always be small enough that client-side filtering should be
// good enough. Can swap out down the line if it becomes an issue
const [searchTerm, setSearchTerm] = useState("");
@@ -69,15 +69,16 @@ export function WorkspacesButton({
onValueChange={(newValue) => setSearchTerm(newValue)}
placeholder="Type/select a workspace template"
label="Template select for workspace"
sx={{ flexShrink: 0, columnGap: 1.5 }}
css={{ flexShrink: 0, columnGap: 12 }}
/>
<OverflowY
maxHeight={380}
sx={{
css={{
display: "flex",
flexDirection: "column",
paddingY: 1,
paddingTop: "8px",
paddingBottom: "8px",
}}
>
{templatesFetchStatus === "loading" ? (
@@ -93,7 +94,7 @@ export function WorkspacesButton({
)}
</OverflowY>
<Box
<div
css={(theme) => ({
padding: "8px 0",
borderTop: `1px solid ${theme.palette.divider}`,
@@ -105,20 +106,23 @@ export function WorkspacesButton({
display: "flex",
alignItems: "center",
columnGap: 12,
color: theme.palette.primary.main,
})}
>
<OpenIcon css={{ width: 14, height: 14 }} />
<span>See all templates</span>
</PopoverLink>
</Box>
</div>
</PopoverContent>
</Popover>
);
};
interface WorkspaceResultsRowProps {
template: Template;
}
function WorkspaceResultsRow({ template }: { template: Template }) {
const WorkspaceResultsRow: FC<WorkspaceResultsRowProps> = ({ template }) => {
return (
<PopoverLink
to={`/templates/${template.name}/workspace`}
@@ -132,7 +136,7 @@ function WorkspaceResultsRow({ template }: { template: Template }) {
src={template.icon}
fitImage
alt={template.display_name || "Coder template"}
sx={{
css={{
width: `${ICON_SIZE}px`,
height: `${ICON_SIZE}px`,
fontSize: `${ICON_SIZE * 0.5}px`,
@@ -142,7 +146,7 @@ function WorkspaceResultsRow({ template }: { template: Template }) {
{template.display_name || "-"}
</Avatar>
<Box
<div
css={(theme) => ({
color: theme.palette.text.primary,
display: "flex",
@@ -170,15 +174,15 @@ function WorkspaceResultsRow({ template }: { template: Template }) {
developer
{template.active_user_count === 1 ? "" : "s"}
</span>
</Box>
</div>
</PopoverLink>
);
}
};
function PopoverLink(props: RouterLinkProps) {
const PopoverLink: FC<RouterLinkProps> = ({ children, ...linkProps }) => {
return (
<RouterLink
{...props}
{...linkProps}
css={(theme) => ({
color: theme.palette.text.primary,
padding: "8px 16px",
@@ -193,9 +197,11 @@ function PopoverLink(props: RouterLinkProps) {
backgroundColor: theme.palette.action.hover,
},
})}
/>
>
{children}
</RouterLink>
);
}
};
function sortTemplatesByUsersDesc(
templates: readonly Template[],
@@ -12,7 +12,6 @@ import { ErrorAlert } from "components/Alert/ErrorAlert";
import { WorkspacesFilter } from "./filter/filter";
import { hasError, isApiValidationError } from "api/errors";
import { TableToolbar } from "components/TableToolbar/TableToolbar";
import Box from "@mui/material/Box";
import DeleteOutlined from "@mui/icons-material/DeleteOutlined";
import { WorkspacesButton } from "./WorkspacesButton";
import { UseQueryResult } from "react-query";
@@ -136,11 +135,11 @@ export const WorkspacesPageView = ({
<TableToolbar>
{checkedWorkspaces.length > 0 ? (
<>
<Box>
<div>
Selected <strong>{checkedWorkspaces.length}</strong> of{" "}
<strong>{workspaces?.length}</strong>{" "}
{workspaces?.length === 1 ? "workspace" : "workspaces"}
</Box>
</div>
<MoreMenu>
<MoreMenuTrigger>
@@ -7,88 +7,86 @@
import {
type ForwardedRef,
type KeyboardEvent,
type InputHTMLAttributes,
forwardRef,
useId,
} from "react";
import Box from "@mui/system/Box";
import SearchIcon from "@mui/icons-material/SearchOutlined";
import { visuallyHidden } from "@mui/utils";
import { type SystemStyleObject } from "@mui/system";
import { useTheme } from "@emotion/react";
type Props = {
value: string;
onValueChange: (newValue: string) => void;
placeholder?: string;
interface SearchBoxProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
value: string;
onKeyDown?: (event: KeyboardEvent) => void;
sx?: SystemStyleObject;
};
onValueChange: (newValue: string) => void;
}
export const SearchBox = forwardRef(function SearchBox(
{
value,
props: SearchBoxProps,
ref?: ForwardedRef<HTMLInputElement>,
) {
const {
onValueChange,
onKeyDown,
label = "Search",
placeholder = "Search...",
sx = {},
}: Props,
ref?: ForwardedRef<HTMLInputElement>,
) {
...attrs
} = props;
const hookId = useId();
const theme = useTheme();
const inputId = `${hookId}-${SearchBox.name}-input`;
return (
<Box
sx={{
<div
css={{
display: "flex",
flexFlow: "row nowrap",
alignItems: "center",
paddingX: 2,
padding: "0 8px",
height: "40px",
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
...sx,
borderBottom: `1px solid ${theme.palette.divider}`,
}}
onKeyDown={onKeyDown}
>
<Box component="div" sx={{ width: "18px" }}>
<div css={{ width: 18 }}>
<SearchIcon
sx={{
css={{
display: "block",
fontSize: "14px",
marginX: "auto",
color: (theme) => theme.palette.text.secondary,
marginLeft: "auto",
marginRight: "auto",
color: theme.palette.text.secondary,
}}
/>
</Box>
</div>
<Box component="label" sx={visuallyHidden} htmlFor={inputId}>
<label css={{ ...visuallyHidden }} htmlFor={inputId}>
{label}
</Box>
</label>
<Box
component="input"
<input
type="text"
ref={ref}
id={inputId}
autoFocus
tabIndex={0}
placeholder={placeholder}
value={value}
{...attrs}
onKeyDown={onKeyDown}
onChange={(e) => onValueChange(e.target.value)}
sx={{
css={{
height: "100%",
border: 0,
background: "none",
width: "100%",
outline: 0,
"&::placeholder": {
color: (theme) => theme.palette.text.secondary,
color: theme.palette.text.secondary,
},
}}
/>
</Box>
</div>
);
});
@@ -4,16 +4,18 @@ import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import { Template, Workspace } from "api/typesGenerated";
import { FC, ReactNode } from "react";
import Checkbox from "@mui/material/Checkbox";
import Skeleton from "@mui/material/Skeleton";
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
import { useTheme } from "@emotion/react";
import { type FC, type ReactNode } from "react";
import { useNavigate } from "react-router-dom";
import type { Template, Workspace } from "api/typesGenerated";
import {
TableLoaderSkeleton,
TableRowSkeleton,
} from "components/TableLoader/TableLoader";
import { useNavigate } from "react-router-dom";
import { useClickableTableRow } from "hooks/useClickableTableRow";
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
import Box from "@mui/material/Box";
import { AvatarData } from "components/AvatarData/AvatarData";
import { Avatar } from "components/Avatar/Avatar";
import { Stack } from "components/Stack/Stack";
@@ -21,9 +23,7 @@ import { LastUsed } from "pages/WorkspacesPage/LastUsed";
import { WorkspaceOutdatedTooltip } from "components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip";
import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge";
import { getDisplayWorkspaceTemplateName } from "utils/workspace";
import Checkbox from "@mui/material/Checkbox";
import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton";
import Skeleton from "@mui/material/Skeleton";
import { InfoTooltip } from "components/InfoTooltip/InfoTooltip";
import { WorkspacesEmpty } from "./WorkspacesEmpty";
@@ -49,18 +49,20 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
templates,
canCreateTemplate,
}) => {
const theme = useTheme();
return (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell width="40%">
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<div css={{ display: "flex", alignItems: "center", gap: 8 }}>
{canCheckWorkspaces && (
<Checkbox
// Remove the extra padding added for the first cell in the
// table
sx={{ marginLeft: "-20px" }}
css={{ marginLeft: "-20px" }}
disabled={!workspaces || workspaces.length === 0}
checked={checkedWorkspaces.length === workspaces?.length}
size="small"
@@ -78,7 +80,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
/>
)}
Name
</Box>
</div>
</TableCell>
<TableCell width="25%">Template</TableCell>
<TableCell width="20%">Last used</TableCell>
@@ -109,12 +111,14 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
checked={checked}
>
<TableCell>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<div
css={{ display: "flex", alignItems: "center", gap: 8 }}
>
{canCheckWorkspaces && (
<Checkbox
// Remove the extra padding added for the first cell in the
// table
sx={{ marginLeft: "-20px" }}
css={{ marginLeft: "-20px" }}
data-testid={`checkbox-${workspace.id}`}
size="small"
disabled={cantBeChecked(workspace)}
@@ -169,7 +173,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
</Avatar>
}
/>
</Box>
</div>
</TableCell>
<TableCell>
@@ -181,7 +185,9 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
</TableCell>
<TableCell>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<div
css={{ display: "flex", alignItems: "center", gap: 8 }}
>
<WorkspaceStatusBadge workspace={workspace} />
{workspace.latest_build.status === "running" &&
!workspace.health.healthy && (
@@ -191,19 +197,19 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
message="Your workspace is running but some agents are unhealthy."
/>
)}
</Box>
</div>
</TableCell>
<TableCell>
<Box css={{ display: "flex", paddingLeft: 16 }}>
<div css={{ display: "flex", paddingLeft: 16 }}>
<KeyboardArrowRight
sx={{
color: (theme) => theme.palette.text.secondary,
css={{
color: theme.palette.text.secondary,
width: 20,
height: 20,
}}
/>
</Box>
</div>
</TableCell>
</WorkspacesRow>
);
@@ -214,12 +220,19 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
);
};
const WorkspacesRow: FC<{
interface WorkspacesRowProps {
workspace: Workspace;
children: ReactNode;
children?: ReactNode;
checked: boolean;
}> = ({ workspace, children, checked }) => {
}
const WorkspacesRow: FC<WorkspacesRowProps> = ({
workspace,
children,
checked,
}) => {
const navigate = useNavigate();
const theme = useTheme();
const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}`;
const openLinkInNewTab = () => window.open(workspacePageLink, "_blank");
@@ -245,9 +258,8 @@ const WorkspacesRow: FC<{
<TableRow
{...clickableProps}
data-testid={`workspace-${workspace.id}`}
sx={{
backgroundColor: (theme) =>
checked ? theme.palette.action.hover : undefined,
css={{
backgroundColor: checked ? theme.palette.action.hover : undefined,
}}
>
{children}
@@ -255,21 +267,21 @@ const WorkspacesRow: FC<{
);
};
const TableLoader = ({
canCheckWorkspaces,
}: {
canCheckWorkspaces: boolean;
}) => {
interface TableLoaderProps {
canCheckWorkspaces?: boolean;
}
const TableLoader: FC<TableLoaderProps> = ({ canCheckWorkspaces }) => {
return (
<TableLoaderSkeleton>
<TableRowSkeleton>
<TableCell width="40%">
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<div css={{ display: "flex", alignItems: "center", gap: 8 }}>
{canCheckWorkspaces && (
<Checkbox size="small" disabled sx={{ marginLeft: "-20px" }} />
<Checkbox size="small" disabled css={{ marginLeft: "-20px" }} />
)}
<AvatarDataSkeleton />
</Box>
</div>
</TableCell>
<TableCell>
<Skeleton variant="text" width="25%" />
+42 -35
View File
@@ -1,10 +1,9 @@
import { FC } from "react";
import Box from "@mui/material/Box";
import { type FC } from "react";
import { useTheme } from "@emotion/react";
import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProvider";
import { Avatar, AvatarProps } from "components/Avatar/Avatar";
import { Palette, PaletteColor } from "@mui/material/styles";
import { TemplateFilterMenu, StatusFilterMenu } from "./menus";
import { TemplateOption, StatusOption } from "./options";
import { Avatar, type AvatarProps } from "components/Avatar/Avatar";
import type { TemplateFilterMenu, StatusFilterMenu } from "./menus";
import type { TemplateOption, StatusOption } from "./options";
import {
Filter,
FilterMenu,
@@ -14,7 +13,7 @@ import {
SearchFieldSkeleton,
useFilter,
} from "components/Filter/filter";
import { UserFilterMenu, UserMenu } from "components/Filter/UserFilter";
import { type UserFilterMenu, UserMenu } from "components/Filter/UserFilter";
import { docs } from "utils/docs";
export const workspaceFilterQuery = {
@@ -70,11 +69,11 @@ type WorkspaceFilterProps = {
};
};
export const WorkspacesFilter = ({
export const WorkspacesFilter: FC<WorkspaceFilterProps> = ({
filter,
error,
menus,
}: WorkspaceFilterProps) => {
}) => {
const actionsEnabled = useIsWorkspaceActionsEnabled();
const presets = actionsEnabled ? PRESETS_WITH_DORMANT : PRESET_FILTERS;
@@ -87,7 +86,7 @@ export const WorkspacesFilter = ({
learnMoreLink={docs("/workspaces#workspace-filtering")}
options={
<>
{menus.user && <UserMenu {...menus.user} />}
{menus.user && <UserMenu menu={menus.user} />}
<TemplateMenu {...menus.template} />
<StatusMenu {...menus.status} />
</>
@@ -122,12 +121,14 @@ const TemplateMenu = (menu: TemplateFilterMenu) => {
);
};
const TemplateOptionItem = ({
option,
isSelected,
}: {
interface TemplateOptionItemProps {
option: TemplateOption;
isSelected?: boolean;
}
const TemplateOptionItem: FC<TemplateOptionItemProps> = ({
option,
isSelected,
}) => {
return (
<OptionItem
@@ -137,16 +138,23 @@ const TemplateOptionItem = ({
<TemplateAvatar
templateName={option.label}
icon={option.icon}
sx={{ width: 14, height: 14, fontSize: 8 }}
css={{ width: 14, height: 14, fontSize: 8 }}
/>
}
/>
);
};
const TemplateAvatar: FC<
AvatarProps & { templateName: string; icon?: string }
> = ({ templateName, icon, ...avatarProps }) => {
interface TemplateAvatarProps extends AvatarProps {
templateName: string;
icon?: string;
}
const TemplateAvatar: FC<TemplateAvatarProps> = ({
templateName,
icon,
...avatarProps
}) => {
return icon ? (
<Avatar src={icon} variant="square" fitImage {...avatarProps} />
) : (
@@ -172,13 +180,12 @@ const StatusMenu = (menu: StatusFilterMenu) => {
);
};
const StatusOptionItem = ({
option,
isSelected,
}: {
interface StatusOptionItem {
option: StatusOption;
isSelected?: boolean;
}) => {
}
const StatusOptionItem: FC<StatusOptionItem> = ({ option, isSelected }) => {
return (
<OptionItem
option={option}
@@ -188,20 +195,20 @@ const StatusOptionItem = ({
);
};
const StatusIndicator: FC<{ option: StatusOption }> = ({ option }) => {
const color = option.color === "notice" ? "warning" : option.color;
interface StatusIndicatorProps {
option: StatusOption;
}
const StatusIndicator: FC<StatusIndicatorProps> = ({ option }) => {
const theme = useTheme();
return (
<Box
height={8}
width={8}
borderRadius={9999}
css={(theme) => {
return {
backgroundColor: (
theme.palette[color as keyof Palette] as PaletteColor
).light,
};
<div
css={{
height: 8,
width: 8,
borderRadius: 4,
backgroundColor: theme.experimental.roles[option.color].fill,
}}
/>
);
@@ -1,7 +1,8 @@
import { BaseOption } from "components/Filter/options";
import type { ThemeRole } from "theme/experimental";
export type StatusOption = BaseOption & {
color: string;
color: ThemeRole;
};
export type TemplateOption = BaseOption & {
+2 -6
View File
@@ -122,13 +122,9 @@ type RenderHookWithAuthOptions<Props> = Partial<
*/
export async function renderHookWithAuth<Result, Props>(
render: (initialProps: Props) => Result,
{
initialProps,
path = "/",
route = "/",
extraRoutes = [],
}: RenderHookWithAuthOptions<Props> = {},
options: RenderHookWithAuthOptions<Props> = {},
) {
const { initialProps, path = "/", route = "/", extraRoutes = [] } = options;
const queryClient = createTestQueryClient();
// Easy to miss – there's an evil definite assignment via the !