From df1344c228c25114a544dfb91997e0b631a255e7 Mon Sep 17 00:00:00 2001 From: Ida Dittrich Date: Mon, 9 Mar 2026 13:49:03 +0100 Subject: [PATCH] updated feature automation and feature chat playground routes --- src/App.tsx | 9 +- src/api/workflowApi.ts | 140 +++++++++--------- src/hooks/useWorkflows.ts | 45 +++--- .../automation/AutomationDefinitionsView.tsx | 42 ++++-- src/pages/workflows/WorkflowsPage.tsx | 17 ++- 5 files changed, 147 insertions(+), 106 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7a058cd..d56bc0a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -102,12 +102,15 @@ function App() { } /> {/* ============================================== */} - {/* WORKFLOWS ROUTES (global) */} + {/* WORKFLOWS ROUTES (deprecated - redirect to /) */} + {/* Workflows are accessed via feature routes: */} + {/* /mandates/:mandateId/chatplayground/:id/workflows */} + {/* /mandates/:mandateId/automation/:id/definitions */} {/* ============================================== */} } /> - } /> - } /> + } /> + } /> {/* ============================================== */} diff --git a/src/api/workflowApi.ts b/src/api/workflowApi.ts index a6191b4..3d7b0db 100644 --- a/src/api/workflowApi.ts +++ b/src/api/workflowApi.ts @@ -69,17 +69,24 @@ export type ApiRequestFunction = (options: ApiRequestOptions) => Promise { - console.log('📤 fetchWorkflows: Making API request to /api/workflows/'); +export async function fetchWorkflows( + request: ApiRequestFunction, + params?: { pagination?: string }, + apiBaseUrl?: string +): Promise { + const url = apiBaseUrl ? `${apiBaseUrl}/workflows` : '/api/workflows/'; + console.log('📤 fetchWorkflows: Making API request to', url); try { - const data = await request({ - url: '/api/workflows/', - method: 'get' - }); + const requestConfig: any = { url, method: 'get' as const }; + if (params?.pagination) { + requestConfig.params = { pagination: params.pagination }; + } + const data = await request(requestConfig); console.log('📥 fetchWorkflows: API response:', data); @@ -129,30 +136,30 @@ export async function fetchWorkflows(request: ApiRequestFunction): Promise { - return await request({ - url: `/api/workflows/${workflowId}`, - method: 'get' - }); + const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}` : `/api/workflows/${workflowId}`; + return await request({ url, method: 'get' }); } /** * Fetch workflow status (lightweight status check) - * Endpoint: GET /api/workflows/{workflowId}/status + * When apiBaseUrl provided: GET {apiBaseUrl}/workflows/{workflowId}/status + * Otherwise: GET /api/workflows/{workflowId}/status */ export async function fetchWorkflowStatus( request: ApiRequestFunction, - workflowId: string + workflowId: string, + apiBaseUrl?: string ): Promise { - const data = await request({ - url: `/api/workflows/${workflowId}/status`, - method: 'get' - }); + const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/status` : `/api/workflows/${workflowId}/status`; + const data = await request({ url, method: 'get' }); if (data && typeof data === 'object') { if (data.status) { @@ -166,20 +173,18 @@ export async function fetchWorkflowStatus( /** * Fetch workflow messages - * Endpoint: GET /api/workflows/{workflowId}/messages - * Query params: messageId (optional) - fetch only newer messages + * When apiBaseUrl provided: GET {apiBaseUrl}/workflows/{workflowId}/messages + * Otherwise: GET /api/workflows/{workflowId}/messages */ export async function fetchWorkflowMessages( request: ApiRequestFunction, workflowId: string, - messageId?: string + messageId?: string, + apiBaseUrl?: string ): Promise { const params = messageId ? { messageId } : undefined; - const data = await request({ - url: `/api/workflows/${workflowId}/messages`, - method: 'get', - params - }); + const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/messages` : `/api/workflows/${workflowId}/messages`; + const data = await request({ url, method: 'get', params }); if (Array.isArray(data)) { return data; @@ -199,20 +204,18 @@ export async function fetchWorkflowMessages( /** * Fetch workflow logs - * Endpoint: GET /api/workflows/{workflowId}/logs - * Query params: logId (optional) - fetch only newer logs + * When apiBaseUrl provided: GET {apiBaseUrl}/workflows/{workflowId}/logs + * Otherwise: GET /api/workflows/{workflowId}/logs */ export async function fetchWorkflowLogs( request: ApiRequestFunction, workflowId: string, - logId?: string + logId?: string, + apiBaseUrl?: string ): Promise { const params = logId ? { logId } : undefined; - const data = await request({ - url: `/api/workflows/${workflowId}/logs`, - method: 'get', - params - }); + const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/logs` : `/api/workflows/${workflowId}/logs`; + const data = await request({ url, method: 'get', params }); if (Array.isArray(data)) { return data; @@ -232,7 +235,7 @@ export async function fetchWorkflowLogs( /** * Fetch unified chat data (messages, logs, stats, documents) - * Endpoint: GET /api/chatplayground/{instanceId}/{workflowId}/chatData + * Endpoint: GET /api/chatplayground/{instanceId}/workflows/{workflowId}/chatData * Query params: afterTimestamp (optional) - fetch only data created after this time */ export async function fetchChatData( @@ -243,7 +246,7 @@ export async function fetchChatData( ): Promise { const params = afterTimestamp ? { afterTimestamp: afterTimestamp.toString() } : undefined; const requestConfig = { - url: `/api/chatplayground/${instanceId}/${workflowId}/chatData`, + url: `/api/chatplayground/${instanceId}/workflows/${workflowId}/chatData`, method: 'get' as const, params }; @@ -372,7 +375,7 @@ export async function startWorkflowApi( /** * Stop a running workflow - * Endpoint: POST /api/chatplayground/{instanceId}/{workflowId}/stop + * Endpoint: POST /api/chatplayground/{instanceId}/workflows/{workflowId}/stop */ export async function stopWorkflowApi( request: ApiRequestFunction, @@ -380,52 +383,53 @@ export async function stopWorkflowApi( workflowId: string ): Promise { await request({ - url: `/api/chatplayground/${instanceId}/${workflowId}/stop`, + url: `/api/chatplayground/${instanceId}/workflows/${workflowId}/stop`, method: 'post' }); } /** * Update workflow properties - * Endpoint: PUT /api/workflows/{workflowId} + * When apiBaseUrl provided: PUT {apiBaseUrl}/workflows/{workflowId} + * Otherwise: PUT /api/workflows/{workflowId} */ export async function updateWorkflowApi( request: ApiRequestFunction, workflowId: string, - updateData: Partial<{ name: string; description?: string; tags?: string[] }> + updateData: Partial<{ name: string; description?: string; tags?: string[] }>, + apiBaseUrl?: string ): Promise { - return await request({ - url: `/api/workflows/${workflowId}`, - method: 'put', - data: updateData - }); + const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}` : `/api/workflows/${workflowId}`; + return await request({ url, method: 'put', data: updateData }); } /** * Delete a workflow and all associated data - * Endpoint: DELETE /api/workflows/{workflowId} + * When apiBaseUrl provided: DELETE {apiBaseUrl}/workflows/{workflowId} + * Otherwise: DELETE /api/workflows/{workflowId} */ export async function deleteWorkflowApi( request: ApiRequestFunction, - workflowId: string + workflowId: string, + apiBaseUrl?: string ): Promise { - await request({ - url: `/api/workflows/${workflowId}`, - method: 'delete' - }); + const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}` : `/api/workflows/${workflowId}`; + await request({ url, method: 'delete' }); } /** * Delete multiple workflows + * When apiBaseUrl provided, uses feature-scoped delete endpoint */ export async function deleteWorkflowsApi( request: ApiRequestFunction, - workflowIds: string[] + workflowIds: string[], + apiBaseUrl?: string ): Promise { - // Delete workflows one by one since there's no bulk delete endpoint + const base = apiBaseUrl ? `${apiBaseUrl}/workflows` : '/api/workflows'; const deletePromises = workflowIds.map(workflowId => request({ - url: `/api/workflows/${workflowId}`, + url: `${base}/${workflowId}`, method: 'delete' }).catch(error => { console.error(`Failed to delete workflow ${workflowId}:`, error); @@ -438,33 +442,33 @@ export async function deleteWorkflowsApi( /** * Delete a message from a workflow - * Endpoint: DELETE /api/workflows/{workflowId}/messages/{messageId} + * When apiBaseUrl provided: DELETE {apiBaseUrl}/workflows/{workflowId}/messages/{messageId} + * Otherwise: DELETE /api/workflows/{workflowId}/messages/{messageId} */ export async function deleteMessageApi( request: ApiRequestFunction, workflowId: string, - messageId: string + messageId: string, + apiBaseUrl?: string ): Promise { - await request({ - url: `/api/workflows/${workflowId}/messages/${messageId}`, - method: 'delete' - }); + const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/messages/${messageId}` : `/api/workflows/${workflowId}/messages/${messageId}`; + await request({ url, method: 'delete' }); } /** * Delete a file reference from a message - * Endpoint: DELETE /api/workflows/{workflowId}/messages/{messageId}/files/{fileId} + * When apiBaseUrl provided: DELETE {apiBaseUrl}/workflows/{workflowId}/messages/{messageId}/files/{fileId} + * Otherwise: DELETE /api/workflows/{workflowId}/messages/{messageId}/files/{fileId} */ export async function deleteFileFromMessageApi( request: ApiRequestFunction, workflowId: string, messageId: string, - fileId: string + fileId: string, + apiBaseUrl?: string ): Promise { - await request({ - url: `/api/workflows/${workflowId}/messages/${messageId}/files/${fileId}`, - method: 'delete' - }); + const url = apiBaseUrl ? `${apiBaseUrl}/workflows/${workflowId}/messages/${messageId}/files/${fileId}` : `/api/workflows/${workflowId}/messages/${messageId}/files/${fileId}`; + await request({ url, method: 'delete' }); } /** diff --git a/src/hooks/useWorkflows.ts b/src/hooks/useWorkflows.ts index 79c12cc..adcb033 100644 --- a/src/hooks/useWorkflows.ts +++ b/src/hooks/useWorkflows.ts @@ -46,8 +46,17 @@ export interface PaginationParams { search?: string; } -// Workflows list hook -export function useUserWorkflows() { +/** Get apiBaseUrl from instanceId and featureCode for feature-scoped workflow APIs */ +export function getWorkflowApiBaseUrl(instanceId: string | undefined, featureCode: string | undefined): string | undefined { + if (!instanceId || !featureCode) return undefined; + if (featureCode === 'chatplayground') return `/api/chatplayground/${instanceId}`; + if (featureCode === 'automation') return `/api/automations/${instanceId}`; + return undefined; +} + +// Workflows list hook - pass instanceId and featureCode when in feature context for feature-scoped API +export function useUserWorkflows(options?: { instanceId?: string; featureCode?: string }) { + const apiBaseUrl = getWorkflowApiBaseUrl(options?.instanceId, options?.featureCode); const [workflows, setWorkflows] = useState([]); const [attributes, setAttributes] = useState([]); const [permissions, setPermissions] = useState(null); @@ -113,16 +122,11 @@ export function useUserWorkflows() { } let data: any; + const url = apiBaseUrl ? `${apiBaseUrl}/workflows` : '/api/workflows/'; if (Object.keys(requestParams).length > 0) { - // Use request for paginated queries - data = await request({ - url: '/api/workflows/', - method: 'get', - params: requestParams - }); + data = await request({ url, method: 'get', params: requestParams }); } else { - // Use API function for simple queries - data = await fetchWorkflowsApi(request); + data = await fetchWorkflowsApi(request, undefined, apiBaseUrl); } // Handle paginated response @@ -164,7 +168,7 @@ export function useUserWorkflows() { setWorkflows([]); setPagination(null); } - }, [request]); + }, [request, apiBaseUrl]); // Optimistically remove a workflow from the local state const removeOptimistically = (workflowId: string) => { @@ -185,13 +189,13 @@ export function useUserWorkflows() { // Fetch a single workflow by ID const fetchWorkflowById = useCallback(async (workflowId: string): Promise => { try { - const workflow = await fetchWorkflowByIdApi(request, workflowId); + const workflow = await fetchWorkflowByIdApi(request, workflowId, apiBaseUrl); return workflow as UserWorkflow | null; } catch (error: any) { console.error('Error fetching workflow by ID:', error); return null; } - }, [request]); + }, [request, apiBaseUrl]); // Generate edit fields from attributes dynamically const generateEditFieldsFromAttributes = useCallback((): Array<{ @@ -375,8 +379,9 @@ export function useUserWorkflows() { }; } -// Workflow operations hook -export function useWorkflowOperations() { +// Workflow operations hook - pass instanceId and featureCode when in feature context for feature-scoped API +export function useWorkflowOperations(options?: { instanceId?: string; featureCode?: string }) { + const apiBaseUrl = getWorkflowApiBaseUrl(options?.instanceId, options?.featureCode); const [startingWorkflow, setStartingWorkflow] = useState(false); const [stoppingWorkflows, setStoppingWorkflows] = useState>(new Set()); const [deletingWorkflows, setDeletingWorkflows] = useState>(new Set()); @@ -476,7 +481,7 @@ export function useWorkflowOperations() { workflowId, setDeletingWorkflows, setDeleteError, - () => deleteWorkflowApi(request, workflowId), + () => deleteWorkflowApi(request, workflowId, apiBaseUrl), { default: 'Failed to delete workflow', notFound: 'Workflow not found or has already been deleted.', @@ -512,7 +517,7 @@ export function useWorkflowOperations() { try { // Delete workflows one by one since there's no bulk delete endpoint - await deleteWorkflowsApi(request, workflowIds); + await deleteWorkflowsApi(request, workflowIds, apiBaseUrl); // Add a small delay to ensure backend has time to process await new Promise(resolve => setTimeout(resolve, 300)); @@ -547,7 +552,7 @@ export function useWorkflowOperations() { operationKey, setDeletingMessages, setDeleteMessageError, - () => deleteMessageApi(request, workflowId, messageId), + () => deleteMessageApi(request, workflowId, messageId, apiBaseUrl), { default: 'Failed to delete message', notFound: 'Message not found or has already been deleted.', @@ -566,7 +571,7 @@ export function useWorkflowOperations() { operationKey, setDeletingFiles, setDeleteFileError, - () => deleteFileFromMessageApi(request, workflowId, messageId, fileId), + () => deleteFileFromMessageApi(request, workflowId, messageId, fileId, apiBaseUrl), { default: 'Failed to delete file', notFound: 'File not found or has already been deleted.', @@ -580,7 +585,7 @@ export function useWorkflowOperations() { setEditingWorkflows(prev => new Set(prev).add(workflowId)); try { - const updatedWorkflow = await updateWorkflowApi(request, workflowId, updateData); + const updatedWorkflow = await updateWorkflowApi(request, workflowId, updateData, apiBaseUrl); return { success: true, workflowData: updatedWorkflow }; } catch (error: any) { diff --git a/src/pages/views/automation/AutomationDefinitionsView.tsx b/src/pages/views/automation/AutomationDefinitionsView.tsx index 3a4d492..a96bd9e 100644 --- a/src/pages/views/automation/AutomationDefinitionsView.tsx +++ b/src/pages/views/automation/AutomationDefinitionsView.tsx @@ -13,6 +13,7 @@ import { FaSync, FaRobot, FaRocket, FaPlus, FaFileAlt, FaStop, FaList, FaTimes, import { useToast } from '../../../contexts/ToastContext'; import { useApiRequest } from '../../../hooks/useApi'; import { useFeatureStore } from '../../../stores/featureStore'; +import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; import styles from '../../admin/Admin.module.css'; interface WorkflowLog { @@ -24,11 +25,19 @@ interface WorkflowLog { } export const AutomationDefinitionsView: React.FC = () => { + const { instanceId: routeInstanceId, featureCode: routeFeatureCode } = useCurrentInstance(); const { getAllInstances } = useFeatureStore(); const instances = getAllInstances(); const chatbotInstance = instances.find(i => i.featureCode === 'chatbot') || instances[0]; - const mandateId = chatbotInstance?.mandateId; - const featureInstanceId = chatbotInstance?.id; + const automationInstance = instances.find(i => i.featureCode === 'automation'); + // When under automation feature route, use route context; otherwise use featureStore + const mandateId = routeFeatureCode === 'automation' && routeInstanceId + ? (automationInstance?.mandateId ?? chatbotInstance?.mandateId) + : chatbotInstance?.mandateId; + const featureInstanceId = routeFeatureCode === 'automation' && routeInstanceId + ? routeInstanceId + : (chatbotInstance?.id ?? automationInstance?.id); + const automationWorkflowInstanceId = routeFeatureCode === 'automation' ? routeInstanceId : undefined; const { data: automations, @@ -66,6 +75,7 @@ export const AutomationDefinitionsView: React.FC = () => { visible: boolean; automationId: string | null; automationLabel: string; + featureInstanceId: string | null; workflowId: string | null; status: 'starting' | 'running' | 'completed' | 'stopped' | 'error'; logs: WorkflowLog[]; @@ -73,6 +83,7 @@ export const AutomationDefinitionsView: React.FC = () => { visible: false, automationId: null, automationLabel: '', + featureInstanceId: null, workflowId: null, status: 'starting', logs: [], @@ -259,14 +270,16 @@ export const AutomationDefinitionsView: React.FC = () => { setShowEditor(true); }; - const pollWorkflowLogs = useCallback(async (workflowId: string) => { + const pollWorkflowLogs = useCallback(async (workflowId: string, instanceId: string) => { try { const contextHeaders: Record = {}; if (mandateId) contextHeaders['X-Mandate-Id'] = mandateId; + const logsUrl = `/api/automations/${instanceId}/workflows/${workflowId}/logs`; + const workflowUrl = `/api/automations/${instanceId}/workflows/${workflowId}`; const response = await request({ - url: `/api/workflows/${workflowId}/logs`, + url: logsUrl, method: 'get', - params: lastLogIdRef.current ? { afterId: lastLogIdRef.current } : {}, + params: lastLogIdRef.current ? { logId: lastLogIdRef.current } : {}, additionalConfig: { headers: contextHeaders }, }); const logs: WorkflowLog[] = response?.items || response || []; @@ -279,7 +292,7 @@ export const AutomationDefinitionsView: React.FC = () => { lastLogIdRef.current = logs[logs.length - 1].id; } const statusResponse = await request({ - url: `/api/workflows/${workflowId}`, + url: workflowUrl, method: 'get', additionalConfig: { headers: contextHeaders }, }); @@ -309,6 +322,7 @@ export const AutomationDefinitionsView: React.FC = () => { visible: true, automationId: automation.id, automationLabel: automation.label, + featureInstanceId: automation.featureInstanceId ?? automationWorkflowInstanceId ?? null, workflowId: null, status: 'starting', logs: [{ id: 'init', timestamp: Date.now() / 1000, message: 'Automatisierung wird gestartet...' }], @@ -316,14 +330,17 @@ export const AutomationDefinitionsView: React.FC = () => { try { const result = await handleAutomationExecute(automation.id); const workflowId = result?.id; - if (workflowId) { + const instanceId = automation.featureInstanceId ?? automationWorkflowInstanceId; + if (workflowId && instanceId) { setExecutionModal(prev => ({ ...prev, workflowId, status: 'running', logs: [...prev.logs, { id: 'started', timestamp: Date.now() / 1000, message: `Workflow ${workflowId} gestartet`, status: 'running' }], })); - pollIntervalRef.current = setInterval(() => pollWorkflowLogs(workflowId), 2000); + pollIntervalRef.current = setInterval(() => pollWorkflowLogs(workflowId, instanceId), 2000); + } else if (workflowId && !instanceId) { + setExecutionModal(prev => ({ ...prev, status: 'error', logs: [...prev.logs, { id: 'error', timestamp: Date.now() / 1000, message: 'Keine Feature-Instanz für Polling', status: 'error' }] })); } } catch (err: any) { setExecutionModal(prev => ({ @@ -337,11 +354,17 @@ export const AutomationDefinitionsView: React.FC = () => { const handleStopWorkflow = async () => { if (!executionModal.workflowId) return; + const instanceId = executionModal.featureInstanceId ?? automationWorkflowInstanceId; + if (!instanceId) { + showError('Keine Feature-Instanz für Stopp verfügbar'); + return; + } try { const stopHeaders: Record = {}; if (mandateId) stopHeaders['X-Mandate-Id'] = mandateId; + const stopUrl = `/api/automations/${instanceId}/workflows/${executionModal.workflowId}/stop`; await request({ - url: `/api/workflows/${executionModal.workflowId}/stop`, + url: stopUrl, method: 'post', additionalConfig: { headers: stopHeaders }, }); @@ -363,6 +386,7 @@ export const AutomationDefinitionsView: React.FC = () => { visible: false, automationId: null, automationLabel: '', + featureInstanceId: null, workflowId: null, status: 'starting', logs: [], diff --git a/src/pages/workflows/WorkflowsPage.tsx b/src/pages/workflows/WorkflowsPage.tsx index 0fdcc84..d32fb08 100644 --- a/src/pages/workflows/WorkflowsPage.tsx +++ b/src/pages/workflows/WorkflowsPage.tsx @@ -6,11 +6,12 @@ */ import React, { useState, useMemo, useEffect } from 'react'; -import { useUserWorkflows, useWorkflowOperations } from '../../hooks/useWorkflows'; +import { useUserWorkflows, useWorkflowOperations, getWorkflowApiBaseUrl } from '../../hooks/useWorkflows'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm'; import { FaSync, FaList, FaPlay } from 'react-icons/fa'; import { useNavigate } from 'react-router-dom'; +import { useCurrentInstance } from '../../hooks/useCurrentInstance'; import styles from '../admin/Admin.module.css'; interface Workflow { @@ -23,8 +24,12 @@ interface Workflow { export const WorkflowsPage: React.FC = () => { const navigate = useNavigate(); + const { instanceId, featureCode } = useCurrentInstance(); + const workflowOptions = instanceId && featureCode ? { instanceId, featureCode } : undefined; + const apiBaseUrl = getWorkflowApiBaseUrl(instanceId, featureCode); + const apiEndpoint = apiBaseUrl ? `${apiBaseUrl}/workflows` : '/api/workflows/'; - // Data hook + // Data hook - pass instance context when in feature route const { data: workflows, attributes, @@ -35,16 +40,16 @@ export const WorkflowsPage: React.FC = () => { refetch, fetchWorkflowById, updateOptimistically, - } = useUserWorkflows(); + } = useUserWorkflows(workflowOptions); - // Operations hook + // Operations hook - pass instance context when in feature route const { handleWorkflowDelete, handleWorkflowDeleteMultiple, handleWorkflowUpdate, handleInlineUpdate, deletingWorkflows, - } = useWorkflowOperations(); + } = useWorkflowOperations(workflowOptions); const [editingWorkflow, setEditingWorkflow] = useState(null); @@ -173,7 +178,7 @@ export const WorkflowsPage: React.FC = () => {