Unverified Commit 31123c19 authored by Shimon Walner's avatar Shimon Walner Committed by GitHub
Browse files

UI plugin: WC - adds CCVAR redux action

parent ad099dc0
Showing with 129 additions and 135 deletions
+129 -135
......@@ -17,10 +17,11 @@ import { CdsIcon } from '@cds/react/icon';
import { CdsButton } from '@cds/react/button';
// App imports
import { StepProps } from '../../../shared/components/wizard/Wizard';
import { AwsStore } from '../../../state-management/stores/Store.aws';
import './ManagementClusterSettings.scss';
import { AwsStore } from '../../../state-management/stores/Store.aws';
import { INPUT_CHANGE } from '../../../state-management/actions/Form.actions';
import { NavRoutes } from '../../../shared/constants/NavRoutes.constants';
import { StepProps } from '../../../shared/components/wizard/Wizard';
import { Store } from '../../../state-management/stores/Store';
import { TOGGLE_APP_STATUS } from '../../../state-management/actions/Ui.actions';
......@@ -78,6 +79,7 @@ function ManagementClusterSettings(props: Partial<StepProps>) {
const handleClusterNameChange = (event: ChangeEvent<HTMLInputElement>) => {
if (handleValueChange) {
handleValueChange(
INPUT_CHANGE,
'CLUSTER_NAME',
event.target.value,
currentStep,
......
......@@ -23,15 +23,16 @@ import { yupResolver } from '@hookform/resolvers/yup';
import { CdsControlMessage, CdsFormGroup } from '@cds/react/forms';
// App import
import './ManagementCredentials.scss';
import { AwsService } from '../../../swagger-api/services/AwsService';
import { AwsStore } from '../../../state-management/stores/Store.aws';
import { AWSAccountParams } from '../../../swagger-api/models/AWSAccountParams';
import { AWSKeyPair } from '../../../swagger-api/models/AWSKeyPair';
import { StepProps } from '../../../shared/components/wizard/Wizard';
import { INPUT_CHANGE } from '../../../state-management/actions/Form.actions';
import { managementCredentialFormSchema } from './management.credential.form.schema';
import ManagementCredentialProfile from './ManagementCredentialProfile';
import ManagementCredentialOneTime from './ManagementCredentialOneTime';
import './ManagementCredentials.scss';
import { StepProps } from '../../../shared/components/wizard/Wizard';
ClarityIcons.addIcons(refreshIcon, connectIcon, infoCircleIcon);
......@@ -118,7 +119,7 @@ function ManagementCredentials(props: Partial<StepProps>) {
setConnection(false);
if (handleValueChange) {
setTimeout(() => {
handleValueChange('PROFILE', profile, currentStep, errors);
handleValueChange(INPUT_CHANGE, 'PROFILE', profile, currentStep, errors);
});
}
};
......@@ -127,7 +128,7 @@ function ManagementCredentials(props: Partial<StepProps>) {
setConnection(false);
if (handleValueChange) {
setTimeout(() => {
handleValueChange('REGION', region, currentStep, errors);
handleValueChange(INPUT_CHANGE, 'REGION', region, currentStep, errors);
});
}
};
......@@ -137,6 +138,7 @@ function ManagementCredentials(props: Partial<StepProps>) {
if (handleValueChange) {
setTimeout(() => {
handleValueChange(
INPUT_CHANGE,
'EC2_KEY_PAIR',
event.target.value,
currentStep,
......@@ -150,7 +152,7 @@ function ManagementCredentials(props: Partial<StepProps>) {
setConnection(false);
if (handleValueChange) {
setTimeout(() => {
handleValueChange(field, value, currentStep, errors);
handleValueChange(INPUT_CHANGE, field, value, currentStep, errors);
});
}
};
......
......@@ -7,11 +7,10 @@ import _ from 'lodash';
import StepWizard, { StepWizardChildProps } from 'react-step-wizard';
// App imports
import { INPUT_CHANGE } from '../../../state-management/actions/Form.actions';
import './Wizard.scss';
import { STATUS } from '../../constants/App.constants';
import { StoreDispatch } from '../../types/types';
import StepNav from './StepNav';
import './Wizard.scss';
import { StoreDispatch } from '../../types/types';
interface WizardProps {
tabNames: string[];
......@@ -25,31 +24,24 @@ export interface StepProps extends StepWizardChildProps {
key: number;
submitForm: (data: any | undefined) => void;
handleValueChange: (
type: string,
field: string,
value: any,
currentStep: number | undefined,
errors: { [key: string]: FieldError | undefined }
errors: { [key: string]: FieldError | undefined },
locationData?: any,
) => void;
}
function Wizard(props: WizardProps) {
const { tabNames, children, dispatch } = props;
const [tabStatus, setTabStatus] = useState([
STATUS.CURRENT,
..._.times(children.length - 1, () => STATUS.DISABLED),
]);
const submitForm = (currentStep: number) => {
const status = [...tabStatus];
status[currentStep] = STATUS.TOUCHED;
setTabStatus(status);
};
const handleValueChange = (
type: string,
field: string,
value: string,
currentStep: number | undefined,
errors: { [key: string]: FieldError | undefined }
errors: { [key: string]: FieldError | undefined },
locationData?: any,
) => {
// update status bar for the wizard tab
if (errors[field] && currentStep) {
......@@ -61,14 +53,26 @@ function Wizard(props: WizardProps) {
status[currentStep - 1] = STATUS.VALID;
setTabStatus(status);
}
// update the field in the data store.
// update the field in the data store.
dispatch({
type: INPUT_CHANGE,
type,
field,
payload: value,
locationData,
});
};
const [tabStatus, setTabStatus] = useState([
STATUS.CURRENT,
..._.times(children.length - 1, () => STATUS.DISABLED),
]);
const submitForm = (currentStep: number) => {
const status = [...tabStatus];
status[currentStep] = STATUS.TOUCHED;
setTabStatus(status);
};
return (
<div className="wizard-container">
<StepWizard
......
import { Dispatch, Reducer, ReducerAction } from 'react';
export interface Action {
type: string,
field: string,
payload?: any
type: string, // type of action, e.g. INPUT_CHANGE
field: string, // name of form field related to the action
payload?: any, // the payload of the action, generally the new value
locationData?: any, // data needed for storing the payload, generally only used when store location is dynamic (cf CCVAR_CHANGE)
}
export type StoreDispatch = Dispatch<ReducerAction<Reducer<any, any>>>;
\ No newline at end of file
export type StoreDispatch = Dispatch<ReducerAction<Reducer<any, any>>>;
......@@ -4,10 +4,11 @@
* Actions
*/
// Form submit actions
export const CCVAR_CHANGE = 'CCVAR_CHANGE';
export const INPUT_CHANGE = 'INPUT_CHANGE';
export const SUBMIT_FORM = 'SUBMIT_FORM';
export const RESET_DEPENDENT_FIELDS = 'RESET_DEPENDENT_FIELDS';
/**
* Constants
*/
\ No newline at end of file
*/
// App imports
import {
CCVAR_CHANGE,
INPUT_CHANGE,
/*
RESET_DEPENDENT_FIELDS,
SUBMIT_FORM,
*/
} from '../actions/Form.actions';
import { Action } from '../../shared/types/types';
import { ensureDataPath } from './index';
interface FormState {
[key: string]: any;
}
function createNewState(state: FormState, action: Action): FormState {
return {
...state,
[action.field]: action.payload
}
}
function createNewCcVarState(state: FormState, action: Action): FormState {
const newState = { ...state }
const clusterName = action.locationData
if (!clusterName) {
console.error(
`Form reducer unable to store ccvar data from this action: ${JSON.stringify(action)}, because no cluster name was provided!`)
}
const dataPath = `ccAttributes.${clusterName}`
const leafObject = ensureDataPath(dataPath, newState)
if (!action.payload) {
delete leafObject[action.field]
} else {
leafObject[action.field] = action.payload
}
return newState
}
export function formReducer(state: FormState, action: Action) {
let newState;
switch (action.type) {
case INPUT_CHANGE:
newState = {
...state,
[action.field]: action.payload,
};
newState = createNewState(state, action)
break;
case CCVAR_CHANGE:
newState = createNewCcVarState(state, action)
break;
default:
newState = { ...state };
}
console.log(`New state: ${JSON.stringify(newState)}`);
return newState;
// let newState = { ...state };
// if (action.type === SUBMIT_FORM) {
// newState = {
// ...newState,
// ...action.payload,
// };
// } else if (action.type === RESET_DEPENDENT_FIELDS) {
// const resetFields = action.payload.fields.reduce(
// (acc: { [key: string]: string }, cur: string) => {
// acc[cur] = '';
// return acc;
// },
// {}
// );
// newState = {
// ...newState,
// ...resetFields,
// };
// }
// console.log(newState);
// return newState;
}
......@@ -8,4 +8,32 @@ export default combineReducers({
app: appReducer,
data: formReducer,
ui: uiReducer
});
\ No newline at end of file
});
// NOTE: this method's purpose is a side effect: to ensure the given data path will be valid for the state object
export function ensureDataPath(dataPath: string | undefined, state: any): any {
// if there is no data path, store the data at the top level of the state object
if (!dataPath) {
return state
}
// This reducer keeps adding empty objects if given part of the path does not exist.
// Thus if the path is foo.bar.eeyore and only the object foo exists, the reducer starts
// with the existing foo and then adds foo.bar; then it takes foo.bar and adds foo.bar.eeyore.
// It then returns the final object (in this case foo.bar.eeyore).
// NOTE: the field name should NOT be part of the path! The field is added to the final object (elsewhere)
return dataPath.split('.').reduce<any>((accumulator, pathSegment) => {
if (!accumulator[pathSegment]) {
accumulator[pathSegment] = {}
}
return accumulator[pathSegment]
}, state)
}
export function getDataPath(dataPath: string | undefined, state: any): any {
if (!dataPath) {
return state
}
return dataPath.split('.').reduce<any>((accumulator, pathSegment) => {
return accumulator?.[pathSegment]
}, state)
}
......@@ -11,10 +11,8 @@ import wcReducer from '../reducers/Wc.reducer';
const initialState = {
data: {
CLUSTER_CLASS_VARIABLE_VALUES: {},
ccAttributes: {},
SELECTED_MANAGEMENT_CLUSTER: '',
SELECTED_WORKER_NODE_INSTANCE_TYPE: '',
WORKLOAD_CLUSTER_NAME: '',
},
ui: {
wcCcAdvancedExpanded: false,
......
......@@ -8,19 +8,18 @@ import { CdsButton } from '@cds/react/button';
import { yupResolver } from '@hookform/resolvers/yup/dist/yup';
// App imports
import { StepProps } from '../../shared/components/wizard/Wizard';
import { CCVAR_CHANGE } from '../../state-management/actions/Form.actions';
import { ClusterClassDefinition } from '../../shared/models/ClusterClass';
import { WcStore } from '../../state-management/stores/Store.wc';
import { ClusterClassMultipleVariablesDisplay, createFormSchema } from './ClusterClassVariableDisplay';
import { TOGGLE_WC_CC_ADVANCED, TOGGLE_WC_CC_OPTIONAL, TOGGLE_WC_CC_REQUIRED } from '../../state-management/actions/Ui.actions';
import { NavRoutes } from '../../shared/constants/NavRoutes.constants';
import ManagementClusterInfoBanner from './ManagementClusterInfoBanner';
import {
getSelectedManagementCluster,
getValueFromChangeEvent,
keyClusterClassVariableData,
modifyClusterVariableDataItem
} from './WorkloadClusterUtility';
import ManagementClusterInfoBanner from './ManagementClusterInfoBanner';
import { NavRoutes } from '../../shared/constants/NavRoutes.constants';
import { StepProps } from '../../shared/components/wizard/Wizard';
import { TOGGLE_WC_CC_ADVANCED, TOGGLE_WC_CC_OPTIONAL, TOGGLE_WC_CC_REQUIRED } from '../../state-management/actions/Ui.actions';
import { WcStore } from '../../state-management/stores/Store.wc';
interface ClusterAttributeStepProps extends StepProps {
retrieveClusterClassDefinition: (mc: string) => ClusterClassDefinition | undefined
......@@ -77,8 +76,7 @@ function ClusterAttributeStep(props: Partial<ClusterAttributeStepProps>) {
if (handleValueChange) {
const value = getValueFromChangeEvent(evt)
const varName = evt.target.name
const updatedCcVarClusterData = modifyClusterVariableDataItem(varName, value, cluster, state)
handleValueChange(keyClusterClassVariableData(), updatedCcVarClusterData, currentStep, errors)
handleValueChange(CCVAR_CHANGE, varName, value, currentStep, errors, cluster.name)
} else {
console.error('ClusterAttributeStep unable to find a handleValueChange handler!')
}
......
......@@ -13,7 +13,8 @@ import { ClarityIcons, computerIcon, cpuIcon, flaskIcon, memoryIcon } from '@cds
// App imports
import './WorkloadClusterWizard.scss';
import { getSelectedManagementCluster, keyClusterClassVariableData, modifyClusterVariableDataItem } from './WorkloadClusterUtility';
import { CCVAR_CHANGE } from '../../state-management/actions/Form.actions';
import { getSelectedManagementCluster } from './WorkloadClusterUtility';
import { isK8sCompliantString } from '../../shared/validations/Validation.service';
import ManagementClusterInfoBanner from './ManagementClusterInfoBanner';
import RadioButton from '../../shared/components/widgets/RadioButton';
......@@ -22,13 +23,13 @@ import { WcStore } from '../../state-management/stores/Store.wc';
interface ClusterTopologyStepFormInputs {
WORKLOAD_CLUSTER_NAME: string;
SELECTED_WORKER_NODE_INSTANCE_TYPE: string;
WORKER_NODE_INSTANCE_TYPE: string;
}
const clusterTopologyStepFormSchema = yup.object({
WORKLOAD_CLUSTER_NAME: yup.string().nullable().required('Please enter a name for your workload cluster')
.test('', 'Cluster name must contain only lower case letters and hyphen', value => value !== null && isK8sCompliantString(value)),
SELECTED_WORKER_NODE_INSTANCE_TYPE: yup.string().nullable().required('Please select an instance type for your workload cluster nodes')
WORKER_NODE_INSTANCE_TYPE: yup.string().nullable().required('Please select an instance type for your workload cluster nodes')
}).required();
interface WorkerNodeInstanceType {
......@@ -71,23 +72,19 @@ function ClusterTopologyStep(props: Partial<StepProps>) {
const { register, handleSubmit, formState: { errors } } = methods;
const onSubmit: SubmitHandler<ClusterTopologyStepFormInputs> = (data) => {
if (Object.keys(errors).length === 0) {
if (goToStep && currentStep && submitForm && handleValueChange) {
if (goToStep && currentStep && submitForm) {
goToStep(currentStep + 1);
submitForm(currentStep);
}
}
};
const onSelectNodeInstanceType = (evt: ChangeEvent<HTMLSelectElement>) => {
const updatedCcVarClusterData = modifyClusterVariableDataItem('WORKER_NODE_INSTANCE_TYPE',
evt.target.value, cluster, state)
handleValueChange && handleValueChange(keyClusterClassVariableData(), updatedCcVarClusterData, currentStep, errors)
}
const onEnterClusterName = (evt: ChangeEvent<HTMLSelectElement>) => {
let updatedCcVarClusterData = modifyClusterVariableDataItem('CLUSTER_NAME',
evt.target.value, cluster, state)
handleValueChange && handleValueChange(keyClusterClassVariableData(), updatedCcVarClusterData, currentStep, errors)
const onValueChange = (evt: ChangeEvent<HTMLSelectElement>) => {
if (handleValueChange) {
const value = evt.target.value
const key = evt.target.name
handleValueChange(CCVAR_CHANGE, key, value, currentStep, errors, cluster.name)
}
}
return (<div className="wizard-content-container" key="cluster-topology">
......@@ -96,9 +93,11 @@ function ClusterTopologyStep(props: Partial<StepProps>) {
{ManagementClusterInfoBanner(cluster)}
<br/>
<div cds-layout="grid gap:md" key="section-holder">
<div cds-layout="col:6" key="cluster-name-section"> {ClusterNameSection(errors, register, onEnterClusterName)} </div>
<div cds-layout="col:6" key="cluster-name-section">
{ClusterNameSection(errors, register, onValueChange)}
</div>
<div cds-layout="col:6" key="instance-type-section">
{WorkerNodeInstanceTypeSection(errors, register, onSelectNodeInstanceType)}
{WorkerNodeInstanceTypeSection(errors, register, onValueChange)}
</div>
</div>
<br/>
......@@ -117,8 +116,8 @@ function WorkerNodeInstanceTypeSection(errors: any, register: any,
})
}
</div>
{ errors.SELECTED_WORKER_NODE_INSTANCE_TYPE &&
<CdsControlMessage status="error">{errors.SELECTED_WORKER_NODE_INSTANCE_TYPE.message}</CdsControlMessage>
{ errors.WORKER_NODE_INSTANCE_TYPE &&
<CdsControlMessage status="error">{errors.WORKER_NODE_INSTANCE_TYPE.message}</CdsControlMessage>
}
</div>;
}
......@@ -128,7 +127,7 @@ function InstanceTypeInList(instance: WorkerNodeInstanceType, register: any,
return <>
<div className="text-white" cds-layout="col:1"><CdsIcon shape={instance.icon}></CdsIcon></div>
<RadioButton className="input-radio" cdsLayout="col:1" value={instance.id} register={register}
name="SELECTED_WORKER_NODE_INSTANCE_TYPE" onChange={onSelectNodeInstanceType} />
name="WORKER_NODE_INSTANCE_TYPE" onChange={onSelectNodeInstanceType} />
<div className="text-white" cds-layout="col:10">{instance.name} {instance.description}</div>
</>
}
......@@ -138,8 +137,7 @@ function ClusterNameSection(errors: any, register: any, onEnterClusterName: (evt
<CdsFormGroup layout="vertical">
<CdsInput layout="vertical">
<label>Cluster Name</label>
<input placeholder="workload-cluster-name" {...register('WORKLOAD_CLUSTER_NAME')}
onChange={onEnterClusterName} />
<input placeholder="workload-cluster-name" {...register('WORKLOAD_CLUSTER_NAME')} onChange={onEnterClusterName} />
{ errors.WORKLOAD_CLUSTER_NAME &&
<CdsControlMessage status="error">{errors.WORKLOAD_CLUSTER_NAME.message}</CdsControlMessage>
}
......
......@@ -10,6 +10,7 @@ import * as yup from 'yup';
// App imports
import './select-management-cluster.scss';
import { INPUT_CHANGE } from '../../state-management/actions/Form.actions';
import { ManagementCluster } from '../../shared/models/ManagementCluster';
import RadioButton from '../../shared/components/widgets/RadioButton';
import { StepProps } from '../../shared/components/wizard/Wizard';
......@@ -64,7 +65,7 @@ function SelectManagementCluster (props: Partial<SelectManagementClusterProps>)
const clusterName = evt.target.value;
const cluster = findClusterFromName(clusterName, clusters);
if (handleValueChange) {
handleValueChange('SELECTED_MANAGEMENT_CLUSTER', cluster, currentStep, errors);
handleValueChange(INPUT_CHANGE, 'SELECTED_MANAGEMENT_CLUSTER', cluster, currentStep, errors);
} else {
console.error('Unable to record selected management cluster because handleValueChange method is null/undefined')
}
......
......@@ -14,47 +14,7 @@ export function getValueFromChangeEvent(evt: ChangeEvent<HTMLSelectElement>) {
return value
}
// NOTE: Rather than store the CC variables at the top level of our data store,
// we accumulate them in a single object per management cluster.
// This makes using them much easier (later), and prevents "collisions" if the user switches management clusters.
function getClusterVariableData(managementCluster: ManagementCluster, state: any): any {
if (!managementCluster || !managementCluster.name) {
console.error('getClusterVariableData() called with undefined/unnamed cluster')
return {}
}
return getClusterVariableDataForAllClusters(state)[managementCluster.name] || {}
}
function createModifiedClusterVariableDataForAllClusters(ccVarClusterData: any, clusterName: string, state: any) {
const ccVarData = getClusterVariableDataForAllClusters(state)
ccVarData[clusterName] = ccVarClusterData
return ccVarData
}
function getClusterVariableDataForAllClusters(state: any) {
return { ...state.data.CLUSTER_CLASS_VARIABLE_VALUES }
}
export function keyClusterClassVariableData(): string {
return 'CLUSTER_CLASS_VARIABLE_VALUES'
}
export function modifyClusterVariableDataItem(key: string, value: any, managementCluster: ManagementCluster, state: any): any {
const ccVarClusterData = { ...getClusterVariableData(managementCluster, state) }
if (key) {
if (value) {
ccVarClusterData[key] = value
} else {
// NOTE: A boolean field with a value of FALSE gets omitted. That works for all our current fields.
delete ccVarClusterData[key]
}
} else {
console.error(`setClusterVariableDataItem was called with a null key (MC=${managementCluster?.name})`)
}
return createModifiedClusterVariableDataForAllClusters(ccVarClusterData, managementCluster.name, state)
}
// NOTE: we assume that state.data.CLUSTER_CLASS_VARIABLE_VALUES is never null or undefined
export function getSelectedManagementCluster(state: any): ManagementCluster {
return state.data.SELECTED_MANAGEMENT_CLUSTER
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment