Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"@rjsf/validator-ajv8": "5.24.10",
"@sentry/nextjs": "8.55.0",
"@squonk/account-server-client": "4.2.1",
"@squonk/data-manager-client": "4.1.0",
"@squonk/data-manager-client": "4.1.5",
"@squonk/mui-theme": "5.0.0",
"@squonk/sdf-parser": "1.3.1",
"@tanstack/match-sorter-utils": "8.19.4",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { type InputFieldSchema } from "../../../runCards/JobCard/JobInputFields"
import { TEST_JOB_ID } from "../../../runCards/TestJob/jobId";

// Contains only fields we are interested in
type ApplicationSpecification = { variables: Record<string, unknown> };
type ApplicationSpecification = { variables?: Record<string, unknown> };

// Contains only fields we are interested in
type JobInput = { title: string; type: InputFieldSchema["type"] };
Expand All @@ -27,6 +27,8 @@ export const useGetJobInputs = (instance: InstanceGetResponse | InstanceSummary)
{ query: { enabled: inputsEnabled, retry: instance.job_id === TEST_JOB_ID ? 1 : 3 } },
);

console.log(instance);

// Parse application specification
const applicationSpecification: ApplicationSpecification = instance.application_specification
? JSON.parse(instance.application_specification)
Expand All @@ -40,10 +42,10 @@ export const useGetJobInputs = (instance: InstanceGetResponse | InstanceSummary)
// Get information about inputs that were provided when creating the job with their respective
// values
const usedInputs = Object.entries(jobVariables.properties)
.filter(([name]) => Boolean(applicationSpecification.variables[name]))
.filter(([name]) => Boolean(applicationSpecification.variables?.[name]))
.map(([name, jobInput]) => {
// Let's assume inputs can only contain string or array of strings as values
const value = applicationSpecification.variables[name] as string[] | string;
const value = applicationSpecification.variables?.[name] as string[] | string;

return {
name,
Expand Down
81 changes: 81 additions & 0 deletions src/components/runCards/JobCard/JobInputsAndOptionsForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { type Dispatch, type RefObject, type SetStateAction } from "react";

import { type JobOrderDetail } from "@squonk/data-manager-client";

import { Grid2 as Grid, Typography } from "@mui/material";
import { Form } from "@rjsf/mui";
import validator from "@rjsf/validator-ajv8";

import { JobInputFields } from "./JobInputFields";
import { type InputData } from "./JobModal";

interface JobInputsAndOptionsFormProps {
inputs?: any;
options?: any;
order: JobOrderDetail["options"];
projectId: string;
inputsData: InputData;
setInputsData: Dispatch<SetStateAction<InputData>>;
optionsFormData: any;
setOptionsFormData: Dispatch<SetStateAction<any>>;
formRef: RefObject<any>;
specVariables?: any;
}

export const JobInputsAndOptionsForm = ({
inputs,
options,
order,
projectId,
inputsData,
setInputsData,
optionsFormData,
setOptionsFormData,
formRef,
specVariables,
}: JobInputsAndOptionsFormProps) => {
return (
<Grid container spacing={2}>
<>
<Grid size={{ xs: 12 }}>
<Typography component="h3" sx={{ fontWeight: "bold" }} variant="subtitle1">
Inputs
</Typography>
</Grid>
{!!inputs && (
<JobInputFields
initialValues={specVariables}
inputs={inputs}
inputsData={inputsData}
projectId={projectId}
onChange={setInputsData}
/>
)}
</>
<Grid size={{ xs: 12 }}>
{!!options && (
<>
<Typography component="h3" sx={{ fontWeight: "bold" }} variant="subtitle1">
Options
</Typography>
<Form
liveValidate
noHtml5Validate
formData={optionsFormData}
ref={formRef}
schema={options}
showErrorList="bottom"
uiSchema={{ "ui:order": order }}
validator={validator}
onChange={(event) => setOptionsFormData(event.formData)}
onError={() => {}}
>
{/* Remove the default submit button */}
<div />
</Form>
</>
)}
</Grid>
</Grid>
);
};
81 changes: 17 additions & 64 deletions src/components/runCards/JobCard/JobModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,17 @@ import {
import { getGetInstancesQueryKey, useCreateInstance } from "@squonk/data-manager-client/instance";
import { useGetJob } from "@squonk/data-manager-client/job";

import { Box, Grid2 as Grid, TextField, Typography } from "@mui/material";
import { type FormProps } from "@rjsf/core";
import validator from "@rjsf/validator-ajv8";
import { Box, TextField } from "@mui/material";
import { useQueryClient } from "@tanstack/react-query";
import dynamic from "next/dynamic";

import { useEnqueueError } from "../../../hooks/useEnqueueStackError";
import { CenterLoader } from "../../CenterLoader";
import { ModalWrapper } from "../../modals/ModalWrapper";
import { DebugCheckbox, type DebugValue } from "../DebugCheckbox";
import { TEST_JOB_ID } from "../TestJob/jobId";
import { type CommonModalProps } from "../types";
import { type InputSchema, type JobInputFieldsProps, validateInputData } from "./JobInputFields";

const JobInputFields = dynamic<JobInputFieldsProps>(
() => import("./JobInputFields").then((mod) => mod.JobInputFields),
{ loading: () => <CenterLoader /> },
);

// this dynamic import is necessary to avoid hydration issues with the form
const Form = dynamic<FormProps>(() => import("@rjsf/mui").then((mod) => mod.Form), {
loading: () => <CenterLoader />,
});
import { type InputSchema, validateInputData } from "./JobInputFields";
import { JobInputsAndOptionsForm } from "./JobInputsAndOptionsForm";

export type InputData = Record<string, string[] | string | undefined>;

Expand Down Expand Up @@ -167,16 +155,14 @@ export const JobModal = ({
}
};

const validateForm = formRef.current?.validateForm;
const variables = job?.variables;

return (
<ModalWrapper
DialogProps={{ maxWidth: "md", fullWidth: true }}
id={`job-${jobId}`}
open={open}
submitDisabled={
(validateForm && validateForm() !== undefined && validateForm()) ?? !inputsValid
}
submitDisabled={!inputsValid}
submitText="Run"
title={job?.name ?? "Run Job"}
onClose={onClose}
Expand All @@ -194,51 +180,18 @@ export const JobModal = ({
</Box>

<DebugCheckbox value={debug} onChange={(debug) => setDebug(debug)} />
{!!job.variables && (
<Grid container spacing={2}>
{!!job.variables.inputs && (
<>
<Grid size={{ xs: 12 }}>
<Typography component="h3" sx={{ fontWeight: "bold" }} variant="subtitle1">
Inputs
</Typography>
</Grid>
<JobInputFields
initialValues={specVariables}
inputs={job.variables.inputs as any} // TODO: should validate this with zod
inputsData={inputsData}
projectId={projectId}
onChange={setInputsData}
/>
</>
)}

<Grid size={{ xs: 12 }}>
{!!job.variables.options && (
<>
<Typography component="h3" sx={{ fontWeight: "bold" }} variant="subtitle1">
Options
</Typography>
<Form
liveValidate
noHtml5Validate
formData={optionsFormData}
ref={formRef}
schema={job.variables.options} // TODO: should validate this with zod
showErrorList="bottom"
uiSchema={{ "ui:order": job.variables.order?.options }}
validator={validator}
onChange={(event) => setOptionsFormData(event.formData)}
onError={() => {}}
>
{/* Remove the default submit button */}
<div />
</Form>
</>
)}
</Grid>
</Grid>
)}
<JobInputsAndOptionsForm
formRef={formRef}
inputs={variables?.inputs}
inputsData={inputsData}
options={variables?.options}
optionsFormData={optionsFormData}
order={variables?.order?.options ?? []}
projectId={projectId}
setInputsData={setInputsData}
setOptionsFormData={setOptionsFormData}
specVariables={specVariables}
/>
</>
) : (
<CenterLoader />
Expand Down
59 changes: 59 additions & 0 deletions src/components/runCards/WorkflowCard/RunWorkflowButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useState } from "react";

import { type WorkflowSummary } from "@squonk/data-manager-client";

import { Button, CircularProgress, Tooltip } from "@mui/material";
import dynamic from "next/dynamic";

export interface RunWorkflowButtonProps {
workflowId: WorkflowSummary["id"];
projectId: string;
onLaunch?: (instanceId: string) => void;
disabled?: boolean;
}

const WorkflowModal = dynamic<any>(
() => import("./WorkflowModal").then((mod) => mod.WorkflowModal),
{ loading: () => <CircularProgress size="1rem" /> },
);

/**
* MuiButton that controls a modal to run a workflow instance
*/
export const RunWorkflowButton = ({
workflowId,
projectId,
onLaunch,
disabled,
}: RunWorkflowButtonProps) => {
const [open, setOpen] = useState(false);
const [hasOpened, setHasOpened] = useState(false);

return (
<>
<Tooltip title="Run workflow">
<span>
<Button
color="primary"
disabled={disabled ?? !projectId}
onClick={() => {
setOpen(true);
setHasOpened(true);
}}
>
Run
</Button>
</span>
</Tooltip>
{!!hasOpened && (
<WorkflowModal
open={open}
projectId={projectId}
workflowId={workflowId}
onClose={() => setOpen(false)}
onLaunch={onLaunch}
/>
)}
</>
);
};
60 changes: 60 additions & 0 deletions src/components/runCards/WorkflowCard/WorkflowCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { type WorkflowSummary } from "@squonk/data-manager-client";

import { Chip, Typography } from "@mui/material";

import { useCurrentProjectId } from "../../../hooks/projectHooks";
import { BaseCard } from "../../BaseCard";
import { RunWorkflowButton } from "./RunWorkflowButton";

export interface WorkflowCardProps {
workflow: WorkflowSummary;
}

/**
* MuiCard that displays a summary of a workflow definition.
*/
export const WorkflowCard = ({ workflow }: WorkflowCardProps) => {
const { projectId } = useCurrentProjectId();
return (
<BaseCard
actions={() => (
<RunWorkflowButton
disabled={!projectId}
projectId={projectId ?? ""}
workflowId={workflow.id}
/>
)}
header={{
color: "#f1c40f",
subtitle: workflow.name,
avatar: workflow.name[0],
title: workflow.workflow_name ?? workflow.name,
}}
key={workflow.id}
>
<Typography gutterBottom>
{workflow.workflow_description ?? <em>No description</em>}
</Typography>
<Typography gutterBottom variant="body2">
Version: {workflow.version ?? <em>n/a</em>}
</Typography>
<Typography gutterBottom variant="body2">
Scope: {workflow.scope}
{workflow.scope_id ? ` (${workflow.scope_id})` : null}
</Typography>
<Typography gutterBottom variant="body2">
Validated:{" "}
{workflow.validated ? (
<Chip color="success" label="Validated" size="small" />
) : (
<Chip color="warning" label="Not validated" size="small" />
)}
</Typography>
{!!workflow.source_id && (
<Typography gutterBottom variant="body2">
Source Workflow ID: {workflow.source_id}
</Typography>
)}
</BaseCard>
);
};
Loading