Skip to content

Commit

Permalink
fix(dashboard): added react-hook-forms
Browse files Browse the repository at this point in the history
* added form component
* added mark as null component
  • Loading branch information
muhammadqamar committed Nov 14, 2024
1 parent 4ecb4cf commit 7ce5bf1
Show file tree
Hide file tree
Showing 19 changed files with 1,188 additions and 295 deletions.
1,024 changes: 867 additions & 157 deletions dashboard/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.1.1",
"@tanstack/react-query": "^5.37.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { forwardRef } from 'react'
import { useForm, FormProvider } from 'react-hook-form'
import { FormFields } from './components/FormFields'
import { ThreadVarDef } from 'littlehorse-client/proto'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'

type Prop = {
wfSpecVariables: ThreadVarDef[]
onSubmit: any
}
ThreadVarDef

// eslint-disable-next-line react/display-name
export const WfRunForm = forwardRef<HTMLDivElement, Prop>(({ wfSpecVariables, onSubmit }, ref) => {
const methods = useForm()
const { register, handleSubmit, formState } = methods

const onSubmitForm = data => {
onSubmit(data)
}

return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmitForm)} ref={ref}>
<div>
<Label htmlFor={'custom-id'}>Id (Custom WfRun Id)</Label>
<Input type="text" className="mb-4 mt-1" id="custom-id" {...register('custom-id-wfRun-flow')} />
</div>
{!!wfSpecVariables?.length &&
wfSpecVariables.map((variable: ThreadVarDef, index: number) => (
<FormFields key={`form-field-${index}`} variables={variable} register={register} formState={formState} />
))}
</form>
</FormProvider>
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React, { FC } from 'react'
import { FormComponent } from './formType'
import { VariableType, ThreadVarDef } from 'littlehorse-client/proto'
import { FieldValues, UseFormRegister } from 'react-hook-form'

type Prop = {
variables: ThreadVarDef
register: UseFormRegister<FieldValues>
formState: any
}
export const FormFields: FC<Prop> = props => {
const type = props.variables?.varDef?.type as VariableType
if (!type) return
const Component = FormComponent[type]
return <Component {...props} />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { FC, useState } from 'react'
import { VariableType } from 'littlehorse-client/proto'
import { FormFieldProp } from '@/types'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { MarkFieldNull } from "./MarkFieldNull"
export const FormInput: FC<FormFieldProp> = props => {
const [isDisabled, setIsDisabled] = useState(false)
if (!props.variables?.varDef?.name) return null

const {
variables: {
varDef: { type, name },
required,
},
register,
formState: { errors },
} = props

const variableToType = (variable: VariableType) => {
switch (variable) {
case VariableType.INT:
return 'number'
case VariableType.STR:
return 'text'
default:
return 'text'
}
}

return (
<div>
<div className="flex justify-between">
<Label htmlFor={name}>
{name} {required && <span className="text-red-700">*</span>}
</Label>
{!required && <MarkFieldNull name={name} setIsDisabled={setIsDisabled} />}
</div>
<Input
type={variableToType(type)}
className="mb-4 mt-1"
id={name}
disabled={isDisabled}
{...register(name, { required: required ? `${name} is required` : false })}
/>
{errors[name] && <p className="text-sm text-red-700">{errors[name]?.message}</p>}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, { FC, useState } from 'react'
import { Label } from '@/components/ui/label'
import { FormFieldProp } from '@/types'
import { SwitchButton } from '@/components/ui/switch'
import { MarkFieldNull } from './MarkFieldNull'

export const FormSwitch: FC<FormFieldProp> = props => {
const [isDisabled, setIsDisabled] = useState(false)
if (!props.variables?.varDef?.name) return
const {
variables: {
varDef: { type, name },
required,
},
register,
formState: { errors },
} = props

return (
<div>
<div className="flex justify-between">
<Label htmlFor={name}>
{name} {required && <span className="text-red-700">*</span>}
</Label>
{!required && <MarkFieldNull name={name} setIsDisabled={setIsDisabled} />}
</div>

<div className="mb-4 mt-1">
<SwitchButton disabled={isDisabled} />
</div>
{errors[name] && <p className="text-red-700 text-sm">{errors[name]?.message}</p>}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { FC, useState } from 'react'
import { FormFieldProp } from '@/types'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { MarkFieldNull } from './MarkFieldNull'

export const FormTextarea: FC<FormFieldProp> = props => {
const [isDisabled, setIsDisabled] = useState(false)
if (!props.variables?.varDef?.name) return
const {
variables: {
varDef: { name },
required,
},
register,
formState: { errors },
} = props
return (
<div>
<div className="flex justify-between">
<Label htmlFor={name}>
{name} {required && <span className="text-red-700">*</span>}
</Label>
{!required && <MarkFieldNull name={name} setIsDisabled={setIsDisabled} />}
</div>
<Textarea
className="mb-4 mt-1"
id={name}
disabled={isDisabled}
{...register(name, { required: required ? `${name} is required` : false })}
/>
{errors[name] && <p className="text-red-700 text-sm">{errors[name]?.message}</p>}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, { FC } from 'react'
import { useFormContext } from 'react-hook-form'
import { Label } from '@/components/ui/label'

type Prop = { name: string; setIsDisabled: (checked: boolean) => void }
export const MarkFieldNull: FC<Prop> = ({ name, setIsDisabled }) => {
const { setValue } = useFormContext()

const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const checked = e.target.checked
setIsDisabled(checked)
if (checked) {
setValue(name, null)
}
}

return (
<Label htmlFor="mark-null" className="flex items-center gap-2">
mark as null
<input id="mark-null" type="checkbox" onChange={handleCheckboxChange} />
</Label>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { FormInput, FormTextarea, FormSwitch } from './'
import { VariableType } from 'littlehorse-client/proto'

export const FormComponent = {
[VariableType.INT]: FormInput,
[VariableType.STR]: FormInput,
[VariableType.DOUBLE]: FormInput,
[VariableType.BOOL]: FormSwitch,
[VariableType.JSON_OBJ]: FormTextarea,
[VariableType.JSON_ARR]: FormTextarea,
[VariableType.BYTES]: FormInput,
[VariableType.UNRECOGNIZED]: FormInput,
} as const

export type FormFieldType = keyof typeof FormComponent
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './FormInput'
export * from './FormTextarea'
export * from './FormSwitch'
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,44 @@ import { useModal } from '../../hooks/useModal'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { Modal } from '../../context'
import { FC } from 'react'
import { WfSpec } from 'littlehorse-client/proto'
import { ThreadVarDef, WfSpec } from 'littlehorse-client/proto'
import { runWfSpec } from '../../wfSpec/[...props]/actions/runWfSpec'
import { useWhoAmI } from '@/contexts/WhoAmIContext'
import { useRouter } from 'next/navigation'
import { WfForm } from '@/components/ui/wfForm'
import { WfRunForm } from '@/app/(authenticated)/(diagram)/components/Forms/WfRunForm'

export const ExecuteWorkflowRun: FC<Modal> = ({ data }) => {
const { showModal, setShowModal } = useModal()
const lhWorkflowSpec = data as WfSpec
console.log(lhWorkflowSpec)
const { tenantId } = useWhoAmI()
const router = useRouter()
const formRef = useRef<any>(null);
const handleFormSubmit = async (event: any) => {
console.log('values', event)
// event.preventDefault()
const formRef = useRef<any>(null)
const wfSpecVariables = lhWorkflowSpec.threadSpecs?.entrypoint?.variableDefs
const formatVariablesPayload = (values): any => {
console.log('values', values)
const transformedObj = Object.keys(values).reduce((acc, key) => {
acc[key] = { [matchVariableType(key)]: values[key] }
return acc
}, {})

return transformedObj
}

const matchVariableType = (key: string): string => {
return wfSpecVariables.filter((variable: ThreadVarDef) => variable.varDef?.name === key)[0]?.varDef?.type as string
}

const handleFormSubmit = async (values: any) => {
const customWfRunId = values['custom-id-wfRun-flow'] || undefined
delete values['custom-id-wfRun-flow']
if (!lhWorkflowSpec.id) return
const wfRun = await runWfSpec({
...lhWorkflowSpec.id,
wfSpecName: lhWorkflowSpec.id.name,
tenantId,
// id:"test-variables-greet-3",
variables: {
'input-name': { str: 'qamar' },
},
id: customWfRunId,
variables: formatVariablesPayload(values),
// parentWfRunId:{
// id: ,
// lhWorkflowSpec.parentWfSpec?.wfSpecName
Expand All @@ -37,32 +51,32 @@ export const ExecuteWorkflowRun: FC<Modal> = ({ data }) => {
setShowModal(false)
router.push(`/wfRun/${wfRun.id.id}`)
}
const triggerSubmit = () => {

};

return (
<Dialog open={showModal} onOpenChange={open => setShowModal(open)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Execute Workflow</DialogTitle>
<DialogTitle>Execute {lhWorkflowSpec.id?.name}</DialogTitle>
</DialogHeader>
<div>
{lhWorkflowSpec.threadSpecs?.entrypoint &&
lhWorkflowSpec.threadSpecs?.entrypoint?.variableDefs?.some(def => def.required) ? (
<WfForm variableDefs={lhWorkflowSpec.threadSpecs.entrypoint.variableDefs} onSubmit={handleFormSubmit} formRef={formRef} />
) : (
<p>Currently no variables required!</p>
)}
<DialogFooter>
<div className="mt-2 flex gap-1">
<button className="rounded-sm bg-gray-100 px-4 py-1 text-gray-900" onClick={() => setShowModal(false)}>
Cancel
</button>
<button onClick={() => {formRef.current?.requestSubmit()}} type="submit" className="flex items-center gap-1 rounded-sm bg-blue-500 p-1 px-4 text-white">
Run
</button>
</div>
</DialogFooter>
<hr className="mb-2" />
<WfRunForm wfSpecVariables={wfSpecVariables} onSubmit={handleFormSubmit} ref={formRef} />
<DialogFooter>
<div className="mt-2 flex gap-1">
<button className="rounded-sm bg-gray-100 px-4 py-1 text-gray-900" onClick={() => setShowModal(false)}>
Cancel
</button>
<button
onClick={() => {
formRef.current?.requestSubmit()
}}
type="submit"
className="flex items-center gap-1 rounded-sm bg-blue-500 p-1 px-4 text-white"
>
Run
</button>
</div>
</DialogFooter>
</div>
</DialogContent>
</Dialog>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
'use server'
import { lhClient } from '@/app/lhClient'
import { WithTenant } from '@/types'
import { WfRun, RunWfRequest, VariableValue } from 'littlehorse-client/proto'
import { WfRun, RunWfRequest } from 'littlehorse-client/proto'

type RunWf = {
variables?: { [key: string]: VariableValue };
} & Omit<RunWfRequest, 'variables'> &
WithTenant
export const runWfSpec = async ({
wfSpecName,
tenantId,
majorVersion,
revision,
parentWfRunId,
id,
}: RunWf): Promise<WfRun> => {
variables
}: RunWfRequest & WithTenant ): Promise<WfRun> => {
const client = await lhClient({ tenantId })
return client.runWf({ wfSpecName, majorVersion, revision, parentWfRunId, id })
return client.runWf({ wfSpecName, majorVersion, revision, parentWfRunId, id,variables })
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Details } from './Details'
import { Thread } from './Thread'
import { WfRuns } from './WfRuns'
import { useModal } from '../../../hooks/useModal'
import { LucidePlayCircle } from 'lucide-react'


type WfSpecProps = {
spec: Spec
Expand All @@ -26,10 +28,11 @@ export const WfSpec: FC<WfSpecProps> = ({ spec }) => {
<div className='flex justify-between items-center'>
<Details status={spec.status} id={spec.id} />
<button
className='flex items-center gap-1 p-1 text-white bg-blue-500 rounded-sm'
className='flex items-center gap-1 p-2 px-4 text-white bg-blue-500 rounded-sm'
onClick={onClick}
>
Execute Workflow
<LucidePlayCircle size={18} />
Execute
</button>
</div>
<Diagram spec={spec} />
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/components/ui/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type,
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:bg-gray-300 disabled:opacity-50',
className
)}
ref={ref}
Expand Down
Loading

0 comments on commit 7ce5bf1

Please sign in to comment.