chore(litmus-portal): Added validation in probes modal and minor fix (#2842)

* Added validation in probes modal and minor fix
* Minor change in directory structure and fixed template graph not rendering issue
* Minor regex change for validating ssh links

Signed-off-by: Amit Kumar Das <amit@chaosnative.com>
This commit is contained in:
Amit Kumar Das 2021-05-31 09:34:15 +05:30 committed by GitHub
parent cd72a7a40a
commit c638304960
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 357 additions and 298 deletions

View File

@ -855,6 +855,7 @@ createWorkflow:
addProbe:
heading: Add
headingStrong: Probe
validate: Probe name already exists
labels:
probeName: Probe Name
probeType: Probe Type

View File

@ -84,6 +84,7 @@ export interface ListManifestTemplateArray {
project_name: string;
template_description: string;
template_name: string;
isCustomWorkflow: boolean;
}
export interface ListManifestTemplate {

View File

@ -41,6 +41,18 @@ export const validateWorkflowName = (value: string) => {
return false;
};
export const validateProbeName = (allProbe: any, probeName: string) => {
if (allProbe.length) {
const filteredProbes = allProbe.filter(
(probe: any) => probe.name.toLowerCase() === probeName
);
if (filteredProbes.length) {
return true;
}
}
return false;
};
export const validatePassword = (value: string) => {
const passValid = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,20}$/;
if (value.length > 0) {
@ -77,7 +89,7 @@ export const isValidWebUrl = (value: string) => {
const regExLocal = /^http:\/\/localhost:([0-9]){1,4}$/g;
const regExIpv4 = /^http:\/\/(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]):([0-9]){1,4}$/g;
const regExIpv6 = /^http:\/\/((([0-9a-fA-F]){1,4})\\:){7}([0-9a-fA-F]){1,4}:([0-9]){1,4}$/g;
const sshRegEx = /^([A-Za-z0-9]+@|http(|s)\:\/\/)([A-Za-z0-9.]+(:\d+)?)(?::|\/)([\d\/\w.-]+?)(\.git)?$/i;
const sshRegEx = /^([A-Za-z0-9]+@|http(|s)\:\/\/)([-a-zA-Z0-9@:%._\+~#=]+(:\d+)?)(?::|\/)([\d\/\w.-]+?)(\.git)?$/i;
if (
value.match(regEx) ||
value.match(regExLocal) ||

View File

@ -19,7 +19,9 @@ import {
ListManifestTemplateArray,
} from '../../../models/graphql/workflowListData';
import { getProjectID } from '../../../utils/getSearchParams';
import * as WorkflowActions from '../../../redux/actions/workflow';
import useStyles from './styles';
import useActions from '../../../redux/actions';
interface ChooseWorkflowRadio {
selected: string;
@ -33,7 +35,7 @@ const ChooseWorkflowFromExisting = () => {
// Local States
const [search, setSearch] = useState<string | null>(null);
const [selected, setSelected] = useState<string>('');
const workflowAction = useActions(WorkflowActions);
const { data: templateData } = useQuery<ListManifestTemplate>(
LIST_MANIFEST_TEMPLATE,
{
@ -74,7 +76,9 @@ const ChooseWorkflowFromExisting = () => {
const templateData = filteredExistingWorkflows.filter((workflow) => {
return workflow.template_id === event.target.value;
})[0];
workflowAction.setWorkflowManifest({
isCustomWorkflow: templateData.isCustomWorkflow,
});
localforage.setItem('selectedScheduleOption', selection);
localforage.setItem('workflow', {
name: templateData.template_name.toLowerCase(),

View File

@ -37,7 +37,7 @@ const ProbeDetails: React.FC<ProbeDetailsProps> = ({
| { name?: string | undefined; value: unknown }
>
) => {
if (e.target.name === 'url' || e.target.name === 'responseTimeout') {
if (e.target.name === 'url') {
setProbeData({
...probeData,
'httpProbe/inputs': {
@ -46,6 +46,15 @@ const ProbeDetails: React.FC<ProbeDetailsProps> = ({
},
});
}
if (e.target.name === 'responseTimeout') {
setProbeData({
...probeData,
'httpProbe/inputs': {
...probeData['httpProbe/inputs'],
[e.target.name]: parseInt(e.target.value as string, 10),
},
});
}
if (e.target.name === 'insecureSkipVerify') {
setProbeData({
...probeData,
@ -185,7 +194,7 @@ const ProbeDetails: React.FC<ProbeDetailsProps> = ({
width="50%"
id="responseTimeout"
name="responseTimeout"
type="text"
type="number"
value={probeData['httpProbe/inputs']?.responseTimeout}
onChange={handleHttp}
/>

View File

@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import React from 'react';
import useStyles from './styles';
import ProbeDetails from './ProbeDetails';
import { validateProbeName } from '../../../../utils/validate';
interface AddProbeProps {
probesValue: any;
@ -153,12 +154,21 @@ const AddProbe: React.FC<AddProbeProps> = ({
{t('createWorkflow.tuneWorkflow.addProbe.labels.probeName')}
</InputLabel>
<InputField
variant="primary"
id="name"
name="name"
type="text"
required
value={probeData.name}
helperText={
validateProbeName(allProbes, probeData.name)
? t('createWorkflow.tuneWorkflow.addProbe.validate')
: ''
}
variant={
validateProbeName(allProbes, probeData.name)
? 'error'
: 'primary'
}
onChange={(e) =>
setProbeData({ ...probeData, name: e.target.value })
}
@ -328,7 +338,13 @@ const AddProbe: React.FC<AddProbeProps> = ({
>
{t('createWorkflow.tuneWorkflow.addProbe.button.cancel')}
</ButtonOutlined>
<ButtonFilled type="submit">
<ButtonFilled
type="submit"
disabled={
validateProbeName(allProbes, probeData.name) ||
probeData.name.trim().length === 0
}
>
{t('createWorkflow.tuneWorkflow.addProbe.button.addProbe')}
</ButtonFilled>
</div>

View File

@ -30,6 +30,7 @@ import useStyles from './styles';
interface WorkflowTableProps {
isCustom: boolean | undefined;
namespace: string;
}
interface ChaosCRDTable {
@ -41,311 +42,322 @@ interface ChaosCRDTable {
ChaosEngine: string;
}
const WorkflowTable = forwardRef(({ isCustom }: WorkflowTableProps, ref) => {
const classes = useStyles();
const { t } = useTranslation();
const WorkflowTable = forwardRef(
({ isCustom, namespace }: WorkflowTableProps, ref) => {
const classes = useStyles();
const { t } = useTranslation();
const theme = useTheme();
const workflow = useActions(WorkflowActions);
const [experiments, setExperiments] = useState<ChaosCRDTable[]>([]);
const [revertChaos, setRevertChaos] = useState<boolean>(true);
const [displayStepper, setDisplayStepper] = useState<boolean>(false);
const [engineIndex, setEngineIndex] = useState<number>(0);
const [selected, setSelected] = useState<string>('');
const manifest = useSelector(
(state: RootState) => state.workflowManifest.manifest
);
const imageRegistryData = useSelector(
(state: RootState) => state.selectedImageRegistry
);
const addWeights = (manifest: string) => {
const arr: experimentMap[] = [];
const hashMap = new Map();
const tests = parsed(manifest);
tests.forEach((test) => {
let value = 10;
if (hashMap.has(test)) {
value = hashMap.get(test);
}
arr.push({ experimentName: test, weight: value });
});
localforage.setItem('weights', arr);
};
const parsing = (yamlText: string) => {
const parsedYaml = YAML.parse(yamlText);
const expData: ChaosCRDTable[] = [];
addWeights(manifest);
const extractInfo = (template: any, index: number) => {
if (template.inputs && template.inputs.artifacts) {
template.inputs.artifacts.forEach((artifact: any) => {
const chaosEngine = YAML.parse(artifact.raw.data);
if (chaosEngine.kind === 'ChaosEngine') {
expData.push({
StepIndex: index,
Name: chaosEngine.metadata.generateName,
Namespace: chaosEngine.spec.appinfo?.appns ?? '',
Application: chaosEngine.spec.appinfo?.applabel ?? '',
Probes: chaosEngine.spec.experiments[0].spec.probe?.length ?? 0,
ChaosEngine: artifact.raw.data,
});
}
});
}
const theme = useTheme();
const workflow = useActions(WorkflowActions);
const [experiments, setExperiments] = useState<ChaosCRDTable[]>([]);
const [revertChaos, setRevertChaos] = useState<boolean>(true);
const [displayStepper, setDisplayStepper] = useState<boolean>(false);
const [engineIndex, setEngineIndex] = useState<number>(0);
const [selected, setSelected] = useState<string>('');
const manifest = useSelector(
(state: RootState) => state.workflowManifest.manifest
);
const imageRegistryData = useSelector(
(state: RootState) => state.selectedImageRegistry
);
const addWeights = (manifest: string) => {
const arr: experimentMap[] = [];
const hashMap = new Map();
const tests = parsed(manifest);
tests.forEach((test) => {
let value = 10;
if (hashMap.has(test)) {
value = hashMap.get(test);
}
arr.push({ experimentName: test, weight: value });
});
localforage.setItem('weights', arr);
};
if (parsedYaml.kind === 'Workflow') {
parsedYaml.spec.templates.forEach((template: any, index: number) => {
extractInfo(template, index);
});
} else if (parsedYaml.kind === 'CronWorkflow') {
parsedYaml.spec.workflowSpec.templates.forEach(
(template: any, index: number) => {
extractInfo(template, index);
const parsing = (yamlText: string) => {
const parsedYaml = YAML.parse(yamlText);
const expData: ChaosCRDTable[] = [];
addWeights(manifest);
const extractInfo = (template: any, index: number) => {
if (template.inputs && template.inputs.artifacts) {
template.inputs.artifacts.forEach((artifact: any) => {
const chaosEngine = YAML.parse(artifact.raw.data);
if (chaosEngine.kind === 'ChaosEngine') {
expData.push({
StepIndex: index,
Name: chaosEngine.metadata.generateName,
Namespace:
chaosEngine.spec.appinfo?.appns ===
'{{workflow.parameters.adminModeNamespace}}'
? namespace
: chaosEngine.spec.appinfo?.appns ?? '',
Application: chaosEngine.spec.appinfo?.applabel ?? '',
Probes: chaosEngine.spec.experiments[0].spec.probe?.length ?? 0,
ChaosEngine: artifact.raw.data,
});
}
});
}
};
if (parsedYaml.kind === 'Workflow') {
parsedYaml.spec.templates.forEach((template: any, index: number) => {
extractInfo(template, index);
});
} else if (parsedYaml.kind === 'CronWorkflow') {
parsedYaml.spec.workflowSpec.templates.forEach(
(template: any, index: number) => {
extractInfo(template, index);
}
);
}
setExperiments(expData);
};
// Revert Chaos
const toggleRevertChaos = (manifest: string) => {
const parsedYAML = YAML.parse(manifest);
let deleteEngines: string = '';
// Else if Revert Chaos is set to true and it is not already set in the manifest
// For Workflows
if (revertChaos && parsedYAML.kind === 'Workflow') {
parsedYAML.spec.templates[0].steps.push([
{
name: 'revert-chaos',
template: 'revert-chaos',
},
]);
parsed(manifest).forEach((_, i) => {
deleteEngines += `${
YAML.parse(
parsedYAML.spec.templates[2 + i].inputs.artifacts[0].raw.data
).metadata.labels['instance_id']
}, `;
});
parsedYAML.spec.templates[parsedYAML.spec.templates.length] = {
name: 'revert-chaos',
container: {
image: 'litmuschaos/k8s:latest',
command: ['sh', '-c'],
args: [
`kubectl delete chaosengine -l 'instance_id in (${deleteEngines})' -n {{workflow.parameters.adminModeNamespace}} `,
],
},
};
}
// Else if Revert Chaos is set to True and it is not already set in the manifest
// For Cron Workflow
else if (revertChaos && parsedYAML.kind === 'CronWorkflow') {
parsedYAML.spec.workflowSpec.templates[0].steps.push([
{
name: 'revert-chaos',
template: 'revert-chaos',
},
]);
parsed(manifest).forEach((_, i) => {
deleteEngines = `${
deleteEngines +
YAML.parse(
parsedYAML.spec.workflowSpec.templates[2 + i].inputs.artifacts[0]
.raw.data
).metadata.name
} `;
});
deleteEngines += '-n {{workflow.parameters.adminModeNamespace}}';
parsedYAML.spec.workflowSpec.templates[
parsedYAML.spec.workflowSpec.templates.length
] = {
name: 'revert-chaos',
container: {
image: 'litmuschaos/k8s:latest',
command: ['sh', '-c'],
args: [deleteEngines],
},
};
}
const updatedManifest = updateManifestImage(
parsedYAML,
imageRegistryData
);
}
setExperiments(expData);
};
// Revert Chaos
const toggleRevertChaos = (manifest: string) => {
const parsedYAML = YAML.parse(manifest);
let deleteEngines: string = '';
// Else if Revert Chaos is set to true and it is not already set in the manifest
// For Workflows
if (revertChaos && parsedYAML.kind === 'Workflow') {
parsedYAML.spec.templates[0].steps.push([
{
name: 'revert-chaos',
template: 'revert-chaos',
},
]);
parsed(manifest).forEach((_, i) => {
deleteEngines += `${
YAML.parse(
parsedYAML.spec.templates[2 + i].inputs.artifacts[0].raw.data
).metadata.labels['instance_id']
}, `;
workflow.setWorkflowManifest({
manifest: updatedManifest,
});
};
parsedYAML.spec.templates[parsedYAML.spec.templates.length] = {
name: 'revert-chaos',
container: {
image: 'litmuschaos/k8s:latest',
command: ['sh', '-c'],
args: [
`kubectl delete chaosengine -l 'instance_id in (${deleteEngines})' -n {{workflow.parameters.adminModeNamespace}} `,
],
},
};
}
// Else if Revert Chaos is set to True and it is not already set in the manifest
// For Cron Workflow
else if (revertChaos && parsedYAML.kind === 'CronWorkflow') {
parsedYAML.spec.workflowSpec.templates[0].steps.push([
{
name: 'revert-chaos',
template: 'revert-chaos',
},
]);
parsed(manifest).forEach((_, i) => {
deleteEngines = `${
deleteEngines +
YAML.parse(
parsedYAML.spec.workflowSpec.templates[2 + i].inputs.artifacts[0]
.raw.data
).metadata.name
} `;
const closeConfigurationStepper = () => {
workflow.setWorkflowManifest({
engineYAML: '',
});
setDisplayStepper(false);
};
deleteEngines += '-n {{workflow.parameters.adminModeNamespace}}';
const handleChange = (
event: React.MouseEvent<HTMLElement>,
newValue: boolean
) => {
setRevertChaos(newValue);
};
parsedYAML.spec.workflowSpec.templates[
parsedYAML.spec.workflowSpec.templates.length
] = {
name: 'revert-chaos',
container: {
image: 'litmuschaos/k8s:latest',
command: ['sh', '-c'],
args: [deleteEngines],
},
};
useEffect(() => {
if (manifest.length) {
parsing(manifest);
}
localforage.getItem('selectedScheduleOption').then((value) => {
if (value) {
setSelected((value as ChooseWorkflowRadio).selected);
} else setSelected('');
});
}, [manifest]);
function onNext() {
if (experiments.length === 0) {
return false; // Should show alert
}
if (!isCustom) {
return true;
}
if (selected === 'C') {
toggleRevertChaos(manifest);
}
return true; // Should not show any alert
}
const updatedManifest = updateManifestImage(parsedYAML, imageRegistryData);
workflow.setWorkflowManifest({
manifest: updatedManifest,
});
};
useImperativeHandle(ref, () => ({
onNext,
}));
const closeConfigurationStepper = () => {
workflow.setWorkflowManifest({
engineYAML: '',
});
setDisplayStepper(false);
};
const handleChange = (
event: React.MouseEvent<HTMLElement>,
newValue: boolean
) => {
setRevertChaos(newValue);
};
useEffect(() => {
if (manifest.length) {
parsing(manifest);
}
localforage.getItem('selectedScheduleOption').then((value) => {
if (value) {
setSelected((value as ChooseWorkflowRadio).selected);
} else setSelected('');
});
}, [manifest]);
function onNext() {
if (experiments.length === 0) {
return false; // Should show alert
}
if (!isCustom) {
return true;
}
if (selected === 'C') {
toggleRevertChaos(manifest);
}
return true; // Should not show any alert
}
useImperativeHandle(ref, () => ({
onNext,
}));
return (
<div>
{!displayStepper ? (
<>
<TableContainer className={classes.table} component={Paper}>
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>
{t('createWorkflow.chooseWorkflow.table.head1')}
</TableCell>
<TableCell align="left">
{t('createWorkflow.chooseWorkflow.table.head2')}
</TableCell>
<TableCell align="left">
{t('createWorkflow.chooseWorkflow.table.head3')}
</TableCell>
<TableCell align="left">
{t('createWorkflow.chooseWorkflow.table.head4')}
</TableCell>
<TableCell align="left">
{t('createWorkflow.chooseWorkflow.table.head5')}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{experiments.length > 0 ? (
experiments.map((experiment: ChaosCRDTable, index) => (
<TableRow
key={experiment.Name}
onClick={() => {
setDisplayStepper(true);
setEngineIndex(experiment.StepIndex);
workflow.setWorkflowManifest({
engineYAML: experiment.ChaosEngine,
});
}}
className={classes.selection}
>
<TableCell component="th" scope="row">
{index + 1}
</TableCell>
<TableCell align="left">{experiment.Name}</TableCell>
<TableCell align="left">{experiment.Namespace}</TableCell>
<TableCell align="left">
{experiment.Application}
</TableCell>
<TableCell align="left">{experiment.Probes}</TableCell>
</TableRow>
))
) : (
return (
<div>
{!displayStepper ? (
<>
<TableContainer className={classes.table} component={Paper}>
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell colSpan={5}>
<Typography align="center">
{t('createWorkflow.chooseWorkflow.pleaseAddExp')}
</Typography>
<TableCell>
{t('createWorkflow.chooseWorkflow.table.head1')}
</TableCell>
<TableCell align="left">
{t('createWorkflow.chooseWorkflow.table.head2')}
</TableCell>
<TableCell align="left">
{t('createWorkflow.chooseWorkflow.table.head3')}
</TableCell>
<TableCell align="left">
{t('createWorkflow.chooseWorkflow.table.head4')}
</TableCell>
<TableCell align="left">
{t('createWorkflow.chooseWorkflow.table.head5')}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
{selected === 'C' && (
<TableContainer className={classes.revertChaos} component={Paper}>
<Row className={classes.wrapper}>
<div className={classes.key}>
<Typography>
{t('createWorkflow.chooseWorkflow.revertSchedule')}
</Typography>
</div>
<div>
<ToggleButtonGroup
value={revertChaos}
exclusive
onChange={handleChange}
aria-label="text alignment"
>
<ToggleButton
value
style={{
backgroundColor: revertChaos
? theme.palette.success.main
: theme.palette.disabledBackground,
color: revertChaos
? theme.palette.common.white
: theme.palette.text.disabled,
}}
aria-label="centered"
>
{t('createWorkflow.chooseWorkflow.trueValue')}
</ToggleButton>
<ToggleButton
value={false}
style={{
backgroundColor: !revertChaos
? theme.palette.error.main
: theme.palette.disabledBackground,
color: !revertChaos
? theme.palette.common.white
: theme.palette.text.disabled,
}}
aria-label="centered"
>
{t('createWorkflow.chooseWorkflow.falseValue')}
</ToggleButton>
</ToggleButtonGroup>
</div>
</Row>
</TableHead>
<TableBody>
{experiments.length > 0 ? (
experiments.map((experiment: ChaosCRDTable, index) => (
<TableRow
key={experiment.Name}
onClick={() => {
setDisplayStepper(true);
setEngineIndex(experiment.StepIndex);
workflow.setWorkflowManifest({
engineYAML: experiment.ChaosEngine,
});
}}
className={classes.selection}
>
<TableCell component="th" scope="row">
{index + 1}
</TableCell>
<TableCell align="left">{experiment.Name}</TableCell>
<TableCell align="left">
{experiment.Namespace}
</TableCell>
<TableCell align="left">
{experiment.Application}
</TableCell>
<TableCell align="left">{experiment.Probes}</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5}>
<Typography align="center">
{t('createWorkflow.chooseWorkflow.pleaseAddExp')}
</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
)}
</>
) : (
<ConfigurationStepper
experimentIndex={engineIndex}
closeStepper={closeConfigurationStepper}
isCustom={isCustom}
/>
)}
</div>
);
});
{selected === 'C' && (
<TableContainer className={classes.revertChaos} component={Paper}>
<Row className={classes.wrapper}>
<div className={classes.key}>
<Typography>
{t('createWorkflow.chooseWorkflow.revertSchedule')}
</Typography>
</div>
<div>
<ToggleButtonGroup
value={revertChaos}
exclusive
onChange={handleChange}
aria-label="text alignment"
>
<ToggleButton
value
style={{
backgroundColor: revertChaos
? theme.palette.success.main
: theme.palette.disabledBackground,
color: revertChaos
? theme.palette.common.white
: theme.palette.text.disabled,
}}
aria-label="centered"
>
{t('createWorkflow.chooseWorkflow.trueValue')}
</ToggleButton>
<ToggleButton
value={false}
style={{
backgroundColor: !revertChaos
? theme.palette.error.main
: theme.palette.disabledBackground,
color: !revertChaos
? theme.palette.common.white
: theme.palette.text.disabled,
}}
aria-label="centered"
>
{t('createWorkflow.chooseWorkflow.falseValue')}
</ToggleButton>
</ToggleButtonGroup>
</div>
</Row>
</TableContainer>
)}
</>
) : (
<ConfigurationStepper
experimentIndex={engineIndex}
closeStepper={closeConfigurationStepper}
isCustom={isCustom}
/>
)}
</div>
);
}
);
export default WorkflowTable;

View File

@ -788,7 +788,11 @@ const TuneWorkflow = forwardRef((_, ref) => {
</Width>
{/* Workflow Table */}
<Width width="70%">
<WorkflowTable ref={childRef} isCustom={isCustomWorkflow} />
<WorkflowTable
ref={childRef}
isCustom={isCustomWorkflow}
namespace={namespace}
/>
</Width>
</Row>
</div>

View File

@ -3,7 +3,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import DeveloperGuide from '../../../components/DeveloperGuide';
import ExperimentHeader from '../../../components/ExperimentHeader';
import ExperimentHeader from '../ExperimentHeader';
import ExperimentInfo from '../../../components/ExperimentInfo';
import InstallChaos from '../../../components/InstallChaos';
import Loader from '../../../components/Loader';