diff --git a/src/components/Executions/ExecutionDetails/RelaunchExecutionForm.tsx b/src/components/Executions/ExecutionDetails/RelaunchExecutionForm.tsx index 7b4e9df11..d72282b9f 100644 --- a/src/components/Executions/ExecutionDetails/RelaunchExecutionForm.tsx +++ b/src/components/Executions/ExecutionDetails/RelaunchExecutionForm.tsx @@ -34,7 +34,12 @@ function useRelaunchWorkflowFormState({ doFetch: async execution => { const { closure: { workflowId }, - spec: { launchPlan } + spec: { + launchPlan, + disableAll, + maxParallelism, + qualityOfService + } } = execution; const workflow = await apiContext.getWorkflow(workflowId); const inputDefinitions = getWorkflowInputs(workflow); @@ -45,7 +50,14 @@ function useRelaunchWorkflowFormState({ }, apiContext ); - return { values, launchPlan, workflowId }; + return { + values, + launchPlan, + workflowId, + disableAll, + maxParallelism, + qualityOfService + }; } }, execution diff --git a/src/components/Launch/LaunchForm/LaunchFormAdvancedInputs.tsx b/src/components/Launch/LaunchForm/LaunchFormAdvancedInputs.tsx new file mode 100644 index 000000000..f711b6f9a --- /dev/null +++ b/src/components/Launch/LaunchForm/LaunchFormAdvancedInputs.tsx @@ -0,0 +1,176 @@ +import * as React from 'react'; +import { Admin } from 'flyteidl'; +import { makeStyles, Theme } from '@material-ui/core/styles'; +import Checkbox from '@material-ui/core/Checkbox'; +import FormControl from '@material-ui/core/FormControl'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import InputLabel from '@material-ui/core/InputLabel'; +import MenuItem from '@material-ui/core/MenuItem'; +import Select from '@material-ui/core/Select'; +import Typography from '@material-ui/core/Typography'; + +import { qualityOfServiceTier, qualityOfServiceTierLabels } from './constants'; +import { LaunchAdvancedOptionsRef } from './types'; +import { flyteidl } from '@flyteorg/flyteidl/gen/pb-js/flyteidl'; +import IExecutionSpec = flyteidl.admin.IExecutionSpec; +import { Grid, TextField } from '@material-ui/core'; + +const useStyles = makeStyles((theme: Theme) => ({ + sectionTitle: { + marginBottom: theme.spacing(2) + }, + sectionContainer: { + display: 'flex', + flexDirection: 'column' + }, + qosContainer: { + display: 'flex' + }, + autoFlex: { + flex: 1, + display: 'flex' + } +})); + +interface LaunchAdvancedOptionsProps { + spec: Admin.IExecutionSpec; +} + +export const LaunchFormAdvancedInputs = React.forwardRef< + LaunchAdvancedOptionsRef, + LaunchAdvancedOptionsProps +>(({ spec }, ref) => { + const styles = useStyles(); + const [qosTier, setQosTier] = React.useState( + qualityOfServiceTier.UNDEFINED.toString() + ); + const [disableAll, setDisableAll] = React.useState(false); + const [maxParallelism, setMaxParallelism] = React.useState(''); + const [queueingBudget, setQueueingBudget] = React.useState(''); + + React.useEffect(() => { + if (spec.disableAll !== undefined && spec.disableAll !== null) { + setDisableAll(spec.disableAll); + } + if (spec.maxParallelism !== undefined && spec.maxParallelism !== null) { + setMaxParallelism(`${spec.maxParallelism}`); + } + if ( + spec.qualityOfService?.tier !== undefined && + spec.qualityOfService?.tier !== null + ) { + setQosTier(spec.qualityOfService.tier.toString()); + } + }, [spec]); + + React.useImperativeHandle( + ref, + () => ({ + getValues: () => { + return { + disableAll, + qualityOfService: { + tier: parseInt(qosTier || '0', 10) + }, + maxParallelism: parseInt(maxParallelism || '', 10) + } as IExecutionSpec; + }, + validate: () => { + return true; + } + }), + [disableAll, qosTier, maxParallelism] + ); + + const handleQosTierChange = React.useCallback(({ target: { value } }) => { + setQosTier(value); + }, []); + + const handleDisableAllChange = React.useCallback(() => { + setDisableAll(prevState => !prevState); + }, []); + + const handleMaxParallelismChange = React.useCallback( + ({ target: { value } }) => { + setMaxParallelism(value); + }, + [] + ); + + const handleQueueingBudgetChange = React.useCallback( + ({ target: { value } }) => { + setQueueingBudget(value); + }, + [] + ); + + return ( + <> +
+ + } + label="Disable all notifications" + /> +
+
+ +
+
+ + Quality of Service + + + + + + Quality of Service Tier + + + + + + + + +
+ + ); +}); diff --git a/src/components/Launch/LaunchForm/LaunchWorkflowForm.tsx b/src/components/Launch/LaunchForm/LaunchWorkflowForm.tsx index b58ff2879..e692179f0 100644 --- a/src/components/Launch/LaunchForm/LaunchWorkflowForm.tsx +++ b/src/components/Launch/LaunchForm/LaunchWorkflowForm.tsx @@ -1,4 +1,9 @@ -import { DialogContent } from '@material-ui/core'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + DialogContent +} from '@material-ui/core'; import { getCacheKey } from 'components/Cache/utils'; import * as React from 'react'; import { formStrings } from './constants'; @@ -14,11 +19,16 @@ import { LaunchWorkflowFormProps } from './types'; import { useLaunchWorkflowFormState } from './useLaunchWorkflowFormState'; +import { isEnterInputsState } from './utils'; +import { LaunchRoleInput } from './LaunchRoleInput'; +import { LaunchFormAdvancedInputs } from './LaunchFormAdvancedInputs'; /** Renders the form for initiating a Launch request based on a Workflow */ export const LaunchWorkflowForm: React.FC = props => { const { formInputsRef, + roleInputRef, + advancedOptionsRef, state, service, workflowSourceSelectorState @@ -96,6 +106,33 @@ export const LaunchWorkflowForm: React.FC = props => { state={baseState} variant="workflow" /> + + + Advanced options + + + {isEnterInputsState(baseState) ? ( + + ) : null} + + + ({ footer: { @@ -14,7 +17,8 @@ export const useStyles = makeStyles((theme: Theme) => ({ width: '100%' }, inputsSection: { - padding: theme.spacing(2) + padding: theme.spacing(2), + maxHeight: theme.spacing(90) }, inputLabel: { color: theme.palette.text.hint, @@ -28,5 +32,25 @@ export const useStyles = makeStyles((theme: Theme) => ({ sectionHeader: { marginBottom: theme.spacing(1), marginTop: theme.spacing(1) + }, + advancedOptions: { + color: interactiveTextColor, + justifyContent: 'flex-end' + }, + noBorder: { + '&:before': { + height: 0 + } + }, + summaryWrapper: { + padding: 0 + }, + detailsWrapper: { + paddingLeft: 0, + paddingRight: 0, + flexDirection: 'column', + '& section': { + flex: 1 + } } })); diff --git a/src/components/Launch/LaunchForm/types.ts b/src/components/Launch/LaunchForm/types.ts index 65a3193a9..2d8a13f89 100644 --- a/src/components/Launch/LaunchForm/types.ts +++ b/src/components/Launch/LaunchForm/types.ts @@ -54,6 +54,10 @@ export interface WorkflowInitialLaunchParameters extends BaseInitialLaunchParameters { launchPlan?: Identifier; workflowId?: WorkflowId; + authRole?: Admin.IAuthRole; + disableAll?: boolean | null; + maxParallelism?: number | null; + qualityOfService?: Core.IQualityOfService | null; } export interface LaunchWorkflowFormProps extends BaseLaunchFormProps { workflowId: NamedEntityIdentifier; @@ -85,6 +89,10 @@ export interface LaunchRoleInputRef { getValue(): Admin.IAuthRole; validate(): boolean; } +export interface LaunchAdvancedOptionsRef { + getValues(): Admin.IExecutionSpec; + validate(): boolean; +} export interface WorkflowSourceSelectorState { launchPlanSelectorOptions: SearchableSelectorOption[]; @@ -110,7 +118,9 @@ export interface TaskSourceSelectorState { } export interface LaunchWorkflowFormState { + advancedOptionsRef: React.RefObject; formInputsRef: React.RefObject; + roleInputRef: React.RefObject; state: State< WorkflowLaunchContext, WorkflowLaunchEvent, diff --git a/src/components/Launch/LaunchForm/useLaunchWorkflowFormState.ts b/src/components/Launch/LaunchForm/useLaunchWorkflowFormState.ts index fa6f0ef2e..d2fb1deb1 100644 --- a/src/components/Launch/LaunchForm/useLaunchWorkflowFormState.ts +++ b/src/components/Launch/LaunchForm/useLaunchWorkflowFormState.ts @@ -16,15 +16,18 @@ import { workflowLaunchMachine, WorkflowLaunchTypestate } from './launchMachine'; -import { validate } from './services'; +import { validate as baseValidate } from './services'; import { LaunchFormInputsRef, + LaunchRoleInputRef, + LaunchAdvancedOptionsRef, LaunchWorkflowFormProps, LaunchWorkflowFormState, ParsedInput } from './types'; import { useWorkflowSourceSelectorState } from './useWorkflowSourceSelectorState'; import { getUnsupportedRequiredInputs } from './utils'; +import { correctInputErrors } from './constants'; async function loadLaunchPlans( { listLaunchPlans }: APIContextValue, @@ -155,6 +158,8 @@ async function loadInputs( async function submit( { createWorkflowExecution }: APIContextValue, formInputsRef: RefObject, + roleInputRef: RefObject, + advancedOptionsRef: RefObject, { launchPlan, referenceExecutionId, workflowVersion }: WorkflowLaunchContext ) { if (!launchPlan) { @@ -166,11 +171,25 @@ async function submit( if (formInputsRef.current === null) { throw new Error('Unexpected empty form inputs ref'); } + // if (roleInputRef.current === null) { + // throw new Error('Unexpected empty role input ref'); + // } + // if (advancedOptionsRef.current === null) { + // throw new Error('Unexpected empty advanced options ref'); + // } + + const authRole = roleInputRef.current?.getValue(); const literals = formInputsRef.current.getValues(); + const { disableAll, qualityOfService, maxParallelism } = + advancedOptionsRef.current?.getValues() || {}; const launchPlanId = launchPlan.id; const { domain, project } = workflowVersion; const response = await createWorkflowExecution({ + authRole, + disableAll, + qualityOfServiceTier: qualityOfService?.tier, + maxParallelism, domain, launchPlanId, project, @@ -185,16 +204,44 @@ async function submit( return newExecutionId; } +async function validate( + formInputsRef: RefObject, + roleInputRef: RefObject, + advancedOptionsRef: RefObject +) { + if (roleInputRef.current === null) { + throw new Error('Unexpected empty role input ref'); + } + + // if (!roleInputRef.current.validate()) { + // throw new Error(correctInputErrors); + // } + return baseValidate(formInputsRef); +} + function getServices( apiContext: APIContextValue, - formInputsRef: RefObject + formInputsRef: RefObject, + roleInputRef: RefObject, + advancedOptionsRef: RefObject ) { return { loadWorkflowVersions: partial(loadWorkflowVersions, apiContext), loadLaunchPlans: partial(loadLaunchPlans, apiContext), loadInputs: partial(loadInputs, apiContext), - submit: partial(submit, apiContext, formInputsRef), - validate: partial(validate, formInputsRef) + submit: partial( + submit, + apiContext, + formInputsRef, + roleInputRef, + advancedOptionsRef + ), + validate: partial( + validate, + formInputsRef, + roleInputRef, + advancedOptionsRef + ) }; } @@ -209,18 +256,30 @@ export function useLaunchWorkflowFormState({ // These values will be used to auto-select items from the workflow // version/launch plan drop downs. const { + authRole: defaultAuthRole, launchPlan: preferredLaunchPlanId, workflowId: preferredWorkflowId, - values: defaultInputValues + values: defaultInputValues, + disableAll, + maxParallelism, + qualityOfService } = initialParameters; const apiContext = useAPIContext(); const formInputsRef = useRef(null); + const roleInputRef = useRef(null); + const advancedOptionsRef = useRef(null); - const services = useMemo(() => getServices(apiContext, formInputsRef), [ - apiContext, - formInputsRef - ]); + const services = useMemo( + () => + getServices( + apiContext, + formInputsRef, + roleInputRef, + advancedOptionsRef + ), + [apiContext, formInputsRef, roleInputRef, advancedOptionsRef] + ); const [state, sendEvent, service] = useMachine< WorkflowLaunchContext, @@ -230,11 +289,15 @@ export function useLaunchWorkflowFormState({ ...defaultStateMachineConfig, services, context: { + defaultAuthRole, defaultInputValues, preferredLaunchPlanId, preferredWorkflowId, referenceExecutionId, - sourceId + sourceId, + disableAll, + maxParallelism, + qualityOfService } }); @@ -351,7 +414,9 @@ export function useLaunchWorkflowFormState({ }, [service, sendEvent]); return { + advancedOptionsRef, formInputsRef, + roleInputRef, state, service, workflowSourceSelectorState diff --git a/src/models/Execution/api.ts b/src/models/Execution/api.ts index b0b03b3f2..6ccb5db2c 100644 --- a/src/models/Execution/api.ts +++ b/src/models/Execution/api.ts @@ -94,6 +94,9 @@ export const getExecutionData = ( export interface CreateWorkflowExecutionArguments { authRole?: Admin.IAuthRole; domain: string; + disableAll?: boolean | null; + qualityOfServiceTier?: Core.QualityOfService.Tier | null; + maxParallelism?: number | null; inputs: Core.ILiteralMap; launchPlanId: Identifier; project: string; @@ -106,14 +109,39 @@ export const createWorkflowExecution = ( { authRole, domain, + disableAll, + qualityOfServiceTier, + maxParallelism, inputs, launchPlanId: launchPlan, project, referenceExecutionId: referenceExecution }: CreateWorkflowExecutionArguments, config?: RequestConfig -) => - postAdminEntity< +) => { + const spec: Admin.IExecutionSpec = { + inputs, + launchPlan, + metadata: { + referenceExecution, + principal: defaultExecutionPrincipal + } + }; + if (authRole?.assumableIamRole || authRole?.kubernetesServiceAccount) { + spec.authRole = authRole; + } + if (disableAll) { + spec.disableAll = disableAll; + } + if (qualityOfServiceTier !== undefined) { + spec.qualityOfService = { + tier: qualityOfServiceTier + }; + } + if (maxParallelism !== undefined) { + spec.maxParallelism = maxParallelism; + } + return postAdminEntity< Admin.IExecutionCreateRequest, Admin.ExecutionCreateResponse >( @@ -121,15 +149,7 @@ export const createWorkflowExecution = ( data: { project, domain, - spec: { - authRole, - inputs, - launchPlan, - metadata: { - referenceExecution, - principal: defaultExecutionPrincipal - } - } + spec }, path: endpointPrefixes.execution, requestMessageType: Admin.ExecutionCreateRequest, @@ -137,6 +157,7 @@ export const createWorkflowExecution = ( }, config ); +}; /** Submits a request to terminate a WorkflowExecution by id */ export const terminateWorkflowExecution = (