diff --git a/docs/assets/Hierarchical_lineage.png b/docs/assets/Hierarchical_lineage.png new file mode 100644 index 00000000..4b1fc639 Binary files /dev/null and b/docs/assets/Hierarchical_lineage.png differ diff --git a/docs/ui/lineage.md b/docs/ui/lineage.md index f28c16e6..84f3b297 100644 --- a/docs/ui/lineage.md +++ b/docs/ui/lineage.md @@ -69,6 +69,25 @@ The Lineage page offers three different visualization modes: ![Artifact Execution Tree Lineage](../assets/artifact_exec_tree_lineage.png) +### 4. Heirarchical Lineage + +**Purpose**: High-level, multi-column tree abstraction tracking task orchestration flows branching from root configurations down through execution chains. + +**Use Cases**: + +- Tracking simultaneous execution tracks branching from a single environment or pipeline root. +- Tracing linear execution histories sequentially across multiple stages like preparation, features, training, and evaluation. +- Auditing full procedural histories by analyzing vertically ordered step progressions. + +**Features**: + +- Environment-centric root anchoring branching into independent functional swimlanes. +- Color-coded stage markers distinguishing entry hooks from down-stream operational tasks. +- Distinct step blocks capturing individual execution hashes sequentially. +- Vertical progression layouts tracing step dependencies from top to bottom. + +![Heirarchical Lineage](../assets/Hierarchical_lineage.png) + ## Using the Lineage Page diff --git a/server/app/main.py b/server/app/main.py index db2c6023..17a23504 100644 --- a/server/app/main.py +++ b/server/app/main.py @@ -13,7 +13,7 @@ import asyncio from sqlalchemy.ext.asyncio import AsyncSession from collections import defaultdict -from server.app.utils import extract_hostname, get_fqdn +from server.app.utils import extract_hostname, get_fqdn, convert_to_stage_json from server.app.get_data import ( get_mlmd_from_server, get_artifact_types, @@ -611,6 +611,30 @@ async def artifact_execution_lineage(request: Request, pipeline_name: str): return response +@app.get("/hierarchical-lineage/tangled-tree/{pipeline_name}") +async def hierarchical_lineage(request: Request, pipeline_name: str): + """ + Return MLMD data for the given pipeline converted into the UI `stage.json` schema. + """ + # checks if mlmd file exists on server + await check_mlmd_file_exists() + # checks if pipeline exists + await check_pipeline_exists(pipeline_name) + + # Pull MLMD JSON for the pipeline + json_payload = await async_api(get_mlmd_from_server, query, pipeline_name, None, None, dict_of_exe_ids) + + if json_payload is None: + raise HTTPException(status_code=404, detail=f"Pipeline {pipeline_name} not found or has no MLMD data.") + + try: + converted = convert_to_stage_json(json_payload, pipeline_name) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to convert MLMD to stage JSON: {e}") + + return converted + + @app.get("/list-of-executions/{pipeline_name}") async def list_of_executions(request: Request, pipeline_name: str): ''' diff --git a/server/app/utils.py b/server/app/utils.py index db483f73..79c92a16 100644 --- a/server/app/utils.py +++ b/server/app/utils.py @@ -99,3 +99,130 @@ def get_fqdn(name: str) -> str: return fqdn except Exception: return "127.0.0.1" + +import json + +def convert_to_stage_json(input_json: dict, pipeline_name: str) -> dict: + """ + Converts MLMD JSON for a clean hierarchy map. + - Removes trailing child nodes underneath executions. + - Combines Execution Type name and a 6-digit truncated UUID. + - Includes rich metadata fields for frontend hover/tooltip parsing. + """ + + def extract_stages_recursively(data): + """Recursively parses and drills down to locate the raw stages list.""" + if isinstance(data, str): + cleaned = data.strip() + try: + return extract_stages_recursively(json.loads(cleaned)) + except Exception: + if "stages" in cleaned or "Pipeline" in cleaned: + try: + if not (cleaned.startswith('{') and cleaned.endswith('}')): + return extract_stages_recursively(json.loads("{" + cleaned + "}")) + except Exception: + pass + return None + + if isinstance(data, dict): + for key in ("stages", "stage_list", "stageList"): + if key in data and isinstance(data[key], list): + return data[key] + for key in ("Pipeline", "pipelines", "name"): + if key in data: + res = extract_stages_recursively(data[key]) + if res: return res + for v in data.values(): + if isinstance(v, (dict, list, str)): + res = extract_stages_recursively(v) + if res: return res + + if isinstance(data, list): + for item in data: + res = extract_stages_recursively(item) + if res: return res + + return None + + stages_candidate = extract_stages_recursively(input_json) + if not stages_candidate: + stages_candidate = [] + + out = { + "environment": pipeline_name, + "metadata": { "version": "4.0.0", "description": "Executions Lineage Map" }, + "stages": [] + } + + stage_idx = 1 + exec_idx = 1 + + for s_item in stages_candidate: + if not isinstance(s_item, dict): + continue + + raw_executions = s_item.get("executions") or [] + if isinstance(raw_executions, str): + try: raw_executions = json.loads(raw_executions) + except Exception: raw_executions = [] + + if not raw_executions: + continue + + # Extract context attributes from the execution definition + first_exec = raw_executions[0] if isinstance(raw_executions, list) and len(raw_executions) > 0 else {} + props = first_exec.get("properties") or {} if isinstance(first_exec, dict) else {} + + context_type = props.get("Context_Type") or props.get("Execution_type_name") or first_exec.get("type") or "" + if "/" in context_type: + stage_name = context_type.split("/", 1)[1] + elif context_type: + stage_name = context_type + else: + stage_name = f"stage_{stage_idx}" + + stage_obj = { + "stage_id": f"stage_{stage_idx:02d}", + "stage_name": stage_name, + "status": "completed", + "executions": [] + } + stage_idx += 1 + + for e in raw_executions: + if not isinstance(e, dict): + continue + + e_props = e.get("properties") or {} + + # 1. Fetch execution base name (e.g., "Prepare") + exec_base_name = e.get("type") or "Execution" + + # 2. Use Execution_uuid first — same field the tangled-tree lineage keys off of, + # so the same execution shows the same short UUID in both views. + full_uuid = str(e_props.get("Execution_uuid") or e_props.get("Git_End_Commit") or e_props.get("Git_Start_Commit") or e.get("id") or "") + truncated_uuid = full_uuid[:4] if full_uuid else f"ex_{exec_idx}" + + # 3. Create the multi-line UI text block + # Note: '\n' handles line breaks in graph frameworks configured with CSS white-space: pre-wrap + display_text = f"{exec_base_name}\n{truncated_uuid}" + + exec_obj = { + "execution_id": f"exec_{exec_idx:03d}", + "execution_type": display_text, + "children": [], # Empty array ensures no leaf artifact nodes render underneath + + # Broad compatibility fields for the UI graphing framework's hover tooltip engine + "tooltip": full_uuid, + "title": full_uuid, + "description": f"Full UUID: {full_uuid}", + "full_uuid": full_uuid + } + exec_idx += 1 + + stage_obj["executions"].append(exec_obj) + + out["stages"].append(stage_obj) + + return out diff --git a/ui/package-lock.json b/ui/package-lock.json index 68b9d047..aacdb784 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -14,16 +14,21 @@ "@testing-library/user-event": "^13.5.0", "axios": "^0.24.0", "create-react-app": "^5.0.1", + "dagre": "^0.8.5", "file-system": "^2.2.2", "fs": "^0.0.1-security", "moment": "^2.29.4", + "papaparse": "^5.5.2", "path": "^0.12.7", "rc-tabs": "^12.5.10", "react": "^18.2.0", + "react-data-table-component": "^7.7.0", "react-dom": "^18.2.0", + "react-flow": "^1.0.3", "react-native": "^0.71.4", "react-scripts": "5.0.1", "react-table": "^7.8.0", + "reactflow": "^11.11.4", "web-vitals": "^2.1.4" }, "devDependencies": { @@ -2360,6 +2365,23 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT", + "peer": true + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.3.0.tgz", @@ -5292,6 +5314,108 @@ "resolved": "https://registry.npmjs.org/@react-native/polyfills/-/polyfills-2.0.0.tgz", "integrity": "sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ==" }, + "node_modules/@reactflow/background": { + "version": "11.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", + "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", + "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", + "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", + "license": "MIT", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.14", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", + "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/@remix-run/router": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.4.0.tgz", @@ -6011,6 +6135,259 @@ "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/eslint": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.3.tgz", @@ -6055,6 +6432,12 @@ "@types/range-parser": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", @@ -8177,6 +8560,16 @@ "node": ">= 6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -8391,6 +8784,12 @@ "node": ">=0.10.0" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/classnames": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", @@ -8935,6 +9334,16 @@ "postcss": "^8.4" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=4" + } + }, "node_modules/css-declaration-sorter": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz", @@ -9092,6 +9501,18 @@ "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==" }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -9279,9 +9700,125 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" }, "node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "license": "MIT", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -11911,6 +12448,15 @@ "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -17915,6 +18461,12 @@ "node": ">=6" } }, + "node_modules/papaparse": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.4.tgz", + "integrity": "sha512-SwzWD9gl/ElwYLCI0nUja1mFJzjq2D8ziShfNBa7zCHzkOozeOGDwHWQ+tvCzEZcewecWZ5U7kUopDnG+DFYEQ==", + "license": "MIT" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -19884,6 +20436,24 @@ "node": ">=14" } }, + "node_modules/react-data-table-component": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/react-data-table-component/-/react-data-table-component-7.7.1.tgz", + "integrity": "sha512-F1qciUONe0EiItxd+LZg9K7UXxvilSQH8WvUoSnPnAVZ/PK2x4Djr3nzkuCqCIfiMuU3imI+8gI7nYRIW2Kfxw==", + "license": "Apache-2.0", + "dependencies": { + "deepmerge": "^4.3.1" + }, + "peerDependencies": { + "react": ">= 17.0.0", + "styled-components": ">= 5.0.0" + }, + "peerDependenciesMeta": { + "styled-components": { + "optional": false + } + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -20026,6 +20596,12 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-flow": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/react-flow/-/react-flow-1.0.3.tgz", + "integrity": "sha512-Rx4Oqqbe9y7NyrUGzCrmtNvuGuMJBVjfIeMFuwbtbMc9f22sUSmxMjIA0c8r6Z7iMtB1zOWoGkTgfpmmpH9ReQ==", + "license": "ISC" + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -20528,6 +21104,24 @@ "react": "^16.8.3 || ^17.0.0-0 || ^18.0.0" } }, + "node_modules/reactflow": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", + "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", + "license": "MIT", + "dependencies": { + "@reactflow/background": "11.3.14", + "@reactflow/controls": "11.2.14", + "@reactflow/core": "11.11.4", + "@reactflow/minimap": "11.7.14", + "@reactflow/node-resizer": "2.2.14", + "@reactflow/node-toolbar": "1.3.14" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -22266,6 +22860,43 @@ "webpack": "^5.0.0" } }, + "node_modules/styled-components": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.4.3.tgz", + "integrity": "sha512-wYXrhu+JmDjZ1Tv7O0OopGTfztbzun43Pjjhh2H+xc0h5A09dwpZ5FJbrifJDcL8g5TA9btpWOX2+iSRuJTExw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@emotion/is-prop-valid": "1.4.0", + "css-to-react-native": "3.2.0", + "csstype": "3.2.3", + "stylis": "4.3.6" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "css-to-react-native": ">= 3.2.0", + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "react-native": ">= 0.68.0" + }, + "peerDependenciesMeta": { + "css-to-react-native": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -22281,6 +22912,13 @@ "postcss": "^8.2.15" } }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT", + "peer": true + }, "node_modules/sudo-prompt": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.2.1.tgz", @@ -23180,11 +23818,12 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/util": { @@ -24160,6 +24799,34 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/ui/package.json b/ui/package.json index 2a965fe2..0213302e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,19 +9,22 @@ "@testing-library/user-event": "^13.5.0", "axios": "^0.24.0", "create-react-app": "^5.0.1", + "dagre": "^0.8.5", "file-system": "^2.2.2", "fs": "^0.0.1-security", "moment": "^2.29.4", + "papaparse": "^5.5.2", "path": "^0.12.7", "rc-tabs": "^12.5.10", "react": "^18.2.0", + "react-data-table-component": "^7.7.0", "react-dom": "^18.2.0", + "react-flow": "^1.0.3", "react-native": "^0.71.4", "react-scripts": "5.0.1", "react-table": "^7.8.0", - "web-vitals": "^2.1.4", - "react-data-table-component": "^7.7.0", - "papaparse": "^5.5.2" + "reactflow": "^11.11.4", + "web-vitals": "^2.1.4" }, "scripts": { "start": "react-scripts start", diff --git a/ui/src/client.js b/ui/src/client.js index f1b6e648..c32e1748 100644 --- a/ui/src/client.js +++ b/ui/src/client.js @@ -125,6 +125,13 @@ class FastAPIClient { }); } + async getHierarchicalLineage(pipeline) { + return this.apiClient + .get(`/hierarchical-lineage/tangled-tree/${pipeline}`) + .then(({ data }) => { + return data; + }); + } // Deprecated legacy method (unused by current stage-based grid pages). // Replaced by: getExecutionsByStage // async getExecutions(pipeline_name, active_page, filter_value, sort_order) { diff --git a/ui/src/components/HeirarchicalLineageFlow/heirarchical_lineage.jsx b/ui/src/components/HeirarchicalLineageFlow/heirarchical_lineage.jsx new file mode 100644 index 00000000..7a21e00d --- /dev/null +++ b/ui/src/components/HeirarchicalLineageFlow/heirarchical_lineage.jsx @@ -0,0 +1,276 @@ +import React, { useMemo, useState } from "react"; +import ReactFlow, { Controls, Background, MiniMap, MarkerType, useNodes } from "reactflow"; +import dagre from "dagre"; +import "reactflow/dist/style.css"; +import "./index.css"; +import LineageNode1 from "./lineagenode"; + +const nodeTypes = { lineageNode: LineageNode1 }; +const nodeWidth = 220; +const nodeHeight = 80; + +// Central color thematic scheme helper matching your MiniMap rules +const getNodeThemeColor = (type) => { + switch (type) { + case "Environment": return "#10b981"; // Green + case "Stage": return "#f59e0b"; // Amber / Orange + case "Model": return "#f59e0b"; // Alias fallback + case "Node": return "#ef4444"; // Red + case "Execution": return "#3b82f6"; // Blue + default: return "#64748b"; // Gray + } +}; + +// MiniMap Node Component +const CustomMiniMapNode = ({ id, x, y, width, height }) => { + const nodes = useNodes(); + const graphNode = nodes.find((n) => n.id === id); + + const nodeType = graphNode?.data?.type || "Node"; + const nodeName = graphNode?.data?.name || ""; + + return ( + + + + {nodeType.toUpperCase()} + + + {nodeName.length > 18 ? `${nodeName.substring(0, 16)}...` : nodeName} + + + ); +}; + +// MODIFIED: Generates the exact clean layout tree seen in your example image +const transformLineageData = (rawJson) => { + const nodesMap = new Map(); + const links = []; + const flatItems = rawJson.flat(); + + const determineType = (id) => { + if (id.toLowerCase().includes("metrics")) return "Metrics"; + if (id.toLowerCase().includes("model")) return "Stage"; + if (id.toLowerCase().includes("train") || id.toLowerCase().includes("test") || id.toLowerCase().includes(".xml") || id.toLowerCase().includes("dataset") || id.toLowerCase().includes("input") || id.toLowerCase().includes("output")) return "Dataset"; + return "Execution"; + }; + + // Step 1: Parse every element as an independent individual tree node (No Collapsing) + flatItems.forEach((item) => { + if (!item || !item.id) return; + + // Clean string name labels cleanly + let cleanName = item.id.split("/").pop().split(":")[0]; + const type = determineType(item.id); + + if (!nodesMap.has(item.id)) { + nodesMap.set(item.id, { + id: item.id, + name: cleanName, + type: type, + parents: item.parents || [] + }); + } + }); + + // Step 2: Directly map one-to-one edges to allow separate parallel branching + const finalizedEdgesSet = new Set(); + nodesMap.forEach((node) => { + node.parents.forEach((parentId) => { + if (nodesMap.has(parentId) && parentId !== node.id) { + const edgeKey = `${parentId}->${node.id}`; + if (!finalizedEdgesSet.has(edgeKey)) { + finalizedEdgesSet.add(edgeKey); + links.push({ source: parentId, target: node.id }); + } + } + }); + }); + + return { nodes: Array.from(nodesMap.values()), links }; +}; + +// Layout reconfigured for a top-down vertical tree structure with side-by-side spacing +// FIXED LAYOUT ENGINE: Keeps stages horizontal, stacks ONLY execution leaf nodes vertically +// FIXED LAYOUT ENGINE: Removes massive empty horizontal spacing between stages +const getLayoutedElements = (nodes = [], edges = []) => { + const g = new dagre.graphlib.Graph(); + + g.setGraph({ + rankdir: "TB", + ranksep: 100, // Vertical gap between Environment and Stages + nodesep: 40, // MUCH closer horizontal spacing between your Stage blocks + edgesep: 20, + marginx: 40, + marginy: 40 + }); + g.setDefaultEdgeLabel(() => ({})); + + // STEP 1: Only feed non-Execution nodes (Environment & Stages) into Dagre + nodes.forEach((node) => { + if (node.data?.type !== "Execution") { + g.setNode(node.id, { width: nodeWidth, height: nodeHeight }); + } + }); + + // Only feed edges that don't point to an execution node into Dagre + edges.forEach((edge) => { + const targetNode = nodes.find((n) => n.id === edge.target); + if (targetNode && targetNode.data?.type !== "Execution") { + g.setEdge(edge.source, edge.target, { weight: 10, minlen: 1 }); + } + }); + + // Layout the main horizontal backbone (Environment -> Stages) + dagre.layout(g); + + // STEP 2: Group the execution leaves manually by their stage parent + const parentToExecutionLeavesMap = {}; + nodes.forEach((node) => { + if (node.data?.type === "Execution") { + const parentEdge = edges.find((e) => e.target === node.id); + if (parentEdge) { + const pId = parentEdge.source; + if (!parentToExecutionLeavesMap[pId]) parentToExecutionLeavesMap[pId] = []; + parentToExecutionLeavesMap[pId].push(node.id); + } + } + }); + + // STEP 3: Assign positions to all elements + return nodes?.map((node) => { + // Default top-down connection routing + node.targetPosition = 'top'; + node.sourcePosition = 'bottom'; + + if (node.data?.type !== "Execution") { + // Use the compact positions generated by Dagre for main stages + const pos = g.node(node.id) || { x: 0, y: 0 }; + node.position = { + x: pos.x - nodeWidth / 2, + y: pos.y - nodeHeight / 2, + }; + } else { + // Calculate clean, compact vertical positions for Execution nodes + const parentEdge = edges.find((e) => e.target === node.id); + if (parentEdge) { + const parentId = parentEdge.source; + const parentPos = g.node(parentId) || { x: 0, y: 0 }; + const siblings = parentToExecutionLeavesMap[parentId] || []; + const siblingIndex = siblings.indexOf(node.id); + + node.position = { + x: parentPos.x - nodeWidth / 2, + // Increased gap: was +20, now +40 for clearer separation between stacked nodes + y: (parentPos.y + nodeHeight / 2) + 60 + (siblingIndex * (nodeHeight + 40)), + }; + } + } + return node; + }); +}; + + + +const Hierarchical_LineageFlow = ({ data }) => { + const proOptions = { hideAttribution: true }; + const { nodes, edges } = useMemo(() => { + if (!data || data.length === 0) return { nodes: [], edges: [] }; + + const formattedData = Array?.isArray(data) && !data?.nodes ? transformLineageData(data) : data; + + const rfNodes = formattedData?.nodes?.map((node) => ({ + id: node.id, + type: "lineageNode", + position: { x: 0, y: 0 }, + data: { + ...node, + }, + })); + + const rfEdges = (formattedData?.links ?? formattedData?.edges ??[]).map((link, index) => ({ + id: `edge-${index}`, + source: link.source, + target: link.target, + type: "step", + markerEnd: { + type: MarkerType.Arrow, + color: "#b1b1b7" + }, + style: { + stroke: "#b1b1b7", + strokeWidth: 1.5, + } + })); + + return { + nodes: getLayoutedElements(rfNodes, rfEdges), + edges: rfEdges, + }; + }, [data]); + + return ( +
+ + + + + +
+ ); +}; + +export default Hierarchical_LineageFlow; \ No newline at end of file diff --git a/ui/src/components/HeirarchicalLineageFlow/index.css b/ui/src/components/HeirarchicalLineageFlow/index.css new file mode 100644 index 00000000..af6ad730 --- /dev/null +++ b/ui/src/components/HeirarchicalLineageFlow/index.css @@ -0,0 +1,104 @@ +.lineage-title { + font-size: 14px; + font-weight: 600; + margin-top: 8px; +} + +.lineage-subtitle { + font-size: 14px; + color: #666; + font-weight: 600; + margin-top: 2px; +} + +.lineage-badge { + color: white; + display: inline-block; + padding: 4px 8px; + border-radius: 4px; + font-size: 11px; +} + +.lineage-card { + padding: 10px; + border-radius: 6px; + background: #ffffff; + border: 1px solid #e2e8f0; + width: 200px; + word-break: break-word; + overflow-wrap: break-word; + white-space: normal; + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); + box-sizing: border-box; + position: relative; +} + +/* Ensure the react-flow minimap sub-canvas container doesn't overlap completely */ +.react-flow__minimap { + border-radius: 8px; + overflow: hidden; + box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1); +} + +/* Styling for individual nodes inside the MiniMap element container */ +.react-flow__minimap-node { + rx: 4px !important; /* Rounded corners */ + ry: 4px !important; + stroke: #ffffff !important; + stroke-width: 2px !important; +} + +/* Color matching rules mapping exactly to your main lineage diagram */ +.minimap-node-Dataset { + fill: #10b981 !important; /* Emerald Green */ +} + +.minimap-node-Model { + fill: #f59e0b !important; /* Warm Amber */ +} + +.minimap-node-Metrics { + fill: #ef4444 !important; /* Rose Red */ +} + +.minimap-node-Execution, .minimap-node-default { + fill: #3b82f6 !important; /* Blue Accent */ +} + +/* Enhances the viewport bounding box look */ +.react-flow__minimap-mask { + stroke: #cbd5e1 !important; + stroke-width: 1px !important; +} + +.react-flow__attribution { + display: none !important; +} + +.lineage-tooltip { + position: absolute; + top: 0; + left: 100%; + margin-left: 12px; + z-index: 9999; + padding: 12px 16px; + background: #ffffff; + border: 1px solid #cbd5e1; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + font-size: 13px; + line-height: 1.5; + white-space: pre-wrap; + min-width: 260px; + max-width: 360px; + pointer-events: none; +} + +/* Raise the whole node above its siblings while hovered, so the tooltip escapes cleanly */ +.react-flow__node { + transition: z-index 0s; +} + +.react-flow__node:hover { + z-index: 9999 !important; +} \ No newline at end of file diff --git a/ui/src/components/HeirarchicalLineageFlow/lineagenode.jsx b/ui/src/components/HeirarchicalLineageFlow/lineagenode.jsx new file mode 100644 index 00000000..e9ff2e3b --- /dev/null +++ b/ui/src/components/HeirarchicalLineageFlow/lineagenode.jsx @@ -0,0 +1,66 @@ +import React, { useState } from "react"; +import { Handle, Position } from "reactflow"; + +const getColor = (type) => { + switch (type) { + case "Dataset": + return "#10b981"; + + case "Execution": + return "#3b82f6"; + + case "Node": + case "Metrics": + return "#ef4444"; + + case "Model": + case "Stage": + return "#f59e0b"; + + case "Environment": + return "#14b8a6"; + + default: + return "#64748b"; + } +}; + +const getBadgeLabel = (type) => { + if (type === "Environment") return "PIPELINE_NAME"; + return type ? type.toUpperCase() : "NODE"; +}; + +const LineageNode1 = ({ data }) => { + const [showTooltip, setShowTooltip] = useState(false); + const { backgroundColor, fullUuid, ...rest } = data; + const tooltipData = { ...rest, uuid: fullUuid || data.uuid }; + + return ( +
setShowTooltip(true)} + onMouseLeave={() => setShowTooltip(false)} + style={{ position: "relative" }} + > + + +
+ {getBadgeLabel(data.type)} +
+ +
{data.name}
+ + {data.uuid &&
{data.uuid}
} + + {showTooltip && ( +
+          {JSON.stringify(tooltipData, null, 2)}
+        
+ )} + + +
+ ); +}; + +export default LineageNode1; \ No newline at end of file diff --git a/ui/src/components/HeirarchicalLineageFlow/trasformeddata.jsx b/ui/src/components/HeirarchicalLineageFlow/trasformeddata.jsx new file mode 100644 index 00000000..330da82c --- /dev/null +++ b/ui/src/components/HeirarchicalLineageFlow/trasformeddata.jsx @@ -0,0 +1,83 @@ +export const transformNestedStageData = (rawJson) => { + const nodes = []; + const links = []; + + if (!rawJson?.stages) { + return { nodes, links }; + } + + const envId = `${rawJson.environment || "env"}`; + + nodes.push({ + id: envId, + name: rawJson.environment || "Environment", + type: "Environment", + }); + + const addExecutionChildren = (execution, executionId) => { + if (!Array.isArray(execution.children)) return; + + execution.children.forEach((child) => { + const childId = + child.node_id || + child.execution_id || + `${executionId}-${child.node_name}`; + + nodes.push({ + id: childId, + name: child.node_name || child.execution_type || "Node", + type: "Node", + }); + + links.push({ + source: executionId, + target: childId, + }); + + if (Array.isArray(child.children)) { + addExecutionChildren(child, childId); + } + }); + }; + + rawJson.stages.forEach((stage) => { + const stageId = `stage-${stage.stage_id}`; + + nodes.push({ + id: stageId, + name: stage.stage_name, + type: "Stage", + }); + + links.push({ + source: envId, + target: stageId, + }); + + if (Array.isArray(stage.executions)) { + const orderedExecutions = [...stage.executions].reverse(); // fix reversed backend order + orderedExecutions.forEach((exec) => { + const execId = `exec-${exec.execution_id}`; + const [execName, execUuidLine] = (exec.execution_type || "Execution").split("\n"); + + nodes.push({ + id: execId, + name: execName || "Execution", + type: "Execution", + uuid: execUuidLine || (exec.full_uuid ? exec.full_uuid.substring(0, 4) : ""), + fullUuid: exec.full_uuid || execUuidLine || "", + }); + + links.push({ + source: stageId, + target: execId, + }); + + // ADD CHILD NODES HERE + addExecutionChildren(exec, execId); + }); + } + }); + + return { nodes, links }; +}; \ No newline at end of file diff --git a/ui/src/components/Sidebar/index.jsx b/ui/src/components/Sidebar/index.jsx index 73b3f997..719bf48f 100644 --- a/ui/src/components/Sidebar/index.jsx +++ b/ui/src/components/Sidebar/index.jsx @@ -23,6 +23,7 @@ const LINEAGE_LABELS = { Artifact_Tree: "Artifact Lineage", Execution_Tree: "Execution Lineage", Artifact_Execution_Tree: "Artifact Execution Lineage", + Heirarchical_Lineage: "Hierarchical Lineage", }; // Icon per lineage type @@ -41,6 +42,13 @@ const LineageIcon = ({ type }) => { ); } + if (type === "Heirarchical_Lineage") { + return ( + + + + ); + } return ( diff --git a/ui/src/pages/lineage/index.jsx b/ui/src/pages/lineage/index.jsx index 9e11eea5..91b8b2bc 100644 --- a/ui/src/pages/lineage/index.jsx +++ b/ui/src/pages/lineage/index.jsx @@ -27,6 +27,8 @@ import ExecutionTree from "../../components/ExecutionTree"; import ExecutionTangledDropdown from "../../components/ExecutionTangledDropdown"; import ArtifactExecutionTangledTree from "../../components/ArtifactExecutionTangledTree"; import Loader from "../../components/Loader"; +import Hierarchical_LineageFlow from "../../components/HeirarchicalLineageFlow/heirarchical_lineage"; +import { transformNestedStageData } from "../../components/HeirarchicalLineageFlow/trasformeddata"; const client = new FastAPIClient(config); @@ -37,6 +39,7 @@ const Lineage = () => { "Artifact_Tree", "Execution_Tree", "Artifact_Execution_Tree", + "Heirarchical_Lineage" ]; const [selectedLineageType, setSelectedLineageType] = useState("Artifact_Tree"); const [selectedExecutionType, setSelectedExecutionType] = useState(null); @@ -47,6 +50,7 @@ const Lineage = () => { const [artitreeData, setArtiTreeData] = useState(null); const [artiexetreeData, setArtiExeTreeData] = useState(null); const [loading, setLoading] = useState(true); + const [hierarchicalDataImp, setHierarchicalDataImp] = useState(null); // fetching list of pipelines useEffect(() => { @@ -90,6 +94,8 @@ const Lineage = () => { fetchExecutionTypes(pipeline, selectedLineageType); } else if (selectedLineageType === "Artifact_Execution_Tree") { fetchArtiExeTree(pipeline); + } else if (selectedLineageType === "Heirarchical_Lineage") { + fetchHierarchicalLineageImp(pipeline); } else { fetchArtifactTree(pipeline); } @@ -112,6 +118,8 @@ const Lineage = () => { fetchExecutionTypes(selectedPipeline, lineageType); } else if (lineageType === "Artifact_Execution_Tree") { fetchArtiExeTree(selectedPipeline); + } else if (lineageType === "Heirarchical_Lineage") { + fetchHierarchicalLineageImp(selectedPipeline, ""); } else { fetchArtifactTree(selectedPipeline); } @@ -174,6 +182,25 @@ const Lineage = () => { setLineageArtifactsKey((prevKey) => prevKey + 1); }; + const fetchHierarchicalLineageImp = (pipelineName) => { + setLoading(true); + client.getHierarchicalLineage(pipelineName).then((data) => { + if (data === null) { + setHierarchicalDataImp(null); + setLoading(false); + return; + } + const transformed = transformNestedStageData(data); + setHierarchicalDataImp(transformed); + setLoading(false); + + }).catch((err) => { + console.error("Failed to fetch hierarchical lineage:", err); + setHierarchicalDataImp(null); + setLoading(false); + }); + }; + // Extract uuid from execution_type_name "Prepare_3f45" ---> "3f45" const extractUuid = (data) => { return data.split("_").pop(); @@ -324,6 +351,10 @@ const Lineage = () => { /> )} + {!loading && selectedPipeline !== null && + selectedLineageType === "Heirarchical_Lineage" && + hierarchicalDataImp && () + }