From a4a29c4515b506ba71f917f71bc88296dbb54039 Mon Sep 17 00:00:00 2001 From: manirajkumar-tavro Date: Wed, 3 Jun 2026 16:14:04 +0530 Subject: [PATCH 1/3] added ai ucase to application relation viceversa , now ai use case detail page shows created and updated time stamp --- .../ai_use_case_business_applications.sql | 8 + sql/core/zz_agent_upsert_unique_indexes.sql | 19 + tavro_api/api/routers/business_relations.py | 123 ++++++ tavro_api/api/routers/use_cases.py | 308 +++++++++++++- tavro_app/src/components/UseCaseView.tsx | 110 ++--- .../src/pages/BusinessApplicationViewPage.tsx | 231 ++++++++++- tavro_app/src/pages/CreateUseCasePage.tsx | 34 +- tavro_app/src/pages/UseCaseViewPage.tsx | 384 +++++++++++++++++- tavro_app/src/services/useCaseApi.ts | 13 + tavro_app/src/types/businessRelations.ts | 1 + 10 files changed, 1154 insertions(+), 77 deletions(-) create mode 100644 sql/core/ai_use_case_business_applications.sql diff --git a/sql/core/ai_use_case_business_applications.sql b/sql/core/ai_use_case_business_applications.sql new file mode 100644 index 0000000..1ddfff8 --- /dev/null +++ b/sql/core/ai_use_case_business_applications.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS core.ai_use_case_business_applications ( + tenant_id TEXT, + ai_use_case_id TEXT, + business_application_id TEXT, + application_name TEXT, + created_ts TIMESTAMP, + updated_ts TIMESTAMP +); diff --git a/sql/core/zz_agent_upsert_unique_indexes.sql b/sql/core/zz_agent_upsert_unique_indexes.sql index 73d6aa6..6b9b25b 100644 --- a/sql/core/zz_agent_upsert_unique_indexes.sql +++ b/sql/core/zz_agent_upsert_unique_indexes.sql @@ -251,4 +251,23 @@ BEGIN END IF; END IF; + IF to_regclass('core.ai_use_case_business_applications') IS NOT NULL THEN + EXECUTE ' + CREATE UNIQUE INDEX IF NOT EXISTS ux_core_ai_use_case_business_applications + ON core.ai_use_case_business_applications (ai_use_case_id, business_application_id, tenant_id) + '; + + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'fk_core_ai_use_case_business_applications_business_application' + ) THEN + ALTER TABLE core.ai_use_case_business_applications + ADD CONSTRAINT fk_core_ai_use_case_business_applications_business_application + FOREIGN KEY (business_application_id) + REFERENCES core.business_applications (business_application_id) + ON DELETE CASCADE; + END IF; + END IF; + END $$; diff --git a/tavro_api/api/routers/business_relations.py b/tavro_api/api/routers/business_relations.py index 561082e..2455353 100644 --- a/tavro_api/api/routers/business_relations.py +++ b/tavro_api/api/routers/business_relations.py @@ -548,6 +548,27 @@ def _normalize_integration_row(row: dict[str, Any]) -> dict[str, Any]: def _normalize_application_row(row: dict[str, Any]) -> dict[str, Any]: row["related_agents"] = _json_list(row.get("related_agents")) + related_use_cases_raw = _json_list(row.get("related_use_cases")) + normalized_related_use_cases: list[dict[str, Any]] = [] + seen_use_case_ids: set[str] = set() + for rel in related_use_cases_raw: + if not isinstance(rel, dict): + continue + use_case_id = _text_or_none(rel.get("identifier") or rel.get("ai_use_case_id")) + if not use_case_id or use_case_id in seen_use_case_ids: + continue + seen_use_case_ids.add(use_case_id) + normalized_related_use_cases.append( + { + "identifier": use_case_id, + "name": _text_or_none(rel.get("name")), + "description": _text_or_none(rel.get("description")), + "owner": _text_or_none(rel.get("owner")), + "priority": _text_or_none(rel.get("priority")), + "status": _text_or_none(rel.get("status")), + } + ) + row["related_use_cases"] = normalized_related_use_cases row["related_agent_count"] = int(row.get("related_agent_count") or 0) return row @@ -943,6 +964,7 @@ async def _fetch_applications( _col_expr("ba", app_cols, "updated_ts"), "rel.related_agents", "COALESCE(rel.related_agent_count, 0) AS related_agent_count", + "uc_rel.related_use_cases", ] has_aba = await _table_exists(db, "core", "agent_business_applications") @@ -986,6 +1008,93 @@ async def _fetch_applications( ) rel ON TRUE """ + has_uc_app_rel = await _table_exists(db, "core", "ai_use_case_business_applications") + has_auc = await _table_exists(db, "core", "ai_use_cases") + auc_order_sql = "ORDER BY 1" + if has_auc: + auc_cols = await _table_columns(db, "core", "ai_use_cases") + order_parts: list[str] = [] + if "updated_ts" in auc_cols: + order_parts.append("auc.updated_ts DESC NULLS LAST") + if "created_ts" in auc_cols: + order_parts.append("auc.created_ts DESC NULLS LAST") + if "ai_use_case_id" in auc_cols: + order_parts.append("auc.ai_use_case_id") + if order_parts: + auc_order_sql = f"ORDER BY {', '.join(order_parts)}" + if has_uc_app_rel and has_auc: + uc_rel_sql = f""" + LEFT JOIN LATERAL ( + SELECT + json_agg( + json_build_object( + 'identifier', related.ai_use_case_id, + 'name', related.name, + 'description', related.description, + 'owner', related.owner, + 'priority', related.priority, + 'status', related.status + ) + ORDER BY LOWER(COALESCE(related.name, related.ai_use_case_id)) + ) AS related_use_cases + FROM ( + SELECT DISTINCT + rel.ai_use_case_id, + latest.name, + latest.description, + latest.owner, + latest.priority, + latest.status + FROM core.ai_use_case_business_applications rel + LEFT JOIN LATERAL ( + SELECT + auc.name, + auc.description, + auc.owner, + auc.priority, + auc.status + FROM core.ai_use_cases auc + WHERE auc.ai_use_case_id = rel.ai_use_case_id + {auc_order_sql} + LIMIT 1 + ) latest ON TRUE + WHERE rel.business_application_id = ba.business_application_id + AND rel.ai_use_case_id IS NOT NULL + AND rel.ai_use_case_id <> '' + ) related + ) uc_rel ON TRUE + """ + elif has_uc_app_rel: + uc_rel_sql = """ + LEFT JOIN LATERAL ( + SELECT + json_agg( + json_build_object( + 'identifier', related.ai_use_case_id, + 'name', NULL, + 'description', NULL, + 'owner', NULL, + 'priority', NULL, + 'status', NULL + ) + ORDER BY LOWER(related.ai_use_case_id) + ) AS related_use_cases + FROM ( + SELECT DISTINCT rel.ai_use_case_id + FROM core.ai_use_case_business_applications rel + WHERE rel.business_application_id = ba.business_application_id + AND rel.ai_use_case_id IS NOT NULL + AND rel.ai_use_case_id <> '' + ) related + ) uc_rel ON TRUE + """ + else: + uc_rel_sql = """ + LEFT JOIN LATERAL ( + SELECT NULL::json AS related_use_cases + ) uc_rel ON TRUE + """ + search_clean = _clean(search) order_sql = ( "LOWER(COALESCE(ba.application_name, ba.business_application_id))" @@ -1017,6 +1126,7 @@ async def _fetch_applications( {", ".join(select_cols)} FROM core.business_applications ba {rel_join_sql} + {uc_rel_sql} {where_sql} ORDER BY {order_sql} """ @@ -1609,6 +1719,19 @@ async def delete_application( {"business_application_id": application_id}, ) + if await _table_exists(db, "core", "ai_use_case_business_applications"): + uc_app_cols = await _table_columns(db, "core", "ai_use_case_business_applications") + if "business_application_id" in uc_app_cols: + await db.execute( + text( + """ + DELETE FROM core.ai_use_case_business_applications + WHERE business_application_id = :business_application_id + """ + ), + {"business_application_id": application_id}, + ) + await _ensure_application_attachments_table(db) await db.execute( text("DELETE FROM public.application_attachment WHERE application_id = :application_id"), diff --git a/tavro_api/api/routers/use_cases.py b/tavro_api/api/routers/use_cases.py index f9d436d..3af1ead 100644 --- a/tavro_api/api/routers/use_cases.py +++ b/tavro_api/api/routers/use_cases.py @@ -87,6 +87,10 @@ class LinkProcessRequest(BaseModel): process_id: str +class LinkApplicationRequest(BaseModel): + application_id: str + + class UseCaseAttachmentCreate(BaseModel): filename: str mime_type: str @@ -144,6 +148,31 @@ async def _ensure_use_case_process_relation_table(db: AsyncSession) -> None: ) +async def _ensure_use_case_application_relation_table(db: AsyncSession) -> None: + await db.execute( + text( + f""" + CREATE TABLE IF NOT EXISTS {CORE}.ai_use_case_business_applications ( + tenant_id TEXT, + ai_use_case_id TEXT, + business_application_id TEXT, + application_name TEXT, + created_ts TIMESTAMP, + updated_ts TIMESTAMP + ) + """ + ) + ) + await db.execute( + text( + f""" + CREATE UNIQUE INDEX IF NOT EXISTS ux_core_ai_use_case_business_applications + ON {CORE}.ai_use_case_business_applications (ai_use_case_id, business_application_id, tenant_id) + """ + ) + ) + + async def _ensure_use_case_tables(db: AsyncSession) -> None: await db.execute( text( @@ -437,6 +466,11 @@ async def get_use_case(use_case_id: str, request: Request, db: AsyncSession = De if tenant_id else "" ) + application_tenant_filter = ( + "AND (rela.tenant_id = :tid OR rela.tenant_id IS NULL OR rela.tenant_id = '' OR rela.tenant_id = 'None')" + if tenant_id + else "" + ) try: result = await db.execute( text(f""" @@ -516,9 +550,47 @@ async def get_use_case(use_case_id: str, request: Request, db: AsyncSession = De for r in processes_result.mappings().all() ] + await _ensure_use_case_application_relation_table(db) + applications_result = await db.execute( + text( + f""" + SELECT DISTINCT + rela.business_application_id, + COALESCE(ba.application_name, rela.application_name, rela.business_application_id) AS application_name, + ba.application_description AS description, + ba.business_criticality, + ba.emergency_tier, + LOWER(COALESCE(ba.application_name, rela.application_name, rela.business_application_id)) AS application_sort_key + FROM {CORE}.ai_use_case_business_applications rela + LEFT JOIN {CORE}.business_applications ba + ON ba.business_application_id = rela.business_application_id + WHERE LOWER(TRIM(rela.ai_use_case_id)) = LOWER(TRIM(:uid)) + AND rela.business_application_id IS NOT NULL + AND rela.business_application_id <> '' + {application_tenant_filter} + ORDER BY application_sort_key + """ + ), + {"uid": normalized_use_case_id, "tid": tenant_id}, + ) + linked_applications = [ + { + "identifier": r["business_application_id"], + "business_application_id": r["business_application_id"], + "name": r["application_name"], + "application_name": r["application_name"], + "description": r["description"], + "business_criticality": r["business_criticality"], + "emergency_tier": r["emergency_tier"], + } + for r in applications_result.mappings().all() + ] + data = { **dict(row), "of_associated_agents": linked_agents, + "of_associated_business_applications": linked_applications, + "applications": linked_applications, "of_associated_business_processes": linked_processes, } return {"start_record": 1, "end_record": 1, "record_count": 1, "total_records": 1, "data": [data]} @@ -596,12 +668,17 @@ async def delete_use_case(use_case_id: str, db: AsyncSession = Depends(get_db)): if not exists.first(): raise HTTPException(status_code=404, detail=f"AI Use Case '{use_case_id}' not found.") - await _ensure_use_case_process_relation_table(db) await _ensure_use_case_attachments_table(db) + await _ensure_use_case_process_relation_table(db) + await _ensure_use_case_application_relation_table(db) await db.execute( text(f"DELETE FROM {CORE}.ai_use_case_business_processes WHERE ai_use_case_id = :uid"), {"uid": use_case_id}, ) + await db.execute( + text(f"DELETE FROM {CORE}.ai_use_case_business_applications WHERE ai_use_case_id = :uid"), + {"uid": use_case_id}, + ) await db.execute( text("DELETE FROM public.use_case_attachment WHERE use_case_id = :uid"), {"uid": use_case_id}, @@ -845,6 +922,235 @@ async def unlink_agent(use_case_id: str, agent_id: str, request: Request, db: As raise HTTPException(status_code=500, detail=str(e)) +# --------------------------------------------------------------------------- +# POST /{use_case_id}/applications — link business application +# --------------------------------------------------------------------------- + +@router.post("/{use_case_id}/applications", summary="Link Application to AI Use Case") +async def link_application(use_case_id: str, body: LinkApplicationRequest, request: Request, db: AsyncSession = Depends(get_db)): + await _ensure_use_case_tables(db) + normalized_use_case_id = _norm_id(use_case_id) + requested_application_id = _norm_id(body.application_id) + if not normalized_use_case_id: + raise HTTPException(status_code=400, detail="AI Use Case ID is required.") + if not requested_application_id: + raise HTTPException(status_code=400, detail="Application ID is required.") + + tenant_id = _tenant(request) + tenant_filter = ( + "AND (tenant_id = :tid OR tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'None')" + if tenant_id + else "" + ) + + try: + await _ensure_use_case_application_relation_table(db) + + uc_exists = await db.execute( + text(f"SELECT 1 FROM {CORE}.ai_use_cases WHERE LOWER(TRIM(ai_use_case_id)) = LOWER(TRIM(:uid)) {tenant_filter} LIMIT 1"), + {"uid": normalized_use_case_id, "tid": tenant_id}, + ) + if not uc_exists.first(): + raise HTTPException(status_code=404, detail=f"AI Use Case '{normalized_use_case_id}' not found.") + + application_row = await db.execute( + text(f""" + SELECT business_application_id, application_name + FROM {CORE}.business_applications + WHERE LOWER(TRIM(business_application_id)) = LOWER(TRIM(:aid)) + {tenant_filter} + LIMIT 1 + """), + {"aid": requested_application_id, "tid": tenant_id}, + ) + application = application_row.mappings().first() + if not application: + raise HTTPException(status_code=404, detail=f"Application '{requested_application_id}' not found.") + canonical_application_id = _norm_id(str(application.get("business_application_id") or requested_application_id)) + + dup = await db.execute( + text(f""" + SELECT 1 + FROM {CORE}.ai_use_case_business_applications + WHERE LOWER(TRIM(ai_use_case_id)) = LOWER(TRIM(:uid)) + AND LOWER(TRIM(business_application_id)) = LOWER(TRIM(:aid)) + {tenant_filter} + LIMIT 1 + """), + {"uid": normalized_use_case_id, "aid": canonical_application_id, "tid": tenant_id}, + ) + if dup.first(): + cnt = await db.execute( + text(f""" + SELECT COUNT(DISTINCT business_application_id) + FROM {CORE}.ai_use_case_business_applications + WHERE LOWER(TRIM(ai_use_case_id)) = LOWER(TRIM(:uid)) + {tenant_filter} + """), + {"uid": normalized_use_case_id, "tid": tenant_id}, + ) + return {"message": "Relationship already exists", "associated_count": int(cnt.scalar() or 0)} + + await db.execute( + text(f""" + INSERT INTO {CORE}.ai_use_case_business_applications ( + tenant_id, ai_use_case_id, business_application_id, application_name, created_ts, updated_ts + ) + VALUES ( + :tid, :uid, :aid, :aname, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + ) + """), + { + "tid": tenant_id, + "uid": normalized_use_case_id, + "aid": canonical_application_id, + "aname": application.get("application_name") or canonical_application_id, + }, + ) + + cnt = await db.execute( + text(f""" + SELECT COUNT(DISTINCT business_application_id) + FROM {CORE}.ai_use_case_business_applications + WHERE LOWER(TRIM(ai_use_case_id)) = LOWER(TRIM(:uid)) + {tenant_filter} + """), + {"uid": normalized_use_case_id, "tid": tenant_id}, + ) + await db.commit() + return {"message": "Relationship synchronized", "associated_count": int(cnt.scalar() or 0)} + except HTTPException: + raise + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + + +# --------------------------------------------------------------------------- +# DELETE /{use_case_id}/applications/{application_id} — unlink business application +# --------------------------------------------------------------------------- + +@router.delete("/{use_case_id}/applications/{application_id}", summary="Unlink Application from AI Use Case") +async def unlink_application(use_case_id: str, application_id: str, request: Request, db: AsyncSession = Depends(get_db)): + await _ensure_use_case_tables(db) + normalized_use_case_id = _norm_id(use_case_id) + normalized_application_id = _norm_id(application_id) + if not normalized_use_case_id: + raise HTTPException(status_code=400, detail="AI Use Case ID is required.") + if not normalized_application_id: + raise HTTPException(status_code=400, detail="Application ID is required.") + + tenant_id = _tenant(request) + tenant_filter = ( + "AND (tenant_id = :tid OR tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'None')" + if tenant_id + else "" + ) + + try: + await _ensure_use_case_application_relation_table(db) + + uc_exists = await db.execute( + text(f"SELECT 1 FROM {CORE}.ai_use_cases WHERE LOWER(TRIM(ai_use_case_id)) = LOWER(TRIM(:uid)) {tenant_filter} LIMIT 1"), + {"uid": normalized_use_case_id, "tid": tenant_id}, + ) + if not uc_exists.first(): + raise HTTPException(status_code=404, detail=f"AI Use Case '{normalized_use_case_id}' not found.") + + exists = await db.execute( + text(f""" + SELECT 1 + FROM {CORE}.ai_use_case_business_applications + WHERE LOWER(TRIM(ai_use_case_id)) = LOWER(TRIM(:uid)) + AND LOWER(TRIM(business_application_id)) = LOWER(TRIM(:aid)) + {tenant_filter} + LIMIT 1 + """), + {"uid": normalized_use_case_id, "aid": normalized_application_id, "tid": tenant_id}, + ) + if not exists.first(): + fallback_exists = await db.execute( + text( + f""" + SELECT 1 + FROM {CORE}.ai_use_case_business_applications + WHERE LOWER(TRIM(ai_use_case_id)) = LOWER(TRIM(:uid)) + AND LOWER(TRIM(business_application_id)) = LOWER(TRIM(:aid)) + LIMIT 1 + """ + ), + {"uid": normalized_use_case_id, "aid": normalized_application_id}, + ) + if fallback_exists.first(): + await db.execute( + text( + f""" + DELETE FROM {CORE}.ai_use_case_business_applications + WHERE LOWER(TRIM(ai_use_case_id)) = LOWER(TRIM(:uid)) + AND LOWER(TRIM(business_application_id)) = LOWER(TRIM(:aid)) + """ + ), + {"uid": normalized_use_case_id, "aid": normalized_application_id}, + ) + cnt = await db.execute( + text(f""" + SELECT COUNT(DISTINCT business_application_id) + FROM {CORE}.ai_use_case_business_applications + WHERE LOWER(TRIM(ai_use_case_id)) = LOWER(TRIM(:uid)) + {tenant_filter} + """), + {"uid": normalized_use_case_id, "tid": tenant_id}, + ) + await db.commit() + return { + "message": "Relationship removed", + "associated_count": int(cnt.scalar() or 0), + "rows_deleted": 1, + } + + cnt = await db.execute( + text(f""" + SELECT COUNT(DISTINCT business_application_id) + FROM {CORE}.ai_use_case_business_applications + WHERE LOWER(TRIM(ai_use_case_id)) = LOWER(TRIM(:uid)) + {tenant_filter} + """), + {"uid": normalized_use_case_id, "tid": tenant_id}, + ) + return {"message": "Relationship not found", "associated_count": int(cnt.scalar() or 0)} + + delete_result = await db.execute( + text(f""" + DELETE FROM {CORE}.ai_use_case_business_applications + WHERE LOWER(TRIM(ai_use_case_id)) = LOWER(TRIM(:uid)) + AND LOWER(TRIM(business_application_id)) = LOWER(TRIM(:aid)) + {tenant_filter} + """), + {"uid": normalized_use_case_id, "aid": normalized_application_id, "tid": tenant_id}, + ) + + cnt = await db.execute( + text(f""" + SELECT COUNT(DISTINCT business_application_id) + FROM {CORE}.ai_use_case_business_applications + WHERE LOWER(TRIM(ai_use_case_id)) = LOWER(TRIM(:uid)) + {tenant_filter} + """), + {"uid": normalized_use_case_id, "tid": tenant_id}, + ) + await db.commit() + return { + "message": "Relationship removed", + "associated_count": int(cnt.scalar() or 0), + "rows_deleted": int(delete_result.rowcount or 0), + } + except HTTPException: + raise + except Exception as e: + await db.rollback() + raise HTTPException(status_code=500, detail=str(e)) + + # --------------------------------------------------------------------------- # POST /{use_case_id}/processes — link business process # --------------------------------------------------------------------------- diff --git a/tavro_app/src/components/UseCaseView.tsx b/tavro_app/src/components/UseCaseView.tsx index 721fb5d..96e92f2 100644 --- a/tavro_app/src/components/UseCaseView.tsx +++ b/tavro_app/src/components/UseCaseView.tsx @@ -24,6 +24,7 @@ interface UseCaseViewProps { useCase: UseCaseDetail; agentsComponent?: React.ReactNode; businessImpactComponent?: React.ReactNode; + headerActions?: React.ReactNode; } function MetaBadge({ text, color = 'slate' }: { text: string; color?: 'blue' | 'emerald' | 'amber' | 'slate' }) { @@ -133,7 +134,10 @@ function MetaField({ label, children }: { label: string; children: React.ReactNo function formatDate(raw?: string | null): string { if (!raw) return '—'; try { - return new Date(raw).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); + const date = new Date(raw); + if (Number.isNaN(date.getTime())) return raw; + const pad = (value: number) => String(value).padStart(2, '0'); + return `${pad(date.getDate())}/${pad(date.getMonth() + 1)}/${date.getFullYear()} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; } catch { return raw; } @@ -158,10 +162,10 @@ function getLabel(item: any, fallback = 'N/A'): string { } function getId(item: any): string | undefined { - return item?.sys_id ?? item?.id ?? item?.identifier ?? item?.value ?? item?.agent_id; + return item?.sys_id ?? item?.id ?? item?.identifier ?? item?.value ?? item?.agent_id ?? item?.business_application_id; } -const UseCaseView: React.FC = ({ useCase: uc, agentsComponent, businessImpactComponent }) => { +const UseCaseView: React.FC = ({ useCase: uc, agentsComponent, businessImpactComponent, headerActions }) => { const [activeTab, setActiveTab] = React.useState('details'); const applications = uc.applications?.filter(Boolean) ?? []; @@ -177,8 +181,8 @@ const UseCaseView: React.FC = ({ useCase: uc, agentsComponent, const owner = uc.owner ?? (uc as any).use_case_owner ?? null; const proposedBy = uc.proposed_by ?? (uc as any).proposed_by ?? null; - const createdAt = (uc as any).created_at ?? (uc as any).sys_created_on ?? (uc as any).created ?? null; - const updatedAt = (uc as any).updated_at ?? (uc as any).sys_updated_on ?? (uc as any).updated ?? null; + const createdAt = (uc as any).created_ts ?? (uc as any).created_at ?? (uc as any).sys_created_on ?? null; + const updatedAt = (uc as any).updated_ts ?? (uc as any).updated_at ?? (uc as any).sys_updated_on ?? null; const description = uc.description ?? (uc as any).description ?? null; const tabs = [ @@ -198,24 +202,31 @@ const UseCaseView: React.FC = ({ useCase: uc, agentsComponent,
{/* Title row */} -
-
- -
-
-

- {(uc as any).name ?? (uc as any).title ?? 'Unnamed Use Case'} -

-
- {uc.identifier && ( - - {uc.identifier} - - )} - {uc.function && } - {(uc as any).use_case_type && } +
+
+
+ +
+
+

+ {(uc as any).name ?? (uc as any).title ?? 'Unnamed Use Case'} +

+
+ {uc.identifier && ( + + {uc.identifier} + + )} + {uc.function && } + {(uc as any).use_case_type && } +
+ {headerActions && ( +
+ {headerActions} +
+ )}
{/* Metadata grid */} @@ -337,36 +348,37 @@ const UseCaseView: React.FC = ({ useCase: uc, agentsComponent, {/* Business Impact tab */} {activeTab === 'business_impact' && (
-
- {applications.length > 0 && ( - } title="Applications" count={applications.length}> -
- {applications.map((app: any, i: number) => ( -
-
- {getId(app) ? ( - - {getLabel(app)} - - ) : ( -

{getLabel(app)}

- )} - {getLabel(app.business_criticality ?? app.u_business_criticality, '') && ( - - {getLabel(app.business_criticality ?? app.u_business_criticality, '')} - + {businessImpactComponent ?? ( +
+ {applications.length > 0 && ( + } title="Applications" count={applications.length}> +
+ {applications.map((app: any, i: number) => ( +
+
+ {getId(app) ? ( + + {getLabel(app)} + + ) : ( +

{getLabel(app)}

+ )} + {getLabel(app.business_criticality ?? app.u_business_criticality, '') && ( + + {getLabel(app.business_criticality ?? app.u_business_criticality, '')} + + )} +
+ {(app.description ?? app.short_description) && ( +

{app.description ?? app.short_description}

)}
- {(app.description ?? app.short_description) && ( -

{app.description ?? app.short_description}

- )} -
- ))} -
- - )} -
- {businessImpactComponent} + ))} +
+ + )} +
+ )}
)} diff --git a/tavro_app/src/pages/BusinessApplicationViewPage.tsx b/tavro_app/src/pages/BusinessApplicationViewPage.tsx index 1db9923..ee485b7 100644 --- a/tavro_app/src/pages/BusinessApplicationViewPage.tsx +++ b/tavro_app/src/pages/BusinessApplicationViewPage.tsx @@ -19,13 +19,15 @@ import { XCircle, } from 'lucide-react'; import { businessRelationsApi } from '../services/businessRelationsApi'; +import { useCaseApi } from '../services/useCaseApi'; import type { BusinessApplicationRecord, BusinessApplicationUpsertPayload, } from '../types/businessRelations'; import { useCatalog } from '../context/CatalogContext'; +import { useUseCases } from '../context/UseCaseContext'; -type Tab = 'overview' | 'related'; +type Tab = 'overview' | 'related' | 'related_use_cases'; type Option = { label: string; value: string }; const EMERGENCY_TIER_OPTIONS: Option[] = [ @@ -267,8 +269,10 @@ const BusinessApplicationViewPage: React.FC = () => { const [searchParams] = useSearchParams(); const navigate = useNavigate(); const { agents } = useCatalog(); + const { useCases: allUseCases, refresh: refreshUseCases } = useUseCases(); const isCreateMode = !id || id === 'new'; const linkAgentId = (searchParams.get('linkAgentId') || '').trim(); + const linkUseCaseId = (searchParams.get('linkUseCaseId') || '').trim(); const [application, setApplication] = useState(null); const [form, setForm] = useState(emptyForm); @@ -283,8 +287,11 @@ const BusinessApplicationViewPage: React.FC = () => { const [deleting, setDeleting] = useState(false); const [searchAgents, setSearchAgents] = useState(''); + const [searchUseCases, setSearchUseCases] = useState(''); const [actingAgent, setActingAgent] = useState(null); + const [actingUseCase, setActingUseCase] = useState(null); const [relationError, setRelationError] = useState(null); + const [useCaseRelationError, setUseCaseRelationError] = useState(null); const agentNameById = useMemo(() => { const map = new Map(); @@ -299,6 +306,8 @@ const BusinessApplicationViewPage: React.FC = () => { if (!id || isCreateMode) return; setLoading(true); setError(null); + setRelationError(null); + setUseCaseRelationError(null); try { const data = await businessRelationsApi.getApplication(id); setApplication(data); @@ -348,6 +357,32 @@ const BusinessApplicationViewPage: React.FC = () => { }); }, [agents, linkedAgentIds, searchAgents]); + const relatedUseCases = useMemo(() => { + return application?.related_use_cases ?? []; + }, [application]); + + const linkedUseCaseIds = useMemo(() => { + const ids = new Set(); + relatedUseCases.forEach((useCase) => { + if (useCase.identifier) ids.add(useCase.identifier); + }); + return ids; + }, [relatedUseCases]); + + const availableUseCases = useMemo(() => { + const q = searchUseCases.trim().toLowerCase(); + return allUseCases.filter((useCase) => { + const useCaseId = useCase.identifier || ''; + if (!useCaseId || linkedUseCaseIds.has(useCaseId)) return false; + if (!q) return true; + return ( + useCaseId.toLowerCase().includes(q) || + (useCase.name ?? '').toLowerCase().includes(q) || + (useCase.description ?? '').toLowerCase().includes(q) + ); + }); + }, [allUseCases, linkedUseCaseIds, searchUseCases]); + const setField = (key: keyof ApplicationFormState, value: string) => { setForm(prev => ({ ...prev, [key]: value })); }; @@ -394,6 +429,15 @@ const BusinessApplicationViewPage: React.FC = () => { console.warn('Application created but auto-link to agent failed.', linkErr); } } + if (linkUseCaseId) { + try { + await useCaseApi.linkApplication(linkUseCaseId, created.business_application_id); + } catch (linkErr) { + console.warn('Application created but auto-link to AI use case failed.', linkErr); + } + navigate(`/use-case/${encodeURIComponent(linkUseCaseId)}`, { replace: true }); + return; + } navigate(`/applications/${encodeURIComponent(created.business_application_id)}`, { replace: true }); return; } @@ -414,6 +458,10 @@ const BusinessApplicationViewPage: React.FC = () => { setActionError(null); setAttemptedSave(false); if (isCreateMode) { + if (linkUseCaseId) { + navigate(`/use-case/${encodeURIComponent(linkUseCaseId)}`); + return; + } navigate('/applications'); return; } @@ -464,6 +512,36 @@ const BusinessApplicationViewPage: React.FC = () => { } }; + const addUseCase = async (useCaseId: string) => { + if (!application) return; + setActingUseCase(useCaseId); + setUseCaseRelationError(null); + try { + await useCaseApi.linkApplication(useCaseId, application.business_application_id); + await load(); + refreshUseCases(); + } catch (err: any) { + setUseCaseRelationError(err.message || 'Failed to add AI use case relation'); + } finally { + setActingUseCase(null); + } + }; + + const removeUseCase = async (useCaseId: string) => { + if (!application) return; + setActingUseCase(useCaseId); + setUseCaseRelationError(null); + try { + await useCaseApi.unlinkApplication(useCaseId, application.business_application_id); + await load(); + refreshUseCases(); + } catch (err: any) { + setUseCaseRelationError(err.message || 'Failed to remove AI use case relation'); + } finally { + setActingUseCase(null); + } + }; + if (loading) { return (
@@ -477,10 +555,16 @@ const BusinessApplicationViewPage: React.FC = () => { return (
@@ -496,6 +580,7 @@ const BusinessApplicationViewPage: React.FC = () => { const appTitle = form.application_name || application?.application_name || 'New Application'; const appId = application?.business_application_id || 'Will be generated on create'; const relatedAgentCount = application?.related_agents?.length ?? 0; + const relatedUseCaseCount = relatedUseCases.length; const criticalityMeta = getCriticalityMeta(form.business_criticality); const emergencyTierMeta = getEmergencyTierMeta(form.emergency_tier); @@ -503,10 +588,16 @@ const BusinessApplicationViewPage: React.FC = () => {
@@ -613,16 +704,28 @@ const BusinessApplicationViewPage: React.FC = () => { Details {!isCreateMode && !editing && ( - + <> + + + )}
@@ -900,6 +1003,104 @@ const BusinessApplicationViewPage: React.FC = () => {
)} + {tab === 'related_use_cases' && application && ( +
+ {useCaseRelationError && ( +
+ + {useCaseRelationError} +
+ )} + +
+
+

Currently Related AI Use Cases ({relatedUseCaseCount})

+
+
+ {relatedUseCases.length === 0 && ( +
No AI use cases linked.
+ )} + {relatedUseCases.map((rel, idx) => { + const useCaseId = rel.identifier || `missing-${idx}`; + const catalogMatch = allUseCases.find((uc) => uc.identifier === useCaseId); + const displayName = rel.name || catalogMatch?.name || useCaseId; + const busy = actingUseCase === useCaseId; + return ( +
+
+ + {displayName} + +

{useCaseId}

+
+ +
+ ); + })} +
+
+ +
+
+

Add AI Use Case Relation

+
+ + + Create AI Use Case + +
+ + setSearchUseCases(e.target.value)} + placeholder="Filter AI use cases..." + className="w-full pl-7 pr-3 py-1.5 border border-slate-200 rounded-lg text-xs text-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500/20" + /> +
+
+
+
+ {availableUseCases.length === 0 && ( +
No available AI use cases to link.
+ )} + {availableUseCases.map((useCase) => { + const useCaseId = useCase.identifier || ''; + const busy = actingUseCase === useCaseId; + return ( +
+
+

{useCase.name || useCaseId}

+

{useCaseId}

+
+ +
+ ); + })} +
+
+
+ )} + {tab === 'related' && application && (
{relationError && ( diff --git a/tavro_app/src/pages/CreateUseCasePage.tsx b/tavro_app/src/pages/CreateUseCasePage.tsx index 25a7602..1aa2918 100644 --- a/tavro_app/src/pages/CreateUseCasePage.tsx +++ b/tavro_app/src/pages/CreateUseCasePage.tsx @@ -20,6 +20,7 @@ const CreateUseCasePage: React.FC = () => { const { refresh } = useUseCases(); const linkAgentId = searchParams.get('linkAgentId')?.trim() || ''; const linkProcessId = searchParams.get('linkProcessId')?.trim() || ''; + const linkApplicationId = searchParams.get('linkApplicationId')?.trim() || ''; const [form, setForm] = useState({ name: '', @@ -78,16 +79,27 @@ const CreateUseCasePage: React.FC = () => { if (linkProcessId && created?.use_case_id) { await useCaseApi.linkProcess(created.use_case_id, linkProcessId); } + if (linkApplicationId && created?.use_case_id) { + await useCaseApi.linkApplication(created.use_case_id, linkApplicationId); + } setSuccess(true); sessionStorage.setItem( 'tavro_use_case_notice', - linkAgentId && linkProcessId + linkAgentId && linkProcessId && linkApplicationId + ? 'AI Use Case created and linked to agent, process, and application successfully.' + : linkAgentId && linkProcessId ? 'AI Use Case created and linked to agent and process successfully.' + : linkAgentId && linkApplicationId + ? 'AI Use Case created and linked to agent and application successfully.' + : linkProcessId && linkApplicationId + ? 'AI Use Case created and linked to process and application successfully.' : linkAgentId ? 'AI Use Case created and linked to agent successfully.' : linkProcessId ? 'AI Use Case created and linked to process successfully.' - : 'AI Use Case created successfully. It will appear in the catalog shortly.' + : linkApplicationId + ? 'AI Use Case created and linked to application successfully.' + : 'AI Use Case created successfully. It will appear in the catalog shortly.' ); refresh(); setTimeout(() => { @@ -99,6 +111,10 @@ const CreateUseCasePage: React.FC = () => { navigate(`/processes/${encodeURIComponent(linkProcessId)}`); return; } + if (linkApplicationId) { + navigate(`/applications/${encodeURIComponent(linkApplicationId)}`); + return; + } navigate('/use-cases'); }, 1200); } catch (err: any) { @@ -127,11 +143,15 @@ const CreateUseCasePage: React.FC = () => { navigate(`/processes/${encodeURIComponent(linkProcessId)}`); return; } + if (linkApplicationId) { + navigate(`/applications/${encodeURIComponent(linkApplicationId)}`); + return; + } navigate('/use-cases'); }} className="flex items-center gap-2 text-sm font-medium text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-100 transition-all bg-transparent border-none cursor-pointer" > - {linkAgentId ? 'Back to Agent' : linkProcessId ? 'Back to Process' : 'Back to Use Cases'} + {linkAgentId ? 'Back to Agent' : linkProcessId ? 'Back to Process' : linkApplicationId ? 'Back to Application' : 'Back to Use Cases'}
@@ -148,7 +168,9 @@ const CreateUseCasePage: React.FC = () => { ? 'Register a new AI use case and link it to this agent' : linkProcessId ? 'Register a new AI use case and link it to this process' - : 'Register a new AI use case in the Agent Biz Ops catalog'} + : linkApplicationId + ? 'Register a new AI use case and link it to this application' + : 'Register a new AI use case in the Agent Biz Ops catalog'}

@@ -296,6 +318,10 @@ const CreateUseCasePage: React.FC = () => { navigate(`/processes/${encodeURIComponent(linkProcessId)}`); return; } + if (linkApplicationId) { + navigate(`/applications/${encodeURIComponent(linkApplicationId)}`); + return; + } navigate('/use-cases'); }} className="px-5 py-2.5 rounded-xl text-sm font-semibold text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all"> diff --git a/tavro_app/src/pages/UseCaseViewPage.tsx b/tavro_app/src/pages/UseCaseViewPage.tsx index 73fc7c9..9f28ea2 100644 --- a/tavro_app/src/pages/UseCaseViewPage.tsx +++ b/tavro_app/src/pages/UseCaseViewPage.tsx @@ -12,7 +12,7 @@ import AuditInitModal from '../components/audit/AuditInitModal'; import EditUseCaseModal from '../components/EditUseCaseModal'; import { useCaseApi } from '../services/useCaseApi'; import { businessRelationsApi } from '../services/businessRelationsApi'; -import type { BusinessProcessRecord } from '../types/businessRelations'; +import type { BusinessApplicationRecord, BusinessProcessRecord } from '../types/businessRelations'; const USE_CASE_AGENT_COUNT_CACHE_KEY = 'tavro_use_case_agent_count_cache'; @@ -27,6 +27,44 @@ interface ProcessRelationsSectionProps { onSilentRefetch: () => void; } +interface ApplicationRelationsSectionProps { + useCase: UseCaseDetail; + onSilentRefetch: () => void; +} + +const normalizeUseCaseApplications = (raw: any): Array<{ + identifier: string; + name: string; + description: string | null; + business_criticality: string | null; + emergency_tier: string | null; +}> => { + if (!Array.isArray(raw)) return []; + const seen = new Set(); + const rows: Array<{ + identifier: string; + name: string; + description: string | null; + business_criticality: string | null; + emergency_tier: string | null; + }> = []; + + raw.forEach((item: any) => { + const identifier = String(item?.business_application_id ?? item?.identifier ?? item?.id ?? '').trim(); + if (!identifier || seen.has(identifier)) return; + seen.add(identifier); + rows.push({ + identifier, + name: String(item?.application_name ?? item?.name ?? identifier), + description: item?.description ?? item?.application_description ?? null, + business_criticality: item?.business_criticality ?? null, + emergency_tier: item?.emergency_tier ?? null, + }); + }); + + return rows; +}; + const normalizeUseCaseProcesses = (raw: any): Array<{ identifier: string; name: string; @@ -79,10 +117,28 @@ const normalizeUseCaseAgents = (raw: any): Array<{ agent_id: string; name: strin const mergeUseCaseWithRestDetail = ( base: UseCaseDetail | undefined, restPayload: any, + applicationCatalog: any[] | undefined, processCatalog: any[] | undefined, fallbackId: string, ): UseCaseDetail | undefined => { const normalizedUseCaseId = String(fallbackId || '').trim().toLowerCase(); + const catalogLinkedApplications = normalizeUseCaseApplications( + (applicationCatalog ?? []).filter((app: any) => { + const related = Array.isArray(app?.related_use_cases) ? app.related_use_cases : []; + return related.some((uc: any) => { + const ucId = String(uc?.identifier ?? uc?.ai_use_case_id ?? '').trim().toLowerCase(); + return ucId && ucId === normalizedUseCaseId; + }); + }).map((app: any) => ({ + identifier: app.business_application_id, + business_application_id: app.business_application_id, + name: app.application_name, + application_name: app.application_name, + description: app.application_description, + business_criticality: app.business_criticality, + emergency_tier: app.emergency_tier, + })), + ); const catalogLinkedProcesses = normalizeUseCaseProcesses( (processCatalog ?? []).filter((proc: any) => { const related = Array.isArray(proc?.related_use_cases) ? proc.related_use_cases : []; @@ -132,7 +188,47 @@ const mergeUseCaseWithRestDetail = ( return Array.from(byId.values()); }; + const mergeApplicationLists = (...lists: Array>) => { + const byId = new Map(); + lists.forEach((list) => { + list.forEach((app) => { + const key = String(app.identifier || '').trim().toLowerCase(); + if (!key) return; + if (!byId.has(key)) { + byId.set(key, app); + return; + } + const existing = byId.get(key)!; + byId.set(key, { + ...existing, + name: existing.name || app.name, + description: existing.description || app.description, + business_criticality: existing.business_criticality || app.business_criticality, + emergency_tier: existing.emergency_tier || app.emergency_tier, + }); + }); + }); + return Array.from(byId.values()); + }; + const row = Array.isArray(restPayload?.data) ? restPayload.data[0] : null; + const restLinkedApplications = row + ? normalizeUseCaseApplications(row.of_associated_business_applications ?? row.applications ?? []) + : []; + const baseLinkedApplications = normalizeUseCaseApplications((base as any)?.applications ?? (base as any)?.of_associated_business_applications ?? []); + const linkedApplications = mergeApplicationLists(restLinkedApplications, catalogLinkedApplications, baseLinkedApplications); const restLinkedProcesses = row ? normalizeUseCaseProcesses(row.of_associated_business_processes ?? row.business_processes ?? []) : []; @@ -143,6 +239,7 @@ const mergeUseCaseWithRestDetail = ( if (!base) return undefined; return { ...base, + applications: linkedApplications, business_processes: linkedProcesses, } as UseCaseDetail; } @@ -153,6 +250,7 @@ const mergeUseCaseWithRestDetail = ( if (base) { return { ...base, + applications: linkedApplications, business_processes: linkedProcesses, agents: linkedAgents.length > 0 ? linkedAgents : (base as any).agents, } as UseCaseDetail; @@ -169,6 +267,7 @@ const mergeUseCaseWithRestDetail = ( expected_benefits: row.expected_benefits ?? null, function: row.function ?? null, agents: linkedAgents, + applications: linkedApplications, business_processes: linkedProcesses, } as UseCaseDetail; }; @@ -408,6 +507,262 @@ const AgentsSection: React.FC = ({ useCase, agents, onSilent ); }; +const ApplicationRelationsSection: React.FC = ({ useCase, onSilentRefetch }) => { + const { refresh: refreshUC } = useUseCases(); + const useCaseId = useCase.identifier ?? ''; + const [allApplications, setAllApplications] = useState([]); + const [loadingCatalog, setLoadingCatalog] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [acting, setActing] = useState(null); + const [relationError, setRelationError] = useState(null); + const [pendingLinkIds, setPendingLinkIds] = useState>(new Set()); + const [pendingUnlinkIds, setPendingUnlinkIds] = useState>(new Set()); + + const linkedApplicationsFromServer = useMemo( + () => normalizeUseCaseApplications((useCase as any).applications ?? (useCase as any).of_associated_business_applications ?? []), + [useCase], + ); + + const serverLinkedById = useMemo(() => { + const map = new Map(); + linkedApplicationsFromServer.forEach((app) => { + if (app.identifier) map.set(app.identifier, app); + }); + return map; + }, [linkedApplicationsFromServer]); + + useEffect(() => { + setPendingLinkIds((prev) => { + const next = new Set(prev); + for (const id of prev) { + if (serverLinkedById.has(id)) next.delete(id); + } + return next; + }); + setPendingUnlinkIds((prev) => { + const next = new Set(prev); + for (const id of prev) { + if (!serverLinkedById.has(id)) next.delete(id); + } + return next; + }); + }, [serverLinkedById]); + + const linkedApplications = useMemo(() => { + const visibleServerRows = linkedApplicationsFromServer.filter((app) => !pendingUnlinkIds.has(app.identifier)); + const optimisticLinks = Array.from(pendingLinkIds) + .filter((id) => !serverLinkedById.has(id)) + .map((id) => { + const catalogRow = allApplications.find((app) => app.business_application_id === id); + return { + identifier: id, + name: catalogRow?.application_name || id, + description: catalogRow?.application_description ?? null, + business_criticality: catalogRow?.business_criticality ?? null, + emergency_tier: catalogRow?.emergency_tier ?? null, + }; + }); + return [...visibleServerRows, ...optimisticLinks]; + }, [allApplications, linkedApplicationsFromServer, pendingLinkIds, pendingUnlinkIds, serverLinkedById]); + + const linkedApplicationIds = useMemo(() => { + const ids = new Set(linkedApplicationsFromServer.map(app => app.identifier).filter(Boolean)); + pendingLinkIds.forEach((id) => ids.add(id)); + pendingUnlinkIds.forEach((id) => ids.delete(id)); + return ids; + }, [linkedApplicationsFromServer, pendingLinkIds, pendingUnlinkIds]); + + const availableApplications = useMemo(() => { + const q = searchTerm.trim().toLowerCase(); + return allApplications.filter(app => { + if (linkedApplicationIds.has(app.business_application_id)) return false; + if (!q) return true; + return ( + app.business_application_id.toLowerCase().includes(q) || + (app.application_name ?? '').toLowerCase().includes(q) || + (app.application_description ?? '').toLowerCase().includes(q) + ); + }); + }, [allApplications, linkedApplicationIds, searchTerm]); + + const loadApplicationCatalog = async () => { + setLoadingCatalog(true); + try { + const data = await businessRelationsApi.listApplications(); + setAllApplications(data); + } catch (err: any) { + setRelationError(err.message || 'Failed to load application catalog.'); + } finally { + setLoadingCatalog(false); + } + }; + + useEffect(() => { + loadApplicationCatalog(); + }, []); + + const handleLinkApplication = async (applicationId: string) => { + if (!useCaseId || !applicationId || linkedApplicationIds.has(applicationId)) return; + setActing(`add:${applicationId}`); + setRelationError(null); + setPendingUnlinkIds((prev) => { + const next = new Set(prev); + next.delete(applicationId); + return next; + }); + setPendingLinkIds((prev) => new Set([...prev, applicationId])); + try { + await useCaseApi.linkApplication(useCaseId, applicationId); + refreshUC(); + onSilentRefetch(); + } catch (err: any) { + setPendingLinkIds((prev) => { + const next = new Set(prev); + next.delete(applicationId); + return next; + }); + setRelationError(err.message || 'Failed to link application.'); + } finally { + setActing(null); + } + }; + + const handleUnlinkApplication = async (applicationId: string) => { + if (!useCaseId || !applicationId || !linkedApplicationIds.has(applicationId)) return; + setActing(`remove:${applicationId}`); + setRelationError(null); + setPendingLinkIds((prev) => { + const next = new Set(prev); + next.delete(applicationId); + return next; + }); + setPendingUnlinkIds((prev) => new Set([...prev, applicationId])); + try { + await useCaseApi.unlinkApplication(useCaseId, applicationId); + refreshUC(); + onSilentRefetch(); + } catch (err: any) { + setPendingUnlinkIds((prev) => { + const next = new Set(prev); + next.delete(applicationId); + return next; + }); + setRelationError(err.message || 'Failed to unlink application.'); + } finally { + setActing(null); + } + }; + + return ( +
+ {relationError && ( +
+ + {relationError} +
+ )} + +
+
+

Currently Related Applications ({linkedApplications.length})

+
+
+ {linkedApplications.length === 0 && ( +
No business applications linked.
+ )} + {linkedApplications.map((app) => { + const applicationId = app.identifier; + const removeKey = `remove:${applicationId}`; + const isPendingUnlink = pendingUnlinkIds.has(applicationId); + return ( +
+
+ + {app.name} + +

{applicationId}

+
+ +
+ ); + })} +
+
+ +
+
+

Add Application Relation

+
+ {useCaseId && ( + + + Create Application + + )} +
+ + setSearchTerm(e.target.value)} + placeholder="Filter applications..." + className="w-full pl-7 pr-3 py-1.5 border border-slate-200 rounded-lg text-xs text-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500/20" + /> +
+
+
+
+ {loadingCatalog && ( +
+ + Loading applications... +
+ )} + {!loadingCatalog && availableApplications.length === 0 && ( +
No available applications to link.
+ )} + {!loadingCatalog && availableApplications.map(app => { + const applicationId = app.business_application_id; + const addKey = `add:${applicationId}`; + const busy = acting === addKey; + return ( +
+
+

{app.application_name || applicationId}

+

{applicationId}

+
+ +
+ ); + })} +
+
+
+ ); +}; + const ProcessRelationsSection: React.FC = ({ useCase, onSilentRefetch }) => { const { refresh: refreshUC } = useUseCases(); const useCaseId = useCase.identifier ?? ''; @@ -706,18 +1061,22 @@ const UseCaseViewPage: React.FC = () => { setLoading(true); setError(null); try { - const [mcpResult, restResult, processesResult] = await Promise.allSettled([ + const [mcpResult, restResult, applicationsResult, processesResult] = await Promise.allSettled([ mcpClient.getUseCaseDetails(id, { forceRefresh: true }), useCaseApi.getUseCase(id), + businessRelationsApi.listApplications(), businessRelationsApi.listProcesses(), ]); const mcpDetail = mcpResult.status === 'fulfilled' ? mcpResult.value : undefined; const restDetail = restResult.status === 'fulfilled' ? restResult.value : undefined; + const applicationRows = applicationsResult.status === 'fulfilled' && Array.isArray(applicationsResult.value) + ? applicationsResult.value + : []; const processRows = processesResult.status === 'fulfilled' && Array.isArray(processesResult.value) ? processesResult.value : []; - const merged = mergeUseCaseWithRestDetail(mcpDetail, restDetail, processRows, id); + const merged = mergeUseCaseWithRestDetail(mcpDetail, restDetail, applicationRows, processRows, id); if (!merged) throw new Error('Use Case not found'); setUseCase(merged); @@ -731,17 +1090,21 @@ const UseCaseViewPage: React.FC = () => { async function fetchUseCaseSilently(forceRefresh = false) { if (!id) return; try { - const [mcpResult, restResult, processesResult] = await Promise.allSettled([ + const [mcpResult, restResult, applicationsResult, processesResult] = await Promise.allSettled([ mcpClient.getUseCaseDetails(id, { forceRefresh }), useCaseApi.getUseCase(id), + businessRelationsApi.listApplications(), businessRelationsApi.listProcesses(), ]); const mcpDetail = mcpResult.status === 'fulfilled' ? mcpResult.value : undefined; const restDetail = restResult.status === 'fulfilled' ? restResult.value : undefined; + const applicationRows = applicationsResult.status === 'fulfilled' && Array.isArray(applicationsResult.value) + ? applicationsResult.value + : []; const processRows = processesResult.status === 'fulfilled' && Array.isArray(processesResult.value) ? processesResult.value : []; - const merged = mergeUseCaseWithRestDetail(mcpDetail, restDetail, processRows, id); + const merged = mergeUseCaseWithRestDetail(mcpDetail, restDetail, applicationRows, processRows, id); if (merged) setUseCase(merged); } catch { // silent — don't disrupt the UI @@ -818,7 +1181,7 @@ const UseCaseViewPage: React.FC = () => { return (
-
+