diff --git a/src/components/IOC/CreateIOC/CreateIOC.js b/src/components/IOC/CreateIOC/CreateIOC.js index 150cef1a42b96708977367940b242d04699904c9..d4ceec222bd1ac849c13d39bd3642a340143b9e4 100644 --- a/src/components/IOC/CreateIOC/CreateIOC.js +++ b/src/components/IOC/CreateIOC/CreateIOC.js @@ -1,6 +1,9 @@ import React, { useMemo, useEffect, useState, useContext } from "react"; import { useNavigate } from "react-router-dom"; import { useTypingTimer } from "../../common/SearchBoxFilter/TypingTimer"; +import { useCustomSnackbar } from "../../common/snackbar"; +import { RepositoryOptions, WITHOUT_REPO } from "./RepositoryOptions"; +import { RepositoryName } from "./RepositoryName"; import { RootPaper } from "@ess-ics/ce-ui-common/dist/components/common/container/RootPaper"; import { Alert, @@ -24,10 +27,13 @@ const createRequestParams = (query) => { export function CreateIOC() { const navigate = useNavigate(); - const [name, setName] = useState(); - const [gitId, setGitId] = useState(null); + const showSnackBar = useCustomSnackbar(); + const [namingEntity, setNamingEntity] = useState({}); + const [gitProject, setGitProject] = useState({}); const [nameQuery, onNameKeyUp] = useTypingTimer({ interval: 500 }); const [repoQuery, onRepoKeyUp] = useTypingTimer({ interval: 500 }); + const [selectedRepoOption, setSelectedRepoOption] = useState(WITHOUT_REPO); + const [repoName, setRepoName] = useState(""); const client = useContext(apiContext); @@ -65,11 +71,6 @@ export function CreateIOC() { call: false }); - // Return home on cancel - const handleCancel = () => { - navigate("/"); - }; - // create the ioc on form submit const onSubmit = (event) => { event.preventDefault(); @@ -77,13 +78,20 @@ export function CreateIOC() { {}, { requestBody: { - gitProjectId: gitId, - namingUuid: name ? name.uuid : undefined + gitProjectId: gitProject.id, + namingUuid: namingEntity ? namingEntity.uuid : undefined, + repository_name: repoName ? repoName : undefined } } ); }; + const handleSelectRepoOption = (option) => { + setSelectedRepoOption(option); + setNamingEntity({}); + setGitProject({}); + }; + // fetch new names when name query changes useEffect(() => { if (nameQuery) { @@ -100,9 +108,15 @@ export function CreateIOC() { // navigate home once ioc created useEffect(() => { if (ioc) { - navigate(`/iocs/${ioc.id}`); + showSnackBar( + selectedRepoOption === WITHOUT_REPO + ? "IOC created" + : "IOC created with a repository", + "success" + ); + navigate(`/iocs/${ioc.id}?&tab=Management`); } - }, [ioc, navigate]); + }, [ioc, showSnackBar, selectedRepoOption, navigate]); return ( <RootPaper @@ -116,15 +130,18 @@ export function CreateIOC() { width="600px" > <Typography variant="h2">Create new IOC</Typography> + <RepositoryOptions + selectedRepoOption={selectedRepoOption} + onSelectRepoOption={handleSelectRepoOption} + /> <Autocomplete autoHighlight id="nameAutocomplete" + value={namingEntity} options={nameQuery ? names ?? [] : []} loading={loadingNames} clearOnBlur={false} - getOptionLabel={(option) => { - return option?.name ?? ""; - }} + getOptionLabel={(option) => option?.name ?? ""} renderInput={(params) => ( <TextField {...params} @@ -146,70 +163,74 @@ export function CreateIOC() { ) }} disabled={loading} - helperText={name ? name.description : ""} + helperText={namingEntity ? namingEntity.description : ""} /> )} - onChange={(event, value, reason) => { - setName(value); - }} - onInputChange={(event) => { - event && onNameKeyUp(event.nativeEvent); - }} + onChange={(_, value) => setNamingEntity(value)} + onInputChange={(event) => event && onNameKeyUp(event.nativeEvent)} autoSelect /> + {selectedRepoOption === WITHOUT_REPO ? ( + <Autocomplete + autoHighlight + id="gitId" + value={gitProject} + options={repoQuery || gitProject ? allowedGitProjects ?? [] : []} + loading={loadingAllowedGitProjects} + clearOnBlur={false} + getOptionLabel={(option) => { + return option?.url ?? ""; + }} + onChange={(_, value) => setGitProject(value)} + onInputChange={(event) => event && onRepoKeyUp(event.nativeEvent)} + renderInput={(params) => ( + <TextField + {...params} + label="Git repository" + variant="outlined" + fullWidth + required + InputProps={{ + ...params.InputProps, + endAdornment: ( + <React.Fragment> + {loadingAllowedGitProjects ? ( + <CircularProgress + color="inherit" + size={20} + /> + ) : null} + {params.InputProps.endAdornment} + </React.Fragment> + ) + }} + disabled={loading} + /> + )} + autoSelect + /> + ) : ( + <RepositoryName + repoName={repoName} + onRepoNameChange={(name) => setRepoName(name)} + /> + )} - <Autocomplete - autoHighlight - id="gitId" - options={repoQuery || gitId ? allowedGitProjects ?? [] : []} - loading={loadingAllowedGitProjects} - clearOnBlur={false} - getOptionLabel={(option) => { - return option?.url ?? ""; - }} - onChange={(event, value, reason) => { - setGitId(value?.id); - }} - onInputChange={(event) => { - event && onRepoKeyUp(event.nativeEvent); - }} - renderInput={(params) => ( - <TextField - {...params} - label="Git repository" - variant="outlined" - fullWidth - required - InputProps={{ - ...params.InputProps, - endAdornment: ( - <React.Fragment> - {loadingAllowedGitProjects ? ( - <CircularProgress - color="inherit" - size={20} - /> - ) : null} - {params.InputProps.endAdornment} - </React.Fragment> - ) - }} - disabled={loading} - /> - )} - autoSelect - /> {error ? ( <Alert severity="error">{getErrorMessage(error)}</Alert> - ) : ( - <></> - )} + ) : null} + {loading ? ( + <LinearProgress + aria-busy="true" + aria-label="Creating IOC, please wait" + /> + ) : null} <Stack direction="row" justifyContent="flex-end" > <Button - onClick={handleCancel} + onClick={() => navigate("/")} color="primary" disabled={loading} > @@ -219,17 +240,15 @@ export function CreateIOC() { color="primary" variant="contained" type="submit" - disabled={!name || !gitId || loading} + disabled={ + loading || !namingEntity || selectedRepoOption === WITHOUT_REPO + ? !gitProject + : !repoName + } > Create </Button> </Stack> - {loading ? ( - <LinearProgress - aria-busy="true" - aria-label="Creating IOC, please wait" - /> - ) : null} </Stack> </RootPaper> ); diff --git a/src/components/IOC/CreateIOC/RepositoryName.js b/src/components/IOC/CreateIOC/RepositoryName.js new file mode 100644 index 0000000000000000000000000000000000000000..0381116757bcaa998055b879c79fda948ccd46c8 --- /dev/null +++ b/src/components/IOC/CreateIOC/RepositoryName.js @@ -0,0 +1,81 @@ +import React, { useState, useCallback } from "react"; +import { Box, Stack, TextField, Typography } from "@mui/material"; +import { string, func } from "prop-types"; + +const propTypes = { + repoName: string, + onRepoNameChange: func +}; + +// match from beginning to end +// starts with an alpha numeric character (a-Z 0-9) +// followed by 0 or more combinations of alphanumeric characters or - or _ +// ends with an alpha numeric character (a-Z 0-9) +const REPO_NAME_REGEX = "^[a-z0-9]+([a-z0-9_-]+)*[a-z0-9]$"; + +export const RepositoryName = ({ repoName, onRepoNameChange }) => { + const [valid, setValid] = useState(repoName || repoName.length === 0); + + const handleNameChange = useCallback( + (e) => { + const reg = new RegExp(REPO_NAME_REGEX); + const hasValidCharacters = reg.test(e.target.value); + const hasValidLength = + e.target.value.length >= 4 && e.target.value.length <= 20; + const valid = hasValidCharacters && hasValidLength; + setValid(valid); + + if (valid) { + onRepoNameChange(e.target.value); + } else { + onRepoNameChange(""); + } + }, + [onRepoNameChange] + ); + + return ( + <Stack + width="100%" + gap={1} + > + <Box> + <TextField + autoComplete="off" + id="repositoryName" + label="Git repository name" + variant="outlined" + fullWidth + onChange={handleNameChange} + error={!valid} + helperText={ + !valid + ? "Only lowercase alphanumeric chars, hyphens and underscores are allowed in Git repository name (min 4 and max 20 chars)" + : null + } + /> + <Typography + variant="body2" + fontStyle="italic" + id="iocTypeName-name-preview" + > + The Git repository name will follow the pattern: + <Box + sx={{ fontFamily: "Monospace" }} + component="span" + > + e3-ioc- + <Box + sx={{ fontWeight: "bold", display: "inline" }} + component="span" + > + {repoName ? repoName : "{git repository name}"} + </Box> + </Box> + </Typography> + </Box> + </Stack> + ); +}; + +RepositoryName.propTypes = propTypes; diff --git a/src/components/IOC/CreateIOC/RepositoryOptions.js b/src/components/IOC/CreateIOC/RepositoryOptions.js new file mode 100644 index 0000000000000000000000000000000000000000..e6022b5ddb25e7504d66ef0fecac6f2b26e2bf9d --- /dev/null +++ b/src/components/IOC/CreateIOC/RepositoryOptions.js @@ -0,0 +1,44 @@ +import React from "react"; +import { string, func } from "prop-types"; +import { + FormControl, + FormControlLabel, + RadioGroup, + Radio +} from "@mui/material"; + +const propTypes = { + selectedRepoOption: string, + onSelectRepoOption: func +}; + +export const WITH_REPO = "with"; +export const WITHOUT_REPO = "without"; + +export const RepositoryOptions = ({ + selectedRepoOption, + onSelectRepoOption +}) => ( + <FormControl> + <RadioGroup + row + aria-label="Choose option to create IOC with or without existing repository" + name="repositoryOptions" + value={selectedRepoOption} + onChange={(event) => onSelectRepoOption(event.target.value)} + > + <FormControlLabel + value={WITHOUT_REPO} + control={<Radio />} + label="Create IOC with existing repository" + /> + <FormControlLabel + value={WITH_REPO} + control={<Radio />} + label="Create IOC and a repository" + /> + </RadioGroup> + </FormControl> +); + +RepositoryOptions.propTypes = propTypes;