mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
chore: remove Language objects (#23866)
This commit is contained in:
@@ -1,11 +1,5 @@
|
||||
import { type AxiosError, type AxiosResponse, isAxiosError } from "axios";
|
||||
|
||||
const Language = {
|
||||
errorsByCode: {
|
||||
defaultErrorCode: "Invalid value",
|
||||
},
|
||||
};
|
||||
|
||||
export interface FieldError {
|
||||
field: string;
|
||||
detail: string;
|
||||
@@ -64,8 +58,7 @@ export const mapApiErrorToFieldErrors = (
|
||||
|
||||
if (apiErrorResponse.validations) {
|
||||
for (const error of apiErrorResponse.validations) {
|
||||
result[error.field] =
|
||||
error.detail || Language.errorsByCode.defaultErrorCode;
|
||||
result[error.field] = error.detail || "Invalid value";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "#/components/DropdownMenu/DropdownMenu";
|
||||
import { MockUserOwner } from "#/testHelpers/entities";
|
||||
import { render, waitForLoaderToBeRemoved } from "#/testHelpers/renderHelpers";
|
||||
import { Language, UserDropdownContent } from "./UserDropdownContent";
|
||||
import { UserDropdownContent } from "./UserDropdownContent";
|
||||
|
||||
const renderUserDropdownContent = (props: { onSignOut: () => void }) => {
|
||||
return render(
|
||||
@@ -28,7 +28,7 @@ describe("UserDropdownContent", () => {
|
||||
renderUserDropdownContent({ onSignOut: vi.fn() });
|
||||
await waitForLoaderToBeRemoved();
|
||||
|
||||
const link = screen.getByText(Language.accountLabel).closest("a");
|
||||
const link = screen.getByText("Account").closest("a");
|
||||
if (!link) {
|
||||
throw new Error("Anchor tag not found for the account menu item");
|
||||
}
|
||||
@@ -40,7 +40,7 @@ describe("UserDropdownContent", () => {
|
||||
const onSignOut = vi.fn();
|
||||
renderUserDropdownContent({ onSignOut });
|
||||
await waitForLoaderToBeRemoved();
|
||||
screen.getByText(Language.signOutLabel).click();
|
||||
screen.getByText("Sign Out").click();
|
||||
expect(onSignOut).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,12 +21,6 @@ import {
|
||||
import { useClipboard } from "#/hooks/useClipboard";
|
||||
import { SupportIcon } from "../SupportIcon";
|
||||
|
||||
export const Language = {
|
||||
accountLabel: "Account",
|
||||
signOutLabel: "Sign Out",
|
||||
copyrightText: `\u00a9 ${new Date().getFullYear()} Coder Technologies, Inc.`,
|
||||
};
|
||||
|
||||
interface UserDropdownContentProps {
|
||||
user: TypesGen.User;
|
||||
buildInfo?: TypesGen.BuildInfoResponse;
|
||||
@@ -126,7 +120,7 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
<DropdownMenuItem className="text-xs" disabled>
|
||||
<span>{Language.copyrightText}</span>
|
||||
<span>© {new Date().getFullYear()} Coder Technologies, Inc.</span>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -8,11 +8,6 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "#/components/Tooltip/Tooltip";
|
||||
|
||||
const Language = {
|
||||
showLabel: "Show value",
|
||||
hideLabel: "Hide value",
|
||||
};
|
||||
|
||||
interface SensitiveValueProps {
|
||||
value: string;
|
||||
}
|
||||
@@ -20,7 +15,7 @@ interface SensitiveValueProps {
|
||||
export const SensitiveValue: FC<SensitiveValueProps> = ({ value }) => {
|
||||
const [shouldDisplay, setShouldDisplay] = useState(false);
|
||||
const displayValue = shouldDisplay ? value : "••••••••";
|
||||
const buttonLabel = shouldDisplay ? Language.hideLabel : Language.showLabel;
|
||||
const buttonLabel = shouldDisplay ? "Hide value" : "Show value";
|
||||
const icon = shouldDisplay ? (
|
||||
<EyeOffIcon className="size-icon-xs" />
|
||||
) : (
|
||||
|
||||
@@ -13,10 +13,6 @@ import { DEFAULT_LOG_LINE_SIDE_PADDING, Logs } from "#/components/Logs/Logs";
|
||||
import { BODY_FONT_FAMILY } from "#/theme/constants";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
const Language = {
|
||||
seconds: "seconds",
|
||||
};
|
||||
|
||||
type Stage = ProvisionerJobLog["stage"];
|
||||
type LogsGroupedByStage = Record<Stage, ProvisionerJobLog[]>;
|
||||
type GroupLogsByStageFn = (logs: ProvisionerJobLog[]) => LogsGroupedByStage;
|
||||
@@ -98,9 +94,7 @@ export const WorkspaceBuildLogs: FC<WorkspaceBuildLogsProps> = ({
|
||||
>
|
||||
<div>{stage}</div>
|
||||
{shouldDisplayDuration && (
|
||||
<div css={styles.duration}>
|
||||
{duration} {Language.seconds}
|
||||
</div>
|
||||
<div css={styles.duration}>{duration} seconds</div>
|
||||
)}
|
||||
</div>
|
||||
{!isEmpty && <Logs hideTimestamps={hideTimestamps} lines={lines} />}
|
||||
|
||||
@@ -10,23 +10,20 @@ import {
|
||||
} from "#/components/HelpPopover/HelpPopover";
|
||||
import { docs } from "#/utils/docs";
|
||||
|
||||
const Language = {
|
||||
title: "What is an audit log?",
|
||||
body: "An audit log is a record of events and changes made throughout a system.",
|
||||
docs: "Events we track",
|
||||
};
|
||||
|
||||
export const AuditHelpPopover: FC = () => {
|
||||
return (
|
||||
<HelpPopover>
|
||||
<HelpPopoverIconTrigger />
|
||||
|
||||
<HelpPopoverContent>
|
||||
<HelpPopoverTitle>{Language.title}</HelpPopoverTitle>
|
||||
<HelpPopoverText>{Language.body}</HelpPopoverText>
|
||||
<HelpPopoverTitle>What is an audit log?</HelpPopoverTitle>
|
||||
<HelpPopoverText>
|
||||
An audit log is a record of events and changes made throughout a
|
||||
system.
|
||||
</HelpPopoverText>
|
||||
<HelpPopoverLinksGroup>
|
||||
<HelpPopoverLink href={docs("/admin/security/audit-logs")}>
|
||||
{Language.docs}
|
||||
Events we track
|
||||
</HelpPopoverLink>
|
||||
</HelpPopoverLinksGroup>
|
||||
</HelpPopoverContent>
|
||||
|
||||
@@ -27,11 +27,6 @@ import { AuditFilter } from "./AuditFilter";
|
||||
import { AuditHelpPopover } from "./AuditHelpPopover";
|
||||
import { AuditLogRow } from "./AuditLogRow/AuditLogRow";
|
||||
|
||||
const Language = {
|
||||
title: "Audit",
|
||||
subtitle: "View events in your audit log.",
|
||||
};
|
||||
|
||||
interface AuditPageViewProps {
|
||||
auditLogs?: readonly AuditLog[];
|
||||
isNonInitialPage: boolean;
|
||||
@@ -62,11 +57,11 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
|
||||
<PageHeader>
|
||||
<PageHeaderTitle>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<span>{Language.title}</span>
|
||||
<span>Audit</span>
|
||||
<AuditHelpPopover />
|
||||
</Stack>
|
||||
</PageHeaderTitle>
|
||||
<PageHeaderSubtitle>{Language.subtitle}</PageHeaderSubtitle>
|
||||
<PageHeaderSubtitle>View events in your audit log.</PageHeaderSubtitle>
|
||||
</PageHeader>
|
||||
|
||||
<ChooseOne>
|
||||
|
||||
@@ -10,23 +10,21 @@ import {
|
||||
} from "#/components/HelpPopover/HelpPopover";
|
||||
import { docs } from "#/utils/docs";
|
||||
|
||||
const Language = {
|
||||
title: "Why are some events missing?",
|
||||
body: "The connection log is a best-effort log of workspace access. Some events are reported by workspace agents, and receipt of these events by the server is not guaranteed.",
|
||||
docs: "Connection log documentation",
|
||||
};
|
||||
|
||||
export const ConnectionLogHelpPopover: FC = () => {
|
||||
return (
|
||||
<HelpPopover>
|
||||
<HelpPopoverIconTrigger />
|
||||
|
||||
<HelpPopoverContent>
|
||||
<HelpPopoverTitle>{Language.title}</HelpPopoverTitle>
|
||||
<HelpPopoverText>{Language.body}</HelpPopoverText>
|
||||
<HelpPopoverTitle>Why are some events missing?</HelpPopoverTitle>
|
||||
<HelpPopoverText>
|
||||
The connection log is a best-effort log of workspace access. Some
|
||||
events are reported by workspace agents, and receipt of these events
|
||||
by the server is not guaranteed.
|
||||
</HelpPopoverText>
|
||||
<HelpPopoverLinksGroup>
|
||||
<HelpPopoverLink href={docs("/admin/monitoring/connection-logs")}>
|
||||
{Language.docs}
|
||||
Connection log documentation
|
||||
</HelpPopoverLink>
|
||||
</HelpPopoverLinksGroup>
|
||||
</HelpPopoverContent>
|
||||
|
||||
@@ -27,11 +27,6 @@ import { ConnectionLogFilter } from "./ConnectionLogFilter";
|
||||
import { ConnectionLogHelpPopover } from "./ConnectionLogHelpPopover";
|
||||
import { ConnectionLogRow } from "./ConnectionLogRow/ConnectionLogRow";
|
||||
|
||||
const Language = {
|
||||
title: "Connection Log",
|
||||
subtitle: "View workspace connection events.",
|
||||
};
|
||||
|
||||
interface ConnectionLogPageViewProps {
|
||||
connectionLogs?: readonly ConnectionLog[];
|
||||
isNonInitialPage: boolean;
|
||||
@@ -61,11 +56,13 @@ export const ConnectionLogPageView: FC<ConnectionLogPageViewProps> = ({
|
||||
<PageHeader>
|
||||
<PageHeaderTitle>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<span>{Language.title}</span>
|
||||
<span>Connection Log</span>
|
||||
<ConnectionLogHelpPopover />
|
||||
</Stack>
|
||||
</PageHeaderTitle>
|
||||
<PageHeaderSubtitle>{Language.subtitle}</PageHeaderSubtitle>
|
||||
<PageHeaderSubtitle>
|
||||
View workspace connection events.
|
||||
</PageHeaderSubtitle>
|
||||
</PageHeader>
|
||||
|
||||
<ChooseOne>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export const Language = {
|
||||
emailLabel: "Email",
|
||||
passwordLabel: "Password",
|
||||
emailInvalid: "Please enter a valid email address.",
|
||||
emailRequired: "Please enter an email address.",
|
||||
passwordSignIn: "Sign In",
|
||||
githubSignIn: "GitHub",
|
||||
oidcSignIn: "OpenID Connect",
|
||||
};
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
waitForLoaderToBeRemoved,
|
||||
} from "#/testHelpers/renderHelpers";
|
||||
import { server } from "#/testHelpers/server";
|
||||
import { Language } from "./Language";
|
||||
import LoginPage from "./LoginPage";
|
||||
|
||||
describe("LoginPage", () => {
|
||||
@@ -35,12 +34,12 @@ describe("LoginPage", () => {
|
||||
// When
|
||||
render(<LoginPage />);
|
||||
await waitForLoaderToBeRemoved();
|
||||
const email = screen.getByLabelText(new RegExp(Language.emailLabel));
|
||||
const password = screen.getByLabelText(new RegExp(Language.passwordLabel));
|
||||
const email = screen.getByLabelText(/Email/);
|
||||
const password = screen.getByLabelText(/Password/);
|
||||
await userEvent.type(email, "test@coder.com");
|
||||
await userEvent.type(password, "password");
|
||||
// Click sign-in
|
||||
const signInButton = await screen.findByText(Language.passwordSignIn);
|
||||
const signInButton = await screen.findByText("Sign In");
|
||||
fireEvent.click(signInButton);
|
||||
|
||||
// Then
|
||||
@@ -53,10 +52,8 @@ describe("LoginPage", () => {
|
||||
render(<LoginPage />);
|
||||
await waitForLoaderToBeRemoved();
|
||||
|
||||
const emailInput = screen.getByLabelText(new RegExp(Language.emailLabel));
|
||||
const passwordInput = screen.getByLabelText(
|
||||
new RegExp(Language.passwordLabel),
|
||||
);
|
||||
const emailInput = screen.getByLabelText(/Email/);
|
||||
const passwordInput = screen.getByLabelText(/Password/);
|
||||
expect(emailInput).not.toHaveAttribute("aria-invalid", "true");
|
||||
expect(emailInput).not.toHaveAttribute(
|
||||
"aria-describedby",
|
||||
@@ -68,11 +65,13 @@ describe("LoginPage", () => {
|
||||
"signin-password-error",
|
||||
);
|
||||
|
||||
const signInButton = await screen.findByText(Language.passwordSignIn);
|
||||
const signInButton = await screen.findByText("Sign In");
|
||||
fireEvent.click(signInButton);
|
||||
|
||||
// Then
|
||||
const emailError = await screen.findByText(Language.emailRequired);
|
||||
const emailError = await screen.findByText(
|
||||
"Please enter an email address.",
|
||||
);
|
||||
expect(emailInput).toHaveAttribute("aria-invalid", "true");
|
||||
expect(emailInput).toHaveAttribute(
|
||||
"aria-describedby",
|
||||
@@ -81,7 +80,9 @@ describe("LoginPage", () => {
|
||||
|
||||
const emailErrorElement = document.getElementById("signin-email-error");
|
||||
expect(emailErrorElement).toBe(emailError);
|
||||
expect(emailErrorElement).toHaveTextContent(Language.emailRequired);
|
||||
expect(emailErrorElement).toHaveTextContent(
|
||||
"Please enter an email address.",
|
||||
);
|
||||
|
||||
expect(passwordInput).not.toHaveAttribute("aria-invalid", "true");
|
||||
expect(passwordInput).not.toHaveAttribute(
|
||||
@@ -240,13 +241,13 @@ describe("LoginPage", () => {
|
||||
|
||||
await waitForLoaderToBeRemoved();
|
||||
|
||||
const email = screen.getByLabelText(new RegExp(Language.emailLabel));
|
||||
const password = screen.getByLabelText(new RegExp(Language.passwordLabel));
|
||||
const email = screen.getByLabelText(/Email/);
|
||||
const password = screen.getByLabelText(/Password/);
|
||||
|
||||
await userEvent.type(email, "test@coder.com");
|
||||
await userEvent.type(password, "password");
|
||||
|
||||
const signInButton = await screen.findByText(Language.passwordSignIn);
|
||||
const signInButton = await screen.findByText("Sign In");
|
||||
fireEvent.click(signInButton);
|
||||
|
||||
// Then - it should hard redirect to OAuth endpoint
|
||||
|
||||
@@ -3,7 +3,6 @@ import { type FC, useId } from "react";
|
||||
import type { AuthMethods } from "#/api/typesGenerated";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
|
||||
import { Language } from "./Language";
|
||||
|
||||
type OAuthSignInFormProps = {
|
||||
isSigningIn: boolean;
|
||||
@@ -33,7 +32,7 @@ export const OAuthSignInForm: FC<OAuthSignInFormProps> = ({
|
||||
)}`}
|
||||
>
|
||||
<ExternalImage src="/icon/github.svg" />
|
||||
{Language.githubSignIn}
|
||||
GitHub
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
@@ -57,7 +56,7 @@ export const OAuthSignInForm: FC<OAuthSignInFormProps> = ({
|
||||
) : (
|
||||
<KeyIcon />
|
||||
)}
|
||||
{authMethods.oidc.signInText || Language.oidcSignIn}
|
||||
{authMethods.oidc.signInText || "OpenID Connect"}
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { Label } from "#/components/Label/Label";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import { getFormHelpers, onChangeTrimmed } from "#/utils/formUtils";
|
||||
import { Language } from "./Language";
|
||||
|
||||
type PasswordSignInFormProps = {
|
||||
onSubmit: (credentials: { email: string; password: string }) => void;
|
||||
@@ -24,8 +23,8 @@ export const PasswordSignInForm: FC<PasswordSignInFormProps> = ({
|
||||
const validationSchema = Yup.object({
|
||||
email: Yup.string()
|
||||
.trim()
|
||||
.email(Language.emailInvalid)
|
||||
.required(Language.emailRequired),
|
||||
.email("Please enter a valid email address.")
|
||||
.required("Please enter an email address."),
|
||||
password: Yup.string(),
|
||||
});
|
||||
|
||||
@@ -48,7 +47,7 @@ export const PasswordSignInForm: FC<PasswordSignInFormProps> = ({
|
||||
<form onSubmit={form.handleSubmit} className="flex flex-col gap-5">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<Label htmlFor={emailField.id}>
|
||||
{Language.emailLabel}{" "}
|
||||
Email{" "}
|
||||
<span className="text-xs text-content-destructive font-bold">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -75,7 +74,7 @@ export const PasswordSignInForm: FC<PasswordSignInFormProps> = ({
|
||||
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<Label htmlFor={passwordField.id}>
|
||||
{Language.passwordLabel}{" "}
|
||||
Password{" "}
|
||||
<span className="text-xs text-content-destructive font-bold">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
@@ -101,7 +100,7 @@ export const PasswordSignInForm: FC<PasswordSignInFormProps> = ({
|
||||
|
||||
<Button size="lg" disabled={isSigningIn} className="w-full" type="submit">
|
||||
<Spinner loading={isSigningIn} />
|
||||
{Language.passwordSignIn}
|
||||
Sign In
|
||||
</Button>
|
||||
|
||||
<Link
|
||||
|
||||
@@ -18,7 +18,7 @@ type TooltipData = {
|
||||
links: readonly { text: string; href: string }[];
|
||||
};
|
||||
|
||||
const Language = {
|
||||
const tooltipData: Record<ColumnHeader, TooltipData> = {
|
||||
roles: {
|
||||
title: "What is a role?",
|
||||
text:
|
||||
@@ -26,7 +26,6 @@ const Language = {
|
||||
"View our docs on how to use the available roles.",
|
||||
links: [{ text: "User Roles", href: docs("/admin/users/groups-roles") }],
|
||||
},
|
||||
|
||||
groups: {
|
||||
title: "What is a group?",
|
||||
text:
|
||||
@@ -34,7 +33,6 @@ const Language = {
|
||||
"to specific templates. View our docs on how to use groups.",
|
||||
links: [{ text: "User Groups", href: docs("/admin/users/groups-roles") }],
|
||||
},
|
||||
|
||||
ai_addon: {
|
||||
title: "What is the AI add-on?",
|
||||
text:
|
||||
@@ -42,24 +40,24 @@ const Language = {
|
||||
"who are actively consuming a seat.",
|
||||
links: [],
|
||||
},
|
||||
} as const satisfies Record<ColumnHeader, TooltipData>;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
variant: ColumnHeader;
|
||||
};
|
||||
|
||||
export const TableColumnHelpPopover: FC<Props> = ({ variant }) => {
|
||||
const variantLang = Language[variant];
|
||||
const data = tooltipData[variant];
|
||||
|
||||
return (
|
||||
<HelpPopover>
|
||||
<HelpPopoverIconTrigger size="small" />
|
||||
<HelpPopoverContent>
|
||||
<HelpPopoverTitle>{variantLang.title}</HelpPopoverTitle>
|
||||
<HelpPopoverText>{variantLang.text}</HelpPopoverText>
|
||||
{variantLang.links.length > 0 && (
|
||||
<HelpPopoverTitle>{data.title}</HelpPopoverTitle>
|
||||
<HelpPopoverText>{data.text}</HelpPopoverText>
|
||||
{data.links.length > 0 && (
|
||||
<HelpPopoverLinksGroup>
|
||||
{variantLang.links.map((link) => (
|
||||
{data.links.map((link) => (
|
||||
<HelpPopoverLink key={link.text} href={link.href}>
|
||||
{link.text}
|
||||
</HelpPopoverLink>
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from "#/testHelpers/renderHelpers";
|
||||
import { server } from "#/testHelpers/server";
|
||||
import { SetupPage } from "./SetupPage";
|
||||
import { Language as PageViewLanguage } from "./SetupPageView";
|
||||
|
||||
const fillForm = async ({
|
||||
email = "someone@coder.com",
|
||||
@@ -20,12 +19,12 @@ const fillForm = async ({
|
||||
email?: string;
|
||||
password?: string;
|
||||
} = {}) => {
|
||||
const emailField = screen.getByLabelText(PageViewLanguage.emailLabel);
|
||||
const passwordField = screen.getByLabelText(PageViewLanguage.passwordLabel);
|
||||
const emailField = screen.getByLabelText("Email");
|
||||
const passwordField = screen.getByLabelText("Password");
|
||||
await userEvent.type(emailField, email);
|
||||
await userEvent.type(passwordField, password);
|
||||
const submitButton = screen.getByRole("button", {
|
||||
name: PageViewLanguage.create,
|
||||
name: "Continue with email",
|
||||
});
|
||||
await userEvent.click(submitButton);
|
||||
};
|
||||
|
||||
@@ -24,33 +24,7 @@ import {
|
||||
onChangeTrimmed,
|
||||
} from "#/utils/formUtils";
|
||||
|
||||
export const Language = {
|
||||
emailLabel: "Email",
|
||||
passwordLabel: "Password",
|
||||
nameLabel: "Full Name",
|
||||
usernameLabel: "Username",
|
||||
emailInvalid: "Please enter a valid email address.",
|
||||
emailRequired: "Please enter an email address.",
|
||||
passwordRequired: "Please enter a password.",
|
||||
create: "Continue with email",
|
||||
githubCreate: "Continue with GitHub",
|
||||
welcomeMessage: <>Welcome to Coder</>,
|
||||
firstNameLabel: "First name",
|
||||
lastNameLabel: "Last name",
|
||||
companyLabel: "Company",
|
||||
jobTitleLabel: "Job title",
|
||||
phoneNumberLabel: "Phone number",
|
||||
countryLabel: "Country",
|
||||
developersLabel: "Number of developers",
|
||||
firstNameRequired: "Please enter your first name.",
|
||||
phoneNumberRequired: "Please enter your phone number.",
|
||||
jobTitleRequired: "Please enter your job title.",
|
||||
companyNameRequired: "Please enter your company name.",
|
||||
countryRequired: "Please select your country.",
|
||||
developersRequired: "Please select the number of developers in your company.",
|
||||
};
|
||||
|
||||
const usernameValidator = nameValidator(Language.usernameLabel);
|
||||
const usernameValidator = nameValidator("Username");
|
||||
const usernameFromEmail = (email: string): string => {
|
||||
try {
|
||||
const emailPrefix = email.split("@")[0];
|
||||
@@ -69,22 +43,24 @@ const usernameFromEmail = (email: string): string => {
|
||||
const validationSchema = Yup.object({
|
||||
email: Yup.string()
|
||||
.trim()
|
||||
.email(Language.emailInvalid)
|
||||
.required(Language.emailRequired),
|
||||
password: Yup.string().required(Language.passwordRequired),
|
||||
.email("Please enter a valid email address.")
|
||||
.required("Please enter an email address."),
|
||||
password: Yup.string().required("Please enter a password."),
|
||||
username: usernameValidator,
|
||||
trial: Yup.bool(),
|
||||
trial_info: Yup.object().when("trial", {
|
||||
is: true,
|
||||
then: (schema) =>
|
||||
schema.shape({
|
||||
first_name: Yup.string().required(Language.firstNameRequired),
|
||||
last_name: Yup.string().required(Language.firstNameRequired),
|
||||
phone_number: Yup.string().required(Language.phoneNumberRequired),
|
||||
job_title: Yup.string().required(Language.jobTitleRequired),
|
||||
company_name: Yup.string().required(Language.companyNameRequired),
|
||||
country: Yup.string().required(Language.countryRequired),
|
||||
developers: Yup.string().required(Language.developersRequired),
|
||||
first_name: Yup.string().required("Please enter your first name."),
|
||||
last_name: Yup.string().required("Please enter your last name."),
|
||||
phone_number: Yup.string().required("Please enter your phone number."),
|
||||
job_title: Yup.string().required("Please enter your job title."),
|
||||
company_name: Yup.string().required("Please enter your company name."),
|
||||
country: Yup.string().required("Please select your country."),
|
||||
developers: Yup.string().required(
|
||||
"Please select the number of developers in your company.",
|
||||
),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
@@ -163,7 +139,7 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
|
||||
<Button className="w-full" asChild type="submit" size="lg">
|
||||
<a href="/api/v2/users/oauth2/github/callback">
|
||||
<ExternalImage src="/icon/github.svg" />
|
||||
{Language.githubCreate}
|
||||
Continue with GitHub
|
||||
</a>
|
||||
</Button>
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -185,13 +161,13 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
|
||||
}}
|
||||
autoComplete="email"
|
||||
fullWidth
|
||||
label={Language.emailLabel}
|
||||
label="Email"
|
||||
/>
|
||||
<PasswordField
|
||||
{...getFieldHelpers("password")}
|
||||
autoComplete="current-password"
|
||||
fullWidth
|
||||
label={Language.passwordLabel}
|
||||
label="Password"
|
||||
/>
|
||||
<label
|
||||
htmlFor="trial"
|
||||
@@ -239,14 +215,14 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
|
||||
id="trial_info.first_name"
|
||||
name="trial_info.first_name"
|
||||
fullWidth
|
||||
label={Language.firstNameLabel}
|
||||
label="First name"
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("trial_info.last_name")}
|
||||
id="trial_info.last_name"
|
||||
name="trial_info.last_name"
|
||||
fullWidth
|
||||
label={Language.lastNameLabel}
|
||||
label="Last name"
|
||||
/>
|
||||
</Stack>
|
||||
<TextField
|
||||
@@ -254,21 +230,21 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
|
||||
id="trial_info.company_name"
|
||||
name="trial_info.company_name"
|
||||
fullWidth
|
||||
label={Language.companyLabel}
|
||||
label="Company"
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("trial_info.job_title")}
|
||||
id="trial_info.job_title"
|
||||
name="trial_info.job_title"
|
||||
fullWidth
|
||||
label={Language.jobTitleLabel}
|
||||
label="Job title"
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("trial_info.phone_number")}
|
||||
id="trial_info.phone_number"
|
||||
name="trial_info.phone_number"
|
||||
fullWidth
|
||||
label={Language.phoneNumberLabel}
|
||||
label="Phone number"
|
||||
/>
|
||||
<Autocomplete
|
||||
autoHighlight
|
||||
@@ -287,7 +263,7 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
|
||||
{...getFieldHelpers("trial_info.country")}
|
||||
id="trial_info.country"
|
||||
name="trial_info.country"
|
||||
label={Language.countryLabel}
|
||||
label="Country"
|
||||
fullWidth
|
||||
inputProps={{
|
||||
...params.inputProps,
|
||||
@@ -301,7 +277,7 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
|
||||
id="trial_info.developers"
|
||||
name="trial_info.developers"
|
||||
fullWidth
|
||||
label={Language.developersLabel}
|
||||
label="Number of developers"
|
||||
select
|
||||
>
|
||||
{numberOfDevelopersOptions.map((opt) => (
|
||||
@@ -356,7 +332,7 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
|
||||
size="lg"
|
||||
>
|
||||
<Spinner loading={isLoading} />
|
||||
{Language.create}
|
||||
Continue with email
|
||||
</Button>
|
||||
</FormFields>
|
||||
</VerticalForm>
|
||||
|
||||
@@ -8,16 +8,6 @@ import {
|
||||
formatTemplateBuildTime,
|
||||
} from "#/utils/templates";
|
||||
|
||||
const Language = {
|
||||
usedByLabel: "Used by",
|
||||
buildTimeLabel: "Build time",
|
||||
activeVersionLabel: "Active version",
|
||||
lastUpdateLabel: "Last updated",
|
||||
developerPlural: "developers",
|
||||
developerSingular: "developer",
|
||||
createdByLabel: "Created by",
|
||||
};
|
||||
|
||||
interface TemplateStatsProps {
|
||||
template: Template;
|
||||
activeVersion: TemplateVersion;
|
||||
@@ -30,22 +20,20 @@ export const TemplateStats: FC<TemplateStatsProps> = ({
|
||||
return (
|
||||
<Stats>
|
||||
<StatsItem
|
||||
label={Language.usedByLabel}
|
||||
label="Used by"
|
||||
value={
|
||||
<>
|
||||
{formatTemplateActiveDevelopers(template.active_user_count)}{" "}
|
||||
{template.active_user_count === 1
|
||||
? Language.developerSingular
|
||||
: Language.developerPlural}
|
||||
{template.active_user_count === 1 ? "developer" : "developers"}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StatsItem
|
||||
label={Language.buildTimeLabel}
|
||||
label="Build time"
|
||||
value={formatTemplateBuildTime(template.build_time_stats.start.P50)}
|
||||
/>
|
||||
<StatsItem
|
||||
label={Language.activeVersionLabel}
|
||||
label="Active version"
|
||||
value={
|
||||
<Link to={`versions/${activeVersion.name}`}>
|
||||
{activeVersion.name}
|
||||
@@ -53,13 +41,10 @@ export const TemplateStats: FC<TemplateStatsProps> = ({
|
||||
}
|
||||
/>
|
||||
<StatsItem
|
||||
label={Language.lastUpdateLabel}
|
||||
label="Last updated"
|
||||
value={createDayString(template.updated_at)}
|
||||
/>
|
||||
<StatsItem
|
||||
label={Language.createdByLabel}
|
||||
value={template.created_by_name}
|
||||
/>
|
||||
<StatsItem label="Created by" value={template.created_by_name} />
|
||||
</Stats>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,13 +11,6 @@ import { TableLoader } from "#/components/TableLoader/TableLoader";
|
||||
import { Timeline } from "#/components/Timeline/Timeline";
|
||||
import { VersionRow } from "./VersionRow";
|
||||
|
||||
const Language = {
|
||||
emptyMessage: "No versions found",
|
||||
nameLabel: "Version name",
|
||||
createdAtLabel: "Created at",
|
||||
createdByLabel: "Created by",
|
||||
};
|
||||
|
||||
interface VersionsTableProps {
|
||||
activeVersionId: string;
|
||||
versions?: TypesGen.TemplateVersion[];
|
||||
@@ -75,7 +68,7 @@ export const VersionsTable: FC<VersionsTableProps> = ({
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<div className="p-8">
|
||||
<EmptyState message={Language.emptyMessage} />
|
||||
<EmptyState message="No versions found" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -4,6 +4,7 @@ import TextField from "@mui/material/TextField";
|
||||
import { useFormik } from "formik";
|
||||
import type { FC } from "react";
|
||||
import * as Yup from "yup";
|
||||
import { EnterpriseBadge } from "#/components/Badges/Badges";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
import type { DialogProps } from "#/components/Dialogs/Dialog";
|
||||
import { FormFields } from "#/components/Form/Form";
|
||||
@@ -18,18 +19,8 @@ import {
|
||||
} from "#/components/HelpPopover/HelpPopover";
|
||||
import { Stack } from "#/components/Stack/Stack";
|
||||
import type { PublishVersionData } from "#/pages/TemplateVersionEditorPage/types";
|
||||
import { docs } from "#/utils/docs";
|
||||
import { getFormHelpers } from "#/utils/formUtils";
|
||||
import { docs } from "../../utils/docs";
|
||||
|
||||
export const Language = {
|
||||
versionNameLabel: "Version name",
|
||||
messagePlaceholder: "Write a short message about the changes you made...",
|
||||
defaultCheckboxLabel: "Promote to active version",
|
||||
activeVersionHelpTitle: "Active versions",
|
||||
activeVersionHelpText:
|
||||
"Templates can enforce that the active version be used for all workspaces (enterprise-only)",
|
||||
activeVersionHelpBody: "Review the documentation",
|
||||
};
|
||||
|
||||
type PublishTemplateVersionDialogProps = DialogProps & {
|
||||
defaultName: string;
|
||||
@@ -88,7 +79,7 @@ export const PublishTemplateVersionDialog: FC<
|
||||
<FormFields>
|
||||
<TextField
|
||||
{...getFieldHelpers("name")}
|
||||
label={Language.versionNameLabel}
|
||||
label="Version name"
|
||||
autoFocus
|
||||
disabled={isPublishing}
|
||||
/>
|
||||
@@ -96,7 +87,7 @@ export const PublishTemplateVersionDialog: FC<
|
||||
<TextField
|
||||
{...getFieldHelpers("message")}
|
||||
label="Message"
|
||||
placeholder={Language.messagePlaceholder}
|
||||
placeholder="Write a short message about the changes you made..."
|
||||
disabled={isPublishing}
|
||||
multiline
|
||||
rows={5}
|
||||
@@ -104,7 +95,7 @@ export const PublishTemplateVersionDialog: FC<
|
||||
|
||||
<Stack direction="row">
|
||||
<FormControlLabel
|
||||
label={Language.defaultCheckboxLabel}
|
||||
label="Promote to active version"
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
@@ -128,11 +119,10 @@ export const PublishTemplateVersionDialog: FC<
|
||||
* this prop may not need to be set when we switch away from MuiDialog
|
||||
*/}
|
||||
<HelpPopoverContent disablePortal>
|
||||
<HelpPopoverTitle>
|
||||
{Language.activeVersionHelpTitle}
|
||||
</HelpPopoverTitle>
|
||||
<HelpPopoverTitle>Active versions</HelpPopoverTitle>
|
||||
<HelpPopoverText>
|
||||
{Language.activeVersionHelpText}
|
||||
Templates can enforce that the active version be used for
|
||||
all workspaces <EnterpriseBadge />
|
||||
</HelpPopoverText>
|
||||
<HelpPopoverLinksGroup>
|
||||
<HelpPopoverLink
|
||||
@@ -140,7 +130,7 @@ export const PublishTemplateVersionDialog: FC<
|
||||
"/admin/templates/managing-templates#template-update-policies",
|
||||
)}
|
||||
>
|
||||
{Language.activeVersionHelpBody}
|
||||
Review the documentation
|
||||
</HelpPopoverLink>
|
||||
</HelpPopoverLinksGroup>
|
||||
</HelpPopoverContent>
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
import { server } from "#/testHelpers/server";
|
||||
import type { FileTree } from "#/utils/filetree";
|
||||
import type { MonacoEditorProps } from "./MonacoEditor";
|
||||
import { Language } from "./PublishTemplateVersionDialog";
|
||||
import TemplateVersionEditorPage, {
|
||||
findEntrypointFile,
|
||||
getActivePath,
|
||||
@@ -188,7 +187,7 @@ test("Do not mark as active if promote is not checked", async () => {
|
||||
await user.clear(nameField);
|
||||
await user.type(nameField, "v1.0");
|
||||
await user.click(
|
||||
within(publishDialog).getByLabelText(Language.defaultCheckboxLabel),
|
||||
within(publishDialog).getByLabelText("Promote to active version"),
|
||||
);
|
||||
await user.click(
|
||||
within(publishDialog).getByRole("button", { name: "Publish" }),
|
||||
|
||||
@@ -52,32 +52,19 @@ import { EmptyTemplates } from "./EmptyTemplates";
|
||||
import { TemplatesFilter } from "./TemplatesFilter";
|
||||
import type { TemplateFilterState } from "./TemplatesPage";
|
||||
|
||||
const Language = {
|
||||
developerCount: (activeCount: number): string => {
|
||||
return `${formatTemplateActiveDevelopers(activeCount)} developer${
|
||||
activeCount !== 1 ? "s" : ""
|
||||
}`;
|
||||
},
|
||||
nameLabel: "Name",
|
||||
buildTimeLabel: "Build time",
|
||||
usedByLabel: "Used by",
|
||||
lastUpdatedLabel: "Last updated",
|
||||
templateTooltipTitle: "What is template?",
|
||||
templateTooltipText:
|
||||
"With templates you can create a common configuration for your workspaces using Terraform.",
|
||||
templateTooltipLink: "Manage templates",
|
||||
};
|
||||
|
||||
const TemplateHelpPopover: FC = () => {
|
||||
return (
|
||||
<HelpPopover>
|
||||
<HelpPopoverIconTrigger />
|
||||
<HelpPopoverContent>
|
||||
<HelpPopoverTitle>{Language.templateTooltipTitle}</HelpPopoverTitle>
|
||||
<HelpPopoverText>{Language.templateTooltipText}</HelpPopoverText>
|
||||
<HelpPopoverTitle>What is a template?</HelpPopoverTitle>
|
||||
<HelpPopoverText>
|
||||
With templates you can create a common configuration for your
|
||||
workspaces using Terraform.
|
||||
</HelpPopoverText>
|
||||
<HelpPopoverLinksGroup>
|
||||
<HelpPopoverLink href={docs("/admin/templates")}>
|
||||
{Language.templateTooltipLink}
|
||||
Manage templates
|
||||
</HelpPopoverLink>
|
||||
</HelpPopoverLinksGroup>
|
||||
</HelpPopoverContent>
|
||||
@@ -145,6 +132,8 @@ const TemplateRow: FC<TemplateRowProps> = ({
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const developerCount = `${formatTemplateActiveDevelopers(template.active_user_count)} developer${template.active_user_count !== 1 ? "s" : ""}`;
|
||||
|
||||
const clickableRow = useClickableTableRow({
|
||||
onClick: () => navigate(templatePageLink),
|
||||
});
|
||||
@@ -175,11 +164,11 @@ const TemplateRow: FC<TemplateRowProps> = ({
|
||||
{showOrganizations ? (
|
||||
<AvatarData
|
||||
title={template.organization_display_name}
|
||||
subtitle={`Used by ${Language.developerCount(template.active_user_count)}`}
|
||||
subtitle={`Used by ${developerCount}`}
|
||||
avatar={<Avatar variant="icon" src={template.organization_icon} />}
|
||||
/>
|
||||
) : (
|
||||
Language.developerCount(template.active_user_count)
|
||||
developerCount
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
@@ -262,14 +251,12 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = ({
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[35%]">{Language.nameLabel}</TableHead>
|
||||
<TableHead className="w-[35%]">Name</TableHead>
|
||||
<TableHead className="w-[15%]">
|
||||
{showOrganizations ? "Organization" : Language.usedByLabel}
|
||||
</TableHead>
|
||||
<TableHead className="w-[10%]">{Language.buildTimeLabel}</TableHead>
|
||||
<TableHead className="w-[15%]">
|
||||
{Language.lastUpdatedLabel}
|
||||
{showOrganizations ? "Organization" : "Used by"}
|
||||
</TableHead>
|
||||
<TableHead className="w-[10%]">Build time</TableHead>
|
||||
<TableHead className="w-[15%]">Last updated</TableHead>
|
||||
<TableHead className="w-[1%]" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "#/testHelpers/entities";
|
||||
import { renderWithAuth } from "#/testHelpers/renderHelpers";
|
||||
import { server } from "#/testHelpers/server";
|
||||
import TerminalPage, { Language } from "./TerminalPage";
|
||||
import TerminalPage from "./TerminalPage";
|
||||
|
||||
const renderTerminal = async (
|
||||
route = `/${MockUserOwner.username}/${MockWorkspace.name}/terminal`,
|
||||
@@ -83,7 +83,7 @@ describe("TerminalPage", () => {
|
||||
|
||||
const { container } = await renderTerminal();
|
||||
|
||||
await expectTerminalText(container, Language.workspaceErrorMessagePrefix);
|
||||
await expectTerminalText(container, "Unable to fetch workspace: ");
|
||||
});
|
||||
|
||||
it("shows reconnect message when websocket fails", async () => {
|
||||
|
||||
@@ -36,12 +36,6 @@ import { getMatchingAgentOrFirst } from "#/utils/workspace";
|
||||
import { TerminalAlerts } from "./TerminalAlerts";
|
||||
import type { ConnectionStatus } from "./types";
|
||||
|
||||
export const Language = {
|
||||
workspaceErrorMessagePrefix: "Unable to fetch workspace: ",
|
||||
workspaceAgentErrorMessagePrefix: "Unable to fetch workspace agent: ",
|
||||
websocketErrorMessagePrefix: "WebSocket failed: ",
|
||||
};
|
||||
|
||||
const TerminalPage: FC = () => {
|
||||
// Maybe one day we'll support a light themed terminal, but terminal coloring
|
||||
// is notably a pain because of assumptions certain programs might make about your
|
||||
@@ -271,16 +265,14 @@ const TerminalPage: FC = () => {
|
||||
}
|
||||
|
||||
if (workspace.error instanceof Error) {
|
||||
terminal.writeln(
|
||||
Language.workspaceErrorMessagePrefix + workspace.error.message,
|
||||
);
|
||||
terminal.writeln(`Unable to fetch workspace: ${workspace.error.message}`);
|
||||
setConnectionStatus("disconnected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!workspaceAgent) {
|
||||
terminal.writeln(
|
||||
`${Language.workspaceAgentErrorMessagePrefix}no agent found with ID, is the workspace started?`,
|
||||
"Unable to fetch workspace agent: no agent found with ID, is the workspace started?",
|
||||
);
|
||||
setConnectionStatus("disconnected");
|
||||
return;
|
||||
|
||||
@@ -13,15 +13,8 @@ import {
|
||||
onChangeTrimmed,
|
||||
} from "#/utils/formUtils";
|
||||
|
||||
export const Language = {
|
||||
usernameLabel: "Username",
|
||||
emailLabel: "Email",
|
||||
nameLabel: "Name",
|
||||
updateSettings: "Update account",
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
username: nameValidator(Language.usernameLabel),
|
||||
username: nameValidator("Username"),
|
||||
name: Yup.string(),
|
||||
});
|
||||
|
||||
@@ -60,12 +53,7 @@ export const AccountForm: FC<AccountFormProps> = ({
|
||||
<ErrorAlert error={updateProfileError} />
|
||||
)}
|
||||
|
||||
<TextField
|
||||
disabled
|
||||
fullWidth
|
||||
label={Language.emailLabel}
|
||||
value={email}
|
||||
/>
|
||||
<TextField disabled fullWidth label="Email" value={email} />
|
||||
<TextField
|
||||
{...getFieldHelpers("username")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
@@ -73,7 +61,7 @@ export const AccountForm: FC<AccountFormProps> = ({
|
||||
autoComplete="username"
|
||||
disabled={!editable}
|
||||
fullWidth
|
||||
label={Language.usernameLabel}
|
||||
label="Username"
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("name")}
|
||||
@@ -83,14 +71,14 @@ export const AccountForm: FC<AccountFormProps> = ({
|
||||
e.target.value = e.target.value.trim();
|
||||
form.handleChange(e);
|
||||
}}
|
||||
label={Language.nameLabel}
|
||||
label="Name"
|
||||
helperText='The human-readable name is optional and can be accessed in a template via the "data.coder_workspace_owner.me.full_name" property.'
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button disabled={isLoading} type="submit">
|
||||
<Spinner loading={isLoading} />
|
||||
{Language.updateSettings}
|
||||
Update account
|
||||
</Button>
|
||||
</div>
|
||||
</FormFields>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { API } from "#/api/api";
|
||||
import { mockApiError } from "#/testHelpers/entities";
|
||||
import { renderWithAuth } from "#/testHelpers/renderHelpers";
|
||||
import * as AccountForm from "./AccountForm";
|
||||
import AccountPage from "./AccountPage";
|
||||
|
||||
const newData = {
|
||||
@@ -19,7 +18,7 @@ const fillAndSubmitForm = async () => {
|
||||
fireEvent.change(screen.getByLabelText("Name"), {
|
||||
target: { value: newData.name },
|
||||
});
|
||||
fireEvent.click(screen.getByText(AccountForm.Language.updateSettings));
|
||||
fireEvent.click(screen.getByText("Update account"));
|
||||
};
|
||||
|
||||
describe("AccountPage", () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { fireEvent, screen, within } from "@testing-library/react";
|
||||
import { API } from "#/api/api";
|
||||
import { MockGitSSHKey, mockApiError } from "#/testHelpers/entities";
|
||||
import { renderWithAuth } from "#/testHelpers/renderHelpers";
|
||||
import SSHKeysPage, { Language as SSHKeysPageLanguage } from "./SSHKeysPage";
|
||||
import SSHKeysPage from "./SSHKeysPage";
|
||||
|
||||
describe("SSH keys Page", () => {
|
||||
it("shows the SSH key", async () => {
|
||||
@@ -23,7 +23,7 @@ describe("SSH keys Page", () => {
|
||||
fireEvent.click(regenerateButton);
|
||||
const confirmDialog = screen.getByRole("dialog");
|
||||
expect(confirmDialog).toHaveTextContent(
|
||||
SSHKeysPageLanguage.regenerateDialogMessage,
|
||||
"You will need to replace the public SSH key on services you use it with, and you'll need to rebuild existing workspaces.",
|
||||
);
|
||||
|
||||
const newUserSSHKey =
|
||||
@@ -35,7 +35,7 @@ describe("SSH keys Page", () => {
|
||||
|
||||
// Click on the "Confirm" button
|
||||
const confirmButton = within(confirmDialog).getByRole("button", {
|
||||
name: SSHKeysPageLanguage.confirmLabel,
|
||||
name: "Confirm",
|
||||
});
|
||||
fireEvent.click(confirmButton);
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("SSH keys Page", () => {
|
||||
|
||||
vi.spyOn(API, "regenerateUserSSHKey").mockRejectedValueOnce(
|
||||
mockApiError({
|
||||
message: SSHKeysPageLanguage.regenerationError,
|
||||
message: "Failed to regenerate SSH key",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -68,17 +68,17 @@ describe("SSH keys Page", () => {
|
||||
fireEvent.click(regenerateButton);
|
||||
const confirmDialog = screen.getByRole("dialog");
|
||||
expect(confirmDialog).toHaveTextContent(
|
||||
SSHKeysPageLanguage.regenerateDialogMessage,
|
||||
"You will need to replace the public SSH key on services you use it with, and you'll need to rebuild existing workspaces.",
|
||||
);
|
||||
|
||||
// Click on the "Confirm" button
|
||||
const confirmButton = within(confirmDialog).getByRole("button", {
|
||||
name: SSHKeysPageLanguage.confirmLabel,
|
||||
name: "Confirm",
|
||||
});
|
||||
fireEvent.click(confirmButton);
|
||||
|
||||
// Check if the error message is displayed
|
||||
await screen.findByText(SSHKeysPageLanguage.regenerationError);
|
||||
await screen.findByText("Failed to regenerate SSH key");
|
||||
|
||||
// Check if the API was called correctly
|
||||
expect(API.regenerateUserSSHKey).toBeCalledTimes(1);
|
||||
|
||||
@@ -7,17 +7,6 @@ import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog"
|
||||
import { Section } from "../Section";
|
||||
import { SSHKeysPageView } from "./SSHKeysPageView";
|
||||
|
||||
export const Language = {
|
||||
title: "SSH keys",
|
||||
regenerateDialogTitle: "Regenerate SSH key?",
|
||||
regenerationError: "Failed to regenerate SSH key",
|
||||
regenerationSuccess: "SSH Key regenerated successfully.",
|
||||
regenerateDialogMessage:
|
||||
"You will need to replace the public SSH key on services you use it with, and you'll need to rebuild existing workspaces.",
|
||||
confirmLabel: "Confirm",
|
||||
cancelLabel: "Cancel",
|
||||
};
|
||||
|
||||
const SSHKeysPage: FC = () => {
|
||||
const [isConfirmingRegeneration, setIsConfirmingRegeneration] =
|
||||
useState(false);
|
||||
@@ -30,7 +19,7 @@ const SSHKeysPage: FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section title={Language.title}>
|
||||
<Section title="SSH keys">
|
||||
<SSHKeysPageView
|
||||
isLoading={userSSHKeyQuery.isLoading}
|
||||
getSSHKeyError={userSSHKeyQuery.error}
|
||||
@@ -44,18 +33,21 @@ const SSHKeysPage: FC = () => {
|
||||
hideCancel={false}
|
||||
open={isConfirmingRegeneration}
|
||||
confirmLoading={regenerateSSHKeyMutation.isPending}
|
||||
title={Language.regenerateDialogTitle}
|
||||
description={Language.regenerateDialogMessage}
|
||||
confirmText={Language.confirmLabel}
|
||||
title="Regenerate SSH key?"
|
||||
description="You will need to replace the public SSH key on services you use it with, and you'll need to rebuild existing workspaces."
|
||||
confirmText="Confirm"
|
||||
onClose={() => setIsConfirmingRegeneration(false)}
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
await regenerateSSHKeyMutation.mutateAsync();
|
||||
toast.success(Language.regenerationSuccess);
|
||||
toast.success("SSH Key regenerated successfully.");
|
||||
} catch (error) {
|
||||
toast.error(getErrorMessage(error, Language.regenerationError), {
|
||||
description: getErrorDetail(error),
|
||||
});
|
||||
toast.error(
|
||||
getErrorMessage(error, "Failed to regenerate SSH key"),
|
||||
{
|
||||
description: getErrorDetail(error),
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
setIsConfirmingRegeneration(false);
|
||||
}
|
||||
|
||||
@@ -16,27 +16,18 @@ interface SecurityFormValues {
|
||||
confirm_password: string;
|
||||
}
|
||||
|
||||
export const Language = {
|
||||
oldPasswordLabel: "Old Password",
|
||||
newPasswordLabel: "New Password",
|
||||
confirmPasswordLabel: "Confirm Password",
|
||||
oldPasswordRequired: "Old password is required",
|
||||
newPasswordRequired: "New password is required",
|
||||
confirmPasswordRequired: "Password confirmation is required",
|
||||
passwordMinLength: "Password must be at least 8 characters",
|
||||
passwordMaxLength: "Password must be no more than 64 characters",
|
||||
confirmPasswordMatch: "Password and confirmation must match",
|
||||
updatePassword: "Update password",
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
old_password: Yup.string().trim().required(Language.oldPasswordRequired),
|
||||
password: Yup.string().trim().required(Language.newPasswordRequired),
|
||||
old_password: Yup.string().trim().required("Old password is required"),
|
||||
password: Yup.string().trim().required("New password is required"),
|
||||
confirm_password: Yup.string()
|
||||
.trim()
|
||||
.test("passwords-match", Language.confirmPasswordMatch, function (value) {
|
||||
return (this.parent as SecurityFormValues).password === value;
|
||||
}),
|
||||
.test(
|
||||
"passwords-match",
|
||||
"Password and confirmation must match",
|
||||
function (value) {
|
||||
return (this.parent as SecurityFormValues).password === value;
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
interface SecurityFormProps {
|
||||
@@ -80,27 +71,27 @@ export const SecurityForm: FC<SecurityFormProps> = ({
|
||||
{...getFieldHelpers("old_password")}
|
||||
autoComplete="old_password"
|
||||
fullWidth
|
||||
label={Language.oldPasswordLabel}
|
||||
label="Old Password"
|
||||
type="password"
|
||||
/>
|
||||
<PasswordField
|
||||
{...getFieldHelpers("password")}
|
||||
autoComplete="password"
|
||||
fullWidth
|
||||
label={Language.newPasswordLabel}
|
||||
label="New Password"
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("confirm_password")}
|
||||
autoComplete="confirm_password"
|
||||
fullWidth
|
||||
label={Language.confirmPasswordLabel}
|
||||
label="Confirm Password"
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Button disabled={isLoading} type="submit">
|
||||
<Spinner loading={isLoading} />
|
||||
{Language.updatePassword}
|
||||
Update password
|
||||
</Button>
|
||||
</div>
|
||||
</FormFields>
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
renderWithAuth,
|
||||
waitForLoaderToBeRemoved,
|
||||
} from "#/testHelpers/renderHelpers";
|
||||
import { Language } from "./SecurityForm";
|
||||
import SecurityPage from "./SecurityPage";
|
||||
import * as SSO from "./SingleSignOnSection";
|
||||
|
||||
@@ -33,7 +32,7 @@ const fillAndSubmitSecurityForm = () => {
|
||||
fireEvent.change(screen.getByLabelText("Confirm Password"), {
|
||||
target: { value: newSecurityFormValues.confirm_password },
|
||||
});
|
||||
fireEvent.click(screen.getByText(Language.updatePassword));
|
||||
fireEvent.click(screen.getByText("Update password"));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FC, JSX } from "react";
|
||||
import type { FC } from "react";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { CodeExample } from "#/components/CodeExample/CodeExample";
|
||||
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
|
||||
@@ -12,16 +12,6 @@ interface ResetPasswordDialogProps {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const Language = {
|
||||
title: "Reset password",
|
||||
message: (username?: string): JSX.Element => (
|
||||
<>
|
||||
You will need to send <strong>{username}</strong> the following password:
|
||||
</>
|
||||
),
|
||||
confirmText: "Reset password",
|
||||
};
|
||||
|
||||
export const ResetPasswordDialog: FC<ResetPasswordDialogProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
@@ -32,7 +22,10 @@ export const ResetPasswordDialog: FC<ResetPasswordDialogProps> = ({
|
||||
}) => {
|
||||
const description = (
|
||||
<>
|
||||
<p>{Language.message(user?.username)}</p>
|
||||
<p>
|
||||
You will need to send <strong>{user?.username}</strong> the following
|
||||
password:
|
||||
</p>
|
||||
<CodeExample
|
||||
secret={false}
|
||||
code={newPassword ?? ""}
|
||||
@@ -48,9 +41,9 @@ export const ResetPasswordDialog: FC<ResetPasswordDialogProps> = ({
|
||||
open={open}
|
||||
onConfirm={onConfirm}
|
||||
onClose={onClose}
|
||||
title={Language.title}
|
||||
title="Reset password"
|
||||
confirmLoading={loading}
|
||||
confirmText={Language.confirmText}
|
||||
confirmText="Reset password"
|
||||
description={description}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -12,16 +12,6 @@ import {
|
||||
import { TableColumnHelpPopover } from "../../OrganizationSettingsPage/UserTable/TableColumnHelpPopover";
|
||||
import { UsersTableBody } from "./UsersTableBody";
|
||||
|
||||
const Language = {
|
||||
usernameLabel: "User",
|
||||
rolesLabel: "Roles",
|
||||
groupsLabel: "Groups",
|
||||
aiAddonLabel: "AI add-on",
|
||||
statusLabel: "Status",
|
||||
lastSeenLabel: "Last Seen",
|
||||
loginTypeLabel: "Login Type",
|
||||
} as const;
|
||||
|
||||
interface UsersTableProps {
|
||||
users: readonly TypesGen.User[] | undefined;
|
||||
roles: TypesGen.AssignableRoles[] | undefined;
|
||||
@@ -72,29 +62,29 @@ export const UsersTable: FC<UsersTableProps> = ({
|
||||
<Table data-testid="users-table">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-2/6">{Language.usernameLabel}</TableHead>
|
||||
<TableHead className="w-2/6">User</TableHead>
|
||||
<TableHead className="w-2/6">
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<span>{Language.rolesLabel}</span>
|
||||
<span>Roles</span>
|
||||
<TableColumnHelpPopover variant="roles" />
|
||||
</Stack>
|
||||
</TableHead>
|
||||
<TableHead className="w-1/6">
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<span>{Language.groupsLabel}</span>
|
||||
<span>Groups</span>
|
||||
<TableColumnHelpPopover variant="groups" />
|
||||
</Stack>
|
||||
</TableHead>
|
||||
{showAISeatColumn && (
|
||||
<TableHead className="w-1/6">
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<span>{Language.aiAddonLabel}</span>
|
||||
<span>AI add-on</span>
|
||||
<TableColumnHelpPopover variant="ai_addon" />
|
||||
</Stack>
|
||||
</TableHead>
|
||||
)}
|
||||
<TableHead className="w-1/6">{Language.loginTypeLabel}</TableHead>
|
||||
<TableHead className="w-1/6">{Language.statusLabel}</TableHead>
|
||||
<TableHead className="w-1/6">Login Type</TableHead>
|
||||
<TableHead className="w-1/6">Status</TableHead>
|
||||
{canEditUsers && <TableHead className="w-auto" />}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
+14
-9
@@ -5,7 +5,6 @@ import { MockTemplate } from "#/testHelpers/entities";
|
||||
import { render } from "#/testHelpers/renderHelpers";
|
||||
import { timeZones } from "#/utils/timeZones";
|
||||
import {
|
||||
Language,
|
||||
ttlShutdownAt,
|
||||
validationSchema,
|
||||
WorkspaceScheduleForm,
|
||||
@@ -71,7 +70,9 @@ describe("validationSchema", () => {
|
||||
saturday: false,
|
||||
};
|
||||
const validate = () => validationSchema.validateSync(values);
|
||||
expect(validate).toThrow(Language.errorNoDayOfWeek);
|
||||
expect(validate).toThrow(
|
||||
"Must set at least one day of week if autostart is enabled.",
|
||||
);
|
||||
});
|
||||
|
||||
it("disallows empty startTime when autostart is enabled", () => {
|
||||
@@ -87,7 +88,9 @@ describe("validationSchema", () => {
|
||||
startTime: "",
|
||||
};
|
||||
const validate = () => validationSchema.validateSync(values);
|
||||
expect(validate).toThrow(Language.errorNoTime);
|
||||
expect(validate).toThrow(
|
||||
"Start time is required when autostart is enabled.",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows startTime 16:20", () => {
|
||||
@@ -105,7 +108,7 @@ describe("validationSchema", () => {
|
||||
startTime: "9:30",
|
||||
};
|
||||
const validate = () => validationSchema.validateSync(values);
|
||||
expect(validate).toThrow(Language.errorTime);
|
||||
expect(validate).toThrow("Time must be in HH:mm format.");
|
||||
});
|
||||
|
||||
it("disallows startTime to be HH:m", () => {
|
||||
@@ -114,7 +117,7 @@ describe("validationSchema", () => {
|
||||
startTime: "09:5",
|
||||
};
|
||||
const validate = () => validationSchema.validateSync(values);
|
||||
expect(validate).toThrow(Language.errorTime);
|
||||
expect(validate).toThrow("Time must be in HH:mm format.");
|
||||
});
|
||||
|
||||
it("disallows an invalid startTime 24:01", () => {
|
||||
@@ -123,7 +126,7 @@ describe("validationSchema", () => {
|
||||
startTime: "24:01",
|
||||
};
|
||||
const validate = () => validationSchema.validateSync(values);
|
||||
expect(validate).toThrow(Language.errorTime);
|
||||
expect(validate).toThrow("Time must be in HH:mm format.");
|
||||
});
|
||||
|
||||
it("disallows an invalid startTime 09:60", () => {
|
||||
@@ -132,7 +135,7 @@ describe("validationSchema", () => {
|
||||
startTime: "09:60",
|
||||
};
|
||||
const validate = () => validationSchema.validateSync(values);
|
||||
expect(validate).toThrow(Language.errorTime);
|
||||
expect(validate).toThrow("Time must be in HH:mm format.");
|
||||
});
|
||||
|
||||
it("disallows an invalid timezone Canada/North", () => {
|
||||
@@ -141,7 +144,7 @@ describe("validationSchema", () => {
|
||||
timezone: "Canada/North",
|
||||
};
|
||||
const validate = () => validationSchema.validateSync(values);
|
||||
expect(validate).toThrow(Language.errorTimezone);
|
||||
expect(validate).toThrow("Invalid timezone.");
|
||||
});
|
||||
|
||||
it("validation passes for all timezones", () => {
|
||||
@@ -179,7 +182,9 @@ describe("validationSchema", () => {
|
||||
ttl: 24 * 30 + 1,
|
||||
};
|
||||
const validate = () => validationSchema.validateSync(values);
|
||||
expect(validate).toThrow(Language.errorTtlMax);
|
||||
expect(validate).toThrow(
|
||||
"Please enter a limit that is less than or equal to 30 days (720 hours).",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows a ttl of 1.2 hours", () => {
|
||||
|
||||
+44
-61
@@ -34,34 +34,6 @@ import { timeZones } from "#/utils/timeZones";
|
||||
// Need dayjs.tz functions for timezone validation
|
||||
dayjs.extend(timezone);
|
||||
|
||||
export const Language = {
|
||||
errorNoDayOfWeek:
|
||||
"Must set at least one day of week if autostart is enabled.",
|
||||
errorNoTime: "Start time is required when autostart is enabled.",
|
||||
errorTime: "Time must be in HH:mm format.",
|
||||
errorTimezone: "Invalid timezone.",
|
||||
errorNoStop:
|
||||
"Time until shutdown must be greater than zero when autostop is enabled.",
|
||||
errorTtlMax:
|
||||
"Please enter a limit that is less than or equal to 720 hours (30 days).",
|
||||
daysOfWeekLabel: "Days of Week",
|
||||
daySundayLabel: "Sun",
|
||||
dayMondayLabel: "Mon",
|
||||
dayTuesdayLabel: "Tue",
|
||||
dayWednesdayLabel: "Wed",
|
||||
dayThursdayLabel: "Thu",
|
||||
dayFridayLabel: "Fri",
|
||||
daySaturdayLabel: "Sat",
|
||||
startTimeLabel: "Start time",
|
||||
timezoneLabel: "Timezone",
|
||||
ttlLabel: "Time until shutdown (hours)",
|
||||
formTitle: "Workspace schedule",
|
||||
startSection: "Start",
|
||||
startSwitch: "Enable Autostart",
|
||||
stopSection: "Stop",
|
||||
stopSwitch: "Enable Autostop",
|
||||
};
|
||||
|
||||
export interface WorkspaceScheduleFormProps {
|
||||
template: Template;
|
||||
error?: unknown;
|
||||
@@ -93,7 +65,7 @@ export const validationSchema = Yup.object({
|
||||
sunday: Yup.boolean(),
|
||||
monday: Yup.boolean().test(
|
||||
"at-least-one-day",
|
||||
Language.errorNoDayOfWeek,
|
||||
"Must set at least one day of week if autostart is enabled.",
|
||||
function (value) {
|
||||
const parent = this.parent as WorkspaceScheduleFormValues;
|
||||
|
||||
@@ -121,14 +93,18 @@ export const validationSchema = Yup.object({
|
||||
|
||||
startTime: Yup.string()
|
||||
.ensure()
|
||||
.test("required-if-autostart", Language.errorNoTime, function (value) {
|
||||
const parent = this.parent as WorkspaceScheduleFormValues;
|
||||
if (parent.autostartEnabled) {
|
||||
return value !== "";
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.test("is-time-string", Language.errorTime, (value) => {
|
||||
.test(
|
||||
"required-if-autostart",
|
||||
"Start time is required when autostart is enabled.",
|
||||
function (value) {
|
||||
const parent = this.parent as WorkspaceScheduleFormValues;
|
||||
if (parent.autostartEnabled) {
|
||||
return value !== "";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
)
|
||||
.test("is-time-string", "Time must be in HH:mm format.", (value) => {
|
||||
if (value === "") {
|
||||
return true;
|
||||
}
|
||||
@@ -142,7 +118,7 @@ export const validationSchema = Yup.object({
|
||||
}),
|
||||
timezone: Yup.string()
|
||||
.ensure()
|
||||
.test("is-timezone", Language.errorTimezone, function (value) {
|
||||
.test("is-timezone", "Invalid timezone.", function (value) {
|
||||
const parent = this.parent as WorkspaceScheduleFormValues;
|
||||
|
||||
if (!parent.startTime) {
|
||||
@@ -161,14 +137,21 @@ export const validationSchema = Yup.object({
|
||||
}),
|
||||
ttl: Yup.number()
|
||||
.min(0)
|
||||
.max(24 * 30 /* 30 days */, Language.errorTtlMax)
|
||||
.test("positive-if-autostop", Language.errorNoStop, function (value) {
|
||||
const parent = this.parent as WorkspaceScheduleFormValues;
|
||||
if (parent.autostopEnabled) {
|
||||
return Boolean(value);
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
.max(
|
||||
24 * 30 /* 30 days */,
|
||||
"Please enter a limit that is less than or equal to 30 days (720 hours).",
|
||||
)
|
||||
.test(
|
||||
"positive-if-autostop",
|
||||
"Time until shutdown must be greater than zero when autostop is enabled.",
|
||||
function (value) {
|
||||
const parent = this.parent as WorkspaceScheduleFormValues;
|
||||
if (parent.autostopEnabled) {
|
||||
return Boolean(value);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
|
||||
@@ -194,37 +177,37 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
|
||||
{
|
||||
value: form.values.monday,
|
||||
name: "monday",
|
||||
label: Language.dayMondayLabel,
|
||||
label: "Mon",
|
||||
},
|
||||
{
|
||||
value: form.values.tuesday,
|
||||
name: "tuesday",
|
||||
label: Language.dayTuesdayLabel,
|
||||
label: "Tue",
|
||||
},
|
||||
{
|
||||
value: form.values.wednesday,
|
||||
name: "wednesday",
|
||||
label: Language.dayWednesdayLabel,
|
||||
label: "Wed",
|
||||
},
|
||||
{
|
||||
value: form.values.thursday,
|
||||
name: "thursday",
|
||||
label: Language.dayThursdayLabel,
|
||||
label: "Thu",
|
||||
},
|
||||
{
|
||||
value: form.values.friday,
|
||||
name: "friday",
|
||||
label: Language.dayFridayLabel,
|
||||
label: "Fri",
|
||||
},
|
||||
{
|
||||
value: form.values.saturday,
|
||||
name: "saturday",
|
||||
label: Language.daySaturdayLabel,
|
||||
label: "Sat",
|
||||
},
|
||||
{
|
||||
value: form.values.sunday,
|
||||
name: "sunday",
|
||||
label: Language.daySundayLabel,
|
||||
label: "Sun",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -268,7 +251,7 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
|
||||
htmlFor="autostartEnabled"
|
||||
className="font-medium cursor-pointer"
|
||||
>
|
||||
{Language.startSwitch}
|
||||
Enable Autostart
|
||||
</Label>
|
||||
{!template.allow_user_autostart && (
|
||||
<span className="text-xs text-content-secondary mt-0.5">
|
||||
@@ -281,7 +264,7 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="flex flex-col gap-2 flex-1">
|
||||
<Label htmlFor="startTime">{Language.startTimeLabel}</Label>
|
||||
<Label htmlFor="startTime">Start time</Label>
|
||||
<Input
|
||||
id="startTime"
|
||||
name="startTime"
|
||||
@@ -299,7 +282,7 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 flex-1">
|
||||
<Label htmlFor="timezone">{Language.timezoneLabel}</Label>
|
||||
<Label htmlFor="timezone">Timezone</Label>
|
||||
<Select
|
||||
value={form.values.timezone}
|
||||
onValueChange={(value) => {
|
||||
@@ -328,7 +311,7 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
|
||||
|
||||
<fieldset className="border-0 p-0 m-0">
|
||||
<legend className="text-xs text-content-secondary font-medium mb-1">
|
||||
{Language.daysOfWeekLabel}
|
||||
Days of Week
|
||||
</legend>
|
||||
|
||||
<div className="flex flex-row flex-wrap gap-x-4 gap-y-2 pt-1">
|
||||
@@ -358,7 +341,7 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
|
||||
|
||||
{form.errors.monday && (
|
||||
<span className="text-xs text-content-destructive mt-1 block">
|
||||
{Language.errorNoDayOfWeek}
|
||||
Must set at least one day of week if autostart is enabled.
|
||||
</span>
|
||||
)}
|
||||
</fieldset>
|
||||
@@ -395,7 +378,7 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
|
||||
htmlFor="autostopEnabled"
|
||||
className="font-medium cursor-pointer"
|
||||
>
|
||||
{Language.stopSwitch}
|
||||
Enable Autostop
|
||||
</Label>
|
||||
{!template.allow_user_autostop && (
|
||||
<span className="text-xs text-content-secondary mt-0.5">
|
||||
@@ -407,7 +390,7 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="ttl">{Language.ttlLabel}</Label>
|
||||
<Label htmlFor="ttl">Time until shutdown (hours)</Label>
|
||||
<Input
|
||||
id="ttl"
|
||||
name="ttl"
|
||||
@@ -465,7 +448,7 @@ export const ttlShutdownAt = (formTTL: number): string => {
|
||||
return `Your workspace will shut down ${humanDuration(formTTL * 60 * 60 * 1000)} after its next start.`;
|
||||
} catch (e) {
|
||||
if (e instanceof RangeError) {
|
||||
return Language.errorTtlMax;
|
||||
return "Please enter a limit that is less than or equal to 30 days (720 hours).";
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
+5
-16
@@ -14,10 +14,7 @@ import {
|
||||
} from "./formToRequest";
|
||||
import { scheduleToAutostart } from "./schedule";
|
||||
import { ttlMsToAutostop } from "./ttl";
|
||||
import {
|
||||
Language as FormLanguage,
|
||||
type WorkspaceScheduleFormValues,
|
||||
} from "./WorkspaceScheduleForm";
|
||||
import type { WorkspaceScheduleFormValues } from "./WorkspaceScheduleForm";
|
||||
import WorkspaceSchedulePage from "./WorkspaceSchedulePage";
|
||||
|
||||
const validValues: WorkspaceScheduleFormValues = {
|
||||
@@ -266,9 +263,7 @@ describe("WorkspaceSchedulePage", () => {
|
||||
path: "/:username/:workspace/schedule",
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
const autostopToggle = await screen.findByLabelText(
|
||||
FormLanguage.stopSwitch,
|
||||
);
|
||||
const autostopToggle = await screen.findByLabelText("Enable Autostop");
|
||||
// enable autostop
|
||||
await user.click(autostopToggle);
|
||||
// find helper text that describes the mock template's 24 hour default
|
||||
@@ -287,9 +282,7 @@ describe("WorkspaceSchedulePage", () => {
|
||||
path: "/:username/:workspace/schedule",
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
const autostopToggle = await screen.findByLabelText(
|
||||
FormLanguage.stopSwitch,
|
||||
);
|
||||
const autostopToggle = await screen.findByLabelText("Enable Autostop");
|
||||
await user.click(autostopToggle);
|
||||
const submitButton = await screen.findByRole("button", {
|
||||
name: /save/i,
|
||||
@@ -322,9 +315,7 @@ describe("WorkspaceSchedulePage", () => {
|
||||
],
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
const autostopToggle = await screen.findByLabelText(
|
||||
FormLanguage.stopSwitch,
|
||||
);
|
||||
const autostopToggle = await screen.findByLabelText("Enable Autostop");
|
||||
await user.click(autostopToggle);
|
||||
const submitButton = await screen.findByRole("button", {
|
||||
name: /save/i,
|
||||
@@ -349,9 +340,7 @@ describe("WorkspaceSchedulePage", () => {
|
||||
],
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
const autostartToggle = await screen.findByLabelText(
|
||||
FormLanguage.startSwitch,
|
||||
);
|
||||
const autostartToggle = await screen.findByLabelText("Enable Autostart");
|
||||
await user.click(autostartToggle);
|
||||
const submitButton = await screen.findByRole("button", {
|
||||
name: /save/i,
|
||||
|
||||
@@ -10,28 +10,22 @@ import {
|
||||
} from "#/components/HelpPopover/HelpPopover";
|
||||
import { docs } from "#/utils/docs";
|
||||
|
||||
const Language = {
|
||||
workspaceTooltipTitle: "What is a workspace?",
|
||||
workspaceTooltipText:
|
||||
"A workspace is your development environment in the cloud. It includes the infrastructure and tools you need to work on your project.",
|
||||
workspaceTooltipLink1: "Create Workspaces",
|
||||
workspaceTooltipLink2: "Connect with SSH",
|
||||
workspaceTooltipLink3: "Editors and IDEs",
|
||||
};
|
||||
|
||||
export const WorkspaceHelpPopover: FC = () => {
|
||||
return (
|
||||
<HelpPopover>
|
||||
<HelpPopoverIconTrigger />
|
||||
<HelpPopoverContent>
|
||||
<HelpPopoverTitle>{Language.workspaceTooltipTitle}</HelpPopoverTitle>
|
||||
<HelpPopoverText>{Language.workspaceTooltipText}</HelpPopoverText>
|
||||
<HelpPopoverTitle>What is a workspace?</HelpPopoverTitle>
|
||||
<HelpPopoverText>
|
||||
A workspace is your development environment in the cloud. It includes
|
||||
the infrastructure and tools you need to work on your project.
|
||||
</HelpPopoverText>
|
||||
<HelpPopoverLinksGroup>
|
||||
<HelpPopoverLink href={docs("/user-guides")}>
|
||||
{Language.workspaceTooltipLink1}
|
||||
Create Workspaces
|
||||
</HelpPopoverLink>
|
||||
<HelpPopoverLink href={docs("/user-guides/workspace-access")}>
|
||||
{Language.workspaceTooltipLink2}
|
||||
Connect with SSH
|
||||
</HelpPopoverLink>
|
||||
</HelpPopoverLinksGroup>
|
||||
</HelpPopoverContent>
|
||||
|
||||
@@ -33,15 +33,6 @@ import {
|
||||
import { WorkspaceHelpPopover } from "./WorkspaceHelpPopover";
|
||||
import { WorkspacesButton } from "./WorkspacesButton";
|
||||
|
||||
const Language = {
|
||||
pageTitle: "Workspaces",
|
||||
yourWorkspacesButton: "Your workspaces",
|
||||
allWorkspacesButton: "All workspaces",
|
||||
runningWorkspacesButton: "Running workspaces",
|
||||
seeAllTemplates: "See all templates",
|
||||
template: "Template",
|
||||
};
|
||||
|
||||
type TemplateQuery = UseQueryResult<Template[]>;
|
||||
interface WorkspacesPageViewProps {
|
||||
error: unknown;
|
||||
@@ -109,7 +100,7 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
|
||||
>
|
||||
<PageHeaderTitle>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<span>{Language.pageTitle}</span>
|
||||
<span>Workspaces</span>
|
||||
<WorkspaceHelpPopover />
|
||||
</Stack>
|
||||
</PageHeaderTitle>
|
||||
|
||||
@@ -8,21 +8,6 @@ import type {
|
||||
import * as Yup from "yup";
|
||||
import { isApiValidationError, mapApiErrorToFieldErrors } from "#/api/errors";
|
||||
|
||||
const Language = {
|
||||
nameRequired: (name: string): string => {
|
||||
return name ? `Please enter a ${name.toLowerCase()}.` : "Required";
|
||||
},
|
||||
nameInvalidChars: (): string => {
|
||||
return "Special characters (e.g.: !, @, #) are not supported";
|
||||
},
|
||||
nameTooLong: (name: string, len: number): string => {
|
||||
return `${name} cannot be longer than ${len} characters`;
|
||||
},
|
||||
displayNameInvalidChars: (name: string): string => {
|
||||
return `${name} must start and end with non-whitespace character`;
|
||||
},
|
||||
};
|
||||
|
||||
interface GetFormHelperOptions {
|
||||
helperText?: ReactNode;
|
||||
/**
|
||||
@@ -118,16 +103,19 @@ const displayNameRE = /^[^\s](.*[^\s])?$/;
|
||||
// REMARK: see #1756 for name/username semantics
|
||||
export const nameValidator = (name: string): Yup.StringSchema =>
|
||||
Yup.string()
|
||||
.required(Language.nameRequired(name))
|
||||
.matches(usernameRE, Language.nameInvalidChars())
|
||||
.max(maxLenName, Language.nameTooLong(name, maxLenName));
|
||||
.required(`Please enter a ${name.toLowerCase()}.`)
|
||||
.matches(usernameRE, "Special characters (e.g.: !, @, #) are not supported")
|
||||
.max(maxLenName, `${name} cannot be longer than ${maxLenName} characters`);
|
||||
|
||||
export const displayNameValidator = (displayName: string): Yup.StringSchema =>
|
||||
Yup.string()
|
||||
.matches(displayNameRE, Language.displayNameInvalidChars(displayName))
|
||||
.matches(
|
||||
displayNameRE,
|
||||
`${displayName} must start and end with non-whitespace character`,
|
||||
)
|
||||
.max(
|
||||
displayNameMaxLength,
|
||||
Language.nameTooLong(displayName, displayNameMaxLength),
|
||||
`${displayName} cannot be longer than ${displayNameMaxLength} characters`,
|
||||
)
|
||||
.optional();
|
||||
|
||||
|
||||
@@ -54,15 +54,6 @@ export const extractTimezone = (
|
||||
return defaultTZ;
|
||||
};
|
||||
|
||||
/** Language used in the schedule components */
|
||||
const Language = {
|
||||
manual: "Manual",
|
||||
workspaceShuttingDownLabel: "Workspace is shutting down",
|
||||
afterStart: "after start",
|
||||
autostartLabel: "Starts at",
|
||||
autostopLabel: "Stops at",
|
||||
};
|
||||
|
||||
export const autostartDisplay = (schedule: string | undefined): string => {
|
||||
if (schedule) {
|
||||
return (
|
||||
@@ -74,7 +65,7 @@ export const autostartDisplay = (schedule: string | undefined): string => {
|
||||
.replace("At", "")
|
||||
);
|
||||
}
|
||||
return Language.manual;
|
||||
return "Manual";
|
||||
};
|
||||
|
||||
const isShuttingDown = (workspace: Workspace, deadline?: Dayjs): boolean => {
|
||||
@@ -135,7 +126,7 @@ export const autostopDisplay = (
|
||||
|
||||
if (isShuttingDown(workspace, deadline)) {
|
||||
return {
|
||||
message: Language.workspaceShuttingDownLabel,
|
||||
message: "Workspace is shutting down",
|
||||
};
|
||||
}
|
||||
let title = (
|
||||
@@ -173,14 +164,14 @@ export const autostopDisplay = (
|
||||
// If the workspace is not on, and the ttl is 0 or undefined, then the
|
||||
// workspace is set to manually shutdown.
|
||||
return {
|
||||
message: Language.manual,
|
||||
message: "Manual",
|
||||
};
|
||||
}
|
||||
// The workspace has a ttl set, but is either in an unknown state or is
|
||||
// not running. Therefore, we derive from workspace.ttl.
|
||||
const duration = dayjs.duration(ttl, "milliseconds");
|
||||
return {
|
||||
message: `Stop ${duration.humanize()} ${Language.afterStart}`,
|
||||
message: `Stop ${duration.humanize()} after start`,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user