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 = (