Skip to content
Draft
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
13 changes: 9 additions & 4 deletions portals/apps/trader-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ReactNode } from 'react'
import {type ReactNode} from 'react'
import {Routes, Route, Navigate} from 'react-router-dom'
import './App.css'
import {Layout} from './components/Layout'
Expand All @@ -9,10 +9,11 @@ import {PreconsignmentScreen} from "./screens/PreconsignmentScreen.tsx"
import {useAsgardeo, SignedOut} from '@asgardeo/react'
import {LoginScreen} from "./screens/LoginScreen.tsx";
import {ApiProvider, useApi} from './services/ApiContext'
import { UploadProvider } from '@opennsw/jsonforms-renderers'
import { uploadFile, getDownloadUrl } from './services/upload'
import {UploadProvider} from '@opennsw/jsonforms-renderers'
import {uploadFile, getDownloadUrl} from './services/upload'
import {TestWorkflowScreen} from "./screens/TestWorkflowScreen.tsx";

function UploadWrapper({ children }: { children: ReactNode }) {
function UploadWrapper({children}: { children: ReactNode }) {
const api = useApi()
return (
<UploadProvider
Expand Down Expand Up @@ -43,6 +44,10 @@ function App() {
<Routes>
<Route path="/login" element={<SignedOut><LoginScreen/></SignedOut>}/>

if (import.meta.env.DEV) {
<Route path="/test-workflow" element={<TestWorkflowScreen/>}/>
}

<Route element={<ProtectedLayout/>}>
<Route path="/" element={<Navigate to="/consignments" replace/>}/>
<Route path="/consignments" element={<ConsignmentScreen/>}/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Handle, Position } from '@xyflow/react'
import { Text, Tooltip } from '@radix-ui/themes'
import type { WorkflowNodeV2 } from '../../services/types/workflow'

interface EventNodeProps {
step: WorkflowNodeV2
targetPosition?: Position
sourcePosition?: Position
}

export function EventNode({ step, targetPosition = Position.Left, sourcePosition = Position.Right }: EventNodeProps) {
const isStart = step.event_type === 'START'

return (
<div className="flex flex-col items-center">
<Handle type="target" position={targetPosition} className="bg-slate-400! w-2! h-2!" />
<Tooltip content={step.name}>
<div
className={`w-10 h-10 rounded-full border-2 flex items-center justify-center ${isStart ? 'bg-emerald-50 border-emerald-400' : 'bg-red-50 border-red-400'
} shadow-sm`}
>
<div className={`w-3 h-3 rounded-full ${isStart ? 'bg-emerald-500' : 'bg-red-500'}`} />
</div>
</Tooltip>
<Text size="1" weight="bold" color="gray" className="mt-1">{step.name}</Text>
<Handle type="source" position={sourcePosition} className="bg-slate-400! w-2! h-2!" />
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Handle, Position } from '@xyflow/react'
import { Text, Tooltip } from '@radix-ui/themes'
import { PlusIcon, Cross2Icon, QuestionMarkIcon } from '@radix-ui/react-icons'
import type { WorkflowNodeV2 } from '../../services/types/workflow'
import { getStatusStyle } from './utils'

interface GatewayNodeProps {
step: WorkflowNodeV2
targetPosition?: Position
sourcePosition?: Position
}

export function GatewayNode({ step, targetPosition = Position.Left, sourcePosition = Position.Right }: GatewayNodeProps) {
const statusStyle = getStatusStyle(step.state)
const isParallel = step.gateway_type?.includes('PARALLEL')
const isExclusive = step.gateway_type?.includes('EXCLUSIVE')

return (
<div className="relative flex items-center justify-center">
<Handle type="target" position={targetPosition} className="bg-slate-400! w-2! h-2!" />
<Tooltip content={step.name}>
<div
className={`w-12 h-12 rotate-45 border-2 flex items-center justify-center bg-white ${statusStyle.borderColor} shadow-sm group hover:scale-110 transition-transform`}
>
<div className="-rotate-45 flex items-center justify-center">
{isParallel ? (
<PlusIcon className={`w-6 h-6 ${statusStyle.iconColor}`} />
) : isExclusive ? (
<Cross2Icon className={`w-6 h-6 ${statusStyle.iconColor}`} />
) : (
<QuestionMarkIcon className={`w-6 h-6 ${statusStyle.iconColor}`} />
)}
</div>
</div>
</Tooltip>
<div className="absolute top-full mt-2 whitespace-nowrap">
<Text size="1" weight="bold" color="gray">{step.name}</Text>
</div>
<Handle type="source" position={sourcePosition} className="bg-slate-400! w-2! h-2!" />
</div>
)
}
138 changes: 138 additions & 0 deletions portals/apps/trader-app/src/components/WorkflowViewer/TaskNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React, {useState} from 'react'
import {Handle, Position} from '@xyflow/react'
import {Text, Tooltip} from '@radix-ui/themes'
import {useParams, useNavigate} from 'react-router-dom'
import {
PlayIcon,
UpdateIcon,
FileTextIcon,
ReaderIcon,
} from '@radix-ui/react-icons'
import type {WorkflowNodeV2} from '../../services/types/workflow'
import type {WorkflowNode as LegacyWorkflowNode} from '../../services/types/consignment'
import {getStatusStyle, getStepLabel, getTooltipContent, legacyTypeIcons} from './utils'

interface TaskNodeProps {
step: WorkflowNodeV2 | LegacyWorkflowNode
targetPosition?: Position
sourcePosition?: Position
}

export function TaskNode({step, targetPosition = Position.Left, sourcePosition = Position.Right}: TaskNodeProps) {
const {consignmentId} = useParams<{ consignmentId: string }>()
const navigate = useNavigate()
const [isLoading, setIsLoading] = useState(false)

const statusStyle = getStatusStyle(step.state)
const isV2Step = 'type' in step
const isExecutable = step.state === 'READY'
const isViewable = step.state !== 'LOCKED' && !isExecutable && (!isV2Step || (step as WorkflowNodeV2).type === 'TASK')

const getViewButtonColors = () => {
switch (step.state) {
case 'COMPLETED':
return 'bg-emerald-500 hover:bg-emerald-600 active:bg-emerald-700'
case 'IN_PROGRESS':
return 'bg-orange-500 hover:bg-orange-600 active:bg-orange-700'
case 'REJECTED':
return 'bg-red-500 hover:bg-red-600 active:bg-red-700'
default:
return 'bg-slate-500 hover:bg-slate-600 active:bg-slate-700'
}
}

const handleOpen = (e: React.MouseEvent) => {
e.stopPropagation()
if (!consignmentId) {
console.error('No consignment ID found in URL')
return
}

setIsLoading(true)
navigate(`/consignments/${consignmentId}/tasks/${step.id}`)
}

const icon = isV2Step
? <FileTextIcon className="w-3.5 h-3.5"/>
: legacyTypeIcons[(step as LegacyWorkflowNode).workflowNodeTemplate.type] || <FileTextIcon className="w-3.5 h-3.5"/>

return (
<div
className={`px-3 py-2 rounded-lg border-2 hover:cursor-default shadow-sm w-72 min-h-[80px] flex flex-col justify-center ${statusStyle.bgColor
} ${statusStyle.borderColor} ${step.state === 'READY' ? 'ring-2 ring-blue-300 ring-offset-2' : ''
}`}
>
<Handle
type="target"
position={targetPosition}
className="bg-slate-400! w-3! h-3!"
/>
<div className="flex items-center justify-between gap-2">
<div className="flex items-start gap-2 flex-1 min-w-0">
<div className={`mt-0.5 shrink-0 ${statusStyle.iconColor}`}>
{icon}
</div>
<div className="min-w-0">
<Tooltip content={getTooltipContent(step)}>
<Text
size="1"
weight="bold"
className={`${statusStyle.textColor} block cursor-pointer whitespace-normal break-words leading-tight`}
>
{getStepLabel(step)}
</Text>
</Tooltip>
<div>
<Text size="1" className={`${statusStyle.textColor} font-mono mt-0.5 text-xs`}>
{step.state}
</Text>
</div>
<div>
{step.state === "IN_PROGRESS" &&
<div className={`${statusStyle.textColor} mt-0.5 text-[0.5rem] italic`}>
{step.extendedState}
</div>
}
</div>
</div>
</div>

{isExecutable && (
<button
onClick={handleOpen}
disabled={isLoading}
className="flex items-center justify-center w-8 h-8 rounded-full bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white shadow-md hover:cursor-pointer hover:shadow-lg transition-all duration-150 shrink-0 disabled:bg-slate-400 disabled:cursor-not-allowed"
title="Execute task"
>
{isLoading ? (
<UpdateIcon className="w-4 h-4 animate-spin"/>
) : (
<PlayIcon className="w-4 h-4 ml-0.5"/>
)}
</button>
)}

{isViewable && (
<button
onClick={handleOpen}
disabled={isLoading}
className={`flex items-center justify-center w-8 h-8 rounded-full ${getViewButtonColors()} text-white shadow-md hover:cursor-pointer hover:shadow-lg transition-all duration-150 shrink-0 disabled:bg-slate-400 disabled:cursor-not-allowed`}
title="View task"
>
{isLoading ? (
<UpdateIcon className="w-4 h-4 animate-spin"/>
) : (
<ReaderIcon className="w-4 h-4"/>
)}
</button>
)}
</div>

<Handle
type="source"
position={sourcePosition}
className="bg-slate-400! w-3! h-3!"
/>
</div>
)
}
Loading
Loading