mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
@ -1,12 +1,11 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"printWidth": 80,
|
||||
"semi": false,
|
||||
"trailingComma": "all",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["./README.md", "**/*.yaml"],
|
||||
"options": {
|
||||
"printWidth": 80,
|
||||
"proseWrap": "always"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
},
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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" })
|
||||
|
@ -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>
|
||||
|
@ -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: [
|
||||
|
@ -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,
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
@ -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 />
|
||||
|
@ -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",
|
||||
|
@ -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) => {
|
||||
expect(getURLWithSearchParams(basePath, filter)).toBe(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) => {
|
||||
expect(getURLWithSearchParams(basePath, filter)).toBe(expected)
|
||||
})
|
||||
])(
|
||||
`Users - getURLWithSearchParams(%p, %p) returns %p`,
|
||||
(basePath, filter, expected) => {
|
||||
expect(getURLWithSearchParams(basePath, filter)).toBe(expected)
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -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}`, {
|
||||
params,
|
||||
})
|
||||
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> => {
|
||||
const response = await axios.get(`/api/v2/applications/host`)
|
||||
return response.data
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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 =
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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] }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 = {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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>
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
||||
|
@ -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 = {
|
||||
|
@ -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}
|
||||
|
@ -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}>
|
||||
|
@ -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 = {
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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" })
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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",
|
||||
},
|
||||
|
@ -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 = {
|
||||
|
@ -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>
|
||||
|
@ -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 }) => {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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`,
|
||||
},
|
||||
}))
|
||||
|
@ -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 (
|
||||
|
@ -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")
|
||||
|
@ -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>
|
||||
|
@ -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 = {
|
||||
|
@ -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 (
|
||||
|
@ -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 }
|
||||
|
@ -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"
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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>
|
||||
|
|
||||
<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>
|
||||
|
|
||||
<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>
|
||||
|
@ -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 = {}
|
||||
|
@ -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(() => {
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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" />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 (
|
||||
|
@ -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) => {
|
||||
setNotification(event.detail)
|
||||
setOpen(true)
|
||||
}, [])
|
||||
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}
|
||||
|
@ -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,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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 */
|
||||
|
@ -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 = {
|
||||
|
@ -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>
|
||||
|
||||
<a href="mailto:sales@coder.com" className={styles.link}>
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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 = {
|
||||
|
@ -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}> </span>
|
||||
<span>{line.output}</span>
|
||||
</div>
|
||||
|
@ -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 = {
|
||||
|
@ -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"
|
||||
|
||||
/**
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
@ -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()
|
||||
})
|
||||
|
@ -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} />}
|
||||
|
@ -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>
|
||||
)
|
||||
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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>
|
||||
|
@ -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 }) => {
|
||||
expect(buildPagedList(numPages, activePage)).toEqual(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)
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -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",
|
||||
],
|
||||
}),
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
|
@ -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()
|
||||
|
@ -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 {
|
||||
|
@ -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 = {
|
||||
|
@ -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> {Math.round(value.latency_ms * 100) / 100} ms
|
||||
<b>{region}:</b> {Math.round(value.latency_ms * 100) / 100}{" "}
|
||||
ms
|
||||
{value.preferred && <StarIcon className={styles.star} />}
|
||||
</div>
|
||||
)
|
||||
|
@ -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 = {
|
||||
|
@ -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 (
|
||||
|
@ -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,49 +172,51 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className={styles.accessLinks}>
|
||||
{canUpdateWorkspace && agent.status === "connected" && (
|
||||
<>
|
||||
{applicationsHost !== undefined && (
|
||||
<PortForwardButton
|
||||
host={applicationsHost}
|
||||
{canUpdateWorkspace &&
|
||||
agent.status === "connected" && (
|
||||
<>
|
||||
{applicationsHost !== undefined && (
|
||||
<PortForwardButton
|
||||
host={applicationsHost}
|
||||
workspaceName={workspace.name}
|
||||
agentName={agent.name}
|
||||
username={workspace.owner_name}
|
||||
/>
|
||||
)}
|
||||
{!hideSSHButton && (
|
||||
<SSHButton
|
||||
workspaceName={workspace.name}
|
||||
agentName={agent.name}
|
||||
/>
|
||||
)}
|
||||
<TerminalLink
|
||||
workspaceName={workspace.name}
|
||||
agentName={agent.name}
|
||||
username={workspace.owner_name}
|
||||
userName={workspace.owner_name}
|
||||
/>
|
||||
)}
|
||||
{!hideSSHButton && (
|
||||
<SSHButton
|
||||
workspaceName={workspace.name}
|
||||
agentName={agent.name}
|
||||
/>
|
||||
)}
|
||||
<TerminalLink
|
||||
workspaceName={workspace.name}
|
||||
agentName={agent.name}
|
||||
userName={workspace.owner_name}
|
||||
/>
|
||||
{agent.apps.map((app) => (
|
||||
<AppLink
|
||||
key={app.name}
|
||||
appsHost={applicationsHost}
|
||||
appIcon={app.icon}
|
||||
appName={app.name}
|
||||
appCommand={app.command}
|
||||
appSubdomain={app.subdomain}
|
||||
username={workspace.owner_name}
|
||||
workspaceName={workspace.name}
|
||||
agentName={agent.name}
|
||||
health={app.health}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{canUpdateWorkspace && agent.status === "connecting" && (
|
||||
<>
|
||||
<Skeleton width={80} height={60} />
|
||||
<Skeleton width={120} height={60} />
|
||||
</>
|
||||
)}
|
||||
{agent.apps.map((app) => (
|
||||
<AppLink
|
||||
key={app.name}
|
||||
appsHost={applicationsHost}
|
||||
appIcon={app.icon}
|
||||
appName={app.name}
|
||||
appCommand={app.command}
|
||||
appSubdomain={app.subdomain}
|
||||
username={workspace.owner_name}
|
||||
workspaceName={workspace.name}
|
||||
agentName={agent.name}
|
||||
health={app.health}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{canUpdateWorkspace &&
|
||||
agent.status === "connecting" && (
|
||||
<>
|
||||
<Skeleton width={80} height={60} />
|
||||
<Skeleton width={120} height={60} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
})}
|
||||
|
@ -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)
|
||||
|
@ -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 = {
|
||||
|
@ -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(() => {
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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
Reference in New Issue
Block a user