keboola-cli 0.63.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- keboola_agent_cli/__init__.py +34 -0
- keboola_agent_cli/__main__.py +5 -0
- keboola_agent_cli/_ui_dist/assets/arc-DhFYIddx.js +2 -0
- keboola_agent_cli/_ui_dist/assets/arc-DhFYIddx.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/architecture-7EHR7CIX-hNCijx_H.js +1 -0
- keboola_agent_cli/_ui_dist/assets/architectureDiagram-3BPJPVTR-C6hUlprM.js +37 -0
- keboola_agent_cli/_ui_dist/assets/architectureDiagram-3BPJPVTR-C6hUlprM.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/array-BifhSqXX.js +2 -0
- keboola_agent_cli/_ui_dist/assets/array-BifhSqXX.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/blockDiagram-GPEHLZMM-DC7qY9i4.js +133 -0
- keboola_agent_cli/_ui_dist/assets/blockDiagram-GPEHLZMM-DC7qY9i4.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/c4Diagram-AAUBKEIU-5Lh44evt.js +11 -0
- keboola_agent_cli/_ui_dist/assets/c4Diagram-AAUBKEIU-5Lh44evt.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/channel-DBMrXlxx.js +2 -0
- keboola_agent_cli/_ui_dist/assets/channel-DBMrXlxx.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-2J33WTMH-Coy82EBh.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-2J33WTMH-Coy82EBh.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-3OPIFGDE-BQC5CRHI.js +63 -0
- keboola_agent_cli/_ui_dist/assets/chunk-3OPIFGDE-BQC5CRHI.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-4BX2VUAB-DUuEt70o.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-4BX2VUAB-DUuEt70o.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-55IACEB6-BvR-6chF.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-55IACEB6-BvR-6chF.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-5ZQYHXKU-BjcTN7ul.js +3 -0
- keboola_agent_cli/_ui_dist/assets/chunk-5ZQYHXKU-BjcTN7ul.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-727SXJPM-C0zxqqRN.js +207 -0
- keboola_agent_cli/_ui_dist/assets/chunk-727SXJPM-C0zxqqRN.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-AQP2D5EJ-CXf7rIlZ.js +232 -0
- keboola_agent_cli/_ui_dist/assets/chunk-AQP2D5EJ-CXf7rIlZ.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-BSJP7CBP-Oj_FO9Q7.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-BSJP7CBP-Oj_FO9Q7.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-CSCIHK7Q-CcTsLrFc.js +124 -0
- keboola_agent_cli/_ui_dist/assets/chunk-CSCIHK7Q-CcTsLrFc.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-FMBD7UC4-FH-zLkkW.js +16 -0
- keboola_agent_cli/_ui_dist/assets/chunk-FMBD7UC4-FH-zLkkW.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-L5ZTLDWV-B1Ky_e7O.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-L5ZTLDWV-B1Ky_e7O.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-ND2GUHAM-BHz1rpbm.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-ND2GUHAM-BHz1rpbm.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-NNHCCRGN-DlpIbxXb.js +160 -0
- keboola_agent_cli/_ui_dist/assets/chunk-NNHCCRGN-DlpIbxXb.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-NZK2D7GU-tnrSoegS.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-NZK2D7GU-tnrSoegS.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-O5CBEL6O-DxxqDH0l.js +71 -0
- keboola_agent_cli/_ui_dist/assets/chunk-O5CBEL6O-DxxqDH0l.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-QZHKN3VN-CSjc2gjj.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-QZHKN3VN-CSjc2gjj.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/classDiagram-4FO5ZUOK-BuZcZu85.js +2 -0
- keboola_agent_cli/_ui_dist/assets/classDiagram-4FO5ZUOK-BuZcZu85.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/classDiagram-v2-Q7XG4LA2-BuZcZu85.js +2 -0
- keboola_agent_cli/_ui_dist/assets/classDiagram-v2-Q7XG4LA2-BuZcZu85.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/cose-bilkent-S5V4N54A-Y0L8LDMa.js +2 -0
- keboola_agent_cli/_ui_dist/assets/cose-bilkent-S5V4N54A-Y0L8LDMa.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/cytoscape.esm-C8YCVR3_.js +322 -0
- keboola_agent_cli/_ui_dist/assets/cytoscape.esm-C8YCVR3_.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/dagre-BM42HDAG-UZ-9BTqF.js +5 -0
- keboola_agent_cli/_ui_dist/assets/dagre-BM42HDAG-UZ-9BTqF.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/dagre-Bx709z4p.js +2 -0
- keboola_agent_cli/_ui_dist/assets/dagre-Bx709z4p.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/defaultLocale-C8Fc0cco.js +2 -0
- keboola_agent_cli/_ui_dist/assets/defaultLocale-C8Fc0cco.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-2AECGRRQ-DoDQ60wi.js +44 -0
- keboola_agent_cli/_ui_dist/assets/diagram-2AECGRRQ-DoDQ60wi.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-5GNKFQAL-CMGFxpUs.js +11 -0
- keboola_agent_cli/_ui_dist/assets/diagram-5GNKFQAL-CMGFxpUs.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-KO2AKTUF-1uGDa-Iu.js +4 -0
- keboola_agent_cli/_ui_dist/assets/diagram-KO2AKTUF-1uGDa-Iu.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-LMA3HP47-XtFH7B51.js +25 -0
- keboola_agent_cli/_ui_dist/assets/diagram-LMA3HP47-XtFH7B51.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-OG6HWLK6-B4_Te1T5.js +25 -0
- keboola_agent_cli/_ui_dist/assets/diagram-OG6HWLK6-B4_Te1T5.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/dist-Di6zmlv0.js +2 -0
- keboola_agent_cli/_ui_dist/assets/dist-Di6zmlv0.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/erDiagram-TEJ5UH35-NjQkrdFt.js +86 -0
- keboola_agent_cli/_ui_dist/assets/erDiagram-TEJ5UH35-NjQkrdFt.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/eventmodeling-FCH6USID-BrJMIks8.js +1 -0
- keboola_agent_cli/_ui_dist/assets/flowDiagram-I6XJVG4X-CIr8DWl7.js +163 -0
- keboola_agent_cli/_ui_dist/assets/flowDiagram-I6XJVG4X-CIr8DWl7.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/ganttDiagram-6RSMTGT7-C1VY_xbQ.js +293 -0
- keboola_agent_cli/_ui_dist/assets/ganttDiagram-6RSMTGT7-C1VY_xbQ.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/gitGraph-WXDBUCRP-COacYjo-.js +1 -0
- keboola_agent_cli/_ui_dist/assets/gitGraphDiagram-PVQCEYII-DQT8-kg2.js +107 -0
- keboola_agent_cli/_ui_dist/assets/gitGraphDiagram-PVQCEYII-DQT8-kg2.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/graphlib-B8gBHxth.js +2 -0
- keboola_agent_cli/_ui_dist/assets/graphlib-B8gBHxth.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/index-CMq50kkV.css +1 -0
- keboola_agent_cli/_ui_dist/assets/index-D8W97DAz.js +118 -0
- keboola_agent_cli/_ui_dist/assets/index-D8W97DAz.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/info-J43DQDTF-DdCTRIzU.js +1 -0
- keboola_agent_cli/_ui_dist/assets/infoDiagram-5YYISTIA-C77rsoTp.js +3 -0
- keboola_agent_cli/_ui_dist/assets/infoDiagram-5YYISTIA-C77rsoTp.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/init-D6jRqBbL.js +2 -0
- keboola_agent_cli/_ui_dist/assets/init-D6jRqBbL.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/ishikawaDiagram-YF4QCWOH-BcTbXaLy.js +71 -0
- keboola_agent_cli/_ui_dist/assets/ishikawaDiagram-YF4QCWOH-BcTbXaLy.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/journeyDiagram-JHISSGLW-BejeAJQ_.js +140 -0
- keboola_agent_cli/_ui_dist/assets/journeyDiagram-JHISSGLW-BejeAJQ_.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/kanban-definition-UN3LZRKU-BRNz_UrH.js +90 -0
- keboola_agent_cli/_ui_dist/assets/kanban-definition-UN3LZRKU-BRNz_UrH.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/katex-C4eR7coU.js +258 -0
- keboola_agent_cli/_ui_dist/assets/katex-C4eR7coU.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/line-CzAQKFbJ.js +2 -0
- keboola_agent_cli/_ui_dist/assets/line-CzAQKFbJ.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/linear-DUNFFdck.js +2 -0
- keboola_agent_cli/_ui_dist/assets/linear-DUNFFdck.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/mermaid-parser.core-CpuBOkFa.js +5 -0
- keboola_agent_cli/_ui_dist/assets/mermaid-parser.core-CpuBOkFa.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/mindmap-definition-RKZ34NQL-9EJQNjH0.js +97 -0
- keboola_agent_cli/_ui_dist/assets/mindmap-definition-RKZ34NQL-9EJQNjH0.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/ordinal-hYBb2elL.js +2 -0
- keboola_agent_cli/_ui_dist/assets/ordinal-hYBb2elL.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/packet-YPE3B663-DLiiw_B2.js +1 -0
- keboola_agent_cli/_ui_dist/assets/path-BWPyau1x.js +2 -0
- keboola_agent_cli/_ui_dist/assets/path-BWPyau1x.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/pie-LRSECV5Y-CRoO8G1g.js +1 -0
- keboola_agent_cli/_ui_dist/assets/pieDiagram-4H26LBE5-XH4cy6Cb.js +31 -0
- keboola_agent_cli/_ui_dist/assets/pieDiagram-4H26LBE5-XH4cy6Cb.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/quadrantDiagram-W4KKPZXB-fdhc93U8.js +8 -0
- keboola_agent_cli/_ui_dist/assets/quadrantDiagram-W4KKPZXB-fdhc93U8.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/radar-GUYGQ44K-DAlLVJHm.js +1 -0
- keboola_agent_cli/_ui_dist/assets/requirementDiagram-4Y6WPE33-a94eP3R9.js +85 -0
- keboola_agent_cli/_ui_dist/assets/requirementDiagram-4Y6WPE33-a94eP3R9.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/rough.esm-CSKSodPl.js +2 -0
- keboola_agent_cli/_ui_dist/assets/rough.esm-CSKSodPl.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/sankeyDiagram-5OEKKPKP-jcBa02sp.js +41 -0
- keboola_agent_cli/_ui_dist/assets/sankeyDiagram-5OEKKPKP-jcBa02sp.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/sequenceDiagram-3UESZ5HK-A5-GGM-e.js +163 -0
- keboola_agent_cli/_ui_dist/assets/sequenceDiagram-3UESZ5HK-A5-GGM-e.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/src-ZI-V_AF0.js +2 -0
- keboola_agent_cli/_ui_dist/assets/src-ZI-V_AF0.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/stateDiagram-AJRCARHV-BKAA5rqE.js +2 -0
- keboola_agent_cli/_ui_dist/assets/stateDiagram-AJRCARHV-BKAA5rqE.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/stateDiagram-v2-BHNVJYJU-DnJwJBsE.js +2 -0
- keboola_agent_cli/_ui_dist/assets/stateDiagram-v2-BHNVJYJU-DnJwJBsE.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/timeline-definition-PNZ67QCA-Cy39jp8b.js +121 -0
- keboola_agent_cli/_ui_dist/assets/timeline-definition-PNZ67QCA-Cy39jp8b.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/treeView-BLDUP644-DbLYl23-.js +1 -0
- keboola_agent_cli/_ui_dist/assets/treemap-LRROVOQU-Bp0eGlOt.js +1 -0
- keboola_agent_cli/_ui_dist/assets/vennDiagram-CIIHVFJN-BGECKubd.js +35 -0
- keboola_agent_cli/_ui_dist/assets/vennDiagram-CIIHVFJN-BGECKubd.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/wardley-L42UT6IY-D4yH4jqS.js +1 -0
- keboola_agent_cli/_ui_dist/assets/wardleyDiagram-YWT4CUSO-D6XRG3cZ.js +79 -0
- keboola_agent_cli/_ui_dist/assets/wardleyDiagram-YWT4CUSO-D6XRG3cZ.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/xychartDiagram-2RQKCTM6-DRre-pfZ.js +8 -0
- keboola_agent_cli/_ui_dist/assets/xychartDiagram-2RQKCTM6-DRre-pfZ.js.map +1 -0
- keboola_agent_cli/_ui_dist/index.html +50 -0
- keboola_agent_cli/ai_client.py +83 -0
- keboola_agent_cli/auto_update.py +550 -0
- keboola_agent_cli/changelog.py +1198 -0
- keboola_agent_cli/cli.py +448 -0
- keboola_agent_cli/client.py +3422 -0
- keboola_agent_cli/commands/__init__.py +0 -0
- keboola_agent_cli/commands/_data_app_git.py +343 -0
- keboola_agent_cli/commands/_helpers.py +377 -0
- keboola_agent_cli/commands/_metadata_input.py +49 -0
- keboola_agent_cli/commands/_semantic_layer_crud.py +632 -0
- keboola_agent_cli/commands/_semantic_layer_helpers.py +44 -0
- keboola_agent_cli/commands/_semantic_layer_reference_data.py +247 -0
- keboola_agent_cli/commands/agent.py +968 -0
- keboola_agent_cli/commands/branch.py +423 -0
- keboola_agent_cli/commands/changelog.py +168 -0
- keboola_agent_cli/commands/component.py +216 -0
- keboola_agent_cli/commands/config.py +2442 -0
- keboola_agent_cli/commands/context.py +1481 -0
- keboola_agent_cli/commands/data_app.py +1279 -0
- keboola_agent_cli/commands/dev_portal.py +584 -0
- keboola_agent_cli/commands/doctor.py +37 -0
- keboola_agent_cli/commands/encrypt.py +145 -0
- keboola_agent_cli/commands/feature.py +311 -0
- keboola_agent_cli/commands/flow.py +948 -0
- keboola_agent_cli/commands/http_client.py +157 -0
- keboola_agent_cli/commands/init.py +279 -0
- keboola_agent_cli/commands/job.py +661 -0
- keboola_agent_cli/commands/kai.py +301 -0
- keboola_agent_cli/commands/lineage.py +1464 -0
- keboola_agent_cli/commands/org.py +292 -0
- keboola_agent_cli/commands/permissions.py +360 -0
- keboola_agent_cli/commands/project.py +1192 -0
- keboola_agent_cli/commands/repl.py +243 -0
- keboola_agent_cli/commands/schedule.py +340 -0
- keboola_agent_cli/commands/search.py +178 -0
- keboola_agent_cli/commands/semantic_layer.py +939 -0
- keboola_agent_cli/commands/serve.py +272 -0
- keboola_agent_cli/commands/sharing.py +340 -0
- keboola_agent_cli/commands/storage.py +2630 -0
- keboola_agent_cli/commands/stream.py +266 -0
- keboola_agent_cli/commands/sync.py +1277 -0
- keboola_agent_cli/commands/tool.py +206 -0
- keboola_agent_cli/commands/version.py +186 -0
- keboola_agent_cli/commands/workspace.py +635 -0
- keboola_agent_cli/config_store.py +582 -0
- keboola_agent_cli/constants.py +528 -0
- keboola_agent_cli/data_science_client.py +342 -0
- keboola_agent_cli/dev_portal_client.py +323 -0
- keboola_agent_cli/errors.py +248 -0
- keboola_agent_cli/http_base.py +315 -0
- keboola_agent_cli/json_utils.py +126 -0
- keboola_agent_cli/lib.py +536 -0
- keboola_agent_cli/manage_client.py +324 -0
- keboola_agent_cli/metastore_client.py +214 -0
- keboola_agent_cli/models.py +427 -0
- keboola_agent_cli/output.py +1084 -0
- keboola_agent_cli/permissions.py +469 -0
- keboola_agent_cli/py.typed +3 -0
- keboola_agent_cli/result_models.py +271 -0
- keboola_agent_cli/server/__init__.py +34 -0
- keboola_agent_cli/server/agent_runner.py +1289 -0
- keboola_agent_cli/server/agents_store.py +325 -0
- keboola_agent_cli/server/app.py +764 -0
- keboola_agent_cli/server/auth.py +117 -0
- keboola_agent_cli/server/dependencies.py +149 -0
- keboola_agent_cli/server/pricing.py +303 -0
- keboola_agent_cli/server/routers/__init__.py +1 -0
- keboola_agent_cli/server/routers/agents.py +616 -0
- keboola_agent_cli/server/routers/ai_chat.py +129 -0
- keboola_agent_cli/server/routers/branches.py +133 -0
- keboola_agent_cli/server/routers/components.py +48 -0
- keboola_agent_cli/server/routers/configs.py +507 -0
- keboola_agent_cli/server/routers/data_apps.py +384 -0
- keboola_agent_cli/server/routers/dev_portal.py +67 -0
- keboola_agent_cli/server/routers/encrypt.py +35 -0
- keboola_agent_cli/server/routers/feature.py +179 -0
- keboola_agent_cli/server/routers/flows.py +204 -0
- keboola_agent_cli/server/routers/health.py +53 -0
- keboola_agent_cli/server/routers/jobs.py +175 -0
- keboola_agent_cli/server/routers/kai.py +80 -0
- keboola_agent_cli/server/routers/lineage.py +226 -0
- keboola_agent_cli/server/routers/mcp.py +70 -0
- keboola_agent_cli/server/routers/members.py +170 -0
- keboola_agent_cli/server/routers/org.py +96 -0
- keboola_agent_cli/server/routers/projects.py +106 -0
- keboola_agent_cli/server/routers/schedules.py +54 -0
- keboola_agent_cli/server/routers/search.py +30 -0
- keboola_agent_cli/server/routers/semantic_layer.py +650 -0
- keboola_agent_cli/server/routers/sharing.py +86 -0
- keboola_agent_cli/server/routers/storage.py +574 -0
- keboola_agent_cli/server/routers/stream.py +100 -0
- keboola_agent_cli/server/routers/workspaces.py +302 -0
- keboola_agent_cli/server/run_broadcaster.py +329 -0
- keboola_agent_cli/server/sse.py +25 -0
- keboola_agent_cli/services/__init__.py +0 -0
- keboola_agent_cli/services/_encryption.py +217 -0
- keboola_agent_cli/services/_semantic_layer_cascade.py +147 -0
- keboola_agent_cli/services/_semantic_layer_crud.py +382 -0
- keboola_agent_cli/services/_semantic_layer_internals.py +1078 -0
- keboola_agent_cli/services/_semantic_layer_lookup.py +181 -0
- keboola_agent_cli/services/_semantic_layer_reference_data.py +217 -0
- keboola_agent_cli/services/_sync_bindings.py +456 -0
- keboola_agent_cli/services/_sync_branch.py +191 -0
- keboola_agent_cli/services/_sync_bulk.py +228 -0
- keboola_agent_cli/services/_sync_clone.py +163 -0
- keboola_agent_cli/services/_sync_models.py +97 -0
- keboola_agent_cli/services/_sync_push_ops.py +369 -0
- keboola_agent_cli/services/_sync_storage.py +376 -0
- keboola_agent_cli/services/_sync_writeback.py +167 -0
- keboola_agent_cli/services/agent_service.py +458 -0
- keboola_agent_cli/services/base.py +175 -0
- keboola_agent_cli/services/branch_service.py +588 -0
- keboola_agent_cli/services/component_service.py +694 -0
- keboola_agent_cli/services/config_service.py +2099 -0
- keboola_agent_cli/services/data_app_git_service.py +224 -0
- keboola_agent_cli/services/data_app_service.py +2082 -0
- keboola_agent_cli/services/deep_lineage_service.py +1322 -0
- keboola_agent_cli/services/dev_portal_service.py +345 -0
- keboola_agent_cli/services/doctor_service.py +445 -0
- keboola_agent_cli/services/encrypt_service.py +87 -0
- keboola_agent_cli/services/feature_service.py +268 -0
- keboola_agent_cli/services/flow_service.py +769 -0
- keboola_agent_cli/services/flow_validation.py +188 -0
- keboola_agent_cli/services/http_forwarder_service.py +236 -0
- keboola_agent_cli/services/job_idempotency_store.py +285 -0
- keboola_agent_cli/services/job_service.py +797 -0
- keboola_agent_cli/services/kai_service.py +367 -0
- keboola_agent_cli/services/lineage_service.py +274 -0
- keboola_agent_cli/services/mcp_service.py +1498 -0
- keboola_agent_cli/services/mcp_transport.py +259 -0
- keboola_agent_cli/services/member_service.py +593 -0
- keboola_agent_cli/services/org_service.py +619 -0
- keboola_agent_cli/services/project_service.py +947 -0
- keboola_agent_cli/services/repo_validate_service.py +767 -0
- keboola_agent_cli/services/schedule_service.py +731 -0
- keboola_agent_cli/services/search_service.py +331 -0
- keboola_agent_cli/services/semantic_layer_service.py +1497 -0
- keboola_agent_cli/services/sharing_service.py +307 -0
- keboola_agent_cli/services/storage_service.py +2524 -0
- keboola_agent_cli/services/stream_service.py +395 -0
- keboola_agent_cli/services/sync_service.py +2244 -0
- keboola_agent_cli/services/variables_service.py +447 -0
- keboola_agent_cli/services/version_service.py +1038 -0
- keboola_agent_cli/services/workspace_service.py +1103 -0
- keboola_agent_cli/stream_client.py +217 -0
- keboola_agent_cli/sync/__init__.py +1 -0
- keboola_agent_cli/sync/branch_mapping.py +174 -0
- keboola_agent_cli/sync/clone.py +211 -0
- keboola_agent_cli/sync/code_extraction.py +655 -0
- keboola_agent_cli/sync/config_format.py +290 -0
- keboola_agent_cli/sync/diff_engine.py +566 -0
- keboola_agent_cli/sync/git_utils.py +93 -0
- keboola_agent_cli/sync/manifest.py +162 -0
- keboola_agent_cli/sync/naming.py +90 -0
- keboola_agent_cli/sync/secrets.py +62 -0
- keboola_agent_cli/sync/sql_split.py +134 -0
- keboola_cli-0.63.4.dist-info/METADATA +308 -0
- keboola_cli-0.63.4.dist-info/RECORD +306 -0
- keboola_cli-0.63.4.dist-info/WHEEL +4 -0
- keboola_cli-0.63.4.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,1464 @@
|
|
|
1
|
+
"""Lineage commands - column-level dependency analysis across projects.
|
|
2
|
+
|
|
3
|
+
Thin CLI layer: parses arguments, calls DeepLineageService, formats output.
|
|
4
|
+
No business logic belongs here.
|
|
5
|
+
|
|
6
|
+
Four subcommands:
|
|
7
|
+
build -- scan sync'd projects, build lineage graph, save cache
|
|
8
|
+
show -- query upstream/downstream from cached graph
|
|
9
|
+
serve -- start local web server with interactive lineage browser
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import http.server
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import threading
|
|
16
|
+
import webbrowser
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from urllib.parse import parse_qs, urlparse
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
|
|
22
|
+
from ..errors import ErrorCode
|
|
23
|
+
from ..services.deep_lineage_service import DeepLineageService, LineageGraph
|
|
24
|
+
from ._helpers import (
|
|
25
|
+
check_cli_permission,
|
|
26
|
+
get_formatter,
|
|
27
|
+
get_service,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
lineage_app = typer.Typer(
|
|
31
|
+
help="Column-level data lineage across projects.\n\n"
|
|
32
|
+
"Build a dependency graph from sync'd data, then query upstream/downstream."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@lineage_app.callback(invoke_without_command=True)
|
|
37
|
+
def _lineage_callback(ctx: typer.Context) -> None:
|
|
38
|
+
check_cli_permission(ctx, "lineage")
|
|
39
|
+
if ctx.invoked_subcommand is None:
|
|
40
|
+
# No subcommand -> show help
|
|
41
|
+
click_cmd = typer.main.get_command(lineage_app)
|
|
42
|
+
ctx_help = click_cmd.make_context("lineage", [])
|
|
43
|
+
typer.echo(click_cmd.get_help(ctx_help))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# -- lineage build ---------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@lineage_app.command("build")
|
|
50
|
+
def lineage_build(
|
|
51
|
+
ctx: typer.Context,
|
|
52
|
+
directory: Path = typer.Option(
|
|
53
|
+
Path("."),
|
|
54
|
+
"--directory",
|
|
55
|
+
"-d",
|
|
56
|
+
help="Root directory with sync'd projects (default: current directory).",
|
|
57
|
+
),
|
|
58
|
+
output: Path = typer.Option(
|
|
59
|
+
...,
|
|
60
|
+
"--output",
|
|
61
|
+
"-o",
|
|
62
|
+
help="Output JSON file for the lineage graph (required).",
|
|
63
|
+
),
|
|
64
|
+
ai: bool = typer.Option(
|
|
65
|
+
False,
|
|
66
|
+
"--ai",
|
|
67
|
+
help="Generate AI task file for SQL/Python analysis (2-step: AI processes, then re-build).",
|
|
68
|
+
),
|
|
69
|
+
refresh: bool = typer.Option(
|
|
70
|
+
False,
|
|
71
|
+
"--refresh",
|
|
72
|
+
help="Sync pull all projects first, then rebuild.",
|
|
73
|
+
),
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Build column-level lineage graph from sync'd data.
|
|
76
|
+
|
|
77
|
+
Scans all sync'd projects (from `sync pull --all-projects`), detects
|
|
78
|
+
table dependencies via config mappings and SQL parsing, and saves the
|
|
79
|
+
graph to a JSON cache file for fast queries with `lineage show`.
|
|
80
|
+
|
|
81
|
+
AI-enhanced analysis is a 2-step process:
|
|
82
|
+
|
|
83
|
+
1. kbagent lineage build -d /path -o lineage.json --ai
|
|
84
|
+
(builds deterministic graph + generates .lineage_ai_tasks.json)
|
|
85
|
+
|
|
86
|
+
2. AI agent reads the task file, analyzes each code file from disk,
|
|
87
|
+
writes results to .lineage_ai_results.json
|
|
88
|
+
|
|
89
|
+
3. kbagent lineage build -d /path -o lineage.json
|
|
90
|
+
(automatically applies AI results if .lineage_ai_results.json exists)
|
|
91
|
+
"""
|
|
92
|
+
formatter = get_formatter(ctx)
|
|
93
|
+
service = get_service(ctx, "deep_lineage_service")
|
|
94
|
+
|
|
95
|
+
root = directory.resolve()
|
|
96
|
+
if not root.is_dir():
|
|
97
|
+
formatter.error(message=f"Directory not found: {root}", error_code=ErrorCode.DIR_NOT_FOUND)
|
|
98
|
+
raise typer.Exit(code=1)
|
|
99
|
+
|
|
100
|
+
# --refresh: sync pull all projects first
|
|
101
|
+
if refresh:
|
|
102
|
+
if not formatter.json_mode:
|
|
103
|
+
formatter.console.print("[bold]Syncing all projects...[/bold]")
|
|
104
|
+
sync_service = get_service(ctx, "sync_service")
|
|
105
|
+
sync_result = sync_service.pull_all(base_dir=root)
|
|
106
|
+
summary = sync_result.get("summary", {})
|
|
107
|
+
if not formatter.json_mode:
|
|
108
|
+
formatter.console.print(
|
|
109
|
+
f" Synced {summary.get('success', 0)}/{summary.get('total', 0)} projects"
|
|
110
|
+
f" ({summary.get('failed', 0)} failed)\n"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
result = service.build_lineage(root, generate_ai_tasks=ai)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
with open(output, "w") as f:
|
|
117
|
+
json.dump(result, f, indent=2)
|
|
118
|
+
except OSError as exc:
|
|
119
|
+
formatter.error(
|
|
120
|
+
message=f"Cannot write output file '{output}': {exc}",
|
|
121
|
+
error_code=ErrorCode.WRITE_ERROR,
|
|
122
|
+
)
|
|
123
|
+
raise typer.Exit(code=1) from None
|
|
124
|
+
|
|
125
|
+
if formatter.json_mode:
|
|
126
|
+
formatter.output(result)
|
|
127
|
+
else:
|
|
128
|
+
summary = result.get("summary", {})
|
|
129
|
+
formatter.console.print("\n[bold]Lineage graph built[/bold]")
|
|
130
|
+
formatter.console.print(f" Tables: {summary.get('tables', 0)}")
|
|
131
|
+
formatter.console.print(f" Configurations: {summary.get('configurations', 0)}")
|
|
132
|
+
formatter.console.print(f" Edges: {summary.get('edges', 0)}")
|
|
133
|
+
if summary.get("detection_methods"):
|
|
134
|
+
formatter.console.print("\n Detection methods:")
|
|
135
|
+
for k, v in sorted(summary["detection_methods"].items(), key=lambda x: -x[1]):
|
|
136
|
+
formatter.console.print(f" {k}: {v}")
|
|
137
|
+
# AI status
|
|
138
|
+
ai_status = result.get("ai_status", {})
|
|
139
|
+
if ai_status.get("ai_results_applied"):
|
|
140
|
+
formatter.console.print(
|
|
141
|
+
f"\n AI results applied: {ai_status.get('ai_edges_added', 0)} edges added"
|
|
142
|
+
)
|
|
143
|
+
if ai_status.get("ai_tasks_generated"):
|
|
144
|
+
formatter.console.print(
|
|
145
|
+
f"\n [bold]AI tasks generated: {ai_status['ai_tasks_generated']}[/bold]"
|
|
146
|
+
f" (already done: {ai_status.get('ai_already_done', 0)})"
|
|
147
|
+
)
|
|
148
|
+
formatter.console.print(f" Task file: {ai_status['ai_tasks_file']}")
|
|
149
|
+
formatter.console.print(
|
|
150
|
+
" Next: let your AI agent process the tasks, then re-run this command."
|
|
151
|
+
)
|
|
152
|
+
# Surface warnings (e.g. empty scan) so users notice layout issues.
|
|
153
|
+
for warning in result.get("warnings", []) or []:
|
|
154
|
+
formatter.console.print(f"\n[yellow]Warning:[/yellow] {warning}")
|
|
155
|
+
formatter.console.print(f"\n Saved to: {output}")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# -- lineage info ----------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@lineage_app.command("info")
|
|
162
|
+
def lineage_info(
|
|
163
|
+
ctx: typer.Context,
|
|
164
|
+
load: Path = typer.Option(
|
|
165
|
+
...,
|
|
166
|
+
"--load",
|
|
167
|
+
"-l",
|
|
168
|
+
help="Lineage JSON cache file (from `lineage build`).",
|
|
169
|
+
),
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Show what's in a cached lineage graph.
|
|
172
|
+
|
|
173
|
+
Displays per-project breakdown (tables, configs) and lists the most
|
|
174
|
+
connected tables -- good starting points for upstream/downstream queries.
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
|
|
178
|
+
kbagent lineage info -l lineage.json
|
|
179
|
+
"""
|
|
180
|
+
formatter = get_formatter(ctx)
|
|
181
|
+
service = get_service(ctx, "deep_lineage_service")
|
|
182
|
+
|
|
183
|
+
if not load.exists():
|
|
184
|
+
formatter.error(
|
|
185
|
+
message=f"Cache file not found: {load}", error_code=ErrorCode.FILE_NOT_FOUND
|
|
186
|
+
)
|
|
187
|
+
raise typer.Exit(code=1)
|
|
188
|
+
|
|
189
|
+
graph = service.load_from_cache(load)
|
|
190
|
+
|
|
191
|
+
if formatter.json_mode:
|
|
192
|
+
formatter.output(graph.to_dict())
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
summary = graph.summary()
|
|
196
|
+
formatter.console.print("\n[bold]Lineage Graph Contents[/bold]")
|
|
197
|
+
formatter.console.print(f" Tables: {summary.get('tables', 0)}")
|
|
198
|
+
formatter.console.print(f" Configurations: {summary.get('configurations', 0)}")
|
|
199
|
+
formatter.console.print(f" Edges: {summary.get('edges', 0)}")
|
|
200
|
+
if summary.get("detection_methods"):
|
|
201
|
+
formatter.console.print("\n Detection methods:")
|
|
202
|
+
for k, v in sorted(summary["detection_methods"].items(), key=lambda x: -x[1]):
|
|
203
|
+
formatter.console.print(f" {k}: {v}")
|
|
204
|
+
|
|
205
|
+
# Per-project breakdown
|
|
206
|
+
proj_tables: dict[str, int] = {}
|
|
207
|
+
proj_configs: dict[str, int] = {}
|
|
208
|
+
for t in graph.tables.values():
|
|
209
|
+
proj_tables[t.project_alias] = proj_tables.get(t.project_alias, 0) + 1
|
|
210
|
+
for c in graph.configurations.values():
|
|
211
|
+
proj_configs[c.project_alias] = proj_configs.get(c.project_alias, 0) + 1
|
|
212
|
+
all_projects = sorted(set(proj_tables) | set(proj_configs))
|
|
213
|
+
if all_projects:
|
|
214
|
+
formatter.console.print("\n [bold]Projects:[/bold]")
|
|
215
|
+
for proj in all_projects:
|
|
216
|
+
nt = proj_tables.get(proj, 0)
|
|
217
|
+
nc = proj_configs.get(proj, 0)
|
|
218
|
+
formatter.console.print(f" {proj:40s} {nt:4d} tables, {nc:4d} configs")
|
|
219
|
+
|
|
220
|
+
# Most connected tables
|
|
221
|
+
edge_counts: dict[str, int] = {}
|
|
222
|
+
for e in graph.edges:
|
|
223
|
+
for fqn in (e.source_fqn, e.target_fqn):
|
|
224
|
+
if fqn in graph.tables:
|
|
225
|
+
edge_counts[fqn] = edge_counts.get(fqn, 0) + 1
|
|
226
|
+
top = sorted(edge_counts.items(), key=lambda x: -x[1])[:15]
|
|
227
|
+
if top:
|
|
228
|
+
formatter.console.print(
|
|
229
|
+
"\n [bold]Most connected tables[/bold]"
|
|
230
|
+
" (use with [cyan]lineage show --upstream/--downstream[/cyan]):"
|
|
231
|
+
)
|
|
232
|
+
for fqn, count in top:
|
|
233
|
+
t = graph.tables[fqn]
|
|
234
|
+
formatter.console.print(f" {fqn:60s} {count:3d} edges, {t.rows_count:>12,} rows")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# -- lineage show ----------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@lineage_app.command("show")
|
|
241
|
+
def lineage_show(
|
|
242
|
+
ctx: typer.Context,
|
|
243
|
+
load: Path = typer.Option(
|
|
244
|
+
...,
|
|
245
|
+
"--load",
|
|
246
|
+
"-l",
|
|
247
|
+
help="Lineage JSON cache file (from `lineage build`).",
|
|
248
|
+
),
|
|
249
|
+
upstream: str | None = typer.Option(
|
|
250
|
+
None,
|
|
251
|
+
"--upstream",
|
|
252
|
+
help="Show upstream dependencies. Use 'project:table_id' or just 'table_id'.",
|
|
253
|
+
),
|
|
254
|
+
downstream: str | None = typer.Option(
|
|
255
|
+
None,
|
|
256
|
+
"--downstream",
|
|
257
|
+
help="Show downstream dependents. Use 'project:table_id' or just 'table_id'.",
|
|
258
|
+
),
|
|
259
|
+
column: str | None = typer.Option(
|
|
260
|
+
None,
|
|
261
|
+
"--column",
|
|
262
|
+
"-c",
|
|
263
|
+
help="Trace a specific column (use with --upstream/--downstream).",
|
|
264
|
+
),
|
|
265
|
+
columns: bool = typer.Option(
|
|
266
|
+
False,
|
|
267
|
+
"--columns",
|
|
268
|
+
help="Show column-level mapping detail on edges.",
|
|
269
|
+
),
|
|
270
|
+
project: str | None = typer.Option(
|
|
271
|
+
None,
|
|
272
|
+
"--project",
|
|
273
|
+
"-p",
|
|
274
|
+
help="Project alias filter for queries.",
|
|
275
|
+
),
|
|
276
|
+
depth: int = typer.Option(10, "--depth", help="Max traversal depth (default: 10)."),
|
|
277
|
+
format: str = typer.Option(
|
|
278
|
+
"text",
|
|
279
|
+
"--format",
|
|
280
|
+
"-f",
|
|
281
|
+
help="Output format: text, mermaid, html, or er (entity-relationship).",
|
|
282
|
+
),
|
|
283
|
+
) -> None:
|
|
284
|
+
"""Query upstream/downstream dependencies from a cached lineage graph.
|
|
285
|
+
|
|
286
|
+
Requires a lineage cache file built with `lineage build`.
|
|
287
|
+
|
|
288
|
+
Node identifiers for --upstream/--downstream:
|
|
289
|
+
|
|
290
|
+
Full FQN: project-alias:bucket_id.table_name
|
|
291
|
+
|
|
292
|
+
Table only: bucket_id.table_name (auto-resolves, warns if ambiguous)
|
|
293
|
+
|
|
294
|
+
Output formats (--format):
|
|
295
|
+
|
|
296
|
+
text Rich tree (default)
|
|
297
|
+
|
|
298
|
+
mermaid Mermaid flowchart source code
|
|
299
|
+
|
|
300
|
+
html Self-contained HTML file with embedded mermaid diagram
|
|
301
|
+
|
|
302
|
+
Examples:
|
|
303
|
+
|
|
304
|
+
kbagent lineage show -l lineage.json --downstream "project:table"
|
|
305
|
+
|
|
306
|
+
kbagent lineage show -l lineage.json --upstream "project:table" --columns
|
|
307
|
+
|
|
308
|
+
kbagent lineage show -l lineage.json --upstream "project:table" -c "col_name"
|
|
309
|
+
|
|
310
|
+
kbagent lineage show -l lineage.json --downstream "project:table" -f mermaid
|
|
311
|
+
|
|
312
|
+
kbagent lineage show -l lineage.json --downstream "project:table" -f html
|
|
313
|
+
"""
|
|
314
|
+
formatter = get_formatter(ctx)
|
|
315
|
+
service = get_service(ctx, "deep_lineage_service")
|
|
316
|
+
|
|
317
|
+
valid_formats = ("text", "mermaid", "html", "er")
|
|
318
|
+
if format not in valid_formats:
|
|
319
|
+
formatter.error(
|
|
320
|
+
message=f"Invalid format '{format}'. Must be one of: {', '.join(valid_formats)}",
|
|
321
|
+
error_code=ErrorCode.INVALID_FORMAT,
|
|
322
|
+
)
|
|
323
|
+
raise typer.Exit(code=2)
|
|
324
|
+
|
|
325
|
+
if not load.exists():
|
|
326
|
+
formatter.error(
|
|
327
|
+
message=f"Cache file not found: {load}", error_code=ErrorCode.FILE_NOT_FOUND
|
|
328
|
+
)
|
|
329
|
+
raise typer.Exit(code=1)
|
|
330
|
+
|
|
331
|
+
graph = service.load_from_cache(load)
|
|
332
|
+
|
|
333
|
+
if not upstream and not downstream:
|
|
334
|
+
formatter.error(
|
|
335
|
+
message="Specify --upstream or --downstream to query.\n"
|
|
336
|
+
"Use `kbagent lineage info -l FILE` to see what's in the graph.",
|
|
337
|
+
error_code=ErrorCode.MISSING_QUERY,
|
|
338
|
+
)
|
|
339
|
+
raise typer.Exit(code=2)
|
|
340
|
+
|
|
341
|
+
display_opts = {"show_columns": columns, "filter_column": column}
|
|
342
|
+
|
|
343
|
+
if upstream:
|
|
344
|
+
query_result = service.query_upstream(graph, upstream, project or "", depth)
|
|
345
|
+
if "error" in query_result:
|
|
346
|
+
suggestions = query_result.get("suggestions", [])
|
|
347
|
+
msg = query_result["error"]
|
|
348
|
+
if suggestions:
|
|
349
|
+
msg += "\nDid you mean: " + ", ".join(suggestions[:5])
|
|
350
|
+
formatter.error(message=msg, error_code=ErrorCode.NODE_NOT_FOUND)
|
|
351
|
+
raise typer.Exit(code=1)
|
|
352
|
+
|
|
353
|
+
if formatter.json_mode:
|
|
354
|
+
if column:
|
|
355
|
+
query_result = _filter_column_json(query_result, column)
|
|
356
|
+
formatter.output(query_result)
|
|
357
|
+
elif format in ("mermaid", "html", "er"):
|
|
358
|
+
_output_mermaid_or_html(formatter, service, graph, query_result, "upstream", format)
|
|
359
|
+
else:
|
|
360
|
+
_format_lineage_tree(formatter, graph, query_result, "upstream", **display_opts)
|
|
361
|
+
|
|
362
|
+
if downstream:
|
|
363
|
+
query_result = service.query_downstream(graph, downstream, project or "", depth)
|
|
364
|
+
if "error" in query_result:
|
|
365
|
+
suggestions = query_result.get("suggestions", [])
|
|
366
|
+
msg = query_result["error"]
|
|
367
|
+
if suggestions:
|
|
368
|
+
msg += "\nDid you mean: " + ", ".join(suggestions[:5])
|
|
369
|
+
formatter.error(message=msg, error_code=ErrorCode.NODE_NOT_FOUND)
|
|
370
|
+
raise typer.Exit(code=1)
|
|
371
|
+
|
|
372
|
+
if formatter.json_mode:
|
|
373
|
+
if column:
|
|
374
|
+
query_result = _filter_column_json(query_result, column)
|
|
375
|
+
formatter.output(query_result)
|
|
376
|
+
elif format in ("mermaid", "html", "er"):
|
|
377
|
+
_output_mermaid_or_html(formatter, service, graph, query_result, "downstream", format)
|
|
378
|
+
else:
|
|
379
|
+
_format_lineage_tree(formatter, graph, query_result, "downstream", **display_opts)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# -- Output formatting helpers ----------------------------------------------
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _output_mermaid_or_html(
|
|
386
|
+
formatter,
|
|
387
|
+
service,
|
|
388
|
+
graph,
|
|
389
|
+
query_result: dict,
|
|
390
|
+
direction: str,
|
|
391
|
+
output_format: str,
|
|
392
|
+
) -> None:
|
|
393
|
+
"""Render query result as mermaid, html, or er diagram and output it."""
|
|
394
|
+
from ..services.deep_lineage_service import DeepLineageService
|
|
395
|
+
|
|
396
|
+
node_fqn = query_result["node"]
|
|
397
|
+
edges = query_result.get("edges", [])
|
|
398
|
+
|
|
399
|
+
if output_format == "er":
|
|
400
|
+
er_code = DeepLineageService.render_er_diagram(edges, graph, node_fqn)
|
|
401
|
+
typer.echo(er_code)
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
mermaid_code = DeepLineageService.render_mermaid(edges, graph, direction, node_fqn)
|
|
405
|
+
|
|
406
|
+
if output_format == "mermaid":
|
|
407
|
+
typer.echo(mermaid_code)
|
|
408
|
+
elif output_format == "html":
|
|
409
|
+
title = f"Lineage {direction} of {node_fqn}"
|
|
410
|
+
html_content = DeepLineageService.render_html(mermaid_code, title)
|
|
411
|
+
sanitized_node = re.sub(r"[^a-zA-Z0-9_]", "_", node_fqn)
|
|
412
|
+
filename = f"lineage_{direction}_{sanitized_node}.html"
|
|
413
|
+
try:
|
|
414
|
+
with open(filename, "w") as f:
|
|
415
|
+
f.write(html_content)
|
|
416
|
+
if not formatter.json_mode:
|
|
417
|
+
formatter.console.print(f"HTML lineage diagram saved to: {filename}")
|
|
418
|
+
except OSError as exc:
|
|
419
|
+
formatter.error(
|
|
420
|
+
message=f"Cannot write HTML file '{filename}': {exc}",
|
|
421
|
+
error_code=ErrorCode.WRITE_ERROR,
|
|
422
|
+
)
|
|
423
|
+
raise typer.Exit(code=1) from None
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _filter_column_json(result: dict, column_name: str) -> dict:
|
|
427
|
+
"""Filter JSON query result to only edges relevant to a specific column."""
|
|
428
|
+
filtered_edges = []
|
|
429
|
+
col_lower = column_name.lower()
|
|
430
|
+
for edge in result.get("edges", []):
|
|
431
|
+
col_map = edge.get("column_mapping", {})
|
|
432
|
+
edge_columns = edge.get("columns", [])
|
|
433
|
+
mapped_keys = [k for k in col_map if k.lower() == col_lower]
|
|
434
|
+
mapped_vals = [k for k, v in col_map.items() if v.lower().endswith(f".{col_lower}")]
|
|
435
|
+
col_match = any(c.lower() == col_lower for c in edge_columns)
|
|
436
|
+
if mapped_keys or mapped_vals or col_match:
|
|
437
|
+
relevant_map = {
|
|
438
|
+
k: v for k, v in col_map.items() if k in mapped_keys or k in mapped_vals
|
|
439
|
+
}
|
|
440
|
+
edge_copy = dict(edge)
|
|
441
|
+
if relevant_map:
|
|
442
|
+
edge_copy["column_mapping"] = relevant_map
|
|
443
|
+
filtered_edges.append(edge_copy)
|
|
444
|
+
elif not col_map and not edge_columns:
|
|
445
|
+
filtered_edges.append(edge)
|
|
446
|
+
result_copy = dict(result)
|
|
447
|
+
result_copy["edges"] = filtered_edges
|
|
448
|
+
result_copy["column_filter"] = column_name
|
|
449
|
+
return result_copy
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _format_lineage_tree(
|
|
453
|
+
formatter,
|
|
454
|
+
graph,
|
|
455
|
+
result: dict,
|
|
456
|
+
direction: str,
|
|
457
|
+
show_columns: bool = False,
|
|
458
|
+
filter_column: str | None = None,
|
|
459
|
+
) -> None:
|
|
460
|
+
"""Format lineage query result as a human-readable tree."""
|
|
461
|
+
from ..services.deep_lineage_service import LineageGraph
|
|
462
|
+
|
|
463
|
+
node_fqn = result["node"]
|
|
464
|
+
node_info = result.get("node_info", {})
|
|
465
|
+
edges = result.get("edges", [])
|
|
466
|
+
|
|
467
|
+
arrow = "<-" if direction == "upstream" else "->"
|
|
468
|
+
label = "Upstream dependencies" if direction == "upstream" else "Downstream dependents"
|
|
469
|
+
|
|
470
|
+
node_type = node_info.get("type", "unknown")
|
|
471
|
+
if node_type == "table":
|
|
472
|
+
n_cols = node_info.get("columns", 0)
|
|
473
|
+
rows = node_info.get("rows", 0)
|
|
474
|
+
desc = f"[table] {node_fqn} ({n_cols} cols, {rows:,} rows)"
|
|
475
|
+
elif node_info.get("name"):
|
|
476
|
+
desc = f"[{node_type}] {node_info['name']} ({node_info.get('component', '')})"
|
|
477
|
+
else:
|
|
478
|
+
desc = f"[{node_type}] {node_fqn}"
|
|
479
|
+
|
|
480
|
+
header = f"\n[bold]{label} of {desc}[/bold]"
|
|
481
|
+
if filter_column:
|
|
482
|
+
header += f" [dim](column: {filter_column})[/dim]"
|
|
483
|
+
formatter.console.print(header + "\n")
|
|
484
|
+
|
|
485
|
+
if not edges:
|
|
486
|
+
formatter.console.print(" (none found)")
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
col_lower = filter_column.lower() if filter_column else None
|
|
490
|
+
|
|
491
|
+
for edge in sorted(edges, key=lambda e: e["depth"]):
|
|
492
|
+
col_map = edge.get("column_mapping", {})
|
|
493
|
+
edge_columns = edge.get("columns", [])
|
|
494
|
+
|
|
495
|
+
if col_lower:
|
|
496
|
+
has_in_map = any(
|
|
497
|
+
k.lower() == col_lower or v.lower().endswith(f".{col_lower}")
|
|
498
|
+
for k, v in col_map.items()
|
|
499
|
+
)
|
|
500
|
+
has_in_cols = any(c.lower() == col_lower for c in edge_columns)
|
|
501
|
+
is_structural = not col_map and not edge_columns
|
|
502
|
+
if not has_in_map and not has_in_cols and not is_structural:
|
|
503
|
+
continue
|
|
504
|
+
|
|
505
|
+
indent = " " * edge["depth"]
|
|
506
|
+
target_fqn = edge["source"] if direction == "upstream" else edge["target"]
|
|
507
|
+
|
|
508
|
+
if isinstance(graph, LineageGraph):
|
|
509
|
+
if target_fqn in graph.tables:
|
|
510
|
+
t = graph.tables[target_fqn]
|
|
511
|
+
node_desc = f"[table] {target_fqn} ({len(t.columns)} cols, {t.rows_count:,} rows)"
|
|
512
|
+
elif target_fqn in graph.configurations:
|
|
513
|
+
c = graph.configurations[target_fqn]
|
|
514
|
+
node_desc = (
|
|
515
|
+
f"[{c.component_type}] {c.project_alias}:{c.config_name} ({c.component_id})"
|
|
516
|
+
)
|
|
517
|
+
else:
|
|
518
|
+
node_desc = target_fqn
|
|
519
|
+
else:
|
|
520
|
+
node_desc = target_fqn
|
|
521
|
+
|
|
522
|
+
col_hint = ""
|
|
523
|
+
if not show_columns and edge_columns:
|
|
524
|
+
col_list = edge_columns[:5]
|
|
525
|
+
suffix = f"... +{len(edge_columns) - 5}" if len(edge_columns) > 5 else ""
|
|
526
|
+
col_hint = f" [{', '.join(col_list)}{suffix}]"
|
|
527
|
+
|
|
528
|
+
formatter.console.print(f"{indent}{arrow} ({edge['detection']}) {node_desc}{col_hint}")
|
|
529
|
+
|
|
530
|
+
if show_columns and col_map:
|
|
531
|
+
map_indent = " " * (edge["depth"] + 1)
|
|
532
|
+
items = list(col_map.items())
|
|
533
|
+
if col_lower:
|
|
534
|
+
items = [
|
|
535
|
+
(k, v)
|
|
536
|
+
for k, v in items
|
|
537
|
+
if k.lower() == col_lower or v.lower().endswith(f".{col_lower}")
|
|
538
|
+
]
|
|
539
|
+
for out_col, src_expr in items:
|
|
540
|
+
src_short = src_expr.split(".")[-1] if "." in src_expr else src_expr
|
|
541
|
+
src_table = ".".join(src_expr.split(".")[:-1]) if "." in src_expr else ""
|
|
542
|
+
if src_table:
|
|
543
|
+
formatter.console.print(
|
|
544
|
+
f"{map_indent}[dim]{out_col}[/dim] <- {src_table}.[bold]{src_short}[/bold]"
|
|
545
|
+
)
|
|
546
|
+
else:
|
|
547
|
+
formatter.console.print(f"{map_indent}[dim]{out_col}[/dim] <- {src_expr}")
|
|
548
|
+
elif show_columns and edge_columns:
|
|
549
|
+
map_indent = " " * (edge["depth"] + 1)
|
|
550
|
+
show_cols = edge_columns
|
|
551
|
+
if col_lower:
|
|
552
|
+
show_cols = [c for c in edge_columns if c.lower() == col_lower]
|
|
553
|
+
for c in show_cols[:10]:
|
|
554
|
+
formatter.console.print(f"{map_indent}[dim]{c}[/dim]")
|
|
555
|
+
if len(show_cols) > 10:
|
|
556
|
+
formatter.console.print(f"{map_indent}[dim]... +{len(show_cols) - 10} more[/dim]")
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
# -- lineage serve ---------------------------------------------------------
|
|
560
|
+
|
|
561
|
+
_LINEAGE_HTML_TEMPLATE = r"""<!DOCTYPE html>
|
|
562
|
+
<html lang="en">
|
|
563
|
+
<head>
|
|
564
|
+
<meta charset="utf-8">
|
|
565
|
+
<title>Keboola Lineage Browser</title>
|
|
566
|
+
<style>
|
|
567
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
568
|
+
body {
|
|
569
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
570
|
+
height: 100vh; overflow: hidden; display: flex; background: #fff; color: #333;
|
|
571
|
+
}
|
|
572
|
+
/* -- Sidebar -- */
|
|
573
|
+
#sidebar {
|
|
574
|
+
width: 280px; min-width: 280px; background: #f8f9fa;
|
|
575
|
+
border-right: 1px solid #e0e0e0; display: flex; flex-direction: column;
|
|
576
|
+
height: 100vh; overflow: hidden;
|
|
577
|
+
}
|
|
578
|
+
#sidebar-header {
|
|
579
|
+
padding: 16px; border-bottom: 1px solid #e0e0e0;
|
|
580
|
+
}
|
|
581
|
+
#sidebar-header h1 {
|
|
582
|
+
font-size: 15px; font-weight: 700; color: #1a73e8; margin-bottom: 12px;
|
|
583
|
+
}
|
|
584
|
+
#project-select {
|
|
585
|
+
width: 100%; padding: 6px 8px; border: 1px solid #dadce0; border-radius: 4px;
|
|
586
|
+
font-size: 13px; background: #fff; color: #333; outline: none;
|
|
587
|
+
}
|
|
588
|
+
#project-select:focus { border-color: #1a73e8; }
|
|
589
|
+
|
|
590
|
+
/* Tabs */
|
|
591
|
+
#tabs {
|
|
592
|
+
display: flex; border-bottom: 1px solid #e0e0e0;
|
|
593
|
+
}
|
|
594
|
+
.tab {
|
|
595
|
+
flex: 1; padding: 8px 0; text-align: center; font-size: 12px; font-weight: 600;
|
|
596
|
+
cursor: pointer; color: #5f6368; border-bottom: 2px solid transparent;
|
|
597
|
+
background: none; border-top: none; border-left: none; border-right: none;
|
|
598
|
+
}
|
|
599
|
+
.tab.active { color: #1a73e8; border-bottom-color: #1a73e8; }
|
|
600
|
+
.tab:hover { background: #e8eaed; }
|
|
601
|
+
|
|
602
|
+
/* Search */
|
|
603
|
+
#node-search {
|
|
604
|
+
margin: 8px 12px; padding: 6px 8px; border: 1px solid #dadce0; border-radius: 4px;
|
|
605
|
+
font-size: 13px; outline: none; width: calc(100% - 24px);
|
|
606
|
+
}
|
|
607
|
+
#node-search:focus { border-color: #1a73e8; }
|
|
608
|
+
|
|
609
|
+
/* Node list */
|
|
610
|
+
#node-list {
|
|
611
|
+
flex: 1; overflow-y: auto; padding: 0;
|
|
612
|
+
}
|
|
613
|
+
.node-item {
|
|
614
|
+
padding: 6px 12px; cursor: pointer; font-size: 12px; color: #333;
|
|
615
|
+
border-bottom: 1px solid #f0f0f0; white-space: nowrap; overflow: hidden;
|
|
616
|
+
text-overflow: ellipsis;
|
|
617
|
+
}
|
|
618
|
+
.node-item:hover { background: #e8f0fe; }
|
|
619
|
+
.node-item.selected { background: #d2e3fc; font-weight: 600; }
|
|
620
|
+
.node-item .node-meta {
|
|
621
|
+
font-size: 11px; color: #888; margin-top: 1px;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/* Controls below list */
|
|
625
|
+
#query-controls {
|
|
626
|
+
padding: 12px; border-top: 1px solid #e0e0e0; background: #f8f9fa;
|
|
627
|
+
}
|
|
628
|
+
#query-controls label { font-size: 12px; font-weight: 600; color: #5f6368; }
|
|
629
|
+
.radio-group {
|
|
630
|
+
display: flex; gap: 12px; margin: 4px 0 8px 0;
|
|
631
|
+
}
|
|
632
|
+
.radio-group label { font-weight: 400; font-size: 12px; cursor: pointer; }
|
|
633
|
+
.radio-group input { margin-right: 3px; }
|
|
634
|
+
#depth-row { display: flex; align-items: center; gap: 8px; }
|
|
635
|
+
#depth-slider { flex: 1; }
|
|
636
|
+
#depth-value { font-size: 12px; color: #333; min-width: 16px; }
|
|
637
|
+
|
|
638
|
+
/* -- Main area -- */
|
|
639
|
+
#main {
|
|
640
|
+
flex: 1; display: flex; flex-direction: column; overflow: hidden;
|
|
641
|
+
}
|
|
642
|
+
#main-header {
|
|
643
|
+
padding: 12px 20px; border-bottom: 1px solid #e0e0e0;
|
|
644
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
645
|
+
min-height: 48px; background: #fff;
|
|
646
|
+
}
|
|
647
|
+
#main-header h2 {
|
|
648
|
+
font-size: 14px; font-weight: 600; color: #333; margin: 0;
|
|
649
|
+
}
|
|
650
|
+
#main-stats {
|
|
651
|
+
font-size: 12px; color: #888;
|
|
652
|
+
}
|
|
653
|
+
#export-buttons {
|
|
654
|
+
display: flex; gap: 6px;
|
|
655
|
+
}
|
|
656
|
+
#export-buttons button {
|
|
657
|
+
padding: 4px 10px; font-size: 11px; border: 1px solid #dadce0;
|
|
658
|
+
border-radius: 4px; background: #fff; color: #333; cursor: pointer;
|
|
659
|
+
}
|
|
660
|
+
#export-buttons button:hover { background: #f1f3f4; }
|
|
661
|
+
|
|
662
|
+
/* Diagram area */
|
|
663
|
+
#diagram-area {
|
|
664
|
+
flex: 1; overflow: auto; padding: 20px; display: flex;
|
|
665
|
+
align-items: flex-start; justify-content: center;
|
|
666
|
+
}
|
|
667
|
+
#diagram-area .mermaid-container {
|
|
668
|
+
max-width: 100%; overflow: auto;
|
|
669
|
+
}
|
|
670
|
+
#diagram-area .mermaid-container svg {
|
|
671
|
+
max-width: none;
|
|
672
|
+
}
|
|
673
|
+
#placeholder {
|
|
674
|
+
color: #999; font-size: 14px; text-align: center; margin-top: 100px;
|
|
675
|
+
}
|
|
676
|
+
#placeholder p { margin: 8px 0; }
|
|
677
|
+
|
|
678
|
+
/* Loading indicator */
|
|
679
|
+
#loading {
|
|
680
|
+
display: none; color: #1a73e8; font-size: 13px; text-align: center;
|
|
681
|
+
margin-top: 80px;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/* Responsive: collapse sidebar on narrow screens */
|
|
685
|
+
@media (max-width: 700px) {
|
|
686
|
+
#sidebar { width: 220px; min-width: 220px; }
|
|
687
|
+
}
|
|
688
|
+
@media (max-width: 500px) {
|
|
689
|
+
body { flex-direction: column; }
|
|
690
|
+
#sidebar { width: 100%; min-width: 100%; height: 40vh; }
|
|
691
|
+
#main { height: 60vh; }
|
|
692
|
+
}
|
|
693
|
+
</style>
|
|
694
|
+
</head>
|
|
695
|
+
<body>
|
|
696
|
+
|
|
697
|
+
<!-- Sidebar -->
|
|
698
|
+
<div id="sidebar">
|
|
699
|
+
<div id="sidebar-header">
|
|
700
|
+
<h1>Keboola Lineage Browser</h1>
|
|
701
|
+
<select id="project-select"><option value="">Loading...</option></select>
|
|
702
|
+
</div>
|
|
703
|
+
<div id="tabs">
|
|
704
|
+
<button class="tab active" data-type="tables">Tables</button>
|
|
705
|
+
<button class="tab" data-type="configs">Configs</button>
|
|
706
|
+
</div>
|
|
707
|
+
<input id="node-search" type="text" placeholder="Filter nodes..." autocomplete="off">
|
|
708
|
+
<div id="node-list"></div>
|
|
709
|
+
<div id="query-controls">
|
|
710
|
+
<label>Direction</label>
|
|
711
|
+
<div class="radio-group">
|
|
712
|
+
<label><input type="radio" name="direction" value="upstream" checked> Upstream</label>
|
|
713
|
+
<label><input type="radio" name="direction" value="downstream"> Downstream</label>
|
|
714
|
+
<label><input type="radio" name="direction" value="both"> Both</label>
|
|
715
|
+
</div>
|
|
716
|
+
<label>Depth</label>
|
|
717
|
+
<div id="depth-row">
|
|
718
|
+
<input type="range" id="depth-slider" min="1" max="10" value="3">
|
|
719
|
+
<span id="depth-value">3</span>
|
|
720
|
+
</div>
|
|
721
|
+
</div>
|
|
722
|
+
</div>
|
|
723
|
+
|
|
724
|
+
<!-- Main -->
|
|
725
|
+
<div id="main">
|
|
726
|
+
<div id="main-header">
|
|
727
|
+
<h2 id="diagram-title">Select a node to explore lineage</h2>
|
|
728
|
+
<span id="main-stats"></span>
|
|
729
|
+
<div id="export-buttons" style="display:none">
|
|
730
|
+
<label style="font-size:12px;margin-right:8px;cursor:pointer">
|
|
731
|
+
<input type="radio" name="view-mode" value="flow" checked> Flow
|
|
732
|
+
</label>
|
|
733
|
+
<label style="font-size:12px;margin-right:12px;cursor:pointer">
|
|
734
|
+
<input type="radio" name="view-mode" value="er"> ER
|
|
735
|
+
</label>
|
|
736
|
+
<label style="font-size:12px;margin-right:12px;cursor:pointer">
|
|
737
|
+
<input type="checkbox" id="show-columns"> Columns
|
|
738
|
+
</label>
|
|
739
|
+
<button id="btn-mermaid">Download Mermaid</button>
|
|
740
|
+
<button id="btn-json">Download JSON</button>
|
|
741
|
+
<button id="btn-html">Download HTML</button>
|
|
742
|
+
</div>
|
|
743
|
+
</div>
|
|
744
|
+
<div id="legend" style="display:none;padding:4px 16px;font-size:11px;background:#f8f9fa;border-bottom:1px solid #e0e0e0;gap:16px;flex-wrap:wrap;align-items:center">
|
|
745
|
+
<span><span style="display:inline-block;width:12px;height:12px;background:#e1f5fe;border:2px solid #0288d1;border-radius:2px;vertical-align:middle"></span> Table</span>
|
|
746
|
+
<span><span style="display:inline-block;width:12px;height:12px;background:#e8f5e9;border:2px solid #388e3c;border-radius:2px;vertical-align:middle"></span> Configuration</span>
|
|
747
|
+
<span><span style="display:inline-block;width:12px;height:12px;background:#f3e5f5;border:2px solid #7b1fa2;border-radius:2px;vertical-align:middle"></span> Table from another project</span>
|
|
748
|
+
<span style="color:#888">Edges: input_mapping | output_mapping | sql_tokenizer | bucket_sharing | ai</span>
|
|
749
|
+
</div>
|
|
750
|
+
<div id="diagram-area">
|
|
751
|
+
<div id="placeholder">
|
|
752
|
+
<p>Choose a project and click a table or configuration to visualize its lineage.</p>
|
|
753
|
+
<p style="font-size:12px;color:#bbb">
|
|
754
|
+
Use the sidebar to browse nodes, then click to query upstream or downstream dependencies.
|
|
755
|
+
</p>
|
|
756
|
+
</div>
|
|
757
|
+
<div id="loading">Querying lineage...</div>
|
|
758
|
+
<div id="zoom-controls" style="display:none;position:absolute;top:60px;right:20px;z-index:10">
|
|
759
|
+
<button onclick="zoomDiagram(1.2)" title="Zoom in" style="padding:4px 10px;font-size:18px;cursor:pointer">+</button>
|
|
760
|
+
<button onclick="zoomDiagram(1/1.2)" title="Zoom out" style="padding:4px 10px;font-size:18px;cursor:pointer">−</button>
|
|
761
|
+
<button onclick="zoomDiagram(0)" title="Reset zoom" style="padding:4px 10px;font-size:12px;cursor:pointer">Reset</button>
|
|
762
|
+
</div>
|
|
763
|
+
<div id="mermaid-output" class="mermaid-container" style="overflow:auto;transform-origin:top left"></div>
|
|
764
|
+
</div>
|
|
765
|
+
</div>
|
|
766
|
+
|
|
767
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
768
|
+
<script>
|
|
769
|
+
(function() {
|
|
770
|
+
// State
|
|
771
|
+
var allData = null; // full lineage data from /data.json
|
|
772
|
+
var edgeCounts = {}; // fqn -> number of connections
|
|
773
|
+
var currentTab = "tables";
|
|
774
|
+
var selectedProject = "";
|
|
775
|
+
var selectedNode = null; // FQN of the selected node
|
|
776
|
+
var lastQueryResult = null;
|
|
777
|
+
var lastMermaidCode = null;
|
|
778
|
+
var renderCounter = 0; // unique ID for mermaid renders
|
|
779
|
+
|
|
780
|
+
// DOM refs
|
|
781
|
+
var projectSelect = document.getElementById("project-select");
|
|
782
|
+
var nodeSearch = document.getElementById("node-search");
|
|
783
|
+
var nodeList = document.getElementById("node-list");
|
|
784
|
+
var depthSlider = document.getElementById("depth-slider");
|
|
785
|
+
var depthValue = document.getElementById("depth-value");
|
|
786
|
+
var diagramTitle = document.getElementById("diagram-title");
|
|
787
|
+
var mainStats = document.getElementById("main-stats");
|
|
788
|
+
var exportBtns = document.getElementById("export-buttons");
|
|
789
|
+
var mermaidOutput = document.getElementById("mermaid-output");
|
|
790
|
+
var placeholder = document.getElementById("placeholder");
|
|
791
|
+
var loading = document.getElementById("loading");
|
|
792
|
+
|
|
793
|
+
// Initialize mermaid
|
|
794
|
+
mermaid.initialize({
|
|
795
|
+
startOnLoad: false,
|
|
796
|
+
theme: "default",
|
|
797
|
+
flowchart: { useMaxWidth: false, htmlLabels: true },
|
|
798
|
+
securityLevel: "loose"
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// -- Data loading --
|
|
802
|
+
fetch("/data.json")
|
|
803
|
+
.then(function(r) { return r.json(); })
|
|
804
|
+
.then(function(data) {
|
|
805
|
+
allData = data;
|
|
806
|
+
// Pre-compute edge counts per node
|
|
807
|
+
edgeCounts = {};
|
|
808
|
+
(data.edges || []).forEach(function(e) {
|
|
809
|
+
edgeCounts[e.source_fqn] = (edgeCounts[e.source_fqn] || 0) + 1;
|
|
810
|
+
edgeCounts[e.target_fqn] = (edgeCounts[e.target_fqn] || 0) + 1;
|
|
811
|
+
});
|
|
812
|
+
populateProjects();
|
|
813
|
+
})
|
|
814
|
+
.catch(function(err) {
|
|
815
|
+
placeholder.innerHTML = "<p style='color:#d93025'>Failed to load lineage data: " +
|
|
816
|
+
err.message + "</p>";
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
function populateProjects() {
|
|
820
|
+
var projects = {};
|
|
821
|
+
var tables = allData.tables || {};
|
|
822
|
+
var configs = allData.configurations || {};
|
|
823
|
+
for (var fqn in tables) {
|
|
824
|
+
var pa = tables[fqn].project_alias || fqn.split(":")[0] || "";
|
|
825
|
+
if (pa) projects[pa] = true;
|
|
826
|
+
}
|
|
827
|
+
for (var cfqn in configs) {
|
|
828
|
+
var cpa = configs[cfqn].project_alias || cfqn.split(":")[0] || "";
|
|
829
|
+
if (cpa) projects[cpa] = true;
|
|
830
|
+
}
|
|
831
|
+
var sorted = Object.keys(projects).sort();
|
|
832
|
+
projectSelect.innerHTML = '<option value="">-- Select project --</option>';
|
|
833
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
834
|
+
var opt = document.createElement("option");
|
|
835
|
+
opt.value = sorted[i];
|
|
836
|
+
opt.textContent = sorted[i];
|
|
837
|
+
projectSelect.appendChild(opt);
|
|
838
|
+
}
|
|
839
|
+
// Auto-select first project if only one
|
|
840
|
+
if (sorted.length === 1) {
|
|
841
|
+
projectSelect.value = sorted[0];
|
|
842
|
+
selectedProject = sorted[0];
|
|
843
|
+
renderNodeList();
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// -- Event handlers --
|
|
848
|
+
projectSelect.addEventListener("change", function() {
|
|
849
|
+
selectedProject = this.value;
|
|
850
|
+
selectedNode = null;
|
|
851
|
+
nodeSearch.value = "";
|
|
852
|
+
renderNodeList();
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
document.querySelectorAll(".tab").forEach(function(tab) {
|
|
856
|
+
tab.addEventListener("click", function() {
|
|
857
|
+
document.querySelectorAll(".tab").forEach(function(t) { t.classList.remove("active"); });
|
|
858
|
+
tab.classList.add("active");
|
|
859
|
+
currentTab = tab.getAttribute("data-type");
|
|
860
|
+
selectedNode = null;
|
|
861
|
+
renderNodeList();
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
nodeSearch.addEventListener("input", function() {
|
|
866
|
+
renderNodeList();
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
depthSlider.addEventListener("input", function() {
|
|
870
|
+
depthValue.textContent = this.value;
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
// Re-query when direction or depth changes (if a node is selected)
|
|
874
|
+
document.querySelectorAll('input[name="direction"]').forEach(function(radio) {
|
|
875
|
+
radio.addEventListener("change", function() {
|
|
876
|
+
if (selectedNode) queryNode(selectedNode);
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
depthSlider.addEventListener("change", function() {
|
|
880
|
+
if (selectedNode) queryNode(selectedNode);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
// Export buttons
|
|
884
|
+
document.getElementById("btn-mermaid").addEventListener("click", function() {
|
|
885
|
+
if (lastMermaidCode) downloadFile("lineage.mmd", lastMermaidCode, "text/plain");
|
|
886
|
+
});
|
|
887
|
+
document.getElementById("btn-json").addEventListener("click", function() {
|
|
888
|
+
if (lastQueryResult) {
|
|
889
|
+
downloadFile("lineage.json", JSON.stringify(lastQueryResult, null, 2), "application/json");
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
document.getElementById("btn-html").addEventListener("click", function() {
|
|
893
|
+
if (lastMermaidCode) {
|
|
894
|
+
var html = buildStandaloneHtml(lastMermaidCode, diagramTitle.textContent);
|
|
895
|
+
downloadFile("lineage.html", html, "text/html");
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
document.getElementById("show-columns").addEventListener("change", function() {
|
|
899
|
+
if (selectedNode) queryNode(selectedNode);
|
|
900
|
+
});
|
|
901
|
+
document.querySelectorAll('input[name="view-mode"]').forEach(function(r) {
|
|
902
|
+
r.addEventListener("change", function() { if (selectedNode) queryNode(selectedNode); });
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
// -- Render node list (grouped by bucket / component type) --
|
|
906
|
+
function renderNodeList() {
|
|
907
|
+
nodeList.innerHTML = "";
|
|
908
|
+
if (!allData || !selectedProject) return;
|
|
909
|
+
|
|
910
|
+
var query = (nodeSearch.value || "").toLowerCase().trim();
|
|
911
|
+
// groups: { groupName: [items] }
|
|
912
|
+
var groups = {};
|
|
913
|
+
|
|
914
|
+
if (currentTab === "tables") {
|
|
915
|
+
var tables = allData.tables || {};
|
|
916
|
+
for (var fqn in tables) {
|
|
917
|
+
var t = tables[fqn];
|
|
918
|
+
var pa = t.project_alias || fqn.split(":")[0] || "";
|
|
919
|
+
if (pa !== selectedProject) continue;
|
|
920
|
+
var tableId = t.table_id || fqn.split(":").slice(1).join(":") || fqn;
|
|
921
|
+
if (query && tableId.toLowerCase().indexOf(query) < 0) continue;
|
|
922
|
+
var colCount = Array.isArray(t.columns) ? t.columns.length : (t.columns || 0);
|
|
923
|
+
var ec = edgeCounts[fqn] || 0;
|
|
924
|
+
// Group by bucket: "in.c-bucket" or "out.c-bucket"
|
|
925
|
+
var bucketId = t.bucket_id || tableId.split(".").slice(0, -1).join(".") || "other";
|
|
926
|
+
var tableName = t.name || tableId.split(".").pop() || tableId;
|
|
927
|
+
if (!groups[bucketId]) groups[bucketId] = [];
|
|
928
|
+
groups[bucketId].push({
|
|
929
|
+
fqn: fqn, name: tableName, edges: ec,
|
|
930
|
+
meta: ec + " edges, " + colCount + " cols, " + (t.rows_count || 0).toLocaleString() + " rows"
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
} else {
|
|
934
|
+
var configs = allData.configurations || {};
|
|
935
|
+
for (var cfqn in configs) {
|
|
936
|
+
var c = configs[cfqn];
|
|
937
|
+
var cpa = c.project_alias || cfqn.split(":")[0] || "";
|
|
938
|
+
if (cpa !== selectedProject) continue;
|
|
939
|
+
var configName = c.config_name || c.name || cfqn;
|
|
940
|
+
if (query && configName.toLowerCase().indexOf(query) < 0 &&
|
|
941
|
+
(c.component_id || "").toLowerCase().indexOf(query) < 0) continue;
|
|
942
|
+
var cec = edgeCounts[cfqn] || 0;
|
|
943
|
+
// Group by component_id
|
|
944
|
+
var compId = c.component_id || "other";
|
|
945
|
+
if (!groups[compId]) groups[compId] = [];
|
|
946
|
+
groups[compId].push({
|
|
947
|
+
fqn: cfqn, name: configName, edges: cec,
|
|
948
|
+
meta: cec + " edges"
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Sort groups: by total edges in group descending
|
|
954
|
+
var groupNames = Object.keys(groups);
|
|
955
|
+
groupNames.sort(function(a, b) {
|
|
956
|
+
var sumA = groups[a].reduce(function(s, i) { return s + i.edges; }, 0);
|
|
957
|
+
var sumB = groups[b].reduce(function(s, i) { return s + i.edges; }, 0);
|
|
958
|
+
return sumB - sumA || a.localeCompare(b);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
for (var g = 0; g < groupNames.length; g++) {
|
|
962
|
+
var gName = groupNames[g];
|
|
963
|
+
var gItems = groups[gName];
|
|
964
|
+
gItems.sort(function(a, b) { return (b.edges || 0) - (a.edges || 0) || a.name.localeCompare(b.name); });
|
|
965
|
+
|
|
966
|
+
// Foldable group header
|
|
967
|
+
var totalEdges = gItems.reduce(function(s, i) { return s + i.edges; }, 0);
|
|
968
|
+
var header = document.createElement("div");
|
|
969
|
+
header.style.cssText = "padding:8px 8px;font-size:11px;font-weight:600;color:#1a73e8;" +
|
|
970
|
+
"background:#e8f0fe;border-bottom:1px solid #d2e3fc;cursor:pointer;user-select:none;" +
|
|
971
|
+
"display:flex;justify-content:space-between;align-items:center";
|
|
972
|
+
header.innerHTML = '<span>\u25BC ' + escapeHtml(gName) + '</span>' +
|
|
973
|
+
'<span style="color:#5f6368;font-weight:400">' + gItems.length + ', ' + totalEdges + ' edges</span>';
|
|
974
|
+
var groupContainer = document.createElement("div");
|
|
975
|
+
header.addEventListener("click", (function(container, hdr) {
|
|
976
|
+
return function() {
|
|
977
|
+
var hidden = container.style.display === "none";
|
|
978
|
+
container.style.display = hidden ? "block" : "none";
|
|
979
|
+
hdr.querySelector("span").textContent = (hidden ? "\u25BC " : "\u25B6 ") +
|
|
980
|
+
hdr.querySelector("span").textContent.substring(2);
|
|
981
|
+
};
|
|
982
|
+
})(groupContainer, header));
|
|
983
|
+
nodeList.appendChild(header);
|
|
984
|
+
|
|
985
|
+
for (var i = 0; i < gItems.length; i++) {
|
|
986
|
+
var item = gItems[i];
|
|
987
|
+
var div = document.createElement("div");
|
|
988
|
+
div.className = "node-item" + (item.fqn === selectedNode ? " selected" : "");
|
|
989
|
+
div.setAttribute("data-fqn", item.fqn);
|
|
990
|
+
div.innerHTML = '<div>' + escapeHtml(item.name) + '</div>' +
|
|
991
|
+
'<div class="node-meta">' + escapeHtml(item.meta) + '</div>';
|
|
992
|
+
div.addEventListener("click", (function(fqn) {
|
|
993
|
+
return function() { onNodeClick(fqn); };
|
|
994
|
+
})(item.fqn));
|
|
995
|
+
groupContainer.appendChild(div);
|
|
996
|
+
}
|
|
997
|
+
nodeList.appendChild(groupContainer);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function onNodeClick(fqn) {
|
|
1002
|
+
selectedNode = fqn;
|
|
1003
|
+
// Update selection highlight
|
|
1004
|
+
document.querySelectorAll(".node-item").forEach(function(el) {
|
|
1005
|
+
el.classList.toggle("selected", el.getAttribute("data-fqn") === fqn);
|
|
1006
|
+
});
|
|
1007
|
+
queryNode(fqn);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// -- Query API --
|
|
1011
|
+
function getDirection() {
|
|
1012
|
+
var radios = document.querySelectorAll('input[name="direction"]');
|
|
1013
|
+
for (var i = 0; i < radios.length; i++) {
|
|
1014
|
+
if (radios[i].checked) return radios[i].value;
|
|
1015
|
+
}
|
|
1016
|
+
return "upstream";
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function getDepth() {
|
|
1020
|
+
return parseInt(depthSlider.value, 10) || 3;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function queryNode(fqn) {
|
|
1024
|
+
var direction = getDirection();
|
|
1025
|
+
var depth = getDepth();
|
|
1026
|
+
|
|
1027
|
+
var showCols = document.getElementById("show-columns").checked;
|
|
1028
|
+
var viewMode = document.querySelector('input[name="view-mode"]:checked').value;
|
|
1029
|
+
var cp = (showCols ? "&columns=true" : "") + (viewMode === "er" ? "&view=er" : "");
|
|
1030
|
+
|
|
1031
|
+
placeholder.style.display = "none";
|
|
1032
|
+
mermaidOutput.innerHTML = "";
|
|
1033
|
+
loading.style.display = "block";
|
|
1034
|
+
exportBtns.style.display = "none";
|
|
1035
|
+
mainStats.textContent = "";
|
|
1036
|
+
|
|
1037
|
+
if (direction === "both") {
|
|
1038
|
+
// Fetch both directions and merge
|
|
1039
|
+
diagramTitle.textContent = "Both directions of " + fqn + ", depth " + depth;
|
|
1040
|
+
var enc = encodeURIComponent(fqn);
|
|
1041
|
+
Promise.all([
|
|
1042
|
+
fetch("/api/query?node=" + enc + "&direction=upstream&depth=" + depth).then(function(r){return r.json();}),
|
|
1043
|
+
fetch("/api/query?node=" + enc + "&direction=downstream&depth=" + depth).then(function(r){return r.json();}),
|
|
1044
|
+
fetch("/api/mermaid?node=" + enc + "&direction=upstream&depth=" + depth + cp).then(function(r){return r.text();}),
|
|
1045
|
+
fetch("/api/mermaid?node=" + enc + "&direction=downstream&depth=" + depth + cp).then(function(r){return r.text();})
|
|
1046
|
+
]).then(function(res) {
|
|
1047
|
+
loading.style.display = "none";
|
|
1048
|
+
var upQ = res[0], downQ = res[1], upM = res[2], downM = res[3];
|
|
1049
|
+
var allEdges = (upQ.edges || []).concat(downQ.edges || []);
|
|
1050
|
+
var merged = {node: fqn, edges: allEdges};
|
|
1051
|
+
// Merge mermaid from both directions
|
|
1052
|
+
var mermaidCode;
|
|
1053
|
+
if (viewMode === "er") {
|
|
1054
|
+
// ER: merge entity/relationship lines, deduplicate
|
|
1055
|
+
var allL = (upM + "\n" + downM).split("\n");
|
|
1056
|
+
var seenER = {}; var erL = ["erDiagram"];
|
|
1057
|
+
allL.forEach(function(l) {
|
|
1058
|
+
var t = l.trim();
|
|
1059
|
+
if (!t || t === "erDiagram") return;
|
|
1060
|
+
if (!seenER[t]) { seenER[t] = true; erL.push(l); }
|
|
1061
|
+
});
|
|
1062
|
+
mermaidCode = erL.join("\n");
|
|
1063
|
+
} else {
|
|
1064
|
+
// Flowchart: merge node/edge lines, deduplicate
|
|
1065
|
+
var upLines = upM.split("\n"); var downLines = downM.split("\n");
|
|
1066
|
+
var seen = {}; upLines.forEach(function(l){seen[l.trim()]=true;});
|
|
1067
|
+
var extra = downLines.filter(function(l){return l.trim() && !seen[l.trim()] && !l.trim().startsWith("graph ") && !l.trim().startsWith("classDef ");});
|
|
1068
|
+
var combined = upLines.slice(0,-2).concat(extra).concat(upLines.slice(-2));
|
|
1069
|
+
combined[0] = "graph LR";
|
|
1070
|
+
mermaidCode = combined.join("\n");
|
|
1071
|
+
}
|
|
1072
|
+
lastQueryResult = merged; lastMermaidCode = mermaidCode;
|
|
1073
|
+
var nodeSet = {}; if(merged.node) nodeSet[merged.node]=true;
|
|
1074
|
+
allEdges.forEach(function(e){nodeSet[e.source]=true;nodeSet[e.target]=true;});
|
|
1075
|
+
mainStats.textContent = Object.keys(nodeSet).length+" nodes, "+allEdges.length+" edges";
|
|
1076
|
+
exportBtns.style.display = "flex";
|
|
1077
|
+
if(allEdges.length===0){mermaidOutput.innerHTML='<p style="color:#888;padding:20px">No dependencies found in either direction.</p>';return;}
|
|
1078
|
+
buildIdMap(merged); renderMermaid(mermaidCode);
|
|
1079
|
+
});
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
diagramTitle.textContent = direction.charAt(0).toUpperCase() + direction.slice(1) +
|
|
1084
|
+
" of " + fqn + ", depth " + depth;
|
|
1085
|
+
|
|
1086
|
+
var queryUrl = "/api/query?node=" + encodeURIComponent(fqn) +
|
|
1087
|
+
"&direction=" + direction + "&depth=" + depth;
|
|
1088
|
+
var mermaidUrl = "/api/mermaid?node=" + encodeURIComponent(fqn) +
|
|
1089
|
+
"&direction=" + direction + "&depth=" + depth + cp;
|
|
1090
|
+
|
|
1091
|
+
Promise.all([
|
|
1092
|
+
fetch(queryUrl).then(function(r) { return r.json(); }),
|
|
1093
|
+
fetch(mermaidUrl).then(function(r) { return r.text(); })
|
|
1094
|
+
]).then(function(results) {
|
|
1095
|
+
var queryResult = results[0];
|
|
1096
|
+
var mermaidCode = results[1];
|
|
1097
|
+
loading.style.display = "none";
|
|
1098
|
+
|
|
1099
|
+
if (queryResult.error) {
|
|
1100
|
+
mermaidOutput.innerHTML = '<p style="color:#d93025;padding:20px">' +
|
|
1101
|
+
escapeHtml(queryResult.error) + '</p>';
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
lastQueryResult = queryResult;
|
|
1106
|
+
lastMermaidCode = mermaidCode;
|
|
1107
|
+
|
|
1108
|
+
var edges = queryResult.edges || [];
|
|
1109
|
+
var nodeSet = {};
|
|
1110
|
+
if (queryResult.node) nodeSet[queryResult.node] = true;
|
|
1111
|
+
for (var i = 0; i < edges.length; i++) {
|
|
1112
|
+
nodeSet[edges[i].source] = true;
|
|
1113
|
+
nodeSet[edges[i].target] = true;
|
|
1114
|
+
}
|
|
1115
|
+
var nodeCount = Object.keys(nodeSet).length;
|
|
1116
|
+
mainStats.textContent = nodeCount + " nodes, " + edges.length + " edges";
|
|
1117
|
+
exportBtns.style.display = "flex";
|
|
1118
|
+
|
|
1119
|
+
if (edges.length === 0) {
|
|
1120
|
+
var opposite = direction === "upstream" ? "downstream" : "upstream";
|
|
1121
|
+
mermaidOutput.innerHTML = '<p style="color:#888;padding:20px">No ' +
|
|
1122
|
+
direction + ' dependencies found.</p>' +
|
|
1123
|
+
'<p style="padding:0 20px"><a href="#" style="color:#1a73e8" onclick="' +
|
|
1124
|
+
"document.querySelector('input[name=direction][value=" + opposite + "]').checked=true;" +
|
|
1125
|
+
"document.querySelector('.node-item.selected').click();return false;" +
|
|
1126
|
+
'">Try ' + opposite + ' direction</a> ' +
|
|
1127
|
+
'or <a href="#" style="color:#1a73e8" onclick="' +
|
|
1128
|
+
"document.querySelector('input[name=direction][value=both]').checked=true;" +
|
|
1129
|
+
"document.querySelector('.node-item.selected').click();return false;" +
|
|
1130
|
+
'">show both directions</a></p>';
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Build ID map for click traversal, then render
|
|
1135
|
+
buildIdMap(queryResult);
|
|
1136
|
+
renderMermaid(mermaidCode);
|
|
1137
|
+
}).catch(function(err) {
|
|
1138
|
+
loading.style.display = "none";
|
|
1139
|
+
mermaidOutput.innerHTML = '<p style="color:#d93025;padding:20px">Query failed: ' +
|
|
1140
|
+
escapeHtml(err.message) + '</p>';
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Map sanitized mermaid node IDs back to FQNs for click traversal
|
|
1145
|
+
var lastIdToFqn = {};
|
|
1146
|
+
|
|
1147
|
+
function sanitizeFqn(fqn) {
|
|
1148
|
+
return fqn.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function buildIdMap(queryResult) {
|
|
1152
|
+
lastIdToFqn = {};
|
|
1153
|
+
if (!queryResult || !queryResult.edges) return;
|
|
1154
|
+
var fqns = {};
|
|
1155
|
+
if (queryResult.node) fqns[queryResult.node] = true;
|
|
1156
|
+
queryResult.edges.forEach(function(e) {
|
|
1157
|
+
fqns[e.source] = true;
|
|
1158
|
+
fqns[e.target] = true;
|
|
1159
|
+
});
|
|
1160
|
+
for (var fqn in fqns) {
|
|
1161
|
+
lastIdToFqn[sanitizeFqn(fqn)] = fqn;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function attachDiagramClickHandlers() {
|
|
1166
|
+
var nodes = mermaidOutput.querySelectorAll(".node");
|
|
1167
|
+
nodes.forEach(function(el) {
|
|
1168
|
+
el.style.cursor = "pointer";
|
|
1169
|
+
el.addEventListener("click", function() {
|
|
1170
|
+
// Extract the mermaid node ID from the element
|
|
1171
|
+
var nodeId = el.id || "";
|
|
1172
|
+
// Mermaid wraps IDs: "flowchart-{id}-{n}" or just the id
|
|
1173
|
+
for (var sid in lastIdToFqn) {
|
|
1174
|
+
if (nodeId.indexOf(sid) >= 0) {
|
|
1175
|
+
var fqn = lastIdToFqn[sid];
|
|
1176
|
+
// Navigate to this node
|
|
1177
|
+
selectedNode = fqn;
|
|
1178
|
+
queryNode(fqn);
|
|
1179
|
+
// Update sidebar selection
|
|
1180
|
+
var proj = fqn.split(":")[0] || "";
|
|
1181
|
+
if (proj !== selectedProject) {
|
|
1182
|
+
projectSelect.value = proj;
|
|
1183
|
+
selectedProject = proj;
|
|
1184
|
+
renderNodeList();
|
|
1185
|
+
}
|
|
1186
|
+
document.querySelectorAll(".node-item").forEach(function(item) {
|
|
1187
|
+
item.classList.toggle("selected", item.getAttribute("data-fqn") === fqn);
|
|
1188
|
+
});
|
|
1189
|
+
break;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function renderMermaid(code) {
|
|
1197
|
+
renderCounter++;
|
|
1198
|
+
var id = "mermaid-diagram-" + renderCounter;
|
|
1199
|
+
mermaidOutput.innerHTML = "";
|
|
1200
|
+
mermaid.render(id, code).then(function(result) {
|
|
1201
|
+
mermaidOutput.innerHTML = result.svg;
|
|
1202
|
+
// Fit SVG to fill the diagram area
|
|
1203
|
+
var svg = mermaidOutput.querySelector("svg");
|
|
1204
|
+
if (svg) {
|
|
1205
|
+
var area = document.getElementById("diagram-area");
|
|
1206
|
+
var w = area.clientWidth - 20;
|
|
1207
|
+
var h = area.clientHeight - 20;
|
|
1208
|
+
svg.setAttribute("width", w);
|
|
1209
|
+
svg.setAttribute("height", h);
|
|
1210
|
+
svg.style.display = "block";
|
|
1211
|
+
}
|
|
1212
|
+
currentZoom = 1;
|
|
1213
|
+
document.getElementById("zoom-controls").style.display = "block";
|
|
1214
|
+
document.getElementById("legend").style.display = "flex";
|
|
1215
|
+
attachDiagramClickHandlers();
|
|
1216
|
+
}).catch(function(err) {
|
|
1217
|
+
mermaidOutput.innerHTML = '<p style="color:#d93025;padding:20px">' +
|
|
1218
|
+
'Mermaid render error: ' + escapeHtml(err.message) + '</p>' +
|
|
1219
|
+
'<pre style="padding:12px;background:#f5f5f5;border-radius:4px;' +
|
|
1220
|
+
'font-size:11px;overflow:auto;max-height:300px">' +
|
|
1221
|
+
escapeHtml(code) + '</pre>';
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// -- Helpers --
|
|
1226
|
+
var currentZoom = 1;
|
|
1227
|
+
window.zoomDiagram = function(factor) {
|
|
1228
|
+
if (factor === 0) { currentZoom = 1; } else { currentZoom *= factor; }
|
|
1229
|
+
currentZoom = Math.max(0.2, Math.min(3, currentZoom));
|
|
1230
|
+
var svg = mermaidOutput.querySelector("svg");
|
|
1231
|
+
if (svg) { svg.style.transform = "scale(" + currentZoom + ")"; svg.style.transformOrigin = "top left"; }
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
function escapeHtml(text) {
|
|
1235
|
+
var div = document.createElement("div");
|
|
1236
|
+
div.appendChild(document.createTextNode(text || ""));
|
|
1237
|
+
return div.innerHTML;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function downloadFile(filename, content, mimeType) {
|
|
1241
|
+
var blob = new Blob([content], { type: mimeType });
|
|
1242
|
+
var url = URL.createObjectURL(blob);
|
|
1243
|
+
var a = document.createElement("a");
|
|
1244
|
+
a.href = url;
|
|
1245
|
+
a.download = filename;
|
|
1246
|
+
document.body.appendChild(a);
|
|
1247
|
+
a.click();
|
|
1248
|
+
document.body.removeChild(a);
|
|
1249
|
+
URL.revokeObjectURL(url);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function buildStandaloneHtml(mermaidCode, title) {
|
|
1253
|
+
return '<!DOCTYPE html>\n<html>\n<head>\n' +
|
|
1254
|
+
' <title>' + escapeHtml(title) + '</title>\n' +
|
|
1255
|
+
' <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"><' + '/script>\n' +
|
|
1256
|
+
' <style>\n' +
|
|
1257
|
+
' body { font-family: system-ui, -apple-system, sans-serif;\n' +
|
|
1258
|
+
' max-width: 100%; padding: 20px; color: #333; }\n' +
|
|
1259
|
+
' h2 { margin-bottom: 12px; }\n' +
|
|
1260
|
+
' .mermaid { text-align: center; margin-top: 16px; }\n' +
|
|
1261
|
+
' .legend { margin: 16px 0; padding: 12px 16px; background: #f5f5f5;\n' +
|
|
1262
|
+
' border-radius: 8px; font-size: 13px; display: inline-block; }\n' +
|
|
1263
|
+
' .legend-swatch { display: inline-block; width: 14px; height: 14px;\n' +
|
|
1264
|
+
' border-radius: 3px; vertical-align: middle; margin-right: 4px; }\n' +
|
|
1265
|
+
' </style>\n</head>\n<body>\n' +
|
|
1266
|
+
' <h2>' + escapeHtml(title) + '</h2>\n' +
|
|
1267
|
+
' <div class="legend">\n' +
|
|
1268
|
+
' <strong>Legend</strong><br/>\n' +
|
|
1269
|
+
' <span class="legend-swatch" style="background:#e1f5fe;border:2px solid #0288d1"></span> Table\n' +
|
|
1270
|
+
' \n' +
|
|
1271
|
+
' <span class="legend-swatch" style="background:#e8f5e9;border:2px solid #388e3c"></span> Configuration\n' +
|
|
1272
|
+
' <br/><span style="color:#888;font-size:12px">' +
|
|
1273
|
+
' Edge labels: input_mapping / output_mapping | sql_tokenizer | bucket_sharing | ai</span>\n' +
|
|
1274
|
+
' </div>\n' +
|
|
1275
|
+
' <div class="mermaid">\n' + mermaidCode + '\n </div>\n' +
|
|
1276
|
+
' <script>mermaid.initialize({startOnLoad:true, theme:"default", ' +
|
|
1277
|
+
'flowchart:{useMaxWidth:false,htmlLabels:true}, securityLevel:"loose"});<' + '/script>\n' +
|
|
1278
|
+
'</body>\n</html>';
|
|
1279
|
+
}
|
|
1280
|
+
})();
|
|
1281
|
+
</script>
|
|
1282
|
+
</body>
|
|
1283
|
+
</html>"""
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
class _LineageHandler(http.server.BaseHTTPRequestHandler):
|
|
1287
|
+
"""HTTP handler for the lineage browser - serves HTML, data JSON, and query APIs."""
|
|
1288
|
+
|
|
1289
|
+
html_content: str = ""
|
|
1290
|
+
json_content: str = ""
|
|
1291
|
+
service: DeepLineageService | None = None
|
|
1292
|
+
graph: LineageGraph | None = None
|
|
1293
|
+
|
|
1294
|
+
def do_GET(self) -> None:
|
|
1295
|
+
parsed = urlparse(self.path)
|
|
1296
|
+
path = parsed.path
|
|
1297
|
+
|
|
1298
|
+
if path == "/" or path == "/index.html":
|
|
1299
|
+
self._serve(self.html_content, "text/html")
|
|
1300
|
+
elif path == "/data.json":
|
|
1301
|
+
self._serve(self.json_content, "application/json")
|
|
1302
|
+
elif path == "/api/query":
|
|
1303
|
+
self._handle_query(parsed)
|
|
1304
|
+
elif path == "/api/mermaid":
|
|
1305
|
+
self._handle_mermaid(parsed)
|
|
1306
|
+
else:
|
|
1307
|
+
self.send_error(404)
|
|
1308
|
+
|
|
1309
|
+
def _handle_query(self, parsed) -> None:
|
|
1310
|
+
"""Handle /api/query?node=FQN&direction=upstream&depth=3."""
|
|
1311
|
+
params = parse_qs(parsed.query)
|
|
1312
|
+
node = params.get("node", [""])[0]
|
|
1313
|
+
direction = params.get("direction", ["downstream"])[0]
|
|
1314
|
+
depth = int(params.get("depth", ["3"])[0])
|
|
1315
|
+
|
|
1316
|
+
if not node:
|
|
1317
|
+
self._serve(json.dumps({"error": "Missing 'node' parameter"}), "application/json")
|
|
1318
|
+
return
|
|
1319
|
+
|
|
1320
|
+
assert self.service is not None and self.graph is not None
|
|
1321
|
+
if direction == "upstream":
|
|
1322
|
+
result = self.service.query_upstream(self.graph, node, depth=depth)
|
|
1323
|
+
else:
|
|
1324
|
+
result = self.service.query_downstream(self.graph, node, depth=depth)
|
|
1325
|
+
|
|
1326
|
+
self._serve(json.dumps(result), "application/json")
|
|
1327
|
+
|
|
1328
|
+
def _handle_mermaid(self, parsed) -> None:
|
|
1329
|
+
"""Handle /api/mermaid?node=FQN&direction=upstream&depth=3."""
|
|
1330
|
+
from ..services.deep_lineage_service import DeepLineageService
|
|
1331
|
+
|
|
1332
|
+
params = parse_qs(parsed.query)
|
|
1333
|
+
node = params.get("node", [""])[0]
|
|
1334
|
+
direction = params.get("direction", ["downstream"])[0]
|
|
1335
|
+
depth = int(params.get("depth", ["3"])[0])
|
|
1336
|
+
|
|
1337
|
+
if not node:
|
|
1338
|
+
self._serve("graph LR\n empty[No node specified]", "text/plain")
|
|
1339
|
+
return
|
|
1340
|
+
|
|
1341
|
+
assert self.service is not None and self.graph is not None
|
|
1342
|
+
if direction == "upstream":
|
|
1343
|
+
result = self.service.query_upstream(self.graph, node, depth=depth)
|
|
1344
|
+
else:
|
|
1345
|
+
result = self.service.query_downstream(self.graph, node, depth=depth)
|
|
1346
|
+
|
|
1347
|
+
if "error" in result:
|
|
1348
|
+
self._serve(
|
|
1349
|
+
"graph LR\n error[" + result["error"].replace('"', "'") + "]", "text/plain"
|
|
1350
|
+
)
|
|
1351
|
+
return
|
|
1352
|
+
|
|
1353
|
+
edges = result.get("edges", [])
|
|
1354
|
+
view = params.get("view", ["flow"])[0]
|
|
1355
|
+
show_cols = params.get("columns", [""])[0] == "true"
|
|
1356
|
+
|
|
1357
|
+
if view == "er":
|
|
1358
|
+
mermaid_code = DeepLineageService.render_er_diagram(
|
|
1359
|
+
edges,
|
|
1360
|
+
self.graph,
|
|
1361
|
+
node,
|
|
1362
|
+
show_columns=show_cols,
|
|
1363
|
+
)
|
|
1364
|
+
else:
|
|
1365
|
+
mermaid_code = DeepLineageService.render_mermaid(
|
|
1366
|
+
edges,
|
|
1367
|
+
self.graph,
|
|
1368
|
+
direction,
|
|
1369
|
+
node,
|
|
1370
|
+
show_columns=show_cols,
|
|
1371
|
+
)
|
|
1372
|
+
self._serve(mermaid_code, "text/plain")
|
|
1373
|
+
|
|
1374
|
+
def _serve(self, content: str, content_type: str) -> None:
|
|
1375
|
+
encoded = content.encode("utf-8")
|
|
1376
|
+
self.send_response(200)
|
|
1377
|
+
self.send_header("Content-Type", f"{content_type}; charset=utf-8")
|
|
1378
|
+
self.send_header("Content-Length", str(len(encoded)))
|
|
1379
|
+
self.send_header("Cache-Control", "no-cache")
|
|
1380
|
+
self.end_headers()
|
|
1381
|
+
self.wfile.write(encoded)
|
|
1382
|
+
|
|
1383
|
+
def log_message(self, format: str, *args: object) -> None:
|
|
1384
|
+
"""Silence default stderr logging."""
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
@lineage_app.command("server")
|
|
1388
|
+
def lineage_serve(
|
|
1389
|
+
ctx: typer.Context,
|
|
1390
|
+
load: Path = typer.Option(
|
|
1391
|
+
...,
|
|
1392
|
+
"--load",
|
|
1393
|
+
"-l",
|
|
1394
|
+
help="Lineage JSON cache file (from `lineage build`).",
|
|
1395
|
+
),
|
|
1396
|
+
port: int = typer.Option(
|
|
1397
|
+
8088,
|
|
1398
|
+
"--port",
|
|
1399
|
+
help="Port to serve on.",
|
|
1400
|
+
),
|
|
1401
|
+
host: str = typer.Option(
|
|
1402
|
+
"127.0.0.1",
|
|
1403
|
+
"--host",
|
|
1404
|
+
help="Host to bind to.",
|
|
1405
|
+
),
|
|
1406
|
+
) -> None:
|
|
1407
|
+
"""Start a local web server with interactive lineage browser.
|
|
1408
|
+
|
|
1409
|
+
Serves an interactive lineage browser from a cached lineage file.
|
|
1410
|
+
Browse projects, tables, and configurations in the sidebar, then
|
|
1411
|
+
click a node to query and visualize its upstream or downstream
|
|
1412
|
+
dependencies as a mermaid diagram.
|
|
1413
|
+
|
|
1414
|
+
Example:
|
|
1415
|
+
|
|
1416
|
+
kbagent lineage server -l lineage.json
|
|
1417
|
+
kbagent lineage server -l lineage.json --port 9000
|
|
1418
|
+
"""
|
|
1419
|
+
formatter = get_formatter(ctx)
|
|
1420
|
+
|
|
1421
|
+
if not load.exists():
|
|
1422
|
+
formatter.error(
|
|
1423
|
+
message=f"Cache file not found: {load}", error_code=ErrorCode.FILE_NOT_FOUND
|
|
1424
|
+
)
|
|
1425
|
+
raise typer.Exit(code=1)
|
|
1426
|
+
|
|
1427
|
+
try:
|
|
1428
|
+
raw_data = json.loads(load.read_text(encoding="utf-8"))
|
|
1429
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
1430
|
+
formatter.error(message=f"Cannot read lineage file: {exc}", error_code=ErrorCode.READ_ERROR)
|
|
1431
|
+
raise typer.Exit(code=1) from None
|
|
1432
|
+
|
|
1433
|
+
# Load the graph via the service for API queries
|
|
1434
|
+
service = get_service(ctx, "deep_lineage_service")
|
|
1435
|
+
graph = service.load_from_cache(load)
|
|
1436
|
+
|
|
1437
|
+
# Attach content + service/graph to the handler class
|
|
1438
|
+
_LineageHandler.html_content = _LINEAGE_HTML_TEMPLATE
|
|
1439
|
+
_LineageHandler.json_content = json.dumps(raw_data)
|
|
1440
|
+
_LineageHandler.service = service
|
|
1441
|
+
_LineageHandler.graph = graph
|
|
1442
|
+
|
|
1443
|
+
server = http.server.HTTPServer((host, port), _LineageHandler)
|
|
1444
|
+
url = f"http://{host}:{port}"
|
|
1445
|
+
|
|
1446
|
+
if formatter.json_mode:
|
|
1447
|
+
formatter.output({"url": url, "host": host, "port": port})
|
|
1448
|
+
else:
|
|
1449
|
+
formatter.console.print("\n[bold]Lineage browser server[/bold]")
|
|
1450
|
+
formatter.console.print(f" URL: {url}")
|
|
1451
|
+
formatter.console.print(f" Data: {load.resolve()}")
|
|
1452
|
+
formatter.console.print(" Press Ctrl+C to stop.\n")
|
|
1453
|
+
|
|
1454
|
+
# Open browser in a separate thread to avoid blocking
|
|
1455
|
+
threading.Thread(target=webbrowser.open, args=(url,), daemon=True).start()
|
|
1456
|
+
|
|
1457
|
+
try:
|
|
1458
|
+
server.serve_forever()
|
|
1459
|
+
except KeyboardInterrupt:
|
|
1460
|
+
pass
|
|
1461
|
+
finally:
|
|
1462
|
+
server.server_close()
|
|
1463
|
+
if not formatter.json_mode:
|
|
1464
|
+
formatter.console.print("\nServer stopped.")
|