Skip to content

Commit

Permalink
Init checkin
Browse files Browse the repository at this point in the history
  • Loading branch information
jsonporter committed Dec 14, 2021
1 parent e2ebf33 commit aa2ab61
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 214 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ function useRelaunchWorkflowFormState({
disableAll,
maxParallelism,
labels,
annotations
annotations,
authRole,
securityContext
}
} = execution;

const workflow = await apiContext.getWorkflow(workflowId);
const inputDefinitions = getWorkflowInputs(workflow);
const values = await fetchAndMapExecutionInputValues(
Expand All @@ -51,14 +54,17 @@ function useRelaunchWorkflowFormState({
},
apiContext
);

return {
values,
launchPlan,
workflowId,
disableAll,
maxParallelism,
labels,
annotations
annotations,
authRole,
securityContext
};
}
},
Expand Down Expand Up @@ -119,6 +125,7 @@ const RelaunchWorkflowForm: React.FC<RelaunchExecutionFormProps> = props => {
const {
closure: { workflowId }
} = props.execution;

return (
<WaitForData {...initialParameters}>
{initialParameters.state.matches(fetchStates.LOADED) ? (
Expand Down
294 changes: 104 additions & 190 deletions src/components/Launch/LaunchForm/LaunchRoleInput.tsx
Original file line number Diff line number Diff line change
@@ -1,163 +1,25 @@
import {
FormControl,
FormControlLabel,
FormLabel,
Radio,
RadioGroup,
TextField,
Typography
} from '@material-ui/core';
import { log } from 'common/log';
import { TextField, Typography } from '@material-ui/core';
import { NewTargetLink } from 'components/common/NewTargetLink';
import { useDebouncedValue } from 'components/hooks/useDebouncedValue';
import { Admin } from 'flyteidl';
import * as React from 'react';
import { launchInputDebouncDelay, roleTypes } from './constants';
import { AuthRoleStrings } from './constants';
import { makeStringChangeHandler } from './handlers';
import { useInputValueCacheContext } from './inputValueCache';
import { useStyles } from './styles';
import { InputValueMap, LaunchRoleInputRef, RoleType } from './types';
import { LaunchRoleInputRef, LaunchRoles } from './types';
import { AuthRoleTypes } from './types';

const roleHeader = 'Role';
const roleDocLinkUrl =
'https://github.com/flyteorg/flyteidl/blob/3789005a1372221eba28fa20d8386e44b32388f5/protos/flyteidl/admin/common.proto#L241';
const roleTypeLabel = 'type';
const roleInputId = 'launch-auth-role';
const defaultRoleTypeValue = roleTypes.iamRole;
const docLink =
'https://github.com/flyteorg/flyteidl/blob/f4809c1a52073ec80de41be109bccc6331cdbac3/protos/flyteidl/core/security.proto#L111';

export interface LaunchRoleInputProps {
initialValue?: Admin.IAuthRole;
initialValue?: any;
showErrors: boolean;
}

interface LaunchRoleInputState {
error?: string;
roleType: RoleType;
roleString?: string;
getValue(): Admin.IAuthRole;
onChangeRoleString(newValue: string): void;
onChangeRoleType(newValue: string): void;
validate(): boolean;
}

function getRoleTypeByValue(value: string): RoleType | undefined {
return Object.values(roleTypes).find(
({ value: roleTypeValue }) => value === roleTypeValue
);
}

const roleTypeCacheKey = '__roleType';
const roleStringCacheKey = '__roleString';

interface AuthRoleInitialValues {
roleType: RoleType;
roleString: string;
}

function getInitialValues(
cache: InputValueMap,
initialValue?: Admin.IAuthRole
): AuthRoleInitialValues {
let roleType: RoleType | undefined;
let roleString: string | undefined;

// Prefer cached value first, since that is user input
if (cache.has(roleTypeCacheKey)) {
const cachedValue = `${cache.get(roleTypeCacheKey)}`;
roleType = getRoleTypeByValue(cachedValue);
if (roleType === undefined) {
log.error(`Unexepected cached role type: ${cachedValue}`);
}
}
if (cache.has(roleStringCacheKey)) {
roleString = cache.get(roleStringCacheKey)?.toString();
}

// After trying cache, check for an initial value and populate either
// field from the initial value if no cached value was passed.
if (initialValue != null) {
const initialRoleType = Object.values(roleTypes).find(
rt => initialValue[rt.value]
);
if (initialRoleType != null && roleType == null) {
roleType = initialRoleType;
}
if (initialRoleType != null && roleString == null) {
roleString = initialValue[initialRoleType.value]?.toString();
}
}

return {
roleType: roleType ?? defaultRoleTypeValue,
roleString: roleString ?? ''
};
}

export function useRoleInputState(
props: LaunchRoleInputProps
): LaunchRoleInputState {
const inputValueCache = useInputValueCacheContext();
const initialValues = getInitialValues(inputValueCache, props.initialValue);

const [error, setError] = React.useState<string>();
const [roleString, setRoleString] = React.useState<string>(
initialValues.roleString
);

const [roleType, setRoleType] = React.useState<RoleType>(
initialValues.roleType
);

const validationValue = useDebouncedValue(
roleString,
launchInputDebouncDelay
);

const getValue = () => ({ [roleType.value]: roleString });
const validate = () => {
if (roleString == null || roleString.length === 0) {
setError('Value is required');
return false;
}
setError(undefined);
return true;
};

const onChangeRoleString = (value: string) => {
inputValueCache.set(roleStringCacheKey, value);
setRoleString(value);
};

const onChangeRoleType = (value: string) => {
const newRoleType = getRoleTypeByValue(value);
if (newRoleType === undefined) {
throw new Error(`Unexpected role type value: ${value}`);
}
inputValueCache.set(roleTypeCacheKey, value);
setRoleType(newRoleType);
};

React.useEffect(() => {
validate();
}, [validationValue]);

return {
error,
getValue,
onChangeRoleString,
onChangeRoleType,
roleType,
roleString,
validate
};
}

const RoleDescription = () => (
<>
<Typography variant="body2">
Enter a
<NewTargetLink inline={true} href={roleDocLinkUrl}>
&nbsp;role&nbsp;
Enter values for
<NewTargetLink inline={true} href={docLink}>
&nbsp;Security Context&nbsp;
</NewTargetLink>
to assume for this execution.
</Typography>
Expand All @@ -168,64 +30,116 @@ export const LaunchRoleInputImpl: React.RefForwardingComponent<
LaunchRoleInputRef,
LaunchRoleInputProps
> = (props, ref) => {
const styles = useStyles();
const {
error,
getValue,
roleType,
roleString = '',
onChangeRoleString,
onChangeRoleType,
validate
} = useRoleInputState(props);
const hasError = props.showErrors && !!error;
const helperText = hasError ? error : roleType.helperText;
const [state, setState] = React.useState({
[AuthRoleTypes.IAM]: '',
[AuthRoleTypes.k8]: ''
});

React.useEffect(() => {
/* Note: if errors are present in other form elements don't reload new values */
if (!props.showErrors) {
if (props.initialValue?.securityContext) {
setState(state => ({
...state,
[AuthRoleTypes.IAM]:
props.initialValue.securityContext.runAs?.iamRole || '',
[AuthRoleTypes.k8]:
props.initialValue.securityContext.runAs
?.k8sServiceAccount || ''
}));
} else if (props.initialValue) {
setState(state => ({
...state,
[AuthRoleTypes.IAM]:
props.initialValue?.assumableIamRole || '',
[AuthRoleTypes.k8]:
props.initialValue?.kubernetesServiceAccount || ''
}));
}
}
}, [props]);

const getValue = () => {
const authRole = {};
const securityContext = { runAs: {} };

if (state[AuthRoleTypes.k8].length > 0) {
authRole['kubernetesServiceAccount'] = state[AuthRoleTypes.k8];
securityContext['runAs']['k8sServiceAccount'] =
state[AuthRoleTypes.k8];
}

if (state[AuthRoleTypes.IAM].length > 0) {
authRole['assumableIamRole'] = state[AuthRoleTypes.IAM];
securityContext['runAs']['iamRole'] = state[AuthRoleTypes.IAM];
}
return { authRole, securityContext } as LaunchRoles;
};

const handleInputChange = (value, type) => {
setState(state => ({
...state,
[type]: value
}));
};

React.useImperativeHandle(ref, () => ({
getValue,
validate
validate: () => true
}));

const containerStyle: React.CSSProperties = {
padding: '0 .5rem'
};
const inputContainerStyle: React.CSSProperties = {
margin: '.5rem 0'
};
const styles = useStyles();

return (
<section>
<header className={styles.sectionHeader}>
<Typography variant="h6">{roleHeader}</Typography>
<Typography variant="h6">Security Context</Typography>
<RoleDescription />
</header>
<FormControl component="fieldset">
<FormLabel component="legend">{roleTypeLabel}</FormLabel>
<RadioGroup
aria-label="roleType"
name="roleType"
value={roleType.value}
row={true}
onChange={makeStringChangeHandler(onChangeRoleType)}
>
{Object.values(roleTypes).map(({ label, value }) => (
<FormControlLabel
key={value}
value={value}
control={<Radio />}
label={label}
/>
))}
</RadioGroup>
</FormControl>
<div className={styles.formControl}>
<section style={containerStyle}>
<div style={inputContainerStyle}>
<Typography variant="body1">
{AuthRoleStrings[AuthRoleTypes.IAM].label}
</Typography>
</div>
<TextField
id={`launchform-role-${AuthRoleStrings[AuthRoleTypes.IAM]}`}
helperText={AuthRoleStrings[AuthRoleTypes.IAM].helperText}
fullWidth={true}
label={AuthRoleStrings[AuthRoleTypes.IAM].inputLabel}
value={state[AuthRoleTypes.IAM]}
variant="outlined"
onChange={makeStringChangeHandler(
handleInputChange,
AuthRoleTypes.IAM
)}
/>
<div style={inputContainerStyle}>
<Typography variant="body1">
{AuthRoleStrings[AuthRoleTypes.k8].label}
</Typography>
</div>
<TextField
error={hasError}
id={roleInputId}
helperText={helperText}
id={`launchform-role-${AuthRoleStrings[AuthRoleTypes.k8]}`}
helperText={AuthRoleStrings[AuthRoleTypes.k8].helperText}
fullWidth={true}
label={roleType.inputLabel}
onChange={makeStringChangeHandler(onChangeRoleString)}
value={roleString}
label={AuthRoleStrings[AuthRoleTypes.k8].inputLabel}
value={state[AuthRoleTypes.k8]}
variant="outlined"
onChange={makeStringChangeHandler(
handleInputChange,
AuthRoleTypes.k8
)}
/>
</div>
</section>
</section>
);
};

/** Renders controls for selecting an AuthRole type and inputting a value for it. */
export const LaunchRoleInput = React.forwardRef(LaunchRoleInputImpl);
Loading

0 comments on commit aa2ab61

Please sign in to comment.