mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
refactor: Refactor create workspace page (#4862)
This commit is contained in:
@ -47,6 +47,11 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
"&:hover": {
|
"&:hover": {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[theme.breakpoints.down("sm")]: {
|
||||||
|
top: theme.spacing(1),
|
||||||
|
right: theme.spacing(1),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Button from "@material-ui/core/Button"
|
import Button from "@material-ui/core/Button"
|
||||||
import { makeStyles } from "@material-ui/core/styles"
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
|
import { ClassNameMap } from "@material-ui/core/styles/withStyles"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { LoadingButton } from "../LoadingButton/LoadingButton"
|
import { LoadingButton } from "../LoadingButton/LoadingButton"
|
||||||
|
|
||||||
@ -8,36 +9,22 @@ export const Language = {
|
|||||||
defaultSubmitLabel: "Submit",
|
defaultSubmitLabel: "Submit",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FormFooterStyles = ClassNameMap<"footer" | "button">
|
||||||
export interface FormFooterProps {
|
export interface FormFooterProps {
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
|
styles?: FormFooterStyles
|
||||||
submitLabel?: string
|
submitLabel?: string
|
||||||
submitDisabled?: boolean
|
submitDisabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
export const FormFooter: FC<FormFooterProps> = ({
|
||||||
footer: {
|
|
||||||
display: "flex",
|
|
||||||
flex: "0",
|
|
||||||
// The first button is the submit so it is the first element to be focused
|
|
||||||
// on tab so we use row-reverse to display it on the right
|
|
||||||
flexDirection: "row-reverse",
|
|
||||||
gap: theme.spacing(1.5),
|
|
||||||
alignItems: "center",
|
|
||||||
marginTop: theme.spacing(3),
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
export const FormFooter: FC<React.PropsWithChildren<FormFooterProps>> = ({
|
|
||||||
onCancel,
|
onCancel,
|
||||||
isLoading,
|
isLoading,
|
||||||
submitLabel = Language.defaultSubmitLabel,
|
|
||||||
submitDisabled,
|
submitDisabled,
|
||||||
|
submitLabel = Language.defaultSubmitLabel,
|
||||||
|
styles = defaultStyles(),
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
@ -63,3 +50,19 @@ export const FormFooter: FC<React.PropsWithChildren<FormFooterProps>> = ({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultStyles = makeStyles((theme) => ({
|
||||||
|
footer: {
|
||||||
|
display: "flex",
|
||||||
|
flex: "0",
|
||||||
|
// The first button is the submit so it is the first element to be focused
|
||||||
|
// on tab so we use row-reverse to display it on the right
|
||||||
|
flexDirection: "row-reverse",
|
||||||
|
gap: theme.spacing(1.5),
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: theme.spacing(3),
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
55
site/src/components/FullPageForm/FullPageHorizontalForm.tsx
Normal file
55
site/src/components/FullPageForm/FullPageHorizontalForm.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { FormCloseButton } from "../FormCloseButton/FormCloseButton"
|
||||||
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
|
import Typography from "@material-ui/core/Typography"
|
||||||
|
import { Margins } from "components/Margins/Margins"
|
||||||
|
import { FC, ReactNode } from "react"
|
||||||
|
|
||||||
|
export interface FormTitleProps {
|
||||||
|
title: string
|
||||||
|
detail?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FullPageHorizontalFormProps {
|
||||||
|
title: string
|
||||||
|
detail?: ReactNode
|
||||||
|
onCancel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FullPageHorizontalForm: FC<
|
||||||
|
React.PropsWithChildren<FullPageHorizontalFormProps>
|
||||||
|
> = ({ title, detail, onCancel, children }) => {
|
||||||
|
const styles = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header className={styles.title}>
|
||||||
|
<Margins size="medium">
|
||||||
|
<Typography variant="h3">{title}</Typography>
|
||||||
|
{detail && <Typography variant="caption">{detail}</Typography>}
|
||||||
|
</Margins>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<FormCloseButton onClose={onCancel} />
|
||||||
|
|
||||||
|
<main className={styles.main}>
|
||||||
|
<Margins size="medium">{children}</Margins>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
title: {
|
||||||
|
paddingTop: theme.spacing(6),
|
||||||
|
paddingBottom: theme.spacing(8),
|
||||||
|
|
||||||
|
[theme.breakpoints.down("sm")]: {
|
||||||
|
paddingTop: theme.spacing(4),
|
||||||
|
paddingBottom: theme.spacing(4),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
main: {
|
||||||
|
paddingBottom: theme.spacing(10),
|
||||||
|
},
|
||||||
|
}))
|
@ -1,12 +1,16 @@
|
|||||||
import { makeStyles } from "@material-ui/core/styles"
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { containerWidth, sidePadding } from "../../theme/constants"
|
import {
|
||||||
|
containerWidth,
|
||||||
|
containerWidthMedium,
|
||||||
|
sidePadding,
|
||||||
|
} from "../../theme/constants"
|
||||||
|
|
||||||
type Size = "regular" | "medium" | "small"
|
type Size = "regular" | "medium" | "small"
|
||||||
|
|
||||||
const widthBySize: Record<Size, number> = {
|
const widthBySize: Record<Size, number> = {
|
||||||
regular: containerWidth,
|
regular: containerWidth,
|
||||||
medium: containerWidth / 2,
|
medium: containerWidthMedium,
|
||||||
small: containerWidth / 3,
|
small: containerWidth / 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import TextField from "@material-ui/core/TextField"
|
|||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { ParameterSchema } from "../../api/typesGenerated"
|
import { ParameterSchema } from "../../api/typesGenerated"
|
||||||
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
|
|
||||||
|
|
||||||
const isBoolean = (schema: ParameterSchema) => {
|
const isBoolean = (schema: ParameterSchema) => {
|
||||||
return schema.validation_value_type === "bool"
|
return schema.validation_value_type === "bool"
|
||||||
@ -16,12 +15,18 @@ const isBoolean = (schema: ParameterSchema) => {
|
|||||||
const ParameterLabel: React.FC<{ schema: ParameterSchema }> = ({ schema }) => {
|
const ParameterLabel: React.FC<{ schema: ParameterSchema }> = ({ schema }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
|
if (schema.name && schema.description) {
|
||||||
return (
|
return (
|
||||||
<label className={styles.label} htmlFor={schema.name}>
|
<label htmlFor={schema.name}>
|
||||||
<strong>var.{schema.name}</strong>
|
<span className={styles.labelName}>var.{schema.name}</span>
|
||||||
{schema.description && (
|
|
||||||
<span className={styles.labelDescription}>{schema.description}</span>
|
<span className={styles.labelDescription}>{schema.description}</span>
|
||||||
)}
|
</label>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label htmlFor={schema.name}>
|
||||||
|
<span className={styles.labelDescription}>var.{schema.name}</span>
|
||||||
</label>
|
</label>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -38,7 +43,7 @@ export const ParameterInput: FC<
|
|||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="column" className={styles.root}>
|
<Stack direction="column" spacing={0.75}>
|
||||||
<ParameterLabel schema={schema} />
|
<ParameterLabel schema={schema} />
|
||||||
<div className={styles.input}>
|
<div className={styles.input}>
|
||||||
<ParameterField
|
<ParameterField
|
||||||
@ -119,19 +124,17 @@ const ParameterField: React.FC<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
labelName: {
|
||||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
fontSize: 14,
|
||||||
paddingTop: theme.spacing(2),
|
color: theme.palette.text.secondary,
|
||||||
paddingBottom: theme.spacing(2),
|
display: "block",
|
||||||
},
|
marginBottom: theme.spacing(0.5),
|
||||||
label: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
fontSize: 21,
|
|
||||||
},
|
},
|
||||||
labelDescription: {
|
labelDescription: {
|
||||||
fontSize: 14,
|
fontSize: 16,
|
||||||
marginTop: theme.spacing(1),
|
color: theme.palette.text.primary,
|
||||||
|
display: "block",
|
||||||
|
fontWeight: 600,
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
@ -6,7 +6,6 @@ import { AlertBanner } from "components/AlertBanner/AlertBanner"
|
|||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import * as TypesGen from "../../api/typesGenerated"
|
import * as TypesGen from "../../api/typesGenerated"
|
||||||
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
|
|
||||||
|
|
||||||
export const Language = {
|
export const Language = {
|
||||||
of: "of",
|
of: "of",
|
||||||
@ -27,7 +26,7 @@ export const WorkspaceQuota: FC<WorkspaceQuotaProps> = ({ quota, error }) => {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack spacing={1} className={styles.stack}>
|
<Stack spacing={1} className={styles.stack}>
|
||||||
<span className={styles.title}>Workspace Quota</span>
|
<span className={styles.title}>Usage Quota</span>
|
||||||
<AlertBanner severity="error" error={error} />
|
<AlertBanner severity="error" error={error} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
@ -39,7 +38,7 @@ export const WorkspaceQuota: FC<WorkspaceQuotaProps> = ({ quota, error }) => {
|
|||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack spacing={1} className={styles.stack}>
|
<Stack spacing={1} className={styles.stack}>
|
||||||
<span className={styles.title}>Workspace Quota</span>
|
<span className={styles.title}>Usage quota</span>
|
||||||
<LinearProgress color="primary" />
|
<LinearProgress color="primary" />
|
||||||
<div className={styles.label}>
|
<div className={styles.label}>
|
||||||
<Skeleton className={styles.skeleton} />
|
<Skeleton className={styles.skeleton} />
|
||||||
@ -64,8 +63,23 @@ export const WorkspaceQuota: FC<WorkspaceQuotaProps> = ({ quota, error }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack spacing={1} className={styles.stack}>
|
<Stack spacing={1.5} className={styles.stack}>
|
||||||
<span className={styles.title}>Workspace Quota</span>
|
<Stack direction="row" justifyContent="space-between">
|
||||||
|
<span className={styles.title}>Usage Quota</span>
|
||||||
|
<div className={styles.label}>
|
||||||
|
<span className={styles.labelHighlight}>
|
||||||
|
{quota.user_workspace_count}
|
||||||
|
</span>{" "}
|
||||||
|
{Language.of}{" "}
|
||||||
|
<span className={styles.labelHighlight}>
|
||||||
|
{quota.user_workspace_limit}
|
||||||
|
</span>{" "}
|
||||||
|
{quota.user_workspace_limit === 1
|
||||||
|
? Language.workspace
|
||||||
|
: Language.workspaces}
|
||||||
|
{" used"}
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
<LinearProgress
|
<LinearProgress
|
||||||
className={
|
className={
|
||||||
quota.user_workspace_count >= quota.user_workspace_limit
|
quota.user_workspace_count >= quota.user_workspace_limit
|
||||||
@ -75,14 +89,6 @@ export const WorkspaceQuota: FC<WorkspaceQuotaProps> = ({ quota, error }) => {
|
|||||||
value={value}
|
value={value}
|
||||||
variant="determinate"
|
variant="determinate"
|
||||||
/>
|
/>
|
||||||
<div className={styles.label}>
|
|
||||||
{quota.user_workspace_count} {Language.of}{" "}
|
|
||||||
{quota.user_workspace_limit}{" "}
|
|
||||||
{quota.user_workspace_limit === 1
|
|
||||||
? Language.workspace
|
|
||||||
: Language.workspaces}
|
|
||||||
{" used"}
|
|
||||||
</div>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
@ -101,18 +107,16 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
fontSize: 16,
|
||||||
fontSize: 21,
|
|
||||||
paddingBottom: "8px",
|
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
fontSize: 14,
|
||||||
fontSize: 12,
|
|
||||||
textTransform: "uppercase",
|
|
||||||
display: "block",
|
display: "block",
|
||||||
fontWeight: 600,
|
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
},
|
},
|
||||||
|
labelHighlight: {
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
skeleton: {
|
skeleton: {
|
||||||
minWidth: "150px",
|
minWidth: "150px",
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"templateLabel": "Template",
|
"templateLabel": "Template",
|
||||||
"nameLabel": "Name",
|
"nameLabel": "Workspace Name",
|
||||||
"ownerLabel": "Workspace Owner"
|
"ownerLabel": "Owner",
|
||||||
|
"createWorkspace": "Create workspace"
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
import { fireEvent, screen, waitFor } from "@testing-library/react"
|
import { fireEvent, screen, waitFor } from "@testing-library/react"
|
||||||
import userEvent from "@testing-library/user-event"
|
import userEvent from "@testing-library/user-event"
|
||||||
import * as API from "api/api"
|
import * as API from "api/api"
|
||||||
import { Language as FooterLanguage } from "components/FormFooter/FormFooter"
|
|
||||||
import i18next from "i18next"
|
import i18next from "i18next"
|
||||||
import {
|
import {
|
||||||
MockTemplate,
|
MockTemplate,
|
||||||
@ -17,6 +16,7 @@ import CreateWorkspacePage from "./CreateWorkspacePage"
|
|||||||
const { t } = i18next
|
const { t } = i18next
|
||||||
|
|
||||||
const nameLabelText = t("nameLabel", { ns: "createWorkspacePage" })
|
const nameLabelText = t("nameLabel", { ns: "createWorkspacePage" })
|
||||||
|
const createWorkspaceText = t("createWorkspace", { ns: "createWorkspacePage" })
|
||||||
|
|
||||||
const renderCreateWorkspacePage = () => {
|
const renderCreateWorkspacePage = () => {
|
||||||
return renderWithAuth(<CreateWorkspacePage />, {
|
return renderWithAuth(<CreateWorkspacePage />, {
|
||||||
@ -48,7 +48,7 @@ describe("CreateWorkspacePage", () => {
|
|||||||
target: { value: "test" },
|
target: { value: "test" },
|
||||||
})
|
})
|
||||||
|
|
||||||
const submitButton = screen.getByText(FooterLanguage.defaultSubmitLabel)
|
const submitButton = screen.getByText(createWorkspaceText)
|
||||||
userEvent.click(submitButton)
|
userEvent.click(submitButton)
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
|
@ -25,7 +25,6 @@ const CreateWorkspacePage: FC = () => {
|
|||||||
shallowEqual,
|
shallowEqual,
|
||||||
)
|
)
|
||||||
const workspaceQuotaEnabled = featureVisibility[FeatureNames.WorkspaceQuota]
|
const workspaceQuotaEnabled = featureVisibility[FeatureNames.WorkspaceQuota]
|
||||||
|
|
||||||
const [authState] = useActor(xServices.authXService)
|
const [authState] = useActor(xServices.authXService)
|
||||||
const { me } = authState.context
|
const { me } = authState.context
|
||||||
const [createWorkspaceState, send] = useMachine(createWorkspaceMachine, {
|
const [createWorkspaceState, send] = useMachine(createWorkspaceMachine, {
|
||||||
@ -89,7 +88,8 @@ const CreateWorkspacePage: FC = () => {
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
navigate("/templates")
|
// Go back
|
||||||
|
navigate(-1)
|
||||||
}}
|
}}
|
||||||
onSubmit={(request) => {
|
onSubmit={(request) => {
|
||||||
send({
|
send({
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import TextField from "@material-ui/core/TextField"
|
import TextField from "@material-ui/core/TextField"
|
||||||
import * as TypesGen from "api/typesGenerated"
|
import * as TypesGen from "api/typesGenerated"
|
||||||
import { FormFooter } from "components/FormFooter/FormFooter"
|
import { FormFooter } from "components/FormFooter/FormFooter"
|
||||||
import { FullPageForm } from "components/FullPageForm/FullPageForm"
|
|
||||||
import { Loader } from "components/Loader/Loader"
|
|
||||||
import { ParameterInput } from "components/ParameterInput/ParameterInput"
|
import { ParameterInput } from "components/ParameterInput/ParameterInput"
|
||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack"
|
||||||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"
|
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"
|
||||||
@ -14,6 +12,9 @@ import { useTranslation } from "react-i18next"
|
|||||||
import { getFormHelpers, nameValidator, onChangeTrimmed } from "util/formUtils"
|
import { getFormHelpers, nameValidator, onChangeTrimmed } from "util/formUtils"
|
||||||
import * as Yup from "yup"
|
import * as Yup from "yup"
|
||||||
import { AlertBanner } from "components/AlertBanner/AlertBanner"
|
import { AlertBanner } from "components/AlertBanner/AlertBanner"
|
||||||
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
|
import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm"
|
||||||
|
import { FullScreenLoader } from "components/Loader/FullScreenLoader"
|
||||||
|
|
||||||
export enum CreateWorkspaceErrors {
|
export enum CreateWorkspaceErrors {
|
||||||
GET_TEMPLATES_ERROR = "getTemplatesError",
|
GET_TEMPLATES_ERROR = "getTemplatesError",
|
||||||
@ -52,7 +53,8 @@ export const CreateWorkspacePageView: FC<
|
|||||||
React.PropsWithChildren<CreateWorkspacePageViewProps>
|
React.PropsWithChildren<CreateWorkspacePageViewProps>
|
||||||
> = (props) => {
|
> = (props) => {
|
||||||
const { t } = useTranslation("createWorkspacePage")
|
const { t } = useTranslation("createWorkspacePage")
|
||||||
|
const styles = useStyles()
|
||||||
|
const formFooterStyles = useFormFooterStyles()
|
||||||
const [parameterValues, setParameterValues] = useState<
|
const [parameterValues, setParameterValues] = useState<
|
||||||
Record<string, string>
|
Record<string, string>
|
||||||
>({})
|
>({})
|
||||||
@ -92,11 +94,23 @@ export const CreateWorkspacePageView: FC<
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const canSubmit =
|
||||||
|
props.workspaceQuota && props.workspaceQuota.user_workspace_limit > 0
|
||||||
|
? props.workspaceQuota.user_workspace_count <
|
||||||
|
props.workspaceQuota.user_workspace_limit
|
||||||
|
: true
|
||||||
|
|
||||||
|
const isLoading = props.loadingTemplateSchema || props.loadingTemplates
|
||||||
|
|
||||||
const getFieldHelpers = getFormHelpers<TypesGen.CreateWorkspaceRequest>(
|
const getFieldHelpers = getFormHelpers<TypesGen.CreateWorkspaceRequest>(
|
||||||
form,
|
form,
|
||||||
props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR],
|
props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <FullScreenLoader />
|
||||||
|
}
|
||||||
|
|
||||||
if (props.hasTemplateErrors) {
|
if (props.hasTemplateErrors) {
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
@ -132,21 +146,10 @@ export const CreateWorkspacePageView: FC<
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const canSubmit =
|
if (
|
||||||
props.workspaceQuota && props.workspaceQuota.user_workspace_limit > 0
|
props.createWorkspaceErrors[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]
|
||||||
? props.workspaceQuota.user_workspace_count <
|
) {
|
||||||
props.workspaceQuota.user_workspace_limit
|
|
||||||
: true
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FullPageForm title="Create workspace" onCancel={props.onCancel}>
|
|
||||||
<form onSubmit={form.handleSubmit}>
|
|
||||||
<Stack>
|
|
||||||
{Boolean(
|
|
||||||
props.createWorkspaceErrors[
|
|
||||||
CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR
|
|
||||||
],
|
|
||||||
) && (
|
|
||||||
<AlertBanner
|
<AlertBanner
|
||||||
severity="error"
|
severity="error"
|
||||||
error={
|
error={
|
||||||
@ -155,18 +158,50 @@ export const CreateWorkspacePageView: FC<
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)
|
||||||
<TextField
|
}
|
||||||
disabled
|
|
||||||
fullWidth
|
return (
|
||||||
label={t("templateLabel")}
|
<FullPageHorizontalForm title="New workspace" onCancel={props.onCancel}>
|
||||||
value={props.selectedTemplate?.name || props.templateName}
|
<form onSubmit={form.handleSubmit}>
|
||||||
variant="outlined"
|
<Stack direction="column" spacing={10} className={styles.formSections}>
|
||||||
/>
|
{/* General info */}
|
||||||
|
<div className={styles.formSection}>
|
||||||
|
<div className={styles.formSectionInfo}>
|
||||||
|
<h2 className={styles.formSectionInfoTitle}>General info</h2>
|
||||||
|
<p className={styles.formSectionInfoDescription}>
|
||||||
|
The template and name of your new workspace.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
spacing={1}
|
||||||
|
className={styles.formSectionFields}
|
||||||
|
>
|
||||||
|
{props.selectedTemplate && (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
className={styles.template}
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<div className={styles.templateIcon}>
|
||||||
|
<img src={props.selectedTemplate.icon} alt="" />
|
||||||
|
</div>
|
||||||
|
<Stack direction="column" spacing={0.5}>
|
||||||
|
<span className={styles.templateName}>
|
||||||
|
{props.selectedTemplate.name}
|
||||||
|
</span>
|
||||||
|
{props.selectedTemplate.description && (
|
||||||
|
<span className={styles.templateDescription}>
|
||||||
|
{props.selectedTemplate.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
{props.loadingTemplateSchema && <Loader />}
|
|
||||||
{props.selectedTemplate && props.templateSchema && (
|
|
||||||
<>
|
|
||||||
<TextField
|
<TextField
|
||||||
{...getFieldHelpers("name")}
|
{...getFieldHelpers("name")}
|
||||||
disabled={form.isSubmitting}
|
disabled={form.isSubmitting}
|
||||||
@ -176,8 +211,25 @@ export const CreateWorkspacePageView: FC<
|
|||||||
label={t("nameLabel")}
|
label={t("nameLabel")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Workspace owner */}
|
||||||
{props.canCreateForUser && (
|
{props.canCreateForUser && (
|
||||||
|
<div className={styles.formSection}>
|
||||||
|
<div className={styles.formSectionInfo}>
|
||||||
|
<h2 className={styles.formSectionInfoTitle}>Workspace owner</h2>
|
||||||
|
<p className={styles.formSectionInfoDescription}>
|
||||||
|
The user that is going to own this workspace. If you are
|
||||||
|
admin, you can create workspace for others.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
spacing={1}
|
||||||
|
className={styles.formSectionFields}
|
||||||
|
>
|
||||||
<UserAutocomplete
|
<UserAutocomplete
|
||||||
value={props.owner}
|
value={props.owner}
|
||||||
onChange={props.setOwner}
|
onChange={props.setOwner}
|
||||||
@ -185,7 +237,6 @@ export const CreateWorkspacePageView: FC<
|
|||||||
inputMargin="dense"
|
inputMargin="dense"
|
||||||
showAvatar
|
showAvatar
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{props.workspaceQuota && (
|
{props.workspaceQuota && (
|
||||||
<WorkspaceQuota
|
<WorkspaceQuota
|
||||||
@ -197,9 +248,26 @@ export const CreateWorkspacePageView: FC<
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{props.templateSchema.length > 0 && (
|
{/* Template params */}
|
||||||
<Stack>
|
{props.templateSchema && props.templateSchema.length > 0 && (
|
||||||
|
<div className={styles.formSection}>
|
||||||
|
<div className={styles.formSectionInfo}>
|
||||||
|
<h2 className={styles.formSectionInfoTitle}>Template params</h2>
|
||||||
|
<p className={styles.formSectionInfoDescription}>
|
||||||
|
Those values are provided by your template‘s Terraform
|
||||||
|
configuration.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
spacing={4} // Spacing here is diff because the fields here don't have the MUI floating label spacing
|
||||||
|
className={styles.formSectionFields}
|
||||||
|
>
|
||||||
{props.templateSchema.map((schema) => (
|
{props.templateSchema.map((schema) => (
|
||||||
<ParameterInput
|
<ParameterInput
|
||||||
disabled={form.isSubmitting}
|
disabled={form.isSubmitting}
|
||||||
@ -214,17 +282,115 @@ export const CreateWorkspacePageView: FC<
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormFooter
|
<FormFooter
|
||||||
|
styles={formFooterStyles}
|
||||||
onCancel={props.onCancel}
|
onCancel={props.onCancel}
|
||||||
isLoading={props.creatingWorkspace}
|
isLoading={props.creatingWorkspace}
|
||||||
submitDisabled={!canSubmit}
|
submitDisabled={!canSubmit}
|
||||||
|
submitLabel={t("createWorkspace")}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
</FullPageForm>
|
</FullPageHorizontalForm>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
formSections: {
|
||||||
|
[theme.breakpoints.down("sm")]: {
|
||||||
|
gap: theme.spacing(8),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
formSection: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
gap: theme.spacing(15),
|
||||||
|
|
||||||
|
[theme.breakpoints.down("sm")]: {
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
formSectionInfo: {
|
||||||
|
width: 312,
|
||||||
|
flexShrink: 0,
|
||||||
|
position: "sticky",
|
||||||
|
top: theme.spacing(3),
|
||||||
|
|
||||||
|
[theme.breakpoints.down("sm")]: {
|
||||||
|
width: "100%",
|
||||||
|
position: "initial",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
formSectionInfoTitle: {
|
||||||
|
fontSize: 20,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
fontWeight: 400,
|
||||||
|
margin: 0,
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
},
|
||||||
|
|
||||||
|
formSectionInfoDescription: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
lineHeight: "160%",
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
formSectionFields: {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
|
||||||
|
template: {
|
||||||
|
padding: theme.spacing(2.5, 3),
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
templateName: {
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
|
||||||
|
templateDescription: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
},
|
||||||
|
|
||||||
|
templateIcon: {
|
||||||
|
width: theme.spacing(5),
|
||||||
|
lineHeight: 1,
|
||||||
|
|
||||||
|
"& img": {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const useFormFooterStyles = makeStyles((theme) => ({
|
||||||
|
button: {
|
||||||
|
minWidth: theme.spacing(23),
|
||||||
|
|
||||||
|
[theme.breakpoints.down("sm")]: {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
flexDirection: "row-reverse",
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
|
||||||
|
[theme.breakpoints.down("sm")]: {
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
@ -9,6 +9,7 @@ export const lightButtonShadow = "0 2px 2px rgba(0, 23, 121, 0.08)"
|
|||||||
export const emptyBoxShadow = "none"
|
export const emptyBoxShadow = "none"
|
||||||
export const navHeight = 62
|
export const navHeight = 62
|
||||||
export const containerWidth = 1380
|
export const containerWidth = 1380
|
||||||
|
export const containerWidthMedium = 1080
|
||||||
export const sidePadding = 24
|
export const sidePadding = 24
|
||||||
export const TitleIconSize = 48
|
export const TitleIconSize = 48
|
||||||
export const CardRadius = 2
|
export const CardRadius = 2
|
||||||
|
@ -203,5 +203,10 @@ export const getOverrides = ({
|
|||||||
backgroundColor: palette.divider,
|
backgroundColor: palette.divider,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiLinearProgress: {
|
||||||
|
root: {
|
||||||
|
borderRadius: 999,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user