mirror of
https://github.com/coder/coder.git
synced 2025-07-23 21:32:07 +00:00
refactor: update Prettier printWidth to 100 (#2684)
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"printWidth": 100,
|
||||
"semi": false,
|
||||
"trailingComma": "all",
|
||||
"overrides": [
|
||||
|
4
site/can-ndjson-stream.d.ts
vendored
4
site/can-ndjson-stream.d.ts
vendored
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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([
|
||||
|
@ -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" />
|
||||
|
@ -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)}`,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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"))
|
||||
|
@ -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()}`,
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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 }}>
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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 = {
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
|
@ -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 = {
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
|
@ -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: "",
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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 = {
|
||||
|
@ -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 }
|
||||
) : (
|
||||
|
@ -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(", ")
|
||||
|
@ -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(() => ({
|
||||
|
@ -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"),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -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",
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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)}
|
||||
|
@ -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 = {
|
||||
|
@ -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} />}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 (
|
||||
|
@ -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}
|
||||
|
@ -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} />
|
||||
|
@ -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`
|
||||
|
||||
|
@ -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's workspaces.</HelpTooltipText>
|
||||
<HelpTooltipText>
|
||||
A template is a common configuration for your team'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>
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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",
|
||||
}
|
||||
|
||||
|
@ -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 (
|
||||
|
@ -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} />
|
||||
|
@ -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", () => {
|
||||
|
@ -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 (
|
||||
|
@ -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} />
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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}>
|
||||
|
@ -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)) {
|
||||
|
@ -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})`
|
||||
},
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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 = {
|
||||
|
@ -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}.`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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 = {
|
||||
|
@ -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 (
|
||||
|
@ -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)
|
||||
|
@ -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={{
|
||||
|
@ -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" />}
|
||||
/>
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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" }],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
}
|
||||
|
@ -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} />
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 (
|
||||
|
@ -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 = (
|
||||
|
@ -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>
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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]))
|
||||
}),
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
Reference in New Issue
Block a user