chore: remove i18next (#9608)

This commit is contained in:
Kayla Washburn
2023-09-11 11:17:58 -06:00
committed by GitHub
parent 1a1c230534
commit 13acf5976c
110 changed files with 548 additions and 1495 deletions
-1
View File
@@ -4,7 +4,6 @@ import { withRouter } from "storybook-addon-react-router-v6";
import { HelmetProvider } from "react-helmet-async";
import { dark } from "../src/theme";
import "../src/theme/globalFonts";
import "../src/i18n";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
export const decorators = [
-2
View File
@@ -69,7 +69,6 @@
"eventsourcemock": "2.0.0",
"formik": "2.4.1",
"front-matter": "4.0.2",
"i18next": "22.5.0",
"jest-environment-jsdom": "29.5.0",
"lodash": "4.17.21",
"monaco-editor": "0.41.0",
@@ -82,7 +81,6 @@
"react-dom": "18.2.0",
"react-headless-tabs": "6.0.3",
"react-helmet-async": "1.3.0",
"react-i18next": "12.2.2",
"react-markdown": "8.0.3",
"react-router-dom": "6.15.0",
"react-syntax-highlighter": "15.5.0",
-43
View File
@@ -123,9 +123,6 @@ dependencies:
front-matter:
specifier: 4.0.2
version: 4.0.2
i18next:
specifier: 22.5.0
version: 22.5.0
jest-environment-jsdom:
specifier: 29.5.0
version: 29.5.0(canvas@2.11.0)
@@ -162,9 +159,6 @@ dependencies:
react-helmet-async:
specifier: 1.3.0
version: 1.3.0(react-dom@18.2.0)(react@18.2.0)
react-i18next:
specifier: 12.2.2
version: 12.2.2(i18next@22.5.0)(react-dom@18.2.0)(react@18.2.0)
react-markdown:
specifier: 8.0.3
version: 8.0.3(@types/react@18.2.6)(react@18.2.0)
@@ -8944,12 +8938,6 @@ packages:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
dev: true
/html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
dependencies:
void-elements: 3.1.0
dev: false
/html-tags@3.3.1:
resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
engines: {node: '>=8'}
@@ -9005,12 +8993,6 @@ packages:
resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==}
dev: false
/i18next@22.5.0:
resolution: {integrity: sha512-sqWuJFj+wJAKQP2qBQ+b7STzxZNUmnSxrehBCCj9vDOW9RDYPfqCaK1Hbh2frNYQuPziz6O2CGoJPwtzY3vAYA==}
dependencies:
'@babel/runtime': 7.22.6
dev: false
/iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
@@ -11907,26 +11889,6 @@ packages:
shallowequal: 1.1.0
dev: false
/react-i18next@12.2.2(i18next@22.5.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-KBB6buBmVKXUWNxXHdnthp+38gPyBT46hJCAIQ8rX19NFL/m2ahte2KARfIDf2tMnSAL7wwck6eDOd/9zn6aFg==}
peerDependencies:
i18next: '>= 19.0.0'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@babel/runtime': 7.22.6
html-parse-stringify: 3.0.1
i18next: 22.5.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-inspector@6.0.2(react@18.2.0):
resolution: {integrity: sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==}
peerDependencies:
@@ -13898,11 +13860,6 @@ packages:
optionalDependencies:
fsevents: 2.3.3
/void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
dev: false
/vscode-jsonrpc@6.0.0:
resolution: {integrity: sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==}
engines: {node: '>=8.0.0 || >=10.0.0'}
-10
View File
@@ -1,10 +0,0 @@
import "i18next";
// https://github.com/i18next/react-i18next/issues/1543#issuecomment-1528679591
declare module "i18next" {
interface TypeOptions {
returnNull: false;
allowObjectInHTMLChildren: false;
}
export function t<T>(s: string): T;
}
@@ -1,6 +1,5 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import i18next from "i18next";
import { render } from "testHelpers/renderHelpers";
import { DeleteDialog } from "./DeleteDialog";
@@ -20,7 +19,6 @@ describe("DeleteDialog", () => {
});
it("disables confirm button when the text field is filled incorrectly", async () => {
const { t } = i18next;
render(
<DeleteDialog
isOpen
@@ -30,18 +28,13 @@ describe("DeleteDialog", () => {
name="MyTemplate"
/>,
);
const labelText = t("deleteDialog.confirmLabel", {
ns: "common",
entity: "template",
});
const textField = screen.getByLabelText(labelText);
const textField = screen.getByTestId("delete-dialog-name-confirmation");
await userEvent.type(textField, "MyTemplateWrong");
const confirmButton = screen.getByRole("button", { name: "Delete" });
expect(confirmButton).toBeDisabled();
});
it("enables confirm button when the text field is filled correctly", async () => {
const { t } = i18next;
render(
<DeleteDialog
isOpen
@@ -51,11 +44,7 @@ describe("DeleteDialog", () => {
name="MyTemplate"
/>,
);
const labelText = t("deleteDialog.confirmLabel", {
ns: "common",
entity: "template",
});
const textField = screen.getByLabelText(labelText);
const textField = screen.getByTestId("delete-dialog-name-confirmation");
await userEvent.type(textField, "MyTemplate");
const confirmButton = screen.getByRole("button", { name: "Delete" });
expect(confirmButton).not.toBeDisabled();
@@ -2,7 +2,6 @@ import makeStyles from "@mui/styles/makeStyles";
import TextField from "@mui/material/TextField";
import { Maybe } from "components/Conditionals/Maybe";
import { ChangeEvent, useState, PropsWithChildren, FC } from "react";
import { useTranslation } from "react-i18next";
import { ConfirmDialog } from "../ConfirmDialog/ConfirmDialog";
export interface DeleteDialogProps {
@@ -25,7 +24,6 @@ export const DeleteDialog: FC<PropsWithChildren<DeleteDialogProps>> = ({
confirmLoading,
}) => {
const styles = useStyles();
const { t } = useTranslation("common");
const [nameValue, setNameValue] = useState("");
const confirmed = name === nameValue;
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
@@ -35,11 +33,12 @@ export const DeleteDialog: FC<PropsWithChildren<DeleteDialogProps>> = ({
const content = (
<>
<p>{t("deleteDialog.intro", { entity })}</p>
<p>Deleting this {entity} is irreversible!</p>
<Maybe condition={info !== undefined}>
<p className={styles.warning}>{info}</p>
</Maybe>
<p>{t("deleteDialog.confirm", { entity, name })}</p>
<p>Are you sure you want to proceed?</p>
<p>Type {name} below to confirm.</p>
<form
onSubmit={(e) => {
@@ -59,9 +58,12 @@ export const DeleteDialog: FC<PropsWithChildren<DeleteDialogProps>> = ({
placeholder={name}
value={nameValue}
onChange={handleChange}
label={t("deleteDialog.confirmLabel", { entity })}
label={`Name of the ${entity} to delete`}
error={hasError}
helperText={hasError && t("deleteDialog.incorrectName", { entity })}
helperText={
hasError && `${nameValue} does not match the name of this ${entity}`
}
inputProps={{ ["data-testid"]: "delete-dialog-name-confirmation" }}
/>
</form>
</>
@@ -72,7 +74,7 @@ export const DeleteDialog: FC<PropsWithChildren<DeleteDialogProps>> = ({
type="delete"
hideCancel={false}
open={isOpen}
title={t("deleteDialog.title", { entity })}
title={`Delete ${entity}`}
onConfirm={onConfirm}
onClose={onCancel}
description={content}
+2 -4
View File
@@ -6,7 +6,6 @@ import {
} from "components/DropdownArrows/DropdownArrows";
import { PropsWithChildren, FC } from "react";
import Collapse from "@mui/material/Collapse";
import { useTranslation } from "react-i18next";
import { combineClasses } from "utils/combineClasses";
export interface ExpanderProps {
@@ -20,7 +19,6 @@ export const Expander: FC<PropsWithChildren<ExpanderProps>> = ({
children,
}) => {
const styles = useStyles();
const { t } = useTranslation("common");
const toggleExpanded = () => setExpanded(!expanded);
@@ -29,7 +27,7 @@ export const Expander: FC<PropsWithChildren<ExpanderProps>> = ({
{!expanded && (
<Link onClick={toggleExpanded} className={styles.expandLink}>
<span className={styles.text}>
{t("ctas.expand")}
Click here to learn more
<OpenDropdown margin={false} />
</span>
</Link>
@@ -43,7 +41,7 @@ export const Expander: FC<PropsWithChildren<ExpanderProps>> = ({
className={combineClasses([styles.expandLink, styles.collapseLink])}
>
<span className={styles.text}>
{t("ctas.collapse")}
Click here to hide
<CloseDropdown margin={false} />
</span>
</Link>
+2 -4
View File
@@ -7,7 +7,6 @@ import { useRef, FC, useState } from "react";
import Picker from "@emoji-mart/react";
import { makeStyles } from "@mui/styles";
import { colors } from "theme/colors";
import { useTranslation } from "react-i18next";
import data from "@emoji-mart/data/sets/14/twitter.json";
import { Stack } from "components/Stack/Stack";
@@ -26,7 +25,6 @@ const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
const styles = useStyles();
const emojiButtonRef = useRef<HTMLButtonElement>(null);
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false);
const { t } = useTranslation("templateSettingsPage");
const hasIcon = textFieldProps.value && textFieldProps.value !== "";
return (
@@ -34,7 +32,7 @@ const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
<TextField
{...textFieldProps}
fullWidth
label={t("iconLabel")}
label="Icon"
InputProps={{
endAdornment: hasIcon ? (
<InputAdornment position="end" className={styles.adornment}>
@@ -59,7 +57,7 @@ const IconField: FC<IconFieldProps> = ({ onPickEmoji, ...textFieldProps }) => {
setIsEmojiPickerOpen((v) => !v);
}}
>
{t("selectEmoji")}
Select emoji
</Button>
<Popover
@@ -11,7 +11,6 @@ import {
} from "components/HelpTooltip/HelpTooltip";
import { WorkspaceAgent } from "api/typesGenerated";
import { Stack } from "components/Stack/Stack";
import { useTranslation } from "react-i18next";
type AgentOutdatedTooltipProps = ComponentProps<typeof HelpPopover> & {
agent: WorkspaceAgent;
@@ -30,7 +29,6 @@ export const AgentOutdatedTooltip: FC<AgentOutdatedTooltipProps> = ({
anchorEl,
}) => {
const styles = useStyles();
const { t } = useTranslation("workspacePage");
return (
<HelpPopover
@@ -43,25 +41,21 @@ export const AgentOutdatedTooltip: FC<AgentOutdatedTooltipProps> = ({
<HelpTooltipContext.Provider value={{ open, onClose }}>
<Stack spacing={1}>
<div>
<HelpTooltipTitle>
{t("agentOutdatedTooltip.title")}
</HelpTooltipTitle>
<HelpTooltipTitle>Agent Outdated</HelpTooltipTitle>
<HelpTooltipText>
{t("agentOutdatedTooltip.description")}
This agent is an older version than the Coder server. This can
happen after you update Coder with running workspaces. To fix
this, you can stop and start the workspace.
</HelpTooltipText>
</div>
<Stack spacing={0.5}>
<span className={styles.versionLabel}>
{t("agentOutdatedTooltip.agentVersionLabel")}
</span>
<span className={styles.versionLabel}>Agent version</span>
<span>{agent.version}</span>
</Stack>
<Stack spacing={0.5}>
<span className={styles.versionLabel}>
{t("agentOutdatedTooltip.serverVersionLabel")}
</span>
<span className={styles.versionLabel}>Server version</span>
<span>{serverVersion}</span>
</Stack>
@@ -71,7 +65,7 @@ export const AgentOutdatedTooltip: FC<AgentOutdatedTooltipProps> = ({
onClick={onUpdate}
ariaLabel="Update workspace"
>
{t("agentOutdatedTooltip.updateWorkspaceLabel")}
Update workspace
</HelpTooltipAction>
</HelpTooltipLinksGroup>
</Stack>
@@ -2,7 +2,6 @@ import { makeStyles } from "@mui/styles";
import { AppPreviewLink } from "components/Resources/AppLink/AppPreviewLink";
import { Maybe } from "components/Conditionals/Maybe";
import { FC } from "react";
import { useTranslation } from "react-i18next";
import { combineClasses } from "utils/combineClasses";
import { WorkspaceAgent } from "../../api/typesGenerated";
import { Stack } from "../Stack/Stack";
@@ -21,7 +20,6 @@ export const AgentRowPreview: FC<AgentRowPreviewProps> = ({
alignValues,
}) => {
const styles = useStyles({ alignValues });
const { t } = useTranslation("agent");
return (
<Stack
@@ -51,7 +49,7 @@ export const AgentRowPreview: FC<AgentRowPreviewProps> = ({
styles.agentDataName,
])}
>
<span>{t("labels.agent").toString()}:</span>
<span>Agent:</span>
<span className={styles.agentDataValue}>{agent.name}</span>
</Stack>
@@ -65,7 +63,7 @@ export const AgentRowPreview: FC<AgentRowPreviewProps> = ({
styles.agentDataOS,
])}
>
<span>{t("labels.os").toString()}:</span>
<span>OS:</span>
<span
className={combineClasses([
styles.agentDataValue,
@@ -82,7 +80,7 @@ export const AgentRowPreview: FC<AgentRowPreviewProps> = ({
spacing={1}
className={styles.agentDataItem}
>
<span>{t("labels.apps").toString()}:</span>
<span>Apps:</span>
<Stack
direction="row"
alignItems="center"
@@ -93,9 +91,7 @@ export const AgentRowPreview: FC<AgentRowPreviewProps> = ({
<AppPreviewLink key={app.slug} app={app} />
))}
<Maybe condition={agent.apps.length === 0}>
<span className={styles.agentDataValue}>
{t("labels.noApps")}
</span>
<span className={styles.agentDataValue}>None</span>
</Maybe>
</Stack>
</Stack>
+21 -28
View File
@@ -3,7 +3,6 @@ import { makeStyles } from "@mui/styles";
import { combineClasses } from "utils/combineClasses";
import { WorkspaceAgent } from "api/typesGenerated";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { useTranslation } from "react-i18next";
import WarningRounded from "@mui/icons-material/WarningRounded";
import {
HelpPopover,
@@ -21,13 +20,12 @@ import Link from "@mui/material/Link";
const ReadyLifecycle = () => {
const styles = useStyles();
const { t } = useTranslation("workspacePage");
return (
<div
role="status"
data-testid="agent-status-ready"
aria-label={t("agentStatus.connected.ready") || "Ready"}
aria-label="Ready"
className={combineClasses([styles.status, styles.connected])}
/>
);
@@ -50,7 +48,6 @@ const StartingLifecycle: React.FC = () => {
const StartTimeoutLifecycle: React.FC<{
agent: WorkspaceAgent;
}> = ({ agent }) => {
const { t } = useTranslation("agent");
const styles = useStyles();
const anchorRef = useRef<SVGSVGElement>(null);
const [isOpen, setIsOpen] = useState(false);
@@ -63,7 +60,7 @@ const StartTimeoutLifecycle: React.FC<{
onMouseEnter={() => setIsOpen(true)}
onMouseLeave={() => setIsOpen(false)}
role="status"
aria-label={t("status.startTimeout")}
aria-label="Start timeout"
className={styles.timeoutWarning}
/>
<HelpPopover
@@ -73,15 +70,15 @@ const StartTimeoutLifecycle: React.FC<{
onOpen={() => setIsOpen(true)}
onClose={() => setIsOpen(false)}
>
<HelpTooltipTitle>{t("startTimeoutTooltip.title")}</HelpTooltipTitle>
<HelpTooltipTitle>Agent is taking too long to start</HelpTooltipTitle>
<HelpTooltipText>
{t("startTimeoutTooltip.message")}{" "}
We noticed this agent is taking longer than expected to start.{" "}
<Link
target="_blank"
rel="noreferrer"
href={agent.troubleshooting_url}
>
{t("startTimeoutTooltip.link")}
Troubleshoot
</Link>
.
</HelpTooltipText>
@@ -93,7 +90,6 @@ const StartTimeoutLifecycle: React.FC<{
const StartErrorLifecycle: React.FC<{
agent: WorkspaceAgent;
}> = ({ agent }) => {
const { t } = useTranslation("agent");
const styles = useStyles();
const anchorRef = useRef<SVGSVGElement>(null);
const [isOpen, setIsOpen] = useState(false);
@@ -106,7 +102,7 @@ const StartErrorLifecycle: React.FC<{
onMouseEnter={() => setIsOpen(true)}
onMouseLeave={() => setIsOpen(false)}
role="status"
aria-label={t("status.error")}
aria-label="Start error"
className={styles.errorWarning}
/>
<HelpPopover
@@ -116,15 +112,15 @@ const StartErrorLifecycle: React.FC<{
onOpen={() => setIsOpen(true)}
onClose={() => setIsOpen(false)}
>
<HelpTooltipTitle>{t("startErrorTooltip.title")}</HelpTooltipTitle>
<HelpTooltipTitle>Error starting the agent</HelpTooltipTitle>
<HelpTooltipText>
{t("startErrorTooltip.message")}{" "}
Something went wrong during the agent startup.{" "}
<Link
target="_blank"
rel="noreferrer"
href={agent.troubleshooting_url}
>
{t("startErrorTooltip.link")}
Troubleshoot
</Link>
.
</HelpTooltipText>
@@ -150,7 +146,6 @@ const ShuttingDownLifecycle: React.FC = () => {
const ShutdownTimeoutLifecycle: React.FC<{
agent: WorkspaceAgent;
}> = ({ agent }) => {
const { t } = useTranslation("agent");
const styles = useStyles();
const anchorRef = useRef<SVGSVGElement>(null);
const [isOpen, setIsOpen] = useState(false);
@@ -163,7 +158,7 @@ const ShutdownTimeoutLifecycle: React.FC<{
onMouseEnter={() => setIsOpen(true)}
onMouseLeave={() => setIsOpen(false)}
role="status"
aria-label={t("status.shutdownTimeout")}
aria-label="Stop timeout"
className={styles.timeoutWarning}
/>
<HelpPopover
@@ -173,15 +168,15 @@ const ShutdownTimeoutLifecycle: React.FC<{
onOpen={() => setIsOpen(true)}
onClose={() => setIsOpen(false)}
>
<HelpTooltipTitle>{t("shutdownTimeoutTooltip.title")}</HelpTooltipTitle>
<HelpTooltipTitle>Agent is taking too long to stop</HelpTooltipTitle>
<HelpTooltipText>
{t("shutdownTimeoutTooltip.message")}{" "}
We noticed this agent is taking longer than expected to stop.{" "}
<Link
target="_blank"
rel="noreferrer"
href={agent.troubleshooting_url}
>
{t("shutdownTimeoutTooltip.link")}
Troubleshoot
</Link>
.
</HelpTooltipText>
@@ -193,7 +188,6 @@ const ShutdownTimeoutLifecycle: React.FC<{
const ShutdownErrorLifecycle: React.FC<{
agent: WorkspaceAgent;
}> = ({ agent }) => {
const { t } = useTranslation("agent");
const styles = useStyles();
const anchorRef = useRef<SVGSVGElement>(null);
const [isOpen, setIsOpen] = useState(false);
@@ -206,7 +200,7 @@ const ShutdownErrorLifecycle: React.FC<{
onMouseEnter={() => setIsOpen(true)}
onMouseLeave={() => setIsOpen(false)}
role="status"
aria-label={t("status.error")}
aria-label="Stop error"
className={styles.errorWarning}
/>
<HelpPopover
@@ -216,15 +210,15 @@ const ShutdownErrorLifecycle: React.FC<{
onOpen={() => setIsOpen(true)}
onClose={() => setIsOpen(false)}
>
<HelpTooltipTitle>{t("shutdownErrorTooltip.title")}</HelpTooltipTitle>
<HelpTooltipTitle>Error stopping the agent</HelpTooltipTitle>
<HelpTooltipText>
{t("shutdownErrorTooltip.message")}{" "}
Something went wrong while trying to stop the agent.{" "}
<Link
target="_blank"
rel="noreferrer"
href={agent.troubleshooting_url}
>
{t("shutdownErrorTooltip.link")}
Troubleshoot
</Link>
.
</HelpTooltipText>
@@ -316,7 +310,6 @@ const ConnectingStatus: React.FC = () => {
const TimeoutStatus: React.FC<{
agent: WorkspaceAgent;
}> = ({ agent }) => {
const { t } = useTranslation("agent");
const styles = useStyles();
const anchorRef = useRef<SVGSVGElement>(null);
const [isOpen, setIsOpen] = useState(false);
@@ -329,7 +322,7 @@ const TimeoutStatus: React.FC<{
onMouseEnter={() => setIsOpen(true)}
onMouseLeave={() => setIsOpen(false)}
role="status"
aria-label={t("status.timeout")}
aria-label="Timeout"
className={styles.timeoutWarning}
/>
<HelpPopover
@@ -339,15 +332,15 @@ const TimeoutStatus: React.FC<{
onOpen={() => setIsOpen(true)}
onClose={() => setIsOpen(false)}
>
<HelpTooltipTitle>{t("timeoutTooltip.title")}</HelpTooltipTitle>
<HelpTooltipTitle>Agent is taking too long to connect</HelpTooltipTitle>
<HelpTooltipText>
{t("timeoutTooltip.message")}{" "}
We noticed this agent is taking longer than expected to connect.{" "}
<Link
target="_blank"
rel="noreferrer"
href={agent.troubleshooting_url}
>
{t("timeoutTooltip.link")}
Troubleshoot
</Link>
.
</HelpTooltipText>
@@ -3,31 +3,29 @@ import GroupOutlinedIcon from "@mui/icons-material/GroupOutlined";
import LaunchOutlinedIcon from "@mui/icons-material/LaunchOutlined";
import * as TypesGen from "../../../api/typesGenerated";
import Tooltip from "@mui/material/Tooltip";
import { useTranslation } from "react-i18next";
export interface ShareIconProps {
app: TypesGen.WorkspaceApp;
}
export const ShareIcon = ({ app }: ShareIconProps) => {
const { t } = useTranslation("agent");
if (app.external) {
return (
<Tooltip title={t("shareTooltip.external")}>
<Tooltip title="Open external URL">
<LaunchOutlinedIcon />
</Tooltip>
);
}
if (app.sharing_level === "authenticated") {
return (
<Tooltip title={t("shareTooltip.authenticated")}>
<Tooltip title="Shared with all authenticated users">
<GroupOutlinedIcon />
</Tooltip>
);
}
if (app.sharing_level === "public") {
return (
<Tooltip title={t("shareTooltip.public")}>
<Tooltip title="Shared publicly">
<PublicOutlinedIcon />
</Tooltip>
);
@@ -26,7 +26,7 @@ export const MutableTemplateParametersSection: FC<
<FormSection
{...formSectionProps}
title="Parameters"
description="These parameters are provided by your template's Terraform configuration and can be changed after creating the workspace."
description="Settings used by your template"
>
<FormFields>
{templateParameters.map(
@@ -60,9 +60,8 @@ export const ImmutableTemplateParametersSection: FC<
title="Immutable parameters"
description={
<>
These parameters are also provided by your Terraform configuration
but they{" "}
<strong>cannot be changed after creating the workspace.</strong>
These settings <strong>cannot be changed</strong> after creating
the workspace.
</>
}
>
@@ -9,7 +9,6 @@ import { AvatarData } from "components/AvatarData/AvatarData";
import debounce from "just-debounce-it";
import { ChangeEvent, ComponentProps, FC, useEffect, useState } from "react";
import { searchUserMachine } from "xServices/users/searchUserXService";
import { useTranslation } from "react-i18next";
import Box from "@mui/material/Box";
export type UserAutocompleteProps = {
@@ -28,7 +27,6 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
size = "small",
}) => {
const styles = useStyles();
const { t } = useTranslation("common");
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);
const [searchState, sendSearch] = useMachine(searchUserMachine);
const { searchResults } = searchState.context;
@@ -51,7 +49,7 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
return (
<Autocomplete
noOptionsText={t("forms.typeToSearch")}
noOptionsText="Start typing to search..."
className={className}
options={searchResults ?? []}
loading={searchState.matches("searching")}
@@ -86,8 +84,6 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
)}
renderInput={(params) => (
<>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment -- Need it */}
{/* @ts-ignore -- Issue from lib https://github.com/i18next/react-i18next/issues/1543 */}
<TextField
{...params}
fullWidth
@@ -95,8 +95,6 @@ export const UserOrGroupAutocomplete: React.FC<
className={styles.autocomplete}
renderInput={(params) => (
<>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment -- Need it */}
{/* @ts-ignore -- Issue from lib https://github.com/i18next/react-i18next/issues/1543 */}
<TextField
{...params}
margin="none"
-47
View File
@@ -1,47 +0,0 @@
{
"shareTooltip": {
"private": "Private, only accessible by you",
"authenticated": "Shared with all authenticated users",
"public": "Shared publicly",
"external": "Open external URL"
},
"labels": {
"agent": "Agent",
"os": "OS",
"apps": "Apps",
"noApps": "None"
},
"status": {
"timeout": "Timeout",
"startTimeout": "Start Timeout",
"startError": "Start Error",
"shutdownTimeout": "Stop Timeout",
"shutdownError": "Stop Error"
},
"timeoutTooltip": {
"title": "Agent is taking too long to connect",
"message": "We noticed this agent is taking longer than expected to connect.",
"link": "Troubleshoot"
},
"startTimeoutTooltip": {
"title": "Agent is taking too long to start",
"message": "We noticed this agent is taking longer than expected to start.",
"link": "Troubleshoot"
},
"startErrorTooltip": {
"title": "Error starting the agent",
"message": "Something went wrong during the agent startup.",
"link": "Troubleshoot"
},
"shutdownTimeoutTooltip": {
"title": "Agent is taking too long to stop",
"message": "We noticed this agent is taking longer than expected to stop.",
"link": "Troubleshoot"
},
"shutdownErrorTooltip": {
"title": "Error stopping the agent",
"message": "Something went wrong while trying to stop the agent.",
"link": "Troubleshoot"
},
"unableToConnect": "Unable to connect"
}
-5
View File
@@ -1,5 +0,0 @@
{
"updateLabel": "Update",
"messageHelperText": "Markdown bold, italics, and links are supported.",
"showPreviewLabel": "Show Preview"
}
-31
View File
@@ -1,31 +0,0 @@
{
"actions": {
"create": "created a new",
"write": "updated",
"delete": "deleted"
},
"table": {
"emptyPage": "No audit logs available on this page",
"noLogs": "No audit logs available",
"logRow": {
"description": {
"linkedWorkspaceBuild": "{{user}} <1>{{action}}</1> workspace <strong>{{workspaceName}}</strong>",
"unlinkedWorkspaceBuild": "{{user}} {{action}} workspace <strong>{{workspaceName}}</strong>",
"linkedAuditDescription": "{{truncatedDescription}} <1><strong>{{target}}</strong></1> {{onBehalfOf}}",
"unlinkedAuditDescription": "{{truncatedDescription}} <strong>{{target}}</strong> {{onBehalfOf}}"
},
"deletedLabel": " (deleted)",
"ip": "IP: ",
"os": "OS: ",
"browser": "Browser: "
}
},
"paywall": {
"title": "Audit logs",
"description": "Audit Logs allows Auditors to monitor user operations in their deployment. To use this feature, you have to upgrade your account.",
"actions": {
"upgrade": "See how to upgrade",
"readDocs": "Read the docs"
}
}
}
-8
View File
@@ -1,8 +0,0 @@
{
"stats": {
"workspace": "Workspace",
"duration": "Duration",
"startedAt": "Started at",
"action": "Action"
}
}
-48
View File
@@ -1,48 +0,0 @@
{
"coder": "Coder",
"workspaceStatus": {
"loading": "Loading",
"running": "Running",
"starting": "Starting",
"stopping": "Stopping",
"stopped": "Stopped",
"deleting": "Deleting",
"deleted": "Deleted",
"canceling": "Canceling",
"canceled": "Canceled",
"failed": "Failed",
"pending": "Pending"
},
"deleteDialog": {
"title": "Delete {{entity}}",
"intro": "Deleting this {{entity}} is irreversible!",
"confirm": "Are you sure you want to proceed? Type {{name}} below to confirm.",
"confirmLabel": "Name of {{entity}} to delete",
"incorrectName": "Incorrect {{entity}} name."
},
"schedule": {
"autostartLabel": "Starts at",
"autostopLabel": "Stops at"
},
"ctas": {
"dismissCta": "Dismiss",
"expand": "Click here to learn more",
"collapse": "Click here to hide",
"retry": "Retry"
},
"warningsAndErrors": {
"somethingWentWrong": "Something went wrong."
},
"emojiPicker": {
"select": "Select emoji"
},
"updateCheck": {
"message": "Coder {{version}} is now available. View the <4>release notes</4> and <7>upgrade instructions</7> for more information.",
"error": "Coder update check failed."
},
"licenseFieldTextHelper": "You need an enterprise license to use it.",
"learnMore": "Learn more",
"forms": {
"typeToSearch": "Start typing to search..."
}
}
-63
View File
@@ -1,63 +0,0 @@
{
"title": "Create Template",
"form": {
"generalInfo": {
"title": "General",
"description": "The name is used to identify the template in URLs and the API."
},
"displayInfo": {
"title": "Display",
"description": "A friendly name, description, and icon to help developers identify your template."
},
"schedule": {
"title": "Schedule",
"description": "Define when workspaces created from this template automatically stop."
},
"operations": {
"title": "Operations",
"description": "Regulate actions allowed on workspaces created from this template."
},
"parameters": {
"title": "Template params",
"description": "These params are provided by your template's Terraform configuration."
},
"fields": {
"name": "Name",
"displayName": "Display name",
"description": "Description",
"icon": "Icon",
"autostop": "Default autostop (hours)",
"maxTTL": "Max lifetime (hours)",
"allowUsersToCancel": "Allow users to cancel in-progress workspace jobs",
"autostopRequirementDays": "Days with required stop",
"autostopRequirementDays_off": "Off",
"autostopRequirementDays_daily": "Daily",
"autostopRequirementDays_saturday": "Saturday",
"autostopRequirementDays_sunday": "Sunday",
"autostopRequirementWeeks": "Weeks between required stops"
},
"helperText": {
"defaultTTLHelperText_zero": "Workspaces will run until stopped manually.",
"defaultTTLHelperText_one": "Workspaces will default to stopping after {{count}} hour. If Coder detects workspace connection activity, the autostop timer is bumped by this value.",
"defaultTTLHelperText_other": "Workspaces will default to stopping after {{count}} hours. If Coder detects workspace connection activity, the autostop timer is bumped by this value.",
"maxTTLHelperText_zero": "Workspaces may run indefinitely.",
"maxTTLHelperText_one": "Workspaces must stop within 1 hour of starting, regardless of any active connections.",
"maxTTLHelperText_other": "Workspaces must stop within {{count}} hours of starting, regardless of any active connections.",
"allowUsersToCancel": "If checked, users may be able to corrupt their workspace."
},
"upload": {
"removeTitle": "Remove file",
"title": "Upload template"
},
"tooltip": {
"allowUsersToCancel": "Depending on your template, canceling builds may leave workspaces in an unhealthy state. This option isn't recommended for most use cases."
},
"error": {
"descriptionMax": "Please enter a description that is less than or equal to 128 characters.",
"defaultTTLMax": "Please enter a limit that is less than or equal to 720 hours (30 days).",
"defaultTTLMin": "Default time until autostop must not be less than 0.",
"maxTTLMax": "Please enter a limit that is less than or equal to 720 hours (30 days).",
"maxTTLMin": "Maximum time until autostop must not be less than 0."
}
}
}
-11
View File
@@ -1,11 +0,0 @@
{
"templateLabel": "Template",
"versionLabel": "Version ID",
"nameLabel": "Workspace Name",
"ownerLabel": "Owner",
"createWorkspace": "Create Workspace",
"validationNumberLesserThan": "Value must be greater than {{min}}.",
"validationNumberGreaterThan": "Value must be lesser than {{max}}.",
"validationNumberNotInRange": "Value must be between {{min}} and {{max}}.",
"validationPatternNotMatched": "{{error}} (value does not match the pattern {{pattern}})."
}
-47
View File
@@ -1,47 +0,0 @@
import auditLog from "./auditLog.json";
import common from "./common.json";
import createWorkspacePage from "./createWorkspacePage.json";
import templatePage from "./templatePage.json";
import templatesPage from "./templatesPage.json";
import workspacePage from "./workspacePage.json";
import agent from "./agent.json";
import buildPage from "./buildPage.json";
import workspacesPage from "./workspacesPage.json";
import usersPage from "./usersPage.json";
import templateSettingsPage from "./templateSettingsPage.json";
import templateVariablesPage from "./templateVariablesPage.json";
import templateVersionPage from "./templateVersionPage.json";
import loginPage from "./loginPage.json";
import workspaceSchedulePage from "./workspaceSchedulePage.json";
import appearanceSettings from "./appearanceSettings.json";
import starterTemplatesPage from "./starterTemplatesPage.json";
import starterTemplatePage from "./starterTemplatePage.json";
import createTemplatePage from "./createTemplatePage.json";
import userSettingsPage from "./userSettingsPage.json";
import tokensPage from "./tokensPage.json";
import workspaceSettingsPage from "./workspaceSettingsPage.json";
export const en = {
common,
workspacePage,
auditLog,
templatePage,
templatesPage,
createWorkspacePage,
agent,
buildPage,
workspacesPage,
usersPage,
templateSettingsPage,
templateVariablesPage,
templateVersionPage,
loginPage,
workspaceSchedulePage,
appearanceSettings,
starterTemplatesPage,
starterTemplatePage,
createTemplatePage,
userSettingsPage,
tokensPage,
workspaceSettingsPage,
};
-4
View File
@@ -1,4 +0,0 @@
{
"signInTo": "Sign in to",
"showPassword": "Email and password"
}
@@ -1,6 +0,0 @@
{
"actions": {
"viewSourceCode": "View source code",
"useTemplate": "Use Template"
}
}
@@ -1,11 +0,0 @@
{
"title": "Starter Templates",
"subtitle": "Import a built-in template to start developing in the cloud",
"filterCaption": "Filter",
"tags": {
"all": "All templates",
"digitalocean": "Digital Ocean",
"aws": "AWS",
"google": "Google Cloud"
}
}
-41
View File
@@ -1,41 +0,0 @@
{
"generalInfo": {
"title": "General",
"description": "The name is used to identify the template in URLs and the API. It must be unique."
},
"displayInfo": {
"title": "Display",
"description": "A friendly name, description, and icon to help developers identify your template."
},
"schedule": {
"title": "Schedule",
"description": "Define when workspaces created from this template automatically stop."
},
"operations": {
"title": "Operations",
"description": "Regulate actions allowed on workspaces created from this template."
},
"parameters": {
"title": "Template variables",
"description": "These variables are provided by your template's Terraform configuration."
},
"fields": {
"name": "Name",
"displayName": "Display name",
"description": "Description",
"icon": "Icon",
"autostop": "Autostop default",
"allowUsersToCancel": "Allow users to cancel in-progress workspace jobs"
},
"helperText": {
"autostop": "Time in hours",
"allowUsersToCancel": "If checked, users may be able to corrupt their workspace."
},
"upload": {
"removeTitle": "Remove file",
"title": "Upload template"
},
"tooltip": {
"allowUsersToCancel": "Depending on your template, canceling builds may leave workspaces in an unhealthy state. This option isn't recommended for most use cases."
}
}
-4
View File
@@ -1,4 +0,0 @@
{
"deleteSuccess": "Template successfully deleted.",
"createdVersion": "created the version"
}
@@ -1,67 +0,0 @@
{
"title": "General Settings",
"nameLabel": "Name",
"displayNameLabel": "Display name",
"descriptionLabel": "Description",
"descriptionMaxError": "Please enter a description that is less than or equal to 128 characters.",
"defaultTtlLabel": "Default autostop (hours)",
"maxTtlLabel": "Max lifetime (hours)",
"autostopRequirementDaysLabel": "Days with required stop",
"autostopRequirementWeeksLabel": "Weeks between required stops",
"iconLabel": "Icon",
"formAriaLabel": "Template settings form",
"selectEmoji": "Select emoji",
"defaultTTLMaxError": "Please enter a limit that is less than or equal to 720 hours (30 days).",
"defaultTTLMinError": "Default time until autostop must not be less than 0.",
"defaultTTLHelperText_zero": "Workspaces will run until stopped manually.",
"defaultTTLHelperText_one": "Workspaces will default to stopping after {{count}} hour. If Coder detects workspace connection activity, the autostop timer is bumped by this value.",
"defaultTTLHelperText_other": "Workspaces will default to stopping after {{count}} hours. If Coder detects workspace connection activity, the autostop timer is bumped by this value.",
"maxTTLMaxError": "Please enter a limit that is less than or equal to 720 hours (30 days).",
"maxTTLMinError": "Maximum time until autostop must not be less than 0.",
"maxTTLHelperText_zero": "Workspaces may run indefinitely.",
"maxTTLHelperText_one": "Workspaces must stop within 1 hour of starting, regardless of any active connections.",
"maxTTLHelperText_other": "Workspaces must stop within {{count}} hours of starting, regardless of any active connections.",
"autostopRequirementDays_off": "Off",
"autostopRequirementDays_daily": "Daily",
"autostopRequirementDays_saturday": "Saturday",
"autostopRequirementDays_sunday": "Sunday",
"autostopRequirementDaysHelperText_off": "Workspaces are not required to stop periodically.",
"autostopRequirementDaysHelperText_daily": "Workspaces are required to be automatically stopped daily in the user's quiet hours and timezone.",
"autostopRequirementDaysHelperText_saturday": "Workspaces are required to be automatically stopped every Saturday in the user's quiet hours and timezone.",
"autostopRequirementDaysHelperText_sunday": "Workspaces are required to be automatically stopped every Sunday in the user's quiet hours and timezone.",
"autostopRequirementWeeksHelperText_disabled": "Weeks between required stops cannot be set unless days between required stops is Saturday or Sunday.",
"autostopRequirementWeeksHelperText_one": "Workspaces are required to be automatically stopped every week on the specified day in the user's quiet hours and timezone.",
"autostopRequirementWeeksHelperText_other": "Workspaces are required to be automatically stopped every {{count}} weeks on the specified day in the user's quiet hours and timezone.",
"failureTTLHelperText_zero": "Coder will not automatically stop failed workspaces",
"failureTTLHelperText_one": "Coder will attempt to stop failed workspaces after {{count}} day.",
"failureTTLHelperText_other": "Coder will attempt to stop failed workspaces after {{count}} days.",
"dormancyThresholdHelperText_zero": "Coder will not mark workspaces as dormant.",
"dormancyThresholdHelperText_one": "Coder will mark workspaces as dormant after {{count}} day without user connections.",
"dormancyThresholdHelperText_other": "Coder will mark workspaces as dormant after {{count}} days without user connections.",
"dormancyAutoDeletionHelperText_zero": "Coder will not automatically delete dormant workspaces.",
"dormancyAutoDeletionHelperText_one": "Coder will automatically delete dormant workspaces after {{count}} day.",
"dormancyAutoDeletionHelperText_other": "Coder will automatically delete dormant workspaces after {{count}} days.",
"allowUserCancelWorkspaceJobsLabel": "Allow users to cancel in-progress workspace jobs.",
"allowUserCancelWorkspaceJobsNotice": "Depending on your template, canceling builds may leave workspaces in an unhealthy state. This option isn't recommended for most use cases.",
"allowUsersCancelHelperText": "If checked, users may be able to corrupt their workspace.",
"generalInfo": {
"title": "General info",
"description": "The name is used to identify the template in URLs and the API."
},
"displayInfo": {
"title": "Display info",
"description": "A friendly name, description, and icon to help developers identify your template."
},
"schedule": {
"title": "Schedule",
"description": "Define when workspaces created from this template are stopped."
},
"autostopRequirement": {
"title": "Autostop Requirement",
"description": "Define when workspaces created from this template are stopped periodically to enforce template updates and ensure idle workspaces are stopped."
},
"operations": {
"title": "Operations",
"description": "Regulate actions allowed on workspaces created from this template."
}
}
@@ -1,6 +0,0 @@
{
"title": "Template variables",
"sensitiveVariableHelperText": "This variable is sensitive. The previous value will be used if empty.",
"validationRequiredVariable": "Variable is required.",
"unusedVariablesNotice": "This template does not use managed variables."
}
-11
View File
@@ -1,11 +0,0 @@
{
"title": "Version",
"header": {
"caption": "Version"
},
"stats": {
"template": "Template",
"createdBy": "Created by",
"created": "Created"
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"errors": {
"getOrganizationError": "Something went wrong fetching organizations.",
"getTemplatesError": "Something went wrong fetching templates."
},
"empty": {
"message": "Create a Template",
"descriptionWithoutPermissions": "Contact your Coder administrator to create a template. You can share the code below."
}
}
-55
View File
@@ -1,55 +0,0 @@
{
"title": "Tokens",
"description": "Tokens are used to authenticate with the Coder API. You can create a token on this page or with the Coder CLI using the <1>{{cliCreateCommand}}</1> command.",
"emptyState": "No tokens found",
"tokenActions": {
"addToken": "Add token",
"deleteToken": {
"delete": "Delete Token",
"deleteCaption": "Are you sure you want to permanently delete token <strong><4>{{tokenName}}</4></strong>?",
"deleteSuccess": "Token has been deleted",
"deleteFailure": "Failed to delete token"
}
},
"table": {
"id": "ID",
"name": "Name",
"lastUsed": "Last Used",
"expiresAt": "Expires At",
"createdAt": "Created At"
},
"createToken": {
"title": "Create Token",
"detail": "All tokens are unscoped and therefore have full resource access.",
"nameSection": {
"title": "Name",
"description": "What is this token for?"
},
"lifetimeSection": {
"title": "Expiration",
"description": "The token will expire on {{date}}.",
"emptyDescription": "Please set a token expiration.",
"7": "7 days",
"30": "30 days",
"60": "60 days",
"90": "90 days",
"custom": "Custom",
"noExpiration": "No expiration",
"expiresOn": "Expires on"
},
"fields": {
"name": "Name",
"lifetime": "Lifetime"
},
"footer": {
"retry": "Retry",
"submit": "Create token"
},
"createSuccess": "Token has been created",
"createError": "Failed to create token",
"successModal": {
"title": "Creation successful",
"description": "Make sure you copy the below token before proceeding:"
}
}
}
-4
View File
@@ -1,4 +0,0 @@
{
"securityUpdateSuccessMessage": "Updated password.",
"sshRegenerateSuccessMessage": "SSH Key regenerated successfully."
}
-19
View File
@@ -1,19 +0,0 @@
{
"emptyMessage": "No users found",
"emptyPageMessage": "No users found on this page",
"suspendMenuItem": "Suspend",
"deleteMenuItem": "Delete",
"listWorkspacesMenuItem": "View workspaces",
"activateMenuItem": "Activate",
"resetPasswordMenuItem": "Reset password",
"editUserRolesTooltip": "Edit user roles",
"fieldSetRolesTooltip": "Available roles",
"roleDescription": {
"owner": "Owner can manage all resources, including users, groups, templates, and workspaces.",
"user-admin": "User admin can manage all users and groups.",
"template-admin": "Template admin can manage all templates and workspaces.",
"auditor": "Auditor can access the audit logs.",
"member": "Everybody is a member. This is a shared and default role for all users."
},
"member": "Member"
}
-73
View File
@@ -1,73 +0,0 @@
{
"deleteDialog": {
"info": "This workspace was created {{timeAgo}}."
},
"workspaceScheduleButton": {
"schedule": "Schedule",
"editDeadlineMinus": "Subtract hours",
"editDeadlinePlus": "Add hours",
"submitDeadline": "Set",
"hours": "Hours"
},
"ctas": {
"createWorkspaceCta": "Create new workspace",
"extendScheduleCta": "Extend"
},
"warningsAndErrors": {
"workspaceRefreshWarning": "We're having difficulty fetching the latest workspace state. Refresh the page to see the newest changes.",
"workspaceDeletedWarning": "This workspace has been deleted and cannot be edited.",
"workspaceShutdownWarning": "Your workspace is scheduled to automatically shut down soon."
},
"actionButton": {
"start": "Start",
"stop": "Stop",
"restart": "Restart",
"delete": "Delete",
"cancel": "Cancel",
"update": "Update",
"updating": "Updating",
"starting": "Starting...",
"stopping": "Stopping...",
"deleting": "Deleting...",
"settings": "Settings",
"retryDebugMode": "Try in debug mode"
},
"disabledButton": {
"canceling": "Canceling",
"deleted": "Deleted",
"pending": "Pending"
},
"agentStatus": {
"connected": {
"ready": "Ready",
"starting": "Starting...",
"shuttingDown": "Stopping...",
"off": "Stopped"
},
"connecting": "Connecting...",
"disconnected": "Disconnected",
"timeout": "Timeout"
},
"buildMessage": {
"start": "started",
"stop": "stopped",
"delete": "deleted",
"theWorkspace": "the workspace",
"automatically": "automatically "
},
"buildData": {
"reason": "Reason",
"duration": "Duration",
"version": "Version"
},
"agentOutdatedTooltip": {
"title": "Agent Outdated",
"description": "This agent is an older version than the Coder server. This can happen after you update Coder with running workspaces. To fix this, you can stop and start the workspace.",
"agentVersionLabel": "Agent version",
"serverVersionLabel": "Server version",
"updateWorkspaceLabel": "Update workspace"
},
"askParametersDialog": {
"message": "The new version has parameters that need to be filled in to update the workspace."
}
}
@@ -1,7 +0,0 @@
{
"forbiddenError": "You don't have permissions to update the schedule for this workspace.",
"dialogTitle": "Restart workspace?",
"dialogDescription": "Would you like to restart your workspace now to apply your new autostop setting, or let it apply after your next workspace start?",
"restart": "Restart workspace",
"applyLater": "Apply update later"
}
@@ -1,9 +0,0 @@
{
"title": "Workspace Settings",
"defaultErrorMessage": "Error on update workspace settings",
"nameLabel": "Name",
"generalInfo": "General info",
"generalInfoDescription": "The name of your new workspace.",
"parameters": "Parameters",
"parametersDescription": "Those values are provided by your template's Terraform configuration. Values can be changed after creating the workspace."
}
-8
View File
@@ -1,8 +0,0 @@
{
"emptyCreateWorkspaceMessage": "Create a Workspace",
"emptyCreateWorkspaceDescription": "A workspace is your personal, customizable development environment in the cloud",
"createFromTemplateButton": "Select a Template",
"emptyResultsMessage": "No results matched your search",
"emptyPageMessage": "No results on this page",
"updateVersionError": "Error on update workspace version"
}
-21
View File
@@ -1,21 +0,0 @@
import i18next from "i18next";
import { initReactI18next } from "react-i18next";
import { en } from "./en";
export const defaultNS = "common";
export const resources = { en } as const;
export const i18n = i18next.use(initReactI18next);
i18n
.init({
fallbackLng: "en",
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
resources,
})
.catch((error) => {
// we are catching here to avoid lint's no-floating-promises error
console.error("[Translation Service]:", error);
});
-1
View File
@@ -1 +0,0 @@
export * from "./i18n";
-1
View File
@@ -2,7 +2,6 @@ import { inspect } from "@xstate/inspect";
import { createRoot } from "react-dom/client";
import { Interpreter } from "xstate";
import { App } from "./App";
import "./i18n";
// if this is a development build and the developer wants to inspect
// helpful to see realtime changes on the services
@@ -2,14 +2,11 @@ import { FC } from "react";
import { AuditLog } from "api/typesGenerated";
import { Link as RouterLink } from "react-router-dom";
import Link from "@mui/material/Link";
import { Trans, useTranslation } from "react-i18next";
import { BuildAuditDescription } from "./BuildAuditDescription";
export const AuditLogDescription: FC<{ auditLog: AuditLog }> = ({
auditLog,
}): JSX.Element => {
const { t } = useTranslation("auditLog");
let target = auditLog.resource_target.trim();
const user = auditLog.user?.username.trim();
@@ -31,38 +28,20 @@ export const AuditLogDescription: FC<{ auditLog: AuditLog }> = ({
auditLog.additional_fields.workspace_owner &&
auditLog.additional_fields.workspace_owner !== "unknown" &&
auditLog.additional_fields.workspace_owner.trim() !== user
? `on behalf of ${auditLog.additional_fields.workspace_owner}`
? ` on behalf of ${auditLog.additional_fields.workspace_owner}`
: "";
if (auditLog.resource_link) {
return (
<span>
<Trans
t={t}
i18nKey="table.logRow.description.linkedAuditDescription"
values={{ truncatedDescription, target, onBehalfOf }}
>
{"{{truncatedDescription}}"}
<Link component={RouterLink} to={auditLog.resource_link}>
<strong>{"{{target}}"}</strong>
</Link>
{"{{onBehalfOf}}"}
</Trans>
</span>
);
}
return (
<span>
<Trans
t={t}
i18nKey="table.logRow.description.unlinkedAuditDescription"
values={{ truncatedDescription, target, onBehalfOf }}
>
{"{{truncatedDescription}}"}
<strong>{"{{target}}"}</strong>
{"{{onBehalfOf}}"}
</Trans>
{truncatedDescription}
{auditLog.resource_link ? (
<Link component={RouterLink} to={auditLog.resource_link}>
<strong>{target}</strong>
</Link>
) : (
<strong>{target}</strong>
)}
{onBehalfOf}
</span>
);
};
@@ -1,14 +1,11 @@
import { Trans, useTranslation } from "react-i18next";
import { AuditLog } from "api/typesGenerated";
import { FC } from "react";
import { FC, useMemo } from "react";
import { Link as RouterLink } from "react-router-dom";
import Link from "@mui/material/Link";
export const BuildAuditDescription: FC<{ auditLog: AuditLog }> = ({
auditLog,
}): JSX.Element => {
const { t } = useTranslation("auditLog");
const workspaceName = auditLog.additional_fields?.workspace_name?.trim();
// workspaces can be started/stopped/deleted by a user, or kicked off automatically by Coder
const user =
@@ -17,7 +14,7 @@ export const BuildAuditDescription: FC<{ auditLog: AuditLog }> = ({
? "Coder automatically"
: auditLog.user?.username.trim();
const action: string = (() => {
const action = useMemo(() => {
switch (auditLog.action) {
case "start":
return "started";
@@ -28,36 +25,18 @@ export const BuildAuditDescription: FC<{ auditLog: AuditLog }> = ({
default:
return auditLog.action;
}
})();
if (auditLog.resource_link) {
return (
<span>
<Trans
t={t}
i18nKey="table.logRow.description.linkedWorkspaceBuild"
values={{ user, action, workspaceName }}
>
{"{{user}}"}
<Link component={RouterLink} to={auditLog.resource_link}>
{"{{action}}"}
</Link>
workspace{"{{workspaceName}}"}
</Trans>
</span>
);
}
}, [auditLog.action]);
return (
<span>
<Trans
t={t}
i18nKey="table.logRow.description.unlinkedWorkspaceBuild"
values={{ user, action, workspaceName }}
>
{"{{user}}"}
{"{{action}}"}workspace{"{{workspaceName}}"}
</Trans>
{user} <strong>{action}</strong> workspace{" "}
{auditLog.resource_link ? (
<Link component={RouterLink} to={auditLog.resource_link}>
<strong>{workspaceName}</strong>
</Link>
) : (
<strong>{workspaceName}</strong>
)}
</span>
);
};
@@ -13,7 +13,6 @@ import { UserAvatar } from "components/UserAvatar/UserAvatar";
import { useState } from "react";
import userAgentParser from "ua-parser-js";
import { AuditLogDiff } from "./AuditLogDiff/AuditLogDiff";
import { useTranslation } from "react-i18next";
import { AuditLogDescription } from "./AuditLogDescription/AuditLogDescription";
import { PaletteIndex } from "theme/theme";
import { determineGroupDiff } from "./AuditLogDiff/auditUtils";
@@ -46,7 +45,6 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
defaultIsDiffOpen = false,
}) => {
const styles = useStyles();
const { t } = useTranslation("auditLog");
const [isDiffOpen, setIsDiffOpen] = useState(defaultIsDiffOpen);
const diffs = Object.entries(auditLog.diff);
const shouldDisplayDiff = diffs.length > 0;
@@ -114,7 +112,7 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
<AuditLogDescription auditLog={auditLog} />
{auditLog.is_deleted && (
<span className={styles.deletedLabel}>
<>{t("table.logRow.deletedLabel")}</>
<>(deleted)</>
</span>
)}
<span className={styles.auditLogTime}>
@@ -126,19 +124,19 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
<Stack direction="row" spacing={1} alignItems="baseline">
{auditLog.ip && (
<span className={styles.auditLogInfo}>
<>{t("table.logRow.ip")}</>
<>IP: </>
<strong>{auditLog.ip}</strong>
</span>
)}
{os.name && (
<span className={styles.auditLogInfo}>
<>{t("table.logRow.os")}</>
<>OS: </>
<strong>{os.name}</strong>
</span>
)}
{browser.name && (
<span className={styles.auditLogInfo}>
<>{t("table.logRow.browser")}</>
<>Browser: </>
<strong>
{browser.name} {browser.version}
</strong>
+3 -6
View File
@@ -18,7 +18,6 @@ import { TableLoader } from "components/TableLoader/TableLoader";
import { Timeline } from "components/Timeline/Timeline";
import { AuditHelpTooltip } from "./AuditHelpTooltip";
import { ComponentProps, FC } from "react";
import { useTranslation } from "react-i18next";
import { AuditPaywall } from "./AuditPaywall";
import { AuditFilter } from "./AuditFilter";
import {
@@ -55,8 +54,6 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
error,
filterProps,
}) => {
const { t } = useTranslation("auditLog");
const isLoading = (auditLogs === undefined || count === undefined) && !error;
const isEmpty = !isLoading && auditLogs?.length === 0;
@@ -93,7 +90,7 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
<Cond condition={Boolean(error)}>
<TableRow>
<TableCell colSpan={999}>
<EmptyState message={t("table.noLogs")} />
<EmptyState message="An error occurred while loading audit logs" />
</TableCell>
</TableRow>
</Cond>
@@ -105,14 +102,14 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
<Cond condition={isNonInitialPage}>
<TableRow>
<TableCell colSpan={999}>
<EmptyState message={t("table.emptyPage")} />
<EmptyState message="No audit logs available on this page" />
</TableCell>
</TableRow>
</Cond>
<Cond>
<TableRow>
<TableCell colSpan={999}>
<EmptyState message={t("table.noLogs")} />
<EmptyState message="No audit logs available" />
</TableCell>
</TableRow>
</Cond>
+4 -7
View File
@@ -4,21 +4,18 @@ import ArrowRightAltOutlined from "@mui/icons-material/ArrowRightAltOutlined";
import { Paywall } from "components/Paywall/Paywall";
import { Stack } from "components/Stack/Stack";
import { FC } from "react";
import { useTranslation } from "react-i18next";
import { docs } from "utils/docs";
export const AuditPaywall: FC = () => {
const { t } = useTranslation("auditLog");
return (
<Paywall
message={t("paywall.title")}
description={t("paywall.description")}
message="Audit logs"
description="Audit Logs allows Auditors to monitor user operations in their deployment. To use this feature, you have to upgrade your account."
cta={
<Stack direction="row" alignItems="center">
<Link href={docs("/admin/upgrade")} target="_blank" rel="noreferrer">
<Button size="small" startIcon={<ArrowRightAltOutlined />}>
{t("paywall.actions.upgrade")}
See how to upgrade
</Button>
</Link>
<Link
@@ -26,7 +23,7 @@ export const AuditPaywall: FC = () => {
target="_blank"
rel="noreferrer"
>
{t("paywall.actions.readDocs")}
Read the documentation
</Link>
</Stack>
}
@@ -12,7 +12,6 @@ import { TemplateUpload, TemplateUploadProps } from "./TemplateUpload";
import { useFormik } from "formik";
import { SelectedTemplate } from "pages/CreateWorkspacePage/SelectedTemplate";
import { FC, useEffect } from "react";
import { useTranslation } from "react-i18next";
import {
nameValidator,
getFormHelpers,
@@ -27,8 +26,6 @@ import {
HelpTooltipText,
} from "components/HelpTooltip/HelpTooltip";
import { LazyIconField } from "components/IconField/LazyIconField";
import { Maybe } from "components/Conditionals/Maybe";
import i18next from "i18next";
import Link from "@mui/material/Link";
import {
HorizontalForm,
@@ -49,32 +46,51 @@ import MenuItem from "@mui/material/MenuItem";
const MAX_DESCRIPTION_CHAR_LIMIT = 128;
const MAX_TTL_DAYS = 30;
const TTLHelperText = ({
ttl,
translationName,
}: {
ttl?: number;
translationName: string;
}) => {
const { t } = useTranslation("createTemplatePage");
const count = typeof ttl !== "number" ? 0 : ttl;
const hours = (h: number) => (h === 1 ? "hour" : "hours");
const DefaultTTLHelperText = (props: { ttl?: number }) => {
const { ttl = 0 } = props;
// Error will show once field is considered touched
if (ttl < 0) {
return null;
}
if (ttl === 0) {
return <span>Workspaces will run until stopped manually.</span>;
}
return (
// no helper text if ttl is negative - error will show once field is considered touched
<Maybe condition={count >= 0}>
<span>{t(translationName, { count })}</span>
</Maybe>
<span>
Workspaces will default to stopping after {ttl} {hours(ttl)} without
activity.
</span>
);
};
const MaxTTLHelperText = (props: { ttl?: number }) => {
const { ttl = 0 } = props;
// Error will show once field is considered touched
if (ttl < 0) {
return null;
}
if (ttl === 0) {
return <span>Workspaces may run indefinitely.</span>;
}
return (
<span>
Workspaces must stop within {ttl} {hours(ttl)} of starting, regardless of
any active connections.
</span>
);
};
const validationSchema = Yup.object({
name: nameValidator(
i18next.t("form.fields.name", { ns: "createTemplatePage" }),
),
display_name: templateDisplayNameValidator(
i18next.t("form.fields.displayName", {
ns: "createTemplatePage",
}),
),
name: nameValidator("Name"),
display_name: templateDisplayNameValidator("Display name"),
description: Yup.string().max(
MAX_DESCRIPTION_CHAR_LIMIT,
"Please enter a description that is less than or equal to 128 characters.",
@@ -225,8 +241,6 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
onSubmit,
});
const getFieldHelpers = getFormHelpers<CreateTemplateData>(form, error);
const { t } = useTranslation("createTemplatePage");
const { t: commonT } = useTranslation("common");
useEffect(() => {
if (error) {
@@ -288,7 +302,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
autoFocus
fullWidth
required
label={t("form.fields.name")}
label="Name"
/>
</FormFields>
</FormSection>
@@ -303,7 +317,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
{...getFieldHelpers("display_name")}
disabled={isSubmitting}
fullWidth
label={t("form.fields.displayName")}
label="Display name"
/>
<TextField
@@ -312,7 +326,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
rows={5}
multiline
fullWidth
label={t("form.fields.description")}
label="Description"
/>
<LazyIconField
@@ -320,7 +334,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
disabled={isSubmitting}
onChange={onChangeTrimmed(form)}
fullWidth
label={t("form.fields.icon")}
label="Icon"
onPickEmoji={(value) => form.setFieldValue("icon", value)}
/>
</FormFields>
@@ -336,15 +350,12 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
<TextField
{...getFieldHelpers(
"default_ttl_hours",
<TTLHelperText
translationName="form.helperText.defaultTTLHelperText"
ttl={form.values.default_ttl_hours}
/>,
<DefaultTTLHelperText ttl={form.values.default_ttl_hours} />,
)}
disabled={isSubmitting}
onChange={onChangeTrimmed(form)}
fullWidth
label={t("form.fields.autostop")}
label="Default autostop (hours)"
type="number"
/>
@@ -353,23 +364,17 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
{...getFieldHelpers(
"max_ttl_hours",
allowAdvancedScheduling ? (
<TTLHelperText
translationName="form.helperText.maxTTLHelperText"
ttl={form.values.max_ttl_hours}
/>
<MaxTTLHelperText ttl={form.values.max_ttl_hours} />
) : (
<>
{commonT("licenseFieldTextHelper")}{" "}
<Link href={docs("/enterprise")}>
{commonT("learnMore")}
</Link>
.
You need an enterprise license to use it.{" "}
<Link href={docs("/enterprise")}>Learn more</Link>.
</>
),
)}
disabled={isSubmitting || !allowAdvancedScheduling}
fullWidth
label={t("form.fields.maxTTL")}
label="Max lifetime (hours)"
type="number"
/>
)}
@@ -388,19 +393,19 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
fullWidth
select
value={form.values.autostop_requirement_days_of_week}
label={t("form.fields.autostopRequirementDays")}
label="Days with required stop"
>
<MenuItem key="off" value="off">
{t("form.fields.autostopRequirementDays_off")}
Off
</MenuItem>
<MenuItem key="daily" value="daily">
{t("form.fields.autostopRequirementDays_daily")}
Daily
</MenuItem>
<MenuItem key="saturday" value="saturday">
{t("form.fields.autostopRequirementDays_saturday")}
Saturday
</MenuItem>
<MenuItem key="sunday" value="sunday">
{t("form.fields.autostopRequirementDays_sunday")}
Sunday
</MenuItem>
</TextField>
@@ -420,7 +425,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
}
fullWidth
inputProps={{ min: 1, max: 16, step: 1 }}
label={t("form.fields.autostopRequirementWeeks")}
label="Weeks between required stops"
type="number"
/>
</Stack>
@@ -500,16 +505,21 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
spacing={0.5}
className={styles.optionText}
>
<strong>{t("form.fields.allowUsersToCancel")}</strong>
<strong>
Allow users to cancel in-progress workspace jobs
</strong>
<HelpTooltip>
<HelpTooltipText>
{t("form.tooltip.allowUsersToCancel")}
If checked, users may be able to corrupt their
workspace.
</HelpTooltipText>
</HelpTooltip>
</Stack>
<span className={styles.optionHelperText}>
{t("form.helperText.allowUsersToCancel")}
Depending on your template, canceling builds may leave
workspaces in an unhealthy state. This option isn&apos;t
recommended for most use cases.
</span>
</Stack>
</Stack>
@@ -8,7 +8,6 @@ import { Stack } from "components/Stack/Stack";
import { useOrganizationId } from "hooks/useOrganizationId";
import { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import { pageTitle } from "utils/page";
import { createTemplateMachine } from "xServices/createTemplate/createTemplateXService";
@@ -16,7 +15,6 @@ import { CreateTemplateForm } from "./CreateTemplateForm";
import { ErrorAlert } from "components/Alert/ErrorAlert";
const CreateTemplatePage: FC = () => {
const { t } = useTranslation("createTemplatePage");
const navigate = useNavigate();
const organizationId = useOrganizationId();
const [searchParams] = useSearchParams();
@@ -57,7 +55,7 @@ const CreateTemplatePage: FC = () => {
<title>{pageTitle("Create Template")}</title>
</Helmet>
<FullPageHorizontalForm title={t("title")} onCancel={onCancel}>
<FullPageHorizontalForm title="Create Template" onCancel={onCancel}>
<Maybe condition={state.hasTag("loading")}>
<Loader />
</Maybe>
@@ -1,7 +1,6 @@
import Link from "@mui/material/Link";
import { FileUpload } from "components/FileUpload/FileUpload";
import { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link as RouterLink } from "react-router-dom";
export interface TemplateUploadProps {
@@ -17,8 +16,6 @@ export const TemplateUpload: FC<TemplateUploadProps> = ({
onRemove,
file,
}) => {
const { t } = useTranslation("createTemplatePage");
const description = (
<>
The template has to be a .tar file. You can also use our{" "}
@@ -42,8 +39,8 @@ export const TemplateUpload: FC<TemplateUploadProps> = ({
onUpload={onUpload}
onRemove={onRemove}
file={file}
removeLabel={t("form.upload.removeFile")}
title={t("form.upload.title")}
removeLabel="Remove file"
title="Upload template"
description={description}
extension=".tar"
fileTypeRequired="application/x-tar"
@@ -6,7 +6,6 @@ import {
HorizontalForm,
} from "components/Form/Form";
import makeStyles from "@mui/styles/makeStyles";
import { useTranslation } from "react-i18next";
import { onChangeTrimmed, getFormHelpers } from "utils/formUtils";
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
@@ -40,7 +39,6 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
creationFailed,
}) => {
const styles = useStyles();
const { t } = useTranslation("tokensPage");
const navigate = useNavigate();
const [expDays, setExpDays] = useState<number>(1);
@@ -69,7 +67,7 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
<FormFields>
<TextField
{...getFieldHelpers("name")}
label={t("createToken.fields.name")}
label="Name"
required
onChange={onChangeTrimmed(form, () => setFormError(undefined))}
autoFocus
@@ -93,7 +91,7 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
<Stack direction="row">
<TextField
select
label={t("createToken.fields.lifetime")}
label="Lifetime"
required
defaultValue={determineDefaultLtValue(maxTokenLifetime)}
onChange={(event) => {
@@ -117,7 +115,7 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
{lifetimeDays === "custom" && (
<TextField
type="date"
label={t("createToken.lifetimeSection.expiresOn")}
label="Expires on"
defaultValue={dayjs().add(expDays, "day").format("YYYY-MM-DD")}
onChange={(event) => {
const lt = Math.ceil(
@@ -1,5 +1,4 @@
import { FC, useState } from "react";
import { useTranslation } from "react-i18next";
import { Helmet } from "react-helmet-async";
import { pageTitle } from "utils/page";
import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm";
@@ -22,7 +21,6 @@ const initialValues: CreateTokenData = {
};
export const CreateTokenPage: FC = () => {
const { t } = useTranslation("tokensPage");
const styles = useStyles();
const navigate = useNavigate();
@@ -46,13 +44,13 @@ export const CreateTokenPage: FC = () => {
const [formError, setFormError] = useState<unknown>(undefined);
const onCreateSuccess = () => {
displaySuccess(t("createToken.createSuccess"));
displaySuccess("Token has been created");
navigate("/settings/tokens");
};
const onCreateError = (error: unknown) => {
setFormError(error);
displayError(t("createToken.createError"));
displayError("Failed to create token");
};
const form = useFormik<CreateTokenData>({
@@ -73,7 +71,7 @@ export const CreateTokenPage: FC = () => {
const tokenDescription = (
<>
<p>{t("createToken.successModal.description")}</p>
<p>Make sure you copy the below token before proceeding:</p>
<CodeExample code={newToken?.key ?? ""} className={styles.codeExample} />
</>
);
@@ -89,8 +87,8 @@ export const CreateTokenPage: FC = () => {
</Helmet>
{tokenFetchFailed && <ErrorAlert error={tokenFetchError} />}
<FullPageHorizontalForm
title={t("createToken.title")}
detail={t("createToken.detail")}
title="Create Token"
detail="All tokens are unscoped and therefore have full resource access."
>
<CreateTokenForm
form={form}
@@ -104,7 +102,7 @@ export const CreateTokenPage: FC = () => {
<ConfirmDialog
type="info"
hideCancel
title={t("createToken.successModal.title")}
title="Creation successful"
description={tokenDescription}
open={creationSuccessful && Boolean(newToken.key)}
confirmLoading={isCreating}
+5 -7
View File
@@ -1,5 +1,3 @@
import i18next from "i18next";
export const NANO_HOUR = 3600000000000;
export interface CreateTokenData {
@@ -14,25 +12,25 @@ export interface LifetimeDay {
export const lifetimeDayPresets: LifetimeDay[] = [
{
label: i18next.t("tokensPage:createToken.lifetimeSection.7"),
label: "7 days",
value: 7,
},
{
label: i18next.t("tokensPage:createToken.lifetimeSection.30"),
label: "30 days",
value: 30,
},
{
label: i18next.t("tokensPage:createToken.lifetimeSection.60"),
label: "60 days",
value: 60,
},
{
label: i18next.t("tokensPage:createToken.lifetimeSection.90"),
label: "90 days",
value: 90,
},
];
export const customLifetimeDay: LifetimeDay = {
label: i18next.t("tokensPage:createToken.lifetimeSection.custom"),
label: "Custom",
value: "custom",
};
@@ -1,7 +1,6 @@
import { fireEvent, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import * as API from "api/api";
import i18next from "i18next";
import {
MockTemplate,
MockUser,
@@ -20,20 +19,10 @@ import {
} from "testHelpers/renderHelpers";
import CreateWorkspacePage from "./CreateWorkspacePage";
const { t } = i18next;
const nameLabelText = t("nameLabel", { ns: "createWorkspacePage" });
const createWorkspaceText = t("createWorkspace", { ns: "createWorkspacePage" });
const validationNumberNotInRangeText = t("validationNumberNotInRange", {
ns: "createWorkspacePage",
min: "1",
max: "3",
});
const validationPatternNotMatched = t("validationPatternNotMatched", {
ns: "createWorkspacePage",
error: MockTemplateVersionParameter3.validation_error,
pattern: "^[a-z]{3}$",
});
const nameLabelText = "Workspace Name";
const createWorkspaceText = "Create Workspace";
const validationNumberNotInRangeText = "Value must be between 1 and 3.";
const validationPatternNotMatched = `${MockTemplateVersionParameter3.validation_error} (value does not match the pattern ^[a-z]{3}$)`;
const renderCreateWorkspacePage = () => {
return renderWithAuth(<CreateWorkspacePage />, {
@@ -3,7 +3,6 @@ import * as TypesGen from "api/typesGenerated";
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
import { FormikContextType, useFormik } from "formik";
import { FC, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
getFormHelpers,
nameValidator,
@@ -64,7 +63,6 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
onSubmit,
onCancel,
}) => {
const { t } = useTranslation("createWorkspacePage");
const styles = useStyles();
const [owner, setOwner] = useState(defaultOwner);
const { verifyGitAuth, gitAuthErrors } = useGitAuthVerification(gitAuth);
@@ -79,11 +77,8 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
),
},
validationSchema: Yup.object({
name: nameValidator(t("nameLabel", { ns: "createWorkspacePage" })),
rich_parameter_values: useValidationSchemaForRichParameters(
"createWorkspacePage",
parameters,
),
name: nameValidator("Workspace Name"),
rich_parameter_values: useValidationSchemaForRichParameters(parameters),
}),
enableReinitialize: true,
onSubmit: (request) => {
@@ -124,7 +119,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
disabled
fullWidth
value={versionId}
label={t("versionLabel")}
label="Version ID"
/>
<span className={styles.description}>
This parameter has been preset, and cannot be modified.
@@ -137,7 +132,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
onChange={onChangeTrimmed(form)}
autoFocus
fullWidth
label={t("nameLabel")}
label="Workspace Name"
/>
</FormFields>
</FormSection>
@@ -153,7 +148,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
onChange={(user) => {
setOwner(user ?? defaultOwner);
}}
label={t("ownerLabel").toString()}
label="Owner"
size="medium"
/>
</FormFields>
@@ -222,7 +217,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
<FormFooter
onCancel={onCancel}
isLoading={creatingWorkspace}
submitLabel={t("createWorkspace").toString()}
submitLabel="Create Workspace"
/>
</HorizontalForm>
</FullPageHorizontalForm>
@@ -12,7 +12,6 @@ import { getFormHelpers } from "utils/formUtils";
import Button from "@mui/material/Button";
import FormControlLabel from "@mui/material/FormControlLabel";
import { BlockPicker } from "react-color";
import { useTranslation } from "react-i18next";
import makeStyles from "@mui/styles/makeStyles";
import Switch from "@mui/material/Switch";
import TextField from "@mui/material/TextField";
@@ -38,7 +37,6 @@ export const AppearanceSettingsPageView = ({
}: AppearanceSettingsPageViewProps): JSX.Element => {
const styles = useStyles();
const theme = useTheme();
const [t] = useTranslation("appearanceSettings");
const logoForm = useFormik<{
logo_url: string;
}>({
@@ -138,7 +136,7 @@ export const AppearanceSettingsPageView = ({
);
}}
>
{t("showPreviewLabel")}
Show Preview
</Button>
)
}
@@ -180,7 +178,7 @@ export const AppearanceSettingsPageView = ({
<TextField
{...serviceBannerFieldHelpers(
"message",
t("messageHelperText"),
"Markdown bold, italics, and links are supported.",
)}
fullWidth
label="Message"
@@ -8,7 +8,6 @@ import { LazyIconField } from "components/IconField/LazyIconField";
import { Margins } from "components/Margins/Margins";
import { useFormik } from "formik";
import { FC } from "react";
import { useTranslation } from "react-i18next";
import {
getFormHelpers,
nameValidator,
@@ -48,7 +47,6 @@ const UpdateGroupForm: FC<{
onSubmit,
});
const getFieldHelpers = getFormHelpers<FormData>(form, errors);
const { t } = useTranslation("common");
return (
<FullPageForm title="Group settings">
@@ -83,7 +81,7 @@ const UpdateGroupForm: FC<{
{...getFieldHelpers("avatar_url")}
onChange={onChangeTrimmed(form)}
fullWidth
label={t("form.fields.icon")}
label="Avatar URL"
onPickEmoji={(value) => form.setFieldValue("avatar_url", value)}
/>
</>
+1 -5
View File
@@ -11,9 +11,6 @@ import {
import { server } from "testHelpers/server";
import { LoginPage } from "./LoginPage";
import * as TypesGen from "api/typesGenerated";
import { i18n } from "i18n";
const { t } = i18n;
describe("LoginPage", () => {
beforeEach(() => {
@@ -101,8 +98,7 @@ describe("LoginPage", () => {
expect(screen.queryByText(Language.passwordSignIn)).not.toBeInTheDocument();
await screen.findByText(Language.githubSignIn);
const showPasswordLabel = t("showPassword", { ns: "loginPage" });
const showPasswordAuthLink = screen.getByText(showPasswordLabel);
const showPasswordAuthLink = screen.getByText("Email and password");
await userEvent.click(showPasswordAuthLink);
await screen.findByText(Language.passwordSignIn);
+1 -6
View File
@@ -1,7 +1,6 @@
import { useAuth } from "components/AuthProvider/AuthProvider";
import { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
import { Navigate, useLocation } from "react-router-dom";
import { retrieveRedirect } from "../../utils/redirect";
import { LoginPageView } from "./LoginPageView";
@@ -10,8 +9,6 @@ export const LoginPage: FC = () => {
const location = useLocation();
const [authState, authSend] = useAuth();
const redirectTo = retrieveRedirect(location.search);
const commonTranslation = useTranslation("common");
const loginPageTranslation = useTranslation("loginPage");
if (authState.matches("signedIn")) {
return <Navigate to={redirectTo} replace />;
@@ -21,9 +18,7 @@ export const LoginPage: FC = () => {
return (
<>
<Helmet>
<title>
{loginPageTranslation.t("signInTo")} {commonTranslation.t("coder")}
</title>
<title>Sign in to Coder</title>
</Helmet>
<LoginPageView
context={authState.context}
+2 -6
View File
@@ -2,7 +2,6 @@ import { makeStyles } from "@mui/styles";
import { FormikTouched } from "formik";
import { FC, useState } from "react";
import { AuthMethods } from "api/typesGenerated";
import { useTranslation } from "react-i18next";
import { Maybe } from "components/Conditionals/Maybe";
import { PasswordSignInForm } from "./PasswordSignInForm";
import { OAuthSignInForm } from "./OAuthSignInForm";
@@ -92,14 +91,11 @@ export const SignInForm: FC<React.PropsWithChildren<SignInFormProps>> = ({
// Hide password auth by default if any OAuth method is enabled
const [showPasswordAuth, setShowPasswordAuth] = useState(!oAuthEnabled);
const styles = useStyles();
const commonTranslation = useTranslation("common");
const loginPageTranslation = useTranslation("loginPage");
return (
<div className={styles.root}>
<h1 className={styles.title}>
{loginPageTranslation.t("signInTo")}{" "}
<strong>{commonTranslation.t("coder")}</strong>
Sign in to <strong>Coder</strong>
</h1>
<Maybe condition={error !== undefined}>
<div className={styles.alert}>
@@ -150,7 +146,7 @@ export const SignInForm: FC<React.PropsWithChildren<SignInFormProps>> = ({
onClick={() => setShowPasswordAuth(true)}
startIcon={<EmailIcon className={styles.icon} />}
>
{loginPageTranslation.t("showPassword")}
Email and password
</Button>
</Maybe>
</div>
@@ -12,7 +12,6 @@ import { FC } from "react";
import { StarterTemplateContext } from "xServices/starterTemplates/starterTemplateXService";
import ViewCodeIcon from "@mui/icons-material/OpenInNewOutlined";
import PlusIcon from "@mui/icons-material/AddOutlined";
import { useTranslation } from "react-i18next";
import { Stack } from "components/Stack/Stack";
import { Link } from "react-router-dom";
import { ErrorAlert } from "components/Alert/ErrorAlert";
@@ -26,7 +25,6 @@ export const StarterTemplatePageView: FC<StarterTemplatePageViewProps> = ({
}) => {
const styles = useStyles();
const { starterTemplate } = context;
const { t } = useTranslation("starterTemplatePage");
if (context.error) {
return (
@@ -52,7 +50,7 @@ export const StarterTemplatePageView: FC<StarterTemplatePageViewProps> = ({
rel="noreferrer"
startIcon={<ViewCodeIcon />}
>
{t("actions.viewSourceCode")}
View source code
</Button>
<Button
variant="contained"
@@ -60,7 +58,7 @@ export const StarterTemplatePageView: FC<StarterTemplatePageViewProps> = ({
to={`/templates/new?exampleId=${starterTemplate.id}`}
startIcon={<PlusIcon />}
>
{t("actions.useTemplate")}
Use template
</Button>
</>
}
@@ -2,13 +2,11 @@ import { useMachine } from "@xstate/react";
import { useOrganizationId } from "hooks/useOrganizationId";
import { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
import { pageTitle } from "utils/page";
import { starterTemplatesMachine } from "xServices/starterTemplates/starterTemplatesXService";
import { StarterTemplatesPageView } from "./StarterTemplatesPageView";
const StarterTemplatesPage: FC = () => {
const { t } = useTranslation("starterTemplatesPage");
const organizationId = useOrganizationId();
const [state] = useMachine(starterTemplatesMachine, {
context: { organizationId },
@@ -17,7 +15,7 @@ const StarterTemplatesPage: FC = () => {
return (
<>
<Helmet>
<title>{pageTitle(t("title").toString())}</title>
<title>{pageTitle("Starter Templates")}</title>
</Helmet>
<StarterTemplatesPageView context={state.context} />
@@ -11,17 +11,16 @@ import {
import { Stack } from "components/Stack/Stack";
import { TemplateExampleCard } from "components/TemplateExampleCard/TemplateExampleCard";
import { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link, useSearchParams } from "react-router-dom";
import { combineClasses } from "utils/combineClasses";
import { StarterTemplatesContext } from "xServices/starterTemplates/starterTemplatesXService";
const getTagLabel = (tag: string, t: (key: string) => string) => {
const getTagLabel = (tag: string) => {
const labelByTag: Record<string, string> = {
all: t("tags.all"),
digitalocean: t("tags.digitalocean"),
aws: t("tags.aws"),
google: t("tags.google"),
all: "All templates",
digitalocean: "DigitalOcean",
aws: "AWS",
google: "Google Cloud",
};
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- this can be undefined
return labelByTag[tag] ?? tag;
@@ -39,7 +38,6 @@ export interface StarterTemplatesPageViewProps {
export const StarterTemplatesPageView: FC<StarterTemplatesPageViewProps> = ({
context,
}) => {
const { t } = useTranslation("starterTemplatesPage");
const [urlParams] = useSearchParams();
const styles = useStyles();
const { starterTemplatesByTag } = context;
@@ -52,8 +50,10 @@ export const StarterTemplatesPageView: FC<StarterTemplatesPageViewProps> = ({
return (
<Margins>
<PageHeader>
<PageHeaderTitle>{t("title")}</PageHeaderTitle>
<PageHeaderSubtitle>{t("subtitle")}</PageHeaderSubtitle>
<PageHeaderTitle>Starter Templates</PageHeaderTitle>
<PageHeaderSubtitle>
Import a built-in template to start developing in the cloud
</PageHeaderSubtitle>
</PageHeader>
<Maybe condition={Boolean(context.error)}>
@@ -67,7 +67,7 @@ export const StarterTemplatesPageView: FC<StarterTemplatesPageViewProps> = ({
<Stack direction="row" spacing={4}>
{starterTemplatesByTag && tags && (
<Stack className={styles.filter}>
<span className={styles.filterCaption}>{t("filterCaption")}</span>
<span className={styles.filterCaption}>Filter</span>
{tags.map((tag) => (
<Link
key={tag}
@@ -77,7 +77,7 @@ export const StarterTemplatesPageView: FC<StarterTemplatesPageViewProps> = ({
[styles.tagLinkActive]: tag === activeTag,
})}
>
{getTagLabel(tag, t)} ({starterTemplatesByTag[tag].length})
{getTagLabel(tag)} ({starterTemplatesByTag[tag].length})
</Link>
))}
</Stack>
@@ -7,7 +7,6 @@ import { Stack } from "components/Stack/Stack";
import { TimelineEntry } from "components/Timeline/TimelineEntry";
import { UserAvatar } from "components/UserAvatar/UserAvatar";
import { useClickableTableRow } from "hooks/useClickableTableRow";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { colors } from "theme/colors";
import { combineClasses } from "utils/combineClasses";
@@ -26,7 +25,6 @@ export const VersionRow: React.FC<VersionRowProps> = ({
onPromoteClick,
}) => {
const styles = useStyles();
const { t } = useTranslation("templatePage");
const navigate = useNavigate();
const clickableProps = useClickableTableRow(() => {
navigate(version.name);
@@ -61,8 +59,8 @@ export const VersionRow: React.FC<VersionRowProps> = ({
spacing={1}
>
<span>
<strong>{version.created_by.username}</strong>{" "}
{t("createdVersion")} <strong>{version.name}</strong>
<strong>{version.created_by.username}</strong> created the
version <strong>{version.name}</strong>
</span>
<span className={styles.versionTime}>
@@ -10,8 +10,6 @@ import {
iconValidator,
} from "utils/formUtils";
import * as Yup from "yup";
import i18next from "i18next";
import { useTranslation } from "react-i18next";
import { LazyIconField } from "components/IconField/LazyIconField";
import {
FormFields,
@@ -31,17 +29,11 @@ const MAX_DESCRIPTION_CHAR_LIMIT = 128;
export const getValidationSchema = (): Yup.AnyObjectSchema =>
Yup.object({
name: nameValidator(i18next.t("nameLabel", { ns: "templateSettingsPage" })),
display_name: templateDisplayNameValidator(
i18next.t("displayNameLabel", {
ns: "templateSettingsPage",
}),
),
name: nameValidator("Name"),
display_name: templateDisplayNameValidator("Display name"),
description: Yup.string().max(
MAX_DESCRIPTION_CHAR_LIMIT,
i18next
.t("descriptionMaxError", { ns: "templateSettingsPage" })
.toString(),
"Please enter a description that is less than or equal to 128 characters.",
),
allow_user_cancel_workspace_jobs: Yup.boolean(),
icon: iconValidator,
@@ -83,17 +75,16 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
initialTouched,
});
const getFieldHelpers = getFormHelpers(form, error);
const { t } = useTranslation("templateSettingsPage");
const styles = useStyles();
return (
<HorizontalForm
onSubmit={form.handleSubmit}
aria-label={t("formAriaLabel").toString()}
aria-label="Template settings form"
>
<FormSection
title={t("generalInfo.title").toString()}
description={t("generalInfo.description").toString()}
title="General info"
description="The name is used to identify the template in URLs and the API."
>
<FormFields>
<TextField
@@ -102,21 +93,21 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
onChange={onChangeTrimmed(form)}
autoFocus
fullWidth
label={t("nameLabel")}
label="Name"
/>
</FormFields>
</FormSection>
<FormSection
title={t("displayInfo.title").toString()}
description={t("displayInfo.description").toString()}
title="Display info"
description="A friendly name, description, and icon to help developers identify your template."
>
<FormFields>
<TextField
{...getFieldHelpers("display_name")}
disabled={isSubmitting}
fullWidth
label={t("displayNameLabel")}
label="Display name"
/>
<TextField
@@ -124,7 +115,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
multiline
disabled={isSubmitting}
fullWidth
label={t("descriptionLabel")}
label="Description"
rows={2}
/>
@@ -133,15 +124,15 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
disabled={isSubmitting}
onChange={onChangeTrimmed(form)}
fullWidth
label={t("iconLabel")}
label="Icon"
onPickEmoji={(value) => form.setFieldValue("icon", value)}
/>
</FormFields>
</FormSection>
<FormSection
title={t("operations.title").toString()}
description={t("operations.description").toString()}
title="Operations"
description="Regulate actions allowed on workspaces created from this template."
>
<label htmlFor="allow_user_cancel_workspace_jobs">
<Stack direction="row" spacing={1}>
@@ -160,16 +151,17 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
spacing={0.5}
className={styles.optionText}
>
{t("allowUserCancelWorkspaceJobsLabel")}
Allow users to cancel in-progress workspace jobs.
<HelpTooltip>
<HelpTooltipText>
{t("allowUserCancelWorkspaceJobsNotice")}
If checked, users may be able to corrupt their workspace.
</HelpTooltipText>
</HelpTooltip>
</Stack>
<span className={styles.optionHelperText}>
{t("allowUsersCancelHelperText")}
Depending on your template, canceling builds may leave
workspaces in an unhealthy state. This option isn&apos;t
recommended for most use cases.
</span>
</Stack>
</Stack>
@@ -10,9 +10,6 @@ import {
} from "../../../testHelpers/renderHelpers";
import { getValidationSchema } from "./TemplateSettingsForm";
import { TemplateSettingsPage } from "./TemplateSettingsPage";
import i18next from "i18next";
const { t } = i18next;
type FormValues = Required<
Omit<UpdateTemplateMeta, "default_ttl_ms" | "max_ttl_ms">
@@ -52,28 +49,19 @@ const fillAndSubmitForm = async ({
icon,
allow_user_cancel_workspace_jobs,
}: FormValues) => {
const label = t("nameLabel", { ns: "templateSettingsPage" });
const nameField = await screen.findByLabelText(label);
const nameField = await screen.findByLabelText("Name");
await userEvent.clear(nameField);
await userEvent.type(nameField, name);
const displayNameLabel = t("displayNameLabel", {
ns: "templateSettingsPage",
});
const displayNameField = await screen.findByLabelText(displayNameLabel);
const displayNameField = await screen.findByLabelText("Display name");
await userEvent.clear(displayNameField);
await userEvent.type(displayNameField, display_name);
const descriptionLabel = t("descriptionLabel", {
ns: "templateSettingsPage",
});
const descriptionField = await screen.findByLabelText(descriptionLabel);
const descriptionField = await screen.findByLabelText("Description");
await userEvent.clear(descriptionField);
await userEvent.type(descriptionField, description);
const iconLabel = t("iconLabel", { ns: "templateSettingsPage" });
const iconField = await screen.findByLabelText(iconLabel);
const iconField = await screen.findByLabelText("Icon");
await userEvent.clear(iconField);
await userEvent.type(iconField, icon);
@@ -118,7 +106,7 @@ describe("TemplateSettingsPage", () => {
};
const validate = () => getValidationSchema().validateSync(values);
expect(validate).toThrowError(
t("descriptionMaxError", { ns: "templateSettingsPage" }).toString(),
"Please enter a description that is less than or equal to 128 characters.",
);
});
});
@@ -4,7 +4,6 @@ import { UpdateTemplateMeta } from "api/typesGenerated";
import { displaySuccess } from "components/GlobalSnackbar/utils";
import { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import { pageTitle } from "utils/page";
import {
@@ -15,7 +14,6 @@ import { TemplateSettingsPageView } from "./TemplateSettingsPageView";
export const TemplateSettingsPage: FC = () => {
const { template: templateName } = useParams() as { template: string };
const { t } = useTranslation("templateSettingsPage");
const navigate = useNavigate();
const { template } = useTemplateSettingsContext();
const queryClient = useQueryClient();
@@ -38,7 +36,7 @@ export const TemplateSettingsPage: FC = () => {
return (
<>
<Helmet>
<title>{pageTitle([template.name, t("title")])}</title>
<title>{pageTitle([template.name, "General Settings"])}</title>
</Helmet>
<TemplateSettingsPageView
isSubmitting={isSubmitting}
@@ -1,7 +1,6 @@
import { Template, UpdateTemplateMeta } from "api/typesGenerated";
import { ComponentProps, FC } from "react";
import { TemplateSettingsForm } from "./TemplateSettingsForm";
import { useTranslation } from "react-i18next";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
import { makeStyles } from "@mui/styles";
@@ -24,13 +23,12 @@ export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
submitError,
initialTouched,
}) => {
const { t } = useTranslation("templateSettingsPage");
const styles = useStyles();
return (
<>
<PageHeader className={styles.pageHeader}>
<PageHeaderTitle>{t("title")}</PageHeaderTitle>
<PageHeaderTitle>General Settings</PageHeaderTitle>
</PageHeader>
<TemplateSettingsForm
@@ -1,5 +1,4 @@
import { Template } from "api/typesGenerated";
import { useTranslation } from "react-i18next";
export type TemplateAutostopRequirementDaysValue =
| "off"
@@ -7,6 +6,16 @@ export type TemplateAutostopRequirementDaysValue =
| "saturday"
| "sunday";
const autostopRequirementDescriptions = {
off: "Workspaces are not required to stop periodically.",
daily:
"Workspaces are required to be automatically stopped daily in the user's quiet hours and timezone.",
saturday:
"Workspaces are required to be automatically stopped every Saturday in the user's quiet hours and timezone.",
sunday:
"Workspaces are required to be automatically stopped every Sunday in the user's quiet hours and timezone.",
};
export const convertAutostopRequirementDaysValue = (
days: Template["autostop_requirement"]["days_of_week"],
): TemplateAutostopRequirementDaysValue => {
@@ -46,18 +55,11 @@ export const calculateAutostopRequirementDaysValue = (
};
export const AutostopRequirementDaysHelperText = ({
days,
days = "off",
}: {
days: TemplateAutostopRequirementDaysValue;
}) => {
const { t } = useTranslation("templateSettingsPage");
let str = "off";
if (days) {
str = days;
}
return <span>{t("autostopRequirementDaysHelperText_" + str)}</span>;
return <span>{autostopRequirementDescriptions[days]}</span>;
};
export const AutostopRequirementWeeksHelperText = ({
@@ -67,20 +69,29 @@ export const AutostopRequirementWeeksHelperText = ({
days: TemplateAutostopRequirementDaysValue;
weeks: number;
}) => {
const { t } = useTranslation("templateSettingsPage");
// Disabled
if (days !== "saturday" && days !== "sunday") {
return (
<span>
Weeks between required stops cannot be set unless days between required
stops is Saturday or Sunday.
</span>
);
}
let str = "disabled";
if (days === "saturday" || days === "sunday") {
if (weeks === 0 || weeks === 1) {
str = "one";
} else {
str = "other";
}
if (weeks <= 1) {
return (
<span>
Workspaces are required to be automatically stopped every week on the
specified day in the user&apos;s quiet hours and timezone.
</span>
);
}
return (
<span>
{t("autostopRequirementWeeksHelperText_" + str, { count: weeks })}
Workspaces are required to be automatically stopped every {weeks} weeks on
the specified day in the user&apos;s quiet hours and timezone.
</span>
);
};
@@ -1,19 +1,101 @@
import { Maybe } from "components/Conditionals/Maybe";
import { useTranslation } from "react-i18next";
const hours = (h: number) => (h === 1 ? "hour" : "hours");
const days = (d: number) => (d === 1 ? "day" : "days");
export const DefaultTTLHelperText = (props: { ttl?: number }) => {
const { ttl = 0 } = props;
// Error will show once field is considered touched
if (ttl < 0) {
return null;
}
if (ttl === 0) {
return <span>Workspaces will run until stopped manually.</span>;
}
export const TTLHelperText = ({
ttl,
translationName,
}: {
ttl?: number;
translationName: string;
}) => {
const { t } = useTranslation("templateSettingsPage");
const count = typeof ttl !== "number" ? 0 : ttl;
return (
// no helper text if ttl is negative - error will show once field is considered touched
<Maybe condition={count >= 0}>
<span>{t(translationName, { count })}</span>
</Maybe>
<span>
Workspaces will default to stopping after {ttl} {hours(ttl)} without
activity.
</span>
);
};
export const MaxTTLHelperText = (props: { ttl?: number }) => {
const { ttl = 0 } = props;
// Error will show once field is considered touched
if (ttl < 0) {
return null;
}
if (ttl === 0) {
return <span>Workspaces may run indefinitely.</span>;
}
return (
<span>
Workspaces must stop within {ttl} {hours(ttl)} of starting, regardless of
any active connections.
</span>
);
};
export const FailureTTLHelperText = (props: { ttl?: number }) => {
const { ttl = 0 } = props;
// Error will show once field is considered touched
if (ttl < 0) {
return null;
}
if (ttl === 0) {
return <span>Coder will not automatically stop failed workspaces.</span>;
}
return (
<span>
Coder will attempt to stop failed workspaces after {ttl} {days(ttl)}.
</span>
);
};
export const DormancyTTLHelperText = (props: { ttl?: number }) => {
const { ttl = 0 } = props;
// Error will show once field is considered touched
if (ttl < 0) {
return null;
}
if (ttl === 0) {
return <span>Coder will not mark workspaces as dormant.</span>;
}
return (
<span>
Coder will mark workspaces as dormant after {ttl} {days(ttl)} without user
connections.
</span>
);
};
export const DormancyAutoDeletionTTLHelperText = (props: { ttl?: number }) => {
const { ttl = 0 } = props;
// Error will show once field is considered touched
if (ttl < 0) {
return null;
}
if (ttl === 0) {
return <span>Coder will not automatically delete dormant workspaces.</span>;
}
return (
<span>
Coder will automatically delete dormant workspaces after {ttl} {days(ttl)}
.
</span>
);
};
@@ -3,7 +3,6 @@ import { Template, UpdateTemplateMeta } from "api/typesGenerated";
import { FormikTouched, useFormik } from "formik";
import { FC, ChangeEvent, useState, useEffect } from "react";
import { getFormHelpers } from "utils/formUtils";
import { useTranslation } from "react-i18next";
import {
FormSection,
HorizontalForm,
@@ -21,7 +20,13 @@ import {
useWorkspacesToBeDeleted,
} from "./useWorkspacesToBeDeleted";
import { TemplateScheduleFormValues, getValidationSchema } from "./formHelpers";
import { TTLHelperText } from "./TTLHelperText";
import {
DefaultTTLHelperText,
DormancyAutoDeletionTTLHelperText,
DormancyTTLHelperText,
FailureTTLHelperText,
MaxTTLHelperText,
} from "./TTLHelperText";
import { docs } from "utils/docs";
import { ScheduleDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import MenuItem from "@mui/material/MenuItem";
@@ -62,7 +67,6 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
isSubmitting,
initialTouched,
}) => {
const { t: commonT } = useTranslation("common");
const validationSchema = getValidationSchema();
const form = useFormik<TemplateScheduleFormValues>({
initialValues: {
@@ -140,7 +144,6 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
form,
error,
);
const { t } = useTranslation("templateSettingsPage");
const styles = useStyles();
const now = new Date();
@@ -305,25 +308,22 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
return (
<HorizontalForm
onSubmit={form.handleSubmit}
aria-label={t("formAriaLabel").toString()}
aria-label="Template settings form"
>
<FormSection
title={t("schedule.title").toString()}
description={t("schedule.description").toString()}
title="Schedule"
description="Define when workspaces created from this template are stopped."
>
<Stack direction="row" className={styles.ttlFields}>
<TextField
{...getFieldHelpers(
"default_ttl_ms",
<TTLHelperText
translationName="defaultTTLHelperText"
ttl={form.values.default_ttl_ms}
/>,
<DefaultTTLHelperText ttl={form.values.default_ttl_ms} />,
)}
disabled={isSubmitting}
fullWidth
inputProps={{ min: 0, step: 1 }}
label={t("defaultTtlLabel")}
label="Default autostop (hours)"
type="number"
/>
@@ -332,24 +332,18 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
{...getFieldHelpers(
"max_ttl_ms",
allowAdvancedScheduling ? (
<TTLHelperText
translationName="maxTTLHelperText"
ttl={form.values.max_ttl_ms}
/>
<MaxTTLHelperText ttl={form.values.max_ttl_ms} />
) : (
<>
{commonT("licenseFieldTextHelper")}{" "}
<Link href={docs("/enterprise")}>
{commonT("learnMore")}
</Link>
.
You need an enterprise license to use it{" "}
<Link href={docs("/enterprise")}>Learn more</Link>.
</>
),
)}
disabled={isSubmitting || !allowAdvancedScheduling}
fullWidth
inputProps={{ min: 0, step: 1 }}
label={t("maxTtlLabel")}
label="Max lifetime (hours)"
type="number"
/>
)}
@@ -358,8 +352,8 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
{allowAutostopRequirement && (
<FormSection
title={t("autostopRequirement.title").toString()}
description={t("autostopRequirement.description").toString()}
title="Autostop Requirement"
description="Define when workspaces created from this template are stopped periodically to enforce template updates and ensure idle workspaces are stopped."
>
<Stack direction="row" className={styles.ttlFields}>
<TextField
@@ -373,19 +367,19 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
fullWidth
select
value={form.values.autostop_requirement_days_of_week}
label={t("autostopRequirementDaysLabel")}
label="Days with required stop"
>
<MenuItem key="off" value="off">
{t("autostopRequirementDays_off")}
Off
</MenuItem>
<MenuItem key="daily" value="daily">
{t("autostopRequirementDays_daily")}
Daily
</MenuItem>
<MenuItem key="saturday" value="saturday">
{t("autostopRequirementDays_saturday")}
Saturday
</MenuItem>
<MenuItem key="sunday" value="sunday">
{t("autostopRequirementDays_sunday")}
Sunday
</MenuItem>
</TextField>
@@ -405,7 +399,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
}
fullWidth
inputProps={{ min: 1, max: 16, step: 1 }}
label={t("autostopRequirementWeeksLabel")}
label="Weeks between required stops"
type="number"
/>
</Stack>
@@ -484,10 +478,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
<TextField
{...getFieldHelpers(
"failure_ttl_ms",
<TTLHelperText
translationName="failureTTLHelperText"
ttl={form.values.failure_ttl_ms}
/>,
<FailureTTLHelperText ttl={form.values.failure_ttl_ms} />,
)}
disabled={isSubmitting || !form.values.failure_cleanup_enabled}
fullWidth
@@ -515,8 +506,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
<TextField
{...getFieldHelpers(
"time_til_dormant_ms",
<TTLHelperText
translationName="dormancyThresholdHelperText"
<DormancyTTLHelperText
ttl={form.values.time_til_dormant_ms}
/>,
)}
@@ -548,8 +538,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
<TextField
{...getFieldHelpers(
"time_til_dormant_autodelete_ms",
<TTLHelperText
translationName="dormancyAutoDeletionHelperText"
<DormancyAutoDeletionTTLHelperText
ttl={form.values.time_til_dormant_autodelete_ms}
/>,
)}
@@ -12,9 +12,6 @@ import {
} from "testHelpers/renderHelpers";
import { TemplateScheduleFormValues, getValidationSchema } from "./formHelpers";
import TemplateSchedulePage from "./TemplateSchedulePage";
import i18next from "i18next";
const { t } = i18next;
const validFormValues: TemplateScheduleFormValues = {
default_ttl_ms: 1,
@@ -55,17 +52,15 @@ const fillAndSubmitForm = async ({
const user = userEvent.setup();
if (default_ttl_ms) {
const defaultTtlLabel = t("defaultTtlLabel", {
ns: "templateSettingsPage",
});
const defaultTtlField = await screen.findByLabelText(defaultTtlLabel);
const defaultTtlField = await screen.findByLabelText(
"Default autostop (hours)",
);
await user.clear(defaultTtlField);
await user.type(defaultTtlField, default_ttl_ms.toString());
}
if (max_ttl_ms) {
const maxTtlLabel = t("maxTtlLabel", { ns: "templateSettingsPage" });
const maxTtlField = await screen.findByLabelText(maxTtlLabel);
const maxTtlField = await screen.findByLabelText("Max lifetime (hours)");
await user.clear(maxTtlField);
await user.type(maxTtlField, max_ttl_ms.toString());
}
@@ -203,7 +198,7 @@ describe("TemplateSchedulePage", () => {
};
const validate = () => getValidationSchema().validateSync(values);
expect(validate).toThrowError(
t("defaultTTLMaxError", { ns: "templateSettingsPage" }).toString(),
"Please enter a limit that is less than or equal to 720 hours (30 days).",
);
});
@@ -1,6 +1,5 @@
import { UpdateTemplateMeta } from "api/typesGenerated";
import * as Yup from "yup";
import i18next from "i18next";
import { TemplateAutostopRequirementDaysValue } from "./AutostopRequirementHelperText";
export interface TemplateScheduleFormValues
@@ -18,27 +17,17 @@ export const getValidationSchema = (): Yup.AnyObjectSchema =>
Yup.object({
default_ttl_ms: Yup.number()
.integer()
.min(
0,
i18next
.t("defaultTTLMinError", { ns: "templateSettingsPage" })
.toString(),
)
.min(0, "Default time until autostop must not be less than 0.")
.max(
24 * MAX_TTL_DAYS /* 30 days in hours */,
i18next
.t("defaultTTLMaxError", { ns: "templateSettingsPage" })
.toString(),
"Please enter a limit that is less than or equal to 720 hours (30 days).",
),
max_ttl_ms: Yup.number()
.integer()
.min(
0,
i18next.t("maxTTLMinError", { ns: "templateSettingsPage" }).toString(),
)
.min(0, "Maximum time until autostop must not be less than 0.")
.max(
24 * MAX_TTL_DAYS /* 30 days in hours */,
i18next.t("maxTTLMaxError", { ns: "templateSettingsPage" }).toString(),
"Please enter a limit that is less than or equal to 720 hours (30 days).",
),
failure_ttl_ms: Yup.number()
.min(0, "Failure cleanup days must not be less than 0.")
@@ -4,11 +4,13 @@ import RadioGroup from "@mui/material/RadioGroup";
import TextField from "@mui/material/TextField";
import { TemplateVersionVariable } from "api/typesGenerated";
import { FC, useState } from "react";
import { useTranslation } from "react-i18next";
export const SensitiveVariableHelperText = () => {
const { t } = useTranslation("templateVariablesPage");
return <span>{t("sensitiveVariableHelperText")}</span>;
return (
<span>
This variable is sensitive. The previous value will be used if empty.
</span>
);
};
export interface TemplateVariableFieldProps {
@@ -8,7 +8,6 @@ import { FormikContextType, FormikTouched, useFormik } from "formik";
import { FC } from "react";
import { getFormHelpers } from "utils/formUtils";
import * as Yup from "yup";
import { useTranslation } from "react-i18next";
import {
FormFields,
FormSection,
@@ -64,12 +63,11 @@ export const TemplateVariablesForm: FC<TemplateVariablesForm> = ({
form,
error,
);
const { t } = useTranslation("templateVariablesPage");
return (
<HorizontalForm
onSubmit={form.handleSubmit}
aria-label={t("formAriaLabel").toString()}
aria-label="Template variables"
>
{templateVariables.map((templateVariable, index) => {
let fieldHelpers;
@@ -155,8 +153,6 @@ const ValidationSchemaForTemplateVariables = (
ns: string,
templateVariables: TemplateVersionVariable[],
): Yup.AnySchema => {
const { t } = useTranslation(ns);
return Yup.array()
.of(
Yup.object().shape({
@@ -175,7 +171,7 @@ const ValidationSchemaForTemplateVariables = (
if (!val || val.length === 0) {
return ctx.createError({
path: ctx.path,
message: t("validationRequiredVariable").toString(),
message: "Variable is required.",
});
}
}
@@ -5,7 +5,6 @@ import {
waitForLoaderToBeRemoved,
} from "testHelpers/renderHelpers";
import * as API from "api/api";
import i18next from "i18next";
import TemplateVariablesPage from "./TemplateVariablesPage";
import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter";
import {
@@ -17,18 +16,12 @@ import {
MockTemplateVersionVariable5,
} from "testHelpers/entities";
const { t } = i18next;
const validFormValues = {
first_variable: "Hello world",
second_variable: "123",
};
const pageTitleText = t("title", { ns: "templateVariablesPage" });
const validationRequiredField = t("validationRequiredVariable", {
ns: "templateVariablesPage",
});
const validationRequiredField = "Variable is required.";
const renderTemplateVariablesPage = async () => {
renderWithTemplateSettingsLayout(<TemplateVariablesPage />, {
@@ -54,9 +47,6 @@ describe("TemplateVariablesPage", () => {
await renderTemplateVariablesPage();
const element = await screen.findByText(pageTitleText);
expect(element).toBeDefined();
const firstVariable = await screen.findByLabelText(
MockTemplateVersionVariable1.name,
);
@@ -88,9 +78,6 @@ describe("TemplateVariablesPage", () => {
await renderTemplateVariablesPage();
const element = await screen.findByText(pageTitleText);
expect(element).toBeDefined();
const firstVariable = await screen.findByLabelText(
MockTemplateVersionVariable1.name,
);
@@ -144,9 +131,6 @@ describe("TemplateVariablesPage", () => {
await renderTemplateVariablesPage();
const element = await screen.findByText(pageTitleText);
expect(element).toBeDefined();
const firstVariable = await screen.findByLabelText(
MockTemplateVersionVariable1.name,
);
@@ -8,7 +8,6 @@ import { displaySuccess } from "components/GlobalSnackbar/utils";
import { useOrganizationId } from "hooks/useOrganizationId";
import { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import { templateVariablesMachine } from "xServices/template/templateVariablesXService";
import { pageTitle } from "../../../utils/page";
@@ -42,11 +41,10 @@ export const TemplateVariablesPage: FC = () => {
jobError,
} = state.context;
const { t } = useTranslation("templateVariablesPage");
return (
<>
<Helmet>
<title>{pageTitle([template.name, t("title")])}</title>
<title>{pageTitle([template.name, "Template variables"])}</title>
</Helmet>
<TemplateVariablesPageView
@@ -8,7 +8,6 @@ import { Loader } from "components/Loader/Loader";
import { ComponentProps, FC } from "react";
import { TemplateVariablesForm } from "./TemplateVariablesForm";
import { makeStyles } from "@mui/styles";
import { useTranslation } from "react-i18next";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
import { ErrorAlert } from "components/Alert/ErrorAlert";
@@ -43,12 +42,11 @@ export const TemplateVariablesPageView: FC<TemplateVariablesPageViewProps> = ({
!templateVariables &&
!errors.getTemplateDataError &&
!errors.updateTemplateError;
const { t } = useTranslation("templateVariablesPage");
const hasError = Object.values(errors).some((error) => Boolean(error));
return (
<>
<PageHeader className={classes.pageHeader}>
<PageHeaderTitle>{t("title")}</PageHeaderTitle>
<PageHeaderTitle>Template variables</PageHeaderTitle>
</PageHeader>
{hasError && (
<div className={classes.errorContainer}>
@@ -74,7 +72,9 @@ export const TemplateVariablesPageView: FC<TemplateVariablesPageViewProps> = ({
/>
)}
{templateVariables && templateVariables.length === 0 && (
<Alert severity="info">{t("unusedVariablesNotice")}</Alert>
<Alert severity="info">
This template does not use managed variables.
</Alert>
)}
</>
);
@@ -3,7 +3,6 @@ import { useOrganizationId } from "hooks/useOrganizationId";
import { useTab } from "hooks/useTab";
import { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
import { pageTitle } from "utils/page";
import { templateVersionMachine } from "xServices/templateVersion/templateVersionXService";
@@ -22,14 +21,11 @@ export const TemplateVersionPage: FC = () => {
context: { templateName, versionName, orgId },
});
const tab = useTab("file", "0");
const { t } = useTranslation("templateVersionPage");
return (
<>
<Helmet>
<title>
{pageTitle(`${t("title")} ${versionName} · ${templateName}`)}
</title>
<title>{pageTitle(`Version ${versionName} · ${templateName}`)}</title>
</Helmet>
<TemplateVersionPageView
@@ -14,7 +14,6 @@ import { Stats, StatsItem } from "components/Stats/Stats";
import { TemplateFiles } from "components/TemplateFiles/TemplateFiles";
import { UseTabResult } from "hooks/useTab";
import { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link as RouterLink } from "react-router-dom";
import { createDayString } from "utils/createDayString";
import { TemplateVersionMachineContext } from "xServices/templateVersion/templateVersionXService";
@@ -37,7 +36,6 @@ export const TemplateVersionPageView: FC<TemplateVersionPageViewProps> = ({
templateName,
}) => {
const { currentFiles, error, currentVersion, previousFiles } = context;
const { t } = useTranslation("templateVersionPage");
return (
<Margins>
@@ -51,7 +49,7 @@ export const TemplateVersionPageView: FC<TemplateVersionPageViewProps> = ({
</Link>
}
>
<PageHeaderCaption>{t("header.caption")}</PageHeaderCaption>
<PageHeaderCaption>Version</PageHeaderCaption>
<PageHeaderTitle>{versionName}</PageHeaderTitle>
{currentVersion &&
currentVersion.message &&
@@ -68,7 +66,7 @@ export const TemplateVersionPageView: FC<TemplateVersionPageViewProps> = ({
<>
<Stats>
<StatsItem
label={t("stats.template")}
label="Template"
value={
<RouterLink to={`/templates/${templateName}`}>
{templateName}
@@ -76,11 +74,11 @@ export const TemplateVersionPageView: FC<TemplateVersionPageViewProps> = ({
}
/>
<StatsItem
label={t("stats.createdBy")}
label="Created by"
value={currentVersion.created_by.username}
/>
<StatsItem
label={t("stats.created")}
label="Created"
value={createDayString(currentVersion.created_at)}
/>
</Stats>
@@ -7,7 +7,6 @@ import { Stack } from "components/Stack/Stack";
import { TableEmpty } from "components/TableEmpty/TableEmpty";
import { TemplateExampleCard } from "components/TemplateExampleCard/TemplateExampleCard";
import { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link as RouterLink } from "react-router-dom";
import { docs } from "utils/docs";
import { Permissions } from "xServices/auth/authXService";
@@ -42,13 +41,12 @@ export const EmptyTemplates: FC<{
examples: TemplateExample[];
}> = ({ permissions, examples }) => {
const styles = useStyles();
const { t } = useTranslation("templatesPage");
const featuredExamples = findFeaturedExamples(examples);
if (permissions.createTemplates) {
return (
<TableEmpty
message={t("empty.message")}
message="Create a Template"
description={
<>
Templates are written in Terraform and describe the infrastructure
@@ -93,8 +91,8 @@ export const EmptyTemplates: FC<{
return (
<TableEmpty
className={styles.withImage}
message={t("empty.message")}
description={t("empty.descriptionWithoutPermissions")}
message="Create a Template"
description="Contact your Coder administrator to create a template. You can share the code below."
cta={<CodeExample code="coder templates init" />}
image={
<div className={styles.emptyImage}>
@@ -4,11 +4,8 @@ import * as AccountForm from "./AccountForm";
import { renderWithAuth } from "testHelpers/renderHelpers";
import * as AuthXService from "xServices/auth/authXService";
import { AccountPage } from "./AccountPage";
import i18next from "i18next";
import { mockApiError } from "testHelpers/entities";
const { t } = i18next;
const renderPage = () => {
return renderWithAuth(<AccountPage />);
};
@@ -86,10 +83,7 @@ describe("AccountPage", () => {
const { user } = renderPage();
await fillAndSubmitForm();
const errorText = t("warningsAndErrors.somethingWentWrong", {
ns: "common",
});
const errorMessage = await screen.findByText(errorText);
const errorMessage = await screen.findByText("Something went wrong.");
expect(errorMessage).toBeDefined();
expect(API.updateProfile).toBeCalledTimes(1);
expect(API.updateProfile).toBeCalledWith(user.id, newData);
@@ -3,11 +3,8 @@ import * as API from "../../../api/api";
import { renderWithAuth } from "../../../testHelpers/renderHelpers";
import { Language as SSHKeysPageLanguage, SSHKeysPage } from "./SSHKeysPage";
import { Language as SSHKeysPageViewLanguage } from "./SSHKeysPageView";
import { i18n } from "i18n";
import { MockGitSSHKey, mockApiError } from "testHelpers/entities";
const { t } = i18n;
describe("SSH keys Page", () => {
it("shows the SSH key", async () => {
renderWithAuth(<SSHKeysPage />);
@@ -46,10 +43,7 @@ describe("SSH keys Page", () => {
fireEvent.click(confirmButton);
// Check if the success message is displayed
const successMessage = t("sshRegenerateSuccessMessage", {
ns: "userSettingsPage",
});
await screen.findByText(successMessage);
await screen.findByText("SSH Key regenerated successfully.");
// Check if the API was called correctly
expect(API.regenerateUserSSHKey).toBeCalledTimes(1);
@@ -6,7 +6,6 @@ import {
waitForLoaderToBeRemoved,
} from "../../../testHelpers/renderHelpers";
import { SecurityPage } from "./SecurityPage";
import i18next from "i18next";
import {
MockAuthMethodsWithPasswordType,
mockApiError,
@@ -15,8 +14,6 @@ import userEvent from "@testing-library/user-event";
import * as SSO from "./SingleSignOnSection";
import { OAuthConversionResponse } from "api/typesGenerated";
const { t } = i18next;
const renderPage = async () => {
const utils = renderWithAuth(<SecurityPage />);
await waitForLoaderToBeRemoved();
@@ -58,10 +55,7 @@ test("update password successfully", async () => {
const { user } = await renderPage();
fillAndSubmitSecurityForm();
const expectedMessage = t("securityUpdateSuccessMessage", {
ns: "userSettingsPage",
});
const successMessage = await screen.findByText(expectedMessage);
const successMessage = await screen.findByText("Updated password.");
expect(successMessage).toBeDefined();
expect(API.updateUserPassword).toBeCalledTimes(1);
expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues);
@@ -113,10 +107,7 @@ test("update password when submit returns an unknown error", async () => {
const { user } = await renderPage();
fillAndSubmitSecurityForm();
const errorText = t("warningsAndErrors.somethingWentWrong", {
ns: "common",
});
const errorMessage = await screen.findByText(errorText);
const errorMessage = await screen.findByText("Something went wrong.");
expect(errorMessage).toBeDefined();
expect(API.updateUserPassword).toBeCalledTimes(1);
expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues);
@@ -1,6 +1,5 @@
import { FC } from "react";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { useTranslation, Trans } from "react-i18next";
import { useDeleteToken } from "./hooks";
import { displaySuccess, displayError } from "components/GlobalSnackbar/utils";
import { getErrorMessage } from "api/errors";
@@ -17,31 +16,18 @@ export const ConfirmDeleteDialog: FC<ConfirmDeleteDialogProps> = ({
token,
setToken,
}) => {
const { t } = useTranslation("tokensPage");
const tokenName = token?.token_name;
const description = (
<Trans
t={t}
i18nKey="tokenActions.deleteToken.deleteCaption"
values={{ tokenName }}
>
Are you sure you want to permanently delete token {{ tokenName }}?
</Trans>
);
const { mutate: deleteToken, isLoading: isDeleting } =
useDeleteToken(queryKey);
const onDeleteSuccess = () => {
displaySuccess(t("tokenActions.deleteToken.deleteSuccess"));
displaySuccess("Token has been deleted");
setToken(undefined);
};
const onDeleteError = (error: unknown) => {
const message = getErrorMessage(
error,
t("tokenActions.deleteToken.deleteFailure"),
);
const message = getErrorMessage(error, "Failed to delete token");
displayError(message);
setToken(undefined);
};
@@ -49,8 +35,13 @@ export const ConfirmDeleteDialog: FC<ConfirmDeleteDialogProps> = ({
return (
<ConfirmDialog
type="delete"
title={t("tokenActions.deleteToken.delete")}
description={description}
title="Delete Token"
description={
<>
Are you sure you want to permanently delete token{" "}
<strong>{tokenName}</strong>?
</>
}
open={Boolean(token) || isDeleting}
confirmLoading={isDeleting}
onConfirm={() => {
@@ -2,7 +2,6 @@ import { FC, PropsWithChildren, useState } from "react";
import { Section } from "components/SettingsLayout/Section";
import { TokensPageView } from "./TokensPageView";
import makeStyles from "@mui/styles/makeStyles";
import { useTranslation } from "react-i18next";
import { useTokensData } from "./hooks";
import { ConfirmDeleteDialog } from "./ConfirmDeleteDialog";
import { Stack } from "components/Stack/Stack";
@@ -13,14 +12,13 @@ import { APIKeyWithOwner } from "api/typesGenerated";
export const TokensPage: FC<PropsWithChildren<unknown>> = () => {
const styles = useStyles();
const { t } = useTranslation("tokensPage");
const cliCreateCommand = "coder tokens create";
const TokenActions = () => (
<Stack direction="row" justifyContent="end" className={styles.tokenActions}>
<Button startIcon={<AddIcon />} component={RouterLink} to="new">
{t("tokenActions.addToken")}
Add token
</Button>
</Stack>
);
@@ -44,7 +42,7 @@ export const TokensPage: FC<PropsWithChildren<unknown>> = () => {
return (
<>
<Section
title={t("title")}
title="Tokens"
className={styles.section}
description={
<>
@@ -13,7 +13,6 @@ import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import dayjs from "dayjs";
import { FC } from "react";
import IconButton from "@mui/material/IconButton/IconButton";
import { useTranslation } from "react-i18next";
import { APIKeyWithOwner } from "api/typesGenerated";
import relativeTime from "dayjs/plugin/relativeTime";
import { ErrorAlert } from "components/Alert/ErrorAlert";
@@ -46,7 +45,6 @@ export const TokensPageView: FC<
deleteTokenError,
}) => {
const theme = useTheme();
const { t } = useTranslation("tokensPage");
return (
<Stack>
@@ -56,11 +54,11 @@ export const TokensPageView: FC<
<Table>
<TableHead>
<TableRow>
<TableCell width="20%">{t("table.id")}</TableCell>
<TableCell width="20%">{t("table.name")}</TableCell>
<TableCell width="20%">{t("table.lastUsed")}</TableCell>
<TableCell width="20%">{t("table.expiresAt")}</TableCell>
<TableCell width="20%">{t("table.createdAt")}</TableCell>
<TableCell width="20%">ID</TableCell>
<TableCell width="20%">Name</TableCell>
<TableCell width="20%">Last Used</TableCell>
<TableCell width="20%">Expires At</TableCell>
<TableCell width="20%">Created At</TableCell>
<TableCell width="0%"></TableCell>
</TableRow>
</TableHead>
@@ -70,7 +68,7 @@ export const TokensPageView: FC<
<TableLoader />
</Cond>
<Cond condition={hasLoaded && tokens?.length === 0}>
<TableEmpty message={t("emptyState")} />
<TableEmpty message="No tokens found" />
</Cond>
<Cond>
{tokens?.map((token) => {
@@ -116,9 +114,7 @@ export const TokensPageView: FC<
onDelete(token);
}}
size="medium"
aria-label={t(
"tokenActions.deleteToken.delete",
).toString()}
aria-label="Delete token"
>
<DeleteOutlineIcon />
</IconButton>
+8 -25
View File
@@ -1,6 +1,5 @@
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { i18n } from "i18n";
import { rest } from "msw";
import {
MockUser,
@@ -17,8 +16,6 @@ import { renderWithAuth } from "../../testHelpers/renderHelpers";
import { server } from "../../testHelpers/server";
import { Language as UsersPageLanguage, UsersPage } from "./UsersPage";
const { t } = i18n;
const renderPage = () => {
return renderWithAuth(<UsersPage />);
};
@@ -32,8 +29,7 @@ const suspendUser = async (setupActionSpies: () => void) => {
await user.click(firstMoreButton);
const menu = await screen.findByRole("menu");
const text = t("suspendMenuItem", { ns: "usersPage" });
const suspendButton = within(menu).getByText(text);
const suspendButton = within(menu).getByText("Suspend");
await user.click(suspendButton);
@@ -64,27 +60,18 @@ const deleteUser = async (setupActionSpies: () => void) => {
await user.click(selectedMoreButton);
const menu = await screen.findByRole("menu");
const text = t("deleteMenuItem", { ns: "usersPage" });
const deleteButton = within(menu).getByText(text);
const deleteButton = within(menu).getByText("Delete");
await user.click(deleteButton);
// Check if the confirm message is displayed
const confirmDialog = await screen.findByRole("dialog");
expect(confirmDialog).toHaveTextContent(
t("deleteDialog.confirm", {
ns: "common",
entity: "user",
name: MockUser2.username,
}).toString(),
`Type ${MockUser2.username} below to confirm.`,
);
// Confirm with text input
const labelText = t("deleteDialog.confirmLabel", {
ns: "common",
entity: "user",
});
const textField = screen.getByLabelText(labelText);
const textField = screen.getByLabelText("Name of the user to delete");
const dialog = screen.getByRole("dialog");
await user.type(textField, MockUser2.username);
@@ -102,8 +89,7 @@ const activateUser = async (setupActionSpies: () => void) => {
fireEvent.click(suspendedMoreButton);
const menu = screen.getByRole("menu");
const text = t("activateMenuItem", { ns: "usersPage" });
const activateButton = within(menu).getByText(text);
const activateButton = within(menu).getByText("Activate");
fireEvent.click(activateButton);
// Check if the confirm message is displayed
@@ -129,8 +115,7 @@ const resetUserPassword = async (setupActionSpies: () => void) => {
fireEvent.click(firstMoreButton);
const menu = screen.getByRole("menu");
const text = t("resetPasswordMenuItem", { ns: "usersPage" });
const resetPasswordButton = within(menu).getByText(text);
const resetPasswordButton = within(menu).getByText("Reset password");
fireEvent.click(resetPasswordButton);
@@ -160,16 +145,14 @@ const updateUserRole = async (setupActionSpies: () => void, role: Role) => {
}
// Click on the "edit icon" to display the role options
const buttonTitle = t("editUserRolesTooltip", { ns: "usersPage" });
const editButton = within(userRow).getByTitle(buttonTitle);
const editButton = within(userRow).getByTitle("Edit user roles");
fireEvent.click(editButton);
// Setup spies to check the actions after
setupActionSpies();
// Click on the role option
const fieldsetTitle = t("fieldSetRolesTooltip", { ns: "usersPage" });
const fieldset = await screen.findByTitle(fieldsetTitle);
const fieldset = await screen.findByTitle("Available roles");
const auditorOption = within(fieldset).getByText(role.display_name);
fireEvent.click(auditorOption);
@@ -2,7 +2,6 @@ import IconButton from "@mui/material/IconButton";
import { EditSquare } from "components/Icons/EditSquare";
import { useRef, useState, FC } from "react";
import { makeStyles } from "@mui/styles";
import { useTranslation } from "react-i18next";
import Popover from "@mui/material/Popover";
import { Stack } from "components/Stack/Stack";
import Checkbox from "@mui/material/Checkbox";
@@ -15,6 +14,16 @@ import {
} from "components/HelpTooltip/HelpTooltip";
import { Maybe } from "components/Conditionals/Maybe";
const roleDescriptions: Record<string, string> = {
owner:
"Owner can manage all resources, including users, groups, templates, and workspaces.",
"user-admin": "User admin can manage all users and groups.",
"template-admin": "Template admin can manage all templates and workspaces.",
auditor: "Auditor can access the audit logs.",
member:
"Everybody is a member. This is a shared and default role for all users.",
};
const Option: React.FC<{
value: string;
name: string;
@@ -66,7 +75,6 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
oidcRoleSync,
}) => {
const styles = useStyles();
const { t } = useTranslation("usersPage");
const anchorRef = useRef<HTMLButtonElement>(null);
const [isOpen, setIsOpen] = useState(defaultIsOpen);
const id = isOpen ? "edit-roles-popover" : undefined;
@@ -91,7 +99,7 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
ref={anchorRef}
size="small"
className={styles.editButton}
title={t("editUserRolesTooltip") || ""}
title="Edit user roles"
onClick={() => setIsOpen(true)}
>
<EditSquare />
@@ -124,7 +132,7 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
<fieldset
className={styles.fieldset}
disabled={isLoading}
title={t("fieldSetRolesTooltip") || ""}
title="Available roles"
>
<Stack className={styles.options} spacing={3}>
{roles.map((role) => (
@@ -134,7 +142,7 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
isChecked={selectedRoleNames.includes(role.name)}
value={role.name}
name={role.display_name}
description={t(`roleDescription.${role.name}`)}
description={roleDescriptions[role.name] ?? ""}
/>
))}
</Stack>
@@ -143,9 +151,9 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
<Stack direction="row" alignItems="flex-start">
<UserIcon className={styles.userIcon} />
<Stack spacing={0}>
<strong>{t("member")}</strong>
<strong>Member</strong>
<span className={styles.optionDescription}>
{t("roleDescription.member")}
{roleDescriptions.member}
</span>
</Stack>
</Stack>
@@ -4,8 +4,7 @@ import TableCell from "@mui/material/TableCell";
import TableRow from "@mui/material/TableRow";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { Pill } from "components/Pill/Pill";
import { FC } from "react";
import { useTranslation } from "react-i18next";
import { FC, ReactNode } from "react";
import * as TypesGen from "../../../api/typesGenerated";
import { combineClasses } from "../../../utils/combineClasses";
import { AvatarData } from "../../../components/AvatarData/AvatarData";
@@ -91,7 +90,6 @@ export const UsersTableBody: FC<
oidcRoleSyncEnabled,
}) => {
const styles = useStyles();
const { t } = useTranslation("usersPage");
return (
<ChooseOne>
@@ -126,7 +124,7 @@ export const UsersTableBody: FC<
<TableRow>
<TableCell colSpan={999}>
<Box p={4}>
<EmptyState message={t("emptyPageMessage")} />
<EmptyState message="No users found on this page" />
</Box>
</TableCell>
</TableRow>
@@ -135,7 +133,7 @@ export const UsersTableBody: FC<
<TableRow>
<TableCell colSpan={999}>
<Box p={4}>
<EmptyState message={t("emptyMessage")} />
<EmptyState message="No users found" />
</Box>
</TableCell>
</TableRow>
@@ -222,35 +220,31 @@ export const UsersTableBody: FC<
(user.status === "active" || user.status === "dormant"
? [
{
label: t(
"suspendMenuItem",
) as React.ReactNode,
label: "Suspend" as ReactNode,
onClick: onSuspendUser,
disabled: false,
},
]
: [
{
label: t(
"activateMenuItem",
) as React.ReactNode,
label: "Activate" as ReactNode,
onClick: onActivateUser,
disabled: false,
},
]
).concat(
{
label: t("deleteMenuItem"),
label: "Delete",
onClick: onDeleteUser,
disabled: user.id === actorID,
},
{
label: t("resetPasswordMenuItem"),
label: "Reset password",
onClick: onResetUserPassword,
disabled: user.login_type !== "password",
},
{
label: t("listWorkspacesMenuItem"),
label: "View workspaces",
onClick: onListWorkspaces,
disabled: false,
},
+12 -11
View File
@@ -4,7 +4,6 @@ import { WorkspaceBuild } from "api/typesGenerated";
import { Stack } from "components/Stack/Stack";
import { TimelineEntry } from "components/Timeline/TimelineEntry";
import { useClickable } from "hooks/useClickable";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import {
@@ -17,9 +16,14 @@ export interface BuildRowProps {
build: WorkspaceBuild;
}
const transitionMessages = {
start: "started",
stop: "stopped",
delete: "deleted",
};
export const BuildRow: React.FC<BuildRowProps> = ({ build }) => {
const styles = useStyles();
const { t } = useTranslation("workspacePage");
const initiatedBy = getDisplayWorkspaceBuildInitiatedBy(build);
const navigate = useNavigate();
const clickableProps = useClickable(() =>
@@ -54,11 +58,9 @@ export const BuildRow: React.FC<BuildRowProps> = ({ build }) => {
>
<span>
<strong>{initiatedBy}</strong>{" "}
{build.reason !== "initiator"
? t("buildMessage.automatically")
: ""}
<strong>{t(`buildMessage.${build.transition}`)}</strong>{" "}
{t("buildMessage.theWorkspace")}
{build.reason !== "initiator" ? "automatically" : ""}
<strong>{transitionMessages[build.transition]}</strong> the
workspace
</span>
<span className={styles.buildTime}>
@@ -68,17 +70,16 @@ export const BuildRow: React.FC<BuildRowProps> = ({ build }) => {
<Stack direction="row" spacing={1}>
<span className={styles.buildInfo}>
{t("buildData.reason")}: <strong>{build.reason}</strong>
Reason: <strong>{build.reason}</strong>
</span>
<span className={styles.buildInfo}>
{t("buildData.duration")}:{" "}
Duration:{" "}
<strong>{displayWorkspaceBuildDuration(build)}</strong>
</span>
<span className={styles.buildInfo}>
{t("buildData.version")}:{" "}
<strong>{build.template_version_name}</strong>
Version: <strong>{build.template_version_name}</strong>
</span>
</Stack>
</Stack>
@@ -99,8 +99,6 @@ export const ChangeVersionDialog: FC<ChangeVersionDialogProps> = ({
)}
renderInput={(params) => (
<>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment -- Need it */}
{/* @ts-ignore -- Issue from lib https://github.com/i18next/react-i18next/issues/1543 */}
<TextField
{...params}
fullWidth
@@ -20,7 +20,6 @@ import {
import * as Yup from "yup";
import DialogActions from "@mui/material/DialogActions";
import Button from "@mui/material/Button";
import { useTranslation } from "react-i18next";
export type UpdateBuildParametersDialogProps = DialogProps & {
onClose: () => void;
@@ -37,10 +36,8 @@ export const UpdateBuildParametersDialog: FC<
rich_parameter_values: getInitialRichParameterValues(missedParameters),
},
validationSchema: Yup.object({
rich_parameter_values: useValidationSchemaForRichParameters(
"createWorkspacePage",
missedParameters,
),
rich_parameter_values:
useValidationSchemaForRichParameters(missedParameters),
}),
onSubmit: (values) => {
onUpdate(values.rich_parameter_values);
@@ -48,7 +45,6 @@ export const UpdateBuildParametersDialog: FC<
enableReinitialize: true,
});
const getFieldHelpers = getFormHelpers(form);
const { t } = useTranslation("workspacePage");
return (
<Dialog
@@ -66,7 +62,8 @@ export const UpdateBuildParametersDialog: FC<
</DialogTitle>
<DialogContent className={styles.content}>
<DialogContentText className={styles.info}>
{t("askParametersDialog.message")}
This template has new parameters that must be configured to complete
the update
</DialogContentText>
<VerticalForm
className={styles.form}
+1 -3
View File
@@ -7,7 +7,6 @@ import {
WorkspaceBuildProgress,
} from "./WorkspaceBuildProgress";
import { FC, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import * as TypesGen from "api/typesGenerated";
import { Alert, AlertDetail } from "components/Alert/Alert";
@@ -110,7 +109,6 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
const styles = useStyles();
const navigate = useNavigate();
const serverVersion = buildInfo?.version || "";
const { t } = useTranslation("workspacePage");
const { saveLocal, getLocal } = useLocalStorage();
const buildError = Boolean(workspaceErrors[WorkspaceErrors.BUILD_ERROR]) && (
@@ -301,7 +299,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
variant="text"
size="small"
>
{t("actionButton.retryDebugMode")}
Try in debug mode
</Button>
)
}
@@ -1,7 +1,6 @@
import Button from "@mui/material/Button";
import { FC } from "react";
import { Alert } from "components/Alert/Alert";
import { useTranslation } from "react-i18next";
export interface WorkspaceDeletedBannerProps {
handleClick: () => void;
@@ -10,17 +9,15 @@ export interface WorkspaceDeletedBannerProps {
export const WorkspaceDeletedBanner: FC<
React.PropsWithChildren<WorkspaceDeletedBannerProps>
> = ({ handleClick }) => {
const { t } = useTranslation("workspacePage");
const NewWorkspaceButton = (
<Button onClick={handleClick} size="small" variant="text">
{t("ctas.createWorkspaceCta")}
Create new workspace
</Button>
);
return (
<Alert severity="warning" actions={NewWorkspaceButton}>
{t("warningsAndErrors.workspaceDeletedWarning")}
This workspace has been deleted and cannot be edited.
</Alert>
);
};
@@ -1,7 +1,6 @@
import { screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import EventSourceMock from "eventsourcemock";
import i18next from "i18next";
import { rest } from "msw";
import {
MockTemplate,
@@ -34,8 +33,6 @@ import {
import { server } from "../../testHelpers/server";
import { WorkspacePage } from "./WorkspacePage";
const { t } = i18next;
// It renders the workspace page and waits for it be loaded
const renderWorkspacePage = async () => {
jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate);
@@ -114,18 +111,14 @@ describe("WorkspacePage", () => {
// open the workspace action popover so we have access to all available ctas
const trigger = screen.getByTestId("workspace-options-button");
await user.click(trigger);
const buttonText = t("actionButton.delete", { ns: "workspacePage" });
// Click on delete
const button = await screen.findByText(buttonText);
const button = await screen.findByText("Delete");
await user.click(button);
// Get dialog and confirm
const dialog = await screen.findByTestId("dialog");
const labelText = t("deleteDialog.confirmLabel", {
ns: "common",
entity: "workspace",
});
const labelText = "Name of the workspace to delete";
const textField = within(dialog).getByLabelText(labelText);
await user.type(textField, MockWorkspace.name);
const confirmButton = within(dialog).getByRole("button", {
@@ -148,10 +141,7 @@ describe("WorkspacePage", () => {
const startWorkspaceMock = jest
.spyOn(api, "startWorkspace")
.mockImplementation(() => Promise.resolve(MockWorkspaceBuild));
await testButton(
t("actionButton.start", { ns: "workspacePage" }),
startWorkspaceMock,
);
await testButton("Start", startWorkspaceMock);
});
it("requests a stop job when the user presses Stop", async () => {
@@ -159,10 +149,7 @@ describe("WorkspacePage", () => {
.spyOn(api, "stopWorkspace")
.mockResolvedValueOnce(MockWorkspaceBuild);
await testButton(
t("actionButton.stop", { ns: "workspacePage" }),
stopWorkspaceMock,
);
await testButton("Stop", stopWorkspaceMock);
});
it("requests a stop when the user presses Restart", async () => {
@@ -319,66 +306,39 @@ describe("WorkspacePage", () => {
});
it("shows the Stopping status when the workspace is stopping", async () => {
await testStatus(
MockStoppingWorkspace,
t("workspaceStatus.stopping", { ns: "common" }),
);
await testStatus(MockStoppingWorkspace, "Stopping");
});
it("shows the Stopped status when the workspace is stopped", async () => {
await testStatus(
MockStoppedWorkspace,
t("workspaceStatus.stopped", { ns: "common" }),
);
await testStatus(MockStoppedWorkspace, "Stopped");
});
it("shows the Building status when the workspace is starting", async () => {
await testStatus(
MockStartingWorkspace,
t("workspaceStatus.starting", { ns: "common" }),
);
await testStatus(MockStartingWorkspace, "Starting");
});
it("shows the Running status when the workspace is running", async () => {
await testStatus(
MockWorkspace,
t("workspaceStatus.running", { ns: "common" }),
);
await testStatus(MockWorkspace, "Running");
});
it("shows the Failed status when the workspace is failed or canceled", async () => {
await testStatus(
MockFailedWorkspace,
t("workspaceStatus.failed", { ns: "common" }),
);
await testStatus(MockFailedWorkspace, "Failed");
});
it("shows the Canceling status when the workspace is canceling", async () => {
await testStatus(
MockCancelingWorkspace,
t("workspaceStatus.canceling", { ns: "common" }),
);
await testStatus(MockCancelingWorkspace, "Canceling");
});
it("shows the Canceled status when the workspace is canceling", async () => {
await testStatus(
MockCanceledWorkspace,
t("workspaceStatus.canceled", { ns: "common" }),
);
await testStatus(MockCanceledWorkspace, "Canceled");
});
it("shows the Deleting status when the workspace is deleting", async () => {
await testStatus(
MockDeletingWorkspace,
t("workspaceStatus.deleting", { ns: "common" }),
);
await testStatus(MockDeletingWorkspace, "Deleting");
});
it("shows the Deleted status when the workspace is deleted", async () => {
await testStatus(
MockDeletedWorkspace,
t("workspaceStatus.deleted", { ns: "common" }),
);
await testStatus(MockDeletedWorkspace, "Deleted");
});
it("shows the Impending deletion status when the workspace is impending deletion", async () => {
@@ -4,7 +4,6 @@ import dayjs from "dayjs";
import { useFeatureVisibility } from "hooks/useFeatureVisibility";
import { FC, useEffect, useState } from "react";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import {
getDeadline,
@@ -75,7 +74,6 @@ export const WorkspaceReadyPage = ({
const canRetryDebugMode =
Boolean(permissions?.viewDeploymentValues) &&
Boolean(deploymentValues?.enable_terraform_debug_mode);
const { t } = useTranslation("workspacePage");
const favicon = getFaviconByStatus(workspace.latest_build);
const navigate = useNavigate();
const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false);
@@ -199,9 +197,9 @@ export const WorkspaceReadyPage = ({
<DeleteDialog
entity="workspace"
name={workspace.name}
info={t("deleteDialog.info", {
timeAgo: dayjs(workspace.created_at).fromNow(),
}).toString()}
info={`This workspace was created ${dayjs(
workspace.created_at,
).fromNow()}.`}
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}
onCancel={() => workspaceSend({ type: "CANCEL_DELETE" })}
onConfirm={() => {
@@ -7,7 +7,6 @@ import {
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
import { useFormik } from "formik";
import { FC } from "react";
import { useTranslation } from "react-i18next";
import {
getInitialRichParameterValues,
useValidationSchemaForRichParameters,
@@ -38,8 +37,6 @@ export const WorkspaceParametersForm: FC<{
error,
isSubmitting,
}) => {
const { t } = useTranslation("workspaceSettingsPage");
const form = useFormik<WorkspaceParametersFormValues>({
onSubmit,
initialValues: {
@@ -50,7 +47,6 @@ export const WorkspaceParametersForm: FC<{
},
validationSchema: Yup.object({
rich_parameter_values: useValidationSchemaForRichParameters(
"createWorkspacePage",
templateVersionRichParameters,
),
}),
@@ -73,8 +69,8 @@ export const WorkspaceParametersForm: FC<{
<HorizontalForm onSubmit={form.handleSubmit} data-testid="form">
{hasNonEphemeralParameters && (
<FormSection
title={t("parameters").toString()}
description={t("parametersDescription").toString()}
title="Parameters"
description="Settings used by your template"
>
<FormFields>
{templateVersionRichParameters.map((parameter, index) =>
@@ -135,9 +131,8 @@ export const WorkspaceParametersForm: FC<{
title="Immutable parameters"
description={
<>
These parameters are also provided by your Terraform configuration
but they{" "}
<strong>cannot be changed after creating the workspace.</strong>
These settings <strong>cannot be changed</strong> after creating
the workspace.
</>
}
>
@@ -19,13 +19,10 @@ import {
Language as FormLanguage,
} from "./WorkspaceScheduleForm";
import { WorkspaceSchedulePage } from "./WorkspaceSchedulePage";
import i18next from "i18next";
import { server } from "testHelpers/server";
import { rest } from "msw";
import { MockUser, MockWorkspace } from "testHelpers/entities";
const { t } = i18next;
const validValues: WorkspaceScheduleFormValues = {
autostartEnabled: true,
sunday: false,
@@ -306,8 +303,7 @@ describe("WorkspaceSchedulePage", () => {
name: /submit/i,
});
await user.click(submitButton);
const title = t("dialogTitle", { ns: "workspaceSchedulePage" });
const dialog = await screen.findByText(title);
const dialog = await screen.findByText("Restart workspace?");
expect(dialog).toBeInTheDocument();
});
@@ -328,8 +324,7 @@ describe("WorkspaceSchedulePage", () => {
name: /submit/i,
});
await user.click(submitButton);
const title = t("dialogTitle", { ns: "workspaceSchedulePage" });
const dialog = screen.queryByText(title);
const dialog = screen.queryByText("Restart workspace?");
expect(dialog).not.toBeInTheDocument();
});
});
@@ -13,7 +13,6 @@ import { ttlMsToAutostop } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePa
import { useWorkspaceSettingsContext } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout";
import { FC } from "react";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
import { Navigate, useNavigate, useParams } from "react-router-dom";
import { pageTitle } from "utils/page";
import * as TypesGen from "api/typesGenerated";
@@ -40,7 +39,6 @@ const useStyles = makeStyles((theme) => ({
}));
export const WorkspaceSchedulePage: FC = () => {
const { t } = useTranslation("workspaceSchedulePage");
const styles = useStyles();
const params = useParams() as { username: string; workspace: string };
const navigate = useNavigate();
@@ -79,7 +77,10 @@ export const WorkspaceSchedulePage: FC = () => {
<ErrorAlert error={checkPermissionsError || getTemplateError} />
)}
{permissions && !permissions.updateWorkspace && (
<Alert severity="error">{t("forbiddenError")}</Alert>
<Alert severity="error">
You don&apos;t have permissions to update the schedule for this
workspace.
</Alert>
)}
{template &&
workspace &&
@@ -115,10 +116,10 @@ export const WorkspaceSchedulePage: FC = () => {
)}
<ConfirmDialog
open={scheduleState.matches("showingRestartDialog")}
title={t("dialogTitle")}
description={t("dialogDescription")}
confirmText={t("restart")}
cancelText={t("applyLater").toString()}
title="Restart workspace?"
description="Would you like to restart your workspace now to apply your new autostop setting, or let it apply after your next workspace start?"
confirmText="Restart"
cancelText="Apply later"
hideCancel={false}
onConfirm={() => {
scheduleSend("RESTART_WORKSPACE");

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