mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: remove i18next (#9608)
This commit is contained in:
@@ -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 = [
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
-43
@@ -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'}
|
||||
|
||||
Vendored
-10
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"updateLabel": "Update",
|
||||
"messageHelperText": "Markdown bold, italics, and links are supported.",
|
||||
"showPreviewLabel": "Show Preview"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"stats": {
|
||||
"workspace": "Workspace",
|
||||
"duration": "Duration",
|
||||
"startedAt": "Started at",
|
||||
"action": "Action"
|
||||
}
|
||||
}
|
||||
@@ -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..."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}})."
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"title": "Version",
|
||||
"header": {
|
||||
"caption": "Version"
|
||||
},
|
||||
"stats": {
|
||||
"template": "Template",
|
||||
"createdBy": "Created by",
|
||||
"created": "Created"
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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:"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"securityUpdateSuccessMessage": "Updated password.",
|
||||
"sshRegenerateSuccessMessage": "SSH Key regenerated successfully."
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 +0,0 @@
|
||||
export * from "./i18n";
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+11
-32
@@ -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>
|
||||
|
||||
@@ -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,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'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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
+2
-4
@@ -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)}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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,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,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}>
|
||||
|
||||
+19
-27
@@ -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't
|
||||
recommended for most use cases.
|
||||
</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
+5
-17
@@ -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.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+1
-3
@@ -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
-3
@@ -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
|
||||
|
||||
+30
-19
@@ -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'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'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}
|
||||
/>,
|
||||
)}
|
||||
|
||||
+5
-10
@@ -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.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
-17
@@ -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
|
||||
|
||||
+4
-4
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
+4
-9
@@ -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.
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
+2
-7
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+8
-7
@@ -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'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
Reference in New Issue
Block a user