mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor: remove usage of <Box> and sx (#10702)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+1
-108
@@ -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'
|
||||
|
||||
Vendored
+2
-5
@@ -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 {}
|
||||
}
|
||||
|
||||
Vendored
+1
-9
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'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={() => {
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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%" />
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
+10
-11
@@ -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}
|
||||
|
||||
+15
-11
@@ -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%" />
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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…</> : <>Update…</>}
|
||||
</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…</> : "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…</> : "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>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-8
@@ -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 ? (
|
||||
|
||||
@@ -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%" />
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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 !
|
||||
|
||||
Reference in New Issue
Block a user