site: reduce printWidth to 80 (#4437)

Resolves #4435
This commit is contained in:
Ammar Bandukwala
2022-10-10 12:33:35 -05:00
committed by GitHub
parent cb54986d3f
commit 85c679597c
274 changed files with 3238 additions and 1173 deletions

View File

@ -1,12 +1,11 @@
{
"printWidth": 100,
"printWidth": 80,
"semi": false,
"trailingComma": "all",
"overrides": [
{
"files": ["./README.md", "**/*.yaml"],
"options": {
"printWidth": 80,
"proseWrap": "always"
}
}

View File

@ -39,7 +39,11 @@ module.exports = {
//
// SEE: https://storybook.js.org/docs/react/configure/webpack
webpackFinal: async (config) => {
config.resolve.modules = [path.resolve(__dirname, ".."), "node_modules", "../src"]
config.resolve.modules = [
path.resolve(__dirname, ".."),
"node_modules",
"../src",
]
return config
},
}

View File

@ -6,7 +6,10 @@ export class SignInPage extends BasePom {
super(baseURL, "/login", page)
}
async submitBuiltInAuthentication(email: string, password: string): Promise<void> {
async submitBuiltInAuthentication(
email: string,
password: string,
): Promise<void> {
await this.page.fill("text=Email", email)
await this.page.fill("text=Password", password)
await this.page.click("text=Sign In")

View File

@ -1,7 +1,10 @@
import { test } from "@playwright/test"
import { HealthzPage } from "../pom/HealthzPage"
test("Healthz is available without authentication", async ({ baseURL, page }) => {
test("Healthz is available without authentication", async ({
baseURL,
page,
}) => {
const healthzPage = new HealthzPage(baseURL, page)
await page.goto(healthzPage.url, { waitUntil: "networkidle" })
await healthzPage.getOk().waitFor({ state: "visible" })

View File

@ -23,7 +23,12 @@
href="/favicons/favicon.png"
data-react-helmet="true"
/>
<link rel="icon" type="image/svg+xml" href="/favicons/favicon.svg" data-react-helmet="true" />
<link
rel="icon"
type="image/svg+xml"
href="/favicons/favicon.svg"
data-react-helmet="true"
/>
</head>
<body>

View File

@ -35,8 +35,17 @@ module.exports = {
{
displayName: "lint",
runner: "jest-runner-eslint",
testMatch: ["<rootDir>/**/*.js", "<rootDir>/**/*.ts", "<rootDir>/**/*.tsx"],
testPathIgnorePatterns: ["/out/", "/_jest/", "jest.config.js", "jest-runner.*.js"],
testMatch: [
"<rootDir>/**/*.js",
"<rootDir>/**/*.ts",
"<rootDir>/**/*.tsx",
],
testPathIgnorePatterns: [
"/out/",
"/_jest/",
"jest.config.js",
"jest-runner.*.js",
],
},
],
collectCoverageFrom: [

View File

@ -45,7 +45,10 @@ const CONSOLE_FAIL_TYPES = ["error" /* 'warn' */] as const
CONSOLE_FAIL_TYPES.forEach((logType: typeof CONSOLE_FAIL_TYPES[number]) => {
global.console[logType] = <Type>(format: string, ...args: Type[]): void => {
throw new Error(
`Failing due to console.${logType} while running test!\n\n${util.format(format, ...args)}`,
`Failing due to console.${logType} while running test!\n\n${util.format(
format,
...args,
)}`,
)
}
})

View File

@ -23,25 +23,42 @@ import { SettingsLayout } from "./components/SettingsLayout/SettingsLayout"
// - Pages that are secondary, not in the main navigation or not usually accessed
// - Pages that use heavy dependencies like charts or time libraries
const NotFoundPage = lazy(() => import("./pages/404Page/404Page"))
const CliAuthenticationPage = lazy(() => import("./pages/CliAuthPage/CliAuthPage"))
const CliAuthenticationPage = lazy(
() => import("./pages/CliAuthPage/CliAuthPage"),
)
const HealthzPage = lazy(() => import("./pages/HealthzPage/HealthzPage"))
const AccountPage = lazy(() => import("./pages/UserSettingsPage/AccountPage/AccountPage"))
const SecurityPage = lazy(() => import("./pages/UserSettingsPage/SecurityPage/SecurityPage"))
const SSHKeysPage = lazy(() => import("./pages/UserSettingsPage/SSHKeysPage/SSHKeysPage"))
const CreateUserPage = lazy(() => import("./pages/UsersPage/CreateUserPage/CreateUserPage"))
const WorkspaceBuildPage = lazy(() => import("./pages/WorkspaceBuildPage/WorkspaceBuildPage"))
const AccountPage = lazy(
() => import("./pages/UserSettingsPage/AccountPage/AccountPage"),
)
const SecurityPage = lazy(
() => import("./pages/UserSettingsPage/SecurityPage/SecurityPage"),
)
const SSHKeysPage = lazy(
() => import("./pages/UserSettingsPage/SSHKeysPage/SSHKeysPage"),
)
const CreateUserPage = lazy(
() => import("./pages/UsersPage/CreateUserPage/CreateUserPage"),
)
const WorkspaceBuildPage = lazy(
() => import("./pages/WorkspaceBuildPage/WorkspaceBuildPage"),
)
const WorkspacePage = lazy(() => import("./pages/WorkspacePage/WorkspacePage"))
const WorkspaceSchedulePage = lazy(
() => import("./pages/WorkspaceSchedulePage/WorkspaceSchedulePage"),
)
const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage"))
const CreateWorkspacePage = lazy(() => import("./pages/CreateWorkspacePage/CreateWorkspacePage"))
const CreateWorkspacePage = lazy(
() => import("./pages/CreateWorkspacePage/CreateWorkspacePage"),
)
const TemplatePage = lazy(() => import("./pages/TemplatePage/TemplatePage"))
export const AppRouter: FC = () => {
const xServices = useContext(XServiceContext)
const permissions = useSelector(xServices.authXService, selectPermissions)
const featureVisibility = useSelector(xServices.entitlementsXService, selectFeatureVisibility)
const featureVisibility = useSelector(
xServices.entitlementsXService,
selectFeatureVisibility,
)
return (
<Suspense fallback={<FullScreenLoader />}>
@ -142,7 +159,8 @@ export const AppRouter: FC = () => {
<AuthAndFrame>
<RequirePermission
isFeatureVisible={
featureVisibility[FeatureNames.AuditLog] && Boolean(permissions?.viewAuditLog)
featureVisibility[FeatureNames.AuditLog] &&
Boolean(permissions?.viewAuditLog)
}
>
<AuditPage />

View File

@ -6,7 +6,10 @@ import "./i18n"
// if this is a development build and the developer wants to inspect
// helpful to see realtime changes on the services
if (process.env.NODE_ENV === "development" && process.env.INSPECT_XSTATE === "true") {
if (
process.env.NODE_ENV === "development" &&
process.env.INSPECT_XSTATE === "true"
) {
// configure the XState inspector to open in a new tab
inspect({
url: "https://stately.ai/viz?inspect",

View File

@ -119,21 +119,39 @@ describe("api.ts", () => {
["/api/v2/workspaces", undefined, "/api/v2/workspaces"],
["/api/v2/workspaces", { q: "" }, "/api/v2/workspaces"],
["/api/v2/workspaces", { q: "owner:1" }, "/api/v2/workspaces?q=owner%3A1"],
[
"/api/v2/workspaces",
{ q: "owner:1" },
"/api/v2/workspaces?q=owner%3A1",
],
["/api/v2/workspaces", { q: "owner:me" }, "/api/v2/workspaces?q=owner%3Ame"],
])(`Workspaces - getURLWithSearchParams(%p, %p) returns %p`, (basePath, filter, expected) => {
[
"/api/v2/workspaces",
{ q: "owner:me" },
"/api/v2/workspaces?q=owner%3Ame",
],
])(
`Workspaces - getURLWithSearchParams(%p, %p) returns %p`,
(basePath, filter, expected) => {
expect(getURLWithSearchParams(basePath, filter)).toBe(expected)
})
},
)
})
describe("getURLWithSearchParams - users", () => {
it.each<[string, TypesGen.UsersRequest | undefined, string]>([
["/api/v2/users", undefined, "/api/v2/users"],
["/api/v2/users", { q: "status:active" }, "/api/v2/users?q=status%3Aactive"],
[
"/api/v2/users",
{ q: "status:active" },
"/api/v2/users?q=status%3Aactive",
],
["/api/v2/users", { q: "" }, "/api/v2/users"],
])(`Users - getURLWithSearchParams(%p, %p) returns %p`, (basePath, filter, expected) => {
])(
`Users - getURLWithSearchParams(%p, %p) returns %p`,
(basePath, filter, expected) => {
expect(getURLWithSearchParams(basePath, filter)).toBe(expected)
})
},
)
})
})

View File

@ -48,7 +48,8 @@ if (token !== null && token.getAttribute("content") !== null) {
axios.defaults.headers.common["X-CSRF-TOKEN"] = hardCodedCSRFCookie()
token.setAttribute("content", hardCodedCSRFCookie())
} else {
axios.defaults.headers.common["X-CSRF-TOKEN"] = token.getAttribute("content") ?? ""
axios.defaults.headers.common["X-CSRF-TOKEN"] =
token.getAttribute("content") ?? ""
}
} else {
// Do not write error logs if we are in a FE unit test.
@ -106,44 +107,65 @@ export const getUser = async (): Promise<TypesGen.User> => {
}
export const getAuthMethods = async (): Promise<TypesGen.AuthMethods> => {
const response = await axios.get<TypesGen.AuthMethods>("/api/v2/users/authmethods")
const response = await axios.get<TypesGen.AuthMethods>(
"/api/v2/users/authmethods",
)
return response.data
}
export const checkAuthorization = async (
params: TypesGen.AuthorizationRequest,
): Promise<TypesGen.AuthorizationResponse> => {
const response = await axios.post<TypesGen.AuthorizationResponse>(`/api/v2/authcheck`, params)
const response = await axios.post<TypesGen.AuthorizationResponse>(
`/api/v2/authcheck`,
params,
)
return response.data
}
export const getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => {
const response = await axios.post<TypesGen.GenerateAPIKeyResponse>("/api/v2/users/me/keys")
const response = await axios.post<TypesGen.GenerateAPIKeyResponse>(
"/api/v2/users/me/keys",
)
return response.data
}
export const getUsers = async (filter?: TypesGen.UsersRequest): Promise<TypesGen.User[]> => {
export const getUsers = async (
filter?: TypesGen.UsersRequest,
): Promise<TypesGen.User[]> => {
const url = getURLWithSearchParams("/api/v2/users", filter)
const response = await axios.get<TypesGen.User[]>(url)
return response.data
}
export const getOrganization = async (organizationId: string): Promise<TypesGen.Organization> => {
const response = await axios.get<TypesGen.Organization>(`/api/v2/organizations/${organizationId}`)
export const getOrganization = async (
organizationId: string,
): Promise<TypesGen.Organization> => {
const response = await axios.get<TypesGen.Organization>(
`/api/v2/organizations/${organizationId}`,
)
return response.data
}
export const getOrganizations = async (): Promise<TypesGen.Organization[]> => {
const response = await axios.get<TypesGen.Organization[]>("/api/v2/users/me/organizations")
const response = await axios.get<TypesGen.Organization[]>(
"/api/v2/users/me/organizations",
)
return response.data
}
export const getTemplate = async (templateId: string): Promise<TypesGen.Template> => {
const response = await axios.get<TypesGen.Template>(`/api/v2/templates/${templateId}`)
export const getTemplate = async (
templateId: string,
): Promise<TypesGen.Template> => {
const response = await axios.get<TypesGen.Template>(
`/api/v2/templates/${templateId}`,
)
return response.data
}
export const getTemplates = async (organizationId: 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`,
)
@ -160,7 +182,9 @@ export const getTemplateByName = async (
return response.data
}
export const getTemplateVersion = async (versionId: string): Promise<TypesGen.TemplateVersion> => {
export const getTemplateVersion = async (
versionId: string,
): Promise<TypesGen.TemplateVersion> => {
const response = await axios.get<TypesGen.TemplateVersion>(
`/api/v2/templateversions/${versionId}`,
)
@ -198,12 +222,19 @@ export const updateTemplateMeta = async (
templateId: string,
data: TypesGen.UpdateTemplateMeta,
): Promise<TypesGen.Template> => {
const response = await axios.patch<TypesGen.Template>(`/api/v2/templates/${templateId}`, data)
const response = await axios.patch<TypesGen.Template>(
`/api/v2/templates/${templateId}`,
data,
)
return response.data
}
export const deleteTemplate = async (templateId: string): Promise<TypesGen.Template> => {
const response = await axios.delete<TypesGen.Template>(`/api/v2/templates/${templateId}`)
export const deleteTemplate = async (
templateId: string,
): Promise<TypesGen.Template> => {
const response = await axios.delete<TypesGen.Template>(
`/api/v2/templates/${templateId}`,
)
return response.data
}
@ -211,9 +242,12 @@ export const getWorkspace = async (
workspaceId: string,
params?: TypesGen.WorkspaceOptions,
): Promise<TypesGen.Workspace> => {
const response = await axios.get<TypesGen.Workspace>(`/api/v2/workspaces/${workspaceId}`, {
const response = await axios.get<TypesGen.Workspace>(
`/api/v2/workspaces/${workspaceId}`,
{
params,
})
},
)
return response.data
}
@ -268,12 +302,18 @@ export const getWorkspaceByOwnerAndName = async (
const postWorkspaceBuild =
(transition: WorkspaceBuildTransition) =>
async (workspaceId: string, template_version_id?: string): Promise<TypesGen.WorkspaceBuild> => {
async (
workspaceId: string,
template_version_id?: string,
): Promise<TypesGen.WorkspaceBuild> => {
const payload = {
transition,
template_version_id,
}
const response = await axios.post(`/api/v2/workspaces/${workspaceId}/builds`, payload)
const response = await axios.post(
`/api/v2/workspaces/${workspaceId}/builds`,
payload,
)
return response.data
}
@ -284,11 +324,15 @@ export const deleteWorkspace = postWorkspaceBuild("delete")
export const cancelWorkspaceBuild = async (
workspaceBuildId: TypesGen.WorkspaceBuild["id"],
): Promise<Types.Message> => {
const response = await axios.patch(`/api/v2/workspacebuilds/${workspaceBuildId}/cancel`)
const response = await axios.patch(
`/api/v2/workspacebuilds/${workspaceBuildId}/cancel`,
)
return response.data
}
export const createUser = async (user: TypesGen.CreateUserRequest): Promise<TypesGen.User> => {
export const createUser = async (
user: TypesGen.CreateUserRequest,
): Promise<TypesGen.User> => {
const response = await axios.post<TypesGen.User>("/api/v2/users", user)
return response.data
}
@ -338,17 +382,27 @@ export const updateProfile = async (
return response.data
}
export const activateUser = async (userId: TypesGen.User["id"]): Promise<TypesGen.User> => {
const response = await axios.put<TypesGen.User>(`/api/v2/users/${userId}/status/activate`)
export const activateUser = async (
userId: TypesGen.User["id"],
): Promise<TypesGen.User> => {
const response = await axios.put<TypesGen.User>(
`/api/v2/users/${userId}/status/activate`,
)
return response.data
}
export const suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen.User> => {
const response = await axios.put<TypesGen.User>(`/api/v2/users/${userId}/status/suspend`)
export const suspendUser = async (
userId: TypesGen.User["id"],
): Promise<TypesGen.User> => {
const response = await axios.put<TypesGen.User>(
`/api/v2/users/${userId}/status/suspend`,
)
return response.data
}
export const deleteUser = async (userId: TypesGen.User["id"]): Promise<undefined> => {
export const deleteUser = async (
userId: TypesGen.User["id"],
): Promise<undefined> => {
return await axios.delete(`/api/v2/users/${userId}`)
}
@ -379,10 +433,15 @@ export const createFirstUser = async (
export const updateUserPassword = async (
userId: TypesGen.User["id"],
updatePassword: TypesGen.UpdateUserPasswordRequest,
): Promise<undefined> => axios.put(`/api/v2/users/${userId}/password`, updatePassword)
): Promise<undefined> =>
axios.put(`/api/v2/users/${userId}/password`, updatePassword)
export const getSiteRoles = async (): Promise<Array<TypesGen.AssignableRoles>> => {
const response = await axios.get<Array<TypesGen.AssignableRoles>>(`/api/v2/users/roles`)
export const getSiteRoles = async (): Promise<
Array<TypesGen.AssignableRoles>
> => {
const response = await axios.get<Array<TypesGen.AssignableRoles>>(
`/api/v2/users/roles`,
)
return response.data
}
@ -390,17 +449,28 @@ export const updateUserRoles = async (
roles: TypesGen.Role["name"][],
userId: TypesGen.User["id"],
): Promise<TypesGen.User> => {
const response = await axios.put<TypesGen.User>(`/api/v2/users/${userId}/roles`, { roles })
const response = await axios.put<TypesGen.User>(
`/api/v2/users/${userId}/roles`,
{ roles },
)
return response.data
}
export const getUserSSHKey = async (userId = "me"): Promise<TypesGen.GitSSHKey> => {
const response = await axios.get<TypesGen.GitSSHKey>(`/api/v2/users/${userId}/gitsshkey`)
export const getUserSSHKey = async (
userId = "me",
): Promise<TypesGen.GitSSHKey> => {
const response = await axios.get<TypesGen.GitSSHKey>(
`/api/v2/users/${userId}/gitsshkey`,
)
return response.data
}
export const regenerateUserSSHKey = async (userId = "me"): Promise<TypesGen.GitSSHKey> => {
const response = await axios.put<TypesGen.GitSSHKey>(`/api/v2/users/${userId}/gitsshkey`)
export const regenerateUserSSHKey = async (
userId = "me",
): Promise<TypesGen.GitSSHKey> => {
const response = await axios.put<TypesGen.GitSSHKey>(
`/api/v2/users/${userId}/gitsshkey`,
)
return response.data
}
@ -439,7 +509,9 @@ export const putWorkspaceExtension = async (
workspaceId: string,
newDeadline: dayjs.Dayjs,
): Promise<void> => {
await axios.put(`/api/v2/workspaces/${workspaceId}/extend`, { deadline: newDeadline })
await axios.put(`/api/v2/workspaces/${workspaceId}/extend`, {
deadline: newDeadline,
})
}
export const getEntitlements = async (): Promise<TypesGen.Entitlements> => {
@ -479,7 +551,9 @@ export const getAuditLogsCount = async (
if (options.q) {
searchParams.set("q", options.q)
}
const response = await axios.get(`/api/v2/audit/count?${searchParams.toString()}`)
const response = await axios.get(
`/api/v2/audit/count?${searchParams.toString()}`,
)
return response.data
}
@ -490,12 +564,15 @@ export const getTemplateDAUs = async (
return response.data
}
export const getApplicationsHost = async (): Promise<TypesGen.GetAppHostResponse> => {
export const getApplicationsHost =
async (): Promise<TypesGen.GetAppHostResponse> => {
const response = await axios.get(`/api/v2/applications/host`)
return response.data
}
export const getWorkspaceQuota = async (userID: string): Promise<TypesGen.WorkspaceQuota> => {
export const getWorkspaceQuota = async (
userID: string,
): Promise<TypesGen.WorkspaceQuota> => {
const response = await axios.get(`/api/v2/workspace-quota/${userID}`)
return response.data
}

View File

@ -1,4 +1,8 @@
import { getValidationErrorMessage, isApiError, mapApiErrorToFieldErrors } from "./errors"
import {
getValidationErrorMessage,
isApiError,
mapApiErrorToFieldErrors,
} from "./errors"
describe("isApiError", () => {
it("returns true when the object is an API Error", () => {
@ -8,7 +12,9 @@ describe("isApiError", () => {
response: {
data: {
message: "Invalid entry",
errors: [{ detail: "Username is already in use", field: "username" }],
errors: [
{ detail: "Username is already in use", field: "username" },
],
},
},
}),
@ -29,7 +35,9 @@ describe("mapApiErrorToFieldErrors", () => {
expect(
mapApiErrorToFieldErrors({
message: "Invalid entry",
validations: [{ detail: "Username is already in use", field: "username" }],
validations: [
{ detail: "Username is already in use", field: "username" },
],
}),
).toEqual({
username: "Username is already in use",

View File

@ -19,7 +19,9 @@ export interface ApiErrorResponse {
validations?: FieldError[]
}
export type ApiError = AxiosError<ApiErrorResponse> & { response: AxiosResponse<ApiErrorResponse> }
export type ApiError = AxiosError<ApiErrorResponse> & {
response: AxiosResponse<ApiErrorResponse>
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
export const isApiError = (err: any): err is ApiError => {
@ -47,12 +49,15 @@ export const isApiError = (err: any): err is ApiError => {
export const hasApiFieldErrors = (error: ApiError): boolean =>
Array.isArray(error.response.data.validations)
export const mapApiErrorToFieldErrors = (apiErrorResponse: ApiErrorResponse): FieldErrors => {
export const mapApiErrorToFieldErrors = (
apiErrorResponse: ApiErrorResponse,
): FieldErrors => {
const result: FieldErrors = {}
if (apiErrorResponse.validations) {
for (const error of apiErrorResponse.validations) {
result[error.field] = error.detail || Language.errorsByCode.defaultErrorCode
result[error.field] =
error.detail || Language.errorsByCode.defaultErrorCode
}
}
@ -81,11 +86,21 @@ export const getErrorMessage = (
* @returns a combined validation error message if the error is an ApiError
* and contains validation messages for different form fields.
*/
export const getValidationErrorMessage = (error: Error | ApiError | unknown): string => {
export const getValidationErrorMessage = (
error: Error | ApiError | unknown,
): string => {
const validationErrors =
isApiError(error) && error.response.data.validations ? error.response.data.validations : []
isApiError(error) && error.response.data.validations
? error.response.data.validations
: []
return validationErrors.map((error) => error.detail).join("\n")
}
export const getErrorDetail = (error: Error | ApiError | unknown): string | undefined | null =>
isApiError(error) ? error.response.data.detail : error instanceof Error ? error.stack : null
export const getErrorDetail = (
error: Error | ApiError | unknown,
): string | undefined | null =>
isApiError(error)
? error.response.data.detail
: error instanceof Error
? error.stack
: null

View File

@ -707,7 +707,10 @@ export type LogSource = "provisioner" | "provisioner_daemon"
export type LoginType = "github" | "oidc" | "password" | "token"
// From codersdk/parameters.go
export type ParameterDestinationScheme = "environment_variable" | "none" | "provisioner_variable"
export type ParameterDestinationScheme =
| "environment_variable"
| "none"
| "provisioner_variable"
// From codersdk/parameters.go
export type ParameterScope = "import_job" | "template" | "workspace"
@ -753,7 +756,11 @@ export type UserStatus = "active" | "suspended"
export type WorkspaceAgentStatus = "connected" | "connecting" | "disconnected"
// From codersdk/workspaceapps.go
export type WorkspaceAppHealth = "disabled" | "healthy" | "initializing" | "unhealthy"
export type WorkspaceAppHealth =
| "disabled"
| "healthy"
| "initializing"
| "unhealthy"
// From codersdk/workspacebuilds.go
export type WorkspaceStatus =

View File

@ -32,7 +32,10 @@ export const AlertBanner: FC<AlertBannerProps> = ({
// if an error is passed in, display that error, otherwise
// display the text passed in, e.g. warning text
const alertMessage = getErrorMessage(error, text ?? t("warningsAndErrors.somethingWentWrong"))
const alertMessage = getErrorMessage(
error,
text ?? t("warningsAndErrors.somethingWentWrong"),
)
// if we have an error, check if there's detail to display
const detail = error ? getErrorDetail(error) : undefined

View File

@ -5,7 +5,10 @@ import Button from "@material-ui/core/Button"
import RefreshIcon from "@material-ui/icons/Refresh"
import { useTranslation } from "react-i18next"
type AlertBannerCtasProps = Pick<AlertBannerProps, "actions" | "dismissible" | "retry"> & {
type AlertBannerCtasProps = Pick<
AlertBannerProps,
"actions" | "dismissible" | "retry"
> & {
setOpen: (arg0: boolean) => void
}
@ -20,12 +23,18 @@ export const AlertBannerCtas: FC<AlertBannerCtasProps> = ({
return (
<Stack direction="row">
{/* CTAs passed in by the consumer */}
{actions.length > 0 && actions.map((action) => <div key={String(action)}>{action}</div>)}
{actions.length > 0 &&
actions.map((action) => <div key={String(action)}>{action}</div>)}
{/* retry CTA */}
{retry && (
<div>
<Button size="small" onClick={retry} startIcon={<RefreshIcon />} variant="outlined">
<Button
size="small"
onClick={retry}
startIcon={<RefreshIcon />}
variant="outlined"
>
{t("ctas.retry")}
</Button>
</div>

View File

@ -4,13 +4,26 @@ import { colors } from "theme/colors"
import { Severity } from "./alertTypes"
import { ReactElement } from "react"
export const severityConstants: Record<Severity, { color: string; icon: ReactElement }> = {
export const severityConstants: Record<
Severity,
{ color: string; icon: ReactElement }
> = {
warning: {
color: colors.orange[7],
icon: <ReportProblemOutlinedIcon fontSize="small" style={{ color: colors.orange[7] }} />,
icon: (
<ReportProblemOutlinedIcon
fontSize="small"
style={{ color: colors.orange[7] }}
/>
),
},
error: {
color: colors.red[7],
icon: <ErrorOutlineOutlinedIcon fontSize="small" style={{ color: colors.red[7] }} />,
icon: (
<ErrorOutlineOutlinedIcon
fontSize="small"
style={{ color: colors.red[7] }}
/>
),
},
}

View File

@ -10,7 +10,8 @@ import * as TypesGen from "../../api/typesGenerated"
import { generateRandomString } from "../../util/random"
export const Language = {
appTitle: (appName: string, identifier: string): string => `${appName} - ${identifier}`,
appTitle: (appName: string, identifier: string): string =>
`${appName} - ${identifier}`,
}
export interface AppLinkProps {
@ -40,7 +41,9 @@ export const AppLink: FC<PropsWithChildren<AppLinkProps>> = ({
// The backend redirects if the trailing slash isn't included, so we add it
// here to avoid extra roundtrips.
let href = `/@${username}/${workspaceName}.${agentName}/apps/${encodeURIComponent(appName)}/`
let href = `/@${username}/${workspaceName}.${agentName}/apps/${encodeURIComponent(
appName,
)}/`
if (appCommand) {
href = `/@${username}/${workspaceName}.${agentName}/terminal?command=${encodeURIComponent(
appCommand,
@ -52,7 +55,11 @@ export const AppLink: FC<PropsWithChildren<AppLinkProps>> = ({
}
let canClick = true
let icon = appIcon ? <img alt={`${appName} Icon`} src={appIcon} /> : <ComputerIcon />
let icon = appIcon ? (
<img alt={`${appName} Icon`} src={appIcon} />
) : (
<ComputerIcon />
)
let tooltip = ""
if (health === "initializing") {
canClick = false
@ -71,7 +78,12 @@ export const AppLink: FC<PropsWithChildren<AppLinkProps>> = ({
}
const button = (
<Button size="small" startIcon={icon} className={styles.button} disabled={!canClick}>
<Button
size="small"
startIcon={icon}
className={styles.button}
disabled={!canClick}
>
{appName}
</Button>
)

View File

@ -21,7 +21,9 @@ const getDiffValue = (value: unknown): string => {
return value.toString()
}
export const AuditLogDiff: React.FC<{ diff: AuditLog["diff"] }> = ({ diff }) => {
export const AuditLogDiff: React.FC<{ diff: AuditLog["diff"] }> = ({
diff,
}) => {
const styles = useStyles()
const diffEntries = Object.entries(diff)
@ -34,7 +36,12 @@ export const AuditLogDiff: React.FC<{ diff: AuditLog["diff"] }> = ({ diff }) =>
<div className={styles.diffIcon}>-</div>
<div>
{attrName}:{" "}
<span className={combineClasses([styles.diffValue, styles.diffValueOld])}>
<span
className={combineClasses([
styles.diffValue,
styles.diffValueOld,
])}
>
{getDiffValue(valueDiff.old)}
</span>
</div>
@ -48,7 +55,12 @@ export const AuditLogDiff: React.FC<{ diff: AuditLog["diff"] }> = ({ diff }) =>
<div className={styles.diffIcon}>+</div>
<div>
{attrName}:{" "}
<span className={combineClasses([styles.diffValue, styles.diffValueNew])}>
<span
className={combineClasses([
styles.diffValue,
styles.diffValueNew,
])}
>
{getDiffValue(valueDiff.new)}
</span>
</div>

View File

@ -3,7 +3,10 @@ import { makeStyles } from "@material-ui/core/styles"
import TableCell from "@material-ui/core/TableCell"
import TableRow from "@material-ui/core/TableRow"
import { AuditLog } from "api/typesGenerated"
import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows"
import {
CloseDropdown,
OpenDropdown,
} from "components/DropdownArrows/DropdownArrows"
import { Pill } from "components/Pill/Pill"
import { Stack } from "components/Stack/Stack"
import { UserAvatar } from "components/UserAvatar/UserAvatar"
@ -13,7 +16,9 @@ import userAgentParser from "ua-parser-js"
import { createDayString } from "util/createDayString"
import { AuditLogDiff } from "./AuditLogDiff"
const pillTypeByHttpStatus = (httpStatus: number): ComponentProps<typeof Pill>["type"] => {
const pillTypeByHttpStatus = (
httpStatus: number,
): ComponentProps<typeof Pill>["type"] => {
if (httpStatus >= 300 && httpStatus < 500) {
return "warning"
}
@ -47,7 +52,9 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
const shouldDisplayDiff = diffs.length > 0
const { os, browser } = userAgentParser(auditLog.user_agent)
const notAvailableLabel = "Not available"
const displayBrowserInfo = browser.name ? `${browser.name} ${browser.version}` : notAvailableLabel
const displayBrowserInfo = browser.name
? `${browser.name} ${browser.version}`
: notAvailableLabel
const toggle = () => {
if (shouldDisplayDiff) {
@ -85,9 +92,13 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
<div>
<span
className={styles.auditLogResume}
dangerouslySetInnerHTML={{ __html: readableActionMessage(auditLog) }}
dangerouslySetInnerHTML={{
__html: readableActionMessage(auditLog),
}}
/>
<span className={styles.auditLogTime}>{createDayString(auditLog.time)}</span>
<span className={styles.auditLogTime}>
{createDayString(auditLog.time)}
</span>
</div>
</Stack>
@ -101,7 +112,11 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
type={pillTypeByHttpStatus(auditLog.status_code)}
text={auditLog.status_code.toString()}
/>
<Stack direction="row" alignItems="center" className={styles.auditLogExtraInfo}>
<Stack
direction="row"
alignItems="center"
className={styles.auditLogExtraInfo}
>
<div>
<strong>IP</strong> {auditLog.ip ?? notAvailableLabel}
</div>
@ -115,7 +130,11 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
</Stack>
</Stack>
<div className={shouldDisplayDiff ? undefined : styles.disabledDropdownIcon}>
<div
className={
shouldDisplayDiff ? undefined : styles.disabledDropdownIcon
}
>
{isDiffOpen ? <CloseDropdown /> : <OpenDropdown />}
</div>
</Stack>

View File

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

View File

@ -38,14 +38,22 @@ export const AvatarData: FC<PropsWithChildren<AvatarDataProps>> = ({
{link ? (
<Link to={link} underline="none" component={RouterLink}>
<TableCellData>
<TableCellDataPrimary highlight={highlightTitle}>{title}</TableCellDataPrimary>
{subtitle && <TableCellDataSecondary>{subtitle}</TableCellDataSecondary>}
<TableCellDataPrimary highlight={highlightTitle}>
{title}
</TableCellDataPrimary>
{subtitle && (
<TableCellDataSecondary>{subtitle}</TableCellDataSecondary>
)}
</TableCellData>
</Link>
) : (
<TableCellData>
<TableCellDataPrimary highlight={highlightTitle}>{title}</TableCellDataPrimary>
{subtitle && <TableCellDataSecondary>{subtitle}</TableCellDataSecondary>}
<TableCellDataPrimary highlight={highlightTitle}>
{title}
</TableCellDataPrimary>
{subtitle && (
<TableCellDataSecondary>{subtitle}</TableCellDataSecondary>
)}
</TableCellData>
)}
</div>

View File

@ -26,15 +26,9 @@ interface BorderedMenuRowProps {
onClick?: () => void
}
export const BorderedMenuRow: FC<React.PropsWithChildren<BorderedMenuRowProps>> = ({
active,
description,
Icon,
path,
title,
variant,
onClick,
}) => {
export const BorderedMenuRow: FC<
React.PropsWithChildren<BorderedMenuRowProps>
> = ({ active, description, Icon, path, title, variant, onClick }) => {
const styles = useStyles()
return (
@ -53,7 +47,11 @@ export const BorderedMenuRow: FC<React.PropsWithChildren<BorderedMenuRowProps>>
</div>
{description && (
<Typography className={styles.description} color="textSecondary" variant="caption">
<Typography
className={styles.description}
color="textSecondary"
variant="caption"
>
{ellipsizeText(description)}
</Typography>
)}

View File

@ -11,7 +11,10 @@ import useTheme from "@material-ui/styles/useTheme"
import { FC } from "react"
import { useNavigate, useParams } from "react-router-dom"
import * as TypesGen from "../../api/typesGenerated"
import { displayWorkspaceBuildDuration, getDisplayWorkspaceBuildStatus } from "../../util/workspace"
import {
displayWorkspaceBuildDuration,
getDisplayWorkspaceBuildStatus,
} from "../../util/workspace"
import { EmptyState } from "../EmptyState/EmptyState"
import { TableCellLink } from "../TableCellLink/TableCellLink"
import { TableLoader } from "../TableLoader/TableLoader"
@ -72,7 +75,9 @@ export const BuildsTable: FC<React.PropsWithChildren<BuildsTableProps>> = ({
}}
className={styles.clickableTableRow}
>
<TableCellLink to={buildPageLink}>{build.transition}</TableCellLink>
<TableCellLink to={buildPageLink}>
{build.transition}
</TableCellLink>
<TableCellLink to={buildPageLink}>
<span style={{ color: theme.palette.text.secondary }}>
{displayWorkspaceBuildDuration(build)}
@ -84,7 +89,10 @@ export const BuildsTable: FC<React.PropsWithChildren<BuildsTableProps>> = ({
</span>
</TableCellLink>
<TableCellLink to={buildPageLink}>
<span style={{ color: status.color }} className={styles.status}>
<span
style={{ color: status.color }}
className={styles.status}
>
{status.status}
</span>
</TableCellLink>

View File

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

View File

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

View File

@ -11,7 +11,9 @@ export interface CondProps {
* @param condition boolean expression indicating whether the child should be rendered, or undefined
* @returns child. Note that Cond alone does not enforce the condition; it should be used inside ChooseOne.
*/
export const Cond = ({ children }: PropsWithChildren<CondProps>): JSX.Element => {
export const Cond = ({
children,
}: PropsWithChildren<CondProps>): JSX.Element => {
return <>{children}</>
}
@ -22,7 +24,9 @@ export const Cond = ({ children }: PropsWithChildren<CondProps>): JSX.Element =>
* @returns one of its children, or null if there are no children
* @throws an error if its last child has a condition prop, or any non-final children do not have a condition prop
*/
export const ChooseOne = ({ children }: PropsWithChildren): JSX.Element | null => {
export const ChooseOne = ({
children,
}: PropsWithChildren): JSX.Element | null => {
const childArray = Children.toArray(children) as JSX.Element[]
if (childArray.length === 0) {
return null
@ -35,7 +39,9 @@ export const ChooseOne = ({ children }: PropsWithChildren): JSX.Element | null =
)
}
if (conditionedOptions.some((cond) => cond.props.condition === undefined)) {
throw new Error("A non-final Cond in a ChooseOne does not have a condition prop.")
throw new Error(
"A non-final Cond in a ChooseOne does not have a condition prop.",
)
}
const chosen = conditionedOptions.find((child) => child.props.condition)
return chosen ?? defaultCase

View File

@ -6,7 +6,9 @@ export default {
component: Maybe,
}
const Template: Story<MaybeProps> = (args: MaybeProps) => <Maybe {...args}>Now you see me</Maybe>
const Template: Story<MaybeProps> = (args: MaybeProps) => (
<Maybe {...args}>Now you see me</Maybe>
)
export const ConditionIsTrue = Template.bind({})
ConditionIsTrue.args = {

View File

@ -53,7 +53,9 @@ export const CopyButton: React.FC<React.PropsWithChildren<CopyButtonProps>> = ({
setIsCopied(false)
}, 1000)
} else {
const wrappedErr = new Error("copyToClipboard: failed to copy text to clipboard")
const wrappedErr = new Error(
"copyToClipboard: failed to copy text to clipboard",
)
if (err instanceof Error) {
wrappedErr.stack = err.stack
}
@ -64,7 +66,9 @@ export const CopyButton: React.FC<React.PropsWithChildren<CopyButtonProps>> = ({
return (
<Tooltip title={tooltipTitle} placement="top">
<div className={combineClasses([styles.copyButtonWrapper, wrapperClassName])}>
<div
className={combineClasses([styles.copyButtonWrapper, wrapperClassName])}
>
<IconButton
className={combineClasses([styles.copyButton, buttonClassName])}
onClick={copyToClipboard}

View File

@ -4,7 +4,11 @@ import { FormikContextType, FormikErrors, useFormik } from "formik"
import { FC } from "react"
import * as Yup from "yup"
import * as TypesGen from "../../api/typesGenerated"
import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils"
import {
getFormHelpers,
nameValidator,
onChangeTrimmed,
} from "../../util/formUtils"
import { FormFooter } from "../FormFooter/FormFooter"
import { FullPageForm } from "../FullPageForm/FullPageForm"
import { Stack } from "../Stack/Stack"
@ -30,21 +34,19 @@ export interface CreateUserFormProps {
}
const validationSchema = Yup.object({
email: Yup.string().trim().email(Language.emailInvalid).required(Language.emailRequired),
email: Yup.string()
.trim()
.email(Language.emailInvalid)
.required(Language.emailRequired),
password: Yup.string().required(Language.passwordRequired),
username: nameValidator(Language.usernameLabel),
})
export const CreateUserForm: FC<React.PropsWithChildren<CreateUserFormProps>> = ({
onSubmit,
onCancel,
formErrors,
isLoading,
error,
myOrgId,
}) => {
const form: FormikContextType<TypesGen.CreateUserRequest> = useFormik<TypesGen.CreateUserRequest>(
{
export const CreateUserForm: FC<
React.PropsWithChildren<CreateUserFormProps>
> = ({ onSubmit, onCancel, formErrors, isLoading, error, myOrgId }) => {
const form: FormikContextType<TypesGen.CreateUserRequest> =
useFormik<TypesGen.CreateUserRequest>({
initialValues: {
email: "",
password: "",
@ -53,9 +55,11 @@ export const CreateUserForm: FC<React.PropsWithChildren<CreateUserFormProps>> =
},
validationSchema,
onSubmit,
},
})
const getFieldHelpers = getFormHelpers<TypesGen.CreateUserRequest>(
form,
formErrors,
)
const getFieldHelpers = getFormHelpers<TypesGen.CreateUserRequest>(form, formErrors)
return (
<FullPageForm title="Create user" onCancel={onCancel}>

View File

@ -21,7 +21,9 @@ export default {
},
} as ComponentMeta<typeof ConfirmDialog>
const Template: Story<ConfirmDialogProps> = (args) => <ConfirmDialog {...args} />
const Template: Story<ConfirmDialogProps> = (args) => (
<ConfirmDialog {...args} />
)
export const DeleteDialog = Template.bind({})
DeleteDialog.args = {

View File

@ -2,7 +2,11 @@ import DialogActions from "@material-ui/core/DialogActions"
import { alpha, makeStyles } from "@material-ui/core/styles"
import Typography from "@material-ui/core/Typography"
import React, { ReactNode } from "react"
import { Dialog, DialogActionButtons, DialogActionButtonsProps } from "../Dialog"
import {
Dialog,
DialogActionButtons,
DialogActionButtonsProps,
} from "../Dialog"
import { ConfirmDialogType } from "../types"
interface ConfirmDialogTypeConfig {
@ -10,7 +14,10 @@ interface ConfirmDialogTypeConfig {
hideCancel: boolean
}
const CONFIRM_DIALOG_DEFAULTS: Record<ConfirmDialogType, ConfirmDialogTypeConfig> = {
const CONFIRM_DIALOG_DEFAULTS: Record<
ConfirmDialogType,
ConfirmDialogTypeConfig
> = {
delete: {
confirmText: "Delete",
hideCancel: false,
@ -26,7 +33,10 @@ const CONFIRM_DIALOG_DEFAULTS: Record<ConfirmDialogType, ConfirmDialogTypeConfig
}
export interface ConfirmDialogProps
extends Omit<DialogActionButtonsProps, "color" | "confirmDialog" | "onCancel"> {
extends Omit<
DialogActionButtonsProps,
"color" | "confirmDialog" | "onCancel"
> {
readonly description?: React.ReactNode
/**
* hideCancel hides the cancel button when set true, and shows the cancel
@ -78,7 +88,9 @@ const useStyles = makeStyles((theme) => ({
* Quick-use version of the Dialog component with slightly alternative styles,
* great to use for dialogs that don't have any interaction beyond yes / no.
*/
export const ConfirmDialog: React.FC<React.PropsWithChildren<ConfirmDialogProps>> = ({
export const ConfirmDialog: React.FC<
React.PropsWithChildren<ConfirmDialogProps>
> = ({
cancelText,
confirmLoading,
confirmText,
@ -100,7 +112,12 @@ export const ConfirmDialog: React.FC<React.PropsWithChildren<ConfirmDialogProps>
}
return (
<Dialog className={styles.dialogWrapper} maxWidth="sm" onClose={onClose} open={open}>
<Dialog
className={styles.dialogWrapper}
maxWidth="sm"
onClose={onClose}
open={open}
>
<div className={styles.dialogContent}>
<Typography className={styles.titleText} variant="h3">
{title}

View File

@ -22,7 +22,8 @@ export default {
defaultValue: "MyFoo",
},
info: {
defaultValue: "Here's some info about the foo so you know you're deleting the right one.",
defaultValue:
"Here's some info about the foo so you know you're deleting the right one.",
},
},
} as ComponentMeta<typeof DeleteDialog>

View File

@ -30,7 +30,10 @@ describe("DeleteDialog", () => {
name="MyTemplate"
/>,
)
const labelText = t("deleteDialog.confirmLabel", { ns: "common", entity: "template" })
const labelText = t("deleteDialog.confirmLabel", {
ns: "common",
entity: "template",
})
const textField = screen.getByLabelText(labelText)
await userEvent.type(textField, "MyTemplateWrong")
const confirmButton = screen.getByRole("button", { name: "Delete" })
@ -48,7 +51,10 @@ describe("DeleteDialog", () => {
name="MyTemplate"
/>,
)
const labelText = t("deleteDialog.confirmLabel", { ns: "common", entity: "template" })
const labelText = t("deleteDialog.confirmLabel", {
ns: "common",
entity: "template",
})
const textField = screen.getByLabelText(labelText)
await userEvent.type(textField, "MyTemplate")
const confirmButton = screen.getByRole("button", { name: "Delete" })

View File

@ -18,15 +18,9 @@ export interface DeleteDialogProps {
confirmLoading?: boolean
}
export const DeleteDialog: React.FC<React.PropsWithChildren<DeleteDialogProps>> = ({
isOpen,
onCancel,
onConfirm,
entity,
info,
name,
confirmLoading,
}) => {
export const DeleteDialog: React.FC<
React.PropsWithChildren<DeleteDialogProps>
> = ({ isOpen, onCancel, onConfirm, entity, info, name, confirmLoading }) => {
const styles = useStyles()
const { t } = useTranslation("common")
const [nameValue, setNameValue] = useState("")
@ -52,7 +46,9 @@ export const DeleteDialog: React.FC<React.PropsWithChildren<DeleteDialogProps>>
label={t("deleteDialog.confirmLabel", { entity })}
/>
<Maybe condition={nameValue.length > 0 && !confirmed}>
<FormHelperText error>{t("deleteDialog.incorrectName", { entity })}</FormHelperText>
<FormHelperText error>
{t("deleteDialog.incorrectName", { entity })}
</FormHelperText>
</Maybe>
</Stack>
</>

View File

@ -1,10 +1,15 @@
import MuiDialog, { DialogProps as MuiDialogProps } from "@material-ui/core/Dialog"
import MuiDialog, {
DialogProps as MuiDialogProps,
} from "@material-ui/core/Dialog"
import MuiDialogTitle from "@material-ui/core/DialogTitle"
import { alpha, darken, lighten, makeStyles } from "@material-ui/core/styles"
import SvgIcon from "@material-ui/core/SvgIcon"
import * as React from "react"
import { combineClasses } from "../../util/combineClasses"
import { LoadingButton, LoadingButtonProps } from "../LoadingButton/LoadingButton"
import {
LoadingButton,
LoadingButtonProps,
} from "../LoadingButton/LoadingButton"
import { ConfirmDialogType } from "./types"
export interface DialogTitleProps {
@ -19,7 +24,11 @@ export interface DialogTitleProps {
/**
* Override of Material UI's DialogTitle that allows for a supertitle and background icon
*/
export const DialogTitle: React.FC<DialogTitleProps> = ({ title, icon: Icon, superTitle }) => {
export const DialogTitle: React.FC<DialogTitleProps> = ({
title,
icon: Icon,
superTitle,
}) => {
const styles = useTitleStyles()
return (
<MuiDialogTitle disableTypography>
@ -164,7 +173,9 @@ const useButtonStyles = makeStyles((theme) => ({
},
confirmDialogCancelButton: (props: StyleProps) => {
const color =
props.type === "info" ? theme.palette.primary.contrastText : theme.palette.error.contrastText
props.type === "info"
? theme.palette.primary.contrastText
: theme.palette.error.contrastText
return {
background: alpha(color, 0.15),
color,
@ -222,7 +233,10 @@ const useButtonStyles = makeStyles((theme) => ({
color: theme.palette.error.main,
borderColor: theme.palette.error.main,
"&:hover": {
backgroundColor: alpha(theme.palette.error.main, theme.palette.action.hoverOpacity),
backgroundColor: alpha(
theme.palette.error.main,
theme.palette.action.hoverOpacity,
),
"@media (hover: none)": {
backgroundColor: "transparent",
},
@ -239,7 +253,10 @@ const useButtonStyles = makeStyles((theme) => ({
"&.MuiButton-text": {
color: theme.palette.error.main,
"&:hover": {
backgroundColor: alpha(theme.palette.error.main, theme.palette.action.hoverOpacity),
backgroundColor: alpha(
theme.palette.error.main,
theme.palette.action.hoverOpacity,
),
"@media (hover: none)": {
backgroundColor: "transparent",
},
@ -272,7 +289,10 @@ const useButtonStyles = makeStyles((theme) => ({
color: theme.palette.success.main,
borderColor: theme.palette.success.main,
"&:hover": {
backgroundColor: alpha(theme.palette.success.main, theme.palette.action.hoverOpacity),
backgroundColor: alpha(
theme.palette.success.main,
theme.palette.action.hoverOpacity,
),
"@media (hover: none)": {
backgroundColor: "transparent",
},
@ -289,7 +309,10 @@ const useButtonStyles = makeStyles((theme) => ({
"&.MuiButton-text": {
color: theme.palette.success.main,
"&:hover": {
backgroundColor: alpha(theme.palette.success.main, theme.palette.action.hoverOpacity),
backgroundColor: alpha(
theme.palette.success.main,
theme.palette.action.hoverOpacity,
),
"@media (hover: none)": {
backgroundColor: "transparent",
},

View File

@ -1,6 +1,9 @@
import { Story } from "@storybook/react"
import { MockUser } from "../../../testHelpers/renderHelpers"
import { ResetPasswordDialog, ResetPasswordDialogProps } from "./ResetPasswordDialog"
import {
ResetPasswordDialog,
ResetPasswordDialogProps,
} from "./ResetPasswordDialog"
export default {
title: "components/Dialogs/ResetPasswordDialog",
@ -11,9 +14,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

@ -24,19 +24,16 @@ export const Language = {
confirmText: "Reset password",
}
export const ResetPasswordDialog: FC<React.PropsWithChildren<ResetPasswordDialogProps>> = ({
open,
onClose,
onConfirm,
user,
newPassword,
loading,
}) => {
export const ResetPasswordDialog: FC<
React.PropsWithChildren<ResetPasswordDialogProps>
> = ({ open, onClose, onConfirm, user, newPassword, loading }) => {
const styles = useStyles()
const description = (
<>
<DialogContentText variant="subtitle2">{Language.message(user?.username)}</DialogContentText>
<DialogContentText variant="subtitle2">
{Language.message(user?.username)}
</DialogContentText>
<DialogContentText component="div" className={styles.codeBlock}>
<CodeExample code={newPassword ?? ""} className={styles.codeExample} />
</DialogContentText>

View File

@ -22,7 +22,12 @@ interface ArrowProps {
export const OpenDropdown: FC<ArrowProps> = ({ margin = true, color }) => {
const styles = useStyles({ margin, color })
return <KeyboardArrowDown aria-label="open-dropdown" className={styles.arrowIcon} />
return (
<KeyboardArrowDown
aria-label="open-dropdown"
className={styles.arrowIcon}
/>
)
}
export const CloseDropdown: FC<ArrowProps> = ({ margin = true, color }) => {

View File

@ -16,18 +16,26 @@ interface WorkspaceAction {
handleAction: () => void
}
export const UpdateButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({ handleAction }) => {
export const UpdateButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
handleAction,
}) => {
const styles = useStyles()
const { t } = useTranslation("workspacePage")
return (
<Button className={styles.actionButton} startIcon={<CloudQueueIcon />} onClick={handleAction}>
<Button
className={styles.actionButton}
startIcon={<CloudQueueIcon />}
onClick={handleAction}
>
{t("actionButton.update")}
</Button>
)
}
export const StartButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({ handleAction }) => {
export const StartButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
handleAction,
}) => {
const styles = useStyles()
const { t } = useTranslation("workspacePage")
@ -41,7 +49,9 @@ export const StartButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({ hand
)
}
export const StopButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({ handleAction }) => {
export const StopButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
handleAction,
}) => {
const styles = useStyles()
const { t } = useTranslation("workspacePage")
@ -55,7 +65,9 @@ export const StopButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({ handl
)
}
export const DeleteButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({ handleAction }) => {
export const DeleteButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
handleAction,
}) => {
const styles = useStyles()
const { t } = useTranslation("workspacePage")
@ -69,7 +81,9 @@ export const DeleteButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({ han
)
}
export const CancelButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({ handleAction }) => {
export const CancelButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
handleAction,
}) => {
const styles = useStyles()
// this is an icon button, so it's important to include an aria label
@ -94,7 +108,9 @@ interface DisabledProps {
label: string
}
export const DisabledButton: FC<React.PropsWithChildren<DisabledProps>> = ({ label }) => {
export const DisabledButton: FC<React.PropsWithChildren<DisabledProps>> = ({
label,
}) => {
const styles = useStyles()
return (
@ -108,7 +124,9 @@ interface LoadingProps {
label: string
}
export const ActionLoadingButton: FC<React.PropsWithChildren<LoadingProps>> = ({ label }) => {
export const ActionLoadingButton: FC<React.PropsWithChildren<LoadingProps>> = ({
label,
}) => {
const styles = useStyles()
return (
<LoadingButton

View File

@ -1,6 +1,11 @@
import { action } from "@storybook/addon-actions"
import { Story } from "@storybook/react"
import { DeleteButton, DisabledButton, StartButton, UpdateButton } from "./ActionCtas"
import {
DeleteButton,
DisabledButton,
StartButton,
UpdateButton,
} from "./ActionCtas"
import { DropdownButton, DropdownButtonProps } from "./DropdownButton"
export default {
@ -8,14 +13,22 @@ export default {
component: DropdownButton,
}
const Template: Story<DropdownButtonProps> = (args) => <DropdownButton {...args} />
const Template: Story<DropdownButtonProps> = (args) => (
<DropdownButton {...args} />
)
export const WithDropdown = Template.bind({})
WithDropdown.args = {
primaryAction: <StartButton handleAction={action("start")} />,
secondaryActions: [
{ action: "update", button: <UpdateButton handleAction={action("update")} /> },
{ action: "delete", button: <DeleteButton handleAction={action("delete")} /> },
{
action: "update",
button: <UpdateButton handleAction={action("update")} />,
},
{
action: "delete",
button: <DeleteButton handleAction={action("delete")} />,
},
],
canCancel: false,
}

View File

@ -1,7 +1,10 @@
import Button from "@material-ui/core/Button"
import Popover from "@material-ui/core/Popover"
import { makeStyles, useTheme } from "@material-ui/core/styles"
import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows"
import {
CloseDropdown,
OpenDropdown,
} from "components/DropdownArrows/DropdownArrows"
import { DropdownContent } from "components/DropdownButton/DropdownContent/DropdownContent"
import { FC, ReactNode, useRef, useState } from "react"
import { CancelButton } from "./ActionCtas"
@ -51,7 +54,9 @@ export const DropdownButton: FC<DropdownButtonProps> = ({
{isOpen ? (
<CloseDropdown />
) : (
<OpenDropdown color={canOpen ? undefined : theme.palette.action.disabled} />
<OpenDropdown
color={canOpen ? undefined : theme.palette.action.disabled}
/>
)}
</Button>
<Popover
@ -105,6 +110,8 @@ const useStyles = makeStyles((theme) => ({
},
},
popoverPaper: {
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px ${theme.spacing(1)}px`,
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px ${theme.spacing(
1,
)}px`,
},
}))

View File

@ -6,9 +6,9 @@ export interface DropdownContentProps {
}
/* secondary workspace CTAs */
export const DropdownContent: FC<React.PropsWithChildren<DropdownContentProps>> = ({
secondaryActions,
}) => {
export const DropdownContent: FC<
React.PropsWithChildren<DropdownContentProps>
> = ({ secondaryActions }) => {
const styles = useStyles()
return (

View File

@ -13,7 +13,9 @@ describe("EmptyState", () => {
it("renders description text", async () => {
// When
render(<EmptyState message="Hello, world" description="Friendly greeting" />)
render(
<EmptyState message="Hello, world" description="Friendly greeting" />,
)
// Then
await screen.findByText("Hello, world")

View File

@ -23,8 +23,17 @@ export interface EmptyStateProps {
* EmptyState's props extend the [Material UI Box component](https://material-ui.com/components/box/)
* that you can directly pass props through to to customize the shape and layout of it.
*/
export const EmptyState: FC<React.PropsWithChildren<EmptyStateProps>> = (props) => {
const { message, description, cta, descriptionClassName, className, ...boxProps } = props
export const EmptyState: FC<React.PropsWithChildren<EmptyStateProps>> = (
props,
) => {
const {
message,
description,
cta,
descriptionClassName,
className,
...boxProps
} = props
const styles = useStyles()
return (
@ -37,7 +46,10 @@ export const EmptyState: FC<React.PropsWithChildren<EmptyStateProps>> = (props)
<Typography
variant="body2"
color="textSecondary"
className={combineClasses([styles.description, descriptionClassName])}
className={combineClasses([
styles.description,
descriptionClassName,
])}
>
{description}
</Typography>

View File

@ -1,14 +1,17 @@
import { Story } from "@storybook/react"
import { EnterpriseSnackbar, EnterpriseSnackbarProps } from "./EnterpriseSnackbar"
import {
EnterpriseSnackbar,
EnterpriseSnackbarProps,
} from "./EnterpriseSnackbar"
export default {
title: "components/EnterpriseSnackbar",
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

@ -1,5 +1,7 @@
import IconButton from "@material-ui/core/IconButton"
import Snackbar, { SnackbarProps as MuiSnackbarProps } from "@material-ui/core/Snackbar"
import Snackbar, {
SnackbarProps as MuiSnackbarProps,
} from "@material-ui/core/Snackbar"
import { makeStyles } from "@material-ui/core/styles"
import CloseIcon from "@material-ui/icons/Close"
import { FC } from "react"
@ -25,13 +27,9 @@ export interface EnterpriseSnackbarProps extends MuiSnackbarProps {
*
* See original component's Material UI documentation here: https://material-ui.com/components/snackbars/
*/
export const EnterpriseSnackbar: FC<React.PropsWithChildren<EnterpriseSnackbarProps>> = ({
onClose,
variant = "info",
ContentProps = {},
action,
...rest
}) => {
export const EnterpriseSnackbar: FC<
React.PropsWithChildren<EnterpriseSnackbarProps>
> = ({ onClose, variant = "info", ContentProps = {}, action, ...rest }) => {
const styles = useStyles()
return (

View File

@ -11,7 +11,10 @@ interface ErrorBoundaryState {
* Our app's Error Boundary
* Read more about React Error Boundaries: https://reactjs.org/docs/error-boundaries.html
*/
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { error: null }

View File

@ -1,6 +1,9 @@
import Link from "@material-ui/core/Link"
import makeStyles from "@material-ui/core/styles/makeStyles"
import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows"
import {
CloseDropdown,
OpenDropdown,
} from "components/DropdownArrows/DropdownArrows"
import { PropsWithChildren, FC } from "react"
import Collapse from "@material-ui/core/Collapse"
import { useTranslation } from "react-i18next"

View File

@ -10,7 +10,9 @@ describe("Footer", () => {
// Then
await screen.findByText("Copyright", { exact: false })
await screen.findByText(Language.buildInfoText(MockBuildInfo))
const reportBugLink = screen.getByText(Language.reportBugLink, { exact: false }).closest("a")
const reportBugLink = screen
.getByText(Language.reportBugLink, { exact: false })
.closest("a")
if (!reportBugLink) {
throw new Error("Bug report link not found in footer")
}

View File

@ -19,7 +19,9 @@ export interface FooterProps {
buildInfo?: TypesGen.BuildInfoResponse
}
export const Footer: React.FC<React.PropsWithChildren<FooterProps>> = ({ buildInfo }) => {
export const Footer: React.FC<React.PropsWithChildren<FooterProps>> = ({
buildInfo,
}) => {
const styles = useFooterStyles()
const githubUrl = `https://github.com/coder/coder/issues/new?labels=needs+grooming&body=${encodeURIComponent(`Version: [\`${buildInfo?.version}\`](${buildInfo?.external_url})
@ -38,14 +40,25 @@ export const Footer: React.FC<React.PropsWithChildren<FooterProps>> = ({ buildIn
target="_blank"
href={buildInfo.external_url}
>
<AccountTreeIcon className={styles.icon} /> {Language.buildInfoText(buildInfo)}
<AccountTreeIcon className={styles.icon} />{" "}
{Language.buildInfoText(buildInfo)}
</Link>
&nbsp;|&nbsp;
<Link className={styles.link} variant="caption" target="_blank" href={githubUrl}>
<Link
className={styles.link}
variant="caption"
target="_blank"
href={githubUrl}
>
<AssistantIcon className={styles.icon} /> {Language.reportBugLink}
</Link>
&nbsp;|&nbsp;
<Link className={styles.link} variant="caption" target="_blank" href={discordUrl}>
<Link
className={styles.link}
variant="caption"
target="_blank"
href={discordUrl}
>
<ChatIcon className={styles.icon} /> {Language.discordLink}
</Link>
</div>

View File

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

View File

@ -8,9 +8,9 @@ export interface FormCloseButtonProps {
onClose: () => void
}
export const FormCloseButton: React.FC<React.PropsWithChildren<FormCloseButtonProps>> = ({
onClose,
}) => {
export const FormCloseButton: React.FC<
React.PropsWithChildren<FormCloseButtonProps>
> = ({ onClose }) => {
const styles = useStyles()
useEffect(() => {

View File

@ -3,7 +3,10 @@ import MenuItem from "@material-ui/core/MenuItem"
import { makeStyles } from "@material-ui/core/styles"
import Typography from "@material-ui/core/Typography"
import { ReactElement } from "react"
import { FormTextField, FormTextFieldProps } from "../FormTextField/FormTextField"
import {
FormTextField,
FormTextFieldProps,
} from "../FormTextField/FormTextField"
export interface FormDropdownItem {
value: string

View File

@ -53,7 +53,11 @@ export const FormSection: FC<React.PropsWithChildren<FormSectionProps>> = ({
{title}
</Typography>
{description && (
<Typography className={styles.descriptionText} variant="body2" color="textSecondary">
<Typography
className={styles.descriptionText}
variant="body2"
color="textSecondary"
>
{description}
</Typography>
)}

View File

@ -12,7 +12,9 @@ namespace Helpers {
export const requiredValidationMsg = "required"
export const Component: FC<
React.PropsWithChildren<Omit<FormTextFieldProps<FormValues>, "form" | "formFieldName">>
React.PropsWithChildren<
Omit<FormTextFieldProps<FormValues>, "form" | "formFieldName">
>
> = (props) => {
const form = useFormik<FormValues>({
initialValues: {
@ -26,7 +28,9 @@ namespace Helpers {
}),
})
return <FormTextField<FormValues> {...props} form={form} formFieldName="name" />
return (
<FormTextField<FormValues> {...props} form={form} formFieldName="name" />
)
}
}

View File

@ -119,7 +119,8 @@ export const FormTextField = <T,>({
variant = "outlined",
...rest
}: FormTextFieldProps<T>): ReactElement => {
const isError = form.touched[formFieldName] && Boolean(form.errors[formFieldName])
const isError =
form.touched[formFieldName] && Boolean(form.errors[formFieldName])
// Conversion to a string primitive is necessary as formFieldName is an in
// indexable type such as a string, number or enum.
@ -145,7 +146,10 @@ export const FormTextField = <T,>({
}
const event = e
if (typeof eventTransform !== "undefined" && typeof event.target.value === "string") {
if (
typeof eventTransform !== "undefined" &&
typeof event.target.value === "string"
) {
event.target.value = eventTransform(e.target.value)
}
form.handleChange(event)

View File

@ -18,7 +18,10 @@ const useStyles = makeStyles((theme) => ({
},
}))
export const FormTitle: FC<React.PropsWithChildren<FormTitleProps>> = ({ title, detail }) => {
export const FormTitle: FC<React.PropsWithChildren<FormTitleProps>> = ({
title,
detail,
}) => {
const styles = useStyles()
return (

View File

@ -30,23 +30,36 @@ export const GlobalSnackbar: React.FC = () => {
const [open, setOpen] = useState<boolean>(false)
const [notification, setNotification] = useState<NotificationMsg>()
const handleNotification = useCallback<CustomEventListener<NotificationMsg>>((event) => {
const handleNotification = useCallback<CustomEventListener<NotificationMsg>>(
(event) => {
setNotification(event.detail)
setOpen(true)
}, [])
},
[],
)
useCustomEvent(SnackbarEventType, handleNotification)
const renderAdditionalMessage = (msg: AdditionalMessage, idx: number) => {
if (isNotificationText(msg)) {
return (
<Typography key={idx} gutterBottom variant="body2" className={styles.messageSubtitle}>
<Typography
key={idx}
gutterBottom
variant="body2"
className={styles.messageSubtitle}
>
{msg}
</Typography>
)
} else if (isNotificationTextPrefixed(msg)) {
return (
<Typography key={idx} gutterBottom variant="body2" className={styles.messageSubtitle}>
<Typography
key={idx}
gutterBottom
variant="body2"
className={styles.messageSubtitle}
>
<strong>{msg.prefix}:</strong> {msg.text}
</Typography>
)
@ -77,7 +90,9 @@ export const GlobalSnackbar: React.FC = () => {
variant={variantFromMsgType(notification.msgType)}
message={
<div className={styles.messageWrapper}>
{notification.msgType === MsgType.Error && <ErrorIcon className={styles.errorIcon} />}
{notification.msgType === MsgType.Error && (
<ErrorIcon className={styles.errorIcon} />
)}
<div className={styles.message}>
<Typography variant="body1" className={styles.messageTitle}>
{notification.msg}

View File

@ -48,13 +48,17 @@ describe("Snackbar", () => {
describe("displaySuccess", () => {
const originalWindowDispatchEvent = window.dispatchEvent
type TDispatchEventMock = jest.MockedFunction<(msg: CustomEvent<NotificationMsg>) => boolean>
type TDispatchEventMock = jest.MockedFunction<
(msg: CustomEvent<NotificationMsg>) => boolean
>
let dispatchEventMock: TDispatchEventMock
// Helper function to extract the notification event
// that was sent to `dispatchEvent`. This lets us validate
// the contents of the notification event are what we expect.
const extractNotificationEvent = (dispatchEventMock: TDispatchEventMock): NotificationMsg => {
const extractNotificationEvent = (
dispatchEventMock: TDispatchEventMock,
): NotificationMsg => {
// calls[0] is the first call made to the mock (this is reset in `beforeEach`)
// calls[0][0] is the first argument of the first call
// calls[0][0].detail is the 'detail' argument passed to the `CustomEvent` -
@ -64,7 +68,8 @@ describe("Snackbar", () => {
beforeEach(() => {
dispatchEventMock = jest.fn()
window.dispatchEvent = dispatchEventMock as unknown as typeof window.dispatchEvent
window.dispatchEvent =
dispatchEventMock as unknown as typeof window.dispatchEvent
})
afterEach(() => {
@ -84,7 +89,9 @@ describe("Snackbar", () => {
// Then
expect(dispatchEventMock).toBeCalledTimes(1)
expect(extractNotificationEvent(dispatchEventMock)).toStrictEqual(expected)
expect(extractNotificationEvent(dispatchEventMock)).toStrictEqual(
expected,
)
})
it("can be called with a title and additional message", () => {
@ -100,7 +107,9 @@ describe("Snackbar", () => {
// Then
expect(dispatchEventMock).toBeCalledTimes(1)
expect(extractNotificationEvent(dispatchEventMock)).toStrictEqual(expected)
expect(extractNotificationEvent(dispatchEventMock)).toStrictEqual(
expected,
)
})
})

View File

@ -28,7 +28,10 @@ export const isNotificationTextPrefixed = (
msg: AdditionalMessage | null,
): msg is NotificationTextPrefixed => {
if (msg) {
return typeof msg !== "string" && Object.prototype.hasOwnProperty.call(msg, "prefix")
return (
typeof msg !== "string" &&
Object.prototype.hasOwnProperty.call(msg, "prefix")
)
}
return false
}
@ -62,13 +65,25 @@ function dispatchNotificationEvent(
}
export const displayMsg = (msg: string, additionalMsg?: string): void => {
dispatchNotificationEvent(MsgType.Info, msg, additionalMsg ? [additionalMsg] : undefined)
dispatchNotificationEvent(
MsgType.Info,
msg,
additionalMsg ? [additionalMsg] : undefined,
)
}
export const displaySuccess = (msg: string, additionalMsg?: string): void => {
dispatchNotificationEvent(MsgType.Success, msg, additionalMsg ? [additionalMsg] : undefined)
dispatchNotificationEvent(
MsgType.Success,
msg,
additionalMsg ? [additionalMsg] : undefined,
)
}
export const displayError = (msg: string, additionalMsg?: string): void => {
dispatchNotificationEvent(MsgType.Error, msg, additionalMsg ? [additionalMsg] : undefined)
dispatchNotificationEvent(
MsgType.Error,
msg,
additionalMsg ? [additionalMsg] : undefined,
)
}

View File

@ -2,7 +2,13 @@ import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"
export const UsersOutlinedIcon: typeof SvgIcon = (props: SvgIconProps) => (
<SvgIcon {...props} viewBox="0 0 20 20">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.75 18.75H17.5V15.625V15.625C17.498 13.8999 16.1001 12.502 14.375 12.5V11.25C16.7901 11.2527 18.7473 13.2099 18.75 15.625L18.75 18.75Z"
fill="#677693"

View File

@ -5,7 +5,9 @@ import { LicenseBannerView } from "./LicenseBannerView"
export const LicenseBanner: React.FC = () => {
const xServices = useContext(XServiceContext)
const [entitlementsState, entitlementsSend] = useActor(xServices.entitlementsXService)
const [entitlementsState, entitlementsSend] = useActor(
xServices.entitlementsXService,
)
const { warnings } = entitlementsState.context.entitlements
/** Gets license data on app mount because LicenseBanner is mounted in App */

View File

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

View File

@ -16,7 +16,9 @@ export interface LicenseBannerViewProps {
warnings: string[]
}
export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({ warnings }) => {
export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({
warnings,
}) => {
const styles = useStyles()
const [showDetails, setShowDetails] = useState(false)
if (warnings.length === 1) {
@ -35,7 +37,11 @@ export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({ warnings }
<div className={styles.container}>
<div className={styles.flex}>
<div className={styles.leftContent}>
<Pill text={Language.licenseIssues(warnings.length)} type="warning" lightBorder />
<Pill
text={Language.licenseIssues(warnings.length)}
type="warning"
lightBorder
/>
<span className={styles.text}>{Language.exceeded}</span>
&nbsp;
<a href="mailto:sales@coder.com" className={styles.link}>

View File

@ -2,9 +2,17 @@ import Box from "@material-ui/core/Box"
import CircularProgress from "@material-ui/core/CircularProgress"
import { FC } from "react"
export const Loader: FC<React.PropsWithChildren<{ size?: number }>> = ({ size = 26 }) => {
export const Loader: FC<React.PropsWithChildren<{ size?: number }>> = ({
size = 26,
}) => {
return (
<Box p={4} width="100%" display="flex" alignItems="center" justifyContent="center">
<Box
p={4}
width="100%"
display="flex"
alignItems="center"
justifyContent="center"
>
<CircularProgress size={size} />
</Box>
)

View File

@ -10,7 +10,9 @@ export default {
},
}
const Template: Story<LoadingButtonProps> = (args) => <LoadingButton {...args} />
const Template: Story<LoadingButtonProps> = (args) => (
<LoadingButton {...args} />
)
export const Loading = Template.bind({})
Loading.args = {

View File

@ -14,14 +14,19 @@ export interface LogsProps {
className?: string
}
export const Logs: FC<React.PropsWithChildren<LogsProps>> = ({ lines, className = "" }) => {
export const Logs: FC<React.PropsWithChildren<LogsProps>> = ({
lines,
className = "",
}) => {
const styles = useStyles()
return (
<div className={combineClasses([className, styles.root])}>
{lines.map((line, idx) => (
<div className={styles.line} key={idx}>
<span className={styles.time}>{dayjs(line.time).format(`HH:mm:ss.SSS`)}</span>
<span className={styles.time}>
{dayjs(line.time).format(`HH:mm:ss.SSS`)}
</span>
<span className={styles.space}>&nbsp;&nbsp;&nbsp;&nbsp;</span>
<span>{line.output}</span>
</div>

View File

@ -6,7 +6,9 @@ export default {
component: Markdown,
} as ComponentMeta<typeof Markdown>
const Template: Story<MarkdownProps> = ({ children }) => <Markdown>{children}</Markdown>
const Template: Story<MarkdownProps> = ({ children }) => (
<Markdown>{children}</Markdown>
)
export const WithCode = Template.bind({})
WithCode.args = {

View File

@ -2,7 +2,10 @@ import { render, screen, waitFor } from "@testing-library/react"
import { App } from "app"
import { Language } from "components/NavbarView/NavbarView"
import { rest } from "msw"
import { MockEntitlementsWithAuditLog, MockMemberPermissions } from "testHelpers/renderHelpers"
import {
MockEntitlementsWithAuditLog,
MockMemberPermissions,
} from "testHelpers/renderHelpers"
import { server } from "testHelpers/server"
/**

View File

@ -15,8 +15,15 @@ export const Navbar: React.FC = () => {
shallowEqual,
)
const canViewAuditLog =
featureVisibility[FeatureNames.AuditLog] && Boolean(permissions?.viewAuditLog)
featureVisibility[FeatureNames.AuditLog] &&
Boolean(permissions?.viewAuditLog)
const onSignOut = () => authSend("SIGN_OUT")
return <NavbarView user={me} onSignOut={onSignOut} canViewAuditLog={canViewAuditLog} />
return (
<NavbarView
user={me}
onSignOut={onSignOut}
canViewAuditLog={canViewAuditLog}
/>
)
}

View File

@ -10,7 +10,9 @@ export default {
},
}
const Template: Story<NavbarViewProps> = (args: NavbarViewProps) => <NavbarView {...args} />
const Template: Story<NavbarViewProps> = (args: NavbarViewProps) => (
<NavbarView {...args} />
)
export const ForAdmin = Template.bind({})
ForAdmin.args = {

View File

@ -70,7 +70,9 @@ describe("NavbarView", () => {
})
it("audit nav link is hidden for members", async () => {
render(<NavbarView user={MockUser2} onSignOut={noop} canViewAuditLog={false} />)
render(
<NavbarView user={MockUser2} onSignOut={noop} canViewAuditLog={false} />,
)
const auditLink = screen.queryByText(navLanguage.audit)
expect(auditLink).not.toBeInTheDocument()
})

View File

@ -36,7 +36,10 @@ const NavItems: React.FC<
<List className={combineClasses([styles.navItems, className])}>
<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}
@ -86,7 +89,11 @@ export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
<MenuIcon />
</IconButton>
<Drawer anchor="left" open={isDrawerOpen} onClose={() => setIsDrawerOpen(false)}>
<Drawer
anchor="left"
open={isDrawerOpen}
onClose={() => setIsDrawerOpen(false)}
>
<div className={styles.drawer}>
<div className={styles.drawerHeader}>
<Logo fill="white" opacity={1} width={125} />
@ -99,7 +106,10 @@ export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
<Logo fill="white" opacity={1} width={125} />
</NavLink>
<NavItems className={styles.desktopNavItems} canViewAuditLog={canViewAuditLog} />
<NavItems
className={styles.desktopNavItems}
canViewAuditLog={canViewAuditLog}
/>
<div className={styles.profileButton}>
{user && <UserDropdown user={user} onSignOut={onSignOut} />}

View File

@ -17,7 +17,9 @@ export const WithTitle = WithTitleTemplate.bind({})
const WithSubtitleTemplate: Story = () => (
<PageHeader>
<PageHeaderTitle>Templates</PageHeaderTitle>
<PageHeaderSubtitle>Create a new workspace from a Template</PageHeaderSubtitle>
<PageHeaderSubtitle>
Create a new workspace from a Template
</PageHeaderSubtitle>
</PageHeader>
)

View File

@ -26,16 +26,17 @@ export const PageHeader: React.FC<React.PropsWithChildren<PageHeaderProps>> = ({
)
}
export const PageHeaderTitle: React.FC<React.PropsWithChildren<unknown>> = ({ children }) => {
export const PageHeaderTitle: React.FC<React.PropsWithChildren<unknown>> = ({
children,
}) => {
const styles = useStyles({})
return <h1 className={styles.title}>{children}</h1>
}
export const PageHeaderSubtitle: React.FC<React.PropsWithChildren<{ condensed?: boolean }>> = ({
children,
condensed,
}) => {
export const PageHeaderSubtitle: React.FC<
React.PropsWithChildren<{ condensed?: boolean }>
> = ({ children, condensed }) => {
const styles = useStyles({
condensed,
})

View File

@ -7,9 +7,9 @@ export default {
component: PaginationWidget,
}
const Template: Story<PaginationWidgetProps> = (args: PaginationWidgetProps) => (
<PaginationWidget {...args} />
)
const Template: Story<PaginationWidgetProps> = (
args: PaginationWidgetProps,
) => <PaginationWidget {...args} />
const defaultProps = {
prevLabel: "Previous",

View File

@ -13,10 +13,16 @@ describe("PaginatedList", () => {
/>,
)
expect(await screen.findByRole("button", { name: "Previous page" })).toBeTruthy()
expect(await screen.findByRole("button", { name: "Next page" })).toBeTruthy()
expect(
await screen.findByRole("button", { name: "Previous page" }),
).toBeTruthy()
expect(
await screen.findByRole("button", { name: "Next page" }),
).toBeTruthy()
// Shouldn't render any pages if no records are passed in
expect(await container.querySelectorAll(`button[name="Page button"]`)).toHaveLength(0)
expect(
await container.querySelectorAll(`button[name="Page button"]`),
).toHaveLength(0)
})
it("displays the expected number of pages with one ellipsis tile", async () => {
@ -34,7 +40,9 @@ describe("PaginatedList", () => {
)
// 7 total spaces. 6 are page numbers, one is ellipsis
expect(await container.querySelectorAll(`button[name="Page button"]`)).toHaveLength(6)
expect(
await container.querySelectorAll(`button[name="Page button"]`),
).toHaveLength(6)
})
it("displays the expected number of pages with two ellipsis tiles", async () => {
@ -52,6 +60,8 @@ describe("PaginatedList", () => {
)
// 7 total spaces. 2 sets of ellipsis on either side of the active page
expect(await container.querySelectorAll(`button[name="Page button"]`)).toHaveLength(5)
expect(
await container.querySelectorAll(`button[name="Page button"]`),
).toHaveLength(5)
})
})

View File

@ -38,7 +38,10 @@ const NUM_PAGE_BLOCKS = PAGES_TO_DISPLAY + 2
* Builds a list of pages based on how many pages exist and where the user is in their navigation of those pages.
* List result is used to from the buttons that make up the Pagination Widget
*/
export const buildPagedList = (numPages: number, activePage: number): (string | number)[] => {
export const buildPagedList = (
numPages: number,
activePage: number,
): (string | number)[] => {
if (numPages > NUM_PAGE_BLOCKS) {
let pages = []
const leftBound = activePage - PAGE_NEIGHBORS
@ -128,7 +131,11 @@ export const PaginationWidget = ({
</Button>
),
)}
<Button aria-label="Next page" disabled={lastPageActive} onClick={onNextClick}>
<Button
aria-label="Next page"
disabled={lastPageActive}
onClick={onNextClick}
>
<div>{nextLabel}</div>
<KeyboardArrowRight />
</Button>

View File

@ -2,13 +2,28 @@ import { buildPagedList } from "./PaginationWidget"
describe("unit/PaginationWidget", () => {
describe("buildPagedList", () => {
it.each<{ numPages: number; activePage: number; expected: (string | number)[] }>([
it.each<{
numPages: number
activePage: number
expected: (string | number)[]
}>([
{ numPages: 7, activePage: 1, expected: [1, 2, 3, 4, 5, 6, 7] },
{ numPages: 17, activePage: 1, expected: [1, 2, 3, 4, 5, "right", 17] },
{ numPages: 17, activePage: 9, expected: [1, "left", 8, 9, 10, "right", 17] },
{ numPages: 17, activePage: 17, expected: [1, "left", 13, 14, 15, 16, 17] },
])(`buildPagedList($numPages, $activePage)`, ({ numPages, activePage, expected }) => {
{
numPages: 17,
activePage: 9,
expected: [1, "left", 8, 9, 10, "right", 17],
},
{
numPages: 17,
activePage: 17,
expected: [1, "left", 13, 14, 15, 16, 17],
},
])(
`buildPagedList($numPages, $activePage)`,
({ numPages, activePage, expected }) => {
expect(buildPagedList(numPages, activePage)).toEqual(expected)
})
},
)
})
})

View File

@ -11,7 +11,9 @@ const Template: Story<ParameterInputProps> = (args: ParameterInputProps) => (
<ParameterInput {...args} />
)
const createParameterSchema = (partial: Partial<ParameterSchema>): ParameterSchema => {
const createParameterSchema = (
partial: Partial<ParameterSchema>,
): ParameterSchema => {
return {
id: "000000",
job_id: "000000",
@ -38,7 +40,8 @@ export const Basic = Template.bind({})
Basic.args = {
schema: createParameterSchema({
name: "project_name",
description: "Customize the name of a Google Cloud project that will be created!",
description:
"Customize the name of a Google Cloud project that will be created!",
}),
}
@ -58,6 +61,11 @@ Contains.args = {
name: "region",
default_source_value: "🏈 US Central",
description: "Where would you like your workspace to live?",
validation_contains: ["🏈 US Central", "⚽ Brazil East", "💶 EU West", "🦘 Australia South"],
validation_contains: [
"🏈 US Central",
"⚽ Brazil East",
"💶 EU West",
"🦘 Australia South",
],
}),
}

View File

@ -19,7 +19,9 @@ const ParameterLabel: React.FC<{ schema: ParameterSchema }> = ({ schema }) => {
return (
<label className={styles.label} htmlFor={schema.name}>
<strong>var.{schema.name}</strong>
{schema.description && <span className={styles.labelDescription}>{schema.description}</span>}
{schema.description && (
<span className={styles.labelDescription}>{schema.description}</span>
)}
</label>
)
}
@ -30,28 +32,28 @@ export interface ParameterInputProps {
onChange: (value: string) => void
}
export const ParameterInput: FC<React.PropsWithChildren<ParameterInputProps>> = ({
disabled,
onChange,
schema,
}) => {
export const ParameterInput: FC<
React.PropsWithChildren<ParameterInputProps>
> = ({ disabled, onChange, schema }) => {
const styles = useStyles()
return (
<Stack direction="column" className={styles.root}>
<ParameterLabel schema={schema} />
<div className={styles.input}>
<ParameterField disabled={disabled} onChange={onChange} schema={schema} />
<ParameterField
disabled={disabled}
onChange={onChange}
schema={schema}
/>
</div>
</Stack>
)
}
const ParameterField: React.FC<React.PropsWithChildren<ParameterInputProps>> = ({
disabled,
onChange,
schema,
}) => {
const ParameterField: React.FC<
React.PropsWithChildren<ParameterInputProps>
> = ({ disabled, onChange, schema }) => {
if (schema.validation_contains && schema.validation_contains.length > 0) {
return (
<TextField

View File

@ -8,10 +8,9 @@ import React, { useCallback, useState } from "react"
type PasswordFieldProps = Omit<TextFieldProps, "InputProps" | "type">
export const PasswordField: React.FC<React.PropsWithChildren<PasswordFieldProps>> = ({
variant = "outlined",
...rest
}) => {
export const PasswordField: React.FC<
React.PropsWithChildren<PasswordFieldProps>
> = ({ variant = "outlined", ...rest }) => {
const styles = useStyles()
const [showPassword, setShowPassword] = useState<boolean>(false)
@ -19,7 +18,9 @@ export const PasswordField: React.FC<React.PropsWithChildren<PasswordFieldProps>
() => setShowPassword((showPassword) => !showPassword),
[],
)
const VisibilityIcon = showPassword ? VisibilityOffOutlined : VisibilityOutlined
const VisibilityIcon = showPassword
? VisibilityOffOutlined
: VisibilityOutlined
return (
<TextField

View File

@ -16,7 +16,10 @@ export const Pill: FC<PillProps> = (props) => {
const { className, icon, text = false } = props
const styles = useStyles(props)
return (
<div className={combineClasses([styles.wrapper, styles.pillColor, className])} role="status">
<div
className={combineClasses([styles.wrapper, styles.pillColor, className])}
role="status"
>
{icon && <div className={styles.iconWrapper}>{icon}</div>}
{text}
</div>
@ -35,13 +38,15 @@ const useStyles = makeStyles<Theme, PillProps>((theme) => ({
fontWeight: 500,
color: "#FFF",
height: theme.spacing(3),
paddingLeft: ({ icon }) => (icon ? theme.spacing(0.75) : theme.spacing(1.5)),
paddingLeft: ({ icon }) =>
icon ? theme.spacing(0.75) : theme.spacing(1.5),
paddingRight: theme.spacing(1.5),
whiteSpace: "nowrap",
},
pillColor: {
backgroundColor: ({ type }) => (type ? theme.palette[type].dark : theme.palette.text.secondary),
backgroundColor: ({ type }) =>
type ? theme.palette[type].dark : theme.palette.text.secondary,
borderColor: ({ type, lightBorder }) =>
type
? lightBorder

View File

@ -9,7 +9,11 @@ import { Stack } from "components/Stack/Stack"
import { useRef, useState } from "react"
import { colors } from "theme/colors"
import { CodeExample } from "../CodeExample/CodeExample"
import { HelpTooltipLink, HelpTooltipLinksGroup, HelpTooltipText } from "../Tooltips/HelpTooltip"
import {
HelpTooltipLink,
HelpTooltipLinksGroup,
HelpTooltipText,
} from "../Tooltips/HelpTooltip"
export interface PortForwardButtonProps {
host: string
@ -28,13 +32,16 @@ const EnabledView: React.FC<PortForwardButtonProps> = (props) => {
return (
<Stack direction="column" spacing={1}>
<HelpTooltipText>
Access ports running on the agent with the <strong>port, agent name, workspace name</strong>{" "}
and <strong>your username</strong> URL schema, as shown below.
Access ports running on the agent with the{" "}
<strong>port, agent name, workspace name</strong> and{" "}
<strong>your username</strong> URL schema, as shown below.
</HelpTooltipText>
<CodeExample code={urlExample} />
<HelpTooltipText>Use the form to open applications in a new tab.</HelpTooltipText>
<HelpTooltipText>
Use the form to open applications in a new tab.
</HelpTooltipText>
<Stack direction="row" spacing={1} alignItems="center">
<TextField
@ -70,8 +77,8 @@ const DisabledView: React.FC<PortForwardButtonProps> = () => {
return (
<Stack direction="column" spacing={1}>
<HelpTooltipText>
<strong>Your deployment does not have port forward enabled.</strong> See the docs for more
details.
<strong>Your deployment does not have port forward enabled.</strong> See
the docs for more details.
</HelpTooltipText>
<HelpTooltipLinksGroup>
@ -136,7 +143,9 @@ export const PortForwardButton: React.FC<PortForwardButtonProps> = (props) => {
const useStyles = makeStyles((theme) => ({
popoverPaper: {
padding: `${theme.spacing(2.5)}px ${theme.spacing(3.5)}px ${theme.spacing(3.5)}px`,
padding: `${theme.spacing(2.5)}px ${theme.spacing(3.5)}px ${theme.spacing(
3.5,
)}px`,
width: theme.spacing(46),
color: theme.palette.text.secondary,
marginTop: theme.spacing(0.25),

View File

@ -9,7 +9,9 @@ export interface RequireAuthProps {
children: JSX.Element
}
export const RequireAuth: React.FC<React.PropsWithChildren<RequireAuthProps>> = ({ children }) => {
export const RequireAuth: React.FC<
React.PropsWithChildren<RequireAuthProps>
> = ({ children }) => {
const xServices = useContext(XServiceContext)
const [authState] = useActor(xServices.authXService)
const location = useLocation()

View File

@ -9,7 +9,10 @@ export interface RequirePermissionProps {
/**
* Wraps routes that are available based on RBAC or licensing.
*/
export const RequirePermission: FC<RequirePermissionProps> = ({ children, isFeatureVisible }) => {
export const RequirePermission: FC<RequirePermissionProps> = ({
children,
isFeatureVisible,
}) => {
if (!isFeatureVisible) {
return <Navigate to="/workspaces" />
} else {

View File

@ -1,12 +1,17 @@
import { Story } from "@storybook/react"
import { ResourceAgentLatency, ResourceAgentLatencyProps } from "./ResourceAgentLatency"
import {
ResourceAgentLatency,
ResourceAgentLatencyProps,
} from "./ResourceAgentLatency"
export default {
title: "components/ResourceAgentLatency",
component: ResourceAgentLatency,
}
const Template: Story<ResourceAgentLatencyProps> = (args) => <ResourceAgentLatency {...args} />
const Template: Story<ResourceAgentLatencyProps> = (args) => (
<ResourceAgentLatency {...args} />
)
export const Single = Template.bind({})
Single.args = {

View File

@ -8,7 +8,9 @@ export interface ResourceAgentLatencyProps {
latency: WorkspaceAgent["latency"]
}
export const ResourceAgentLatency: React.FC<ResourceAgentLatencyProps> = (props) => {
export const ResourceAgentLatency: React.FC<ResourceAgentLatencyProps> = (
props,
) => {
const styles = useStyles()
if (!props.latency) {
return null
@ -23,8 +25,8 @@ export const ResourceAgentLatency: React.FC<ResourceAgentLatencyProps> = (props)
<b>Latency</b>
<HelpTooltip size="small">
<HelpTooltipText>
Latency from relay servers, used when connections cannot connect peer-to-peer. Star
indicates the preferred relay.
Latency from relay servers, used when connections cannot connect
peer-to-peer. Star indicates the preferred relay.
</HelpTooltipText>
</HelpTooltip>
</div>
@ -34,7 +36,8 @@ export const ResourceAgentLatency: React.FC<ResourceAgentLatencyProps> = (props)
const value = latency[region]
return (
<div key={region} className={styles.region}>
<b>{region}:</b>&nbsp;{Math.round(value.latency_ms * 100) / 100} ms
<b>{region}:</b>&nbsp;{Math.round(value.latency_ms * 100) / 100}{" "}
ms
{value.preferred && <StarIcon className={styles.star} />}
</div>
)

View File

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

View File

@ -5,7 +5,10 @@ import VisibilityOffOutlined from "@material-ui/icons/VisibilityOffOutlined"
import VisibilityOutlined from "@material-ui/icons/VisibilityOutlined"
import { WorkspaceResource } from "api/typesGenerated"
import { FC, useState } from "react"
import { TableCellData, TableCellDataPrimary } from "../TableCellData/TableCellData"
import {
TableCellData,
TableCellDataPrimary,
} from "../TableCellData/TableCellData"
import { ResourceAvatar } from "./ResourceAvatar"
const Language = {
@ -18,7 +21,11 @@ const SensitiveValue: React.FC<{ value: string }> = ({ value }) => {
const styles = useStyles()
const displayValue = shouldDisplay ? value : "••••••••"
const buttonLabel = shouldDisplay ? Language.hideLabel : Language.showLabel
const icon = shouldDisplay ? <VisibilityOffOutlined /> : <VisibilityOutlined />
const icon = shouldDisplay ? (
<VisibilityOffOutlined />
) : (
<VisibilityOutlined />
)
return (
<div className={styles.sensitiveValue}>
@ -43,7 +50,9 @@ export interface ResourceAvatarDataProps {
resource: WorkspaceResource
}
export const ResourceAvatarData: FC<ResourceAvatarDataProps> = ({ resource }) => {
export const ResourceAvatarData: FC<ResourceAvatarDataProps> = ({
resource,
}) => {
const styles = useStyles()
return (

View File

@ -8,12 +8,19 @@ import TableHead from "@material-ui/core/TableHead"
import TableRow from "@material-ui/core/TableRow"
import { Skeleton } from "@material-ui/lab"
import useTheme from "@material-ui/styles/useTheme"
import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows"
import {
CloseDropdown,
OpenDropdown,
} from "components/DropdownArrows/DropdownArrows"
import { PortForwardButton } from "components/PortForwardButton/PortForwardButton"
import { TableCellDataPrimary } from "components/TableCellData/TableCellData"
import { FC, useState } from "react"
import { getDisplayAgentStatus, getDisplayVersionStatus } from "util/workspace"
import { BuildInfoResponse, Workspace, WorkspaceResource } from "../../api/typesGenerated"
import {
BuildInfoResponse,
Workspace,
WorkspaceResource,
} from "../../api/typesGenerated"
import { AppLink } from "../AppLink/AppLink"
import { SSHButton } from "../SSHButton/SSHButton"
import { Stack } from "../Stack/Stack"
@ -58,7 +65,8 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
const styles = useStyles()
const theme: Theme = useTheme()
const serverVersion = buildInfo?.version || ""
const [shouldDisplayHideResources, setShouldDisplayHideResources] = useState(false)
const [shouldDisplayHideResources, setShouldDisplayHideResources] =
useState(false)
const displayResources = shouldDisplayHideResources
? resources
: resources.filter((resource) => !resource.hide)
@ -95,13 +103,18 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
/* We need to initialize the agents to display the resource */
}
const agents = resource.agents ?? [null]
const resourceName = <ResourceAvatarData resource={resource} />
const resourceName = (
<ResourceAvatarData resource={resource} />
)
return agents.map((agent, agentIndex) => {
{
/* If there is no agent, just display the resource name */
}
if (!agent || workspace.latest_build.transition === "stop") {
if (
!agent ||
workspace.latest_build.transition === "stop"
) {
return (
<TableRow key={`${resource.id}-${agentIndex}`}>
<TableCell>{resourceName}</TableCell>
@ -109,27 +122,33 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
</TableRow>
)
}
const { displayVersion, outdated } = getDisplayVersionStatus(
agent.version,
serverVersion,
)
const { displayVersion, outdated } =
getDisplayVersionStatus(agent.version, serverVersion)
const agentStatus = getDisplayAgentStatus(theme, agent)
return (
<TableRow key={`${resource.id}-${agent.id}`}>
{/* We only want to display the name in the first row because we are using rowSpan */}
{/* The rowspan should be the same than the number of agents */}
{agentIndex === 0 && (
<TableCell className={styles.resourceNameCell} rowSpan={agents.length}>
<TableCell
className={styles.resourceNameCell}
rowSpan={agents.length}
>
{resourceName}
</TableCell>
)}
<TableCell className={styles.agentColumn}>
<TableCellDataPrimary highlight>{agent.name}</TableCellDataPrimary>
<TableCellDataPrimary highlight>
{agent.name}
</TableCellDataPrimary>
<div className={styles.data}>
<div className={styles.dataRow}>
<strong>{Language.statusLabel}</strong>
<span style={{ color: agentStatus.color }} className={styles.status}>
<span
style={{ color: agentStatus.color }}
className={styles.status}
>
{agentStatus.status}
</span>
</div>
@ -141,7 +160,9 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
</div>
<div className={styles.dataRow}>
<strong>{Language.versionLabel}</strong>
<span className={styles.agentVersion}>{displayVersion}</span>
<span className={styles.agentVersion}>
{displayVersion}
</span>
<AgentOutdatedTooltip outdated={outdated} />
</div>
<div className={styles.dataRow}>
@ -151,7 +172,8 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
</TableCell>
<TableCell>
<div className={styles.accessLinks}>
{canUpdateWorkspace && agent.status === "connected" && (
{canUpdateWorkspace &&
agent.status === "connected" && (
<>
{applicationsHost !== undefined && (
<PortForwardButton
@ -188,7 +210,8 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
))}
</>
)}
{canUpdateWorkspace && agent.status === "connecting" && (
{canUpdateWorkspace &&
agent.status === "connecting" && (
<>
<Skeleton width={80} height={60} />
<Skeleton width={120} height={60} />

View File

@ -21,7 +21,11 @@ describe("UserRoleSelect", () => {
assignableRole(MockAuditorRole, true),
assignableRole(MockUserAdminRole, true),
]}
selectedRoles={[MockUserAdminRole, MockTemplateAdminRole, MockMemberRole]}
selectedRoles={[
MockUserAdminRole,
MockTemplateAdminRole,
MockMemberRole,
]}
loading={false}
onChange={jest.fn()}
open
@ -30,7 +34,9 @@ describe("UserRoleSelect", () => {
// Then
const owner = await screen.findByText(MockOwnerRole.display_name)
const templateAdmin = await screen.findByText(MockTemplateAdminRole.display_name)
const templateAdmin = await screen.findByText(
MockTemplateAdminRole.display_name,
)
const auditor = await screen.findByText(MockAuditorRole.display_name)
const userAdmin = await screen.findByText(MockUserAdminRole.display_name)

View File

@ -26,7 +26,9 @@ export const RoleSelect: FC<React.PropsWithChildren<RoleSelectProps>> = ({
const styles = useStyles()
const value = selectedRoles.map((r) => r.name)
const renderValue = () => selectedRoles.map((r) => r.display_name).join(", ")
const sortedRoles = roles.sort((a, b) => a.display_name.localeCompare(b.display_name))
const sortedRoles = roles.sort((a, b) =>
a.display_name.localeCompare(b.display_name),
)
return (
<Select
@ -43,11 +45,18 @@ export const RoleSelect: FC<React.PropsWithChildren<RoleSelectProps>> = ({
}}
>
{sortedRoles.map((r) => {
const isChecked = selectedRoles.some((selectedRole) => selectedRole.name === r.name)
const isChecked = selectedRoles.some(
(selectedRole) => selectedRole.name === r.name,
)
return (
<MenuItem key={r.name} value={r.name} disabled={loading || !r.assignable}>
<Checkbox size="small" color="primary" checked={isChecked} /> {r.display_name}
<MenuItem
key={r.name}
value={r.name}
disabled={loading || !r.assignable}
>
<Checkbox size="small" color="primary" checked={isChecked} />{" "}
{r.display_name}
</MenuItem>
)
})}

View File

@ -26,21 +26,29 @@ export const stackTraceUnavailable = {
type ReportMessage = StackTraceAvailableMsg | typeof stackTraceUnavailable
export const stackTraceAvailable = (stackTrace: string[]): StackTraceAvailableMsg => {
export const stackTraceAvailable = (
stackTrace: string[],
): StackTraceAvailableMsg => {
return {
type: "stackTraceAvailable",
stackTrace,
}
}
const setStackTrace = (model: ReportState, mappedStack: string[]): ReportState => {
const setStackTrace = (
model: ReportState,
mappedStack: string[],
): ReportState => {
return {
...model,
mappedStack,
}
}
export const reducer = (model: ReportState, msg: ReportMessage): ReportState => {
export const reducer = (
model: ReportState,
msg: ReportMessage,
): ReportState => {
switch (msg.type) {
case "stackTraceAvailable":
return setStackTrace(model, msg.stackTrace)
@ -49,7 +57,10 @@ export const reducer = (model: ReportState, msg: ReportMessage): ReportState =>
}
}
export const createFormattedStackTrace = (error: Error, mappedStack: string[] | null): string[] => {
export const createFormattedStackTrace = (
error: Error,
mappedStack: string[] | null,
): string[] => {
return [
"======================= STACK TRACE ========================",
"",
@ -63,11 +74,19 @@ export const createFormattedStackTrace = (error: Error, mappedStack: string[] |
/**
* A code block component that contains the error stack resulting from an error boundary trigger
*/
export const RuntimeErrorReport = ({ error, mappedStack }: ReportState): ReactElement => {
export const RuntimeErrorReport = ({
error,
mappedStack,
}: ReportState): ReactElement => {
const styles = useStyles()
if (!mappedStack) {
return <CodeBlock lines={[Language.reportLoading]} className={styles.codeBlock} />
return (
<CodeBlock
lines={[Language.reportLoading]}
className={styles.codeBlock}
/>
)
}
const formattedStackTrace = createFormattedStackTrace(error, mappedStack)

View File

@ -13,7 +13,9 @@ export default {
},
} as ComponentMeta<typeof RuntimeErrorState>
const Template: Story<RuntimeErrorStateProps> = (args) => <RuntimeErrorState {...args} />
const Template: Story<RuntimeErrorStateProps> = (args) => (
<RuntimeErrorState {...args} />
)
export const Errored = Template.bind({})
Errored.parameters = {

View File

@ -1,7 +1,10 @@
import { screen } from "@testing-library/react"
import { render } from "../../testHelpers/renderHelpers"
import { Language as ButtonLanguage } from "./createCtas"
import { Language as RuntimeErrorStateLanguage, RuntimeErrorState } from "./RuntimeErrorState"
import {
Language as RuntimeErrorStateLanguage,
RuntimeErrorState,
} from "./RuntimeErrorState"
describe("RuntimeErrorState", () => {
beforeEach(() => {

View File

@ -61,13 +61,20 @@ const ErrorStateDescription = ({ emailBody }: { emailBody?: string }) => {
/**
* An error UI that is displayed when our error boundary (ErrorBoundary.tsx) is triggered
*/
export const RuntimeErrorState: React.FC<RuntimeErrorStateProps> = ({ error }) => {
export const RuntimeErrorState: React.FC<RuntimeErrorStateProps> = ({
error,
}) => {
const styles = useStyles()
const [reportState, dispatch] = useReducer(reducer, { error, mappedStack: null })
const [reportState, dispatch] = useReducer(reducer, {
error,
mappedStack: null,
})
useEffect(() => {
try {
mapStackTrace(error.stack, (mappedStack) => dispatch(stackTraceAvailable(mappedStack)))
mapStackTrace(error.stack, (mappedStack) =>
dispatch(stackTraceAvailable(mappedStack)),
)
} catch {
dispatch(stackTraceUnavailable)
}
@ -81,13 +88,17 @@ 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")}
/>
}
>
<RuntimeErrorReport error={reportState.error} mappedStack={reportState.mappedStack} />
<RuntimeErrorReport
error={reportState.error}
mappedStack={reportState.mappedStack}
/>
</Section>
</Margins>
</Box>

View File

@ -1,5 +1,8 @@
import { Story } from "@storybook/react"
import { MockWorkspace, MockWorkspaceAgent } from "../../testHelpers/renderHelpers"
import {
MockWorkspace,
MockWorkspaceAgent,
} from "../../testHelpers/renderHelpers"
import { SSHButton, SSHButtonProps } from "./SSHButton"
export default {

View File

@ -5,7 +5,11 @@ import CloudIcon from "@material-ui/icons/CloudOutlined"
import { useRef, useState } from "react"
import { CodeExample } from "../CodeExample/CodeExample"
import { Stack } from "../Stack/Stack"
import { HelpTooltipLink, HelpTooltipLinksGroup, HelpTooltipText } from "../Tooltips/HelpTooltip"
import {
HelpTooltipLink,
HelpTooltipLinksGroup,
HelpTooltipText,
} from "../Tooltips/HelpTooltip"
export interface SSHButtonProps {
workspaceName: string
@ -54,19 +58,25 @@ export const SSHButton: React.FC<React.PropsWithChildren<SSHButtonProps>> = ({
horizontal: "left",
}}
>
<HelpTooltipText>Run the following commands to connect with SSH:</HelpTooltipText>
<HelpTooltipText>
Run the following commands to connect with SSH:
</HelpTooltipText>
<Stack spacing={0.5} className={styles.codeExamples}>
<div>
<HelpTooltipText>
<strong className={styles.codeExampleLabel}>Configure SSH hosts on machine:</strong>
<strong className={styles.codeExampleLabel}>
Configure SSH hosts on machine:
</strong>
</HelpTooltipText>
<CodeExample code="coder config-ssh" />
</div>
<div>
<HelpTooltipText>
<strong className={styles.codeExampleLabel}>Connect to the agent:</strong>
<strong className={styles.codeExampleLabel}>
Connect to the agent:
</strong>
</HelpTooltipText>
<CodeExample code={`ssh coder.${workspaceName}.${agentName}`} />
</div>
@ -93,7 +103,9 @@ export const SSHButton: React.FC<React.PropsWithChildren<SSHButtonProps>> = ({
const useStyles = makeStyles((theme) => ({
popoverPaper: {
padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing(3)}px`,
padding: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing(
3,
)}px`,
width: theme.spacing(38),
color: theme.palette.text.secondary,
marginTop: theme.spacing(0.25),

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