mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: use emotion for styling (pt. 9) (#10474)
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { css, Global, useTheme } from "@emotion/react";
|
||||
import Button from "@mui/material/Button";
|
||||
import InputAdornment from "@mui/material/InputAdornment";
|
||||
import TextField, { TextFieldProps } from "@mui/material/TextField";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import TextField, { type TextFieldProps } from "@mui/material/TextField";
|
||||
import Picker from "@emoji-mart/react";
|
||||
import { FC } from "react";
|
||||
import { type FC } from "react";
|
||||
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { colors } from "theme/colors";
|
||||
@@ -48,7 +48,7 @@ const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
|
||||
throw new Error(`Invalid icon value "${typeof textFieldProps.value}"`);
|
||||
}
|
||||
|
||||
const styles = useStyles();
|
||||
const theme = useTheme();
|
||||
const hasIcon = textFieldProps.value && textFieldProps.value !== "";
|
||||
|
||||
return (
|
||||
@@ -59,7 +59,21 @@ const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
|
||||
label="Icon"
|
||||
InputProps={{
|
||||
endAdornment: hasIcon ? (
|
||||
<InputAdornment position="end" className={styles.adornment}>
|
||||
<InputAdornment
|
||||
position="end"
|
||||
css={{
|
||||
width: theme.spacing(3),
|
||||
height: theme.spacing(3),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
|
||||
"& img": {
|
||||
maxWidth: "100%",
|
||||
objectFit: "contain",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
src={textFieldProps.value}
|
||||
@@ -85,6 +99,18 @@ const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
|
||||
id="emoji"
|
||||
css={{ marginTop: 0, ".MuiPaper-root": { width: "auto" } }}
|
||||
>
|
||||
<Global
|
||||
styles={css`
|
||||
em-emoji-picker {
|
||||
--rgb-background: ${theme.palette.background.paper};
|
||||
--rgb-input: ${colors.gray[17]};
|
||||
--rgb-color: ${colors.gray[4]};
|
||||
|
||||
// Hack to prevent the right side from being cut off
|
||||
width: 350px;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<Picker
|
||||
set="twitter"
|
||||
theme="dark"
|
||||
@@ -104,29 +130,4 @@ const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
"@global": {
|
||||
"em-emoji-picker": {
|
||||
"--rgb-background": theme.palette.background.paper,
|
||||
"--rgb-input": colors.gray[17],
|
||||
"--rgb-color": colors.gray[4],
|
||||
|
||||
// Hack to prevent the right side from being cut off
|
||||
width: 350,
|
||||
},
|
||||
},
|
||||
adornment: {
|
||||
width: theme.spacing(3),
|
||||
height: theme.spacing(3),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
|
||||
"& img": {
|
||||
maxWidth: "100%",
|
||||
objectFit: "contain",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export default IconField;
|
||||
|
||||
@@ -5,7 +5,7 @@ import Skeleton from "@mui/material/Skeleton";
|
||||
export const TableToolbar = styled(Box)(({ theme }) => ({
|
||||
fontSize: 13,
|
||||
marginBottom: theme.spacing(1),
|
||||
marginTop: theme.spacing(0),
|
||||
marginTop: 0,
|
||||
height: 36, // The size of a small button
|
||||
color: theme.palette.text.secondary,
|
||||
"& strong": { color: theme.palette.text.primary },
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import { User } from "api/typesGenerated";
|
||||
import type { User } from "api/typesGenerated";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { AvatarData } from "components/AvatarData/AvatarData";
|
||||
import { ChangeEvent, ComponentProps, FC, useState } from "react";
|
||||
import {
|
||||
type ChangeEvent,
|
||||
type ComponentProps,
|
||||
type FC,
|
||||
useState,
|
||||
} from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useDebouncedFunction } from "hooks/debounce";
|
||||
import { useQuery } from "react-query";
|
||||
@@ -27,7 +33,7 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
|
||||
className,
|
||||
size = "small",
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
const theme = useTheme();
|
||||
const [autoComplete, setAutoComplete] = useState<{
|
||||
value: string;
|
||||
open: boolean;
|
||||
@@ -101,7 +107,11 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
|
||||
size={size}
|
||||
label={label}
|
||||
placeholder="User email or username"
|
||||
className={styles.textField}
|
||||
css={{
|
||||
"&:not(:has(label))": {
|
||||
margin: 0,
|
||||
},
|
||||
}}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
onChange: debouncedInputOnChange,
|
||||
@@ -119,7 +129,12 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
|
||||
</>
|
||||
),
|
||||
classes: {
|
||||
root: styles.inputRoot,
|
||||
root: css`
|
||||
padding-left: ${theme.spacing(
|
||||
1.75,
|
||||
)} !important; // Same padding left as input
|
||||
gap: ${theme.spacing(0.5)};
|
||||
`,
|
||||
},
|
||||
}}
|
||||
InputLabelProps={{
|
||||
@@ -130,15 +145,3 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const useStyles = makeStyles((theme) => ({
|
||||
textField: {
|
||||
"&:not(:has(label))": {
|
||||
margin: 0,
|
||||
},
|
||||
},
|
||||
inputRoot: {
|
||||
paddingLeft: `${theme.spacing(1.75)} !important`, // Same padding left as input
|
||||
gap: theme.spacing(0.5),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import dayjs from "dayjs";
|
||||
import { ComponentProps, FC, Fragment } from "react";
|
||||
import { ProvisionerJobLog } from "api/typesGenerated";
|
||||
import { type ComponentProps, type FC, Fragment } from "react";
|
||||
import type { ProvisionerJobLog } from "api/typesGenerated";
|
||||
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
|
||||
import { Logs } from "./Logs";
|
||||
import Box from "@mui/material/Box";
|
||||
import { combineClasses } from "utils/combineClasses";
|
||||
import { type Interpolation, type Theme } from "@emotion/react";
|
||||
|
||||
const Language = {
|
||||
seconds: "seconds",
|
||||
@@ -53,7 +52,6 @@ export const WorkspaceBuildLogs: FC<WorkspaceBuildLogsProps> = ({
|
||||
}) => {
|
||||
const groupedLogsByStage = groupLogsByStage(logs);
|
||||
const stages = Object.keys(groupedLogsByStage);
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -79,15 +77,10 @@ export const WorkspaceBuildLogs: FC<WorkspaceBuildLogsProps> = ({
|
||||
|
||||
return (
|
||||
<Fragment key={stage}>
|
||||
<div
|
||||
className={combineClasses([
|
||||
styles.header,
|
||||
sticky ? styles.sticky : "",
|
||||
])}
|
||||
>
|
||||
<div css={[styles.header, sticky && styles.sticky]}>
|
||||
<div>{stage}</div>
|
||||
{shouldDisplayDuration && (
|
||||
<div className={styles.duration}>
|
||||
<div css={styles.duration}>
|
||||
{duration} {Language.seconds}
|
||||
</div>
|
||||
)}
|
||||
@@ -100,8 +93,8 @@ export const WorkspaceBuildLogs: FC<WorkspaceBuildLogsProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
header: {
|
||||
const styles = {
|
||||
header: (theme) => ({
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
padding: theme.spacing(0.5, 3),
|
||||
@@ -119,16 +112,16 @@ const useStyles = makeStyles((theme) => ({
|
||||
"&:first-child": {
|
||||
borderRadius: "8px 8px 0 0",
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
sticky: {
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
},
|
||||
|
||||
duration: {
|
||||
duration: (theme) => ({
|
||||
marginLeft: "auto",
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 12,
|
||||
},
|
||||
}));
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Workspace } from "api/typesGenerated";
|
||||
import type { Workspace } from "api/typesGenerated";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import { FC, PropsWithChildren } from "react";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { combineClasses } from "utils/combineClasses";
|
||||
import { type FC, type PropsWithChildren } from "react";
|
||||
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
|
||||
import { DormantDeletionText } from "components/WorkspaceDeletion";
|
||||
import { getDisplayWorkspaceStatus } from "utils/workspace";
|
||||
import Tooltip, { TooltipProps, tooltipClasses } from "@mui/material/Tooltip";
|
||||
import Tooltip, {
|
||||
type TooltipProps,
|
||||
tooltipClasses,
|
||||
} from "@mui/material/Tooltip";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import Box from "@mui/material/Box";
|
||||
import ErrorOutline from "@mui/icons-material/ErrorOutline";
|
||||
import { type Interpolation, type Theme } from "@emotion/react";
|
||||
|
||||
export type WorkspaceStatusBadgeProps = {
|
||||
workspace: Workspace;
|
||||
@@ -56,7 +58,6 @@ export const WorkspaceStatusBadge: FC<
|
||||
export const WorkspaceStatusText: FC<
|
||||
PropsWithChildren<WorkspaceStatusBadgeProps>
|
||||
> = ({ workspace, className }) => {
|
||||
const styles = useStyles();
|
||||
const { text, type } = getDisplayWorkspaceStatus(
|
||||
workspace.latest_build.status,
|
||||
);
|
||||
@@ -71,11 +72,8 @@ export const WorkspaceStatusText: FC<
|
||||
<span
|
||||
role="status"
|
||||
data-testid="build-status"
|
||||
className={combineClasses([
|
||||
className,
|
||||
styles.root,
|
||||
styles[`type-${type}`],
|
||||
])}
|
||||
className={className}
|
||||
css={[styles.root, styles[`type-${type}`]]}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
@@ -95,27 +93,22 @@ const FailureTooltip = styled(({ className, ...props }: TooltipProps) => (
|
||||
},
|
||||
}));
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
const styles = {
|
||||
root: { fontWeight: 600 },
|
||||
"type-error": {
|
||||
|
||||
"type-error": (theme) => ({
|
||||
color: theme.palette.error.light,
|
||||
},
|
||||
"type-warning": {
|
||||
}),
|
||||
"type-warning": (theme) => ({
|
||||
color: theme.palette.warning.light,
|
||||
},
|
||||
"type-success": {
|
||||
}),
|
||||
"type-success": (theme) => ({
|
||||
color: theme.palette.success.light,
|
||||
},
|
||||
"type-info": {
|
||||
}),
|
||||
"type-info": (theme) => ({
|
||||
color: theme.palette.info.light,
|
||||
},
|
||||
"type-undefined": {
|
||||
}),
|
||||
"type-undefined": (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
"type-primary": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
"type-secondary": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}));
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useTheme, type CSSObject } from "@emotion/react";
|
||||
import { type MouseEventHandler } from "react";
|
||||
import { type TableRowProps } from "@mui/material/TableRow";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { useClickable, type UseClickableResult } from "./useClickable";
|
||||
|
||||
type UseClickableTableRowResult = UseClickableResult<HTMLTableRowElement> &
|
||||
TableRowProps & {
|
||||
className: string;
|
||||
css: CSSObject;
|
||||
hover: true;
|
||||
onAuxClick: MouseEventHandler<HTMLTableRowElement>;
|
||||
};
|
||||
@@ -28,12 +28,24 @@ export const useClickableTableRow = ({
|
||||
onDoubleClick,
|
||||
onMiddleClick,
|
||||
}: UseClickableTableRowConfig): UseClickableTableRowResult => {
|
||||
const styles = useStyles();
|
||||
const clickableProps = useClickable(onClick);
|
||||
const theme = useTheme();
|
||||
|
||||
return {
|
||||
...clickableProps,
|
||||
className: styles.row,
|
||||
css: {
|
||||
cursor: "pointer",
|
||||
|
||||
"&:focus": {
|
||||
outline: `1px solid ${theme.palette.secondary.dark}`,
|
||||
outlineOffset: -1,
|
||||
},
|
||||
|
||||
"&:last-of-type": {
|
||||
borderBottomLeftRadius: theme.shape.borderRadius,
|
||||
borderBottomRightRadius: theme.shape.borderRadius,
|
||||
},
|
||||
},
|
||||
hover: true,
|
||||
onDoubleClick,
|
||||
onAuxClick: (event) => {
|
||||
@@ -46,19 +58,3 @@ export const useClickableTableRow = ({
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
row: {
|
||||
cursor: "pointer",
|
||||
|
||||
"&:focus": {
|
||||
outline: `1px solid ${theme.palette.secondary.dark}`,
|
||||
outlineOffset: -1,
|
||||
},
|
||||
|
||||
"&:last-of-type": {
|
||||
borderBottomLeftRadius: theme.shape.borderRadius,
|
||||
borderBottomRightRadius: theme.shape.borderRadius,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type Interpolation, type Theme } from "@emotion/react";
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import {
|
||||
import type {
|
||||
ProvisionerJobLog,
|
||||
Template,
|
||||
TemplateExample,
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
VariableValue,
|
||||
} from "api/typesGenerated";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { TemplateUpload, TemplateUploadProps } from "./TemplateUpload";
|
||||
import { TemplateUpload, type TemplateUploadProps } from "./TemplateUpload";
|
||||
import { useFormik } from "formik";
|
||||
import { SelectedTemplate } from "pages/CreateWorkspacePage/SelectedTemplate";
|
||||
import { FC, useEffect } from "react";
|
||||
import { type FC, useEffect } from "react";
|
||||
import {
|
||||
nameValidator,
|
||||
getFormHelpers,
|
||||
@@ -43,8 +43,8 @@ import {
|
||||
} from "pages/TemplateSettingsPage/TemplateSchedulePage/AutostopRequirementHelperText";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import {
|
||||
TemplateAutostartRequirementDaysValue,
|
||||
TemplateAutostopRequirementDaysValue,
|
||||
type TemplateAutostartRequirementDaysValue,
|
||||
type TemplateAutostopRequirementDaysValue,
|
||||
} from "utils/schedule";
|
||||
import {
|
||||
TemplateScheduleAutostart,
|
||||
@@ -218,7 +218,6 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||
allowDisableEveryoneAccess,
|
||||
allowAutostopRequirement,
|
||||
} = props;
|
||||
const styles = useStyles();
|
||||
const form = useFormik<CreateTemplateData>({
|
||||
initialValues: getInitialValues({
|
||||
allowAdvancedScheduling,
|
||||
@@ -338,7 +337,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||
description="Define when workspaces created from this template automatically stop."
|
||||
>
|
||||
<FormFields>
|
||||
<Stack direction="row" className={styles.ttlFields}>
|
||||
<Stack direction="row" css={styles.ttlFields}>
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"default_ttl_hours",
|
||||
@@ -373,7 +372,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||
</Stack>
|
||||
|
||||
{allowAutostopRequirement && (
|
||||
<Stack direction="row" className={styles.ttlFields}>
|
||||
<Stack direction="row" css={styles.ttlFields}>
|
||||
<TextField
|
||||
{...getFieldHelpers(
|
||||
"autostop_requirement_days_of_week",
|
||||
@@ -481,7 +480,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||
<strong>
|
||||
Allow users to customize autostop duration for workspaces.
|
||||
</strong>
|
||||
<span className={styles.optionHelperText}>
|
||||
<span css={styles.optionHelperText}>
|
||||
Workspaces will always use the default TTL if this is set.
|
||||
Regardless of this setting, workspaces can only stay on for
|
||||
the max TTL.
|
||||
@@ -514,7 +513,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={0.5}
|
||||
className={styles.optionText}
|
||||
css={styles.optionText}
|
||||
>
|
||||
<strong>
|
||||
Allow users to cancel in-progress workspace jobs
|
||||
@@ -527,7 +526,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||
</HelpTooltipText>
|
||||
</HelpTooltip>
|
||||
</Stack>
|
||||
<span className={styles.optionHelperText}>
|
||||
<span css={styles.optionHelperText}>
|
||||
Depending on your template, canceling builds may leave
|
||||
workspaces in an unhealthy state. This option isn't
|
||||
recommended for most use cases.
|
||||
@@ -552,7 +551,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={0.5}
|
||||
className={styles.optionText}
|
||||
css={styles.optionText}
|
||||
>
|
||||
<strong>Allow everyone to use the template</strong>
|
||||
|
||||
@@ -569,7 +568,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||
</HelpTooltipText>
|
||||
</HelpTooltip>
|
||||
</Stack>
|
||||
<span className={styles.optionHelperText}>
|
||||
<span css={styles.optionHelperText}>
|
||||
This setting requires an enterprise license for the
|
||||
<Link href={docs("/admin/rbac")}>
|
||||
'Template RBAC'
|
||||
@@ -610,14 +609,14 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
|
||||
|
||||
{jobError && (
|
||||
<Stack>
|
||||
<div className={styles.error}>
|
||||
<h5 className={styles.errorTitle}>Error during provisioning</h5>
|
||||
<p className={styles.errorDescription}>
|
||||
<div css={styles.error}>
|
||||
<h5 css={styles.errorTitle}>Error during provisioning</h5>
|
||||
<p css={styles.errorDescription}>
|
||||
Looks like we found an error during the template provisioning. You
|
||||
can see the logs bellow.
|
||||
</p>
|
||||
|
||||
<code className={styles.errorDetails}>{jobError}</code>
|
||||
<code css={styles.errorDetails}>{jobError}</code>
|
||||
</div>
|
||||
|
||||
<WorkspaceBuildLogs logs={logs ?? []} />
|
||||
@@ -690,43 +689,43 @@ const MaxTTLHelperText = (props: { ttl?: number }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
const styles = {
|
||||
ttlFields: {
|
||||
width: "100%",
|
||||
},
|
||||
|
||||
optionText: {
|
||||
optionText: (theme) => ({
|
||||
fontSize: theme.spacing(2),
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}),
|
||||
|
||||
optionHelperText: {
|
||||
optionHelperText: (theme) => ({
|
||||
fontSize: theme.spacing(1.5),
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}),
|
||||
|
||||
error: {
|
||||
error: (theme) => ({
|
||||
padding: theme.spacing(3),
|
||||
borderRadius: theme.spacing(1),
|
||||
background: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.error.main}`,
|
||||
},
|
||||
}),
|
||||
|
||||
errorTitle: {
|
||||
fontSize: 16,
|
||||
margin: 0,
|
||||
},
|
||||
|
||||
errorDescription: {
|
||||
errorDescription: (theme) => ({
|
||||
margin: 0,
|
||||
color: theme.palette.text.secondary,
|
||||
marginTop: theme.spacing(0.5),
|
||||
},
|
||||
}),
|
||||
|
||||
errorDetails: {
|
||||
errorDetails: (theme) => ({
|
||||
display: "block",
|
||||
marginTop: theme.spacing(1),
|
||||
color: theme.palette.error.light,
|
||||
fontSize: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { useTheme, type Interpolation, type Theme } from "@emotion/react";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import * as TypesGen from "api/typesGenerated";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
|
||||
import { FormikContextType, useFormik } from "formik";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import {
|
||||
getFormHelpers,
|
||||
nameValidator,
|
||||
@@ -17,7 +19,6 @@ import {
|
||||
FormFooter,
|
||||
HorizontalForm,
|
||||
} from "components/Form/Form";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import {
|
||||
getInitialRichParameterValues,
|
||||
useValidationSchemaForRichParameters,
|
||||
@@ -31,7 +32,7 @@ import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { type ExternalAuthPollingState } from "./CreateWorkspacePage";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { CreateWSPermissions } from "./permissions";
|
||||
import type { CreateWSPermissions } from "./permissions";
|
||||
|
||||
export interface CreateWorkspacePageViewProps {
|
||||
error: unknown;
|
||||
@@ -69,7 +70,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
const theme = useTheme();
|
||||
const [owner, setOwner] = useState(defaultOwner);
|
||||
const { verifyExternalAuth, externalAuthErrors } =
|
||||
useExternalAuthVerification(externalAuth);
|
||||
@@ -123,14 +124,14 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
<FormFields>
|
||||
<SelectedTemplate template={template} />
|
||||
{versionId && versionId !== template.active_version_id && (
|
||||
<Stack spacing={1} className={styles.hasDescription}>
|
||||
<Stack spacing={1} css={styles.hasDescription}>
|
||||
<TextField
|
||||
disabled
|
||||
fullWidth
|
||||
value={versionId}
|
||||
label="Version ID"
|
||||
/>
|
||||
<span className={styles.description}>
|
||||
<span css={styles.description}>
|
||||
This parameter has been preset, and cannot be modified.
|
||||
</span>
|
||||
</Stack>
|
||||
@@ -210,7 +211,16 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
/>
|
||||
<ImmutableTemplateParametersSection
|
||||
templateParameters={parameters}
|
||||
classes={{ root: styles.warningSection }}
|
||||
classes={{
|
||||
root: css`
|
||||
border: 1px solid ${theme.palette.warning.light};
|
||||
border-radius: 8px;
|
||||
background-color: ${theme.palette.background.paper};
|
||||
padding: ${theme.spacing(10)};
|
||||
margin-left: ${theme.spacing(-10)};
|
||||
margin-right: ${theme.spacing(-10)};
|
||||
`,
|
||||
}}
|
||||
getInputProps={(parameter, index) => {
|
||||
return {
|
||||
...getFieldHelpers(
|
||||
@@ -279,23 +289,12 @@ const useExternalAuthVerification = (
|
||||
};
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
hasDescription: {
|
||||
const styles = {
|
||||
hasDescription: (theme) => ({
|
||||
paddingBottom: theme.spacing(2),
|
||||
},
|
||||
description: {
|
||||
}),
|
||||
description: (theme) => ({
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
warningText: {
|
||||
color: theme.palette.warning.light,
|
||||
},
|
||||
warningSection: {
|
||||
border: `1px solid ${theme.palette.warning.light}`,
|
||||
borderRadius: 8,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: theme.spacing(10),
|
||||
marginLeft: theme.spacing(-10),
|
||||
marginRight: theme.spacing(-10),
|
||||
},
|
||||
}));
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
+16
-19
@@ -1,13 +1,14 @@
|
||||
import { type Interpolation, type Theme } from "@emotion/react";
|
||||
import Button from "@mui/material/Button";
|
||||
import { makeStyles, useTheme } from "@mui/styles";
|
||||
import { useTheme } from "@mui/styles";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import AddIcon from "@mui/icons-material/AddOutlined";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import { GetLicensesResponse } from "api/api";
|
||||
import type { GetLicensesResponse } from "api/api";
|
||||
import { Header } from "components/DeploySettingsLayout/Header";
|
||||
import { LicenseCard } from "./LicenseCard";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { FC } from "react";
|
||||
import { type FC } from "react";
|
||||
import Confetti from "react-confetti";
|
||||
import { Link } from "react-router-dom";
|
||||
import useWindowSize from "react-use/lib/useWindowSize";
|
||||
@@ -38,10 +39,8 @@ const LicensesSettingsPageView: FC<Props> = ({
|
||||
removeLicense,
|
||||
refreshEntitlements,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
const { width, height } = useWindowSize();
|
||||
|
||||
const theme = useTheme();
|
||||
const { width, height } = useWindowSize();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -107,13 +106,11 @@ const LicensesSettingsPageView: FC<Props> = ({
|
||||
)}
|
||||
|
||||
{!isLoading && licenses === null && (
|
||||
<div className={styles.root}>
|
||||
<div css={styles.root}>
|
||||
<Stack alignItems="center" spacing={1}>
|
||||
<Stack alignItems="center" spacing={0.5}>
|
||||
<span className={styles.title}>
|
||||
You don't have any licenses!
|
||||
</span>
|
||||
<span className={styles.description}>
|
||||
<span css={styles.title}>You don't have any licenses!</span>
|
||||
<span css={styles.description}>
|
||||
You're missing out on high availability, RBAC, quotas, and
|
||||
much more. Contact{" "}
|
||||
<MuiLink href="mailto:sales@coder.com">sales</MuiLink> or{" "}
|
||||
@@ -130,12 +127,12 @@ const LicensesSettingsPageView: FC<Props> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
title: {
|
||||
const styles = {
|
||||
title: (theme) => ({
|
||||
fontSize: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
|
||||
root: {
|
||||
root: (theme) => ({
|
||||
minHeight: theme.spacing(30),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
@@ -143,14 +140,14 @@ const useStyles = makeStyles((theme) => ({
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
padding: theme.spacing(6),
|
||||
},
|
||||
}),
|
||||
|
||||
description: {
|
||||
description: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
textAlign: "center",
|
||||
maxWidth: theme.spacing(58),
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export default LicensesSettingsPageView;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type Interpolation, type Theme } from "@emotion/react";
|
||||
import Button from "@mui/material/Button";
|
||||
import Link from "@mui/material/Link";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
@@ -20,10 +20,10 @@ import {
|
||||
TableRowSkeleton,
|
||||
} from "components/TableLoader/TableLoader";
|
||||
import { UserAvatar } from "components/UserAvatar/UserAvatar";
|
||||
import { FC } from "react";
|
||||
import { type FC } from "react";
|
||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||
import { Paywall } from "components/Paywall/Paywall";
|
||||
import { Group } from "api/typesGenerated";
|
||||
import type { Group } from "api/typesGenerated";
|
||||
import { GroupAvatar } from "components/GroupAvatar/GroupAvatar";
|
||||
import { docs } from "utils/docs";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
@@ -44,7 +44,6 @@ export const GroupsPageView: FC<GroupsPageViewProps> = ({
|
||||
const isLoading = Boolean(groups === undefined);
|
||||
const isEmpty = Boolean(groups && groups.length === 0);
|
||||
const navigate = useNavigate();
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -137,7 +136,7 @@ export const GroupsPageView: FC<GroupsPageViewProps> = ({
|
||||
navigate(groupPageLink);
|
||||
}
|
||||
}}
|
||||
className={styles.clickableTableRow}
|
||||
css={styles.clickableTableRow}
|
||||
>
|
||||
<TableCell>
|
||||
<AvatarData
|
||||
@@ -170,10 +169,8 @@ export const GroupsPageView: FC<GroupsPageViewProps> = ({
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<div className={styles.arrowCell}>
|
||||
<KeyboardArrowRight
|
||||
className={styles.arrowRight}
|
||||
/>
|
||||
<div css={styles.arrowCell}>
|
||||
<KeyboardArrowRight css={styles.arrowRight} />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -210,8 +207,8 @@ const TableLoader = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
clickableTableRow: {
|
||||
const styles = {
|
||||
clickableTableRow: (theme) => ({
|
||||
cursor: "pointer",
|
||||
|
||||
"&:hover td": {
|
||||
@@ -223,17 +220,17 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
|
||||
"& .MuiTableCell-root:last-child": {
|
||||
paddingRight: theme.spacing(2),
|
||||
paddingRight: `${theme.spacing(2)} !important`,
|
||||
},
|
||||
},
|
||||
arrowRight: {
|
||||
}),
|
||||
arrowRight: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
}),
|
||||
arrowCell: {
|
||||
display: "flex",
|
||||
},
|
||||
}));
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export default GroupsPageView;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { MockAuthMethods, mockApiError } from "testHelpers/entities";
|
||||
import {
|
||||
MockAuthMethodsAll,
|
||||
MockAuthMethodsExternal,
|
||||
MockAuthMethodsPasswordOnly,
|
||||
mockApiError,
|
||||
} from "testHelpers/entities";
|
||||
import { LoginPageView } from "./LoginPageView";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
@@ -12,17 +17,37 @@ type Story = StoryObj<typeof LoginPageView>;
|
||||
|
||||
export const Example: Story = {
|
||||
args: {
|
||||
authMethods: MockAuthMethods,
|
||||
authMethods: MockAuthMethodsPasswordOnly,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithExternalAuthMethods: Story = {
|
||||
args: {
|
||||
authMethods: MockAuthMethodsExternal,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAllAuthMethods: Story = {
|
||||
args: {
|
||||
authMethods: MockAuthMethodsAll,
|
||||
},
|
||||
};
|
||||
|
||||
export const AuthError: Story = {
|
||||
args: {
|
||||
error: mockApiError({
|
||||
message: "User or password is incorrect",
|
||||
detail: "Please, try again",
|
||||
message: "Incorrect email or password.",
|
||||
}),
|
||||
authMethods: MockAuthMethods,
|
||||
authMethods: MockAuthMethodsPasswordOnly,
|
||||
},
|
||||
};
|
||||
|
||||
export const ExternalAuthError: Story = {
|
||||
args: {
|
||||
error: mockApiError({
|
||||
message: "Incorrect email or password.",
|
||||
}),
|
||||
authMethods: MockAuthMethodsAll,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -35,6 +60,6 @@ export const LoadingAuthMethods: Story = {
|
||||
export const SigningIn: Story = {
|
||||
args: {
|
||||
isSigningIn: true,
|
||||
authMethods: MockAuthMethods,
|
||||
authMethods: MockAuthMethodsPasswordOnly,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Button from "@mui/material/Button";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { Margins } from "components/Margins/Margins";
|
||||
import { MemoizedMarkdown } from "components/Markdown/Markdown";
|
||||
@@ -8,13 +8,13 @@ import {
|
||||
PageHeaderSubtitle,
|
||||
PageHeaderTitle,
|
||||
} from "components/PageHeader/PageHeader";
|
||||
import { FC } from "react";
|
||||
import { type FC } from "react";
|
||||
import ViewCodeIcon from "@mui/icons-material/OpenInNewOutlined";
|
||||
import PlusIcon from "@mui/icons-material/AddOutlined";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { TemplateExample } from "api/typesGenerated";
|
||||
import type { TemplateExample } from "api/typesGenerated";
|
||||
|
||||
export interface StarterTemplatePageViewProps {
|
||||
starterTemplate?: TemplateExample;
|
||||
@@ -25,7 +25,7 @@ export const StarterTemplatePageView: FC<StarterTemplatePageViewProps> = ({
|
||||
starterTemplate,
|
||||
error,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
const theme = useTheme();
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@@ -65,7 +65,19 @@ export const StarterTemplatePageView: FC<StarterTemplatePageViewProps> = ({
|
||||
}
|
||||
>
|
||||
<Stack direction="row" spacing={3} alignItems="center">
|
||||
<div className={styles.icon}>
|
||||
<div
|
||||
css={{
|
||||
height: theme.spacing(6),
|
||||
width: theme.spacing(6),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
|
||||
"& img": {
|
||||
width: "100%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<img src={starterTemplate.icon} alt="" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -77,39 +89,24 @@ export const StarterTemplatePageView: FC<StarterTemplatePageViewProps> = ({
|
||||
</Stack>
|
||||
</PageHeader>
|
||||
|
||||
<div className={styles.markdownSection} id="readme">
|
||||
<div className={styles.markdownWrapper}>
|
||||
<div
|
||||
css={{
|
||||
background: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
}}
|
||||
id="readme"
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
padding: theme.spacing(5, 5, 8),
|
||||
maxWidth: 800,
|
||||
margin: "auto",
|
||||
}}
|
||||
>
|
||||
<MemoizedMarkdown>{starterTemplate.markdown}</MemoizedMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</Margins>
|
||||
);
|
||||
};
|
||||
|
||||
export const useStyles = makeStyles((theme) => {
|
||||
return {
|
||||
icon: {
|
||||
height: theme.spacing(6),
|
||||
width: theme.spacing(6),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
|
||||
"& img": {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
|
||||
markdownSection: {
|
||||
background: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
|
||||
markdownWrapper: {
|
||||
padding: theme.spacing(5, 5, 8),
|
||||
maxWidth: 800,
|
||||
margin: "auto",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { type Interpolation, type Theme } from "@emotion/react";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { Margins } from "components/Margins/Margins";
|
||||
@@ -9,10 +9,9 @@ import {
|
||||
} from "components/PageHeader/PageHeader";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { TemplateExampleCard } from "components/TemplateExampleCard/TemplateExampleCard";
|
||||
import { FC } from "react";
|
||||
import { type FC } from "react";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { combineClasses } from "utils/combineClasses";
|
||||
import { StarterTemplatesByTag } from "utils/starterTemplates";
|
||||
import type { StarterTemplatesByTag } from "utils/starterTemplates";
|
||||
|
||||
const getTagLabel = (tag: string) => {
|
||||
const labelByTag: Record<string, string> = {
|
||||
@@ -40,7 +39,6 @@ export const StarterTemplatesPageView: FC<StarterTemplatesPageViewProps> = ({
|
||||
error,
|
||||
}) => {
|
||||
const [urlParams] = useSearchParams();
|
||||
const styles = useStyles();
|
||||
const tags = starterTemplatesByTag
|
||||
? selectTags(starterTemplatesByTag)
|
||||
: undefined;
|
||||
@@ -64,16 +62,16 @@ export const StarterTemplatesPageView: FC<StarterTemplatesPageViewProps> = ({
|
||||
|
||||
<Stack direction="row" spacing={4}>
|
||||
{starterTemplatesByTag && tags && (
|
||||
<Stack className={styles.filter}>
|
||||
<span className={styles.filterCaption}>Filter</span>
|
||||
<Stack css={styles.filter}>
|
||||
<span css={styles.filterCaption}>Filter</span>
|
||||
{tags.map((tag) => (
|
||||
<Link
|
||||
key={tag}
|
||||
to={`?tag=${tag}`}
|
||||
className={combineClasses({
|
||||
[styles.tagLink]: true,
|
||||
[styles.tagLinkActive]: tag === activeTag,
|
||||
})}
|
||||
css={[
|
||||
styles.tagLink,
|
||||
tag === activeTag && styles.tagLinkActive,
|
||||
]}
|
||||
>
|
||||
{getTagLabel(tag)} ({starterTemplatesByTag[tag].length})
|
||||
</Link>
|
||||
@@ -81,7 +79,7 @@ export const StarterTemplatesPageView: FC<StarterTemplatesPageViewProps> = ({
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<div className={styles.templates}>
|
||||
<div css={styles.templates}>
|
||||
{visibleTemplates &&
|
||||
visibleTemplates.map((example) => (
|
||||
<TemplateExampleCard example={example} key={example.id} />
|
||||
@@ -92,21 +90,21 @@ export const StarterTemplatesPageView: FC<StarterTemplatesPageViewProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
filter: {
|
||||
const styles = {
|
||||
filter: (theme) => ({
|
||||
width: theme.spacing(26),
|
||||
flexShrink: 0,
|
||||
},
|
||||
}),
|
||||
|
||||
filterCaption: {
|
||||
filterCaption: (theme) => ({
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 600,
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
letterSpacing: "0.1em",
|
||||
},
|
||||
}),
|
||||
|
||||
tagLink: {
|
||||
tagLink: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
textDecoration: "none",
|
||||
fontSize: 14,
|
||||
@@ -115,18 +113,18 @@ const useStyles = makeStyles((theme) => ({
|
||||
"&:hover": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
tagLinkActive: {
|
||||
tagLinkActive: (theme) => ({
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 600,
|
||||
},
|
||||
}),
|
||||
|
||||
templates: {
|
||||
templates: (theme) => ({
|
||||
flex: "1",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
|
||||
gap: theme.spacing(2),
|
||||
gridAutoRows: "min-content",
|
||||
},
|
||||
}));
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,27 +1,72 @@
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { css } from "@emotion/css";
|
||||
import {
|
||||
useTheme,
|
||||
type CSSObject,
|
||||
type Interpolation,
|
||||
type Theme,
|
||||
} from "@emotion/react";
|
||||
import ScheduleIcon from "@mui/icons-material/TimerOutlined";
|
||||
import VariablesIcon from "@mui/icons-material/CodeOutlined";
|
||||
import { Template } from "api/typesGenerated";
|
||||
import type { Template } from "api/typesGenerated";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { FC, ElementType, PropsWithChildren, ReactNode } from "react";
|
||||
import {
|
||||
type FC,
|
||||
type ElementType,
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { Link, NavLink } from "react-router-dom";
|
||||
import { combineClasses } from "utils/combineClasses";
|
||||
import GeneralIcon from "@mui/icons-material/SettingsOutlined";
|
||||
import SecurityIcon from "@mui/icons-material/LockOutlined";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import { combineClasses } from "utils/combineClasses";
|
||||
|
||||
const SidebarNavItem: FC<
|
||||
PropsWithChildren<{ href: string; icon: ReactNode }>
|
||||
> = ({ children, href, icon }) => {
|
||||
const styles = useStyles();
|
||||
const theme = useTheme();
|
||||
|
||||
const sidebarNavItemStyles = css`
|
||||
color: inherit;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
padding: ${theme.spacing(1.5, 1.5, 1.5, 2)};
|
||||
border-radius: ${theme.shape.borderRadius / 2}px;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
margin-bottom: 1px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: ${theme.palette.action.hover};
|
||||
}
|
||||
`;
|
||||
|
||||
const sidebarNavItemActiveStyles = css`
|
||||
background-color: ${theme.palette.action.hover};
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: ${theme.palette.secondary.dark};
|
||||
border-top-left-radius: ${theme.shape.borderRadius}px;
|
||||
border-bottom-left-radius: ${theme.shape.borderRadius}px;
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
end
|
||||
to={href}
|
||||
className={({ isActive }) =>
|
||||
combineClasses([
|
||||
styles.sidebarNavItem,
|
||||
isActive ? styles.sidebarNavItemActive : undefined,
|
||||
sidebarNavItemStyles,
|
||||
isActive ? sidebarNavItemActiveStyles : undefined,
|
||||
])
|
||||
}
|
||||
>
|
||||
@@ -36,28 +81,21 @@ const SidebarNavItem: FC<
|
||||
const SidebarNavItemIcon: React.FC<{ icon: ElementType }> = ({
|
||||
icon: Icon,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
return <Icon className={styles.sidebarNavItemIcon} />;
|
||||
return <Icon css={styles.sidebarNavItemIcon} />;
|
||||
};
|
||||
|
||||
export const Sidebar: React.FC<{ template: Template }> = ({ template }) => {
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<nav className={styles.sidebar}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
className={styles.templateInfo}
|
||||
>
|
||||
<nav css={styles.sidebar}>
|
||||
<Stack direction="row" alignItems="center" css={styles.templateInfo}>
|
||||
<Avatar src={template.icon} variant="square" fitImage />
|
||||
<Stack spacing={0} className={styles.templateData}>
|
||||
<Link className={styles.name} to={`/templates/${template.name}`}>
|
||||
<Stack spacing={0} css={styles.templateData}>
|
||||
<Link css={styles.name} to={`/templates/${template.name}`}>
|
||||
{template.display_name !== ""
|
||||
? template.display_name
|
||||
: template.name}
|
||||
</Link>
|
||||
<span className={styles.secondary}>{template.name}</span>
|
||||
<span css={styles.secondary}>{template.name}</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -86,65 +124,34 @@ export const Sidebar: React.FC<{ template: Template }> = ({ template }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
const styles = {
|
||||
sidebar: {
|
||||
width: 245,
|
||||
flexShrink: 0,
|
||||
},
|
||||
sidebarNavItem: {
|
||||
color: "inherit",
|
||||
display: "block",
|
||||
fontSize: 14,
|
||||
textDecoration: "none",
|
||||
padding: theme.spacing(1.5, 1.5, 1.5, 2),
|
||||
borderRadius: theme.shape.borderRadius / 2,
|
||||
transition: "background-color 0.15s ease-in-out",
|
||||
marginBottom: 1,
|
||||
position: "relative",
|
||||
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
},
|
||||
sidebarNavItemActive: {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
|
||||
"&:before": {
|
||||
content: '""',
|
||||
display: "block",
|
||||
width: 3,
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
backgroundColor: theme.palette.secondary.dark,
|
||||
borderTopLeftRadius: theme.shape.borderRadius,
|
||||
borderBottomLeftRadius: theme.shape.borderRadius,
|
||||
},
|
||||
},
|
||||
sidebarNavItemIcon: {
|
||||
sidebarNavItemIcon: (theme) => ({
|
||||
width: theme.spacing(2),
|
||||
height: theme.spacing(2),
|
||||
},
|
||||
templateInfo: {
|
||||
...theme.typography.body2,
|
||||
}),
|
||||
templateInfo: (theme) => ({
|
||||
...(theme.typography.body2 as CSSObject),
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
templateData: {
|
||||
overflow: "hidden",
|
||||
},
|
||||
name: {
|
||||
name: (theme) => ({
|
||||
fontWeight: 600,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
color: theme.palette.text.primary,
|
||||
textDecoration: "none",
|
||||
},
|
||||
secondary: {
|
||||
}),
|
||||
secondary: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 12,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
},
|
||||
}));
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type Interpolation, type Theme } from "@emotion/react";
|
||||
import Button from "@mui/material/Button";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
@@ -7,7 +7,7 @@ import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import AddIcon from "@mui/icons-material/AddOutlined";
|
||||
import { FC } from "react";
|
||||
import { type FC } from "react";
|
||||
import { useNavigate, Link as RouterLink } from "react-router-dom";
|
||||
import { createDayString } from "utils/createDayString";
|
||||
import {
|
||||
@@ -35,8 +35,7 @@ import {
|
||||
} from "components/HelpTooltip/HelpTooltip";
|
||||
import { EmptyTemplates } from "./EmptyTemplates";
|
||||
import { useClickableTableRow } from "hooks/useClickableTableRow";
|
||||
import { Template, TemplateExample } from "api/typesGenerated";
|
||||
import { combineClasses } from "utils/combineClasses";
|
||||
import type { Template, TemplateExample } from "api/typesGenerated";
|
||||
import { colors } from "theme/colors";
|
||||
import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
@@ -80,17 +79,17 @@ const TemplateRow: FC<{ template: Template }> = ({ template }) => {
|
||||
const templatePageLink = `/templates/${template.name}`;
|
||||
const hasIcon = template.icon && template.icon !== "";
|
||||
const navigate = useNavigate();
|
||||
const styles = useStyles();
|
||||
|
||||
const { className: clickableClassName, ...clickableRow } =
|
||||
useClickableTableRow({ onClick: () => navigate(templatePageLink) });
|
||||
const { css: clickableCss, ...clickableRow } = useClickableTableRow({
|
||||
onClick: () => navigate(templatePageLink),
|
||||
});
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={template.id}
|
||||
data-testid={`template-${template.id}`}
|
||||
{...clickableRow}
|
||||
className={combineClasses([clickableClassName, styles.tableRow])}
|
||||
css={[clickableCss, styles.tableRow]}
|
||||
>
|
||||
<TableCell>
|
||||
<AvatarData
|
||||
@@ -106,22 +105,23 @@ const TemplateRow: FC<{ template: Template }> = ({ template }) => {
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className={styles.secondary}>
|
||||
<TableCell css={styles.secondary}>
|
||||
{Language.developerCount(template.active_user_count)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className={styles.secondary}>
|
||||
<TableCell css={styles.secondary}>
|
||||
{formatTemplateBuildTime(template.build_time_stats.start.P50)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell data-chromatic="ignore" className={styles.secondary}>
|
||||
<TableCell data-chromatic="ignore" css={styles.secondary}>
|
||||
{createDayString(template.updated_at)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className={styles.actionCell}>
|
||||
<TableCell css={styles.actionCell}>
|
||||
<Button
|
||||
size="small"
|
||||
className={styles.actionButton}
|
||||
css={styles.actionButton}
|
||||
className="actionButton"
|
||||
startIcon={<ArrowForwardOutlined />}
|
||||
title={`Create a workspace using the ${template.display_name} template`}
|
||||
onClick={(e) => {
|
||||
@@ -247,7 +247,7 @@ const TableLoader = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
const styles = {
|
||||
templateIconWrapper: {
|
||||
// Same size then the avatar component
|
||||
width: 36,
|
||||
@@ -261,20 +261,20 @@ const useStyles = makeStyles((theme) => ({
|
||||
actionCell: {
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
secondary: {
|
||||
secondary: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
tableRow: {
|
||||
"&:hover $actionButton": {
|
||||
}),
|
||||
tableRow: (theme) => ({
|
||||
"&:hover .actionButton": {
|
||||
color: theme.palette.text.primary,
|
||||
borderColor: colors.gray[11],
|
||||
"&:hover": {
|
||||
borderColor: theme.palette.text.primary,
|
||||
},
|
||||
},
|
||||
},
|
||||
actionButton: {
|
||||
}),
|
||||
actionButton: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
transition: "none",
|
||||
},
|
||||
}));
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { makeStyles, useTheme } from "@mui/styles";
|
||||
import { FC, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import { type FC, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import { colors } from "theme/colors";
|
||||
@@ -16,7 +16,7 @@ import { pageTitle } from "utils/page";
|
||||
import { useProxy } from "contexts/ProxyContext";
|
||||
import Box from "@mui/material/Box";
|
||||
import { useDashboard } from "components/Dashboard/DashboardProvider";
|
||||
import { Region } from "api/typesGenerated";
|
||||
import type { Region } from "api/typesGenerated";
|
||||
import { getLatencyColor } from "utils/latency";
|
||||
import { ProxyStatusLatency } from "components/ProxyStatusLatency/ProxyStatusLatency";
|
||||
import { openMaybePortForwardedURL } from "utils/portForward";
|
||||
@@ -45,7 +45,6 @@ export const Language = {
|
||||
|
||||
const TerminalPage: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const styles = useStyles();
|
||||
const { proxy } = useProxy();
|
||||
const params = useParams() as { username: string; workspace: string };
|
||||
const username = params.username.replace("@", "");
|
||||
@@ -316,11 +315,7 @@ const TerminalPage: FC = () => {
|
||||
{lifecycleState === "ready" &&
|
||||
prevLifecycleState.current === "starting" && <LoadedScriptsAlert />}
|
||||
{terminalState === "disconnected" && <DisconnectedAlert />}
|
||||
<div
|
||||
className={styles.terminal}
|
||||
ref={xtermRef}
|
||||
data-testid="terminal"
|
||||
/>
|
||||
<div css={styles.terminal} ref={xtermRef} data-testid="terminal" />
|
||||
{dashboard.experiments.includes("moons") &&
|
||||
selectedProxy &&
|
||||
latency && (
|
||||
@@ -426,35 +421,8 @@ const BottomBar = ({ proxy, latency }: { proxy: Region; latency?: number }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
overlay: {
|
||||
position: "absolute",
|
||||
pointerEvents: "none",
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
zIndex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
display: "flex",
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.6)",
|
||||
backdropFilter: "blur(4px)",
|
||||
"&.connected": {
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
overlayText: {
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
},
|
||||
overlaySubtext: {
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
terminal: {
|
||||
const styles = {
|
||||
terminal: (theme) => ({
|
||||
width: "100vw",
|
||||
overflow: "hidden",
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
@@ -480,36 +448,7 @@ const useStyles = makeStyles((theme) => ({
|
||||
minHeight: 20,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.18)",
|
||||
},
|
||||
},
|
||||
alert: {
|
||||
display: "flex",
|
||||
background: theme.palette.background.paperLight,
|
||||
alignItems: "center",
|
||||
padding: theme.spacing(2),
|
||||
gap: theme.spacing(2),
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
...theme.typography.body2,
|
||||
},
|
||||
alertIcon: {
|
||||
color: theme.palette.warning.light,
|
||||
fontSize: theme.spacing(3),
|
||||
},
|
||||
alertError: {
|
||||
"& $alertIcon": {
|
||||
color: theme.palette.error.light,
|
||||
},
|
||||
},
|
||||
alertTitle: {
|
||||
fontWeight: 600,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
alertMessage: {
|
||||
fontSize: 14,
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
alertActions: {
|
||||
marginLeft: "auto",
|
||||
},
|
||||
}));
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export default TerminalPage;
|
||||
|
||||
@@ -6,10 +6,7 @@ import {
|
||||
waitForLoaderToBeRemoved,
|
||||
} from "testHelpers/renderHelpers";
|
||||
import { SecurityPage } from "./SecurityPage";
|
||||
import {
|
||||
MockAuthMethodsWithPasswordType,
|
||||
mockApiError,
|
||||
} from "testHelpers/entities";
|
||||
import { MockAuthMethodsAll, mockApiError } from "testHelpers/entities";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import * as SSO from "./SingleSignOnSection";
|
||||
import { OAuthConversionResponse } from "api/typesGenerated";
|
||||
@@ -40,9 +37,7 @@ const fillAndSubmitSecurityForm = () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(API, "getAuthMethods")
|
||||
.mockResolvedValue(MockAuthMethodsWithPasswordType);
|
||||
jest.spyOn(API, "getAuthMethods").mockResolvedValue(MockAuthMethodsAll);
|
||||
jest.spyOn(API, "getUserLoginType").mockResolvedValue({
|
||||
login_type: "password",
|
||||
});
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { SecurityPageView } from "./SecurityPage";
|
||||
import { action } from "@storybook/addon-actions";
|
||||
import {
|
||||
MockAuthMethods,
|
||||
MockAuthMethodsWithPasswordType,
|
||||
MockAuthMethodsPasswordOnly,
|
||||
MockAuthMethodsAll,
|
||||
} from "testHelpers/entities";
|
||||
import { ComponentProps } from "react";
|
||||
import set from "lodash/fp/set";
|
||||
@@ -22,7 +22,7 @@ const defaultArgs: ComponentProps<typeof SecurityPageView> = {
|
||||
userLoginType: {
|
||||
login_type: "password",
|
||||
},
|
||||
authMethods: MockAuthMethods,
|
||||
authMethods: MockAuthMethodsPasswordOnly,
|
||||
closeConfirmation: action("closeConfirmation"),
|
||||
confirm: action("confirm"),
|
||||
error: undefined,
|
||||
@@ -52,11 +52,7 @@ export const NoOIDCAvailable: Story = {
|
||||
};
|
||||
|
||||
export const UserLoginTypeIsPassword: Story = {
|
||||
args: set(
|
||||
"oidc.section.authMethods",
|
||||
MockAuthMethodsWithPasswordType,
|
||||
defaultArgs,
|
||||
),
|
||||
args: set("oidc.section.authMethods", MockAuthMethodsAll, defaultArgs),
|
||||
};
|
||||
|
||||
export const ConfirmingOIDCConversion: Story = {
|
||||
@@ -64,7 +60,7 @@ export const ConfirmingOIDCConversion: Story = {
|
||||
"oidc.section",
|
||||
{
|
||||
...defaultArgs.oidc?.section,
|
||||
authMethods: MockAuthMethodsWithPasswordType,
|
||||
authMethods: MockAuthMethodsAll,
|
||||
isConfirming: true,
|
||||
},
|
||||
defaultArgs,
|
||||
|
||||
@@ -7,7 +7,7 @@ import KeyIcon from "@mui/icons-material/VpnKey";
|
||||
import Button from "@mui/material/Button";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { convertToOAUTH } from "api/api";
|
||||
import {
|
||||
import type {
|
||||
AuthMethods,
|
||||
LoginType,
|
||||
OIDCAuthMethod,
|
||||
@@ -19,7 +19,6 @@ import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import { getErrorMessage } from "api/errors";
|
||||
import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined";
|
||||
import { EmptyState } from "components/EmptyState/EmptyState";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import Link from "@mui/material/Link";
|
||||
import { docs } from "utils/docs";
|
||||
|
||||
@@ -101,21 +100,15 @@ export const useSingleSignOnSection = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const useEmptyStateStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
minHeight: 0,
|
||||
padding: theme.spacing(6, 4),
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
}));
|
||||
|
||||
function SSOEmptyState() {
|
||||
const styles = useEmptyStateStyles();
|
||||
|
||||
return (
|
||||
<EmptyState
|
||||
className={styles.root}
|
||||
css={(theme) => ({
|
||||
minHeight: 0,
|
||||
padding: theme.spacing(6, 4),
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
})}
|
||||
message="No SSO Providers"
|
||||
description="No SSO providers are configured with this Coder deployment."
|
||||
cta={
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { FC, PropsWithChildren } from "react";
|
||||
import { type FC, type PropsWithChildren } from "react";
|
||||
import { Section } from "components/SettingsLayout/Section";
|
||||
import { WorkspaceProxyView } from "./WorkspaceProxyView";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import { useProxy } from "contexts/ProxyContext";
|
||||
|
||||
export const WorkspaceProxyPage: FC<PropsWithChildren<unknown>> = () => {
|
||||
const styles = useStyles();
|
||||
|
||||
const description =
|
||||
"Workspace proxies improve terminal and web app connections to workspaces.";
|
||||
|
||||
@@ -22,7 +19,15 @@ export const WorkspaceProxyPage: FC<PropsWithChildren<unknown>> = () => {
|
||||
return (
|
||||
<Section
|
||||
title="Workspace Proxies"
|
||||
className={styles.section}
|
||||
css={(theme) => ({
|
||||
"& code": {
|
||||
background: theme.palette.divider,
|
||||
fontSize: 12,
|
||||
padding: "2px 4px",
|
||||
color: theme.palette.text.primary,
|
||||
borderRadius: 2,
|
||||
},
|
||||
})}
|
||||
description={description}
|
||||
layout="fluid"
|
||||
>
|
||||
@@ -38,16 +43,4 @@ export const WorkspaceProxyPage: FC<PropsWithChildren<unknown>> = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
section: {
|
||||
"& code": {
|
||||
background: theme.palette.divider,
|
||||
fontSize: 12,
|
||||
padding: "2px 4px",
|
||||
color: theme.palette.text.primary,
|
||||
borderRadius: 2,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export default WorkspaceProxyPage;
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
MockUser2,
|
||||
MockAssignableSiteRoles,
|
||||
mockApiError,
|
||||
MockAuthMethods,
|
||||
MockAuthMethodsPasswordOnly,
|
||||
} from "testHelpers/entities";
|
||||
import { UsersPageView } from "./UsersPageView";
|
||||
import { ComponentProps } from "react";
|
||||
@@ -37,7 +37,7 @@ const meta: Meta<typeof UsersPageView> = {
|
||||
count: 2,
|
||||
canEditUsers: true,
|
||||
filterProps: defaultFilterProps,
|
||||
authMethods: MockAuthMethods,
|
||||
authMethods: MockAuthMethodsPasswordOnly,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import { EditSquare } from "components/Icons/EditSquare";
|
||||
import { FC } from "react";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { type FC } from "react";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import UserIcon from "@mui/icons-material/PersonOutline";
|
||||
@@ -34,15 +35,13 @@ const Option: React.FC<{
|
||||
isChecked: boolean;
|
||||
onChange: (roleName: string) => void;
|
||||
}> = ({ value, name, description, isChecked, onChange }) => {
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<label htmlFor={name} className={styles.option}>
|
||||
<label htmlFor={name} css={styles.option}>
|
||||
<Stack direction="row" alignItems="flex-start">
|
||||
<Checkbox
|
||||
id={name}
|
||||
size="small"
|
||||
className={styles.checkbox}
|
||||
css={styles.checkbox}
|
||||
value={value}
|
||||
checked={isChecked}
|
||||
onChange={(e) => {
|
||||
@@ -51,7 +50,7 @@ const Option: React.FC<{
|
||||
/>
|
||||
<Stack spacing={0}>
|
||||
<strong>{name}</strong>
|
||||
<span className={styles.optionDescription}>{description}</span>
|
||||
<span css={styles.optionDescription}>{description}</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</label>
|
||||
@@ -77,7 +76,7 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
|
||||
userLoginType,
|
||||
oidcRoleSync,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
const theme = useTheme();
|
||||
|
||||
const handleChange = (roleName: string) => {
|
||||
if (selectedRoleNames.has(roleName)) {
|
||||
@@ -108,20 +107,28 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
size="small"
|
||||
className={styles.editButton}
|
||||
css={styles.editButton}
|
||||
title="Edit user roles"
|
||||
>
|
||||
<EditSquare />
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent classes={{ paper: styles.popoverPaper }}>
|
||||
<PopoverContent
|
||||
classes={{
|
||||
paper: css`
|
||||
width: ${theme.spacing(45)};
|
||||
margin-top: ${theme.spacing(1)};
|
||||
background: ${theme.palette.background.paperLight};
|
||||
`,
|
||||
}}
|
||||
>
|
||||
<fieldset
|
||||
className={styles.fieldset}
|
||||
css={styles.fieldset}
|
||||
disabled={isLoading}
|
||||
title="Available roles"
|
||||
>
|
||||
<Stack className={styles.options} spacing={3}>
|
||||
<Stack css={styles.options} spacing={3}>
|
||||
{roles.map((role) => (
|
||||
<Option
|
||||
key={role.name}
|
||||
@@ -134,12 +141,12 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
|
||||
))}
|
||||
</Stack>
|
||||
</fieldset>
|
||||
<div className={styles.footer}>
|
||||
<div css={styles.footer}>
|
||||
<Stack direction="row" alignItems="flex-start">
|
||||
<UserIcon className={styles.userIcon} />
|
||||
<UserIcon css={styles.userIcon} />
|
||||
<Stack spacing={0}>
|
||||
<strong>Member</strong>
|
||||
<span className={styles.optionDescription}>
|
||||
<span css={styles.optionDescription}>
|
||||
{roleDescriptions.member}
|
||||
</span>
|
||||
</Stack>
|
||||
@@ -150,8 +157,8 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
editButton: {
|
||||
const styles = {
|
||||
editButton: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
|
||||
"& .MuiSvgIcon-root": {
|
||||
@@ -165,12 +172,7 @@ const useStyles = makeStyles((theme) => ({
|
||||
color: theme.palette.text.primary,
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
},
|
||||
popoverPaper: {
|
||||
width: theme.spacing(45),
|
||||
marginTop: theme.spacing(1),
|
||||
background: theme.palette.background.paperLight,
|
||||
},
|
||||
}),
|
||||
fieldset: {
|
||||
border: 0,
|
||||
margin: 0,
|
||||
@@ -180,14 +182,14 @@ const useStyles = makeStyles((theme) => ({
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
options: (theme) => ({
|
||||
padding: theme.spacing(3),
|
||||
},
|
||||
}),
|
||||
option: {
|
||||
cursor: "pointer",
|
||||
fontSize: 14,
|
||||
},
|
||||
checkbox: {
|
||||
checkbox: (theme) => ({
|
||||
padding: 0,
|
||||
position: "relative",
|
||||
top: 1, // Alignment
|
||||
@@ -196,21 +198,21 @@ const useStyles = makeStyles((theme) => ({
|
||||
width: theme.spacing(2.5),
|
||||
height: theme.spacing(2.5),
|
||||
},
|
||||
},
|
||||
optionDescription: {
|
||||
}),
|
||||
optionDescription: (theme) => ({
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "160%",
|
||||
},
|
||||
footer: {
|
||||
}),
|
||||
footer: (theme) => ({
|
||||
padding: theme.spacing(3),
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
fontSize: 14,
|
||||
},
|
||||
userIcon: {
|
||||
}),
|
||||
userIcon: (theme) => ({
|
||||
width: theme.spacing(2.5), // Same as the checkbox
|
||||
height: theme.spacing(2.5),
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
}));
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -90,7 +90,7 @@ export function UserGroupsCell({ userGroups }: GroupsCellProps) {
|
||||
flexFlow: "column nowrap",
|
||||
fontSize: theme.typography.body2.fontSize,
|
||||
padding: theme.spacing(0.5, 0.25),
|
||||
gap: theme.spacing(0),
|
||||
gap: 0,
|
||||
}}
|
||||
>
|
||||
{userGroups.map((group) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
MockUser,
|
||||
MockUser2,
|
||||
MockAssignableSiteRoles,
|
||||
MockAuthMethods,
|
||||
MockAuthMethodsPasswordOnly,
|
||||
MockGroup,
|
||||
} from "testHelpers/entities";
|
||||
import { UsersTable } from "./UsersTable";
|
||||
@@ -18,7 +18,7 @@ const meta: Meta<typeof UsersTable> = {
|
||||
component: UsersTable,
|
||||
args: {
|
||||
isNonInitialPage: false,
|
||||
authMethods: MockAuthMethods,
|
||||
authMethods: MockAuthMethodsPasswordOnly,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { type Interpolation, type Theme } from "@emotion/react";
|
||||
import { BuildAvatar } from "components/BuildAvatar/BuildAvatar";
|
||||
import { FC } from "react";
|
||||
import { type FC } from "react";
|
||||
import { ProvisionerJobLog, WorkspaceBuild } from "api/typesGenerated";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import {
|
||||
FullWidthPageHeader,
|
||||
PageHeaderTitle,
|
||||
@@ -48,8 +48,6 @@ export const WorkspaceBuildPageView: FC<WorkspaceBuildPageViewProps> = ({
|
||||
builds,
|
||||
activeBuildNumber,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
|
||||
if (!build) {
|
||||
return <Loader />;
|
||||
}
|
||||
@@ -65,9 +63,9 @@ export const WorkspaceBuildPageView: FC<WorkspaceBuildPageViewProps> = ({
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<Stats aria-label="Build details" className={styles.stats}>
|
||||
<Stats aria-label="Build details" css={styles.stats}>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
css={styles.statsItem}
|
||||
label="Workspace"
|
||||
value={
|
||||
<Link
|
||||
@@ -78,22 +76,22 @@ export const WorkspaceBuildPageView: FC<WorkspaceBuildPageViewProps> = ({
|
||||
}
|
||||
/>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
css={styles.statsItem}
|
||||
label="Template version"
|
||||
value={build.template_version_name}
|
||||
/>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
css={styles.statsItem}
|
||||
label="Duration"
|
||||
value={displayWorkspaceBuildDuration(build)}
|
||||
/>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
css={styles.statsItem}
|
||||
label="Started at"
|
||||
value={new Date(build.created_at).toLocaleString()}
|
||||
/>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
css={styles.statsItem}
|
||||
label="Action"
|
||||
value={
|
||||
<Box component="span" sx={{ textTransform: "capitalize" }}>
|
||||
@@ -240,8 +238,8 @@ const BuildSidebarItemSkeleton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
stats: {
|
||||
const styles = {
|
||||
stats: (theme) => ({
|
||||
padding: 0,
|
||||
border: 0,
|
||||
gap: theme.spacing(6),
|
||||
@@ -254,7 +252,7 @@ const useStyles = makeStyles((theme) => ({
|
||||
alignItems: "flex-start",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
statsItem: {
|
||||
flexDirection: "column",
|
||||
@@ -266,4 +264,4 @@ const useStyles = makeStyles((theme) => ({
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
}));
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined";
|
||||
import { FC, Fragment, ReactNode, useRef, useState } from "react";
|
||||
import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated";
|
||||
import { type FC, Fragment, type ReactNode, useRef, useState } from "react";
|
||||
import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated";
|
||||
import {
|
||||
ActionLoadingButton,
|
||||
CancelButton,
|
||||
@@ -56,7 +55,6 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
isRestarting,
|
||||
canChangeVersions,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
const {
|
||||
canCancel,
|
||||
canAcceptJobs,
|
||||
@@ -115,7 +113,14 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.actions} data-testid="workspace-actions">
|
||||
<div
|
||||
css={(theme) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1.5),
|
||||
})}
|
||||
data-testid="workspace-actions"
|
||||
>
|
||||
{canBeUpdated &&
|
||||
(isUpdating
|
||||
? buttonMapping[ButtonTypesEnum.updating]
|
||||
@@ -167,11 +172,3 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
actions: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1.5),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { type Interpolation, type Theme } from "@emotion/react";
|
||||
import LinearProgress from "@mui/material/LinearProgress";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import { TransitionStats, Template, Workspace } from "api/typesGenerated";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import type { TransitionStats, Template, Workspace } from "api/typesGenerated";
|
||||
import dayjs, { type Dayjs } from "dayjs";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import capitalize from "lodash/capitalize";
|
||||
|
||||
import duration from "dayjs/plugin/duration";
|
||||
@@ -68,7 +69,6 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
|
||||
workspace,
|
||||
transitionStats: transitionStats,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
const job = workspace.latest_build.job;
|
||||
const [progressValue, setProgressValue] = useState<number | undefined>(0);
|
||||
const [progressText, setProgressText] = useState<string | undefined>(
|
||||
@@ -107,7 +107,7 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<div className={styles.stack}>
|
||||
<div css={styles.stack}>
|
||||
<LinearProgress
|
||||
data-chromatic="ignore"
|
||||
value={progressValue !== undefined ? progressValue : 0}
|
||||
@@ -123,13 +123,17 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
|
||||
// If a transition is set, there is a moment on new load where the
|
||||
// bar accelerates to progressValue and then rapidly decelerates, which
|
||||
// is not indicative of true progress.
|
||||
classes={{ bar: styles.noTransition }}
|
||||
classes={{
|
||||
bar: css`
|
||||
transition: none;
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<div className={styles.barHelpers}>
|
||||
<div className={styles.label}>
|
||||
<div css={styles.barHelpers}>
|
||||
<div css={styles.label}>
|
||||
{capitalize(workspace.latest_build.status)} workspace...
|
||||
</div>
|
||||
<div className={styles.label} data-chromatic="ignore">
|
||||
<div css={styles.label} data-chromatic="ignore">
|
||||
{progressText}
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,23 +141,20 @@ export const WorkspaceBuildProgress: FC<WorkspaceBuildProgressProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
stack: {
|
||||
const styles = {
|
||||
stack: (theme) => ({
|
||||
paddingLeft: theme.spacing(0.2),
|
||||
paddingRight: theme.spacing(0.2),
|
||||
},
|
||||
noTransition: {
|
||||
transition: "none",
|
||||
},
|
||||
barHelpers: {
|
||||
}),
|
||||
barHelpers: (theme) => ({
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginTop: theme.spacing(0.5),
|
||||
},
|
||||
label: {
|
||||
}),
|
||||
label: (theme) => ({
|
||||
fontSize: 12,
|
||||
display: "block",
|
||||
fontWeight: 600,
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}));
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import Link from "@mui/material/Link";
|
||||
import { WorkspaceOutdatedTooltip } from "components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip";
|
||||
import { FC } from "react";
|
||||
import { type FC } from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { createDayString } from "utils/createDayString";
|
||||
import {
|
||||
@@ -9,13 +11,12 @@ import {
|
||||
isWorkspaceOn,
|
||||
workspaceUpdatePolicy,
|
||||
} from "utils/workspace";
|
||||
import { Workspace } from "api/typesGenerated";
|
||||
import type { Workspace } from "api/typesGenerated";
|
||||
import { Stats, StatsItem } from "components/Stats/Stats";
|
||||
import upperFirst from "lodash/upperFirst";
|
||||
import { autostartDisplay, autostopDisplay } from "utils/schedule";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import RemoveIcon from "@mui/icons-material/RemoveOutlined";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import AddIcon from "@mui/icons-material/AddOutlined";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Button from "@mui/material/Button";
|
||||
@@ -70,26 +71,36 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
|
||||
onDeadlineMinus,
|
||||
onDeadlinePlus,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const initiatedBy = getDisplayWorkspaceBuildInitiatedBy(
|
||||
workspace.latest_build,
|
||||
);
|
||||
const displayTemplateName = getDisplayWorkspaceTemplateName(workspace);
|
||||
const styles = useStyles();
|
||||
const deadlinePlusEnabled = maxDeadlineIncrease >= 1;
|
||||
const deadlineMinusEnabled = maxDeadlineDecrease >= 1;
|
||||
const templatePoliciesEnabled = useTemplatePoliciesEnabled();
|
||||
|
||||
const paperStyles = css`
|
||||
padding: ${theme.spacing(3)};
|
||||
max-width: ${theme.spacing(36)};
|
||||
margin-top: ${theme.spacing(1)};
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.spacing(1)};
|
||||
`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stats aria-label={Language.workspaceDetails} className={styles.stats}>
|
||||
<Stats aria-label={Language.workspaceDetails} css={styles.stats}>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
css={styles.statsItem}
|
||||
label="Status"
|
||||
value={<WorkspaceStatusText workspace={workspace} />}
|
||||
/>
|
||||
<DormantDeletionStat workspace={workspace} />
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
css={styles.statsItem}
|
||||
label={Language.templateLabel}
|
||||
value={
|
||||
<Link
|
||||
@@ -101,7 +112,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
|
||||
}
|
||||
/>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
css={styles.statsItem}
|
||||
label={Language.versionLabel}
|
||||
value={
|
||||
<>
|
||||
@@ -124,7 +135,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
|
||||
}
|
||||
/>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
css={styles.statsItem}
|
||||
label={Language.lastBuiltLabel}
|
||||
value={
|
||||
<>
|
||||
@@ -135,10 +146,10 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
|
||||
/>
|
||||
{shouldDisplayScheduleLabel(workspace) && (
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
css={styles.statsItem}
|
||||
label={getScheduleLabel(workspace)}
|
||||
value={
|
||||
<span className={styles.scheduleValue}>
|
||||
<span css={styles.scheduleValue}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to="settings/schedule"
|
||||
@@ -149,21 +160,21 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
|
||||
: autostartDisplay(workspace.autostart_schedule)}
|
||||
</Link>
|
||||
{canUpdateWorkspace && canEditDeadline(workspace) && (
|
||||
<span className={styles.scheduleControls}>
|
||||
<span css={styles.scheduleControls}>
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
disabled={!deadlineMinusEnabled}
|
||||
size="small"
|
||||
title="Subtract hours from deadline"
|
||||
className={styles.scheduleButton}
|
||||
css={styles.scheduleButton}
|
||||
>
|
||||
<RemoveIcon />
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
id="schedule-sub"
|
||||
classes={{ paper: styles.timePopoverPaper }}
|
||||
classes={{ paper: paperStyles }}
|
||||
horizontal="right"
|
||||
>
|
||||
<DecreaseTimeContent
|
||||
@@ -178,14 +189,14 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
|
||||
disabled={!deadlinePlusEnabled}
|
||||
size="small"
|
||||
title="Add hours to deadline"
|
||||
className={styles.scheduleButton}
|
||||
css={styles.scheduleButton}
|
||||
>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
id="schedule-add"
|
||||
classes={{ paper: styles.timePopoverPaper }}
|
||||
classes={{ paper: paperStyles }}
|
||||
horizontal="right"
|
||||
>
|
||||
<AddTimeContent
|
||||
@@ -202,7 +213,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
|
||||
)}
|
||||
{workspace.latest_build.daily_cost > 0 && (
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
css={styles.statsItem}
|
||||
label={Language.costLabel}
|
||||
value={`${workspace.latest_build.daily_cost} ${
|
||||
quotaBudget ? `/ ${quotaBudget}` : ""
|
||||
@@ -212,7 +223,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
|
||||
{templatePoliciesEnabled && (
|
||||
<Stack direction="row" spacing={0.5}>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
css={styles.statsItem}
|
||||
label={Language.updatePolicy}
|
||||
value={upperFirst(
|
||||
workspaceUpdatePolicy(workspace, canChangeVersions),
|
||||
@@ -239,18 +250,17 @@ const AddTimeContent = (props: {
|
||||
maxDeadlineIncrease: number;
|
||||
onDeadlinePlus: (value: number) => void;
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
const popover = usePopover();
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={styles.timePopoverTitle}>Add hours to deadline</span>
|
||||
<span className={styles.timePopoverDescription}>
|
||||
<span css={styles.timePopoverTitle}>Add hours to deadline</span>
|
||||
<span css={styles.timePopoverDescription}>
|
||||
Delay the shutdown of this workspace for a few more hours. This is only
|
||||
applied once.
|
||||
</span>
|
||||
<form
|
||||
className={styles.timePopoverForm}
|
||||
css={styles.timePopoverForm}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
@@ -264,9 +274,9 @@ const AddTimeContent = (props: {
|
||||
type="number"
|
||||
size="small"
|
||||
fullWidth
|
||||
className={styles.timePopoverField}
|
||||
css={styles.timePopoverField}
|
||||
InputProps={{
|
||||
className: styles.timePopoverFieldInput,
|
||||
className: timePopoverFieldInputStyles,
|
||||
}}
|
||||
inputProps={{
|
||||
min: 0,
|
||||
@@ -276,7 +286,7 @@ const AddTimeContent = (props: {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button className={styles.timePopoverButton} type="submit">
|
||||
<Button css={styles.timePopoverButton} type="submit">
|
||||
Apply
|
||||
</Button>
|
||||
</form>
|
||||
@@ -288,20 +298,17 @@ export const DecreaseTimeContent = (props: {
|
||||
onDeadlineMinus: (hours: number) => void;
|
||||
maxDeadlineDecrease: number;
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
const popover = usePopover();
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={styles.timePopoverTitle}>
|
||||
Subtract hours to deadline
|
||||
</span>
|
||||
<span className={styles.timePopoverDescription}>
|
||||
<span css={styles.timePopoverTitle}>Subtract hours to deadline</span>
|
||||
<span css={styles.timePopoverDescription}>
|
||||
Anticipate the shutdown of this workspace for a few more hours. This is
|
||||
only applied once.
|
||||
</span>
|
||||
<form
|
||||
className={styles.timePopoverForm}
|
||||
css={styles.timePopoverForm}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
@@ -315,9 +322,9 @@ export const DecreaseTimeContent = (props: {
|
||||
type="number"
|
||||
size="small"
|
||||
fullWidth
|
||||
className={styles.timePopoverField}
|
||||
css={styles.timePopoverField}
|
||||
InputProps={{
|
||||
className: styles.timePopoverFieldInput,
|
||||
className: timePopoverFieldInputStyles,
|
||||
}}
|
||||
inputProps={{
|
||||
min: 0,
|
||||
@@ -327,7 +334,7 @@ export const DecreaseTimeContent = (props: {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button className={styles.timePopoverButton} type="submit">
|
||||
<Button css={styles.timePopoverButton} type="submit">
|
||||
Apply
|
||||
</Button>
|
||||
</form>
|
||||
@@ -353,8 +360,14 @@ const getScheduleLabel = (workspace: Workspace) => {
|
||||
return isWorkspaceOn(workspace) ? "Stops at" : "Starts at";
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
stats: {
|
||||
const timePopoverFieldInputStyles = css`
|
||||
font-size: 14px;
|
||||
padding: 0px;
|
||||
border-radius: 4px;
|
||||
`;
|
||||
|
||||
const styles = {
|
||||
stats: (theme) => ({
|
||||
padding: 0,
|
||||
border: 0,
|
||||
gap: theme.spacing(6),
|
||||
@@ -367,7 +380,7 @@ const useStyles = makeStyles((theme) => ({
|
||||
alignItems: "flex-start",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
statsItem: {
|
||||
flexDirection: "column",
|
||||
@@ -380,19 +393,19 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
},
|
||||
|
||||
scheduleValue: {
|
||||
scheduleValue: (theme) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1.5),
|
||||
},
|
||||
}),
|
||||
|
||||
scheduleControls: {
|
||||
scheduleControls: (theme) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(0.5),
|
||||
},
|
||||
}),
|
||||
|
||||
scheduleButton: {
|
||||
scheduleButton: (theme) => ({
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: 4,
|
||||
width: 20,
|
||||
@@ -402,47 +415,31 @@ const useStyles = makeStyles((theme) => ({
|
||||
width: theme.spacing(1.5),
|
||||
height: theme.spacing(1.5),
|
||||
},
|
||||
},
|
||||
|
||||
timePopoverPaper: {
|
||||
padding: theme.spacing(3),
|
||||
maxWidth: theme.spacing(36),
|
||||
marginTop: theme.spacing(1),
|
||||
borderRadius: 4,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
}),
|
||||
|
||||
timePopoverTitle: {
|
||||
fontWeight: 600,
|
||||
},
|
||||
|
||||
timePopoverDescription: {
|
||||
timePopoverDescription: (theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}),
|
||||
|
||||
timePopoverForm: {
|
||||
timePopoverForm: (theme) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1),
|
||||
padding: theme.spacing(1, 0),
|
||||
},
|
||||
}),
|
||||
|
||||
timePopoverField: {
|
||||
margin: 0,
|
||||
},
|
||||
|
||||
timePopoverFieldInput: {
|
||||
fontSize: 14,
|
||||
padding: theme.spacing(0),
|
||||
borderRadius: 4,
|
||||
},
|
||||
|
||||
timePopoverButton: {
|
||||
timePopoverButton: (theme) => ({
|
||||
borderRadius: 4,
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
flexShrink: 0,
|
||||
},
|
||||
}));
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
@@ -1254,16 +1254,30 @@ export const MockUserAgent = {
|
||||
os: "Windows 10",
|
||||
};
|
||||
|
||||
export const MockAuthMethods: TypesGen.AuthMethods = {
|
||||
export const MockAuthMethodsPasswordOnly: TypesGen.AuthMethods = {
|
||||
password: { enabled: true },
|
||||
github: { enabled: false },
|
||||
oidc: { enabled: false, signInText: "", iconUrl: "" },
|
||||
};
|
||||
|
||||
export const MockAuthMethodsWithPasswordType: TypesGen.AuthMethods = {
|
||||
...MockAuthMethods,
|
||||
export const MockAuthMethodsExternal: TypesGen.AuthMethods = {
|
||||
password: { enabled: false },
|
||||
github: { enabled: true },
|
||||
oidc: { enabled: true, signInText: "", iconUrl: "" },
|
||||
oidc: {
|
||||
enabled: true,
|
||||
signInText: "Google",
|
||||
iconUrl: "/icon/google.svg",
|
||||
},
|
||||
};
|
||||
|
||||
export const MockAuthMethodsAll: TypesGen.AuthMethods = {
|
||||
password: { enabled: true },
|
||||
github: { enabled: true },
|
||||
oidc: {
|
||||
enabled: true,
|
||||
signInText: "Google",
|
||||
iconUrl: "/icon/google.svg",
|
||||
},
|
||||
};
|
||||
|
||||
export const MockGitSSHKey: TypesGen.GitSSHKey = {
|
||||
|
||||
@@ -182,7 +182,7 @@ export const handlers = [
|
||||
return res(ctx.status(200), ctx.json(M.MockAPIKey));
|
||||
}),
|
||||
rest.get("/api/v2/users/authmethods", async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(M.MockAuthMethods));
|
||||
return res(ctx.status(200), ctx.json(M.MockAuthMethodsPasswordOnly));
|
||||
}),
|
||||
rest.get("/api/v2/users/roles", async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(M.MockSiteRoles));
|
||||
|
||||
Reference in New Issue
Block a user