mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor(site): replace MUI Stack, TextField, and Link with shared components (#27817)
> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell.
Continue the MUI → Tailwind/shadcn migration across a few auth and
settings surfaces.
- Create token form: MUI `TextField`/`MenuItem` → `FormField`, `Select`,
and `Input` (drops Emotion for section min-width)
- Create organization form: `TextField` → `FormField` / `Textarea`,
matching the organization info form
- SSO security section and external auth: MUI `Link`/`TextField` →
shared `Link`, `Input`, and `Label`
- Permission and IdP pill lists: MUI `Stack` → `flex flex-row gap-2`
- Minor link layout polish on Git device auth / external auth pages
---
_Also removes the now-unused `@emotion/css` dependency from
`site/package.json` (and the lockfile), which was the last usage of it.
This fixes the `knip` CI lint failure ("Unused dependencies:
@emotion/css")._
This commit is contained in:
@@ -43,7 +43,6 @@
|
||||
"@emoji-mart/data": "1.2.1",
|
||||
"@emoji-mart/react": "1.1.1",
|
||||
"@emotion/cache": "11.14.0",
|
||||
"@emotion/css": "11.13.5",
|
||||
"@emotion/react": "11.14.0",
|
||||
"@emotion/styled": "11.14.1",
|
||||
"@fontsource-variable/geist": "5.3.0",
|
||||
|
||||
Generated
-16
@@ -50,9 +50,6 @@ importers:
|
||||
'@emotion/cache':
|
||||
specifier: 11.14.0
|
||||
version: 11.14.0
|
||||
'@emotion/css':
|
||||
specifier: 11.13.5
|
||||
version: 11.13.5
|
||||
'@emotion/react':
|
||||
specifier: 11.14.0
|
||||
version: 11.14.0(@types/react@19.2.17)(react@19.2.8)
|
||||
@@ -774,9 +771,6 @@ packages:
|
||||
'@emotion/cache@11.14.0':
|
||||
resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==, tarball: https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz}
|
||||
|
||||
'@emotion/css@11.13.5':
|
||||
resolution: {integrity: sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==, tarball: https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz}
|
||||
|
||||
'@emotion/hash@0.9.2':
|
||||
resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==, tarball: https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz}
|
||||
|
||||
@@ -6914,16 +6908,6 @@ snapshots:
|
||||
'@emotion/weak-memoize': 0.4.0
|
||||
stylis: 4.2.0
|
||||
|
||||
'@emotion/css@11.13.5':
|
||||
dependencies:
|
||||
'@emotion/babel-plugin': 11.13.5
|
||||
'@emotion/cache': 11.14.0
|
||||
'@emotion/serialize': 1.3.3
|
||||
'@emotion/sheet': 1.4.0
|
||||
'@emotion/utils': 1.4.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@emotion/hash@0.9.2': {}
|
||||
|
||||
'@emotion/is-prop-valid@1.4.0':
|
||||
|
||||
@@ -146,9 +146,9 @@ export const GitDeviceAuth: FC<GitDeviceAuthProps> = ({
|
||||
<br />
|
||||
Then open the link below and paste it:
|
||||
</p>
|
||||
<div className="m-4 flex flex-col gap-1">
|
||||
<div className="m-4 flex justify-center">
|
||||
<Link
|
||||
className="flex items-center justify-center gap-2 text-base"
|
||||
className="inline-flex items-center gap-2 p-0 text-base font-medium [&_svg]:size-icon-xs [&_svg]:p-0"
|
||||
href={externalAuthDevice.verification_uri}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { css } from "@emotion/css";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import type { FormikContextType } from "formik";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import { type FC, useEffect, useId, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import {
|
||||
@@ -13,6 +10,16 @@ import {
|
||||
FormSection,
|
||||
HorizontalForm,
|
||||
} from "#/components/Form/Form";
|
||||
import { FormField } from "#/components/FormField/FormField";
|
||||
import { Input } from "#/components/Input/Input";
|
||||
import { Label } from "#/components/Label/Label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "#/components/Select/Select";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import { getFormHelpers, onChangeTrimmed } from "#/utils/formUtils";
|
||||
import {
|
||||
@@ -45,6 +52,8 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
|
||||
now,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const lifetimeId = useId();
|
||||
const expiresOnId = useId();
|
||||
|
||||
const [expDays, setExpDays] = useState<number>(1);
|
||||
const [lifetimeDays, setLifetimeDays] = useState<number | string>(
|
||||
@@ -68,16 +77,16 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
|
||||
<FormSection
|
||||
title="Name"
|
||||
description="What is this token for?"
|
||||
classes={{ sectionInfo: classNames.sectionInfo }}
|
||||
classes={{ sectionInfo: "min-w-[300px]" }}
|
||||
>
|
||||
<FormFields>
|
||||
<TextField
|
||||
{...getFieldHelpers("name")}
|
||||
<FormField
|
||||
field={getFieldHelpers("name")}
|
||||
label="Name"
|
||||
required
|
||||
onChange={onChangeTrimmed(form, () => setFormError(undefined))}
|
||||
autoFocus
|
||||
fullWidth
|
||||
className="w-full"
|
||||
/>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
@@ -98,59 +107,69 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
|
||||
"Please set a token expiration."
|
||||
)
|
||||
}
|
||||
classes={{ sectionInfo: classNames.sectionInfo }}
|
||||
classes={{ sectionInfo: "min-w-[300px]" }}
|
||||
>
|
||||
<FormFields>
|
||||
<div className="flex flex-row gap-4">
|
||||
<TextField
|
||||
select
|
||||
label="Lifetime"
|
||||
required
|
||||
defaultValue={determineDefaultLtValue(maxTokenLifetime)}
|
||||
onChange={(event) => {
|
||||
void setLifetimeDays(event.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
{filterByMaxTokenLifetime(maxTokenLifetime).map((lt) => (
|
||||
<MenuItem key={lt.label} value={lt.value}>
|
||||
{lt.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem
|
||||
key={customLifetimeDay.label}
|
||||
value={customLifetimeDay.value}
|
||||
<div className="flex flex-col gap-2 flex-1">
|
||||
<Label htmlFor={lifetimeId}>
|
||||
Lifetime{" "}
|
||||
<span className="text-xs font-bold text-content-destructive">
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={String(lifetimeDays)}
|
||||
onValueChange={setLifetimeDays}
|
||||
>
|
||||
{customLifetimeDay.label}
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
<SelectTrigger id={lifetimeId} className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{filterByMaxTokenLifetime(maxTokenLifetime).map((lt) => (
|
||||
<SelectItem key={lt.label} value={String(lt.value)}>
|
||||
{lt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value={String(customLifetimeDay.value)}>
|
||||
{customLifetimeDay.label}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{lifetimeDays === "custom" && (
|
||||
<TextField
|
||||
type="date"
|
||||
label="Expires on"
|
||||
defaultValue={dayjs().add(expDays, "day").format("YYYY-MM-DD")}
|
||||
onChange={(event) => {
|
||||
const lt = Math.ceil(
|
||||
dayjs(event.target.value).diff(dayjs(), "day", true),
|
||||
);
|
||||
setExpDays(lt);
|
||||
}}
|
||||
inputProps={{
|
||||
"data-pixel": "ignore",
|
||||
min: dayjs().add(1, "day").format("YYYY-MM-DD"),
|
||||
max: maxTokenLifetime
|
||||
? dayjs()
|
||||
.add(maxTokenLifetime / NANO_HOUR / 24, "day")
|
||||
.format("YYYY-MM-DD")
|
||||
: undefined,
|
||||
required: true,
|
||||
}}
|
||||
fullWidth
|
||||
InputLabelProps={{
|
||||
required: true,
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col gap-2 flex-1">
|
||||
<Label htmlFor={expiresOnId}>
|
||||
Expires on{" "}
|
||||
<span className="text-xs font-bold text-content-destructive">
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
id={expiresOnId}
|
||||
type="date"
|
||||
data-pixel="ignore"
|
||||
defaultValue={dayjs()
|
||||
.add(expDays, "day")
|
||||
.format("YYYY-MM-DD")}
|
||||
min={dayjs().add(1, "day").format("YYYY-MM-DD")}
|
||||
max={
|
||||
maxTokenLifetime
|
||||
? dayjs()
|
||||
.add(maxTokenLifetime / NANO_HOUR / 24, "day")
|
||||
.format("YYYY-MM-DD")
|
||||
: undefined
|
||||
}
|
||||
required
|
||||
onChange={(event) => {
|
||||
const lt = Math.ceil(
|
||||
dayjs(event.target.value).diff(dayjs(), "day", true),
|
||||
);
|
||||
setExpDays(lt);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</FormFields>
|
||||
@@ -168,9 +187,3 @@ export const CreateTokenForm: FC<CreateTokenFormProps> = ({
|
||||
</HorizontalForm>
|
||||
);
|
||||
};
|
||||
|
||||
const classNames = {
|
||||
sectionInfo: css`
|
||||
min-width: 300px;
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Link from "@mui/material/Link";
|
||||
import { ExternalLinkIcon, RotateCwIcon } from "lucide-react";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import type { ApiErrorResponse } from "#/api/errors";
|
||||
@@ -6,6 +5,7 @@ import type { ExternalAuth, ExternalAuthDevice } from "#/api/typesGenerated";
|
||||
import { Alert } from "#/components/Alert/Alert";
|
||||
import { Avatar } from "#/components/Avatar/Avatar";
|
||||
import { GitDeviceAuth } from "#/components/GitDeviceAuth/GitDeviceAuth";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import { SignInLayout } from "#/components/SignInLayout/SignInLayout";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -56,6 +56,8 @@ const ExternalAuthPageView: FC<ExternalAuthPageViewProps> = ({
|
||||
href={externalAuth.app_install_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
showExternalIcon={false}
|
||||
className="p-0 text-sm font-medium"
|
||||
>
|
||||
{installTheApp}
|
||||
</Link>
|
||||
@@ -88,6 +90,8 @@ const ExternalAuthPageView: FC<ExternalAuthPageViewProps> = ({
|
||||
href={install.account.profile_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
showExternalIcon={false}
|
||||
className="p-0 after:hover:content-none"
|
||||
>
|
||||
<Avatar
|
||||
src={install.account.avatar_url}
|
||||
@@ -107,7 +111,7 @@ const ExternalAuthPageView: FC<ExternalAuthPageViewProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="m-4 flex flex-col gap-1">
|
||||
<div className="m-4 flex flex-col items-center gap-1">
|
||||
{!hasInstallations && externalAuth.app_installable && (
|
||||
<Alert severity="warning" className="m-4">
|
||||
You must {installTheApp} to clone private repositories. Accounts
|
||||
@@ -122,7 +126,8 @@ const ExternalAuthPageView: FC<ExternalAuthPageViewProps> = ({
|
||||
href={externalAuth.app_install_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center justify-center gap-2 text-base"
|
||||
showExternalIcon={false}
|
||||
className="inline-flex items-center gap-2 p-0 text-base font-medium [&_svg]:size-icon-xs [&_svg]:p-0"
|
||||
>
|
||||
<ExternalLinkIcon className="size-icon-xs" />
|
||||
{externalAuth.installations.length > 0 ? "Configure" : "Install"}{" "}
|
||||
@@ -130,8 +135,9 @@ const ExternalAuthPageView: FC<ExternalAuthPageViewProps> = ({
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
className="flex items-center justify-center gap-2 text-base"
|
||||
className="inline-flex items-center gap-2 p-0 text-base font-medium [&_svg]:size-icon-xs [&_svg]:p-0"
|
||||
href="#"
|
||||
showExternalIcon={false}
|
||||
onClick={() => {
|
||||
onReauthenticate();
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { useFormik } from "formik";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
@@ -9,15 +8,19 @@ import type { CreateOrganizationRequest } from "#/api/typesGenerated";
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { Badges, PremiumBadge } from "#/components/Badges/Badges";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { FormField } from "#/components/FormField/FormField";
|
||||
import { IconField } from "#/components/IconField/IconField";
|
||||
import { Label } from "#/components/Label/Label";
|
||||
import { PaywallPremium } from "#/components/Paywall/PaywallPremium";
|
||||
import { PopoverPaywall } from "#/components/Paywall/PopoverPaywall";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import { Textarea } from "#/components/Textarea/Textarea";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "#/components/Tooltip/Tooltip";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { docs } from "#/utils/docs";
|
||||
import {
|
||||
displayNameValidator,
|
||||
@@ -59,6 +62,11 @@ export const CreateOrganizationPageView: FC<
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
const getFieldHelpers = getFormHelpers(form, error);
|
||||
const descriptionField = getFieldHelpers("description", {
|
||||
maxLength: MAX_DESCRIPTION_CHAR_LIMIT,
|
||||
});
|
||||
const descriptionErrorId = `${descriptionField.id}-error`;
|
||||
const descriptionHelperId = `${descriptionField.id}-helper`;
|
||||
|
||||
return (
|
||||
<div className="flex flex-row font-medium">
|
||||
@@ -130,23 +138,54 @@ export const CreateOrganizationPageView: FC<
|
||||
disabled={form.isSubmitting}
|
||||
className="flex flex-col gap-6 w-full border-none"
|
||||
>
|
||||
<TextField
|
||||
{...getFieldHelpers("name")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
fullWidth
|
||||
<FormField
|
||||
field={getFieldHelpers("name")}
|
||||
label="Slug"
|
||||
onChange={onChangeTrimmed(form)}
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("display_name")}
|
||||
fullWidth
|
||||
<FormField
|
||||
field={getFieldHelpers("display_name")}
|
||||
label="Display name"
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("description")}
|
||||
multiline
|
||||
label="Description"
|
||||
rows={2}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={descriptionField.id}>Description</Label>
|
||||
<Textarea
|
||||
id={descriptionField.id}
|
||||
name={descriptionField.name}
|
||||
value={descriptionField.value}
|
||||
onChange={descriptionField.onChange}
|
||||
onBlur={descriptionField.onBlur}
|
||||
rows={2}
|
||||
aria-invalid={descriptionField.error}
|
||||
aria-describedby={
|
||||
descriptionField.error
|
||||
? descriptionErrorId
|
||||
: descriptionField.helperText
|
||||
? descriptionHelperId
|
||||
: undefined
|
||||
}
|
||||
className={cn(
|
||||
descriptionField.error && "border-border-destructive",
|
||||
)}
|
||||
/>
|
||||
{descriptionField.error ? (
|
||||
<span
|
||||
id={descriptionErrorId}
|
||||
className="text-xs text-content-destructive"
|
||||
>
|
||||
{descriptionField.helperText}
|
||||
</span>
|
||||
) : (
|
||||
descriptionField.helperText && (
|
||||
<span
|
||||
id={descriptionHelperId}
|
||||
className="text-xs text-content-secondary"
|
||||
>
|
||||
{descriptionField.helperText}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<IconField
|
||||
{...getFieldHelpers("icon")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import type { FC } from "react";
|
||||
import type { Permission } from "#/api/typesGenerated";
|
||||
import { Pill } from "#/components/Pill/Pill";
|
||||
@@ -23,7 +22,7 @@ export const PermissionPillsList: FC<PermissionPillsListProps> = ({
|
||||
const resourceTypes = getUniqueResourceTypes(permissions);
|
||||
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<div className="flex flex-row gap-2">
|
||||
{permissions.length > 0 ? (
|
||||
<PermissionsPill
|
||||
resource={resourceTypes[0]}
|
||||
@@ -39,7 +38,7 @@ export const PermissionPillsList: FC<PermissionPillsListProps> = ({
|
||||
permissions={permissions.slice(1)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import type { FC } from "react";
|
||||
import { Pill } from "#/components/Pill/Pill";
|
||||
import {
|
||||
@@ -14,7 +13,7 @@ interface PillListProps {
|
||||
|
||||
export const IdpPillList: FC<PillListProps> = ({ roles }) => {
|
||||
return (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<div className="flex flex-row gap-2">
|
||||
{roles.length > 0 ? (
|
||||
<Pill className="w-fit" type={isUUID(roles[0]) ? "error" : "muted"}>
|
||||
{roles[0]}
|
||||
@@ -24,7 +23,7 @@ export const IdpPillList: FC<PillListProps> = ({ roles }) => {
|
||||
)}
|
||||
|
||||
{roles.length > 1 && <OverflowPill roles={roles.slice(1)} />}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import Link from "@mui/material/Link";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { CircleCheckIcon, KeyIcon } from "lucide-react";
|
||||
import { type FC, useState } from "react";
|
||||
import { type FC, useId, useState } from "react";
|
||||
import { useMutation } from "react-query";
|
||||
import { API } from "#/api/api";
|
||||
import { getErrorMessage } from "#/api/errors";
|
||||
@@ -15,11 +13,15 @@ import { Button } from "#/components/Button/Button";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { EmptyState } from "#/components/EmptyState/EmptyState";
|
||||
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
|
||||
import { Input } from "#/components/Input/Input";
|
||||
import { Label } from "#/components/Label/Label";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import {
|
||||
SettingsHeader,
|
||||
SettingsHeaderDescription,
|
||||
SettingsHeaderTitle,
|
||||
} from "#/components/SettingsHeader/SettingsHeader";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { docs } from "#/utils/docs";
|
||||
|
||||
type LoginTypeConfirmation =
|
||||
@@ -250,6 +252,12 @@ const ConfirmLoginTypeChangeModal: FC<ConfirmLoginTypeChangeModalProps> = ({
|
||||
onConfirm,
|
||||
}) => {
|
||||
const [password, setPassword] = useState("");
|
||||
const passwordId = useId();
|
||||
const errorId = useId();
|
||||
const hasError = Boolean(error);
|
||||
const errorMessage = error
|
||||
? getErrorMessage(error, "Your password is incorrect")
|
||||
: undefined;
|
||||
|
||||
const handleConfirm = () => {
|
||||
onConfirm(password);
|
||||
@@ -273,26 +281,30 @@ const ConfirmLoginTypeChangeModal: FC<ConfirmLoginTypeChangeModalProps> = ({
|
||||
After changing your login type, you will not be able to change it
|
||||
again. Are you sure you want to proceed and change your login type?
|
||||
</p>
|
||||
<TextField
|
||||
autoFocus
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
handleConfirm();
|
||||
}
|
||||
}}
|
||||
error={Boolean(error)}
|
||||
helperText={
|
||||
error
|
||||
? getErrorMessage(error, "Your password is incorrect")
|
||||
: undefined
|
||||
}
|
||||
name="confirm-password"
|
||||
id="confirm-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.currentTarget.value)}
|
||||
label="Confirm your password"
|
||||
type="password"
|
||||
/>
|
||||
<div className="flex flex-col gap-2 text-left">
|
||||
<Label htmlFor={passwordId}>Confirm your password</Label>
|
||||
<Input
|
||||
autoFocus
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
handleConfirm();
|
||||
}
|
||||
}}
|
||||
name="confirm-password"
|
||||
id={passwordId}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.currentTarget.value)}
|
||||
type="password"
|
||||
aria-invalid={hasError}
|
||||
aria-describedby={hasError ? errorId : undefined}
|
||||
className={cn(hasError && "border-border-destructive")}
|
||||
/>
|
||||
{hasError && (
|
||||
<span id={errorId} className="text-xs text-content-destructive">
|
||||
{errorMessage}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Link from "@mui/material/Link";
|
||||
import cronParser from "cron-parser";
|
||||
import cronstrue from "cronstrue";
|
||||
import dayjs, { type Dayjs } from "dayjs";
|
||||
@@ -10,6 +9,7 @@ import type { ReactNode } from "react";
|
||||
import { Link as RouterLink } from "react-router";
|
||||
import type { Template, Workspace } from "#/api/typesGenerated";
|
||||
import { HelpPopoverTitle } from "#/components/HelpPopover/HelpPopover";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import type { WorkspaceActivityStatus } from "#/modules/workspaces/activity";
|
||||
import { isWorkspaceOn } from "./workspace";
|
||||
|
||||
@@ -140,8 +140,8 @@ export const autostopDisplay = (
|
||||
{" "}
|
||||
because this workspace has enabled autostop. You can disable autostop
|
||||
from this workspace's{" "}
|
||||
<Link component={RouterLink} to="settings/schedule">
|
||||
schedule settings
|
||||
<Link asChild showExternalIcon={false} size="sm" className="p-0">
|
||||
<RouterLink to="settings/schedule">schedule settings</RouterLink>
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user