Skip to content
This repository has been archived by the owner on Jan 9, 2025. It is now read-only.

Commit

Permalink
Merge pull request #16 from tsdataclinic/feature/inputs
Browse files Browse the repository at this point in the history
Added ability to add params to a workflow
  • Loading branch information
Juan Pablo Sarmiento authored Oct 21, 2024
2 parents 53b8461 + 638baaa commit 9d92a41
Show file tree
Hide file tree
Showing 10 changed files with 259 additions and 24 deletions.
2 changes: 1 addition & 1 deletion server/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def run_workflow(
validation_results = process_workflow(filename, resource, {}, db_workflow.schema)

return WorkflowRunReport(
row_count=resource.rows,
row_count=resource.rows if resource.rows else 0,
filename=file.filename if file.filename else '',
workflow_id=workflow_id,
validation_failures=validation_results,
Expand Down
12 changes: 10 additions & 2 deletions server/models/workflow/workflow_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ class FieldsetSchema(BaseModel):
the column schemas. E.g. the column names, order, data types, allowable values.
"""

id: str # uuid
name: str # name of this fieldset, e.g. "demographic data columns"
id: str # uuid
name: str # name of this fieldset, e.g. "demographic data columns"
order_matters: bool = Field(alias="orderMatters") # enforces column order
fields: list[FieldSchema]
allow_extra_columns: Literal["no", "anywhere", "onlyAfterSchemaFields"] = Field(alias="allowExtraColumns")
Expand All @@ -90,8 +90,16 @@ class FieldsetSchema(BaseModel):
class WorkflowParam(BaseModel):
"""The schema representing an argument (an input) for the Workflow that
is passed in when a Workflow is kicked off.
Args:
- id: str - uuid, a stable id for this param that is not user-editable
- name: str - auto-generated name from the `display_name` to be used as the variable name for this param.
- display_name: str - user-editable display name of this param
- description: str
- required: bool
"""

id: str
name: str
display_name: str = Field(alias="displayName")
description: str
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
],
"params": [
{
"id": "123456",
"name": "fieldset_schema",
"displayName": "Fieldset Schema",
"description": "For testing",
Expand Down
6 changes: 5 additions & 1 deletion src/client/schemas.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,10 @@ export const $WorkflowCreate = {

export const $WorkflowParam = {
properties: {
id: {
type: 'string',
title: 'Id',
},
name: {
type: 'string',
title: 'Name',
Expand All @@ -548,7 +552,7 @@ export const $WorkflowParam = {
},
},
type: 'object',
required: ['name', 'displayName', 'description', 'required', 'type'],
required: ['id', 'name', 'displayName', 'description', 'required', 'type'],
title: 'WorkflowParam',
description: `The schema representing an argument (an input) for the Workflow that
is passed in when a Workflow is kicked off.`,
Expand Down
1 change: 1 addition & 0 deletions src/client/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export type WorkflowCreate = {
* is passed in when a Workflow is kicked off.
*/
export type WorkflowParam = {
id: string;
name: string;
displayName: string;
description: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
ComboboxItem,
} from '@mantine/core';
import * as Papa from 'papaparse';
import { IconSettingsFilled } from '@tabler/icons-react';
import { IconTrash } from '@tabler/icons-react';
import { modals } from '@mantine/modals';
import type { FieldsetSchema_Output } from '../../../client';
import { FieldSchemaRow } from './FieldSchemaRow';
Expand Down Expand Up @@ -129,21 +129,16 @@ export function FieldsetSchemaBlock({
className="relative"
legend={<Text>{fieldsetSchema.name}</Text>}
>
<Menu withArrow shadow="md" width={250} position="left">
<Menu.Target>
<ActionIcon
variant="transparent"
className="absolute -top-1 right-1"
color="dark"
size="sm"
>
<IconSettingsFilled />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item onClick={openDeleteModal}>Delete schema</Menu.Item>
</Menu.Dropdown>
</Menu>
<ActionIcon
aria-label="Delete Column Rule"
variant="transparent"
className="absolute -top-1 right-1"
color="dark"
size="sm"
onClick={openDeleteModal}
>
<IconTrash />
</ActionIcon>

<div className="space-y-3">
<TextInput
Expand Down
213 changes: 213 additions & 0 deletions src/components/SingleWorkflowView/ParamsEditor/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { WorkflowsService, FullWorkflow, WorkflowParam } from '../../../client';
import { IconTrash } from '@tabler/icons-react';
import { v4 as uuid } from 'uuid';
import {
Group,
Button,
Fieldset,
Checkbox,
ComboboxItem,
Select,
Text,
TextInput,
Stack,
ActionIcon,
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { useMutation } from '@tanstack/react-query';
import { WorkflowUtil } from '../../../util/WorkflowUtil';
import { processAPIData } from '../../../util/apiUtil';
import { notifications } from '@mantine/notifications';
import { modals } from '@mantine/modals';

type Props = {
workflow: FullWorkflow;
};

/**
* Convert a display string to a valid identifier name for a variable.
*/
function toVariableIdentifierName(displayName: string): string {
return (
displayName
.trim()
// Remove all non-alphanumeric characters except spaces
.replace(/[^\w\s]/g, '')
// Replace spaces to underscores
.replace(/\s+/g, '_')
// make first character lower case
.replace(/^[A-Z]/, (match) => match.toLowerCase())
);
}

function makeEmptyWorkflowParam(index: number): WorkflowParam {
const displayName = `Input ${index}`;
return {
id: uuid(),
displayName,
name: toVariableIdentifierName(displayName),
description: '',
required: true,
type: 'string',
};
}

const PARAM_TYPE_OPTIONS: ComboboxItem[] = [
{
value: 'string',
label: 'Text',
},
{
value: 'number',
label: 'Number',
},
{
value: 'string list',
label: 'Text list',
},
];

export function ParamsEditor({ workflow }: Props): JSX.Element {
const openDeleteModal = (inputIdx: number) => {
modals.openConfirmModal({
title: 'Delete input',
children: (
<Text size="sm">Are you sure you want to delete this input?</Text>
),
labels: { confirm: 'Delete input', cancel: 'Cancel' },
confirmProps: { color: 'red' },
onConfirm: () => {
workflowInputsForm.removeListItem('params', inputIdx);
},
});
};

const workflowInputsForm = useForm<{ params: WorkflowParam[] }>({
mode: 'controlled',
initialValues: {
params: workflow.schema.params,
},
transformValues: (values) => {
return {
params: values.params.map((param) => {
return {
...param,
name: toVariableIdentifierName(param.displayName),
};
}),
};
},
});

const saveInputsMutation = useMutation({
mutationFn: async (workflowParams: readonly WorkflowParam[]) => {
const workflowToSave = WorkflowUtil.updateWorkflowParams(
workflow,
workflowParams,
);
const savedWorkflow = await processAPIData(
WorkflowsService.updateWorkflow({
path: {
workflow_id: workflowToSave.id,
},
body: workflowToSave,
}),
);
return savedWorkflow;
},
});

const { params } = workflowInputsForm.getValues();

return (
<form
onSubmit={workflowInputsForm.onSubmit((paramsToSubmit) => {
saveInputsMutation.mutate(paramsToSubmit.params, {
onSuccess: () => {
notifications.show({
title: 'Success',
message: 'Inputs saved successfully',
color: 'green',
});
},
});
})}
>
<Stack>
{params.length === 0 ? (
<Text>No workflow inputs have been configured yet</Text>
) : (
<>
<Button type="submit">Save</Button>
{params.map((param, i) => {
return (
<Fieldset
key={param.id}
className="relative"
legend={<Text>{param.displayName}</Text>}
>
<ActionIcon
aria-label="Delete input"
variant="transparent"
className="absolute -top-1 right-1"
color="dark"
size="sm"
>
<IconTrash onClick={() => openDeleteModal(i)} />
</ActionIcon>

<Group>
<TextInput
key={workflowInputsForm.key(`params.${i}.displayName`)}
{...workflowInputsForm.getInputProps(
`params.${i}.displayName`,
)}
label="Display name"
/>
<Select
key={workflowInputsForm.key(`params.${i}.type`)}
{...workflowInputsForm.getInputProps(`params.${i}.type`)}
data={PARAM_TYPE_OPTIONS}
label="Type"
/>
<Checkbox
key={workflowInputsForm.key(`params.${i}.required`)}
{...workflowInputsForm.getInputProps(
`params.${i}.required`,
{
type: 'checkbox',
},
)}
label="Required"
/>
</Group>
<Group align="center">
<TextInput
key={workflowInputsForm.key(`params.${i}.description`)}
{...workflowInputsForm.getInputProps(
`params.${i}.description`,
)}
label="Description"
className="w-full"
/>
</Group>
</Fieldset>
);
})}
</>
)}
<Button
variant="outline"
onClick={() => {
workflowInputsForm.insertListItem(
'params',
makeEmptyWorkflowParam(params.length + 1),
);
}}
>
Add new input
</Button>
</Stack>
</form>
);
}
5 changes: 3 additions & 2 deletions src/components/SingleWorkflowView/Workspace/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useDisclosure } from '@mantine/hooks';
import { FullWorkflow } from '../../../client';
import { FieldsetSchemasEditor } from '../FieldsetSchemasEditor';
import { OperationEditor } from '../OperationEditor';
import { ParamsEditor } from '../ParamsEditor';

type Props = { workflow: FullWorkflow };

Expand All @@ -25,8 +26,8 @@ export function Workspace({ workflow }: Props): JSX.Element {
<Grid.Col span={7}>
<Stack>
<Stack>
<Title order={2}>Workflow Inputs</Title>
<Text fs="italic">Workflow Inputs are not implemented yet.</Text>
<Title order={2}>Inputs</Title>
<ParamsEditor workflow={workflow} />
</Stack>
<Stack>
<Title order={2}>Column Rules</Title>
Expand Down
2 changes: 0 additions & 2 deletions src/components/SingleWorkflowView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ export function SingleWorkflowView(): JSX.Element | null {
const [isTestWorkflowModalOpen, testWorkflowModalActions] =
useDisclosure(false);

console.log('Loaded workflow', workflow);

if (isLoading) {
return <Loader />;
}
Expand Down
14 changes: 14 additions & 0 deletions src/util/WorkflowUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
FieldsetSchema_Output,
FullWorkflow,
WorkflowSchema_Output,
WorkflowParam,
} from '../client';
import { getSingleWorkflowBaseURI } from './uriUtil';
import { ArrayElementType } from './types';
Expand All @@ -16,6 +17,19 @@ export const WorkflowUtil = {
return `${getSingleWorkflowBaseURI()}/${id}`;
},

updateWorkflowParams(
workflow: FullWorkflow,
params: readonly WorkflowParam[],
): FullWorkflow {
return {
...workflow,
schema: {
...workflow.schema,
params: params as WorkflowParam[],
},
};
},

updateFieldsetSchemas(
workflow: FullWorkflow,
fieldsetSchemas: readonly FieldsetSchema_Output[],
Expand Down

0 comments on commit 9d92a41

Please sign in to comment.