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,
|
"semi": false,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["./README.md", "**/*.yaml"],
|
"files": ["./README.md", "**/*.yaml"],
|
||||||
"options": {
|
"options": {
|
||||||
"printWidth": 80,
|
|
||||||
"proseWrap": "always"
|
"proseWrap": "always"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,11 @@ module.exports = {
|
|||||||
//
|
//
|
||||||
// SEE: https://storybook.js.org/docs/react/configure/webpack
|
// SEE: https://storybook.js.org/docs/react/configure/webpack
|
||||||
webpackFinal: async (config) => {
|
webpackFinal: async (config) => {
|
||||||
config.resolve.modules = [path.resolve(__dirname, ".."), "node_modules", "../src"]
|
config.resolve.modules = [
|
||||||
|
path.resolve(__dirname, ".."),
|
||||||
|
"node_modules",
|
||||||
|
"../src",
|
||||||
|
]
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,10 @@ export class SignInPage extends BasePom {
|
|||||||
super(baseURL, "/login", page)
|
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=Email", email)
|
||||||
await this.page.fill("text=Password", password)
|
await this.page.fill("text=Password", password)
|
||||||
await this.page.click("text=Sign In")
|
await this.page.click("text=Sign In")
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { test } from "@playwright/test"
|
import { test } from "@playwright/test"
|
||||||
import { HealthzPage } from "../pom/HealthzPage"
|
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)
|
const healthzPage = new HealthzPage(baseURL, page)
|
||||||
await page.goto(healthzPage.url, { waitUntil: "networkidle" })
|
await page.goto(healthzPage.url, { waitUntil: "networkidle" })
|
||||||
await healthzPage.getOk().waitFor({ state: "visible" })
|
await healthzPage.getOk().waitFor({ state: "visible" })
|
||||||
|
@ -23,7 +23,12 @@
|
|||||||
href="/favicons/favicon.png"
|
href="/favicons/favicon.png"
|
||||||
data-react-helmet="true"
|
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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -35,8 +35,17 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
displayName: "lint",
|
displayName: "lint",
|
||||||
runner: "jest-runner-eslint",
|
runner: "jest-runner-eslint",
|
||||||
testMatch: ["<rootDir>/**/*.js", "<rootDir>/**/*.ts", "<rootDir>/**/*.tsx"],
|
testMatch: [
|
||||||
testPathIgnorePatterns: ["/out/", "/_jest/", "jest.config.js", "jest-runner.*.js"],
|
"<rootDir>/**/*.js",
|
||||||
|
"<rootDir>/**/*.ts",
|
||||||
|
"<rootDir>/**/*.tsx",
|
||||||
|
],
|
||||||
|
testPathIgnorePatterns: [
|
||||||
|
"/out/",
|
||||||
|
"/_jest/",
|
||||||
|
"jest.config.js",
|
||||||
|
"jest-runner.*.js",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
collectCoverageFrom: [
|
collectCoverageFrom: [
|
||||||
|
@ -45,7 +45,10 @@ const CONSOLE_FAIL_TYPES = ["error" /* 'warn' */] as const
|
|||||||
CONSOLE_FAIL_TYPES.forEach((logType: typeof CONSOLE_FAIL_TYPES[number]) => {
|
CONSOLE_FAIL_TYPES.forEach((logType: typeof CONSOLE_FAIL_TYPES[number]) => {
|
||||||
global.console[logType] = <Type>(format: string, ...args: Type[]): void => {
|
global.console[logType] = <Type>(format: string, ...args: Type[]): void => {
|
||||||
throw new Error(
|
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 are secondary, not in the main navigation or not usually accessed
|
||||||
// - Pages that use heavy dependencies like charts or time libraries
|
// - Pages that use heavy dependencies like charts or time libraries
|
||||||
const NotFoundPage = lazy(() => import("./pages/404Page/404Page"))
|
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 HealthzPage = lazy(() => import("./pages/HealthzPage/HealthzPage"))
|
||||||
const AccountPage = lazy(() => import("./pages/UserSettingsPage/AccountPage/AccountPage"))
|
const AccountPage = lazy(
|
||||||
const SecurityPage = lazy(() => import("./pages/UserSettingsPage/SecurityPage/SecurityPage"))
|
() => import("./pages/UserSettingsPage/AccountPage/AccountPage"),
|
||||||
const SSHKeysPage = lazy(() => import("./pages/UserSettingsPage/SSHKeysPage/SSHKeysPage"))
|
)
|
||||||
const CreateUserPage = lazy(() => import("./pages/UsersPage/CreateUserPage/CreateUserPage"))
|
const SecurityPage = lazy(
|
||||||
const WorkspaceBuildPage = lazy(() => import("./pages/WorkspaceBuildPage/WorkspaceBuildPage"))
|
() => 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 WorkspacePage = lazy(() => import("./pages/WorkspacePage/WorkspacePage"))
|
||||||
const WorkspaceSchedulePage = lazy(
|
const WorkspaceSchedulePage = lazy(
|
||||||
() => import("./pages/WorkspaceSchedulePage/WorkspaceSchedulePage"),
|
() => import("./pages/WorkspaceSchedulePage/WorkspaceSchedulePage"),
|
||||||
)
|
)
|
||||||
const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage"))
|
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"))
|
const TemplatePage = lazy(() => import("./pages/TemplatePage/TemplatePage"))
|
||||||
|
|
||||||
export const AppRouter: FC = () => {
|
export const AppRouter: FC = () => {
|
||||||
const xServices = useContext(XServiceContext)
|
const xServices = useContext(XServiceContext)
|
||||||
const permissions = useSelector(xServices.authXService, selectPermissions)
|
const permissions = useSelector(xServices.authXService, selectPermissions)
|
||||||
const featureVisibility = useSelector(xServices.entitlementsXService, selectFeatureVisibility)
|
const featureVisibility = useSelector(
|
||||||
|
xServices.entitlementsXService,
|
||||||
|
selectFeatureVisibility,
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<FullScreenLoader />}>
|
<Suspense fallback={<FullScreenLoader />}>
|
||||||
@ -142,7 +159,8 @@ export const AppRouter: FC = () => {
|
|||||||
<AuthAndFrame>
|
<AuthAndFrame>
|
||||||
<RequirePermission
|
<RequirePermission
|
||||||
isFeatureVisible={
|
isFeatureVisible={
|
||||||
featureVisibility[FeatureNames.AuditLog] && Boolean(permissions?.viewAuditLog)
|
featureVisibility[FeatureNames.AuditLog] &&
|
||||||
|
Boolean(permissions?.viewAuditLog)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AuditPage />
|
<AuditPage />
|
||||||
|
@ -6,7 +6,10 @@ import "./i18n"
|
|||||||
|
|
||||||
// if this is a development build and the developer wants to inspect
|
// if this is a development build and the developer wants to inspect
|
||||||
// helpful to see realtime changes on the services
|
// 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
|
// configure the XState inspector to open in a new tab
|
||||||
inspect({
|
inspect({
|
||||||
url: "https://stately.ai/viz?inspect",
|
url: "https://stately.ai/viz?inspect",
|
||||||
|
@ -119,21 +119,39 @@ describe("api.ts", () => {
|
|||||||
["/api/v2/workspaces", undefined, "/api/v2/workspaces"],
|
["/api/v2/workspaces", undefined, "/api/v2/workspaces"],
|
||||||
|
|
||||||
["/api/v2/workspaces", { q: "" }, "/api/v2/workspaces"],
|
["/api/v2/workspaces", { q: "" }, "/api/v2/workspaces"],
|
||||||
["/api/v2/workspaces", { q: "owner:1" }, "/api/v2/workspaces?q=owner%3A1"],
|
[
|
||||||
|
"/api/v2/workspaces",
|
||||||
|
{ q: "owner:1" },
|
||||||
|
"/api/v2/workspaces?q=owner%3A1",
|
||||||
|
],
|
||||||
|
|
||||||
["/api/v2/workspaces", { q: "owner:me" }, "/api/v2/workspaces?q=owner%3Ame"],
|
[
|
||||||
])(`Workspaces - getURLWithSearchParams(%p, %p) returns %p`, (basePath, filter, expected) => {
|
"/api/v2/workspaces",
|
||||||
expect(getURLWithSearchParams(basePath, filter)).toBe(expected)
|
{ 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", () => {
|
describe("getURLWithSearchParams - users", () => {
|
||||||
it.each<[string, TypesGen.UsersRequest | undefined, string]>([
|
it.each<[string, TypesGen.UsersRequest | undefined, string]>([
|
||||||
["/api/v2/users", undefined, "/api/v2/users"],
|
["/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"],
|
["/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()
|
axios.defaults.headers.common["X-CSRF-TOKEN"] = hardCodedCSRFCookie()
|
||||||
token.setAttribute("content", hardCodedCSRFCookie())
|
token.setAttribute("content", hardCodedCSRFCookie())
|
||||||
} else {
|
} else {
|
||||||
axios.defaults.headers.common["X-CSRF-TOKEN"] = token.getAttribute("content") ?? ""
|
axios.defaults.headers.common["X-CSRF-TOKEN"] =
|
||||||
|
token.getAttribute("content") ?? ""
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Do not write error logs if we are in a FE unit test.
|
// 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> => {
|
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
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const checkAuthorization = async (
|
export const checkAuthorization = async (
|
||||||
params: TypesGen.AuthorizationRequest,
|
params: TypesGen.AuthorizationRequest,
|
||||||
): Promise<TypesGen.AuthorizationResponse> => {
|
): 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
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => {
|
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
|
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 url = getURLWithSearchParams("/api/v2/users", filter)
|
||||||
const response = await axios.get<TypesGen.User[]>(url)
|
const response = await axios.get<TypesGen.User[]>(url)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOrganization = async (organizationId: string): Promise<TypesGen.Organization> => {
|
export const getOrganization = async (
|
||||||
const response = await axios.get<TypesGen.Organization>(`/api/v2/organizations/${organizationId}`)
|
organizationId: string,
|
||||||
|
): Promise<TypesGen.Organization> => {
|
||||||
|
const response = await axios.get<TypesGen.Organization>(
|
||||||
|
`/api/v2/organizations/${organizationId}`,
|
||||||
|
)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOrganizations = async (): Promise<TypesGen.Organization[]> => {
|
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
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTemplate = async (templateId: string): Promise<TypesGen.Template> => {
|
export const getTemplate = async (
|
||||||
const response = await axios.get<TypesGen.Template>(`/api/v2/templates/${templateId}`)
|
templateId: string,
|
||||||
|
): Promise<TypesGen.Template> => {
|
||||||
|
const response = await axios.get<TypesGen.Template>(
|
||||||
|
`/api/v2/templates/${templateId}`,
|
||||||
|
)
|
||||||
return response.data
|
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[]>(
|
const response = await axios.get<TypesGen.Template[]>(
|
||||||
`/api/v2/organizations/${organizationId}/templates`,
|
`/api/v2/organizations/${organizationId}/templates`,
|
||||||
)
|
)
|
||||||
@ -160,7 +182,9 @@ export const getTemplateByName = async (
|
|||||||
return response.data
|
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>(
|
const response = await axios.get<TypesGen.TemplateVersion>(
|
||||||
`/api/v2/templateversions/${versionId}`,
|
`/api/v2/templateversions/${versionId}`,
|
||||||
)
|
)
|
||||||
@ -198,12 +222,19 @@ export const updateTemplateMeta = async (
|
|||||||
templateId: string,
|
templateId: string,
|
||||||
data: TypesGen.UpdateTemplateMeta,
|
data: TypesGen.UpdateTemplateMeta,
|
||||||
): Promise<TypesGen.Template> => {
|
): 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
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteTemplate = async (templateId: string): Promise<TypesGen.Template> => {
|
export const deleteTemplate = async (
|
||||||
const response = await axios.delete<TypesGen.Template>(`/api/v2/templates/${templateId}`)
|
templateId: string,
|
||||||
|
): Promise<TypesGen.Template> => {
|
||||||
|
const response = await axios.delete<TypesGen.Template>(
|
||||||
|
`/api/v2/templates/${templateId}`,
|
||||||
|
)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,9 +242,12 @@ export const getWorkspace = async (
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
params?: TypesGen.WorkspaceOptions,
|
params?: TypesGen.WorkspaceOptions,
|
||||||
): Promise<TypesGen.Workspace> => {
|
): Promise<TypesGen.Workspace> => {
|
||||||
const response = await axios.get<TypesGen.Workspace>(`/api/v2/workspaces/${workspaceId}`, {
|
const response = await axios.get<TypesGen.Workspace>(
|
||||||
params,
|
`/api/v2/workspaces/${workspaceId}`,
|
||||||
})
|
{
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,12 +302,18 @@ export const getWorkspaceByOwnerAndName = async (
|
|||||||
|
|
||||||
const postWorkspaceBuild =
|
const postWorkspaceBuild =
|
||||||
(transition: WorkspaceBuildTransition) =>
|
(transition: WorkspaceBuildTransition) =>
|
||||||
async (workspaceId: string, template_version_id?: string): Promise<TypesGen.WorkspaceBuild> => {
|
async (
|
||||||
|
workspaceId: string,
|
||||||
|
template_version_id?: string,
|
||||||
|
): Promise<TypesGen.WorkspaceBuild> => {
|
||||||
const payload = {
|
const payload = {
|
||||||
transition,
|
transition,
|
||||||
template_version_id,
|
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
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,11 +324,15 @@ export const deleteWorkspace = postWorkspaceBuild("delete")
|
|||||||
export const cancelWorkspaceBuild = async (
|
export const cancelWorkspaceBuild = async (
|
||||||
workspaceBuildId: TypesGen.WorkspaceBuild["id"],
|
workspaceBuildId: TypesGen.WorkspaceBuild["id"],
|
||||||
): Promise<Types.Message> => {
|
): 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
|
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)
|
const response = await axios.post<TypesGen.User>("/api/v2/users", user)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
@ -338,17 +382,27 @@ export const updateProfile = async (
|
|||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const activateUser = async (userId: TypesGen.User["id"]): Promise<TypesGen.User> => {
|
export const activateUser = async (
|
||||||
const response = await axios.put<TypesGen.User>(`/api/v2/users/${userId}/status/activate`)
|
userId: TypesGen.User["id"],
|
||||||
|
): Promise<TypesGen.User> => {
|
||||||
|
const response = await axios.put<TypesGen.User>(
|
||||||
|
`/api/v2/users/${userId}/status/activate`,
|
||||||
|
)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen.User> => {
|
export const suspendUser = async (
|
||||||
const response = await axios.put<TypesGen.User>(`/api/v2/users/${userId}/status/suspend`)
|
userId: TypesGen.User["id"],
|
||||||
|
): Promise<TypesGen.User> => {
|
||||||
|
const response = await axios.put<TypesGen.User>(
|
||||||
|
`/api/v2/users/${userId}/status/suspend`,
|
||||||
|
)
|
||||||
return response.data
|
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}`)
|
return await axios.delete(`/api/v2/users/${userId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,10 +433,15 @@ export const createFirstUser = async (
|
|||||||
export const updateUserPassword = async (
|
export const updateUserPassword = async (
|
||||||
userId: TypesGen.User["id"],
|
userId: TypesGen.User["id"],
|
||||||
updatePassword: TypesGen.UpdateUserPasswordRequest,
|
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>> => {
|
export const getSiteRoles = async (): Promise<
|
||||||
const response = await axios.get<Array<TypesGen.AssignableRoles>>(`/api/v2/users/roles`)
|
Array<TypesGen.AssignableRoles>
|
||||||
|
> => {
|
||||||
|
const response = await axios.get<Array<TypesGen.AssignableRoles>>(
|
||||||
|
`/api/v2/users/roles`,
|
||||||
|
)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -390,17 +449,28 @@ export const updateUserRoles = async (
|
|||||||
roles: TypesGen.Role["name"][],
|
roles: TypesGen.Role["name"][],
|
||||||
userId: TypesGen.User["id"],
|
userId: TypesGen.User["id"],
|
||||||
): Promise<TypesGen.User> => {
|
): 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
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getUserSSHKey = async (userId = "me"): Promise<TypesGen.GitSSHKey> => {
|
export const getUserSSHKey = async (
|
||||||
const response = await axios.get<TypesGen.GitSSHKey>(`/api/v2/users/${userId}/gitsshkey`)
|
userId = "me",
|
||||||
|
): Promise<TypesGen.GitSSHKey> => {
|
||||||
|
const response = await axios.get<TypesGen.GitSSHKey>(
|
||||||
|
`/api/v2/users/${userId}/gitsshkey`,
|
||||||
|
)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const regenerateUserSSHKey = async (userId = "me"): Promise<TypesGen.GitSSHKey> => {
|
export const regenerateUserSSHKey = async (
|
||||||
const response = await axios.put<TypesGen.GitSSHKey>(`/api/v2/users/${userId}/gitsshkey`)
|
userId = "me",
|
||||||
|
): Promise<TypesGen.GitSSHKey> => {
|
||||||
|
const response = await axios.put<TypesGen.GitSSHKey>(
|
||||||
|
`/api/v2/users/${userId}/gitsshkey`,
|
||||||
|
)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,7 +509,9 @@ export const putWorkspaceExtension = async (
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
newDeadline: dayjs.Dayjs,
|
newDeadline: dayjs.Dayjs,
|
||||||
): Promise<void> => {
|
): 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> => {
|
export const getEntitlements = async (): Promise<TypesGen.Entitlements> => {
|
||||||
@ -479,7 +551,9 @@ export const getAuditLogsCount = async (
|
|||||||
if (options.q) {
|
if (options.q) {
|
||||||
searchParams.set("q", 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
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,12 +564,15 @@ export const getTemplateDAUs = async (
|
|||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getApplicationsHost = async (): Promise<TypesGen.GetAppHostResponse> => {
|
export const getApplicationsHost =
|
||||||
const response = await axios.get(`/api/v2/applications/host`)
|
async (): Promise<TypesGen.GetAppHostResponse> => {
|
||||||
return response.data
|
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}`)
|
const response = await axios.get(`/api/v2/workspace-quota/${userID}`)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { getValidationErrorMessage, isApiError, mapApiErrorToFieldErrors } from "./errors"
|
import {
|
||||||
|
getValidationErrorMessage,
|
||||||
|
isApiError,
|
||||||
|
mapApiErrorToFieldErrors,
|
||||||
|
} from "./errors"
|
||||||
|
|
||||||
describe("isApiError", () => {
|
describe("isApiError", () => {
|
||||||
it("returns true when the object is an API Error", () => {
|
it("returns true when the object is an API Error", () => {
|
||||||
@ -8,7 +12,9 @@ describe("isApiError", () => {
|
|||||||
response: {
|
response: {
|
||||||
data: {
|
data: {
|
||||||
message: "Invalid entry",
|
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(
|
expect(
|
||||||
mapApiErrorToFieldErrors({
|
mapApiErrorToFieldErrors({
|
||||||
message: "Invalid entry",
|
message: "Invalid entry",
|
||||||
validations: [{ detail: "Username is already in use", field: "username" }],
|
validations: [
|
||||||
|
{ detail: "Username is already in use", field: "username" },
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
username: "Username is already in use",
|
username: "Username is already in use",
|
||||||
|
@ -19,7 +19,9 @@ export interface ApiErrorResponse {
|
|||||||
validations?: FieldError[]
|
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
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
|
||||||
export const isApiError = (err: any): err is ApiError => {
|
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 =>
|
export const hasApiFieldErrors = (error: ApiError): boolean =>
|
||||||
Array.isArray(error.response.data.validations)
|
Array.isArray(error.response.data.validations)
|
||||||
|
|
||||||
export const mapApiErrorToFieldErrors = (apiErrorResponse: ApiErrorResponse): FieldErrors => {
|
export const mapApiErrorToFieldErrors = (
|
||||||
|
apiErrorResponse: ApiErrorResponse,
|
||||||
|
): FieldErrors => {
|
||||||
const result: FieldErrors = {}
|
const result: FieldErrors = {}
|
||||||
|
|
||||||
if (apiErrorResponse.validations) {
|
if (apiErrorResponse.validations) {
|
||||||
for (const error of 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
|
* @returns a combined validation error message if the error is an ApiError
|
||||||
* and contains validation messages for different form fields.
|
* 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 =
|
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")
|
return validationErrors.map((error) => error.detail).join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getErrorDetail = (error: Error | ApiError | unknown): string | undefined | null =>
|
export const getErrorDetail = (
|
||||||
isApiError(error) ? error.response.data.detail : error instanceof Error ? error.stack : null
|
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"
|
export type LoginType = "github" | "oidc" | "password" | "token"
|
||||||
|
|
||||||
// From codersdk/parameters.go
|
// From codersdk/parameters.go
|
||||||
export type ParameterDestinationScheme = "environment_variable" | "none" | "provisioner_variable"
|
export type ParameterDestinationScheme =
|
||||||
|
| "environment_variable"
|
||||||
|
| "none"
|
||||||
|
| "provisioner_variable"
|
||||||
|
|
||||||
// From codersdk/parameters.go
|
// From codersdk/parameters.go
|
||||||
export type ParameterScope = "import_job" | "template" | "workspace"
|
export type ParameterScope = "import_job" | "template" | "workspace"
|
||||||
@ -753,7 +756,11 @@ export type UserStatus = "active" | "suspended"
|
|||||||
export type WorkspaceAgentStatus = "connected" | "connecting" | "disconnected"
|
export type WorkspaceAgentStatus = "connected" | "connecting" | "disconnected"
|
||||||
|
|
||||||
// From codersdk/workspaceapps.go
|
// From codersdk/workspaceapps.go
|
||||||
export type WorkspaceAppHealth = "disabled" | "healthy" | "initializing" | "unhealthy"
|
export type WorkspaceAppHealth =
|
||||||
|
| "disabled"
|
||||||
|
| "healthy"
|
||||||
|
| "initializing"
|
||||||
|
| "unhealthy"
|
||||||
|
|
||||||
// From codersdk/workspacebuilds.go
|
// From codersdk/workspacebuilds.go
|
||||||
export type WorkspaceStatus =
|
export type WorkspaceStatus =
|
||||||
|
@ -32,7 +32,10 @@ export const AlertBanner: FC<AlertBannerProps> = ({
|
|||||||
|
|
||||||
// if an error is passed in, display that error, otherwise
|
// if an error is passed in, display that error, otherwise
|
||||||
// display the text passed in, e.g. warning text
|
// 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
|
// if we have an error, check if there's detail to display
|
||||||
const detail = error ? getErrorDetail(error) : undefined
|
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 RefreshIcon from "@material-ui/icons/Refresh"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
type AlertBannerCtasProps = Pick<AlertBannerProps, "actions" | "dismissible" | "retry"> & {
|
type AlertBannerCtasProps = Pick<
|
||||||
|
AlertBannerProps,
|
||||||
|
"actions" | "dismissible" | "retry"
|
||||||
|
> & {
|
||||||
setOpen: (arg0: boolean) => void
|
setOpen: (arg0: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,12 +23,18 @@ export const AlertBannerCtas: FC<AlertBannerCtasProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Stack direction="row">
|
<Stack direction="row">
|
||||||
{/* CTAs passed in by the consumer */}
|
{/* 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 CTA */}
|
||||||
{retry && (
|
{retry && (
|
||||||
<div>
|
<div>
|
||||||
<Button size="small" onClick={retry} startIcon={<RefreshIcon />} variant="outlined">
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={retry}
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
{t("ctas.retry")}
|
{t("ctas.retry")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,13 +4,26 @@ import { colors } from "theme/colors"
|
|||||||
import { Severity } from "./alertTypes"
|
import { Severity } from "./alertTypes"
|
||||||
import { ReactElement } from "react"
|
import { ReactElement } from "react"
|
||||||
|
|
||||||
export const severityConstants: Record<Severity, { color: string; icon: ReactElement }> = {
|
export const severityConstants: Record<
|
||||||
|
Severity,
|
||||||
|
{ color: string; icon: ReactElement }
|
||||||
|
> = {
|
||||||
warning: {
|
warning: {
|
||||||
color: colors.orange[7],
|
color: colors.orange[7],
|
||||||
icon: <ReportProblemOutlinedIcon fontSize="small" style={{ color: colors.orange[7] }} />,
|
icon: (
|
||||||
|
<ReportProblemOutlinedIcon
|
||||||
|
fontSize="small"
|
||||||
|
style={{ color: colors.orange[7] }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
color: colors.red[7],
|
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"
|
import { generateRandomString } from "../../util/random"
|
||||||
|
|
||||||
export const Language = {
|
export const Language = {
|
||||||
appTitle: (appName: string, identifier: string): string => `${appName} - ${identifier}`,
|
appTitle: (appName: string, identifier: string): string =>
|
||||||
|
`${appName} - ${identifier}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppLinkProps {
|
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
|
// The backend redirects if the trailing slash isn't included, so we add it
|
||||||
// here to avoid extra roundtrips.
|
// here to avoid extra roundtrips.
|
||||||
let href = `/@${username}/${workspaceName}.${agentName}/apps/${encodeURIComponent(appName)}/`
|
let href = `/@${username}/${workspaceName}.${agentName}/apps/${encodeURIComponent(
|
||||||
|
appName,
|
||||||
|
)}/`
|
||||||
if (appCommand) {
|
if (appCommand) {
|
||||||
href = `/@${username}/${workspaceName}.${agentName}/terminal?command=${encodeURIComponent(
|
href = `/@${username}/${workspaceName}.${agentName}/terminal?command=${encodeURIComponent(
|
||||||
appCommand,
|
appCommand,
|
||||||
@ -52,7 +55,11 @@ export const AppLink: FC<PropsWithChildren<AppLinkProps>> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let canClick = true
|
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 = ""
|
let tooltip = ""
|
||||||
if (health === "initializing") {
|
if (health === "initializing") {
|
||||||
canClick = false
|
canClick = false
|
||||||
@ -71,7 +78,12 @@ export const AppLink: FC<PropsWithChildren<AppLinkProps>> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const button = (
|
const button = (
|
||||||
<Button size="small" startIcon={icon} className={styles.button} disabled={!canClick}>
|
<Button
|
||||||
|
size="small"
|
||||||
|
startIcon={icon}
|
||||||
|
className={styles.button}
|
||||||
|
disabled={!canClick}
|
||||||
|
>
|
||||||
{appName}
|
{appName}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
@ -21,7 +21,9 @@ const getDiffValue = (value: unknown): string => {
|
|||||||
return value.toString()
|
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 styles = useStyles()
|
||||||
const diffEntries = Object.entries(diff)
|
const diffEntries = Object.entries(diff)
|
||||||
|
|
||||||
@ -34,7 +36,12 @@ export const AuditLogDiff: React.FC<{ diff: AuditLog["diff"] }> = ({ diff }) =>
|
|||||||
<div className={styles.diffIcon}>-</div>
|
<div className={styles.diffIcon}>-</div>
|
||||||
<div>
|
<div>
|
||||||
{attrName}:{" "}
|
{attrName}:{" "}
|
||||||
<span className={combineClasses([styles.diffValue, styles.diffValueOld])}>
|
<span
|
||||||
|
className={combineClasses([
|
||||||
|
styles.diffValue,
|
||||||
|
styles.diffValueOld,
|
||||||
|
])}
|
||||||
|
>
|
||||||
{getDiffValue(valueDiff.old)}
|
{getDiffValue(valueDiff.old)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -48,7 +55,12 @@ export const AuditLogDiff: React.FC<{ diff: AuditLog["diff"] }> = ({ diff }) =>
|
|||||||
<div className={styles.diffIcon}>+</div>
|
<div className={styles.diffIcon}>+</div>
|
||||||
<div>
|
<div>
|
||||||
{attrName}:{" "}
|
{attrName}:{" "}
|
||||||
<span className={combineClasses([styles.diffValue, styles.diffValueNew])}>
|
<span
|
||||||
|
className={combineClasses([
|
||||||
|
styles.diffValue,
|
||||||
|
styles.diffValueNew,
|
||||||
|
])}
|
||||||
|
>
|
||||||
{getDiffValue(valueDiff.new)}
|
{getDiffValue(valueDiff.new)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,10 @@ import { makeStyles } from "@material-ui/core/styles"
|
|||||||
import TableCell from "@material-ui/core/TableCell"
|
import TableCell from "@material-ui/core/TableCell"
|
||||||
import TableRow from "@material-ui/core/TableRow"
|
import TableRow from "@material-ui/core/TableRow"
|
||||||
import { AuditLog } from "api/typesGenerated"
|
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 { Pill } from "components/Pill/Pill"
|
||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack"
|
||||||
import { UserAvatar } from "components/UserAvatar/UserAvatar"
|
import { UserAvatar } from "components/UserAvatar/UserAvatar"
|
||||||
@ -13,7 +16,9 @@ import userAgentParser from "ua-parser-js"
|
|||||||
import { createDayString } from "util/createDayString"
|
import { createDayString } from "util/createDayString"
|
||||||
import { AuditLogDiff } from "./AuditLogDiff"
|
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) {
|
if (httpStatus >= 300 && httpStatus < 500) {
|
||||||
return "warning"
|
return "warning"
|
||||||
}
|
}
|
||||||
@ -47,7 +52,9 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
|
|||||||
const shouldDisplayDiff = diffs.length > 0
|
const shouldDisplayDiff = diffs.length > 0
|
||||||
const { os, browser } = userAgentParser(auditLog.user_agent)
|
const { os, browser } = userAgentParser(auditLog.user_agent)
|
||||||
const notAvailableLabel = "Not available"
|
const notAvailableLabel = "Not available"
|
||||||
const displayBrowserInfo = browser.name ? `${browser.name} ${browser.version}` : notAvailableLabel
|
const displayBrowserInfo = browser.name
|
||||||
|
? `${browser.name} ${browser.version}`
|
||||||
|
: notAvailableLabel
|
||||||
|
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
if (shouldDisplayDiff) {
|
if (shouldDisplayDiff) {
|
||||||
@ -85,9 +92,13 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
|
|||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
className={styles.auditLogResume}
|
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>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
@ -101,7 +112,11 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
|
|||||||
type={pillTypeByHttpStatus(auditLog.status_code)}
|
type={pillTypeByHttpStatus(auditLog.status_code)}
|
||||||
text={auditLog.status_code.toString()}
|
text={auditLog.status_code.toString()}
|
||||||
/>
|
/>
|
||||||
<Stack direction="row" alignItems="center" className={styles.auditLogExtraInfo}>
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
className={styles.auditLogExtraInfo}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<strong>IP</strong> {auditLog.ip ?? notAvailableLabel}
|
<strong>IP</strong> {auditLog.ip ?? notAvailableLabel}
|
||||||
</div>
|
</div>
|
||||||
@ -115,7 +130,11 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<div className={shouldDisplayDiff ? undefined : styles.disabledDropdownIcon}>
|
<div
|
||||||
|
className={
|
||||||
|
shouldDisplayDiff ? undefined : styles.disabledDropdownIcon
|
||||||
|
}
|
||||||
|
>
|
||||||
{isDiffOpen ? <CloseDropdown /> : <OpenDropdown />}
|
{isDiffOpen ? <CloseDropdown /> : <OpenDropdown />}
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -6,7 +6,9 @@ export default {
|
|||||||
component: AvatarData,
|
component: AvatarData,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Template: Story<AvatarDataProps> = (args: AvatarDataProps) => <AvatarData {...args} />
|
const Template: Story<AvatarDataProps> = (args: AvatarDataProps) => (
|
||||||
|
<AvatarData {...args} />
|
||||||
|
)
|
||||||
|
|
||||||
export const Example = Template.bind({})
|
export const Example = Template.bind({})
|
||||||
Example.args = {
|
Example.args = {
|
||||||
|
@ -38,14 +38,22 @@ export const AvatarData: FC<PropsWithChildren<AvatarDataProps>> = ({
|
|||||||
{link ? (
|
{link ? (
|
||||||
<Link to={link} underline="none" component={RouterLink}>
|
<Link to={link} underline="none" component={RouterLink}>
|
||||||
<TableCellData>
|
<TableCellData>
|
||||||
<TableCellDataPrimary highlight={highlightTitle}>{title}</TableCellDataPrimary>
|
<TableCellDataPrimary highlight={highlightTitle}>
|
||||||
{subtitle && <TableCellDataSecondary>{subtitle}</TableCellDataSecondary>}
|
{title}
|
||||||
|
</TableCellDataPrimary>
|
||||||
|
{subtitle && (
|
||||||
|
<TableCellDataSecondary>{subtitle}</TableCellDataSecondary>
|
||||||
|
)}
|
||||||
</TableCellData>
|
</TableCellData>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<TableCellData>
|
<TableCellData>
|
||||||
<TableCellDataPrimary highlight={highlightTitle}>{title}</TableCellDataPrimary>
|
<TableCellDataPrimary highlight={highlightTitle}>
|
||||||
{subtitle && <TableCellDataSecondary>{subtitle}</TableCellDataSecondary>}
|
{title}
|
||||||
|
</TableCellDataPrimary>
|
||||||
|
{subtitle && (
|
||||||
|
<TableCellDataSecondary>{subtitle}</TableCellDataSecondary>
|
||||||
|
)}
|
||||||
</TableCellData>
|
</TableCellData>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,15 +26,9 @@ interface BorderedMenuRowProps {
|
|||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BorderedMenuRow: FC<React.PropsWithChildren<BorderedMenuRowProps>> = ({
|
export const BorderedMenuRow: FC<
|
||||||
active,
|
React.PropsWithChildren<BorderedMenuRowProps>
|
||||||
description,
|
> = ({ active, description, Icon, path, title, variant, onClick }) => {
|
||||||
Icon,
|
|
||||||
path,
|
|
||||||
title,
|
|
||||||
variant,
|
|
||||||
onClick,
|
|
||||||
}) => {
|
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -53,7 +47,11 @@ export const BorderedMenuRow: FC<React.PropsWithChildren<BorderedMenuRowProps>>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{description && (
|
{description && (
|
||||||
<Typography className={styles.description} color="textSecondary" variant="caption">
|
<Typography
|
||||||
|
className={styles.description}
|
||||||
|
color="textSecondary"
|
||||||
|
variant="caption"
|
||||||
|
>
|
||||||
{ellipsizeText(description)}
|
{ellipsizeText(description)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
@ -11,7 +11,10 @@ import useTheme from "@material-ui/styles/useTheme"
|
|||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { useNavigate, useParams } from "react-router-dom"
|
import { useNavigate, useParams } from "react-router-dom"
|
||||||
import * as TypesGen from "../../api/typesGenerated"
|
import * as TypesGen from "../../api/typesGenerated"
|
||||||
import { displayWorkspaceBuildDuration, getDisplayWorkspaceBuildStatus } from "../../util/workspace"
|
import {
|
||||||
|
displayWorkspaceBuildDuration,
|
||||||
|
getDisplayWorkspaceBuildStatus,
|
||||||
|
} from "../../util/workspace"
|
||||||
import { EmptyState } from "../EmptyState/EmptyState"
|
import { EmptyState } from "../EmptyState/EmptyState"
|
||||||
import { TableCellLink } from "../TableCellLink/TableCellLink"
|
import { TableCellLink } from "../TableCellLink/TableCellLink"
|
||||||
import { TableLoader } from "../TableLoader/TableLoader"
|
import { TableLoader } from "../TableLoader/TableLoader"
|
||||||
@ -72,7 +75,9 @@ export const BuildsTable: FC<React.PropsWithChildren<BuildsTableProps>> = ({
|
|||||||
}}
|
}}
|
||||||
className={styles.clickableTableRow}
|
className={styles.clickableTableRow}
|
||||||
>
|
>
|
||||||
<TableCellLink to={buildPageLink}>{build.transition}</TableCellLink>
|
<TableCellLink to={buildPageLink}>
|
||||||
|
{build.transition}
|
||||||
|
</TableCellLink>
|
||||||
<TableCellLink to={buildPageLink}>
|
<TableCellLink to={buildPageLink}>
|
||||||
<span style={{ color: theme.palette.text.secondary }}>
|
<span style={{ color: theme.palette.text.secondary }}>
|
||||||
{displayWorkspaceBuildDuration(build)}
|
{displayWorkspaceBuildDuration(build)}
|
||||||
@ -84,7 +89,10 @@ export const BuildsTable: FC<React.PropsWithChildren<BuildsTableProps>> = ({
|
|||||||
</span>
|
</span>
|
||||||
</TableCellLink>
|
</TableCellLink>
|
||||||
<TableCellLink to={buildPageLink}>
|
<TableCellLink to={buildPageLink}>
|
||||||
<span style={{ color: status.color }} className={styles.status}>
|
<span
|
||||||
|
style={{ color: status.color }}
|
||||||
|
className={styles.status}
|
||||||
|
>
|
||||||
{status.status}
|
{status.status}
|
||||||
</span>
|
</span>
|
||||||
</TableCellLink>
|
</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({})
|
export const Example = Template.bind({})
|
||||||
Example.args = {
|
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({})
|
export const Example = Template.bind({})
|
||||||
Example.args = {
|
Example.args = {
|
||||||
|
@ -11,7 +11,9 @@ export interface CondProps {
|
|||||||
* @param condition boolean expression indicating whether the child should be rendered, or undefined
|
* @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.
|
* @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}</>
|
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
|
* @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
|
* @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[]
|
const childArray = Children.toArray(children) as JSX.Element[]
|
||||||
if (childArray.length === 0) {
|
if (childArray.length === 0) {
|
||||||
return null
|
return null
|
||||||
@ -35,7 +39,9 @@ export const ChooseOne = ({ children }: PropsWithChildren): JSX.Element | null =
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (conditionedOptions.some((cond) => cond.props.condition === undefined)) {
|
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)
|
const chosen = conditionedOptions.find((child) => child.props.condition)
|
||||||
return chosen ?? defaultCase
|
return chosen ?? defaultCase
|
||||||
|
@ -6,7 +6,9 @@ export default {
|
|||||||
component: Maybe,
|
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({})
|
export const ConditionIsTrue = Template.bind({})
|
||||||
ConditionIsTrue.args = {
|
ConditionIsTrue.args = {
|
||||||
|
@ -53,7 +53,9 @@ export const CopyButton: React.FC<React.PropsWithChildren<CopyButtonProps>> = ({
|
|||||||
setIsCopied(false)
|
setIsCopied(false)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
} else {
|
} 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) {
|
if (err instanceof Error) {
|
||||||
wrappedErr.stack = err.stack
|
wrappedErr.stack = err.stack
|
||||||
}
|
}
|
||||||
@ -64,7 +66,9 @@ export const CopyButton: React.FC<React.PropsWithChildren<CopyButtonProps>> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={tooltipTitle} placement="top">
|
<Tooltip title={tooltipTitle} placement="top">
|
||||||
<div className={combineClasses([styles.copyButtonWrapper, wrapperClassName])}>
|
<div
|
||||||
|
className={combineClasses([styles.copyButtonWrapper, wrapperClassName])}
|
||||||
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
className={combineClasses([styles.copyButton, buttonClassName])}
|
className={combineClasses([styles.copyButton, buttonClassName])}
|
||||||
onClick={copyToClipboard}
|
onClick={copyToClipboard}
|
||||||
|
@ -4,7 +4,11 @@ import { FormikContextType, FormikErrors, useFormik } from "formik"
|
|||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import * as Yup from "yup"
|
import * as Yup from "yup"
|
||||||
import * as TypesGen from "../../api/typesGenerated"
|
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 { FormFooter } from "../FormFooter/FormFooter"
|
||||||
import { FullPageForm } from "../FullPageForm/FullPageForm"
|
import { FullPageForm } from "../FullPageForm/FullPageForm"
|
||||||
import { Stack } from "../Stack/Stack"
|
import { Stack } from "../Stack/Stack"
|
||||||
@ -30,21 +34,19 @@ export interface CreateUserFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const validationSchema = Yup.object({
|
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),
|
password: Yup.string().required(Language.passwordRequired),
|
||||||
username: nameValidator(Language.usernameLabel),
|
username: nameValidator(Language.usernameLabel),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const CreateUserForm: FC<React.PropsWithChildren<CreateUserFormProps>> = ({
|
export const CreateUserForm: FC<
|
||||||
onSubmit,
|
React.PropsWithChildren<CreateUserFormProps>
|
||||||
onCancel,
|
> = ({ onSubmit, onCancel, formErrors, isLoading, error, myOrgId }) => {
|
||||||
formErrors,
|
const form: FormikContextType<TypesGen.CreateUserRequest> =
|
||||||
isLoading,
|
useFormik<TypesGen.CreateUserRequest>({
|
||||||
error,
|
|
||||||
myOrgId,
|
|
||||||
}) => {
|
|
||||||
const form: FormikContextType<TypesGen.CreateUserRequest> = useFormik<TypesGen.CreateUserRequest>(
|
|
||||||
{
|
|
||||||
initialValues: {
|
initialValues: {
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
@ -53,9 +55,11 @@ export const CreateUserForm: FC<React.PropsWithChildren<CreateUserFormProps>> =
|
|||||||
},
|
},
|
||||||
validationSchema,
|
validationSchema,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
},
|
})
|
||||||
|
const getFieldHelpers = getFormHelpers<TypesGen.CreateUserRequest>(
|
||||||
|
form,
|
||||||
|
formErrors,
|
||||||
)
|
)
|
||||||
const getFieldHelpers = getFormHelpers<TypesGen.CreateUserRequest>(form, formErrors)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FullPageForm title="Create user" onCancel={onCancel}>
|
<FullPageForm title="Create user" onCancel={onCancel}>
|
||||||
|
@ -21,7 +21,9 @@ export default {
|
|||||||
},
|
},
|
||||||
} as ComponentMeta<typeof ConfirmDialog>
|
} as ComponentMeta<typeof ConfirmDialog>
|
||||||
|
|
||||||
const Template: Story<ConfirmDialogProps> = (args) => <ConfirmDialog {...args} />
|
const Template: Story<ConfirmDialogProps> = (args) => (
|
||||||
|
<ConfirmDialog {...args} />
|
||||||
|
)
|
||||||
|
|
||||||
export const DeleteDialog = Template.bind({})
|
export const DeleteDialog = Template.bind({})
|
||||||
DeleteDialog.args = {
|
DeleteDialog.args = {
|
||||||
|
@ -2,7 +2,11 @@ import DialogActions from "@material-ui/core/DialogActions"
|
|||||||
import { alpha, makeStyles } from "@material-ui/core/styles"
|
import { alpha, makeStyles } from "@material-ui/core/styles"
|
||||||
import Typography from "@material-ui/core/Typography"
|
import Typography from "@material-ui/core/Typography"
|
||||||
import React, { ReactNode } from "react"
|
import React, { ReactNode } from "react"
|
||||||
import { Dialog, DialogActionButtons, DialogActionButtonsProps } from "../Dialog"
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogActionButtons,
|
||||||
|
DialogActionButtonsProps,
|
||||||
|
} from "../Dialog"
|
||||||
import { ConfirmDialogType } from "../types"
|
import { ConfirmDialogType } from "../types"
|
||||||
|
|
||||||
interface ConfirmDialogTypeConfig {
|
interface ConfirmDialogTypeConfig {
|
||||||
@ -10,7 +14,10 @@ interface ConfirmDialogTypeConfig {
|
|||||||
hideCancel: boolean
|
hideCancel: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIRM_DIALOG_DEFAULTS: Record<ConfirmDialogType, ConfirmDialogTypeConfig> = {
|
const CONFIRM_DIALOG_DEFAULTS: Record<
|
||||||
|
ConfirmDialogType,
|
||||||
|
ConfirmDialogTypeConfig
|
||||||
|
> = {
|
||||||
delete: {
|
delete: {
|
||||||
confirmText: "Delete",
|
confirmText: "Delete",
|
||||||
hideCancel: false,
|
hideCancel: false,
|
||||||
@ -26,7 +33,10 @@ const CONFIRM_DIALOG_DEFAULTS: Record<ConfirmDialogType, ConfirmDialogTypeConfig
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfirmDialogProps
|
export interface ConfirmDialogProps
|
||||||
extends Omit<DialogActionButtonsProps, "color" | "confirmDialog" | "onCancel"> {
|
extends Omit<
|
||||||
|
DialogActionButtonsProps,
|
||||||
|
"color" | "confirmDialog" | "onCancel"
|
||||||
|
> {
|
||||||
readonly description?: React.ReactNode
|
readonly description?: React.ReactNode
|
||||||
/**
|
/**
|
||||||
* hideCancel hides the cancel button when set true, and shows the cancel
|
* 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,
|
* 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.
|
* 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,
|
cancelText,
|
||||||
confirmLoading,
|
confirmLoading,
|
||||||
confirmText,
|
confirmText,
|
||||||
@ -100,7 +112,12 @@ export const ConfirmDialog: React.FC<React.PropsWithChildren<ConfirmDialogProps>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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}>
|
<div className={styles.dialogContent}>
|
||||||
<Typography className={styles.titleText} variant="h3">
|
<Typography className={styles.titleText} variant="h3">
|
||||||
{title}
|
{title}
|
||||||
|
@ -22,7 +22,8 @@ export default {
|
|||||||
defaultValue: "MyFoo",
|
defaultValue: "MyFoo",
|
||||||
},
|
},
|
||||||
info: {
|
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>
|
} as ComponentMeta<typeof DeleteDialog>
|
||||||
|
@ -30,7 +30,10 @@ describe("DeleteDialog", () => {
|
|||||||
name="MyTemplate"
|
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)
|
const textField = screen.getByLabelText(labelText)
|
||||||
await userEvent.type(textField, "MyTemplateWrong")
|
await userEvent.type(textField, "MyTemplateWrong")
|
||||||
const confirmButton = screen.getByRole("button", { name: "Delete" })
|
const confirmButton = screen.getByRole("button", { name: "Delete" })
|
||||||
@ -48,7 +51,10 @@ describe("DeleteDialog", () => {
|
|||||||
name="MyTemplate"
|
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)
|
const textField = screen.getByLabelText(labelText)
|
||||||
await userEvent.type(textField, "MyTemplate")
|
await userEvent.type(textField, "MyTemplate")
|
||||||
const confirmButton = screen.getByRole("button", { name: "Delete" })
|
const confirmButton = screen.getByRole("button", { name: "Delete" })
|
||||||
|
@ -18,15 +18,9 @@ export interface DeleteDialogProps {
|
|||||||
confirmLoading?: boolean
|
confirmLoading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteDialog: React.FC<React.PropsWithChildren<DeleteDialogProps>> = ({
|
export const DeleteDialog: React.FC<
|
||||||
isOpen,
|
React.PropsWithChildren<DeleteDialogProps>
|
||||||
onCancel,
|
> = ({ isOpen, onCancel, onConfirm, entity, info, name, confirmLoading }) => {
|
||||||
onConfirm,
|
|
||||||
entity,
|
|
||||||
info,
|
|
||||||
name,
|
|
||||||
confirmLoading,
|
|
||||||
}) => {
|
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const { t } = useTranslation("common")
|
const { t } = useTranslation("common")
|
||||||
const [nameValue, setNameValue] = useState("")
|
const [nameValue, setNameValue] = useState("")
|
||||||
@ -52,7 +46,9 @@ export const DeleteDialog: React.FC<React.PropsWithChildren<DeleteDialogProps>>
|
|||||||
label={t("deleteDialog.confirmLabel", { entity })}
|
label={t("deleteDialog.confirmLabel", { entity })}
|
||||||
/>
|
/>
|
||||||
<Maybe condition={nameValue.length > 0 && !confirmed}>
|
<Maybe condition={nameValue.length > 0 && !confirmed}>
|
||||||
<FormHelperText error>{t("deleteDialog.incorrectName", { entity })}</FormHelperText>
|
<FormHelperText error>
|
||||||
|
{t("deleteDialog.incorrectName", { entity })}
|
||||||
|
</FormHelperText>
|
||||||
</Maybe>
|
</Maybe>
|
||||||
</Stack>
|
</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 MuiDialogTitle from "@material-ui/core/DialogTitle"
|
||||||
import { alpha, darken, lighten, makeStyles } from "@material-ui/core/styles"
|
import { alpha, darken, lighten, makeStyles } from "@material-ui/core/styles"
|
||||||
import SvgIcon from "@material-ui/core/SvgIcon"
|
import SvgIcon from "@material-ui/core/SvgIcon"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { combineClasses } from "../../util/combineClasses"
|
import { combineClasses } from "../../util/combineClasses"
|
||||||
import { LoadingButton, LoadingButtonProps } from "../LoadingButton/LoadingButton"
|
import {
|
||||||
|
LoadingButton,
|
||||||
|
LoadingButtonProps,
|
||||||
|
} from "../LoadingButton/LoadingButton"
|
||||||
import { ConfirmDialogType } from "./types"
|
import { ConfirmDialogType } from "./types"
|
||||||
|
|
||||||
export interface DialogTitleProps {
|
export interface DialogTitleProps {
|
||||||
@ -19,7 +24,11 @@ export interface DialogTitleProps {
|
|||||||
/**
|
/**
|
||||||
* Override of Material UI's DialogTitle that allows for a supertitle and background icon
|
* 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()
|
const styles = useTitleStyles()
|
||||||
return (
|
return (
|
||||||
<MuiDialogTitle disableTypography>
|
<MuiDialogTitle disableTypography>
|
||||||
@ -164,7 +173,9 @@ const useButtonStyles = makeStyles((theme) => ({
|
|||||||
},
|
},
|
||||||
confirmDialogCancelButton: (props: StyleProps) => {
|
confirmDialogCancelButton: (props: StyleProps) => {
|
||||||
const color =
|
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 {
|
return {
|
||||||
background: alpha(color, 0.15),
|
background: alpha(color, 0.15),
|
||||||
color,
|
color,
|
||||||
@ -222,7 +233,10 @@ const useButtonStyles = makeStyles((theme) => ({
|
|||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
borderColor: theme.palette.error.main,
|
borderColor: theme.palette.error.main,
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
backgroundColor: alpha(theme.palette.error.main, theme.palette.action.hoverOpacity),
|
backgroundColor: alpha(
|
||||||
|
theme.palette.error.main,
|
||||||
|
theme.palette.action.hoverOpacity,
|
||||||
|
),
|
||||||
"@media (hover: none)": {
|
"@media (hover: none)": {
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
},
|
},
|
||||||
@ -239,7 +253,10 @@ const useButtonStyles = makeStyles((theme) => ({
|
|||||||
"&.MuiButton-text": {
|
"&.MuiButton-text": {
|
||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
backgroundColor: alpha(theme.palette.error.main, theme.palette.action.hoverOpacity),
|
backgroundColor: alpha(
|
||||||
|
theme.palette.error.main,
|
||||||
|
theme.palette.action.hoverOpacity,
|
||||||
|
),
|
||||||
"@media (hover: none)": {
|
"@media (hover: none)": {
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
},
|
},
|
||||||
@ -272,7 +289,10 @@ const useButtonStyles = makeStyles((theme) => ({
|
|||||||
color: theme.palette.success.main,
|
color: theme.palette.success.main,
|
||||||
borderColor: theme.palette.success.main,
|
borderColor: theme.palette.success.main,
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
backgroundColor: alpha(theme.palette.success.main, theme.palette.action.hoverOpacity),
|
backgroundColor: alpha(
|
||||||
|
theme.palette.success.main,
|
||||||
|
theme.palette.action.hoverOpacity,
|
||||||
|
),
|
||||||
"@media (hover: none)": {
|
"@media (hover: none)": {
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
},
|
},
|
||||||
@ -289,7 +309,10 @@ const useButtonStyles = makeStyles((theme) => ({
|
|||||||
"&.MuiButton-text": {
|
"&.MuiButton-text": {
|
||||||
color: theme.palette.success.main,
|
color: theme.palette.success.main,
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
backgroundColor: alpha(theme.palette.success.main, theme.palette.action.hoverOpacity),
|
backgroundColor: alpha(
|
||||||
|
theme.palette.success.main,
|
||||||
|
theme.palette.action.hoverOpacity,
|
||||||
|
),
|
||||||
"@media (hover: none)": {
|
"@media (hover: none)": {
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { Story } from "@storybook/react"
|
import { Story } from "@storybook/react"
|
||||||
import { MockUser } from "../../../testHelpers/renderHelpers"
|
import { MockUser } from "../../../testHelpers/renderHelpers"
|
||||||
import { ResetPasswordDialog, ResetPasswordDialogProps } from "./ResetPasswordDialog"
|
import {
|
||||||
|
ResetPasswordDialog,
|
||||||
|
ResetPasswordDialogProps,
|
||||||
|
} from "./ResetPasswordDialog"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "components/Dialogs/ResetPasswordDialog",
|
title: "components/Dialogs/ResetPasswordDialog",
|
||||||
@ -11,9 +14,9 @@ export default {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const Template: Story<ResetPasswordDialogProps> = (args: ResetPasswordDialogProps) => (
|
const Template: Story<ResetPasswordDialogProps> = (
|
||||||
<ResetPasswordDialog {...args} />
|
args: ResetPasswordDialogProps,
|
||||||
)
|
) => <ResetPasswordDialog {...args} />
|
||||||
|
|
||||||
export const Example = Template.bind({})
|
export const Example = Template.bind({})
|
||||||
Example.args = {
|
Example.args = {
|
||||||
|
@ -24,19 +24,16 @@ export const Language = {
|
|||||||
confirmText: "Reset password",
|
confirmText: "Reset password",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ResetPasswordDialog: FC<React.PropsWithChildren<ResetPasswordDialogProps>> = ({
|
export const ResetPasswordDialog: FC<
|
||||||
open,
|
React.PropsWithChildren<ResetPasswordDialogProps>
|
||||||
onClose,
|
> = ({ open, onClose, onConfirm, user, newPassword, loading }) => {
|
||||||
onConfirm,
|
|
||||||
user,
|
|
||||||
newPassword,
|
|
||||||
loading,
|
|
||||||
}) => {
|
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
const description = (
|
const description = (
|
||||||
<>
|
<>
|
||||||
<DialogContentText variant="subtitle2">{Language.message(user?.username)}</DialogContentText>
|
<DialogContentText variant="subtitle2">
|
||||||
|
{Language.message(user?.username)}
|
||||||
|
</DialogContentText>
|
||||||
<DialogContentText component="div" className={styles.codeBlock}>
|
<DialogContentText component="div" className={styles.codeBlock}>
|
||||||
<CodeExample code={newPassword ?? ""} className={styles.codeExample} />
|
<CodeExample code={newPassword ?? ""} className={styles.codeExample} />
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
|
@ -22,7 +22,12 @@ interface ArrowProps {
|
|||||||
|
|
||||||
export const OpenDropdown: FC<ArrowProps> = ({ margin = true, color }) => {
|
export const OpenDropdown: FC<ArrowProps> = ({ margin = true, color }) => {
|
||||||
const styles = useStyles({ margin, 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 }) => {
|
export const CloseDropdown: FC<ArrowProps> = ({ margin = true, color }) => {
|
||||||
|
@ -16,18 +16,26 @@ interface WorkspaceAction {
|
|||||||
handleAction: () => void
|
handleAction: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UpdateButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({ handleAction }) => {
|
export const UpdateButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
|
||||||
|
handleAction,
|
||||||
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const { t } = useTranslation("workspacePage")
|
const { t } = useTranslation("workspacePage")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button className={styles.actionButton} startIcon={<CloudQueueIcon />} onClick={handleAction}>
|
<Button
|
||||||
|
className={styles.actionButton}
|
||||||
|
startIcon={<CloudQueueIcon />}
|
||||||
|
onClick={handleAction}
|
||||||
|
>
|
||||||
{t("actionButton.update")}
|
{t("actionButton.update")}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StartButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({ handleAction }) => {
|
export const StartButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
|
||||||
|
handleAction,
|
||||||
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const { t } = useTranslation("workspacePage")
|
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 styles = useStyles()
|
||||||
const { t } = useTranslation("workspacePage")
|
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 styles = useStyles()
|
||||||
const { t } = useTranslation("workspacePage")
|
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()
|
const styles = useStyles()
|
||||||
|
|
||||||
// this is an icon button, so it's important to include an aria label
|
// this is an icon button, so it's important to include an aria label
|
||||||
@ -94,7 +108,9 @@ interface DisabledProps {
|
|||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DisabledButton: FC<React.PropsWithChildren<DisabledProps>> = ({ label }) => {
|
export const DisabledButton: FC<React.PropsWithChildren<DisabledProps>> = ({
|
||||||
|
label,
|
||||||
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -108,7 +124,9 @@ interface LoadingProps {
|
|||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionLoadingButton: FC<React.PropsWithChildren<LoadingProps>> = ({ label }) => {
|
export const ActionLoadingButton: FC<React.PropsWithChildren<LoadingProps>> = ({
|
||||||
|
label,
|
||||||
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
return (
|
return (
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { action } from "@storybook/addon-actions"
|
import { action } from "@storybook/addon-actions"
|
||||||
import { Story } from "@storybook/react"
|
import { Story } from "@storybook/react"
|
||||||
import { DeleteButton, DisabledButton, StartButton, UpdateButton } from "./ActionCtas"
|
import {
|
||||||
|
DeleteButton,
|
||||||
|
DisabledButton,
|
||||||
|
StartButton,
|
||||||
|
UpdateButton,
|
||||||
|
} from "./ActionCtas"
|
||||||
import { DropdownButton, DropdownButtonProps } from "./DropdownButton"
|
import { DropdownButton, DropdownButtonProps } from "./DropdownButton"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -8,14 +13,22 @@ export default {
|
|||||||
component: DropdownButton,
|
component: DropdownButton,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Template: Story<DropdownButtonProps> = (args) => <DropdownButton {...args} />
|
const Template: Story<DropdownButtonProps> = (args) => (
|
||||||
|
<DropdownButton {...args} />
|
||||||
|
)
|
||||||
|
|
||||||
export const WithDropdown = Template.bind({})
|
export const WithDropdown = Template.bind({})
|
||||||
WithDropdown.args = {
|
WithDropdown.args = {
|
||||||
primaryAction: <StartButton handleAction={action("start")} />,
|
primaryAction: <StartButton handleAction={action("start")} />,
|
||||||
secondaryActions: [
|
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,
|
canCancel: false,
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import Button from "@material-ui/core/Button"
|
import Button from "@material-ui/core/Button"
|
||||||
import Popover from "@material-ui/core/Popover"
|
import Popover from "@material-ui/core/Popover"
|
||||||
import { makeStyles, useTheme } from "@material-ui/core/styles"
|
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 { DropdownContent } from "components/DropdownButton/DropdownContent/DropdownContent"
|
||||||
import { FC, ReactNode, useRef, useState } from "react"
|
import { FC, ReactNode, useRef, useState } from "react"
|
||||||
import { CancelButton } from "./ActionCtas"
|
import { CancelButton } from "./ActionCtas"
|
||||||
@ -51,7 +54,9 @@ export const DropdownButton: FC<DropdownButtonProps> = ({
|
|||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<CloseDropdown />
|
<CloseDropdown />
|
||||||
) : (
|
) : (
|
||||||
<OpenDropdown color={canOpen ? undefined : theme.palette.action.disabled} />
|
<OpenDropdown
|
||||||
|
color={canOpen ? undefined : theme.palette.action.disabled}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Popover
|
<Popover
|
||||||
@ -105,6 +110,8 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
popoverPaper: {
|
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 */
|
/* secondary workspace CTAs */
|
||||||
export const DropdownContent: FC<React.PropsWithChildren<DropdownContentProps>> = ({
|
export const DropdownContent: FC<
|
||||||
secondaryActions,
|
React.PropsWithChildren<DropdownContentProps>
|
||||||
}) => {
|
> = ({ secondaryActions }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -13,7 +13,9 @@ describe("EmptyState", () => {
|
|||||||
|
|
||||||
it("renders description text", async () => {
|
it("renders description text", async () => {
|
||||||
// When
|
// When
|
||||||
render(<EmptyState message="Hello, world" description="Friendly greeting" />)
|
render(
|
||||||
|
<EmptyState message="Hello, world" description="Friendly greeting" />,
|
||||||
|
)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
await screen.findByText("Hello, world")
|
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/)
|
* 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.
|
* that you can directly pass props through to to customize the shape and layout of it.
|
||||||
*/
|
*/
|
||||||
export const EmptyState: FC<React.PropsWithChildren<EmptyStateProps>> = (props) => {
|
export const EmptyState: FC<React.PropsWithChildren<EmptyStateProps>> = (
|
||||||
const { message, description, cta, descriptionClassName, className, ...boxProps } = props
|
props,
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
message,
|
||||||
|
description,
|
||||||
|
cta,
|
||||||
|
descriptionClassName,
|
||||||
|
className,
|
||||||
|
...boxProps
|
||||||
|
} = props
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -37,7 +46,10 @@ export const EmptyState: FC<React.PropsWithChildren<EmptyStateProps>> = (props)
|
|||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="textSecondary"
|
color="textSecondary"
|
||||||
className={combineClasses([styles.description, descriptionClassName])}
|
className={combineClasses([
|
||||||
|
styles.description,
|
||||||
|
descriptionClassName,
|
||||||
|
])}
|
||||||
>
|
>
|
||||||
{description}
|
{description}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import { Story } from "@storybook/react"
|
import { Story } from "@storybook/react"
|
||||||
import { EnterpriseSnackbar, EnterpriseSnackbarProps } from "./EnterpriseSnackbar"
|
import {
|
||||||
|
EnterpriseSnackbar,
|
||||||
|
EnterpriseSnackbarProps,
|
||||||
|
} from "./EnterpriseSnackbar"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "components/EnterpriseSnackbar",
|
title: "components/EnterpriseSnackbar",
|
||||||
component: EnterpriseSnackbar,
|
component: EnterpriseSnackbar,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Template: Story<EnterpriseSnackbarProps> = (args: EnterpriseSnackbarProps) => (
|
const Template: Story<EnterpriseSnackbarProps> = (
|
||||||
<EnterpriseSnackbar {...args} />
|
args: EnterpriseSnackbarProps,
|
||||||
)
|
) => <EnterpriseSnackbar {...args} />
|
||||||
|
|
||||||
export const Error = Template.bind({})
|
export const Error = Template.bind({})
|
||||||
Error.args = {
|
Error.args = {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import IconButton from "@material-ui/core/IconButton"
|
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 { makeStyles } from "@material-ui/core/styles"
|
||||||
import CloseIcon from "@material-ui/icons/Close"
|
import CloseIcon from "@material-ui/icons/Close"
|
||||||
import { FC } from "react"
|
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/
|
* See original component's Material UI documentation here: https://material-ui.com/components/snackbars/
|
||||||
*/
|
*/
|
||||||
export const EnterpriseSnackbar: FC<React.PropsWithChildren<EnterpriseSnackbarProps>> = ({
|
export const EnterpriseSnackbar: FC<
|
||||||
onClose,
|
React.PropsWithChildren<EnterpriseSnackbarProps>
|
||||||
variant = "info",
|
> = ({ onClose, variant = "info", ContentProps = {}, action, ...rest }) => {
|
||||||
ContentProps = {},
|
|
||||||
action,
|
|
||||||
...rest
|
|
||||||
}) => {
|
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -11,7 +11,10 @@ interface ErrorBoundaryState {
|
|||||||
* Our app's Error Boundary
|
* Our app's Error Boundary
|
||||||
* Read more about React Error Boundaries: https://reactjs.org/docs/error-boundaries.html
|
* 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) {
|
constructor(props: ErrorBoundaryProps) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = { error: null }
|
this.state = { error: null }
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import Link from "@material-ui/core/Link"
|
import Link from "@material-ui/core/Link"
|
||||||
import makeStyles from "@material-ui/core/styles/makeStyles"
|
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 { PropsWithChildren, FC } from "react"
|
||||||
import Collapse from "@material-ui/core/Collapse"
|
import Collapse from "@material-ui/core/Collapse"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
@ -10,7 +10,9 @@ describe("Footer", () => {
|
|||||||
// Then
|
// Then
|
||||||
await screen.findByText("Copyright", { exact: false })
|
await screen.findByText("Copyright", { exact: false })
|
||||||
await screen.findByText(Language.buildInfoText(MockBuildInfo))
|
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) {
|
if (!reportBugLink) {
|
||||||
throw new Error("Bug report link not found in footer")
|
throw new Error("Bug report link not found in footer")
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,9 @@ export interface FooterProps {
|
|||||||
buildInfo?: TypesGen.BuildInfoResponse
|
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 styles = useFooterStyles()
|
||||||
|
|
||||||
const githubUrl = `https://github.com/coder/coder/issues/new?labels=needs+grooming&body=${encodeURIComponent(`Version: [\`${buildInfo?.version}\`](${buildInfo?.external_url})
|
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"
|
target="_blank"
|
||||||
href={buildInfo.external_url}
|
href={buildInfo.external_url}
|
||||||
>
|
>
|
||||||
<AccountTreeIcon className={styles.icon} /> {Language.buildInfoText(buildInfo)}
|
<AccountTreeIcon className={styles.icon} />{" "}
|
||||||
|
{Language.buildInfoText(buildInfo)}
|
||||||
</Link>
|
</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}
|
<AssistantIcon className={styles.icon} /> {Language.reportBugLink}
|
||||||
</Link>
|
</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}
|
<ChatIcon className={styles.icon} /> {Language.discordLink}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</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({})
|
export const Example = Template.bind({})
|
||||||
Example.args = {}
|
Example.args = {}
|
||||||
|
@ -8,9 +8,9 @@ export interface FormCloseButtonProps {
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FormCloseButton: React.FC<React.PropsWithChildren<FormCloseButtonProps>> = ({
|
export const FormCloseButton: React.FC<
|
||||||
onClose,
|
React.PropsWithChildren<FormCloseButtonProps>
|
||||||
}) => {
|
> = ({ onClose }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -3,7 +3,10 @@ import MenuItem from "@material-ui/core/MenuItem"
|
|||||||
import { makeStyles } from "@material-ui/core/styles"
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
import Typography from "@material-ui/core/Typography"
|
import Typography from "@material-ui/core/Typography"
|
||||||
import { ReactElement } from "react"
|
import { ReactElement } from "react"
|
||||||
import { FormTextField, FormTextFieldProps } from "../FormTextField/FormTextField"
|
import {
|
||||||
|
FormTextField,
|
||||||
|
FormTextFieldProps,
|
||||||
|
} from "../FormTextField/FormTextField"
|
||||||
|
|
||||||
export interface FormDropdownItem {
|
export interface FormDropdownItem {
|
||||||
value: string
|
value: string
|
||||||
|
@ -53,7 +53,11 @@ export const FormSection: FC<React.PropsWithChildren<FormSectionProps>> = ({
|
|||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
{description && (
|
{description && (
|
||||||
<Typography className={styles.descriptionText} variant="body2" color="textSecondary">
|
<Typography
|
||||||
|
className={styles.descriptionText}
|
||||||
|
variant="body2"
|
||||||
|
color="textSecondary"
|
||||||
|
>
|
||||||
{description}
|
{description}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
@ -12,7 +12,9 @@ namespace Helpers {
|
|||||||
export const requiredValidationMsg = "required"
|
export const requiredValidationMsg = "required"
|
||||||
|
|
||||||
export const Component: FC<
|
export const Component: FC<
|
||||||
React.PropsWithChildren<Omit<FormTextFieldProps<FormValues>, "form" | "formFieldName">>
|
React.PropsWithChildren<
|
||||||
|
Omit<FormTextFieldProps<FormValues>, "form" | "formFieldName">
|
||||||
|
>
|
||||||
> = (props) => {
|
> = (props) => {
|
||||||
const form = useFormik<FormValues>({
|
const form = useFormik<FormValues>({
|
||||||
initialValues: {
|
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",
|
variant = "outlined",
|
||||||
...rest
|
...rest
|
||||||
}: FormTextFieldProps<T>): ReactElement => {
|
}: 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
|
// Conversion to a string primitive is necessary as formFieldName is an in
|
||||||
// indexable type such as a string, number or enum.
|
// indexable type such as a string, number or enum.
|
||||||
@ -145,7 +146,10 @@ export const FormTextField = <T,>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const event = e
|
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)
|
event.target.value = eventTransform(e.target.value)
|
||||||
}
|
}
|
||||||
form.handleChange(event)
|
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()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -30,23 +30,36 @@ export const GlobalSnackbar: React.FC = () => {
|
|||||||
const [open, setOpen] = useState<boolean>(false)
|
const [open, setOpen] = useState<boolean>(false)
|
||||||
const [notification, setNotification] = useState<NotificationMsg>()
|
const [notification, setNotification] = useState<NotificationMsg>()
|
||||||
|
|
||||||
const handleNotification = useCallback<CustomEventListener<NotificationMsg>>((event) => {
|
const handleNotification = useCallback<CustomEventListener<NotificationMsg>>(
|
||||||
setNotification(event.detail)
|
(event) => {
|
||||||
setOpen(true)
|
setNotification(event.detail)
|
||||||
}, [])
|
setOpen(true)
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
useCustomEvent(SnackbarEventType, handleNotification)
|
useCustomEvent(SnackbarEventType, handleNotification)
|
||||||
|
|
||||||
const renderAdditionalMessage = (msg: AdditionalMessage, idx: number) => {
|
const renderAdditionalMessage = (msg: AdditionalMessage, idx: number) => {
|
||||||
if (isNotificationText(msg)) {
|
if (isNotificationText(msg)) {
|
||||||
return (
|
return (
|
||||||
<Typography key={idx} gutterBottom variant="body2" className={styles.messageSubtitle}>
|
<Typography
|
||||||
|
key={idx}
|
||||||
|
gutterBottom
|
||||||
|
variant="body2"
|
||||||
|
className={styles.messageSubtitle}
|
||||||
|
>
|
||||||
{msg}
|
{msg}
|
||||||
</Typography>
|
</Typography>
|
||||||
)
|
)
|
||||||
} else if (isNotificationTextPrefixed(msg)) {
|
} else if (isNotificationTextPrefixed(msg)) {
|
||||||
return (
|
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}
|
<strong>{msg.prefix}:</strong> {msg.text}
|
||||||
</Typography>
|
</Typography>
|
||||||
)
|
)
|
||||||
@ -77,7 +90,9 @@ export const GlobalSnackbar: React.FC = () => {
|
|||||||
variant={variantFromMsgType(notification.msgType)}
|
variant={variantFromMsgType(notification.msgType)}
|
||||||
message={
|
message={
|
||||||
<div className={styles.messageWrapper}>
|
<div className={styles.messageWrapper}>
|
||||||
{notification.msgType === MsgType.Error && <ErrorIcon className={styles.errorIcon} />}
|
{notification.msgType === MsgType.Error && (
|
||||||
|
<ErrorIcon className={styles.errorIcon} />
|
||||||
|
)}
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
<Typography variant="body1" className={styles.messageTitle}>
|
<Typography variant="body1" className={styles.messageTitle}>
|
||||||
{notification.msg}
|
{notification.msg}
|
||||||
|
@ -48,13 +48,17 @@ describe("Snackbar", () => {
|
|||||||
|
|
||||||
describe("displaySuccess", () => {
|
describe("displaySuccess", () => {
|
||||||
const originalWindowDispatchEvent = window.dispatchEvent
|
const originalWindowDispatchEvent = window.dispatchEvent
|
||||||
type TDispatchEventMock = jest.MockedFunction<(msg: CustomEvent<NotificationMsg>) => boolean>
|
type TDispatchEventMock = jest.MockedFunction<
|
||||||
|
(msg: CustomEvent<NotificationMsg>) => boolean
|
||||||
|
>
|
||||||
let dispatchEventMock: TDispatchEventMock
|
let dispatchEventMock: TDispatchEventMock
|
||||||
|
|
||||||
// Helper function to extract the notification event
|
// Helper function to extract the notification event
|
||||||
// that was sent to `dispatchEvent`. This lets us validate
|
// that was sent to `dispatchEvent`. This lets us validate
|
||||||
// the contents of the notification event are what we expect.
|
// 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] 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] is the first argument of the first call
|
||||||
// calls[0][0].detail is the 'detail' argument passed to the `CustomEvent` -
|
// calls[0][0].detail is the 'detail' argument passed to the `CustomEvent` -
|
||||||
@ -64,7 +68,8 @@ describe("Snackbar", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dispatchEventMock = jest.fn()
|
dispatchEventMock = jest.fn()
|
||||||
window.dispatchEvent = dispatchEventMock as unknown as typeof window.dispatchEvent
|
window.dispatchEvent =
|
||||||
|
dispatchEventMock as unknown as typeof window.dispatchEvent
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -84,7 +89,9 @@ describe("Snackbar", () => {
|
|||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(dispatchEventMock).toBeCalledTimes(1)
|
expect(dispatchEventMock).toBeCalledTimes(1)
|
||||||
expect(extractNotificationEvent(dispatchEventMock)).toStrictEqual(expected)
|
expect(extractNotificationEvent(dispatchEventMock)).toStrictEqual(
|
||||||
|
expected,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("can be called with a title and additional message", () => {
|
it("can be called with a title and additional message", () => {
|
||||||
@ -100,7 +107,9 @@ describe("Snackbar", () => {
|
|||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(dispatchEventMock).toBeCalledTimes(1)
|
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: AdditionalMessage | null,
|
||||||
): msg is NotificationTextPrefixed => {
|
): msg is NotificationTextPrefixed => {
|
||||||
if (msg) {
|
if (msg) {
|
||||||
return typeof msg !== "string" && Object.prototype.hasOwnProperty.call(msg, "prefix")
|
return (
|
||||||
|
typeof msg !== "string" &&
|
||||||
|
Object.prototype.hasOwnProperty.call(msg, "prefix")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -62,13 +65,25 @@ function dispatchNotificationEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const displayMsg = (msg: string, additionalMsg?: string): void => {
|
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 => {
|
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 => {
|
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) => (
|
export const UsersOutlinedIcon: typeof SvgIcon = (props: SvgIconProps) => (
|
||||||
<SvgIcon {...props} viewBox="0 0 20 20">
|
<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
|
<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"
|
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"
|
fill="#677693"
|
||||||
|
@ -5,7 +5,9 @@ import { LicenseBannerView } from "./LicenseBannerView"
|
|||||||
|
|
||||||
export const LicenseBanner: React.FC = () => {
|
export const LicenseBanner: React.FC = () => {
|
||||||
const xServices = useContext(XServiceContext)
|
const xServices = useContext(XServiceContext)
|
||||||
const [entitlementsState, entitlementsSend] = useActor(xServices.entitlementsXService)
|
const [entitlementsState, entitlementsSend] = useActor(
|
||||||
|
xServices.entitlementsXService,
|
||||||
|
)
|
||||||
const { warnings } = entitlementsState.context.entitlements
|
const { warnings } = entitlementsState.context.entitlements
|
||||||
|
|
||||||
/** Gets license data on app mount because LicenseBanner is mounted in App */
|
/** Gets license data on app mount because LicenseBanner is mounted in App */
|
||||||
|
@ -6,7 +6,9 @@ export default {
|
|||||||
component: LicenseBannerView,
|
component: LicenseBannerView,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Template: Story<LicenseBannerViewProps> = (args) => <LicenseBannerView {...args} />
|
const Template: Story<LicenseBannerViewProps> = (args) => (
|
||||||
|
<LicenseBannerView {...args} />
|
||||||
|
)
|
||||||
|
|
||||||
export const OneWarning = Template.bind({})
|
export const OneWarning = Template.bind({})
|
||||||
OneWarning.args = {
|
OneWarning.args = {
|
||||||
|
@ -16,7 +16,9 @@ export interface LicenseBannerViewProps {
|
|||||||
warnings: string[]
|
warnings: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({ warnings }) => {
|
export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({
|
||||||
|
warnings,
|
||||||
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const [showDetails, setShowDetails] = useState(false)
|
const [showDetails, setShowDetails] = useState(false)
|
||||||
if (warnings.length === 1) {
|
if (warnings.length === 1) {
|
||||||
@ -35,7 +37,11 @@ export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({ warnings }
|
|||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.flex}>
|
<div className={styles.flex}>
|
||||||
<div className={styles.leftContent}>
|
<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>
|
<span className={styles.text}>{Language.exceeded}</span>
|
||||||
|
|
||||||
<a href="mailto:sales@coder.com" className={styles.link}>
|
<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 CircularProgress from "@material-ui/core/CircularProgress"
|
||||||
import { FC } from "react"
|
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 (
|
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} />
|
<CircularProgress size={size} />
|
||||||
</Box>
|
</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({})
|
export const Loading = Template.bind({})
|
||||||
Loading.args = {
|
Loading.args = {
|
||||||
|
@ -14,14 +14,19 @@ export interface LogsProps {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Logs: FC<React.PropsWithChildren<LogsProps>> = ({ lines, className = "" }) => {
|
export const Logs: FC<React.PropsWithChildren<LogsProps>> = ({
|
||||||
|
lines,
|
||||||
|
className = "",
|
||||||
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={combineClasses([className, styles.root])}>
|
<div className={combineClasses([className, styles.root])}>
|
||||||
{lines.map((line, idx) => (
|
{lines.map((line, idx) => (
|
||||||
<div className={styles.line} key={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 className={styles.space}> </span>
|
||||||
<span>{line.output}</span>
|
<span>{line.output}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,7 +6,9 @@ export default {
|
|||||||
component: Markdown,
|
component: Markdown,
|
||||||
} as ComponentMeta<typeof 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({})
|
export const WithCode = Template.bind({})
|
||||||
WithCode.args = {
|
WithCode.args = {
|
||||||
|
@ -2,7 +2,10 @@ import { render, screen, waitFor } from "@testing-library/react"
|
|||||||
import { App } from "app"
|
import { App } from "app"
|
||||||
import { Language } from "components/NavbarView/NavbarView"
|
import { Language } from "components/NavbarView/NavbarView"
|
||||||
import { rest } from "msw"
|
import { rest } from "msw"
|
||||||
import { MockEntitlementsWithAuditLog, MockMemberPermissions } from "testHelpers/renderHelpers"
|
import {
|
||||||
|
MockEntitlementsWithAuditLog,
|
||||||
|
MockMemberPermissions,
|
||||||
|
} from "testHelpers/renderHelpers"
|
||||||
import { server } from "testHelpers/server"
|
import { server } from "testHelpers/server"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,8 +15,15 @@ export const Navbar: React.FC = () => {
|
|||||||
shallowEqual,
|
shallowEqual,
|
||||||
)
|
)
|
||||||
const canViewAuditLog =
|
const canViewAuditLog =
|
||||||
featureVisibility[FeatureNames.AuditLog] && Boolean(permissions?.viewAuditLog)
|
featureVisibility[FeatureNames.AuditLog] &&
|
||||||
|
Boolean(permissions?.viewAuditLog)
|
||||||
const onSignOut = () => authSend("SIGN_OUT")
|
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({})
|
export const ForAdmin = Template.bind({})
|
||||||
ForAdmin.args = {
|
ForAdmin.args = {
|
||||||
|
@ -70,7 +70,9 @@ describe("NavbarView", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("audit nav link is hidden for members", async () => {
|
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)
|
const auditLink = screen.queryByText(navLanguage.audit)
|
||||||
expect(auditLink).not.toBeInTheDocument()
|
expect(auditLink).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
@ -36,7 +36,10 @@ const NavItems: React.FC<
|
|||||||
<List className={combineClasses([styles.navItems, className])}>
|
<List className={combineClasses([styles.navItems, className])}>
|
||||||
<ListItem button className={styles.item}>
|
<ListItem button className={styles.item}>
|
||||||
<NavLink
|
<NavLink
|
||||||
className={combineClasses([styles.link, location.pathname.startsWith("/@") && "active"])}
|
className={combineClasses([
|
||||||
|
styles.link,
|
||||||
|
location.pathname.startsWith("/@") && "active",
|
||||||
|
])}
|
||||||
to="/workspaces"
|
to="/workspaces"
|
||||||
>
|
>
|
||||||
{Language.workspaces}
|
{Language.workspaces}
|
||||||
@ -86,7 +89,11 @@ export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
|
|||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Drawer anchor="left" open={isDrawerOpen} onClose={() => setIsDrawerOpen(false)}>
|
<Drawer
|
||||||
|
anchor="left"
|
||||||
|
open={isDrawerOpen}
|
||||||
|
onClose={() => setIsDrawerOpen(false)}
|
||||||
|
>
|
||||||
<div className={styles.drawer}>
|
<div className={styles.drawer}>
|
||||||
<div className={styles.drawerHeader}>
|
<div className={styles.drawerHeader}>
|
||||||
<Logo fill="white" opacity={1} width={125} />
|
<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} />
|
<Logo fill="white" opacity={1} width={125} />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
<NavItems className={styles.desktopNavItems} canViewAuditLog={canViewAuditLog} />
|
<NavItems
|
||||||
|
className={styles.desktopNavItems}
|
||||||
|
canViewAuditLog={canViewAuditLog}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={styles.profileButton}>
|
<div className={styles.profileButton}>
|
||||||
{user && <UserDropdown user={user} onSignOut={onSignOut} />}
|
{user && <UserDropdown user={user} onSignOut={onSignOut} />}
|
||||||
|
@ -17,7 +17,9 @@ export const WithTitle = WithTitleTemplate.bind({})
|
|||||||
const WithSubtitleTemplate: Story = () => (
|
const WithSubtitleTemplate: Story = () => (
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageHeaderTitle>Templates</PageHeaderTitle>
|
<PageHeaderTitle>Templates</PageHeaderTitle>
|
||||||
<PageHeaderSubtitle>Create a new workspace from a Template</PageHeaderSubtitle>
|
<PageHeaderSubtitle>
|
||||||
|
Create a new workspace from a Template
|
||||||
|
</PageHeaderSubtitle>
|
||||||
</PageHeader>
|
</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({})
|
const styles = useStyles({})
|
||||||
|
|
||||||
return <h1 className={styles.title}>{children}</h1>
|
return <h1 className={styles.title}>{children}</h1>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageHeaderSubtitle: React.FC<React.PropsWithChildren<{ condensed?: boolean }>> = ({
|
export const PageHeaderSubtitle: React.FC<
|
||||||
children,
|
React.PropsWithChildren<{ condensed?: boolean }>
|
||||||
condensed,
|
> = ({ children, condensed }) => {
|
||||||
}) => {
|
|
||||||
const styles = useStyles({
|
const styles = useStyles({
|
||||||
condensed,
|
condensed,
|
||||||
})
|
})
|
||||||
|
@ -7,9 +7,9 @@ export default {
|
|||||||
component: PaginationWidget,
|
component: PaginationWidget,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Template: Story<PaginationWidgetProps> = (args: PaginationWidgetProps) => (
|
const Template: Story<PaginationWidgetProps> = (
|
||||||
<PaginationWidget {...args} />
|
args: PaginationWidgetProps,
|
||||||
)
|
) => <PaginationWidget {...args} />
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
prevLabel: "Previous",
|
prevLabel: "Previous",
|
||||||
|
@ -13,10 +13,16 @@ describe("PaginatedList", () => {
|
|||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(await screen.findByRole("button", { name: "Previous page" })).toBeTruthy()
|
expect(
|
||||||
expect(await screen.findByRole("button", { name: "Next page" })).toBeTruthy()
|
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
|
// 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 () => {
|
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
|
// 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 () => {
|
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
|
// 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.
|
* 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
|
* 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) {
|
if (numPages > NUM_PAGE_BLOCKS) {
|
||||||
let pages = []
|
let pages = []
|
||||||
const leftBound = activePage - PAGE_NEIGHBORS
|
const leftBound = activePage - PAGE_NEIGHBORS
|
||||||
@ -128,7 +131,11 @@ export const PaginationWidget = ({
|
|||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
<Button aria-label="Next page" disabled={lastPageActive} onClick={onNextClick}>
|
<Button
|
||||||
|
aria-label="Next page"
|
||||||
|
disabled={lastPageActive}
|
||||||
|
onClick={onNextClick}
|
||||||
|
>
|
||||||
<div>{nextLabel}</div>
|
<div>{nextLabel}</div>
|
||||||
<KeyboardArrowRight />
|
<KeyboardArrowRight />
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -2,13 +2,28 @@ import { buildPagedList } from "./PaginationWidget"
|
|||||||
|
|
||||||
describe("unit/PaginationWidget", () => {
|
describe("unit/PaginationWidget", () => {
|
||||||
describe("buildPagedList", () => {
|
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: 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: 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] },
|
numPages: 17,
|
||||||
])(`buildPagedList($numPages, $activePage)`, ({ numPages, activePage, expected }) => {
|
activePage: 9,
|
||||||
expect(buildPagedList(numPages, activePage)).toEqual(expected)
|
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} />
|
<ParameterInput {...args} />
|
||||||
)
|
)
|
||||||
|
|
||||||
const createParameterSchema = (partial: Partial<ParameterSchema>): ParameterSchema => {
|
const createParameterSchema = (
|
||||||
|
partial: Partial<ParameterSchema>,
|
||||||
|
): ParameterSchema => {
|
||||||
return {
|
return {
|
||||||
id: "000000",
|
id: "000000",
|
||||||
job_id: "000000",
|
job_id: "000000",
|
||||||
@ -38,7 +40,8 @@ export const Basic = Template.bind({})
|
|||||||
Basic.args = {
|
Basic.args = {
|
||||||
schema: createParameterSchema({
|
schema: createParameterSchema({
|
||||||
name: "project_name",
|
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",
|
name: "region",
|
||||||
default_source_value: "🏈 US Central",
|
default_source_value: "🏈 US Central",
|
||||||
description: "Where would you like your workspace to live?",
|
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 (
|
return (
|
||||||
<label className={styles.label} htmlFor={schema.name}>
|
<label className={styles.label} htmlFor={schema.name}>
|
||||||
<strong>var.{schema.name}</strong>
|
<strong>var.{schema.name}</strong>
|
||||||
{schema.description && <span className={styles.labelDescription}>{schema.description}</span>}
|
{schema.description && (
|
||||||
|
<span className={styles.labelDescription}>{schema.description}</span>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -30,28 +32,28 @@ export interface ParameterInputProps {
|
|||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ParameterInput: FC<React.PropsWithChildren<ParameterInputProps>> = ({
|
export const ParameterInput: FC<
|
||||||
disabled,
|
React.PropsWithChildren<ParameterInputProps>
|
||||||
onChange,
|
> = ({ disabled, onChange, schema }) => {
|
||||||
schema,
|
|
||||||
}) => {
|
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="column" className={styles.root}>
|
<Stack direction="column" className={styles.root}>
|
||||||
<ParameterLabel schema={schema} />
|
<ParameterLabel schema={schema} />
|
||||||
<div className={styles.input}>
|
<div className={styles.input}>
|
||||||
<ParameterField disabled={disabled} onChange={onChange} schema={schema} />
|
<ParameterField
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={onChange}
|
||||||
|
schema={schema}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ParameterField: React.FC<React.PropsWithChildren<ParameterInputProps>> = ({
|
const ParameterField: React.FC<
|
||||||
disabled,
|
React.PropsWithChildren<ParameterInputProps>
|
||||||
onChange,
|
> = ({ disabled, onChange, schema }) => {
|
||||||
schema,
|
|
||||||
}) => {
|
|
||||||
if (schema.validation_contains && schema.validation_contains.length > 0) {
|
if (schema.validation_contains && schema.validation_contains.length > 0) {
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -8,10 +8,9 @@ import React, { useCallback, useState } from "react"
|
|||||||
|
|
||||||
type PasswordFieldProps = Omit<TextFieldProps, "InputProps" | "type">
|
type PasswordFieldProps = Omit<TextFieldProps, "InputProps" | "type">
|
||||||
|
|
||||||
export const PasswordField: React.FC<React.PropsWithChildren<PasswordFieldProps>> = ({
|
export const PasswordField: React.FC<
|
||||||
variant = "outlined",
|
React.PropsWithChildren<PasswordFieldProps>
|
||||||
...rest
|
> = ({ variant = "outlined", ...rest }) => {
|
||||||
}) => {
|
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const [showPassword, setShowPassword] = useState<boolean>(false)
|
const [showPassword, setShowPassword] = useState<boolean>(false)
|
||||||
|
|
||||||
@ -19,7 +18,9 @@ export const PasswordField: React.FC<React.PropsWithChildren<PasswordFieldProps>
|
|||||||
() => setShowPassword((showPassword) => !showPassword),
|
() => setShowPassword((showPassword) => !showPassword),
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
const VisibilityIcon = showPassword ? VisibilityOffOutlined : VisibilityOutlined
|
const VisibilityIcon = showPassword
|
||||||
|
? VisibilityOffOutlined
|
||||||
|
: VisibilityOutlined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -16,7 +16,10 @@ export const Pill: FC<PillProps> = (props) => {
|
|||||||
const { className, icon, text = false } = props
|
const { className, icon, text = false } = props
|
||||||
const styles = useStyles(props)
|
const styles = useStyles(props)
|
||||||
return (
|
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>}
|
{icon && <div className={styles.iconWrapper}>{icon}</div>}
|
||||||
{text}
|
{text}
|
||||||
</div>
|
</div>
|
||||||
@ -35,13 +38,15 @@ const useStyles = makeStyles<Theme, PillProps>((theme) => ({
|
|||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
color: "#FFF",
|
color: "#FFF",
|
||||||
height: theme.spacing(3),
|
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),
|
paddingRight: theme.spacing(1.5),
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
},
|
},
|
||||||
|
|
||||||
pillColor: {
|
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 }) =>
|
borderColor: ({ type, lightBorder }) =>
|
||||||
type
|
type
|
||||||
? lightBorder
|
? lightBorder
|
||||||
|
@ -9,7 +9,11 @@ import { Stack } from "components/Stack/Stack"
|
|||||||
import { useRef, useState } from "react"
|
import { useRef, useState } from "react"
|
||||||
import { colors } from "theme/colors"
|
import { colors } from "theme/colors"
|
||||||
import { CodeExample } from "../CodeExample/CodeExample"
|
import { CodeExample } from "../CodeExample/CodeExample"
|
||||||
import { HelpTooltipLink, HelpTooltipLinksGroup, HelpTooltipText } from "../Tooltips/HelpTooltip"
|
import {
|
||||||
|
HelpTooltipLink,
|
||||||
|
HelpTooltipLinksGroup,
|
||||||
|
HelpTooltipText,
|
||||||
|
} from "../Tooltips/HelpTooltip"
|
||||||
|
|
||||||
export interface PortForwardButtonProps {
|
export interface PortForwardButtonProps {
|
||||||
host: string
|
host: string
|
||||||
@ -28,13 +32,16 @@ const EnabledView: React.FC<PortForwardButtonProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Stack direction="column" spacing={1}>
|
<Stack direction="column" spacing={1}>
|
||||||
<HelpTooltipText>
|
<HelpTooltipText>
|
||||||
Access ports running on the agent with the <strong>port, agent name, workspace name</strong>{" "}
|
Access ports running on the agent with the{" "}
|
||||||
and <strong>your username</strong> URL schema, as shown below.
|
<strong>port, agent name, workspace name</strong> and{" "}
|
||||||
|
<strong>your username</strong> URL schema, as shown below.
|
||||||
</HelpTooltipText>
|
</HelpTooltipText>
|
||||||
|
|
||||||
<CodeExample code={urlExample} />
|
<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">
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
<TextField
|
<TextField
|
||||||
@ -70,8 +77,8 @@ const DisabledView: React.FC<PortForwardButtonProps> = () => {
|
|||||||
return (
|
return (
|
||||||
<Stack direction="column" spacing={1}>
|
<Stack direction="column" spacing={1}>
|
||||||
<HelpTooltipText>
|
<HelpTooltipText>
|
||||||
<strong>Your deployment does not have port forward enabled.</strong> See the docs for more
|
<strong>Your deployment does not have port forward enabled.</strong> See
|
||||||
details.
|
the docs for more details.
|
||||||
</HelpTooltipText>
|
</HelpTooltipText>
|
||||||
|
|
||||||
<HelpTooltipLinksGroup>
|
<HelpTooltipLinksGroup>
|
||||||
@ -136,7 +143,9 @@ export const PortForwardButton: React.FC<PortForwardButtonProps> = (props) => {
|
|||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
popoverPaper: {
|
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),
|
width: theme.spacing(46),
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
marginTop: theme.spacing(0.25),
|
marginTop: theme.spacing(0.25),
|
||||||
|
@ -9,7 +9,9 @@ export interface RequireAuthProps {
|
|||||||
children: JSX.Element
|
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 xServices = useContext(XServiceContext)
|
||||||
const [authState] = useActor(xServices.authXService)
|
const [authState] = useActor(xServices.authXService)
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
@ -9,7 +9,10 @@ export interface RequirePermissionProps {
|
|||||||
/**
|
/**
|
||||||
* Wraps routes that are available based on RBAC or licensing.
|
* 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) {
|
if (!isFeatureVisible) {
|
||||||
return <Navigate to="/workspaces" />
|
return <Navigate to="/workspaces" />
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import { Story } from "@storybook/react"
|
import { Story } from "@storybook/react"
|
||||||
import { ResourceAgentLatency, ResourceAgentLatencyProps } from "./ResourceAgentLatency"
|
import {
|
||||||
|
ResourceAgentLatency,
|
||||||
|
ResourceAgentLatencyProps,
|
||||||
|
} from "./ResourceAgentLatency"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "components/ResourceAgentLatency",
|
title: "components/ResourceAgentLatency",
|
||||||
component: ResourceAgentLatency,
|
component: ResourceAgentLatency,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Template: Story<ResourceAgentLatencyProps> = (args) => <ResourceAgentLatency {...args} />
|
const Template: Story<ResourceAgentLatencyProps> = (args) => (
|
||||||
|
<ResourceAgentLatency {...args} />
|
||||||
|
)
|
||||||
|
|
||||||
export const Single = Template.bind({})
|
export const Single = Template.bind({})
|
||||||
Single.args = {
|
Single.args = {
|
||||||
|
@ -8,7 +8,9 @@ export interface ResourceAgentLatencyProps {
|
|||||||
latency: WorkspaceAgent["latency"]
|
latency: WorkspaceAgent["latency"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ResourceAgentLatency: React.FC<ResourceAgentLatencyProps> = (props) => {
|
export const ResourceAgentLatency: React.FC<ResourceAgentLatencyProps> = (
|
||||||
|
props,
|
||||||
|
) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
if (!props.latency) {
|
if (!props.latency) {
|
||||||
return null
|
return null
|
||||||
@ -23,8 +25,8 @@ export const ResourceAgentLatency: React.FC<ResourceAgentLatencyProps> = (props)
|
|||||||
<b>Latency</b>
|
<b>Latency</b>
|
||||||
<HelpTooltip size="small">
|
<HelpTooltip size="small">
|
||||||
<HelpTooltipText>
|
<HelpTooltipText>
|
||||||
Latency from relay servers, used when connections cannot connect peer-to-peer. Star
|
Latency from relay servers, used when connections cannot connect
|
||||||
indicates the preferred relay.
|
peer-to-peer. Star indicates the preferred relay.
|
||||||
</HelpTooltipText>
|
</HelpTooltipText>
|
||||||
</HelpTooltip>
|
</HelpTooltip>
|
||||||
</div>
|
</div>
|
||||||
@ -34,7 +36,8 @@ export const ResourceAgentLatency: React.FC<ResourceAgentLatencyProps> = (props)
|
|||||||
const value = latency[region]
|
const value = latency[region]
|
||||||
return (
|
return (
|
||||||
<div key={region} className={styles.region}>
|
<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} />}
|
{value.preferred && <StarIcon className={styles.star} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -7,7 +7,9 @@ export default {
|
|||||||
component: ResourceAvatar,
|
component: ResourceAvatar,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Template: Story<ResourceAvatarProps> = (args) => <ResourceAvatar {...args} />
|
const Template: Story<ResourceAvatarProps> = (args) => (
|
||||||
|
<ResourceAvatar {...args} />
|
||||||
|
)
|
||||||
|
|
||||||
export const VolumeResource = Template.bind({})
|
export const VolumeResource = Template.bind({})
|
||||||
VolumeResource.args = {
|
VolumeResource.args = {
|
||||||
|
@ -5,7 +5,10 @@ import VisibilityOffOutlined from "@material-ui/icons/VisibilityOffOutlined"
|
|||||||
import VisibilityOutlined from "@material-ui/icons/VisibilityOutlined"
|
import VisibilityOutlined from "@material-ui/icons/VisibilityOutlined"
|
||||||
import { WorkspaceResource } from "api/typesGenerated"
|
import { WorkspaceResource } from "api/typesGenerated"
|
||||||
import { FC, useState } from "react"
|
import { FC, useState } from "react"
|
||||||
import { TableCellData, TableCellDataPrimary } from "../TableCellData/TableCellData"
|
import {
|
||||||
|
TableCellData,
|
||||||
|
TableCellDataPrimary,
|
||||||
|
} from "../TableCellData/TableCellData"
|
||||||
import { ResourceAvatar } from "./ResourceAvatar"
|
import { ResourceAvatar } from "./ResourceAvatar"
|
||||||
|
|
||||||
const Language = {
|
const Language = {
|
||||||
@ -18,7 +21,11 @@ const SensitiveValue: React.FC<{ value: string }> = ({ value }) => {
|
|||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const displayValue = shouldDisplay ? value : "••••••••"
|
const displayValue = shouldDisplay ? value : "••••••••"
|
||||||
const buttonLabel = shouldDisplay ? Language.hideLabel : Language.showLabel
|
const buttonLabel = shouldDisplay ? Language.hideLabel : Language.showLabel
|
||||||
const icon = shouldDisplay ? <VisibilityOffOutlined /> : <VisibilityOutlined />
|
const icon = shouldDisplay ? (
|
||||||
|
<VisibilityOffOutlined />
|
||||||
|
) : (
|
||||||
|
<VisibilityOutlined />
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.sensitiveValue}>
|
<div className={styles.sensitiveValue}>
|
||||||
@ -43,7 +50,9 @@ export interface ResourceAvatarDataProps {
|
|||||||
resource: WorkspaceResource
|
resource: WorkspaceResource
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ResourceAvatarData: FC<ResourceAvatarDataProps> = ({ resource }) => {
|
export const ResourceAvatarData: FC<ResourceAvatarDataProps> = ({
|
||||||
|
resource,
|
||||||
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -8,12 +8,19 @@ import TableHead from "@material-ui/core/TableHead"
|
|||||||
import TableRow from "@material-ui/core/TableRow"
|
import TableRow from "@material-ui/core/TableRow"
|
||||||
import { Skeleton } from "@material-ui/lab"
|
import { Skeleton } from "@material-ui/lab"
|
||||||
import useTheme from "@material-ui/styles/useTheme"
|
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 { PortForwardButton } from "components/PortForwardButton/PortForwardButton"
|
||||||
import { TableCellDataPrimary } from "components/TableCellData/TableCellData"
|
import { TableCellDataPrimary } from "components/TableCellData/TableCellData"
|
||||||
import { FC, useState } from "react"
|
import { FC, useState } from "react"
|
||||||
import { getDisplayAgentStatus, getDisplayVersionStatus } from "util/workspace"
|
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 { AppLink } from "../AppLink/AppLink"
|
||||||
import { SSHButton } from "../SSHButton/SSHButton"
|
import { SSHButton } from "../SSHButton/SSHButton"
|
||||||
import { Stack } from "../Stack/Stack"
|
import { Stack } from "../Stack/Stack"
|
||||||
@ -58,7 +65,8 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
|
|||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const theme: Theme = useTheme()
|
const theme: Theme = useTheme()
|
||||||
const serverVersion = buildInfo?.version || ""
|
const serverVersion = buildInfo?.version || ""
|
||||||
const [shouldDisplayHideResources, setShouldDisplayHideResources] = useState(false)
|
const [shouldDisplayHideResources, setShouldDisplayHideResources] =
|
||||||
|
useState(false)
|
||||||
const displayResources = shouldDisplayHideResources
|
const displayResources = shouldDisplayHideResources
|
||||||
? resources
|
? resources
|
||||||
: resources.filter((resource) => !resource.hide)
|
: 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 */
|
/* We need to initialize the agents to display the resource */
|
||||||
}
|
}
|
||||||
const agents = resource.agents ?? [null]
|
const agents = resource.agents ?? [null]
|
||||||
const resourceName = <ResourceAvatarData resource={resource} />
|
const resourceName = (
|
||||||
|
<ResourceAvatarData resource={resource} />
|
||||||
|
)
|
||||||
|
|
||||||
return agents.map((agent, agentIndex) => {
|
return agents.map((agent, agentIndex) => {
|
||||||
{
|
{
|
||||||
/* If there is no agent, just display the resource name */
|
/* 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 (
|
return (
|
||||||
<TableRow key={`${resource.id}-${agentIndex}`}>
|
<TableRow key={`${resource.id}-${agentIndex}`}>
|
||||||
<TableCell>{resourceName}</TableCell>
|
<TableCell>{resourceName}</TableCell>
|
||||||
@ -109,27 +122,33 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const { displayVersion, outdated } = getDisplayVersionStatus(
|
const { displayVersion, outdated } =
|
||||||
agent.version,
|
getDisplayVersionStatus(agent.version, serverVersion)
|
||||||
serverVersion,
|
|
||||||
)
|
|
||||||
const agentStatus = getDisplayAgentStatus(theme, agent)
|
const agentStatus = getDisplayAgentStatus(theme, agent)
|
||||||
return (
|
return (
|
||||||
<TableRow key={`${resource.id}-${agent.id}`}>
|
<TableRow key={`${resource.id}-${agent.id}`}>
|
||||||
{/* We only want to display the name in the first row because we are using rowSpan */}
|
{/* 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 */}
|
{/* The rowspan should be the same than the number of agents */}
|
||||||
{agentIndex === 0 && (
|
{agentIndex === 0 && (
|
||||||
<TableCell className={styles.resourceNameCell} rowSpan={agents.length}>
|
<TableCell
|
||||||
|
className={styles.resourceNameCell}
|
||||||
|
rowSpan={agents.length}
|
||||||
|
>
|
||||||
{resourceName}
|
{resourceName}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TableCell className={styles.agentColumn}>
|
<TableCell className={styles.agentColumn}>
|
||||||
<TableCellDataPrimary highlight>{agent.name}</TableCellDataPrimary>
|
<TableCellDataPrimary highlight>
|
||||||
|
{agent.name}
|
||||||
|
</TableCellDataPrimary>
|
||||||
<div className={styles.data}>
|
<div className={styles.data}>
|
||||||
<div className={styles.dataRow}>
|
<div className={styles.dataRow}>
|
||||||
<strong>{Language.statusLabel}</strong>
|
<strong>{Language.statusLabel}</strong>
|
||||||
<span style={{ color: agentStatus.color }} className={styles.status}>
|
<span
|
||||||
|
style={{ color: agentStatus.color }}
|
||||||
|
className={styles.status}
|
||||||
|
>
|
||||||
{agentStatus.status}
|
{agentStatus.status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -141,7 +160,9 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.dataRow}>
|
<div className={styles.dataRow}>
|
||||||
<strong>{Language.versionLabel}</strong>
|
<strong>{Language.versionLabel}</strong>
|
||||||
<span className={styles.agentVersion}>{displayVersion}</span>
|
<span className={styles.agentVersion}>
|
||||||
|
{displayVersion}
|
||||||
|
</span>
|
||||||
<AgentOutdatedTooltip outdated={outdated} />
|
<AgentOutdatedTooltip outdated={outdated} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.dataRow}>
|
<div className={styles.dataRow}>
|
||||||
@ -151,49 +172,51 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className={styles.accessLinks}>
|
<div className={styles.accessLinks}>
|
||||||
{canUpdateWorkspace && agent.status === "connected" && (
|
{canUpdateWorkspace &&
|
||||||
<>
|
agent.status === "connected" && (
|
||||||
{applicationsHost !== undefined && (
|
<>
|
||||||
<PortForwardButton
|
{applicationsHost !== undefined && (
|
||||||
host={applicationsHost}
|
<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}
|
workspaceName={workspace.name}
|
||||||
agentName={agent.name}
|
agentName={agent.name}
|
||||||
username={workspace.owner_name}
|
userName={workspace.owner_name}
|
||||||
/>
|
/>
|
||||||
)}
|
{agent.apps.map((app) => (
|
||||||
{!hideSSHButton && (
|
<AppLink
|
||||||
<SSHButton
|
key={app.name}
|
||||||
workspaceName={workspace.name}
|
appsHost={applicationsHost}
|
||||||
agentName={agent.name}
|
appIcon={app.icon}
|
||||||
/>
|
appName={app.name}
|
||||||
)}
|
appCommand={app.command}
|
||||||
<TerminalLink
|
appSubdomain={app.subdomain}
|
||||||
workspaceName={workspace.name}
|
username={workspace.owner_name}
|
||||||
agentName={agent.name}
|
workspaceName={workspace.name}
|
||||||
userName={workspace.owner_name}
|
agentName={agent.name}
|
||||||
/>
|
health={app.health}
|
||||||
{agent.apps.map((app) => (
|
/>
|
||||||
<AppLink
|
))}
|
||||||
key={app.name}
|
</>
|
||||||
appsHost={applicationsHost}
|
)}
|
||||||
appIcon={app.icon}
|
{canUpdateWorkspace &&
|
||||||
appName={app.name}
|
agent.status === "connecting" && (
|
||||||
appCommand={app.command}
|
<>
|
||||||
appSubdomain={app.subdomain}
|
<Skeleton width={80} height={60} />
|
||||||
username={workspace.owner_name}
|
<Skeleton width={120} height={60} />
|
||||||
workspaceName={workspace.name}
|
</>
|
||||||
agentName={agent.name}
|
)}
|
||||||
health={app.health}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{canUpdateWorkspace && agent.status === "connecting" && (
|
|
||||||
<>
|
|
||||||
<Skeleton width={80} height={60} />
|
|
||||||
<Skeleton width={120} height={60} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
@ -21,7 +21,11 @@ describe("UserRoleSelect", () => {
|
|||||||
assignableRole(MockAuditorRole, true),
|
assignableRole(MockAuditorRole, true),
|
||||||
assignableRole(MockUserAdminRole, true),
|
assignableRole(MockUserAdminRole, true),
|
||||||
]}
|
]}
|
||||||
selectedRoles={[MockUserAdminRole, MockTemplateAdminRole, MockMemberRole]}
|
selectedRoles={[
|
||||||
|
MockUserAdminRole,
|
||||||
|
MockTemplateAdminRole,
|
||||||
|
MockMemberRole,
|
||||||
|
]}
|
||||||
loading={false}
|
loading={false}
|
||||||
onChange={jest.fn()}
|
onChange={jest.fn()}
|
||||||
open
|
open
|
||||||
@ -30,7 +34,9 @@ describe("UserRoleSelect", () => {
|
|||||||
|
|
||||||
// Then
|
// Then
|
||||||
const owner = await screen.findByText(MockOwnerRole.display_name)
|
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 auditor = await screen.findByText(MockAuditorRole.display_name)
|
||||||
const userAdmin = await screen.findByText(MockUserAdminRole.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 styles = useStyles()
|
||||||
const value = selectedRoles.map((r) => r.name)
|
const value = selectedRoles.map((r) => r.name)
|
||||||
const renderValue = () => selectedRoles.map((r) => r.display_name).join(", ")
|
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 (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@ -43,11 +45,18 @@ export const RoleSelect: FC<React.PropsWithChildren<RoleSelectProps>> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{sortedRoles.map((r) => {
|
{sortedRoles.map((r) => {
|
||||||
const isChecked = selectedRoles.some((selectedRole) => selectedRole.name === r.name)
|
const isChecked = selectedRoles.some(
|
||||||
|
(selectedRole) => selectedRole.name === r.name,
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem key={r.name} value={r.name} disabled={loading || !r.assignable}>
|
<MenuItem
|
||||||
<Checkbox size="small" color="primary" checked={isChecked} /> {r.display_name}
|
key={r.name}
|
||||||
|
value={r.name}
|
||||||
|
disabled={loading || !r.assignable}
|
||||||
|
>
|
||||||
|
<Checkbox size="small" color="primary" checked={isChecked} />{" "}
|
||||||
|
{r.display_name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -26,21 +26,29 @@ export const stackTraceUnavailable = {
|
|||||||
|
|
||||||
type ReportMessage = StackTraceAvailableMsg | typeof stackTraceUnavailable
|
type ReportMessage = StackTraceAvailableMsg | typeof stackTraceUnavailable
|
||||||
|
|
||||||
export const stackTraceAvailable = (stackTrace: string[]): StackTraceAvailableMsg => {
|
export const stackTraceAvailable = (
|
||||||
|
stackTrace: string[],
|
||||||
|
): StackTraceAvailableMsg => {
|
||||||
return {
|
return {
|
||||||
type: "stackTraceAvailable",
|
type: "stackTraceAvailable",
|
||||||
stackTrace,
|
stackTrace,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setStackTrace = (model: ReportState, mappedStack: string[]): ReportState => {
|
const setStackTrace = (
|
||||||
|
model: ReportState,
|
||||||
|
mappedStack: string[],
|
||||||
|
): ReportState => {
|
||||||
return {
|
return {
|
||||||
...model,
|
...model,
|
||||||
mappedStack,
|
mappedStack,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reducer = (model: ReportState, msg: ReportMessage): ReportState => {
|
export const reducer = (
|
||||||
|
model: ReportState,
|
||||||
|
msg: ReportMessage,
|
||||||
|
): ReportState => {
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case "stackTraceAvailable":
|
case "stackTraceAvailable":
|
||||||
return setStackTrace(model, msg.stackTrace)
|
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 [
|
return [
|
||||||
"======================= STACK TRACE ========================",
|
"======================= 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
|
* 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()
|
const styles = useStyles()
|
||||||
|
|
||||||
if (!mappedStack) {
|
if (!mappedStack) {
|
||||||
return <CodeBlock lines={[Language.reportLoading]} className={styles.codeBlock} />
|
return (
|
||||||
|
<CodeBlock
|
||||||
|
lines={[Language.reportLoading]}
|
||||||
|
className={styles.codeBlock}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedStackTrace = createFormattedStackTrace(error, mappedStack)
|
const formattedStackTrace = createFormattedStackTrace(error, mappedStack)
|
||||||
|
@ -13,7 +13,9 @@ export default {
|
|||||||
},
|
},
|
||||||
} as ComponentMeta<typeof RuntimeErrorState>
|
} as ComponentMeta<typeof RuntimeErrorState>
|
||||||
|
|
||||||
const Template: Story<RuntimeErrorStateProps> = (args) => <RuntimeErrorState {...args} />
|
const Template: Story<RuntimeErrorStateProps> = (args) => (
|
||||||
|
<RuntimeErrorState {...args} />
|
||||||
|
)
|
||||||
|
|
||||||
export const Errored = Template.bind({})
|
export const Errored = Template.bind({})
|
||||||
Errored.parameters = {
|
Errored.parameters = {
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { screen } from "@testing-library/react"
|
import { screen } from "@testing-library/react"
|
||||||
import { render } from "../../testHelpers/renderHelpers"
|
import { render } from "../../testHelpers/renderHelpers"
|
||||||
import { Language as ButtonLanguage } from "./createCtas"
|
import { Language as ButtonLanguage } from "./createCtas"
|
||||||
import { Language as RuntimeErrorStateLanguage, RuntimeErrorState } from "./RuntimeErrorState"
|
import {
|
||||||
|
Language as RuntimeErrorStateLanguage,
|
||||||
|
RuntimeErrorState,
|
||||||
|
} from "./RuntimeErrorState"
|
||||||
|
|
||||||
describe("RuntimeErrorState", () => {
|
describe("RuntimeErrorState", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -61,13 +61,20 @@ const ErrorStateDescription = ({ emailBody }: { emailBody?: string }) => {
|
|||||||
/**
|
/**
|
||||||
* An error UI that is displayed when our error boundary (ErrorBoundary.tsx) is triggered
|
* 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 styles = useStyles()
|
||||||
const [reportState, dispatch] = useReducer(reducer, { error, mappedStack: null })
|
const [reportState, dispatch] = useReducer(reducer, {
|
||||||
|
error,
|
||||||
|
mappedStack: null,
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
mapStackTrace(error.stack, (mappedStack) => dispatch(stackTraceAvailable(mappedStack)))
|
mapStackTrace(error.stack, (mappedStack) =>
|
||||||
|
dispatch(stackTraceAvailable(mappedStack)),
|
||||||
|
)
|
||||||
} catch {
|
} catch {
|
||||||
dispatch(stackTraceUnavailable)
|
dispatch(stackTraceUnavailable)
|
||||||
}
|
}
|
||||||
@ -81,13 +88,17 @@ export const RuntimeErrorState: React.FC<RuntimeErrorStateProps> = ({ error }) =
|
|||||||
title={<ErrorStateTitle />}
|
title={<ErrorStateTitle />}
|
||||||
description={
|
description={
|
||||||
<ErrorStateDescription
|
<ErrorStateDescription
|
||||||
emailBody={createFormattedStackTrace(reportState.error, reportState.mappedStack).join(
|
emailBody={createFormattedStackTrace(
|
||||||
"\r\n",
|
reportState.error,
|
||||||
)}
|
reportState.mappedStack,
|
||||||
|
).join("\r\n")}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<RuntimeErrorReport error={reportState.error} mappedStack={reportState.mappedStack} />
|
<RuntimeErrorReport
|
||||||
|
error={reportState.error}
|
||||||
|
mappedStack={reportState.mappedStack}
|
||||||
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
</Margins>
|
</Margins>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { Story } from "@storybook/react"
|
import { Story } from "@storybook/react"
|
||||||
import { MockWorkspace, MockWorkspaceAgent } from "../../testHelpers/renderHelpers"
|
import {
|
||||||
|
MockWorkspace,
|
||||||
|
MockWorkspaceAgent,
|
||||||
|
} from "../../testHelpers/renderHelpers"
|
||||||
import { SSHButton, SSHButtonProps } from "./SSHButton"
|
import { SSHButton, SSHButtonProps } from "./SSHButton"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -5,7 +5,11 @@ import CloudIcon from "@material-ui/icons/CloudOutlined"
|
|||||||
import { useRef, useState } from "react"
|
import { useRef, useState } from "react"
|
||||||
import { CodeExample } from "../CodeExample/CodeExample"
|
import { CodeExample } from "../CodeExample/CodeExample"
|
||||||
import { Stack } from "../Stack/Stack"
|
import { Stack } from "../Stack/Stack"
|
||||||
import { HelpTooltipLink, HelpTooltipLinksGroup, HelpTooltipText } from "../Tooltips/HelpTooltip"
|
import {
|
||||||
|
HelpTooltipLink,
|
||||||
|
HelpTooltipLinksGroup,
|
||||||
|
HelpTooltipText,
|
||||||
|
} from "../Tooltips/HelpTooltip"
|
||||||
|
|
||||||
export interface SSHButtonProps {
|
export interface SSHButtonProps {
|
||||||
workspaceName: string
|
workspaceName: string
|
||||||
@ -54,19 +58,25 @@ export const SSHButton: React.FC<React.PropsWithChildren<SSHButtonProps>> = ({
|
|||||||
horizontal: "left",
|
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}>
|
<Stack spacing={0.5} className={styles.codeExamples}>
|
||||||
<div>
|
<div>
|
||||||
<HelpTooltipText>
|
<HelpTooltipText>
|
||||||
<strong className={styles.codeExampleLabel}>Configure SSH hosts on machine:</strong>
|
<strong className={styles.codeExampleLabel}>
|
||||||
|
Configure SSH hosts on machine:
|
||||||
|
</strong>
|
||||||
</HelpTooltipText>
|
</HelpTooltipText>
|
||||||
<CodeExample code="coder config-ssh" />
|
<CodeExample code="coder config-ssh" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<HelpTooltipText>
|
<HelpTooltipText>
|
||||||
<strong className={styles.codeExampleLabel}>Connect to the agent:</strong>
|
<strong className={styles.codeExampleLabel}>
|
||||||
|
Connect to the agent:
|
||||||
|
</strong>
|
||||||
</HelpTooltipText>
|
</HelpTooltipText>
|
||||||
<CodeExample code={`ssh coder.${workspaceName}.${agentName}`} />
|
<CodeExample code={`ssh coder.${workspaceName}.${agentName}`} />
|
||||||
</div>
|
</div>
|
||||||
@ -93,7 +103,9 @@ export const SSHButton: React.FC<React.PropsWithChildren<SSHButtonProps>> = ({
|
|||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
popoverPaper: {
|
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),
|
width: theme.spacing(38),
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
marginTop: theme.spacing(0.25),
|
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