refactor: update Prettier printWidth to 100 (#2684)

This commit is contained in:
Abhineet Jain
2022-06-27 10:53:44 -04:00
committed by GitHub
parent 09722ae1ef
commit 82938944e7
108 changed files with 786 additions and 232 deletions

View File

@ -1,5 +1,5 @@
{
"printWidth": 120,
"printWidth": 100,
"semi": false,
"trailingComma": "all",
"overrides": [

View File

@ -1,4 +1,6 @@
declare module "can-ndjson-stream" {
function ndjsonStream<TValueType>(body: ReadableStream<Uint8Array> | null): Promise<ReadableStream<TValueType>>
function ndjsonStream<TValueType>(
body: ReadableStream<Uint8Array> | null,
): Promise<ReadableStream<TValueType>>
export default ndjsonStream
}

View File

@ -17,7 +17,10 @@ const config: PlaywrightTestConfig = {
// https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests
webServer: {
// Run the coder daemon directly.
command: `go run -tags embed ${path.join(__dirname, "../../cmd/coder/main.go")} server --in-memory`,
command: `go run -tags embed ${path.join(
__dirname,
"../../cmd/coder/main.go",
)} server --in-memory`,
port: 3000,
timeout: 120 * 10000,
reuseExistingServer: false,

View File

@ -22,7 +22,10 @@ export const timeout = (timeoutInMilliseconds: number): Promise<void> => {
* @param timeToWaitInMilliseconds The total time to wait for the condition to be `true`.
* @returns
*/
export const waitFor = async (f: () => Promise<boolean>, timeToWaitInMilliseconds = 30000): Promise<void> => {
export const waitFor = async (
f: () => Promise<boolean>,
timeToWaitInMilliseconds = 30000,
): Promise<void> => {
let elapsedTime = 0
const timeToWaitPerIteration = 1000
@ -58,7 +61,10 @@ interface WaitForClientSideNavigationOpts {
* waitForNavigation waits for load events on the DOM (ex: after a page load
* from the server).
*/
export const waitForClientSideNavigation = async (page: Page, opts: WaitForClientSideNavigationOpts): Promise<void> => {
export const waitForClientSideNavigation = async (
page: Page,
opts: WaitForClientSideNavigationOpts,
): Promise<void> => {
console.info(`--- waitForClientSideNavigation: start`)
await Promise.all([

View File

@ -17,7 +17,11 @@
<meta property="og:type" content="website" />
<meta property="csp-nonce" content="{{ .CSP.Nonce }}" />
<meta property="csrf-token" content="{{ .CSRF.Token }}" />
<meta id="api-response" data-statuscode="{{ .APIResponse.StatusCode }}" data-message="{{ .APIResponse.Message }}" />
<meta
id="api-response"
data-statuscode="{{ .APIResponse.StatusCode }}"
data-message="{{ .APIResponse.Message }}"
/>
<link rel="mask-icon" href="/static/favicon.svg" color="#000000" crossorigin="use-credentials" />
<link rel="alternate icon" type="image/png" href="/favicon.png" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />

View File

@ -46,7 +46,9 @@ CONSOLE_FAIL_TYPES.forEach((logType: string) => {
const consoleAsAny = global.console as any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
consoleAsAny[logType] = (format: string, ...args: any[]): void => {
throw new Error(`Failing due to console.${logType} while running test!\n\n${util.format(format, ...args)}`)
throw new Error(
`Failing due to console.${logType} while running test!\n\n${util.format(format, ...args)}`,
)
}
})

View File

@ -19,7 +19,9 @@ import { WorkspaceBuildPage } from "./pages/WorkspaceBuildPage/WorkspaceBuildPag
import { WorkspacePage } from "./pages/WorkspacePage/WorkspacePage"
import { WorkspaceSchedulePage } from "./pages/WorkspaceSchedulePage/WorkspaceSchedulePage"
const WorkspaceAppErrorPage = lazy(() => import("./pages/WorkspaceAppErrorPage/WorkspaceAppErrorPage"))
const WorkspaceAppErrorPage = lazy(
() => import("./pages/WorkspaceAppErrorPage/WorkspaceAppErrorPage"),
)
const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage"))
const WorkspacesPage = lazy(() => import("./pages/WorkspacesPage/WorkspacesPage"))
const CreateWorkspacePage = lazy(() => import("./pages/CreateWorkspacePage/CreateWorkspacePage"))

View File

@ -22,15 +22,22 @@ export const provisioners: TypesGen.ProvisionerDaemon[] = [
},
]
export const login = async (email: string, password: string): Promise<TypesGen.LoginWithPasswordResponse> => {
export const login = async (
email: string,
password: string,
): Promise<TypesGen.LoginWithPasswordResponse> => {
const payload = JSON.stringify({
email,
password,
})
const response = await axios.post<TypesGen.LoginWithPasswordResponse>("/api/v2/users/login", payload, {
headers: { ...CONTENT_TYPE_JSON },
})
const response = await axios.post<TypesGen.LoginWithPasswordResponse>(
"/api/v2/users/login",
payload,
{
headers: { ...CONTENT_TYPE_JSON },
},
)
return response.data
}
@ -53,7 +60,10 @@ export const checkUserPermissions = async (
userId: string,
params: TypesGen.UserAuthorizationRequest,
): Promise<TypesGen.UserAuthorizationResponse> => {
const response = await axios.post<TypesGen.UserAuthorizationResponse>(`/api/v2/users/${userId}/authorization`, params)
const response = await axios.post<TypesGen.UserAuthorizationResponse>(
`/api/v2/users/${userId}/authorization`,
params,
)
return response.data
}
@ -83,27 +93,44 @@ export const getTemplate = async (templateId: string): Promise<TypesGen.Template
}
export const getTemplates = async (organizationId: string): Promise<TypesGen.Template[]> => {
const response = await axios.get<TypesGen.Template[]>(`/api/v2/organizations/${organizationId}/templates`)
const response = await axios.get<TypesGen.Template[]>(
`/api/v2/organizations/${organizationId}/templates`,
)
return response.data
}
export const getTemplateByName = async (organizationId: string, name: string): Promise<TypesGen.Template> => {
const response = await axios.get<TypesGen.Template>(`/api/v2/organizations/${organizationId}/templates/${name}`)
export const getTemplateByName = async (
organizationId: string,
name: string,
): Promise<TypesGen.Template> => {
const response = await axios.get<TypesGen.Template>(
`/api/v2/organizations/${organizationId}/templates/${name}`,
)
return response.data
}
export const getTemplateVersion = async (versionId: string): Promise<TypesGen.TemplateVersion> => {
const response = await axios.get<TypesGen.TemplateVersion>(`/api/v2/templateversions/${versionId}`)
const response = await axios.get<TypesGen.TemplateVersion>(
`/api/v2/templateversions/${versionId}`,
)
return response.data
}
export const getTemplateVersionSchema = async (versionId: string): Promise<TypesGen.ParameterSchema[]> => {
const response = await axios.get<TypesGen.ParameterSchema[]>(`/api/v2/templateversions/${versionId}/schema`)
export const getTemplateVersionSchema = async (
versionId: string,
): Promise<TypesGen.ParameterSchema[]> => {
const response = await axios.get<TypesGen.ParameterSchema[]>(
`/api/v2/templateversions/${versionId}/schema`,
)
return response.data
}
export const getTemplateVersionResources = async (versionId: string): Promise<TypesGen.WorkspaceResource[]> => {
const response = await axios.get<TypesGen.WorkspaceResource[]>(`/api/v2/templateversions/${versionId}/resources`)
export const getTemplateVersionResources = async (
versionId: string,
): Promise<TypesGen.WorkspaceResource[]> => {
const response = await axios.get<TypesGen.WorkspaceResource[]>(
`/api/v2/templateversions/${versionId}/resources`,
)
return response.data
}
@ -111,7 +138,9 @@ export const getWorkspace = async (
workspaceId: string,
params?: TypesGen.WorkspaceOptions,
): Promise<TypesGen.Workspace> => {
const response = await axios.get<TypesGen.Workspace>(`/api/v2/workspaces/${workspaceId}`, { params })
const response = await axios.get<TypesGen.Workspace>(`/api/v2/workspaces/${workspaceId}`, {
params,
})
return response.data
}
@ -128,7 +157,9 @@ export const getWorkspacesURL = (filter?: TypesGen.WorkspaceFilter): string => {
return searchString ? `${basePath}?${searchString}` : basePath
}
export const getWorkspaces = async (filter?: TypesGen.WorkspaceFilter): Promise<TypesGen.Workspace[]> => {
export const getWorkspaces = async (
filter?: TypesGen.WorkspaceFilter,
): Promise<TypesGen.Workspace[]> => {
const url = getWorkspacesURL(filter)
const response = await axios.get<TypesGen.Workspace[]>(url)
return response.data
@ -139,13 +170,18 @@ export const getWorkspaceByOwnerAndName = async (
workspaceName: string,
params?: TypesGen.WorkspaceOptions,
): Promise<TypesGen.Workspace> => {
const response = await axios.get<TypesGen.Workspace>(`/api/v2/users/${username}/workspace/${workspaceName}`, {
params,
})
const response = await axios.get<TypesGen.Workspace>(
`/api/v2/users/${username}/workspace/${workspaceName}`,
{
params,
},
)
return response.data
}
export const getWorkspaceResources = async (workspaceBuildID: string): Promise<TypesGen.WorkspaceResource[]> => {
export const getWorkspaceResources = async (
workspaceBuildID: string,
): Promise<TypesGen.WorkspaceResource[]> => {
const response = await axios.get<TypesGen.WorkspaceResource[]>(
`/api/v2/workspacebuilds/${workspaceBuildID}/resources`,
)
@ -167,7 +203,9 @@ export const startWorkspace = postWorkspaceBuild("start")
export const stopWorkspace = postWorkspaceBuild("stop")
export const deleteWorkspace = postWorkspaceBuild("delete")
export const cancelWorkspaceBuild = async (workspaceBuildId: TypesGen.WorkspaceBuild["id"]): Promise<Types.Message> => {
export const cancelWorkspaceBuild = async (
workspaceBuildId: TypesGen.WorkspaceBuild["id"],
): Promise<Types.Message> => {
const response = await axios.patch(`/api/v2/workspacebuilds/${workspaceBuildId}/cancel`)
return response.data
}
@ -181,7 +219,10 @@ export const createWorkspace = async (
organizationId: string,
workspace: TypesGen.CreateWorkspaceRequest,
): Promise<TypesGen.Workspace> => {
const response = await axios.post<TypesGen.Workspace>(`/api/v2/organizations/${organizationId}/workspaces`, workspace)
const response = await axios.post<TypesGen.Workspace>(
`/api/v2/organizations/${organizationId}/workspaces`,
workspace,
)
return response.data
}
@ -263,8 +304,12 @@ export const regenerateUserSSHKey = async (userId = "me"): Promise<TypesGen.GitS
return response.data
}
export const getWorkspaceBuilds = async (workspaceId: string): Promise<TypesGen.WorkspaceBuild[]> => {
const response = await axios.get<TypesGen.WorkspaceBuild[]>(`/api/v2/workspaces/${workspaceId}/builds`)
export const getWorkspaceBuilds = async (
workspaceId: string,
): Promise<TypesGen.WorkspaceBuild[]> => {
const response = await axios.get<TypesGen.WorkspaceBuild[]>(
`/api/v2/workspaces/${workspaceId}/builds`,
)
return response.data
}
@ -279,7 +324,10 @@ export const getWorkspaceBuildByNumber = async (
return response.data
}
export const getWorkspaceBuildLogs = async (buildname: string, before: Date): Promise<TypesGen.ProvisionerJobLog[]> => {
export const getWorkspaceBuildLogs = async (
buildname: string,
before: Date,
): Promise<TypesGen.ProvisionerJobLog[]> => {
const response = await axios.get<TypesGen.ProvisionerJobLog[]>(
`/api/v2/workspacebuilds/${buildname}/logs?before=${before.getTime()}`,
)

View File

@ -27,7 +27,8 @@ export const isApiError = (err: any): err is ApiError => {
const response = err.response?.data
return (
typeof response.message === "string" && (typeof response.errors === "undefined" || Array.isArray(response.errors))
typeof response.message === "string" &&
(typeof response.errors === "undefined" || Array.isArray(response.errors))
)
}
@ -40,7 +41,8 @@ export const isApiError = (err: any): err is ApiError => {
* @param error ApiError
* @returns true if the ApiError contains error messages for specific form fields.
*/
export const hasApiFieldErrors = (error: ApiError): boolean => Array.isArray(error.response.data.validations)
export const hasApiFieldErrors = (error: ApiError): boolean =>
Array.isArray(error.response.data.validations)
export const mapApiErrorToFieldErrors = (apiErrorResponse: ApiErrorResponse): FieldErrors => {
const result: FieldErrors = {}
@ -60,5 +62,12 @@ export const mapApiErrorToFieldErrors = (apiErrorResponse: ApiErrorResponse): Fi
* @param defaultMessage
* @returns error's message if ApiError or Error, else defaultMessage
*/
export const getErrorMessage = (error: Error | ApiError | unknown, defaultMessage: string): string =>
isApiError(error) ? error.response.data.message : error instanceof Error ? error.message : defaultMessage
export const getErrorMessage = (
error: Error | ApiError | unknown,
defaultMessage: string,
): string =>
isApiError(error)
? error.response.data.message
: error instanceof Error
? error.message
: defaultMessage

View File

@ -512,7 +512,13 @@ export type ParameterSourceScheme = "data" | "none"
export type ParameterTypeSystem = "hcl" | "none"
// From codersdk/provisionerdaemons.go:47:6
export type ProvisionerJobStatus = "canceled" | "canceling" | "failed" | "pending" | "running" | "succeeded"
export type ProvisionerJobStatus =
| "canceled"
| "canceling"
| "failed"
| "pending"
| "running"
| "succeeded"
// From codersdk/organizations.go:14:6
export type ProvisionerStorageMethod = "file"

View File

@ -22,7 +22,11 @@ export const AvatarData: FC<AvatarDataProps> = ({ title, subtitle, link }) => {
</Avatar>
{link ? (
<Link component={RouterLink} to={link} className={combineClasses([styles.info, styles.link])}>
<Link
component={RouterLink}
to={link}
className={combineClasses([styles.info, styles.link])}
>
<b>{title}</b>
<span>{subtitle}</span>
</Link>

View File

@ -11,7 +11,12 @@ export default {
const Template: Story<BorderedMenuProps> = (args: BorderedMenuProps) => (
<BorderedMenu {...args}>
<BorderedMenuRow title="Item 1" description="Here's a description" Icon={BuildingIcon} path="/" />
<BorderedMenuRow
title="Item 1"
description="Here's a description"
Icon={BuildingIcon}
path="/"
/>
<BorderedMenuRow
active
title="Item 2"

View File

@ -12,7 +12,11 @@ export const BorderedMenu: FC<BorderedMenuProps> = ({ children, variant, ...rest
const styles = useStyles()
return (
<Popover classes={{ root: styles.root, paper: styles.paperRoot }} data-variant={variant} {...rest}>
<Popover
classes={{ root: styles.root, paper: styles.paperRoot }}
data-variant={variant}
{...rest}
>
{children}
</Popover>
)

View File

@ -69,7 +69,9 @@ export const BuildsTable: FC<BuildsTableProps> = ({ builds, className }) => {
>
<TableCellLink to={buildPageLink}>{build.transition}</TableCellLink>
<TableCellLink to={buildPageLink}>
<span style={{ color: theme.palette.text.secondary }}>{displayWorkspaceBuildDuration(build)}</span>
<span style={{ color: theme.palette.text.secondary }}>
{displayWorkspaceBuildDuration(build)}
</span>
</TableCellLink>
<TableCellLink to={buildPageLink}>
<span style={{ color: theme.palette.text.secondary }}>

View File

@ -37,7 +37,12 @@ const useStyles = makeStyles((theme) => ({
padding: theme.spacing(0.5),
},
code: {
padding: `${theme.spacing(0.5)}px ${theme.spacing(0.75)}px ${theme.spacing(0.5)}px ${theme.spacing(2)}px`,
padding: `
${theme.spacing(0.5)}px
${theme.spacing(0.75)}px
${theme.spacing(0.5)}px
${theme.spacing(2)}px
`,
whiteSpace: "nowrap",
width: "100%",
overflowX: "auto",

View File

@ -25,7 +25,8 @@ const CONFIRM_DIALOG_DEFAULTS: Record<ConfirmDialogType, ConfirmDialogTypeConfig
},
}
export interface ConfirmDialogProps extends Omit<DialogActionButtonsProps, "color" | "confirmDialog" | "onCancel"> {
export interface ConfirmDialogProps
extends Omit<DialogActionButtonsProps, "color" | "confirmDialog" | "onCancel"> {
readonly description?: React.ReactNode
/**
* hideCancel hides the cancel button when set true, and shows the cancel

View File

@ -63,7 +63,11 @@ export const CopyButton: React.FC<CopyButtonProps> = ({
onClick={copyToClipboard}
size="small"
>
{isCopied ? <Check className={styles.fileCopyIcon} /> : <FileCopyIcon className={styles.fileCopyIcon} />}
{isCopied ? (
<Check className={styles.fileCopyIcon} />
) : (
<FileCopyIcon className={styles.fileCopyIcon} />
)}
{ctaCopy && <div className={styles.buttonCopy}>{ctaCopy}</div>}
</IconButton>
</div>

View File

@ -7,7 +7,9 @@ export default {
component: CreateUserForm,
}
const Template: Story<CreateUserFormProps> = (args: CreateUserFormProps) => <CreateUserForm {...args} />
const Template: Story<CreateUserFormProps> = (args: CreateUserFormProps) => (
<CreateUserForm {...args} />
)
export const Ready = Template.bind({})
Ready.args = {

View File

@ -43,16 +43,18 @@ export const CreateUserForm: FC<CreateUserFormProps> = ({
error,
myOrgId,
}) => {
const form: FormikContextType<TypesGen.CreateUserRequest> = useFormik<TypesGen.CreateUserRequest>({
initialValues: {
email: "",
password: "",
username: "",
organization_id: myOrgId,
const form: FormikContextType<TypesGen.CreateUserRequest> = useFormik<TypesGen.CreateUserRequest>(
{
initialValues: {
email: "",
password: "",
username: "",
organization_id: myOrgId,
},
validationSchema,
onSubmit,
},
validationSchema,
onSubmit,
})
)
const getFieldHelpers = getFormHelpers<TypesGen.CreateUserRequest>(form, formErrors)
return (

View File

@ -162,7 +162,8 @@ const useButtonStyles = makeStyles((theme) => ({
},
},
confirmDialogCancelButton: (props: StyleProps) => {
const color = props.type === "info" ? theme.palette.primary.contrastText : theme.palette.error.contrastText
const color =
props.type === "info" ? theme.palette.primary.contrastText : theme.palette.error.contrastText
return {
background: fade(color, 0.15),
color,
@ -299,7 +300,10 @@ const useButtonStyles = makeStyles((theme) => ({
},
}))
export type DialogSearchProps = Omit<OutlinedInputProps, "className" | "fullWidth" | "labelWidth" | "startAdornment">
export type DialogSearchProps = Omit<
OutlinedInputProps,
"className" | "fullWidth" | "labelWidth" | "startAdornment"
>
/**
* Formats a search bar right below the title of a Dialog. Passes all props

View File

@ -6,7 +6,9 @@ export default {
component: EnterpriseSnackbar,
}
const Template: Story<EnterpriseSnackbarProps> = (args: EnterpriseSnackbarProps) => <EnterpriseSnackbar {...args} />
const Template: Story<EnterpriseSnackbarProps> = (args: EnterpriseSnackbarProps) => (
<EnterpriseSnackbar {...args} />
)
export const Error = Template.bind({})
Error.args = {

View File

@ -80,7 +80,12 @@ const useStyles = makeStyles((theme) => ({
border: `1px solid ${theme.palette.divider}`,
borderLeft: `4px solid ${theme.palette.primary.main}`,
borderRadius: theme.shape.borderRadius,
padding: `${theme.spacing(1)}px ${theme.spacing(3)}px ${theme.spacing(1)}px ${theme.spacing(2)}px`,
padding: `
${theme.spacing(1)}px
${theme.spacing(3)}px
${theme.spacing(1)}px
${theme.spacing(2)}px
`,
boxShadow: theme.shadows[6],
alignItems: "inherit",
backgroundColor: theme.palette.background.paper,

View File

@ -15,7 +15,11 @@ export interface ErrorSummaryProps {
export const ErrorSummary: FC<ErrorSummaryProps> = ({ error, retry }) => (
<Stack>
{!(error instanceof Error) ? <div>{Language.unknownErrorMessage}</div> : <div>{error.toString()}</div>}
{!(error instanceof Error) ? (
<div>{Language.unknownErrorMessage}</div>
) : (
<div>{error.toString()}</div>
)}
{retry && (
<div>

View File

@ -26,7 +26,12 @@ export const Footer: React.FC<FooterProps> = ({ buildInfo }) => {
<div className={styles.copyRight}>{Language.copyrightText}</div>
{buildInfo && (
<div className={styles.buildInfo}>
<Link className={styles.link} variant="caption" target="_blank" href={buildInfo.external_url}>
<Link
className={styles.link}
variant="caption"
target="_blank"
href={buildInfo.external_url}
>
<AccountTreeIcon className={styles.icon} /> {Language.buildInfoText(buildInfo)}
</Link>
&nbsp;|&nbsp;

View File

@ -15,7 +15,10 @@ export interface FormDropdownFieldProps<T> extends FormTextFieldProps<T> {
items: FormDropdownItem[]
}
export const FormDropdownField = <T,>({ items, ...props }: FormDropdownFieldProps<T>): ReactElement => {
export const FormDropdownField = <T,>({
items,
...props
}: FormDropdownFieldProps<T>): ReactElement => {
const styles = useStyles()
return (
<FormTextField select {...props}>

View File

@ -28,14 +28,24 @@ const useStyles = makeStyles((theme) => ({
},
}))
export const FormFooter: FC<FormFooterProps> = ({ onCancel, isLoading, submitLabel = Language.defaultSubmitLabel }) => {
export const FormFooter: FC<FormFooterProps> = ({
onCancel,
isLoading,
submitLabel = Language.defaultSubmitLabel,
}) => {
const styles = useStyles()
return (
<div className={styles.footer}>
<Button type="button" className={styles.button} onClick={onCancel} variant="outlined">
{Language.cancelLabel}
</Button>
<LoadingButton loading={isLoading} className={styles.button} variant="contained" color="primary" type="submit">
<LoadingButton
loading={isLoading}
className={styles.button}
variant="contained"
color="primary"
type="submit"
>
{submitLabel}
</LoadingButton>
</div>

View File

@ -11,7 +11,9 @@ namespace Helpers {
export const requiredValidationMsg = "required"
export const Component: FC<Omit<FormTextFieldProps<FormValues>, "form" | "formFieldName">> = (props) => {
export const Component: FC<Omit<FormTextFieldProps<FormValues>, "form" | "formFieldName">> = (
props,
) => {
const form = useFormik<FormValues>({
initialValues: {
name: "",

View File

@ -82,7 +82,8 @@ export const GlobalSnackbar: React.FC = () => {
<Typography variant="body1" className={styles.messageTitle}>
{notification.msg}
</Typography>
{notification.additionalMsgs && notification.additionalMsgs.map(renderAdditionalMessage)}
{notification.additionalMsgs &&
notification.additionalMsgs.map(renderAdditionalMessage)}
</div>
</div>
}

View File

@ -24,7 +24,9 @@ export const isNotificationText = (msg: AdditionalMessage): msg is string => {
return !Array.isArray(msg) && typeof msg === "string"
}
export const isNotificationTextPrefixed = (msg: AdditionalMessage | null): msg is NotificationTextPrefixed => {
export const isNotificationTextPrefixed = (
msg: AdditionalMessage | null,
): msg is NotificationTextPrefixed => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return typeof (msg as NotificationTextPrefixed)?.prefix !== "undefined"
}
@ -45,7 +47,11 @@ export const SnackbarEventType = "coder:notification"
// Notification Functions
///////////////////////////////////////////////////////////////////////////////
function dispatchNotificationEvent(msgType: MsgType, msg: string, additionalMsgs?: AdditionalMessage[]) {
function dispatchNotificationEvent(
msgType: MsgType,
msg: string,
additionalMsgs?: AdditionalMessage[],
) {
dispatchCustomEvent<NotificationMsg>(SnackbarEventType, {
msgType,
msg,

View File

@ -2,6 +2,11 @@ import SvgIcon from "@material-ui/core/SvgIcon"
export const CloseIcon: typeof SvgIcon = (props) => (
<SvgIcon {...props} viewBox="0 0 31 31">
<path d="M29.5 1.5l-28 28M29.5 29.5l-28-28" stroke="currentcolor" strokeMiterlimit="10" strokeLinecap="square" />
<path
d="M29.5 1.5l-28 28M29.5 29.5l-28-28"
stroke="currentcolor"
strokeMiterlimit="10"
strokeLinecap="square"
/>
</SvgIcon>
)

View File

@ -1,7 +1,13 @@
import * as React from "react"
export const Logo = (props: React.SVGProps<SVGSVGElement>): JSX.Element => (
<svg aria-labelledby="title" viewBox="0 0 341 75" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<svg
aria-labelledby="title"
viewBox="0 0 341 75"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title id="title" lang="en">
Coder logo
</title>

View File

@ -14,7 +14,11 @@ export interface LoadingButtonProps extends ButtonProps {
* In Material-UI 5+ - this is built-in, but since we're on an earlier version,
* we have to roll our own.
*/
export const LoadingButton: React.FC<LoadingButtonProps> = ({ loading = false, children, ...rest }) => {
export const LoadingButton: React.FC<LoadingButtonProps> = ({
loading = false,
children,
...rest
}) => {
const styles = useStyles()
const hidden = loading ? { opacity: 0 } : undefined

View File

@ -8,7 +8,9 @@ export default {
const Template: Story = (args) => (
<Margins {...args}>
<div style={{ width: "100%", background: "black" }}>Here is some content that will not get too wide!</div>
<div style={{ width: "100%", background: "black" }}>
Here is some content that will not get too wide!
</div>
</Margins>
)

View File

@ -32,7 +32,10 @@ export const NavbarView: React.FC<NavbarViewProps> = ({ user, onSignOut }) => {
</ListItem>
<ListItem button className={styles.item}>
<NavLink
className={combineClasses([styles.link, location.pathname.startsWith("/@") && "active"])}
className={combineClasses([
styles.link,
location.pathname.startsWith("/@") && "active",
])}
to="/workspaces"
>
{Language.workspaces}
@ -50,7 +53,9 @@ export const NavbarView: React.FC<NavbarViewProps> = ({ user, onSignOut }) => {
</ListItem>
</List>
<div className={styles.fullWidth} />
<div className={styles.fixed}>{user && <UserDropdown user={user} onSignOut={onSignOut} />}</div>
<div className={styles.fixed}>
{user && <UserDropdown user={user} onSignOut={onSignOut} />}
</div>
</nav>
)
}

View File

@ -7,7 +7,9 @@ export default {
component: ParameterInput,
}
const Template: Story<ParameterInputProps> = (args: ParameterInputProps) => <ParameterInput {...args} />
const Template: Story<ParameterInputProps> = (args: ParameterInputProps) => (
<ParameterInput {...args} />
)
const createParameterSchema = (partial: Partial<ParameterSchema>): ParameterSchema => {
return {

View File

@ -12,7 +12,10 @@ export const PasswordField: React.FC<PasswordFieldProps> = ({ variant = "outline
const styles = useStyles()
const [showPassword, setShowPassword] = useState<boolean>(false)
const handleVisibilityChange = useCallback(() => setShowPassword((showPassword) => !showPassword), [])
const handleVisibilityChange = useCallback(
() => setShowPassword((showPassword) => !showPassword),
[],
)
const VisibilityIcon = showPassword ? VisibilityOffOutlined : VisibilityOutlined
return (
@ -23,7 +26,11 @@ export const PasswordField: React.FC<PasswordFieldProps> = ({ variant = "outline
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton aria-label="toggle password visibility" onClick={handleVisibilityChange} size="small">
<IconButton
aria-label="toggle password visibility"
onClick={handleVisibilityChange}
size="small"
>
<VisibilityIcon className={styles.visibilityIcon} />
</IconButton>
</InputAdornment>

View File

@ -11,7 +11,9 @@ export default {
},
}
const Template: Story<ResetPasswordDialogProps> = (args: ResetPasswordDialogProps) => <ResetPasswordDialog {...args} />
const Template: Story<ResetPasswordDialogProps> = (args: ResetPasswordDialogProps) => (
<ResetPasswordDialog {...args} />
)
export const Example = Template.bind({})
Example.args = {

View File

@ -31,12 +31,20 @@ interface ResourcesProps {
canUpdateWorkspace: boolean
}
export const Resources: FC<ResourcesProps> = ({ resources, getResourcesError, workspace, canUpdateWorkspace }) => {
export const Resources: FC<ResourcesProps> = ({
resources,
getResourcesError,
workspace,
canUpdateWorkspace,
}) => {
const styles = useStyles()
const theme: Theme = useTheme()
return (
<WorkspaceSection title={Language.resources} contentsProps={{ className: styles.sectionContents }}>
<WorkspaceSection
title={Language.resources}
contentsProps={{ className: styles.sectionContents }}
>
{getResourcesError ? (
{ getResourcesError }
) : (

View File

@ -16,7 +16,13 @@ export interface RoleSelectProps {
open?: boolean
}
export const RoleSelect: FC<RoleSelectProps> = ({ roles, selectedRoles, loading, onChange, open }) => {
export const RoleSelect: FC<RoleSelectProps> = ({
roles,
selectedRoles,
loading,
onChange,
open,
}) => {
const styles = useStyles()
const value = selectedRoles.map((r) => r.name)
const renderValue = () => selectedRoles.map((r) => r.display_name).join(", ")

View File

@ -71,7 +71,13 @@ export const RuntimeErrorReport = ({ error, mappedStack }: ReportState): ReactEl
}
const formattedStackTrace = createFormattedStackTrace(error, mappedStack)
return <CodeBlock lines={formattedStackTrace} className={styles.codeBlock} ctas={createCtas(formattedStackTrace)} />
return (
<CodeBlock
lines={formattedStackTrace}
className={styles.codeBlock}
ctas={createCtas(formattedStackTrace)}
/>
)
}
const useStyles = makeStyles(() => ({

View File

@ -37,6 +37,9 @@ describe("RuntimeErrorState", () => {
it("should have an email link", () => {
// Then
const emailLink = screen.getByText(RuntimeErrorStateLanguage.link)
expect(emailLink.closest("a")).toHaveAttribute("href", expect.stringContaining("mailto:support@coder.com"))
expect(emailLink.closest("a")).toHaveAttribute(
"href",
expect.stringContaining("mailto:support@coder.com"),
)
})
})

View File

@ -81,7 +81,9 @@ export const RuntimeErrorState: React.FC<RuntimeErrorStateProps> = ({ error }) =
title={<ErrorStateTitle />}
description={
<ErrorStateDescription
emailBody={createFormattedStackTrace(reportState.error, reportState.mappedStack).join("\r\n")}
emailBody={createFormattedStackTrace(reportState.error, reportState.mappedStack).join(
"\r\n",
)}
/>
}
>

View File

@ -33,7 +33,11 @@ interface FilterFormValues {
export type FilterFormErrors = FormikErrors<FilterFormValues>
export const SearchBarWithFilter: React.FC<SearchBarWithFilterProps> = ({ filter, onFilter, presetFilters }) => {
export const SearchBarWithFilter: React.FC<SearchBarWithFilterProps> = ({
filter,
onFilter,
presetFilters,
}) => {
const styles = useStyles()
const form = useFormik<FilterFormValues>({
@ -67,7 +71,12 @@ export const SearchBarWithFilter: React.FC<SearchBarWithFilterProps> = ({ filter
return (
<Stack direction="row" spacing={0} className={styles.filterContainer}>
{presetFilters && presetFilters.length > 0 && (
<Button aria-controls="filter-menu" aria-haspopup="true" onClick={handleClick} className={styles.buttonRoot}>
<Button
aria-controls="filter-menu"
aria-haspopup="true"
onClick={handleClick}
className={styles.buttonRoot}
>
{Language.filterName} {anchorEl ? <CloseDropdown /> : <OpenDropdown />}
</Button>
)}

View File

@ -51,7 +51,13 @@ export const AccountForm: FC<AccountFormProps> = ({
<>
<form onSubmit={form.handleSubmit}>
<Stack>
<TextField disabled fullWidth label={Language.emailLabel} value={email} variant="outlined" />
<TextField
disabled
fullWidth
label={Language.emailLabel}
value={email}
variant="outlined"
/>
<TextField
{...getFieldHelpers("username")}
onChange={onChangeTrimmed(form)}

View File

@ -36,7 +36,10 @@ export const WithLoginError = Template.bind({})
WithLoginError.args = { ...SignedOut.args, authErrorMessage: "Email or password was invalid" }
export const WithAuthMethodsError = Template.bind({})
WithAuthMethodsError.args = { ...SignedOut.args, methodsErrorMessage: "Failed to fetch auth methods" }
WithAuthMethodsError.args = {
...SignedOut.args,
methodsErrorMessage: "Failed to fetch auth methods",
}
export const WithGithub = Template.bind({})
WithGithub.args = {

View File

@ -135,7 +135,9 @@ export const SignInForm: FC<SignInFormProps> = ({
variant="outlined"
/>
{authErrorMessage && <FormHelperText error>{authErrorMessage}</FormHelperText>}
{methodsErrorMessage && <FormHelperText error>{Language.methodsErrorMessage}</FormHelperText>}
{methodsErrorMessage && (
<FormHelperText error>{Language.methodsErrorMessage}</FormHelperText>
)}
<div className={styles.submitBtn}>
<LoadingButton loading={isLoading} fullWidth type="submit" variant="contained">
{isLoading ? "" : Language.passwordSignIn}
@ -153,7 +155,9 @@ export const SignInForm: FC<SignInFormProps> = ({
<div>
<Link
underline="none"
href={`/api/v2/users/oauth2/github/callback?redirect=${encodeURIComponent(redirectTo)}`}
href={`/api/v2/users/oauth2/github/callback?redirect=${encodeURIComponent(
redirectTo,
)}`}
>
<Button
startIcon={<GitHubIcon className={styles.buttonIcon} />}

View File

@ -75,7 +75,12 @@ export const SplitButton = <T,>({
return (
<>
<ButtonGroup aria-label="split button" color={color} ref={anchorRef} variant="contained">
<Button disabled={disabled} onClick={handleClick} startIcon={startIcon} style={{ textTransform }}>
<Button
disabled={disabled}
onClick={handleClick}
startIcon={startIcon}
style={{ textTransform }}
>
{displayedLabel}
</Button>
<Button

View File

@ -27,7 +27,13 @@ export interface StackProps {
alignItems?: CSSProperties["alignItems"]
}
export const Stack: FC<StackProps> = ({ children, className, direction = "column", spacing = 2, alignItems }) => {
export const Stack: FC<StackProps> = ({
children,
className,
direction = "column",
spacing = 2,
alignItems,
}) => {
const styles = useStyles({ spacing, direction, alignItems })
return <div className={combineClasses([styles.stack, className])}>{children}</div>

View File

@ -24,7 +24,13 @@ export const TabSidebar: FC<TabSidebarProps> = ({ menuItems }) => {
return (
<NavLink to={tab.path} key={tab.path} className={styles.link}>
{({ isActive }) => (
<ListItem button className={styles.menuItem} disableRipple focusRipple={false} component="li">
<ListItem
button
className={styles.menuItem}
disableRipple
focusRipple={false}
component="li"
>
<span className={combineClasses({ [styles.menuItemSpan]: true, active: isActive })}>
{hasChanges ? `${tab.label}*` : tab.label}
</span>

View File

@ -48,7 +48,13 @@ export interface TableProps<T> {
rowMenu?: (data: T) => ReactElement
}
export const Table = <T,>({ columns, data, emptyState, title, rowMenu }: TableProps<T>): ReactElement => {
export const Table = <T,>({
columns,
data,
emptyState,
title,
rowMenu,
}: TableProps<T>): ReactElement => {
const columnNames = columns.map(({ name }) => name)
const body = renderTableBody(data, columns, emptyState, rowMenu)
@ -76,9 +82,15 @@ const renderTableBody = <T,>(
const rows = data.map((item: T, index) => {
const cells = columns.map((column) => {
if (column.renderer) {
return <TableCell key={String(column.key)}>{column.renderer(item[column.key], item)}</TableCell>
return (
<TableCell key={String(column.key)}>
{column.renderer(item[column.key], item)}
</TableCell>
)
} else {
return <TableCell key={String(column.key)}>{String(item[column.key]).toString()}</TableCell>
return (
<TableCell key={String(column.key)}>{String(item[column.key]).toString()}</TableCell>
)
}
})
return (

View File

@ -25,10 +25,22 @@ export const TableRowMenu = <T,>({ data, menuItems }: TableRowMenuProps<T>): JSX
return (
<>
<IconButton size="small" aria-label="more" aria-controls="long-menu" aria-haspopup="true" onClick={handleClick}>
<IconButton
size="small"
aria-label="more"
aria-controls="long-menu"
aria-haspopup="true"
onClick={handleClick}
>
<MoreVertIcon />
</IconButton>
<Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}>
<Menu
id="simple-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
{menuItems.map((item) => (
<MenuItem
key={item.label}

View File

@ -31,7 +31,9 @@ export const TemplateStats: FC<TemplateStatsProps> = ({ template, activeVersion
<span className={styles.statsValue}>
{template.workspace_owner_count}{" "}
{template.workspace_owner_count === 1 ? Language.developerSingular : Language.developerPlural}
{template.workspace_owner_count === 1
? Language.developerSingular
: Language.developerPlural}
</span>
</div>
<div className={styles.statsDivider} />

View File

@ -25,7 +25,12 @@ export interface TerminalLinkProps {
* If no user name is provided "me" is used however it makes the link not
* shareable.
*/
export const TerminalLink: FC<TerminalLinkProps> = ({ agentName, userName = "me", workspaceName, className }) => {
export const TerminalLink: FC<TerminalLinkProps> = ({
agentName,
userName = "me",
workspaceName,
className,
}) => {
const styles = useStyles()
const href = `/@${userName}/${workspaceName}${agentName ? `.${agentName}` : ""}/terminal`

View File

@ -16,7 +16,9 @@ export default {
const Template: Story<HelpTooltipProps> = (args) => (
<HelpTooltip {...args}>
<HelpTooltipTitle>What is a template?</HelpTooltipTitle>
<HelpTooltipText>A template is a common configuration for your team&apos;s workspaces.</HelpTooltipText>
<HelpTooltipText>
A template is a common configuration for your team&apos;s workspaces.
</HelpTooltipText>
<HelpTooltipLinksGroup>
<HelpTooltipLink href="https://github.com/coder/coder/">Creating a template</HelpTooltipLink>
<HelpTooltipLink href="https://github.com/coder/coder/">Updating a template</HelpTooltipLink>

View File

@ -15,7 +15,9 @@ export interface HelpTooltipProps {
size?: Size
}
const HelpTooltipContext = createContext<{ open: boolean; onClose: () => void } | undefined>(undefined)
const HelpTooltipContext = createContext<{ open: boolean; onClose: () => void } | undefined>(
undefined,
)
const useHelpTooltip = () => {
const helpTooltipContext = useContext(HelpTooltipContext)
@ -77,7 +79,9 @@ export const HelpTooltip: React.FC<HelpTooltipProps> = ({ children, open, size =
},
}}
>
<HelpTooltipContext.Provider value={{ open: isOpen, onClose }}>{children}</HelpTooltipContext.Provider>
<HelpTooltipContext.Provider value={{ open: isOpen, onClose }}>
{children}
</HelpTooltipContext.Provider>
</Popover>
</>
)
@ -106,7 +110,11 @@ export const HelpTooltipLink: React.FC<{ href: string }> = ({ children, href })
)
}
export const HelpTooltipAction: React.FC<{ icon: Icon; onClick: () => void }> = ({ children, icon: Icon, onClick }) => {
export const HelpTooltipAction: React.FC<{ icon: Icon; onClick: () => void }> = ({
children,
icon: Icon,
onClick,
}) => {
const styles = useStyles()
const tooltip = useHelpTooltip()

View File

@ -8,7 +8,8 @@ import {
export const Language = {
resourceTooltipTitle: "What is a resource?",
resourceTooltipText: "A resource is an infrastructure object that is created when the workspace is provisioned.",
resourceTooltipText:
"A resource is an infrastructure object that is created when the workspace is provisioned.",
resourceTooltipLink: "Persistent and ephemeral resources",
}

View File

@ -35,7 +35,12 @@ const useStyles = makeStyles((theme) => ({
* UserCell is a single cell in an audit log table row that contains user-level
* information
*/
export const UserCell: FC<UserCellProps> = ({ Avatar, caption, primaryText, onPrimaryTextSelect }) => {
export const UserCell: FC<UserCellProps> = ({
Avatar,
caption,
primaryText,
onPrimaryTextSelect,
}) => {
const styles = useStyles()
return (

View File

@ -14,7 +14,10 @@ export interface UserDropdownProps {
onSignOut: () => void
}
export const UserDropdown: React.FC<UserDropdownProps> = ({ user, onSignOut }: UserDropdownProps) => {
export const UserDropdown: React.FC<UserDropdownProps> = ({
user,
onSignOut,
}: UserDropdownProps) => {
const styles = useStyles()
const [anchorEl, setAnchorEl] = useState<HTMLElement | undefined>()
@ -27,7 +30,11 @@ export const UserDropdown: React.FC<UserDropdownProps> = ({ user, onSignOut }: U
return (
<>
<MenuItem className={styles.menuItem} onClick={handleDropdownClick} data-testid="user-dropdown-trigger">
<MenuItem
className={styles.menuItem}
onClick={handleDropdownClick}
data-testid="user-dropdown-trigger"
>
<div className={styles.inner}>
<Badge overlap="circle">
<UserAvatar username={user.username} />

View File

@ -38,7 +38,9 @@ describe("UserDropdownContent", () => {
throw new Error("Anchor tag not found for the documentation menu item")
}
expect(link.getAttribute("href")).toBe(`https://github.com/coder/coder/tree/${process.env.CODER_VERSION}/docs`)
expect(link.getAttribute("href")).toBe(
`https://github.com/coder/coder/tree/${process.env.CODER_VERSION}/docs`,
)
})
it("has the correct link for the account item", () => {

View File

@ -27,7 +27,11 @@ export interface UserDropdownContentProps {
onSignOut: () => void
}
export const UserDropdownContent: FC<UserDropdownContentProps> = ({ user, onPopoverClose, onSignOut }) => {
export const UserDropdownContent: FC<UserDropdownContentProps> = ({
user,
onPopoverClose,
onSignOut,
}) => {
const styles = useStyles()
return (

View File

@ -77,7 +77,10 @@ export const Workspace: FC<WorkspaceProps> = ({
workspace={workspace}
/>
<WorkspaceDeletedBanner workspace={workspace} handleClick={() => navigate(`/templates`)} />
<WorkspaceDeletedBanner
workspace={workspace}
handleClick={() => navigate(`/templates`)}
/>
<WorkspaceStats workspace={workspace} />

View File

@ -8,7 +8,12 @@ export interface WorkspaceActionButtonProps {
className?: string
}
export const WorkspaceActionButton: FC<WorkspaceActionButtonProps> = ({ label, icon, onClick, className }) => {
export const WorkspaceActionButton: FC<WorkspaceActionButtonProps> = ({
label,
icon,
onClick,
className,
}) => {
return (
<Button className={className} startIcon={icon} onClick={onClick}>
{label}

View File

@ -37,9 +37,11 @@ const canAcceptJobs = (workspaceStatus: WorkspaceStatus) =>
const canCancelJobs = (workspaceStatus: WorkspaceStatus) =>
["starting", "stopping", "deleting"].includes(workspaceStatus)
const canStart = (workspaceStatus: WorkspaceStatus) => ["stopped", "canceled", "error"].includes(workspaceStatus)
const canStart = (workspaceStatus: WorkspaceStatus) =>
["stopped", "canceled", "error"].includes(workspaceStatus)
const canStop = (workspaceStatus: WorkspaceStatus) => ["started", "canceled", "error"].includes(workspaceStatus)
const canStop = (workspaceStatus: WorkspaceStatus) =>
["started", "canceled", "error"].includes(workspaceStatus)
const canDelete = (workspaceStatus: WorkspaceStatus) =>
["started", "stopped", "canceled", "error"].includes(workspaceStatus)
@ -99,7 +101,11 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
/>
)}
{workspace.outdated && canAcceptJobs(workspaceStatus) && (
<Button className={styles.actionButton} startIcon={<CloudDownloadIcon />} onClick={handleUpdate}>
<Button
className={styles.actionButton}
startIcon={<CloudDownloadIcon />}
onClick={handleUpdate}
>
{Language.update}
</Button>
)}

View File

@ -54,7 +54,9 @@ export const WorkspaceBuildStats: FC<WorkspaceBuildStatsProps> = ({ build }) =>
<div className={styles.statsDivider} />
<div className={styles.statItem}>
<span className={styles.statsLabel}>Action</span>
<span className={combineClasses([styles.statsValue, styles.capitalize])}>{build.transition}</span>
<span className={combineClasses([styles.statsValue, styles.capitalize])}>
{build.transition}
</span>
</div>
<div className={styles.statsDivider} />
<div className={styles.statItem}>

View File

@ -16,7 +16,10 @@ export interface WorkspaceDeletedBannerProps {
handleClick: () => void
}
export const WorkspaceDeletedBanner: FC<WorkspaceDeletedBannerProps> = ({ workspace, handleClick }) => {
export const WorkspaceDeletedBanner: FC<WorkspaceDeletedBannerProps> = ({
workspace,
handleClick,
}) => {
const styles = useStyles()
if (!isWorkspaceDeleted(workspace)) {

View File

@ -67,7 +67,9 @@ export const Language = {
},
editScheduleLink: "Edit schedule",
scheduleHeader: (workspace: Workspace): string => {
const tz = workspace.autostart_schedule ? extractTimezone(workspace.autostart_schedule) : dayjs.tz.guess()
const tz = workspace.autostart_schedule
? extractTimezone(workspace.autostart_schedule)
: dayjs.tz.guess()
return `Schedule (${tz})`
},
}

View File

@ -12,7 +12,9 @@ export default {
component: WorkspaceScheduleBanner,
}
const Template: Story<WorkspaceScheduleBannerProps> = (args) => <WorkspaceScheduleBanner {...args} />
const Template: Story<WorkspaceScheduleBannerProps> = (args) => (
<WorkspaceScheduleBanner {...args} />
)
export const Example = Template.bind({})
Example.args = {

View File

@ -35,7 +35,11 @@ export const shouldDisplay = (workspace: TypesGen.Workspace): boolean => {
}
}
export const WorkspaceScheduleBanner: FC<WorkspaceScheduleBannerProps> = ({ isLoading, onExtend, workspace }) => {
export const WorkspaceScheduleBanner: FC<WorkspaceScheduleBannerProps> = ({
isLoading,
onExtend,
workspace,
}) => {
if (!shouldDisplay(workspace)) {
return null
} else {

View File

@ -4,7 +4,11 @@ import dayjs from "dayjs"
import advancedFormat from "dayjs/plugin/advancedFormat"
import timezone from "dayjs/plugin/timezone"
import utc from "dayjs/plugin/utc"
import { defaultWorkspaceSchedule, WorkspaceScheduleForm, WorkspaceScheduleFormProps } from "./WorkspaceScheduleForm"
import {
defaultWorkspaceSchedule,
WorkspaceScheduleForm,
WorkspaceScheduleFormProps,
} from "./WorkspaceScheduleForm"
dayjs.extend(advancedFormat)
dayjs.extend(utc)

View File

@ -1,4 +1,9 @@
import { Language, ttlShutdownAt, validationSchema, WorkspaceScheduleFormValues } from "./WorkspaceScheduleForm"
import {
Language,
ttlShutdownAt,
validationSchema,
WorkspaceScheduleFormValues,
} from "./WorkspaceScheduleForm"
import { zones } from "./zones"
const valid: WorkspaceScheduleFormValues = {

View File

@ -281,9 +281,9 @@ export const ttlShutdownAt = (formTTL: number): string => {
// Passing an empty value for TTL in the form results in a number that is not zero but less than 1.
return Language.ttlCausesNoShutdownHelperText
} else {
return `${Language.ttlCausesShutdownHelperText} ${dayjs.duration(formTTL, "hours").humanize()} ${
Language.ttlCausesShutdownAfterStart
}.`
return `${Language.ttlCausesShutdownHelperText} ${dayjs
.duration(formTTL, "hours")
.humanize()} ${Language.ttlCausesShutdownAfterStart}.`
}
}

View File

@ -9,7 +9,9 @@ export default {
component: WorkspaceSection,
}
const Template: Story<WorkspaceSectionProps> = (args) => <WorkspaceSection {...args}>Content</WorkspaceSection>
const Template: Story<WorkspaceSectionProps> = (args) => (
<WorkspaceSection {...args}>Content</WorkspaceSection>
)
export const NoAction = Template.bind({})
NoAction.args = {

View File

@ -14,7 +14,12 @@ export interface WorkspaceSectionProps {
title?: string
}
export const WorkspaceSection: React.FC<WorkspaceSectionProps> = ({ action, children, contentsProps, title }) => {
export const WorkspaceSection: React.FC<WorkspaceSectionProps> = ({
action,
children,
contentsProps,
title,
}) => {
const styles = useStyles()
return (
@ -28,7 +33,10 @@ export const WorkspaceSection: React.FC<WorkspaceSectionProps> = ({ action, chil
</div>
)}
<div {...contentsProps} className={combineClasses([styles.contents, contentsProps?.className])}>
<div
{...contentsProps}
className={combineClasses([styles.contents, contentsProps?.className])}
>
{children}
</div>
</Paper>

View File

@ -7,7 +7,10 @@ import { CustomEventListener } from "../util/events"
* @param eventType a unique name defining the type of the event. e.g. `"coder:workspace:ready"`
* @param listener a custom event listener.
*/
export const useCustomEvent = <T, E extends string = string>(eventType: E, listener: CustomEventListener<T>): void => {
export const useCustomEvent = <T, E extends string = string>(
eventType: E,
listener: CustomEventListener<T>,
): void => {
useEffect(() => {
const handleEvent: CustomEventListener<T> = (event) => {
listener(event)

View File

@ -31,7 +31,9 @@ export default {
component: CreateWorkspacePageView,
} as ComponentMeta<typeof CreateWorkspacePageView>
const Template: Story<CreateWorkspacePageViewProps> = (args) => <CreateWorkspacePageView {...args} />
const Template: Story<CreateWorkspacePageViewProps> = (args) => (
<CreateWorkspacePageView {...args} />
)
export const NoParameters = Template.bind({})
NoParameters.args = {

View File

@ -36,37 +36,38 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props)
const [parameterValues, setParameterValues] = useState<Record<string, string>>({})
useStyles()
const form: FormikContextType<TypesGen.CreateWorkspaceRequest> = useFormik<TypesGen.CreateWorkspaceRequest>({
initialValues: {
name: "",
template_id: props.selectedTemplate ? props.selectedTemplate.id : "",
},
enableReinitialize: true,
validationSchema,
onSubmit: (request) => {
if (!props.templateSchema) {
throw new Error("No template schema loaded")
}
const createRequests: TypesGen.CreateParameterRequest[] = []
props.templateSchema.forEach((schema) => {
let value = schema.default_source_value
if (schema.name in parameterValues) {
value = parameterValues[schema.name]
const form: FormikContextType<TypesGen.CreateWorkspaceRequest> =
useFormik<TypesGen.CreateWorkspaceRequest>({
initialValues: {
name: "",
template_id: props.selectedTemplate ? props.selectedTemplate.id : "",
},
enableReinitialize: true,
validationSchema,
onSubmit: (request) => {
if (!props.templateSchema) {
throw new Error("No template schema loaded")
}
createRequests.push({
name: schema.name,
destination_scheme: schema.default_destination_scheme,
source_scheme: schema.default_source_scheme,
source_value: value,
const createRequests: TypesGen.CreateParameterRequest[] = []
props.templateSchema.forEach((schema) => {
let value = schema.default_source_value
if (schema.name in parameterValues) {
value = parameterValues[schema.name]
}
createRequests.push({
name: schema.name,
destination_scheme: schema.default_destination_scheme,
source_scheme: schema.default_source_scheme,
source_value: value,
})
})
})
return props.onSubmit({
...request,
parameter_values: createRequests,
})
},
})
return props.onSubmit({
...request,
parameter_values: createRequests,
})
},
})
const getFieldHelpers = getFormHelpers<TypesGen.CreateWorkspaceRequest>(form)
return (

View File

@ -1,10 +1,17 @@
import { screen } from "@testing-library/react"
import { MockTemplate, MockWorkspaceResource, renderWithAuth } from "../../testHelpers/renderHelpers"
import {
MockTemplate,
MockWorkspaceResource,
renderWithAuth,
} from "../../testHelpers/renderHelpers"
import { TemplatePage } from "./TemplatePage"
describe("TemplatePage", () => {
it("shows the template name, readme and resources", async () => {
renderWithAuth(<TemplatePage />, { route: `/templates/${MockTemplate.id}`, path: "/templates/:template" })
renderWithAuth(<TemplatePage />, {
route: `/templates/${MockTemplate.id}`,
path: "/templates/:template",
})
await screen.findByText(MockTemplate.name)
screen.getByTestId("markdown")
screen.getByText(MockWorkspaceResource.name)

View File

@ -8,7 +8,11 @@ import ReactMarkdown from "react-markdown"
import { Link as RouterLink } from "react-router-dom"
import { Template, TemplateVersion, WorkspaceResource } from "../../api/typesGenerated"
import { Margins } from "../../components/Margins/Margins"
import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
import {
PageHeader,
PageHeaderSubtitle,
PageHeaderTitle,
} from "../../components/PageHeader/PageHeader"
import { Stack } from "../../components/Stack/Stack"
import { TemplateResourcesTable } from "../../components/TemplateResourcesTable/TemplateResourcesTable"
import { TemplateStats } from "../../components/TemplateStats/TemplateStats"
@ -27,7 +31,11 @@ export interface TemplatePageViewProps {
templateResources: WorkspaceResource[]
}
export const TemplatePageView: FC<TemplatePageViewProps> = ({ template, activeTemplateVersion, templateResources }) => {
export const TemplatePageView: FC<TemplatePageViewProps> = ({
template,
activeTemplateVersion,
templateResources,
}) => {
const styles = useStyles()
const readme = frontMatter(activeTemplateVersion.readme)
@ -39,7 +47,11 @@ export const TemplatePageView: FC<TemplatePageViewProps> = ({ template, activeTe
<Margins>
<PageHeader
actions={
<Link underline="none" component={RouterLink} to={`/templates/${template.name}/workspace`}>
<Link
underline="none"
component={RouterLink}
to={`/templates/${template.name}/workspace`}
>
<Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>
</Link>
}
@ -52,10 +64,16 @@ export const TemplatePageView: FC<TemplatePageViewProps> = ({ template, activeTe
<Stack spacing={3}>
<TemplateStats template={template} activeVersion={activeTemplateVersion} />
<WorkspaceSection title={Language.resourcesTitle} contentsProps={{ className: styles.resourcesTableContents }}>
<WorkspaceSection
title={Language.resourcesTitle}
contentsProps={{ className: styles.resourcesTableContents }}
>
<TemplateResourcesTable resources={getStartedResources(templateResources)} />
</WorkspaceSection>
<WorkspaceSection title={Language.readmeTitle} contentsProps={{ className: styles.readmeContents }}>
<WorkspaceSection
title={Language.readmeTitle}
contentsProps={{ className: styles.readmeContents }}
>
<div className={styles.markdownWrapper}>
<ReactMarkdown
components={{

View File

@ -15,7 +15,11 @@ import { AvatarData } from "../../components/AvatarData/AvatarData"
import { CodeExample } from "../../components/CodeExample/CodeExample"
import { EmptyState } from "../../components/EmptyState/EmptyState"
import { Margins } from "../../components/Margins/Margins"
import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
import {
PageHeader,
PageHeaderSubtitle,
PageHeaderTitle,
} from "../../components/PageHeader/PageHeader"
import { Stack } from "../../components/Stack/Stack"
import { TableCellLink } from "../../components/TableCellLink/TableCellLink"
import { TableLoader } from "../../components/TableLoader/TableLoader"
@ -36,7 +40,8 @@ export const Language = {
nameLabel: "Name",
usedByLabel: "Used by",
lastUpdatedLabel: "Last updated",
emptyViewNoPerms: "Contact your Coder administrator to create a template. You can share the code below.",
emptyViewNoPerms:
"Contact your Coder administrator to create a template. You can share the code below.",
emptyMessage: "Create your first template",
emptyDescription: (
<>
@ -48,7 +53,8 @@ export const Language = {
</>
),
templateTooltipTitle: "What is template?",
templateTooltipText: "With templates you can create a common configuration for your workspaces using Terraform.",
templateTooltipText:
"With templates you can create a common configuration for your workspaces using Terraform.",
templateTooltipLink: "Manage templates",
createdByLabel: "Created by",
}
@ -108,7 +114,9 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = (props) => {
<TableCell colSpan={999}>
<EmptyState
message={Language.emptyMessage}
description={props.canCreateTemplate ? Language.emptyDescription : Language.emptyViewNoPerms}
description={
props.canCreateTemplate ? Language.emptyDescription : Language.emptyViewNoPerms
}
descriptionClassName={styles.emptyDescription}
cta={<CodeExample code="coder template init" />}
/>

View File

@ -59,7 +59,8 @@ const TerminalPage: FC<{
})
const isConnected = terminalState.matches("connected")
const isDisconnected = terminalState.matches("disconnected")
const { workspaceError, workspaceAgentError, workspaceAgent, websocketError } = terminalState.context
const { workspaceError, workspaceAgentError, workspaceAgent, websocketError } =
terminalState.context
// Create the terminal!
useEffect(() => {
@ -177,7 +178,16 @@ const TerminalPage: FC<{
width: terminal.cols,
},
})
}, [workspaceError, workspaceAgentError, websocketError, workspaceAgent, terminal, fitAddon, isConnected, sendEvent])
}, [
workspaceError,
workspaceAgentError,
websocketError,
workspaceAgent,
terminal,
fitAddon,
isConnected,
sendEvent,
])
return (
<>

View File

@ -16,7 +16,9 @@ export const AccountPage: React.FC = () => {
const { me, updateProfileError } = authState.context
const hasError = !!updateProfileError
const formErrors =
hasError && isApiError(updateProfileError) ? mapApiErrorToFieldErrors(updateProfileError.response.data) : undefined
hasError && isApiError(updateProfileError)
? mapApiErrorToFieldErrors(updateProfileError.response.data)
: undefined
const hasUnknownError = hasError && !isApiError(updateProfileError)
if (!me) {

View File

@ -25,19 +25,24 @@ describe("SSH keys Page", () => {
await screen.findByText(MockGitSSHKey.public_key)
// Click on the "Regenerate" button to display the confirm dialog
const regenerateButton = screen.getByRole("button", { name: SSHKeysPageLanguage.regenerateLabel })
const regenerateButton = screen.getByRole("button", {
name: SSHKeysPageLanguage.regenerateLabel,
})
fireEvent.click(regenerateButton)
const confirmDialog = screen.getByRole("dialog")
expect(confirmDialog).toHaveTextContent(SSHKeysPageLanguage.regenerateDialogMessage)
const newUserSSHKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDSC/ouD/LqiT1Rd99vDv/MwUmqzJuinLTMTpk5kVy66"
const newUserSSHKey =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDSC/ouD/LqiT1Rd99vDv/MwUmqzJuinLTMTpk5kVy66"
jest.spyOn(API, "regenerateUserSSHKey").mockResolvedValueOnce({
...MockGitSSHKey,
public_key: newUserSSHKey,
})
// Click on the "Confirm" button
const confirmButton = within(confirmDialog).getByRole("button", { name: SSHKeysPageLanguage.confirmLabel })
const confirmButton = within(confirmDialog).getByRole("button", {
name: SSHKeysPageLanguage.confirmLabel,
})
fireEvent.click(confirmButton)
// Check if the success message is displayed
@ -66,13 +71,17 @@ describe("SSH keys Page", () => {
jest.spyOn(API, "regenerateUserSSHKey").mockRejectedValueOnce({})
// Click on the "Regenerate" button to display the confirm dialog
const regenerateButton = screen.getByRole("button", { name: SSHKeysPageLanguage.regenerateLabel })
const regenerateButton = screen.getByRole("button", {
name: SSHKeysPageLanguage.regenerateLabel,
})
fireEvent.click(regenerateButton)
const confirmDialog = screen.getByRole("dialog")
expect(confirmDialog).toHaveTextContent(SSHKeysPageLanguage.regenerateDialogMessage)
// Click on the "Confirm" button
const confirmButton = within(confirmDialog).getByRole("button", { name: SSHKeysPageLanguage.confirmLabel })
const confirmButton = within(confirmDialog).getByRole("button", {
name: SSHKeysPageLanguage.confirmLabel,
})
fireEvent.click(confirmButton)
// Check if the error message is displayed

View File

@ -24,16 +24,22 @@ const newData = {
const fillAndSubmitForm = async () => {
await waitFor(() => screen.findByLabelText("Old Password"))
fireEvent.change(screen.getByLabelText("Old Password"), { target: { value: newData.old_password } })
fireEvent.change(screen.getByLabelText("Old Password"), {
target: { value: newData.old_password },
})
fireEvent.change(screen.getByLabelText("New Password"), { target: { value: newData.password } })
fireEvent.change(screen.getByLabelText("Confirm Password"), { target: { value: newData.confirm_password } })
fireEvent.change(screen.getByLabelText("Confirm Password"), {
target: { value: newData.confirm_password },
})
fireEvent.click(screen.getByText(SecurityForm.Language.updatePassword))
}
describe("SecurityPage", () => {
describe("when it is a success", () => {
it("shows the success message", async () => {
jest.spyOn(API, "updateUserPassword").mockImplementationOnce((_userId, _data) => Promise.resolve(undefined))
jest
.spyOn(API, "updateUserPassword")
.mockImplementationOnce((_userId, _data) => Promise.resolve(undefined))
const { user } = renderPage()
await fillAndSubmitForm()
@ -71,7 +77,10 @@ describe("SecurityPage", () => {
jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({
isAxiosError: true,
response: {
data: { message: "Invalid password.", validations: [{ detail: "Invalid password.", field: "password" }] },
data: {
message: "Invalid password.",
validations: [{ detail: "Invalid password.", field: "password" }],
},
},
})

View File

@ -21,7 +21,9 @@ export const CreateUserPage: React.FC = () => {
const navigate = useNavigate()
// There is no field for organization id in Community Edition, so handle its field error like a generic error
const genericError =
createUserErrorMessage || createUserFormErrors?.organization_id || (!myOrgId ? Language.unknownError : undefined)
createUserErrorMessage ||
createUserFormErrors?.organization_id ||
(!myOrgId ? Language.unknownError : undefined)
return (
<Margins>

View File

@ -6,7 +6,13 @@ import { GlobalSnackbar } from "../../components/GlobalSnackbar/GlobalSnackbar"
import { Language as ResetPasswordDialogLanguage } from "../../components/ResetPasswordDialog/ResetPasswordDialog"
import { Language as RoleSelectLanguage } from "../../components/RoleSelect/RoleSelect"
import { Language as UsersTableLanguage } from "../../components/UsersTable/UsersTable"
import { MockAuditorRole, MockUser, MockUser2, render, SuspendedMockUser } from "../../testHelpers/renderHelpers"
import {
MockAuditorRole,
MockUser,
MockUser2,
render,
SuspendedMockUser,
} from "../../testHelpers/renderHelpers"
import { server } from "../../testHelpers/server"
import { permissionsToCheck } from "../../xServices/auth/authXService"
import { Language as usersXServiceLanguage } from "../../xServices/users/usersXService"
@ -30,7 +36,9 @@ const suspendUser = async (setupActionSpies: () => void) => {
// Check if the confirm message is displayed
const confirmDialog = screen.getByRole("dialog")
expect(confirmDialog).toHaveTextContent(`${UsersPageLanguage.suspendDialogMessagePrefix} ${MockUser.username}?`)
expect(confirmDialog).toHaveTextContent(
`${UsersPageLanguage.suspendDialogMessagePrefix} ${MockUser.username}?`,
)
// Setup spies to check the actions after
setupActionSpies()
@ -86,13 +94,17 @@ const resetUserPassword = async (setupActionSpies: () => void) => {
// Check if the confirm message is displayed
const confirmDialog = screen.getByRole("dialog")
expect(confirmDialog).toHaveTextContent(`You will need to send ${MockUser.username} the following password:`)
expect(confirmDialog).toHaveTextContent(
`You will need to send ${MockUser.username} the following password:`,
)
// Setup spies to check the actions after
setupActionSpies()
// Click on the "Confirm" button
const confirmButton = within(confirmDialog).getByRole("button", { name: ResetPasswordDialogLanguage.confirmText })
const confirmButton = within(confirmDialog).getByRole("button", {
name: ResetPasswordDialogLanguage.confirmText,
})
fireEvent.click(confirmButton)
}
@ -169,7 +181,9 @@ describe("Users Page", () => {
await suspendUser(() => {
jest.spyOn(API, "suspendUser").mockResolvedValueOnce(MockUser)
jest.spyOn(API, "getUsers").mockImplementationOnce(() => Promise.resolve([MockUser, MockUser2]))
jest
.spyOn(API, "getUsers")
.mockImplementationOnce(() => Promise.resolve([MockUser, MockUser2]))
})
// Check if the success message is displayed
@ -274,7 +288,10 @@ describe("Users Page", () => {
// Check if the API was called correctly
expect(API.updateUserPassword).toBeCalledTimes(1)
expect(API.updateUserPassword).toBeCalledWith(MockUser.id, { password: expect.any(String), old_password: "" })
expect(API.updateUserPassword).toBeCalledWith(MockUser.id, {
password: expect.any(String),
old_password: "",
})
})
})
@ -296,7 +313,10 @@ describe("Users Page", () => {
// Check if the API was called correctly
expect(API.updateUserPassword).toBeCalledTimes(1)
expect(API.updateUserPassword).toBeCalledWith(MockUser.id, { password: expect.any(String), old_password: "" })
expect(API.updateUserPassword).toBeCalledWith(MockUser.id, {
password: expect.any(String),
old_password: "",
})
})
})
})
@ -324,7 +344,10 @@ describe("Users Page", () => {
// Check if the API was called correctly
const currentRoles = MockUser.roles.map((r) => r.name)
expect(API.updateUserRoles).toBeCalledTimes(1)
expect(API.updateUserRoles).toBeCalledWith([...currentRoles, MockAuditorRole.name], MockUser.id)
expect(API.updateUserRoles).toBeCalledWith(
[...currentRoles, MockAuditorRole.name],
MockUser.id,
)
})
})
@ -347,7 +370,10 @@ describe("Users Page", () => {
// Check if the API was called correctly
const currentRoles = MockUser.roles.map((r) => r.name)
expect(API.updateUserRoles).toBeCalledTimes(1)
expect(API.updateUserRoles).toBeCalledWith([...currentRoles, MockAuditorRole.name], MockUser.id)
expect(API.updateUserRoles).toBeCalledWith(
[...currentRoles, MockAuditorRole.name],
MockUser.id,
)
})
it("shows an error from the backend", async () => {
render(

View File

@ -22,8 +22,14 @@ export const UsersPage: React.FC = () => {
const xServices = useContext(XServiceContext)
const [usersState, usersSend] = useActor(xServices.usersXService)
const [rolesState, rolesSend] = useActor(xServices.siteRolesXService)
const { users, getUsersError, userIdToSuspend, userIdToActivate, userIdToResetPassword, newUserPassword } =
usersState.context
const {
users,
getUsersError,
userIdToSuspend,
userIdToActivate,
userIdToResetPassword,
newUserPassword,
} = usersState.context
const navigate = useNavigate()
const userToBeSuspended = users?.find((u) => u.id === userIdToSuspend)
const userToBeActivated = users?.find((u) => u.id === userIdToActivate)

View File

@ -1,16 +1,22 @@
import { Story } from "@storybook/react"
import { WorkspaceAppErrorPageView, WorkspaceAppErrorPageViewProps } from "./WorkspaceAppErrorPageView"
import {
WorkspaceAppErrorPageView,
WorkspaceAppErrorPageViewProps,
} from "./WorkspaceAppErrorPageView"
export default {
title: "pages/WorkspaceAppErrorPageView",
component: WorkspaceAppErrorPageView,
}
const Template: Story<WorkspaceAppErrorPageViewProps> = (args) => <WorkspaceAppErrorPageView {...args} />
const Template: Story<WorkspaceAppErrorPageViewProps> = (args) => (
<WorkspaceAppErrorPageView {...args} />
)
export const NotRunning = Template.bind({})
NotRunning.args = {
appName: "code-server",
// This is a real message copied and pasted from the backend!
message: "remote dial error: dial 'tcp://localhost:13337': dial tcp 127.0.0.1:13337: connect: connection refused",
message:
"remote dial error: dial 'tcp://localhost:13337': dial tcp 127.0.0.1:13337: connect: connection refused",
}

View File

@ -16,7 +16,9 @@ export const WorkspaceBuildPage: FC = () => {
return (
<>
<Helmet>
<title>{build ? pageTitle(`Build #${build.build_number} · ${build.workspace_name}`) : ""}</title>
<title>
{build ? pageTitle(`Build #${build.build_number} · ${build.workspace_name}`) : ""}
</title>
</Helmet>
<WorkspaceBuildPageView logs={logs} build={build} />

View File

@ -8,7 +8,9 @@ import { WorkspaceBuildLogs } from "../../components/WorkspaceBuildLogs/Workspac
import { WorkspaceBuildStats } from "../../components/WorkspaceBuildStats/WorkspaceBuildStats"
const sortLogsByCreatedAt = (logs: ProvisionerJobLog[]) => {
return [...logs].sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
return [...logs].sort(
(a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime(),
)
}
export interface WorkspaceBuildPageViewProps {

View File

@ -77,11 +77,15 @@ describe("Workspace Page", () => {
expect(status).toHaveTextContent("Running")
})
it("requests a stop job when the user presses Stop", async () => {
const stopWorkspaceMock = jest.spyOn(api, "stopWorkspace").mockResolvedValueOnce(MockWorkspaceBuild)
const stopWorkspaceMock = jest
.spyOn(api, "stopWorkspace")
.mockResolvedValueOnce(MockWorkspaceBuild)
await testButton(Language.stop, stopWorkspaceMock)
})
it("requests a delete job when the user presses Delete and confirms", async () => {
const deleteWorkspaceMock = jest.spyOn(api, "deleteWorkspace").mockResolvedValueOnce(MockWorkspaceBuild)
const deleteWorkspaceMock = jest
.spyOn(api, "deleteWorkspace")
.mockResolvedValueOnce(MockWorkspaceBuild)
await renderWorkspacePage()
const button = await screen.findByText(Language.delete)
await waitFor(() => fireEvent.click(button))
@ -172,9 +176,13 @@ describe("Workspace Page", () => {
expect(agent1Names.length).toEqual(2)
const agent2Names = await screen.findAllByText(MockWorkspaceAgentDisconnected.name)
expect(agent2Names.length).toEqual(2)
const agent1Status = await screen.findAllByText(DisplayAgentStatusLanguage[MockWorkspaceAgent.status])
const agent1Status = await screen.findAllByText(
DisplayAgentStatusLanguage[MockWorkspaceAgent.status],
)
expect(agent1Status.length).toEqual(2)
const agent2Status = await screen.findAllByText(DisplayAgentStatusLanguage[MockWorkspaceAgentDisconnected.status])
const agent2Status = await screen.findAllByText(
DisplayAgentStatusLanguage[MockWorkspaceAgentDisconnected.status],
)
expect(agent2Status.length).toEqual(2)
})
})

View File

@ -26,7 +26,8 @@ export const WorkspacePage: React.FC = () => {
userId: me?.id,
},
})
const { workspace, resources, getWorkspaceError, getResourcesError, builds, permissions } = workspaceState.context
const { workspace, resources, getWorkspaceError, getResourcesError, builds, permissions } =
workspaceState.context
const canUpdateWorkspace = !!permissions?.updateWorkspace

View File

@ -1,7 +1,11 @@
import * as TypesGen from "../../api/typesGenerated"
import { WorkspaceScheduleFormValues } from "../../components/WorkspaceScheduleForm/WorkspaceScheduleForm"
import * as Mocks from "../../testHelpers/entities"
import { formValuesToAutoStartRequest, formValuesToTTLRequest, workspaceToInitialValues } from "./WorkspaceSchedulePage"
import {
formValuesToAutoStartRequest,
formValuesToTTLRequest,
workspaceToInitialValues,
} from "./WorkspaceSchedulePage"
const validValues: WorkspaceScheduleFormValues = {
sunday: false,

View File

@ -87,7 +87,9 @@ export const formValuesToAutoStartRequest = (
}
}
export const formValuesToTTLRequest = (values: WorkspaceScheduleFormValues): TypesGen.UpdateWorkspaceTTLRequest => {
export const formValuesToTTLRequest = (
values: WorkspaceScheduleFormValues,
): TypesGen.UpdateWorkspaceTTLRequest => {
return {
// minutes to nanoseconds
ttl_ms: values.ttl ? values.ttl * 60 * 60 * 1000 : undefined,
@ -99,7 +101,9 @@ export const workspaceToInitialValues = (
defaultTimeZone = "",
): WorkspaceScheduleFormValues => {
const schedule = workspace.autostart_schedule
const ttlHours = workspace.ttl_ms ? Math.round(workspace.ttl_ms / (1000 * 60 * 60)) : defaultWorkspaceScheduleTTL
const ttlHours = workspace.ttl_ms
? Math.round(workspace.ttl_ms / (1000 * 60 * 60))
: defaultWorkspaceScheduleTTL
if (!schedule) {
return defaultWorkspaceSchedule(ttlHours, defaultTimeZone)
@ -149,7 +153,11 @@ export const WorkspaceSchedulePage: React.FC = () => {
if (!username || !workspaceName) {
navigate("/workspaces")
return null
} else if (scheduleState.matches("idle") || scheduleState.matches("gettingWorkspace") || !workspace) {
} else if (
scheduleState.matches("idle") ||
scheduleState.matches("gettingWorkspace") ||
!workspace
) {
return <FullScreenLoader />
} else if (scheduleState.matches("error")) {
return (

View File

@ -3,7 +3,10 @@ import { spawn } from "xstate"
import { ProvisionerJobStatus, WorkspaceTransition } from "../../api/typesGenerated"
import { MockWorkspace } from "../../testHelpers/entities"
import { workspaceFilterQuery } from "../../util/workspace"
import { workspaceItemMachine, WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService"
import {
workspaceItemMachine,
WorkspaceItemMachineRef,
} from "../../xServices/workspaces/workspacesXService"
import { WorkspacesPageView, WorkspacesPageViewProps } from "./WorkspacesPageView"
const createWorkspaceItemRef = (

View File

@ -18,7 +18,11 @@ import { Link as RouterLink, useNavigate } from "react-router-dom"
import { AvatarData } from "../../components/AvatarData/AvatarData"
import { EmptyState } from "../../components/EmptyState/EmptyState"
import { Margins } from "../../components/Margins/Margins"
import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
import {
PageHeader,
PageHeaderSubtitle,
PageHeaderTitle,
} from "../../components/PageHeader/PageHeader"
import { SearchBarWithFilter } from "../../components/SearchBarWithFilter/SearchBarWithFilter"
import { Stack } from "../../components/Stack/Stack"
import { TableCellLink } from "../../components/TableCellLink/TableCellLink"
@ -152,7 +156,12 @@ export interface WorkspacesPageViewProps {
onFilter: (query: string) => void
}
export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, workspaceRefs, filter, onFilter }) => {
export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
loading,
workspaceRefs,
filter,
onFilter,
}) => {
const presetFilters = [
{ query: workspaceFilterQuery.me, name: Language.yourWorkspacesButton },
{ query: workspaceFilterQuery.all, name: Language.allWorkspacesButton },
@ -202,7 +211,9 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, works
description={Language.emptyCreateWorkspaceDescription}
cta={
<Link underline="none" component={RouterLink} to="/templates">
<Button startIcon={<AddCircleOutline />}>{Language.createFromTemplateButton}</Button>
<Button startIcon={<AddCircleOutline />}>
{Language.createFromTemplateButton}
</Button>
</Link>
}
/>
@ -218,7 +229,9 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, works
</>
)}
{workspaceRefs &&
workspaceRefs.map((workspaceRef) => <WorkspaceRow workspaceRef={workspaceRef} key={workspaceRef.id} />)}
workspaceRefs.map((workspaceRef) => (
<WorkspaceRow workspaceRef={workspaceRef} key={workspaceRef.id} />
))}
</TableBody>
</Table>
</Margins>

View File

@ -226,7 +226,10 @@ export const MockDeletingWorkspace: TypesGen.Workspace = {
...MockWorkspace,
latest_build: { ...MockWorkspaceBuildDelete, job: MockRunningProvisionerJob },
}
export const MockDeletedWorkspace: TypesGen.Workspace = { ...MockWorkspace, latest_build: MockWorkspaceBuildDelete }
export const MockDeletedWorkspace: TypesGen.Workspace = {
...MockWorkspace,
latest_build: MockWorkspaceBuildDelete,
}
export const MockOutdatedWorkspace: TypesGen.Workspace = { ...MockFailedWorkspace, outdated: true }

View File

@ -115,9 +115,12 @@ export const handlers = [
rest.get("/api/v2/workspaces/:workspaceId/builds", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockBuilds))
}),
rest.get("/api/v2/users/:username/workspace/:workspaceName/builds/:buildNumber", (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspaceBuild))
}),
rest.get(
"/api/v2/users/:username/workspace/:workspaceName/builds/:buildNumber",
(req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspaceBuild))
},
),
rest.get("/api/v2/workspacebuilds/:workspaceBuildId/resources", (req, res, ctx) => {
return res(ctx.status(200), ctx.json([M.MockWorkspaceResource, M.MockWorkspaceResource2]))
}),

View File

@ -2,7 +2,12 @@ import ThemeProvider from "@material-ui/styles/ThemeProvider"
import { render as wrappedRender, RenderResult } from "@testing-library/react"
import { createMemoryHistory } from "history"
import { FC, ReactElement } from "react"
import { MemoryRouter, Route, Routes, unstable_HistoryRouter as HistoryRouter } from "react-router-dom"
import {
MemoryRouter,
Route,
Routes,
unstable_HistoryRouter as HistoryRouter,
} from "react-router-dom"
import { RequireAuth } from "../components/RequireAuth/RequireAuth"
import { dark } from "../theme"
import { XServiceProvider } from "../xServices/StateContext"

View File

@ -42,6 +42,8 @@ export type AnnotatedEventListener<E extends Event> = (event: E) => void
* @remark this is especially necessary when an event originates from an iframe
* as `instanceof` will not match against another origin's prototype chain.
*/
export const isCustomEvent = <D = unknown>(event: CustomEvent<D> | Event): event is CustomEvent<D> => {
export const isCustomEvent = <D = unknown>(
event: CustomEvent<D> | Event,
): event is CustomEvent<D> => {
return !!(event as CustomEvent).detail
}

View File

@ -1,7 +1,12 @@
import dayjs from "dayjs"
import * as TypesGen from "../api/typesGenerated"
import * as Mocks from "../testHelpers/entities"
import { defaultWorkspaceExtension, isWorkspaceDeleted, isWorkspaceOn, workspaceQueryToFilter } from "./workspace"
import {
defaultWorkspaceExtension,
isWorkspaceDeleted,
isWorkspaceOn,
workspaceQueryToFilter,
} from "./workspace"
describe("util > workspace", () => {
describe("isWorkspaceOn", () => {

Some files were not shown because too many files have changed in this diff Show More