diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..88b1e8a --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,57 @@ +name: Deploy React Flow map + +on: + push: + branches: + - master + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: github-pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Configure Pages + id: pages + uses: actions/configure-pages@v5 + + - name: Build site + run: npm run build + env: + BASE_PATH: ${{ steps.pages.outputs.base_path }}/ + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v4 + with: + path: dist + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3bdd52e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.DS_Store diff --git a/README.md b/README.md index bb0a81a..101cf9c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # gNMI Map -[![map](https://gitlab.com/rdodin/pics/-/wikis/uploads/6a9d18f9cb2240656aad5d224aa757df/rsz_image.png)](https://gitlab.com/rdodin/pics/-/wikis/uploads/d275425d2b66601be213c6722dadd4d6/gnmi_0.7.0_map.pdf) +[![map](https://gitlab.com/rdodin/pics/-/wikis/uploads/6a9d18f9cb2240656aad5d224aa757df/rsz_image.png)](./public/gnmi_0.10.0_map.pdf) gNMI Map provides a visual representation of the [gNMI](https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md) service. @@ -9,10 +9,53 @@ gNMI Map makes it easy to understand the composition of the gNMI service as well

+## React Flow map + +This repository now includes a React Flow recreation of the latest tagged upstream gNMI protobuf IDL. The app currently tracks `openconfig/gnmi` release `v0.14.1`, whose `gnmi.proto` advertises gNMI service compatibility `0.10.0`. It adds an interactive canvas with search, field-level links, a minimap, source/documentation links, an extension-edge toggle, and a deprecated-field toggle. Deprecated proto fields are hidden by default so the map follows the current spec-facing surface. + +```bash +npm install +npm run dev +``` + +Open the dev-server URL printed by Vite. For a production build: + +```bash +npm run build +``` + +## GitHub Pages + +The React Flow map deploys to GitHub Pages through `.github/workflows/pages.yml`. +The workflow runs on pushes to `master` and can also be started manually from the Actions tab. + +Enable Pages in the repository settings with **Source: GitHub Actions**. After deployment, the default project Pages URL is: + +```text +https://hellt.github.io/gnmi-map/ +``` + +The workflow builds the Vite app into `dist/` and uses the Pages base path when generating asset URLs, so the app and PDF link work under `/gnmi-map/`. + +To refresh the generated React Flow map from the latest `openconfig/gnmi` tag: + +```bash +npm run build:map +npm run test:map +``` + +Proto links are pinned to the latest gNMI tag. Specification links track `openconfig/reference` `master`, since that repository does not publish tags. + +To refresh the generated PDF map from the default non-deprecated view: + +```bash +npm run build:pdf +``` + ## Usage -The map can be downloaded from this repository or viewed right in a browser. The maps for the following gNMI service versions have been created so far: +The map can be downloaded from this repository or viewed right in a browser: -* **gNMI 0.7.0** - [view](https://gitlab.com/rdodin/pics/-/wikis/uploads/d275425d2b66601be213c6722dadd4d6/gnmi_0.7.0_map.pdf) / [download](https://github.com/hellt/gnmi-map/raw/master/gnmi_0.7.0_map.pdf) +* **gNMI 0.10.0** - [view](./public/gnmi_0.10.0_map.pdf) ## OS X Preview app issue -Mac OS X default PDF reader app - Preview - messes with the link fragments (`http://url.com/page#fragment`), therefore the links won't work in this app (see [1](https://discussions.apple.com/thread/251041261), [2](https://discussions.apple.com/thread/250919338)). \ No newline at end of file +Mac OS X default PDF reader app - Preview - messes with the link fragments (`http://url.com/page#fragment`), therefore the links won't work in this app (see [1](https://discussions.apple.com/thread/251041261), [2](https://discussions.apple.com/thread/250919338)). diff --git a/gnmi_0.7.0_map.pdf b/gnmi_0.7.0_map.pdf deleted file mode 100644 index cce2f5a..0000000 Binary files a/gnmi_0.7.0_map.pdf and /dev/null differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..31dcb56 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + gNMI React Flow Map + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..029e956 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2866 @@ +{ + "name": "gnmi-map", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gnmi-map", + "version": "0.1.0", + "dependencies": { + "@xyflow/react": "^12.8.4", + "elkjs": "^0.11.1", + "lucide-react": "^0.468.0", + "pdfkit": "^0.18.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "@types/pdfkit": "^0.17.6", + "@types/react": "^18.3.29", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.3.4", + "protobufjs": "^8.4.2", + "tsx": "^4.22.3", + "typescript": "^6.0.3", + "vite": "^6.0.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "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-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-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-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-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/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/pdfkit": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.6.tgz", + "integrity": "sha512-tIwzxk2uWKp0Cq9JIluQXJid77lYhF52EsIOwhsMF4iWLA6YneoBR1xVKYYdAysHuepUB0OX4tdwMiUDdGKmig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.29", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.29.tgz", + "integrity": "sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@xyflow/react": { + "version": "12.10.2", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz", + "integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.76", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.76", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz", + "integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", + "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.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/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "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", + "peer": true, + "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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.362", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.362.tgz", + "integrity": "sha512-PUY2DrLvkjkUuWqq+KPL2iWshrJsZOcIojzRQ7eXFacc9dWga7MGMJAa15VbiejSZB1PAXaRLAiKgruHP8LB1w==", + "dev": true, + "license": "ISC" + }, + "node_modules/elkjs": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz", + "integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==", + "license": "EPL-2.0" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-md5": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz", + "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/pdfkit": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.18.0.tgz", + "integrity": "sha512-NvUwSDZ0eYEzqAiWwVQkRkjYUkZ48kcsHuCO31ykqPPIVkwoSDjDGiwIgHHNtsiwls3z3P/zy4q00hl2chg2Ug==", + "license": "MIT", + "dependencies": { + "@noble/ciphers": "^1.0.0", + "@noble/hashes": "^1.6.0", + "fontkit": "^2.0.4", + "js-md5": "^0.8.3", + "linebreak": "^1.1.0", + "png-js": "^1.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/png-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.1.0.tgz", + "integrity": "sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==", + "dependencies": { + "browserify-zlib": "^0.2.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/protobufjs": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.4.2.tgz", + "integrity": "sha512-64rfNzkWOZAIazXzpBFPWq6F9up6gMvTzjE2oWIzApx2N/dqVUEE7+bCn2+40780dFVtKOUab8QfxJ6KJDWbqA==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "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 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "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/package.json b/package.json new file mode 100644 index 0000000..83fe5e1 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "gnmi-map", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build:map": "tsx scripts/generate-map-data.ts", + "build": "npm run typecheck && vite build", + "build:pdf": "tsx scripts/generate-pdf.ts", + "test:map": "tsx scripts/validate-map.ts", + "typecheck": "tsc --noEmit", + "preview": "vite preview --host 0.0.0.0" + }, + "dependencies": { + "@xyflow/react": "^12.8.4", + "elkjs": "^0.11.1", + "lucide-react": "^0.468.0", + "pdfkit": "^0.18.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "@types/pdfkit": "^0.17.6", + "@types/react": "^18.3.29", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^4.3.4", + "protobufjs": "^8.4.2", + "tsx": "^4.22.3", + "typescript": "^6.0.3", + "vite": "^6.0.7" + } +} diff --git a/public/gnmi_0.10.0_map.pdf b/public/gnmi_0.10.0_map.pdf new file mode 100644 index 0000000..7c12c20 Binary files /dev/null and b/public/gnmi_0.10.0_map.pdf differ diff --git a/scripts/generate-map-data.ts b/scripts/generate-map-data.ts new file mode 100644 index 0000000..40c3343 --- /dev/null +++ b/scripts/generate-map-data.ts @@ -0,0 +1,679 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import protobuf from 'protobufjs'; +import { + mapBounds, + mapNodes as layoutNodes, + type MapBounds, + type MapBadge, + type MapEdge, + type MapEdgeKind, + type MapField, + type MapNode, + type MapSource, +} from '../src/gnmiMap'; + +const GNMI_TAGS_API = 'https://api.github.com/repos/openconfig/gnmi/tags?per_page=30'; +const GNMI_GITHUB_BASE = 'https://github.com/openconfig/gnmi/blob'; +const GNMI_RAW_BASE = 'https://raw.githubusercontent.com/openconfig/gnmi'; +const SPECBASE = + 'https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md'; +const OUTPUT_PATH = path.resolve('src/gnmiMap.ts'); + +type Definition = protobuf.Type | protobuf.Enum; +type Definitions = Map; +type ByShortName = Map; +type DefinitionLineStackEntry = { + name: string; + depth: number; +}; +type ProtoNamespace = protobuf.ReflectionObject & { + nested?: Record; +}; +type ProtoField = protobuf.Field & { + keyType?: string; +}; +type SourceKind = 'gnmi' | 'gnmi_ext'; +type LinesBySource = Record>; +type SymbolMaps = { + nodeIdToSymbol: Map; + symbolToNodeId: Map; +}; +type GitHubTag = { + name: string; +}; +type GeneratedSourceInput = { + nodes: MapNode[]; + edges: MapEdge[]; + bounds: MapBounds; + source: MapSource; +}; + +const SCALAR_TYPES = new Set([ + 'bool', + 'bytes', + 'double', + 'fixed32', + 'fixed64', + 'float', + 'int32', + 'int64', + 'sfixed32', + 'sfixed64', + 'sint32', + 'sint64', + 'string', + 'uint32', + 'uint64', +]); + +const EXTERNAL_REFS = new Map([ + ['google.protobuf.Any', 'any'], + ['google.protobuf.Duration', 'duration'], +]); + +async function fetchJson(url: string): Promise { + const response = await fetch(url, { + headers: { Accept: 'application/vnd.github+json' }, + }); + if (!response.ok) { + throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`); + } + return response.json() as Promise; +} + +async function fetchText(url: string): Promise { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`); + } + return response.text(); +} + +function stripLineComment(line: string): string { + return line.replace(/\/\/.*$/, ''); +} + +function definitionLines(protoText: string): Map { + const lines = new Map(); + const stack: DefinitionLineStackEntry[] = []; + let packageName = ''; + let depth = 0; + + protoText.split('\n').forEach((line, index) => { + const code = stripLineComment(line); + const packageMatch = code.match(/^\s*package\s+([A-Za-z0-9_.]+)\s*;/); + if (packageMatch) { + packageName = packageMatch[1]; + } + + const definitionMatch = code.match(/^\s*(message|enum|service)\s+([A-Za-z_][A-Za-z0-9_]*)\b/); + if (definitionMatch) { + const [, , name] = definitionMatch; + const parents = stack.map((entry) => entry.name); + const fullName = [packageName, ...parents, name].filter(Boolean).join('.'); + lines.set(fullName, index + 1); + } + + const openCount = (code.match(/{/g) ?? []).length; + const closeCount = (code.match(/}/g) ?? []).length; + + if (definitionMatch && openCount > 0) { + stack.push({ name: definitionMatch[2], depth: depth + openCount }); + } + + depth += openCount - closeCount; + while (stack.length && depth < stack[stack.length - 1].depth) { + stack.pop(); + } + }); + + return lines; +} + +function methodLines(protoText: string): Map { + const lines = new Map(); + protoText.split('\n').forEach((line, index) => { + const match = stripLineComment(line).match(/^\s*rpc\s+([A-Za-z_][A-Za-z0-9_]*)\b/); + if (match) { + lines.set(match[1], index + 1); + } + }); + return lines; +} + +function kebab(value: string): string { + return value + .replace(/([a-z0-9])([A-Z])/g, '$1-$2') + .replace(/_/g, '-') + .replace(/\./g, '-') + .toLowerCase(); +} + +function titleFromNode(node: MapNode): string { + return node.data.label.replace(/^enum\s+/, ''); +} + +function collectDefinitions(root: protobuf.Root): Definitions { + const definitions: Definitions = new Map(); + + function visit(namespace: ProtoNamespace): void { + if (!namespace.nested) { + return; + } + + Object.values(namespace.nested).forEach((item) => { + if (item instanceof protobuf.Type || item instanceof protobuf.Enum) { + definitions.set(item.fullName.replace(/^\./, ''), item); + } + visit(item as ProtoNamespace); + }); + } + + visit(root); + return definitions; +} + +function groupByShortName(definitions: Definitions): ByShortName { + const byShort: ByShortName = new Map(); + definitions.forEach((definition, fullName) => { + const shortName = fullName.split('.').at(-1) ?? fullName; + byShort.set(shortName, [...(byShort.get(shortName) ?? []), fullName]); + }); + return byShort; +} + +function specUrlFromLayout(node: MapNode): string | undefined { + if (!node.data.specUrl) { + return undefined; + } + const hash = node.data.specUrl.split('#')[1]; + return hash ? `${SPECBASE}#${hash}` : SPECBASE; +} + +function resolveSymbol( + node: MapNode, + definitions: Definitions, + byShortName: ByShortName, +): string | null { + if (node.data.sourceSymbol) { + return node.data.sourceSymbol; + } + + const title = titleFromNode(node); + if (definitions.has(title)) { + return title; + } + + if (title.startsWith('gnmi_ext.')) { + return title; + } + + const matches = byShortName.get(title) ?? []; + if (matches.length === 1) { + return matches[0]; + } + + const gnmiMatch = matches.find((name) => name.startsWith('gnmi.')); + if (gnmiMatch) { + return gnmiMatch; + } + + const extMatch = matches.find((name) => name.startsWith('gnmi_ext.')); + if (extMatch) { + return extMatch; + } + + return null; +} + +function displayType(field: ProtoField): string { + const base = field.map ? `map<${field.keyType ?? 'string'},${field.type}>` : field.type; + return field.repeated ? `repeated ${base}` : base; +} + +function resolveFieldRef( + field: ProtoField, + currentSymbol: string, + symbolToNodeId: Map, + byShortName: ByShortName, +): string | null { + if (field.map || SCALAR_TYPES.has(field.type)) { + return null; + } + + if (EXTERNAL_REFS.has(field.type)) { + return EXTERNAL_REFS.get(field.type) ?? null; + } + + const currentParts = currentSymbol.split('.'); + const packageName = currentParts[0]; + const candidates: string[] = []; + + if (field.type.includes('.')) { + candidates.push(field.type); + } else { + candidates.push(`${currentSymbol}.${field.type}`); + candidates.push(`${packageName}.${field.type}`); + candidates.push(...(byShortName.get(field.type) ?? [])); + } + + const target = candidates.find((candidate) => symbolToNodeId.has(candidate)); + return target ? (symbolToNodeId.get(target) ?? null) : null; +} + +function reservedFields(type: protobuf.Type): MapField[] { + const numbers: string[] = []; + const names: string[] = []; + + for (const item of type.reserved ?? []) { + if (Array.isArray(item)) { + const [start, end] = item; + numbers.push(start === end ? `${start}` : `${start}-${end}`); + } else { + names.push(item); + } + } + + const count = Math.max(numbers.length, names.length); + return Array.from({ length: count }, (_, index) => { + const name = names[index]; + const number = numbers[index]; + const label = [name, number].filter(Boolean).join(' / '); + return { + id: `reserved-${kebab(name ?? number ?? `${index + 1}`)}`, + type: 'reserved', + name: label, + ref: null, + badge: 'reserved', + }; + }); +} + +function fieldData( + field: ProtoField, + currentSymbol: string, + symbolToNodeId: Map, + byShortName: ByShortName, +): MapField { + const deprecated = Boolean(field.options?.deprecated); + const data: MapField = { + id: kebab(field.name), + type: displayType(field), + name: field.name, + ref: resolveFieldRef(field, currentSymbol, symbolToNodeId, byShortName), + }; + + if (field.partOf) { + data.group = `oneof ${field.partOf.name}`; + } + if (deprecated) { + data.badge = 'deprecated'; + data.deprecated = true; + } else if (field.name === 'extension' && field.type === 'gnmi_ext.Extension') { + data.badge = 'optional'; + } + + return data; +} + +function enumFields(enumDefinition: protobuf.Enum): MapField[] { + return Object.entries(enumDefinition.values).map(([name, value]) => ({ + id: kebab(name.replace(/^EID_/, '')), + type: `${value}`, + name, + ref: null, + })); +} + +function protoUrl(source: SourceKind, line: number | undefined, gnmiTag: string): string | undefined { + if (!line) { + return undefined; + } + + const file = + source === 'gnmi_ext' ? 'proto/gnmi_ext/gnmi_ext.proto' : 'proto/gnmi/gnmi.proto'; + return `${GNMI_GITHUB_BASE}/${gnmiTag}/${file}#L${line}`; +} + +function buildSymbolMaps( + nodes: MapNode[], + definitions: Definitions, + byShortName: ByShortName, +): SymbolMaps { + const symbolToNodeId = new Map(); + const nodeIdToSymbol = new Map(); + + nodes.forEach((node) => { + const symbol = resolveSymbol(node, definitions, byShortName); + if (!symbol || !definitions.has(symbol)) { + return; + } + symbolToNodeId.set(symbol, node.id); + nodeIdToSymbol.set(node.id, symbol); + }); + + return { nodeIdToSymbol, symbolToNodeId }; +} + +function serviceNode( + node: MapNode, + service: protobuf.Service, + serviceLine: number | undefined, + serviceVersion: string, + gnmiTag: string, +): MapNode { + const methods = service.methodsArray; + return { + ...node, + style: { ...node.style }, + data: { + id: node.id, + kind: node.data.kind, + label: `service gNMI ${serviceVersion}`, + protoUrl: protoUrl('gnmi', serviceLine, gnmiTag), + specUrl: specUrlFromLayout(node), + fields: methods.map((method) => ({ + id: kebab(method.name), + type: 'rpc', + name: method.name, + ref: `rpc-${kebab(method.name)}`, + ...(method.requestStream || method.responseStream ? { badge: 'stream' as const } : {}), + })), + }, + }; +} + +function rpcNode( + node: MapNode, + service: protobuf.Service, + rpcLines: Map, + gnmiTag: string, +): MapNode { + const methodName = node.id.replace( + /^rpc-/, + '', + ).replace(/(^|-)([a-z])/g, (_match: string, _separator: string, letter: string) => + letter.toUpperCase(), + ); + const method = service.methods[methodName]; + if (!method) { + return node; + } + + return { + ...node, + style: { ...node.style }, + data: { + id: node.id, + kind: node.data.kind, + label: `rpc ${method.name}`, + protoUrl: protoUrl('gnmi', rpcLines.get(method.name), gnmiTag), + specUrl: specUrlFromLayout(node), + fields: [ + { + id: 'takes', + type: method.requestStream ? 'takes stream' : 'takes', + name: method.requestType, + ref: kebab(method.requestType), + }, + { + id: 'returns', + type: method.responseStream ? 'returns stream' : 'returns', + name: method.responseType, + ref: kebab(method.responseType), + }, + ], + }, + }; +} + +function schemaNode( + node: MapNode, + definition: Definition, + symbol: string, + symbolToNodeId: Map, + byShortName: ByShortName, + linesBySource: LinesBySource, + gnmiTag: string, +): MapNode { + const source: SourceKind = symbol.startsWith('gnmi_ext.') ? 'gnmi_ext' : 'gnmi'; + const fields = + definition instanceof protobuf.Type + ? [ + ...definition.fieldsArray.map((field) => + fieldData(field, symbol, symbolToNodeId, byShortName), + ), + ...reservedFields(definition), + ] + : enumFields(definition); + const deprecated = Boolean(definition.options?.deprecated); + const badges = deprecated + ? [...new Set([...(node.data.badges ?? []), 'deprecated'])] + : node.data.badges?.filter((badge) => badge !== 'deprecated'); + + return { + ...node, + style: { ...node.style }, + data: { + id: node.id, + kind: node.data.kind, + label: node.data.label, + sourceSymbol: symbol, + deprecated, + protoUrl: protoUrl(source, linesBySource[source].get(symbol), gnmiTag), + specUrl: specUrlFromLayout(node), + ...(badges?.length ? { badges } : {}), + fields, + }, + }; +} + +function edgeKind(sourceNode: MapNode, field: MapField, targetNode: MapNode): MapEdgeKind { + if (field.type === 'rpc') { + return 'rpc'; + } + if (field.type.includes('gnmi_ext.Extension')) { + return 'extension'; + } + if (sourceNode.data.sourceSymbol?.startsWith('gnmi_ext.')) { + return 'extension-detail'; + } + if (targetNode.data.sourceSymbol?.startsWith('gnmi_ext.')) { + return 'extension-detail'; + } + return 'field'; +} + +function buildEdges(nodes: MapNode[]): MapEdge[] { + const nodesById = new Map(nodes.map((node) => [node.id, node])); + const edges: MapEdge[] = []; + + for (const node of nodes) { + for (const field of node.data.fields ?? []) { + if (!field.ref || !nodesById.has(field.ref)) { + continue; + } + const targetNode = nodesById.get(field.ref); + if (!targetNode) { + continue; + } + const kind = edgeKind(node, field, targetNode); + edges.push({ + id: `${node.id}:${field.id}->${field.ref}`, + source: node.id, + sourceHandle: field.id, + target: field.ref, + kind, + deprecated: Boolean(field.deprecated || targetNode.data.deprecated), + }); + } + } + + return edges; +} + +const GENERATED_TYPE_DEFINITIONS = `import type { Edge, Node } from '@xyflow/react'; + +export type MapNodeKind = 'service' | 'rpc' | 'message' | 'enum' | 'external' | 'legend'; +export type MapEdgeKind = 'rpc' | 'field' | 'extension' | 'extension-detail'; +export type MapBadge = 'stream' | 'optional' | 'deprecated' | 'reserved'; + +export type MapSource = { + gnmiTag: string; + gnmiServiceVersion: string; + gnmiBase: string; + extBase: string; + specBase: string; +}; + +export type MapBounds = { + width: number; + height: number; +}; + +export type MapField = { + id: string; + type: string; + name: string; + ref?: string | null; + group?: string; + badge?: MapBadge; + deprecated?: boolean; +}; + +export type MapNodeData = Record & { + id: string; + kind: MapNodeKind; + label: string; + sourceSymbol?: string; + deprecated?: boolean; + protoUrl?: string; + specUrl?: string; + fields?: MapField[]; + badges?: MapBadge[]; + active?: boolean; + query?: string; + showExtensions?: boolean; +}; + +export type MapNode = Node; + +export type MapEdge = Edge, 'smoothstep'> & { + sourceHandle: string; + kind: MapEdgeKind; + deprecated: boolean; +}; + +export type VisibleMapOptions = { + showDeprecated?: boolean; + showExtensions?: boolean; +}; + +export type VisibleMap = { + nodes: MapNode[]; + edges: MapEdge[]; +};`; + +function generatedSource({ nodes, edges, bounds, source }: GeneratedSourceInput): string { + return `// Generated by scripts/generate-map-data.ts. Do not edit by hand.\n\n${GENERATED_TYPE_DEFINITIONS}\n\nexport const mapSource: MapSource = ${JSON.stringify( + source, + null, + 2, + )};\n\nexport const mapNodes: MapNode[] = ${JSON.stringify(nodes, null, 2)};\n\nexport const mapEdges: MapEdge[] = ${JSON.stringify( + edges, + null, + 2, + )};\n\nexport const mapBounds: MapBounds = ${JSON.stringify(bounds, null, 2)};\n\nexport function getVisibleMap({\n showDeprecated = false,\n showExtensions = true,\n}: VisibleMapOptions = {}): VisibleMap {\n const visibleNodes = mapNodes\n .filter((node) => node.data.kind !== 'legend')\n .filter((node) => showDeprecated || !node.data.deprecated)\n .map((node) => ({\n ...node,\n data: {\n ...node.data,\n fields: (node.data.fields ?? []).filter((field) => showDeprecated || !field.deprecated),\n },\n }));\n const visibleNodeIds = new Set(visibleNodes.map((node) => node.id));\n const visibleHandles = new Set(\n visibleNodes.flatMap((node) =>\n (node.data.fields ?? []).map((field) => \`\${node.id}:\${field.id}\`),\n ),\n );\n const visibleEdges = mapEdges.filter((edge) => {\n if (!showExtensions && edge.kind === 'extension') {\n return false;\n }\n if (!showDeprecated && edge.deprecated) {\n return false;\n }\n return (\n visibleNodeIds.has(edge.source) &&\n visibleNodeIds.has(edge.target) &&\n visibleHandles.has(\`\${edge.source}:\${edge.sourceHandle}\`)\n );\n });\n\n return { nodes: visibleNodes, edges: visibleEdges };\n}\n`; +} + +async function main() { + const tags = await fetchJson(GNMI_TAGS_API); + const latestTag = tags.find((tag) => /^v\d+\.\d+\.\d+$/.test(tag.name)); + if (!latestTag) { + throw new Error('Could not resolve latest openconfig/gnmi tag'); + } + + const gnmiTag = latestTag.name; + const gnmiRawUrl = `${GNMI_RAW_BASE}/${gnmiTag}/proto/gnmi/gnmi.proto`; + const extRawUrl = `${GNMI_RAW_BASE}/${gnmiTag}/proto/gnmi_ext/gnmi_ext.proto`; + const [gnmiProto, extProto] = await Promise.all([fetchText(gnmiRawUrl), fetchText(extRawUrl)]); + + const root = new protobuf.Root(); + protobuf.parse(extProto, root, { keepCase: true }); + protobuf.parse(gnmiProto, root, { keepCase: true }); + + const definitions = collectDefinitions(root); + const byShortName = groupByShortName(definitions); + const { nodeIdToSymbol, symbolToNodeId } = buildSymbolMaps( + layoutNodes, + definitions, + byShortName, + ); + const service = root.lookupService('gnmi.gNMI'); + const gnmiNamespace = root.lookup('gnmi'); + if (!gnmiNamespace) { + throw new Error('Could not resolve gNMI namespace'); + } + const serviceVersion = gnmiNamespace.options?.['(gnmi_service)']; + if (typeof serviceVersion !== 'string') { + throw new Error('Could not resolve gNMI service version'); + } + const linesBySource: LinesBySource = { + gnmi: definitionLines(gnmiProto), + gnmi_ext: definitionLines(extProto), + }; + const rpcLines = methodLines(gnmiProto); + + const nodes: MapNode[] = layoutNodes.map((node) => { + if (node.id === 'service-gnmi') { + return serviceNode(node, service, linesBySource.gnmi.get('gnmi.gNMI'), serviceVersion, gnmiTag); + } + if (node.data.kind === 'rpc') { + return rpcNode(node, service, rpcLines, gnmiTag); + } + + const symbol = nodeIdToSymbol.get(node.id); + if (symbol) { + const definition = definitions.get(symbol); + if (!definition) { + return node; + } + return schemaNode( + node, + definition, + symbol, + symbolToNodeId, + byShortName, + linesBySource, + gnmiTag, + ); + } + + return { + ...node, + style: { ...node.style }, + data: { + ...node.data, + id: node.id, + specUrl: specUrlFromLayout(node), + }, + }; + }); + const edges = buildEdges(nodes); + const source: MapSource = { + gnmiTag, + gnmiServiceVersion: serviceVersion, + gnmiBase: `${GNMI_GITHUB_BASE}/${gnmiTag}/proto/gnmi/gnmi.proto`, + extBase: `${GNMI_GITHUB_BASE}/${gnmiTag}/proto/gnmi_ext/gnmi_ext.proto`, + specBase: SPECBASE, + }; + + await fs.writeFile(OUTPUT_PATH, generatedSource({ nodes, edges, bounds: mapBounds, source })); + console.log(`Wrote ${OUTPUT_PATH} from openconfig/gnmi ${gnmiTag}`); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/generate-pdf.ts b/scripts/generate-pdf.ts new file mode 100644 index 0000000..c850cc9 --- /dev/null +++ b/scripts/generate-pdf.ts @@ -0,0 +1,598 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import PDFDocument from 'pdfkit'; +import { + getVisibleMap, + mapSource, + type MapEdgeKind, + type MapField, + type MapNode, + type MapNodeKind, +} from '../src/gnmiMap'; +import { + computeReadableNodeLayout, + estimatedMapNodeHeight, + mapFieldRowHeight, + mapNodeWidth, + nodeBadgeHeight, + nodeBodyPadding, + nodeHeaderHeight, + routeReadableLayout, + type RoutedLayoutEdge, + type TargetHandleLayout, +} from '../src/mapLayout'; + +const outputPath = path.resolve('public/gnmi_0.10.0_map.pdf'); +const pageMargin = 56; +const titleBandHeight = 84; +const footerBandHeight = 44; +const gridGap = 34; +const edgeBendRadius = 18; +const handleOffset = 5; +const handleRadius = 4; + +const colors = { + background: '#f4f6f8', + panel: '#ffffff', + panelSoft: '#f8fafc', + border: '#b8c5d3', + text: '#172033', + muted: '#677486', + blue: '#0b4b8f', + blueSoft: '#d9eaf9', + rpc: '#1c62a0', + teal: '#16736b', + amber: '#a15c03', + amberSoft: '#fbebd3', + gray: '#4d5a6b', + focus: '#2474c9', + reserved: '#e7ebf0', + deprecated: '#fee2df', + deprecatedText: '#9b1c15', + handle: '#2d6f97', + grid: '#c4ced9', +}; + +type Point = { + x: number; + y: number; +}; + +type PdfContext = { + offset: Point; + connectedHandles: Set; +}; + +type EdgeStyle = { + color: string; + width: number; + dash?: [number, number]; +}; + +const headerColors: Record = { + service: colors.blue, + rpc: colors.rpc, + enum: colors.amber, + external: colors.gray, + legend: colors.gray, + message: colors.teal, +}; + +const edgeStyles: Record = { + rpc: { color: '#0b4b8f', width: 2.2 }, + field: { color: '#5b708a', width: 1.6 }, + extension: { color: '#8a6a1f', width: 1.4, dash: [7, 6] }, + 'extension-detail': { color: '#b47a18', width: 1.5 }, +}; + +function nodeHeight(node: MapNode): number { + return estimatedMapNodeHeight(node); +} + +function toPdfPoint(point: Point, context: PdfContext): Point { + return { + x: point.x + context.offset.x, + y: point.y + context.offset.y, + }; +} + +function trimTextToWidth(doc: PDFKit.PDFDocument, value: string, maxWidth: number): string { + if (doc.widthOfString(value) <= maxWidth) { + return value; + } + + const suffix = '...'; + let trimmed = value; + while (trimmed.length > 0 && doc.widthOfString(`${trimmed}${suffix}`) > maxWidth) { + trimmed = trimmed.slice(0, -1); + } + + return trimmed ? `${trimmed}${suffix}` : suffix; +} + +function fieldHandleKey(node: MapNode, field: MapField): string { + return `${node.id}:${field.id}`; +} + +function handleFill(node: MapNode): string { + return node.data.kind === 'enum' ? colors.amber : colors.handle; +} + +function drawArrow( + doc: PDFKit.PDFDocument, + x: number, + y: number, + angle: number, + color: string, +): void { + const size = 8; + doc + .save() + .fillColor(color) + .moveTo(x, y) + .lineTo( + x - size * Math.cos(angle - Math.PI / 6), + y - size * Math.sin(angle - Math.PI / 6), + ) + .lineTo( + x - size * Math.cos(angle + Math.PI / 6), + y - size * Math.sin(angle + Math.PI / 6), + ) + .closePath() + .fill() + .restore(); +} + +function distance(first: Point, second: Point): number { + return Math.hypot(second.x - first.x, second.y - first.y); +} + +function drawRoundedPath( + doc: PDFKit.PDFDocument, + points: Point[], + context: PdfContext, +): void { + const pdfPoints = points.map((point) => toPdfPoint(point, context)); + const [start] = pdfPoints; + doc.moveTo(start.x, start.y); + + for (let index = 1; index < pdfPoints.length; index += 1) { + const previous = pdfPoints[index - 1]; + const current = pdfPoints[index]; + const next = pdfPoints[index + 1]; + + if (!next) { + doc.lineTo(current.x, current.y); + continue; + } + + const incomingDistance = distance(previous, current); + const outgoingDistance = distance(current, next); + if (incomingDistance === 0 || outgoingDistance === 0) { + doc.lineTo(current.x, current.y); + continue; + } + + const radius = Math.min(edgeBendRadius, incomingDistance / 2, outgoingDistance / 2); + const incomingUnit = { + x: (current.x - previous.x) / incomingDistance, + y: (current.y - previous.y) / incomingDistance, + }; + const outgoingUnit = { + x: (next.x - current.x) / outgoingDistance, + y: (next.y - current.y) / outgoingDistance, + }; + const beforeCorner = { + x: current.x - incomingUnit.x * radius, + y: current.y - incomingUnit.y * radius, + }; + const afterCorner = { + x: current.x + outgoingUnit.x * radius, + y: current.y + outgoingUnit.y * radius, + }; + + doc + .lineTo(beforeCorner.x, beforeCorner.y) + .quadraticCurveTo(current.x, current.y, afterCorner.x, afterCorner.y); + } +} + +function drawEdge( + doc: PDFKit.PDFDocument, + routedEdge: RoutedLayoutEdge, + context: PdfContext, +): void { + if (routedEdge.routePoints.length < 2) { + return; + } + + const edge = routedEdge.edge; + const style = edgeStyles[edge.kind] ?? edgeStyles.field; + const points = routedEdge.routePoints; + const end = toPdfPoint(points[points.length - 1], context); + const beforeEnd = toPdfPoint(points[points.length - 2], context); + + doc + .save() + .lineCap('round') + .lineJoin('round') + .lineWidth(style.width + 3) + .strokeColor('#ffffff') + .opacity(0.72); + drawRoundedPath(doc, points, context); + doc.stroke().restore(); + + doc + .save() + .lineCap('round') + .lineJoin('round') + .lineWidth(style.width) + .strokeColor(style.color); + if (style.dash) { + doc.dash(style.dash[0], { space: style.dash[1] }); + } + drawRoundedPath(doc, points, context); + doc.stroke().undash().restore(); + + drawArrow(doc, end.x, end.y, Math.atan2(end.y - beforeEnd.y, end.x - beforeEnd.x), style.color); +} + +function drawBadge( + doc: PDFKit.PDFDocument, + text: string, + x: number, + y: number, + color: string, + fill: string, +): number { + doc.font('Helvetica-Bold').fontSize(6.8); + const width = Math.max(46, doc.widthOfString(text.toUpperCase()) + 12); + doc + .save() + .fillColor(fill) + .roundedRect(x, y, width, 18, 9) + .fill() + .fillColor(color) + .text(text.toUpperCase(), x + 6, y + 6, { lineBreak: false }) + .restore(); + + return width; +} + +function drawHandle(doc: PDFKit.PDFDocument, x: number, y: number, fill: string): void { + doc + .save() + .circle(x, y, handleRadius) + .fillColor(fill) + .fill() + .circle(x, y, handleRadius) + .lineWidth(2) + .strokeColor('#ffffff') + .stroke() + .restore(); +} + +function drawHeaderLinks(doc: PDFKit.PDFDocument, node: MapNode, x: number, y: number): void { + let linkX = x + mapNodeWidth(node) - 54; + + if (node.data.protoUrl) { + doc + .save() + .fillOpacity(0.14) + .fillColor('#ffffff') + .roundedRect(linkX, y + 7, 24, 24, 6) + .fill() + .fillOpacity(1) + .font('Helvetica-Bold') + .fontSize(8) + .fillColor('#ffffff') + .text('P', linkX + 9, y + 15, { lineBreak: false }) + .restore(); + doc.link(linkX, y + 7, 24, 24, node.data.protoUrl); + linkX += 29; + } + + if (node.data.specUrl) { + doc + .save() + .fillOpacity(0.14) + .fillColor('#ffffff') + .roundedRect(linkX, y + 7, 24, 24, 6) + .fill() + .fillOpacity(1) + .font('Helvetica-Bold') + .fontSize(8) + .fillColor('#ffffff') + .text('D', linkX + 9, y + 15, { lineBreak: false }) + .restore(); + doc.link(linkX, y + 7, 24, 24, node.data.specUrl); + } +} + +function drawFieldRow( + doc: PDFKit.PDFDocument, + node: MapNode, + field: MapField, + rowIndex: number, + x: number, + y: number, + width: number, + context: PdfContext, +): void { + const rowHeight = mapFieldRowHeight(field); + const rowRadius = 6; + const nameX = x + Math.floor(width * 0.46); + const rowRightPadding = context.connectedHandles.has(fieldHandleKey(node, field)) ? 30 : 18; + const typeMaxWidth = nameX - x - 26; + const nameMaxWidth = width - (nameX - x) - rowRightPadding; + + if (rowIndex % 2 === 0) { + doc + .save() + .fillColor(colors.panelSoft) + .roundedRect(x + 8, y, width - 16, rowHeight - 2, rowRadius) + .fill() + .restore(); + } + + doc + .font('Helvetica-Bold') + .fontSize(8.5) + .fillColor('#526071') + .text(trimTextToWidth(doc, field.type, typeMaxWidth), x + 14, y + 8, { + width: typeMaxWidth, + lineBreak: false, + }); + + doc + .font('Courier-Bold') + .fontSize(8.5) + .fillColor(colors.text) + .text(trimTextToWidth(doc, field.name, nameMaxWidth), nameX, y + 8, { + width: nameMaxWidth, + lineBreak: false, + }); + + if (field.badge === 'deprecated') { + doc + .save() + .moveTo(x + 14, y + 15) + .lineTo(nameX + Math.min(nameMaxWidth, doc.widthOfString(field.name)), y + 15) + .lineWidth(0.6) + .strokeColor(colors.deprecatedText) + .opacity(0.55) + .stroke() + .restore(); + } + + if (field.group || field.badge) { + const detailY = y + 28; + let badgeX = x + 14; + if (field.group) { + badgeX += drawBadge(doc, field.group, badgeX, detailY, '#164675', colors.blueSoft) + 6; + } + + if (field.badge) { + const fill = field.badge === 'reserved' ? colors.reserved : colors.deprecated; + const textColor = field.badge === 'reserved' ? colors.gray : colors.deprecatedText; + drawBadge(doc, field.badge, badgeX, detailY, textColor, fill); + } + } + + if (context.connectedHandles.has(fieldHandleKey(node, field))) { + drawHandle(doc, x + width + handleOffset, y + rowHeight / 2, handleFill(node)); + } +} + +function drawNode(doc: PDFKit.PDFDocument, node: MapNode, context: PdfContext): void { + const origin = toPdfPoint(node.position, context); + const x = origin.x; + const y = origin.y; + const width = mapNodeWidth(node); + const height = nodeHeight(node); + const fields = node.data.fields ?? []; + const headerColor = headerColors[node.data.kind] ?? colors.teal; + + doc + .save() + .fillOpacity(0.09) + .fillColor('#1f2937') + .roundedRect(x, y + 8, width, height, 8) + .fill() + .restore(); + + doc + .save() + .fillColor(colors.panel) + .roundedRect(x, y, width, height, 8) + .fill() + .lineWidth(1) + .strokeColor(colors.border) + .roundedRect(x, y, width, height, 8) + .stroke() + .restore(); + + doc + .save() + .fillColor(headerColor) + .roundedRect(x, y, width, nodeHeaderHeight, 8) + .fill() + .rect(x, y + nodeHeaderHeight - 8, width, 8) + .fill() + .restore(); + + const kindLabel = node.data.kind.toUpperCase(); + doc.font('Helvetica-Bold').fontSize(6.8); + const kindPillWidth = Math.max(34, doc.widthOfString(kindLabel) + 12); + doc + .save() + .fillOpacity(0.16) + .fillColor('#ffffff') + .roundedRect(x + 10, y + 10, kindPillWidth, 16, 8) + .fill() + .fillOpacity(1) + .fillColor('#ffffff') + .text(kindLabel, x + 16, y + 15, { lineBreak: false }) + .restore(); + + const titleX = x + 10 + kindPillWidth + 8; + const titleWidth = width - (titleX - x) - 70; + doc + .font('Helvetica-Bold') + .fontSize(13) + .fillColor('#ffffff') + .text(trimTextToWidth(doc, node.data.label, titleWidth), titleX, y + 11, { + width: titleWidth, + lineBreak: false, + }); + + drawHeaderLinks(doc, node, x, y); + + let rowY = y + nodeHeaderHeight + nodeBodyPadding; + + if (node.data.badges?.length) { + let badgeX = x + 10; + for (const badge of node.data.badges) { + badgeX += drawBadge(doc, badge, badgeX, rowY + 2, colors.deprecatedText, colors.deprecated) + 6; + } + rowY += nodeBadgeHeight; + } + + if (!fields.length) { + doc + .font('Helvetica-Oblique') + .fontSize(10) + .fillColor(colors.muted) + .text('empty message', x + 12, rowY + 8, { lineBreak: false }); + } else { + fields.forEach((field, index) => { + drawFieldRow(doc, node, field, index, x, rowY, width, context); + rowY += mapFieldRowHeight(field); + }); + } + + const targetHandles = targetHandlesFromData(node); + if (targetHandles.length) { + for (const handle of targetHandles) { + drawHandle(doc, x - handleOffset, y + handle.y, handleFill(node)); + } + } else { + drawHandle(doc, x - handleOffset, y + height / 2, handleFill(node)); + } +} + +function targetHandlesFromData(node: MapNode): TargetHandleLayout[] { + return Array.isArray(node.data.targetHandles) + ? (node.data.targetHandles as TargetHandleLayout[]) + : []; +} + +function drawCanvasBackground(doc: PDFKit.PDFDocument, pageWidth: number, pageHeight: number): void { + doc.rect(0, 0, pageWidth, pageHeight).fill(colors.background); + + doc + .save() + .fillOpacity(0.04) + .fillColor(colors.blue) + .rect(0, 0, pageWidth * 0.36, pageHeight) + .fill() + .fillColor(colors.teal) + .rect(0, pageHeight * 0.58, pageWidth, pageHeight * 0.42) + .fill() + .restore(); + + doc.save().fillColor(colors.grid).fillOpacity(0.62); + for (let x = gridGap; x < pageWidth; x += gridGap) { + for (let y = gridGap; y < pageHeight; y += gridGap) { + doc.circle(x, y, 0.55).fill(); + } + } + doc.restore(); +} + +function drawTitle(doc: PDFKit.PDFDocument, pageWidth: number): void { + doc + .font('Helvetica-Bold') + .fontSize(26) + .fillColor(colors.text) + .text(`gNMI service ${mapSource.gnmiServiceVersion} map`, pageMargin, 18, { + width: pageWidth - pageMargin * 2, + lineBreak: false, + }); + doc + .font('Helvetica') + .fontSize(12) + .fillColor(colors.muted) + .text(`Generated from openconfig/gnmi ${mapSource.gnmiTag} protobuf IDL`, pageMargin, 48, { + width: pageWidth - pageMargin * 2, + lineBreak: false, + }); +} + +async function main() { + const { nodes, edges: pdfEdges } = getVisibleMap({ + showDeprecated: false, + showExtensions: true, + }); + const layoutNodes = await computeReadableNodeLayout(nodes, pdfEdges); + const layout = routeReadableLayout(layoutNodes, pdfEdges); + const pdfNodes = layout.nodes; + const bounds = layout.bounds; + const pageWidth = bounds.width + pageMargin * 2; + const pageHeight = bounds.height + titleBandHeight + footerBandHeight; + const context: PdfContext = { + offset: { + x: pageMargin - bounds.x, + y: titleBandHeight - bounds.y, + }, + connectedHandles: new Set(pdfEdges.map((edge) => `${edge.source}:${edge.sourceHandle}`)), + }; + const doc = new PDFDocument({ + autoFirstPage: false, + compress: true, + info: { + Title: `gNMI service ${mapSource.gnmiServiceVersion} React Flow Map`, + Author: 'gnmi-map', + Subject: `Generated from openconfig/gnmi ${mapSource.gnmiTag} protobuf IDL`, + Keywords: 'gNMI, OpenConfig, React Flow, protobuf', + }, + }); + + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + + const stream = fs.createWriteStream(outputPath); + doc.pipe(stream); + doc.addPage({ size: [pageWidth, pageHeight], margin: 0 }); + + drawCanvasBackground(doc, pageWidth, pageHeight); + drawTitle(doc, pageWidth); + + for (const edge of layout.edges) { + drawEdge(doc, edge, context); + } + + for (const node of pdfNodes) { + drawNode(doc, node, context); + } + + doc + .font('Helvetica') + .fontSize(10) + .fillColor(colors.muted) + .text('P = proto definition, D = specification documentation', pageMargin, pageHeight - 30, { + lineBreak: false, + }); + + doc.end(); + await new Promise((resolve, reject) => { + stream.on('finish', resolve); + stream.on('error', reject); + }); + + console.log(`Wrote ${outputPath}`); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/validate-map.ts b/scripts/validate-map.ts new file mode 100644 index 0000000..2e18fad --- /dev/null +++ b/scripts/validate-map.ts @@ -0,0 +1,147 @@ +import { + getVisibleMap, + mapEdges, + mapNodes, + mapSource, + type MapEdge, + type MapNode, +} from '../src/gnmiMap'; +import { + computeReadableNodeLayout, + estimatedMapNodeHeight, + mapNodeWidth, + routeIntersectsNode, + routeReadableLayout, +} from '../src/mapLayout'; + +function assert(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new Error(message); + } +} + +function validateEdges(nodes: MapNode[], edges: MapEdge[], label: string): void { + const nodeIds = new Set(nodes.map((node) => node.id)); + const handles = new Set( + nodes.flatMap((node) => (node.data.fields ?? []).map((field) => `${node.id}:${field.id}`)), + ); + + for (const edge of edges) { + assert(nodeIds.has(edge.source), `${label}: edge ${edge.id} has missing source ${edge.source}`); + assert(nodeIds.has(edge.target), `${label}: edge ${edge.id} has missing target ${edge.target}`); + assert( + handles.has(`${edge.source}:${edge.sourceHandle}`), + `${label}: edge ${edge.id} has missing source handle ${edge.sourceHandle}`, + ); + } +} + +function validateLinks(): void { + for (const node of mapNodes) { + if (node.data.protoUrl?.startsWith('https://github.com/openconfig/gnmi/blob/')) { + const validProtoBase = + node.data.protoUrl.startsWith(mapSource.gnmiBase) || + node.data.protoUrl.startsWith(mapSource.extBase); + assert(validProtoBase, `${node.id}: protoUrl does not use configured proto bases`); + } + + if (node.data.specUrl) { + assert( + node.data.specUrl.startsWith(mapSource.specBase), + `${node.id}: specUrl does not use configured spec base`, + ); + } + } +} + +function validateDeprecatedVisibility(): void { + const rawSubscribeResponse = mapNodes.find((node) => node.id === 'subscribe-response'); + assert(rawSubscribeResponse, 'raw map is missing SubscribeResponse'); + const rawErrorField = rawSubscribeResponse.data.fields?.find((field) => field.name === 'error'); + assert(rawErrorField?.deprecated, 'raw SubscribeResponse.error must be preserved as deprecated'); + + const defaultMap = getVisibleMap(); + const defaultSubscribeResponse = defaultMap.nodes.find((node) => node.id === 'subscribe-response'); + assert(defaultSubscribeResponse, 'default map is missing SubscribeResponse'); + assert( + !(defaultSubscribeResponse.data.fields?.some((field) => field.name === 'error') ?? false), + 'default SubscribeResponse must hide deprecated error field', + ); + assert( + defaultMap.nodes.every((node) => !node.data.deprecated), + 'default map must hide deprecated nodes', + ); + assert( + defaultMap.nodes.every((node) => (node.data.fields ?? []).every((field) => !field.deprecated)), + 'default map must hide deprecated fields', + ); + + const deprecatedMap = getVisibleMap({ showDeprecated: true }); + const deprecatedSubscribeResponse = deprecatedMap.nodes.find( + (node) => node.id === 'subscribe-response', + ); + assert( + deprecatedSubscribeResponse?.data.fields?.some((field) => field.name === 'error'), + 'deprecated map must include SubscribeResponse.error', + ); +} + +async function validateReadableLayout(nodes: MapNode[], edges: MapEdge[], label: string): Promise { + const layoutNodes = await computeReadableNodeLayout(nodes, edges); + const layout = routeReadableLayout(layoutNodes, edges); + + for (let firstIndex = 0; firstIndex < layout.nodes.length; firstIndex += 1) { + const first = layout.nodes[firstIndex]; + for (let secondIndex = firstIndex + 1; secondIndex < layout.nodes.length; secondIndex += 1) { + const second = layout.nodes[secondIndex]; + assert(!nodesOverlap(first, second), `${label}: nodes ${first.id} and ${second.id} overlap`); + } + } + + for (const routedEdge of layout.edges) { + for (const node of layout.nodes) { + if (node.id === routedEdge.edge.source || node.id === routedEdge.edge.target) { + continue; + } + + assert( + !routeIntersectsNode(routedEdge.routePoints, node), + `${label}: edge ${routedEdge.edge.id} intersects node ${node.id}`, + ); + } + } +} + +function nodesOverlap(first: MapNode, second: MapNode): boolean { + return ( + first.position.x < second.position.x + mapNodeWidth(second) && + first.position.x + mapNodeWidth(first) > second.position.x && + first.position.y < second.position.y + estimatedMapNodeHeight(second) && + first.position.y + estimatedMapNodeHeight(first) > second.position.y + ); +} + +async function main(): Promise { + const appDefaultMap = getVisibleMap({ showExtensions: false }); + const extensionMap = getVisibleMap({ showExtensions: true }); + const deprecatedMap = getVisibleMap({ showDeprecated: true, showExtensions: false }); + const fullMap = getVisibleMap({ showDeprecated: true, showExtensions: true }); + + validateEdges(mapNodes, mapEdges, 'raw map'); + validateEdges(appDefaultMap.nodes, appDefaultMap.edges, 'default map'); + validateEdges(deprecatedMap.nodes, deprecatedMap.edges, 'deprecated map'); + validateLinks(); + validateDeprecatedVisibility(); + + await validateReadableLayout(appDefaultMap.nodes, appDefaultMap.edges, 'default layout'); + await validateReadableLayout(extensionMap.nodes, extensionMap.edges, 'extension layout'); + await validateReadableLayout(deprecatedMap.nodes, deprecatedMap.edges, 'deprecated layout'); + await validateReadableLayout(fullMap.nodes, fullMap.edges, 'full layout'); + + console.log('Map data is valid'); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..05f3428 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,1015 @@ +import { type CSSProperties, useCallback, useEffect, useMemo, useState } from 'react'; +import { + BaseEdge, + type NodeChange, + Background, + Controls, + type Edge, + type EdgeProps, + type EdgeTypes, + Handle, + MarkerType, + type NodeProps, + type NodeTypes, + Position, + ReactFlow, + ReactFlowProvider, + useReactFlow, +} from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; +import { + BookOpen, + ExternalLink, + EyeOff, + FileCode2, + FileDown, + Focus, + GitBranch, + RotateCcw, + Search, +} from 'lucide-react'; +import { + getVisibleMap, + type MapEdge, + type MapEdgeKind, + type MapField, + type MapNode, + mapSource, +} from './gnmiMap'; +import { + applyManualPositions, + computeReadableNodeLayout, + improveNodeLayout, + routeReadableLayout, + type RoutePoint, + type TargetHandleLayout, +} from './mapLayout'; + +const edgeStyleByKind: Record = { + rpc: { stroke: '#0b4b8f', strokeWidth: 2.2 }, + field: { stroke: '#5b708a', strokeWidth: 1.6 }, + extension: { + stroke: '#8a6a1f', + strokeWidth: 1.4, + strokeDasharray: '7 6', + }, + 'extension-detail': { stroke: '#b47a18', strokeWidth: 1.5 }, +}; + +const nodeTypes: NodeTypes = { + schema: SchemaNode, +}; + +const edgeTypes: EdgeTypes = { + routed: RoutedEdge, +}; + +const pdfMapUrl = `${import.meta.env.BASE_URL}gnmi_0.10.0_map.pdf`; + +type NodePosition = { + x: number; + y: number; +}; + +type RoutedEdgeData = Record & { + routePoints: RoutePoint[]; + routeBridges: RouteBridge[]; +}; + +type RoutedMapEdge = Edge & { + sourceHandle: string; + targetHandle: string; + kind: MapEdgeKind; + deprecated: boolean; +}; + +type FieldConnectionIds = Record; +type FieldClickHandler = (edgeId: string) => void; + +type RouteBridge = RoutePoint & { + orientation: 'horizontal' | 'vertical'; +}; + +type RouteSegment = { + edgeId: string; + index: number; + start: RoutePoint; + end: RoutePoint; + orientation: 'horizontal' | 'vertical'; + fixed: number; + from: number; + to: number; +}; + +function searchableText(node: MapNode): string { + const fieldText = node.data.fields + ?.map((field) => `${field.type} ${field.name} ${field.group ?? ''} ${field.badge ?? ''}`) + .join(' '); + + return `${node.data.kind} ${node.data.label} ${fieldText ?? ''}`.toLowerCase(); +} + +function fieldMatches(field: MapField, query: string): boolean { + if (!query) { + return false; + } + + return `${field.type} ${field.name} ${field.group ?? ''} ${field.badge ?? ''}` + .toLowerCase() + .includes(query); +} + +function AppShell() { + const { fitView } = useReactFlow(); + const [queryValue, setQueryValue] = useState(''); + const [showExtensions, setShowExtensions] = useState(false); + const [showDeprecated, setShowDeprecated] = useState(false); + const [selectedId, setSelectedId] = useState(null); + const [selectedEdgeId, setSelectedEdgeId] = useState(null); + const [manualPositions, setManualPositions] = useState>({}); + const [routingPositions, setRoutingPositions] = useState>({}); + + const query = queryValue.trim().toLowerCase(); + const visibleMap = useMemo( + () => getVisibleMap({ showDeprecated, showExtensions }), + [showDeprecated, showExtensions], + ); + const fallbackLayoutNodes = useMemo(() => improveNodeLayout(visibleMap.nodes), [visibleMap.nodes]); + const [elkLayoutNodes, setElkLayoutNodes] = useState(null); + + useEffect(() => { + let cancelled = false; + setElkLayoutNodes(null); + + computeReadableNodeLayout(visibleMap.nodes, visibleMap.edges) + .then((layoutNodes) => { + if (!cancelled) { + setElkLayoutNodes(layoutNodes); + } + }) + .catch((error) => { + console.error('Failed to compute readable map layout', error); + }); + + return () => { + cancelled = true; + }; + }, [visibleMap.edges, visibleMap.nodes]); + + const layoutNodes = elkLayoutNodes ?? fallbackLayoutNodes; + useEffect(() => { + if (!elkLayoutNodes) { + return; + } + + let secondFrame: number | null = null; + const firstFrame = window.requestAnimationFrame(() => { + secondFrame = window.requestAnimationFrame(() => fitView({ padding: 0.1, duration: 350 })); + }); + + return () => { + window.cancelAnimationFrame(firstFrame); + if (secondFrame !== null) { + window.cancelAnimationFrame(secondFrame); + } + }; + }, [elkLayoutNodes, fitView]); + + const routedLayoutNodes = useMemo( + () => applyManualPositions(layoutNodes, routingPositions), + [layoutNodes, routingPositions], + ); + const readableLayout = useMemo( + () => routeReadableLayout(routedLayoutNodes, visibleMap.edges), + [routedLayoutNodes, visibleMap.edges], + ); + const displayedLayoutNodes = useMemo( + () => applyManualPositions(readableLayout.nodes, manualPositions), + [manualPositions, readableLayout.nodes], + ); + + const nodeMatches = useMemo(() => { + if (!query) { + return new Set(); + } + + return new Set( + visibleMap.nodes + .filter((currentNode) => searchableText(currentNode).includes(query)) + .map((currentNode) => currentNode.id), + ); + }, [query, visibleMap.nodes]); + + const selectedEdge = useMemo( + () => visibleMap.edges.find((edge) => edge.id === selectedEdgeId) ?? null, + [selectedEdgeId, visibleMap.edges], + ); + const selectedEdgeEndpointIds = useMemo( + () => + selectedEdge ? new Set([selectedEdge.source, selectedEdge.target]) : new Set(), + [selectedEdge], + ); + const edgeIdBySourceHandle = useMemo( + () => + new Map( + visibleMap.edges.map((edge) => [`${edge.source}:${edge.sourceHandle}`, edge.id] as const), + ), + [visibleMap.edges], + ); + const selectFieldConnection = useCallback((edgeId: string) => { + setSelectedId(null); + setSelectedEdgeId(edgeId); + }, []); + + const nodes = useMemo( + () => + displayedLayoutNodes.map((currentNode) => { + const active = !query || nodeMatches.has(currentNode.id); + const edgeEndpoint = selectedEdgeEndpointIds.has(currentNode.id); + const fieldConnectionIds = Object.fromEntries( + (currentNode.data.fields ?? []) + .map((field) => [ + field.id, + edgeIdBySourceHandle.get(`${currentNode.id}:${field.id}`), + ]) + .filter((entry): entry is [string, string] => Boolean(entry[1])), + ); + + return { + ...currentNode, + selected: selectedId === currentNode.id, + data: { + ...currentNode.data, + active: active || edgeEndpoint, + edgeEndpoint, + activeEdgeSourceHandle: + selectedEdge?.source === currentNode.id ? selectedEdge.sourceHandle : null, + fieldConnectionIds, + onFieldConnectionClick: selectFieldConnection, + query, + showExtensions, + }, + }; + }), + [ + nodeMatches, + displayedLayoutNodes, + edgeIdBySourceHandle, + query, + selectFieldConnection, + selectedEdge, + selectedEdgeEndpointIds, + selectedId, + showExtensions, + ], + ); + + const routeBridgesByEdge = useMemo( + () => routeBridges(readableLayout.edges), + [readableLayout.edges], + ); + + const edges = useMemo( + () => + readableLayout.edges.map(({ edge, routePoints, targetHandle }) => { + const connectedToMatch = + !query || nodeMatches.has(edge.source) || nodeMatches.has(edge.target); + const style = edgeStyleByKind[edge.kind] ?? edgeStyleByKind.field; + const selected = selectedEdge?.id === edge.id; + const selectionDimmed = Boolean(selectedEdge) && !selected; + const opacity = connectedToMatch ? (selectionDimmed ? 0.24 : 1) : 0.14; + const stroke = selected ? '#d21f3c' : style.stroke; + const strokeWidth = + typeof style.strokeWidth === 'number' + ? style.strokeWidth + (selected ? 1.8 : 0) + : style.strokeWidth; + + return { + ...edge, + type: 'routed', + targetHandle, + selected, + zIndex: selected ? 12 : 1, + interactionWidth: 28, + className: [ + 'flow-edge', + `flow-edge-${edge.kind}`, + selected ? 'is-selected' : '', + selectionDimmed ? 'is-dimmed' : '', + ] + .filter(Boolean) + .join(' '), + markerEnd: { type: MarkerType.ArrowClosed, color: stroke }, + animated: selected || (query ? connectedToMatch : edge.kind === 'rpc'), + data: { routePoints, routeBridges: routeBridgesByEdge.get(edge.id) ?? [] }, + style: { + ...style, + opacity, + stroke, + strokeWidth, + }, + } satisfies RoutedMapEdge; + }), + [nodeMatches, query, readableLayout.edges, routeBridgesByEdge, selectedEdge], + ); + + const selectedNode = useMemo( + () => nodes.find((currentNode) => currentNode.id === selectedId), + [nodes, selectedId], + ); + + const fit = useCallback(() => { + fitView({ padding: 0.12, duration: 450 }); + }, [fitView]); + + const onNodesChange = useCallback((changes: NodeChange[]) => { + setManualPositions((currentPositions) => { + let nextPositions = currentPositions; + + for (const change of changes) { + if (change.type !== 'position' || !change.position) { + continue; + } + + if (nextPositions === currentPositions) { + nextPositions = { ...currentPositions }; + } + + nextPositions[change.id] = change.position; + } + + return nextPositions; + }); + + setRoutingPositions((currentPositions) => { + let nextPositions = currentPositions; + + for (const change of changes) { + if (change.type !== 'position' || !change.position || change.dragging !== false) { + continue; + } + + if (nextPositions === currentPositions) { + nextPositions = { ...currentPositions }; + } + + nextPositions[change.id] = change.position; + } + + return nextPositions; + }); + + setSelectedId((currentSelectedId) => { + let selectedNodeWasCleared = false; + + for (const change of changes) { + if (change.type !== 'select') { + continue; + } + + if (change.selected) { + return change.id; + } + + if (change.id === currentSelectedId) { + selectedNodeWasCleared = true; + } + } + + return selectedNodeWasCleared ? null : currentSelectedId; + }); + }, []); + + const resetLayout = useCallback(() => { + setManualPositions({}); + setRoutingPositions({}); + window.requestAnimationFrame(() => fitView({ padding: 0.12, duration: 450 })); + }, [fitView]); + + const hasManualPositions = Object.keys(manualPositions).length > 0; + + return ( +
+
+
+ gNMI service {mapSource.gnmiServiceVersion} +

React Flow Map

+
+ +
+ + + + + + + + + + + + +
+
+ +
+ + nodes={nodes} + edges={edges} + nodeTypes={nodeTypes} + edgeTypes={edgeTypes} + onNodesChange={onNodesChange} + onNodeDragStop={(_, node) => { + setRoutingPositions((currentPositions) => ({ + ...currentPositions, + [node.id]: node.position, + })); + }} + nodesDraggable + minZoom={0.18} + maxZoom={1.7} + defaultViewport={{ x: 70, y: 40, zoom: 0.42 }} + fitView + fitViewOptions={{ padding: 0.08 }} + onNodeClick={(_, node) => { + setSelectedId(node.id); + setSelectedEdgeId(null); + }} + onEdgeClick={(event, edge) => { + event.stopPropagation(); + setSelectedId(null); + setSelectedEdgeId(edge.id); + }} + onPaneClick={() => { + setSelectedId(null); + setSelectedEdgeId(null); + }} + proOptions={{ hideAttribution: true }} + > + + + + + +
+
+ ); +} + +function SchemaNode({ data, selected }: NodeProps) { + const fields = data.fields ?? []; + const dimmed = data.active === false; + const targetHandles = targetHandlesFromData(data); + const fieldConnectionIds = fieldConnectionIdsFromData(data); + const onFieldConnectionClick = fieldClickHandlerFromData(data); + const activeEdgeSourceHandle = + typeof data.activeEdgeSourceHandle === 'string' ? data.activeEdgeSourceHandle : null; + const className = [ + 'schema-node', + `kind-${data.kind}`, + selected ? 'is-selected' : '', + data.edgeEndpoint ? 'is-edge-endpoint' : '', + dimmed ? 'is-dimmed' : '', + ] + .filter(Boolean) + .join(' '); + + return ( +
+ {targetHandles.length ? ( + targetHandles.map((handle) => ( + + )) + ) : ( + + )} + +
+ {data.kind} + {data.label} +
+ {data.protoUrl ? ( + + + ) : null} + {data.specUrl ? ( + + + ) : null} +
+
+ + {data.badges?.length ? ( +
+ {data.badges.map((badge) => ( + {badge} + ))} +
+ ) : null} + +
+ {fields.length ? ( + fields.map((field) => ( + + )) + ) : ( +
empty message
+ )} +
+
+ ); +} + +type FieldRowProps = { + field: MapField; + highlighted: boolean; + edgeHighlighted: boolean; + connectionEdgeId?: string; + onConnectionClick?: FieldClickHandler; + showExtensions?: boolean; +}; + +function FieldRow({ + field, + highlighted, + edgeHighlighted, + connectionEdgeId, + onConnectionClick, + showExtensions, +}: FieldRowProps) { + const isExtension = field.ref === 'extension'; + const visibleExtensionHandle = !isExtension || showExtensions; + const clickable = Boolean(connectionEdgeId && onConnectionClick); + const selectConnection = () => { + if (connectionEdgeId && onConnectionClick) { + onConnectionClick(connectionEdgeId); + } + }; + + return ( +
${field.ref}` : undefined} + aria-label={clickable ? `Highlight ${field.name} connection to ${field.ref}` : undefined} + onClick={(event) => { + if (!clickable) { + return; + } + event.stopPropagation(); + selectConnection(); + }} + onKeyDown={(event) => { + if (!clickable || (event.key !== 'Enter' && event.key !== ' ')) { + return; + } + event.preventDefault(); + event.stopPropagation(); + selectConnection(); + }} + > + {field.type} + {field.name} + {field.group ? {field.group} : null} + {field.badge ? {field.badge} : null} + {field.ref && visibleExtensionHandle ? ( + ${field.ref}`} + /> + ) : null} +
+ ); +} + +function RoutedEdge({ + id, + sourceX, + sourceY, + targetX, + targetY, + data, + markerEnd, + style, + interactionWidth, +}: EdgeProps) { + const routePoints = + data?.routePoints?.length && data.routePoints.length > 1 + ? data.routePoints + : [ + { x: sourceX, y: sourceY }, + { x: targetX, y: targetY }, + ]; + const routeBridges = data?.routeBridges ?? []; + const stroke = typeof style?.stroke === 'string' ? style.stroke : '#5b708a'; + const strokeWidth = typeof style?.strokeWidth === 'number' ? style.strokeWidth : 1.6; + const path = roundedRoutePath(routePoints); + + return ( + <> + + + {routeBridges.map((bridge, index) => { + const path = bridgePath(bridge); + + return ( + + + + + ); + })} + + ); +} + +function roundedRoutePath(points: RoutePoint[], radius = 18): string { + if (!points.length) { + return ''; + } + + const [start] = points; + const commands = [`M ${start.x} ${start.y}`]; + + for (let index = 1; index < points.length; index += 1) { + const previous = points[index - 1]; + const current = points[index]; + const next = points[index + 1]; + + if (!next) { + commands.push(`L ${current.x} ${current.y}`); + continue; + } + + const incomingDistance = pointDistance(previous, current); + const outgoingDistance = pointDistance(current, next); + if (incomingDistance === 0 || outgoingDistance === 0) { + commands.push(`L ${current.x} ${current.y}`); + continue; + } + + const cornerRadius = Math.min(radius, incomingDistance / 2, outgoingDistance / 2); + const incomingUnit = { + x: (current.x - previous.x) / incomingDistance, + y: (current.y - previous.y) / incomingDistance, + }; + const outgoingUnit = { + x: (next.x - current.x) / outgoingDistance, + y: (next.y - current.y) / outgoingDistance, + }; + const beforeCorner = { + x: current.x - incomingUnit.x * cornerRadius, + y: current.y - incomingUnit.y * cornerRadius, + }; + const afterCorner = { + x: current.x + outgoingUnit.x * cornerRadius, + y: current.y + outgoingUnit.y * cornerRadius, + }; + + commands.push( + `L ${roundPathNumber(beforeCorner.x)} ${roundPathNumber(beforeCorner.y)}`, + `Q ${current.x} ${current.y} ${roundPathNumber(afterCorner.x)} ${roundPathNumber(afterCorner.y)}`, + ); + } + + return commands.join(' '); +} + +function bridgePath(bridge: RouteBridge): string { + const radius = 9; + const height = 4; + + if (bridge.orientation === 'horizontal') { + return [ + `M ${bridge.x - radius} ${bridge.y}`, + `Q ${bridge.x} ${bridge.y - height} ${bridge.x + radius} ${bridge.y}`, + ].join(' '); + } + + return [ + `M ${bridge.x} ${bridge.y - radius}`, + `Q ${bridge.x + height} ${bridge.y} ${bridge.x} ${bridge.y + radius}`, + ].join(' '); +} + +function routeBridges( + routedEdges: Array<{ edge: MapEdge; routePoints: RoutePoint[] }>, +): Map { + const bridgesByEdge = new Map(); + const segments = routedEdges.flatMap(({ edge, routePoints }) => + routeSegments(edge.id, routePoints), + ); + + for (let firstIndex = 0; firstIndex < segments.length; firstIndex += 1) { + const first = segments[firstIndex]; + + for (let secondIndex = firstIndex + 1; secondIndex < segments.length; secondIndex += 1) { + const second = segments[secondIndex]; + if (first.edgeId === second.edgeId) { + continue; + } + + if (first.orientation !== second.orientation) { + addCrossingBridge(bridgesByEdge, first, second); + } + } + } + + for (const [edgeId, bridges] of bridgesByEdge) { + bridgesByEdge.set(edgeId, dedupeBridges(bridges)); + } + + return bridgesByEdge; +} + +function routeSegments(edgeId: string, points: RoutePoint[]): RouteSegment[] { + const segments: RouteSegment[] = []; + + for (let index = 0; index < points.length - 1; index += 1) { + const start = points[index]; + const end = points[index + 1]; + if (start.x === end.x && start.y === end.y) { + continue; + } + + if (start.y === end.y) { + segments.push({ + edgeId, + index, + start, + end, + orientation: 'horizontal', + fixed: start.y, + from: Math.min(start.x, end.x), + to: Math.max(start.x, end.x), + }); + continue; + } + + if (start.x === end.x) { + segments.push({ + edgeId, + index, + start, + end, + orientation: 'vertical', + fixed: start.x, + from: Math.min(start.y, end.y), + to: Math.max(start.y, end.y), + }); + } + } + + return segments; +} + +function addCrossingBridge( + bridgesByEdge: Map, + first: RouteSegment, + second: RouteSegment, +): void { + const horizontal = first.orientation === 'horizontal' ? first : second; + const vertical = first.orientation === 'vertical' ? first : second; + const x = vertical.fixed; + const y = horizontal.fixed; + const crossingMargin = 34; + + if ( + x <= horizontal.from + crossingMargin || + x >= horizontal.to - crossingMargin || + y <= vertical.from + crossingMargin || + y >= vertical.to - crossingMargin + ) { + return; + } + + if (segmentsShareEndpoint(first, second)) { + return; + } + + const bridgeSegment = first.edgeId > second.edgeId ? first : second; + appendBridge(bridgesByEdge, bridgeSegment.edgeId, { + x, + y, + orientation: bridgeSegment.orientation, + }); +} + +function appendBridge( + bridgesByEdge: Map, + edgeId: string, + bridge: RouteBridge, +): void { + bridgesByEdge.set(edgeId, [...(bridgesByEdge.get(edgeId) ?? []), bridge]); +} + +function dedupeBridges(bridges: RouteBridge[]): RouteBridge[] { + const seen = new Set(); + + return bridges + .sort((first, second) => first.x - second.x || first.y - second.y) + .filter((bridge) => { + const key = `${bridge.orientation}:${Math.round(bridge.x / 8)}:${Math.round(bridge.y / 8)}`; + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); +} + +function segmentsShareEndpoint(first: RouteSegment, second: RouteSegment): boolean { + return ( + pointsEqual(first.start, second.start) || + pointsEqual(first.start, second.end) || + pointsEqual(first.end, second.start) || + pointsEqual(first.end, second.end) + ); +} + +function pointsEqual(first: RoutePoint, second: RoutePoint): boolean { + return Math.abs(first.x - second.x) < 0.5 && Math.abs(first.y - second.y) < 0.5; +} + +function pointDistance(first: RoutePoint, second: RoutePoint): number { + return Math.hypot(second.x - first.x, second.y - first.y); +} + +function roundPathNumber(value: number): number { + return Math.round(value * 100) / 100; +} + +function targetHandlesFromData(data: MapNode['data']): TargetHandleLayout[] { + return Array.isArray(data.targetHandles) + ? (data.targetHandles as TargetHandleLayout[]) + : []; +} + +function fieldConnectionIdsFromData(data: MapNode['data']): FieldConnectionIds { + return data.fieldConnectionIds && + typeof data.fieldConnectionIds === 'object' && + !Array.isArray(data.fieldConnectionIds) + ? (data.fieldConnectionIds as FieldConnectionIds) + : {}; +} + +function fieldClickHandlerFromData(data: MapNode['data']): FieldClickHandler | undefined { + return typeof data.onFieldConnectionClick === 'function' + ? (data.onFieldConnectionClick as FieldClickHandler) + : undefined; +} + +type InspectorProps = { + node?: MapNode; + totalNodes: number; + totalEdges: number; +}; + +function Inspector({ node, totalNodes, totalEdges }: InspectorProps) { + if (!node) { + return ( + + ); + } + + return ( + + ); +} + +export default function App() { + return ( + + + + ); +} diff --git a/src/gnmiMap.ts b/src/gnmiMap.ts new file mode 100644 index 0000000..137296d --- /dev/null +++ b/src/gnmiMap.ts @@ -0,0 +1,2959 @@ +// Generated by scripts/generate-map-data.ts. Do not edit by hand. + +import type { Edge, Node } from '@xyflow/react'; + +export type MapNodeKind = 'service' | 'rpc' | 'message' | 'enum' | 'external' | 'legend'; +export type MapEdgeKind = 'rpc' | 'field' | 'extension' | 'extension-detail'; +export type MapBadge = 'stream' | 'optional' | 'deprecated' | 'reserved'; + +export type MapSource = { + gnmiTag: string; + gnmiServiceVersion: string; + gnmiBase: string; + extBase: string; + specBase: string; +}; + +export type MapBounds = { + width: number; + height: number; +}; + +export type MapField = { + id: string; + type: string; + name: string; + ref?: string | null; + group?: string; + badge?: MapBadge; + deprecated?: boolean; +}; + +export type MapNodeData = Record & { + id: string; + kind: MapNodeKind; + label: string; + sourceSymbol?: string; + deprecated?: boolean; + protoUrl?: string; + specUrl?: string; + fields?: MapField[]; + badges?: MapBadge[]; + active?: boolean; + query?: string; + showExtensions?: boolean; +}; + +export type MapNode = Node; + +export type MapEdge = Edge, 'smoothstep'> & { + sourceHandle: string; + kind: MapEdgeKind; + deprecated: boolean; +}; + +export type VisibleMapOptions = { + showDeprecated?: boolean; + showExtensions?: boolean; +}; + +export type VisibleMap = { + nodes: MapNode[]; + edges: MapEdge[]; +}; + +export const mapSource: MapSource = { + "gnmiTag": "v0.14.1", + "gnmiServiceVersion": "0.10.0", + "gnmiBase": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto", + "extBase": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto", + "specBase": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md" +}; + +export const mapNodes: MapNode[] = [ + { + "id": "service-gnmi", + "type": "schema", + "position": { + "x": 1240, + "y": 40 + }, + "style": { + "width": 360 + }, + "data": { + "id": "service-gnmi", + "kind": "service", + "label": "service gNMI 0.10.0", + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L49", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#grpc-network-management-interface-gnmi", + "fields": [ + { + "id": "capabilities", + "type": "rpc", + "name": "Capabilities", + "ref": "rpc-capabilities" + }, + { + "id": "get", + "type": "rpc", + "name": "Get", + "ref": "rpc-get" + }, + { + "id": "set", + "type": "rpc", + "name": "Set", + "ref": "rpc-set" + }, + { + "id": "subscribe", + "type": "rpc", + "name": "Subscribe", + "ref": "rpc-subscribe", + "badge": "stream" + } + ] + } + }, + { + "id": "rpc-set", + "type": "schema", + "position": { + "x": 80, + "y": 230 + }, + "style": { + "width": 250 + }, + "data": { + "id": "rpc-set", + "kind": "rpc", + "label": "rpc Set", + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L67", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#34-modifying-state", + "fields": [ + { + "id": "takes", + "type": "takes", + "name": "SetRequest", + "ref": "set-request" + }, + { + "id": "returns", + "type": "returns", + "name": "SetResponse", + "ref": "set-response" + } + ] + } + }, + { + "id": "rpc-subscribe", + "type": "schema", + "position": { + "x": 780, + "y": 230 + }, + "style": { + "width": 290 + }, + "data": { + "id": "rpc-subscribe", + "kind": "rpc", + "label": "rpc Subscribe", + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L73", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#35-subscribing-to-telemetry-updates", + "fields": [ + { + "id": "takes", + "type": "takes stream", + "name": "SubscribeRequest", + "ref": "subscribe-request" + }, + { + "id": "returns", + "type": "returns stream", + "name": "SubscribeResponse", + "ref": "subscribe-response" + } + ] + } + }, + { + "id": "rpc-get", + "type": "schema", + "position": { + "x": 1520, + "y": 230 + }, + "style": { + "width": 250 + }, + "data": { + "id": "rpc-get", + "kind": "rpc", + "label": "rpc Get", + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L62", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#33-retrieving-snapshots-of-state-information", + "fields": [ + { + "id": "takes", + "type": "takes", + "name": "GetRequest", + "ref": "get-request" + }, + { + "id": "returns", + "type": "returns", + "name": "GetResponse", + "ref": "get-response" + } + ] + } + }, + { + "id": "rpc-capabilities", + "type": "schema", + "position": { + "x": 2200, + "y": 230 + }, + "style": { + "width": 300 + }, + "data": { + "id": "rpc-capabilities", + "kind": "rpc", + "label": "rpc Capabilities", + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L56", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#32-capability-discovery", + "fields": [ + { + "id": "takes", + "type": "takes", + "name": "CapabilityRequest", + "ref": "capability-request" + }, + { + "id": "returns", + "type": "returns", + "name": "CapabilityResponse", + "ref": "capability-response" + } + ] + } + }, + { + "id": "set-request", + "type": "schema", + "position": { + "x": 20, + "y": 450 + }, + "style": { + "width": 340 + }, + "data": { + "id": "set-request", + "kind": "message", + "label": "SetRequest", + "sourceSymbol": "gnmi.SetRequest", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L342", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#341-the-setrequest-message", + "fields": [ + { + "id": "prefix", + "type": "Path", + "name": "prefix", + "ref": "path" + }, + { + "id": "delete", + "type": "repeated Path", + "name": "delete", + "ref": "path" + }, + { + "id": "replace", + "type": "repeated Update", + "name": "replace", + "ref": "update" + }, + { + "id": "update", + "type": "repeated Update", + "name": "update", + "ref": "update" + }, + { + "id": "union-replace", + "type": "repeated Update", + "name": "union_replace", + "ref": "update" + }, + { + "id": "extension", + "type": "repeated gnmi_ext.Extension", + "name": "extension", + "ref": "extension", + "badge": "optional" + } + ] + } + }, + { + "id": "set-response", + "type": "schema", + "position": { + "x": 390, + "y": 450 + }, + "style": { + "width": 360 + }, + "data": { + "id": "set-response", + "kind": "message", + "label": "SetResponse", + "sourceSymbol": "gnmi.SetResponse", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L364", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#342-the-setresponse-message", + "fields": [ + { + "id": "prefix", + "type": "Path", + "name": "prefix", + "ref": "path" + }, + { + "id": "response", + "type": "repeated UpdateResult", + "name": "response", + "ref": "update-result" + }, + { + "id": "message", + "type": "Error", + "name": "message", + "ref": "error", + "badge": "deprecated", + "deprecated": true + }, + { + "id": "timestamp", + "type": "int64", + "name": "timestamp", + "ref": null + }, + { + "id": "extension", + "type": "repeated gnmi_ext.Extension", + "name": "extension", + "ref": "extension", + "badge": "optional" + } + ] + } + }, + { + "id": "subscribe-request", + "type": "schema", + "position": { + "x": 780, + "y": 450 + }, + "style": { + "width": 360 + }, + "data": { + "id": "subscribe-request", + "kind": "message", + "label": "SubscribeRequest", + "sourceSymbol": "gnmi.SubscribeRequest", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L220", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#3511-the-subscriberequest-message", + "fields": [ + { + "id": "subscribe", + "type": "SubscriptionList", + "name": "subscribe", + "ref": "subscription-list", + "group": "oneof request" + }, + { + "id": "poll", + "type": "Poll", + "name": "poll", + "ref": "poll", + "group": "oneof request" + }, + { + "id": "extension", + "type": "repeated gnmi_ext.Extension", + "name": "extension", + "ref": "extension", + "badge": "optional" + }, + { + "id": "reserved-aliases", + "type": "reserved", + "name": "aliases / 4", + "ref": null, + "badge": "reserved" + } + ] + } + }, + { + "id": "subscribe-response", + "type": "schema", + "position": { + "x": 1180, + "y": 450 + }, + "style": { + "width": 360 + }, + "data": { + "id": "subscribe-response", + "kind": "message", + "label": "SubscribeResponse", + "sourceSymbol": "gnmi.SubscribeResponse", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L245", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#3514-the-subscriberesponse-message", + "fields": [ + { + "id": "update", + "type": "Notification", + "name": "update", + "ref": "notification", + "group": "oneof response" + }, + { + "id": "sync-response", + "type": "bool", + "name": "sync_response", + "ref": null, + "group": "oneof response" + }, + { + "id": "error", + "type": "Error", + "name": "error", + "ref": "error", + "group": "oneof response", + "badge": "deprecated", + "deprecated": true + }, + { + "id": "extension", + "type": "repeated gnmi_ext.Extension", + "name": "extension", + "ref": "extension", + "badge": "optional" + } + ] + } + }, + { + "id": "get-request", + "type": "schema", + "position": { + "x": 1570, + "y": 450 + }, + "style": { + "width": 350 + }, + "data": { + "id": "get-request", + "kind": "message", + "label": "GetRequest", + "sourceSymbol": "gnmi.GetRequest", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L405", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#331-the-getrequest-message", + "fields": [ + { + "id": "prefix", + "type": "Path", + "name": "prefix", + "ref": "path" + }, + { + "id": "path", + "type": "repeated Path", + "name": "path", + "ref": "path" + }, + { + "id": "type", + "type": "DataType", + "name": "type", + "ref": "data-type" + }, + { + "id": "encoding", + "type": "Encoding", + "name": "encoding", + "ref": "encoding" + }, + { + "id": "use-models", + "type": "repeated ModelData", + "name": "use_models", + "ref": "model-data" + }, + { + "id": "extension", + "type": "repeated gnmi_ext.Extension", + "name": "extension", + "ref": "extension", + "badge": "optional" + } + ] + } + }, + { + "id": "get-response", + "type": "schema", + "position": { + "x": 1960, + "y": 450 + }, + "style": { + "width": 350 + }, + "data": { + "id": "get-response", + "kind": "message", + "label": "GetResponse", + "sourceSymbol": "gnmi.GetResponse", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L430", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#332-the-getresponse-message", + "fields": [ + { + "id": "notification", + "type": "repeated Notification", + "name": "notification", + "ref": "notification" + }, + { + "id": "error", + "type": "Error", + "name": "error", + "ref": "error", + "badge": "deprecated", + "deprecated": true + }, + { + "id": "extension", + "type": "repeated gnmi_ext.Extension", + "name": "extension", + "ref": "extension", + "badge": "optional" + } + ] + } + }, + { + "id": "capability-request", + "type": "schema", + "position": { + "x": 2350, + "y": 450 + }, + "style": { + "width": 340 + }, + "data": { + "id": "capability-request", + "kind": "message", + "label": "CapabilityRequest", + "sourceSymbol": "gnmi.CapabilityRequest", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L441", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#321-the-capabilityrequest-message", + "fields": [ + { + "id": "extension", + "type": "repeated gnmi_ext.Extension", + "name": "extension", + "ref": "extension", + "badge": "optional" + } + ] + } + }, + { + "id": "capability-response", + "type": "schema", + "position": { + "x": 2730, + "y": 450 + }, + "style": { + "width": 370 + }, + "data": { + "id": "capability-response", + "kind": "message", + "label": "CapabilityResponse", + "sourceSymbol": "gnmi.CapabilityResponse", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L450", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#322-the-capabilityresponse-message", + "fields": [ + { + "id": "supported-models", + "type": "repeated ModelData", + "name": "supported_models", + "ref": "model-data" + }, + { + "id": "supported-encodings", + "type": "repeated Encoding", + "name": "supported_encodings", + "ref": "encoding" + }, + { + "id": "g-nmi-version", + "type": "string", + "name": "gNMI_version", + "ref": null + }, + { + "id": "extension", + "type": "repeated gnmi_ext.Extension", + "name": "extension", + "ref": "extension", + "badge": "optional" + } + ] + } + }, + { + "id": "error", + "type": "schema", + "position": { + "x": 120, + "y": 850 + }, + "style": { + "width": 300 + }, + "data": { + "id": "error", + "kind": "message", + "label": "Error", + "sourceSymbol": "gnmi.Error", + "deprecated": true, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L187", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#23-structured-data-types", + "badges": [ + "deprecated" + ], + "fields": [ + { + "id": "code", + "type": "uint32", + "name": "code", + "ref": null + }, + { + "id": "message", + "type": "string", + "name": "message", + "ref": null + }, + { + "id": "data", + "type": "google.protobuf.Any", + "name": "data", + "ref": "any" + } + ] + } + }, + { + "id": "update-result", + "type": "schema", + "position": { + "x": 450, + "y": 820 + }, + "style": { + "width": 330 + }, + "data": { + "id": "update-result", + "kind": "message", + "label": "UpdateResult", + "sourceSymbol": "gnmi.UpdateResult", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L380", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#342-the-setresponse-message", + "fields": [ + { + "id": "timestamp", + "type": "int64", + "name": "timestamp", + "ref": null, + "badge": "deprecated", + "deprecated": true + }, + { + "id": "path", + "type": "Path", + "name": "path", + "ref": "path" + }, + { + "id": "message", + "type": "Error", + "name": "message", + "ref": "error", + "badge": "deprecated", + "deprecated": true + }, + { + "id": "op", + "type": "Operation", + "name": "op", + "ref": "operation" + } + ] + } + }, + { + "id": "operation", + "type": "schema", + "position": { + "x": 430, + "y": 1120 + }, + "style": { + "width": 260 + }, + "data": { + "id": "operation", + "kind": "enum", + "label": "enum Operation", + "sourceSymbol": "gnmi.UpdateResult.Operation", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L382", + "fields": [ + { + "id": "invalid", + "type": "0", + "name": "INVALID", + "ref": null + }, + { + "id": "delete", + "type": "1", + "name": "DELETE", + "ref": null + }, + { + "id": "replace", + "type": "2", + "name": "REPLACE", + "ref": null + }, + { + "id": "update", + "type": "3", + "name": "UPDATE", + "ref": null + }, + { + "id": "union-replace", + "type": "4", + "name": "UNION_REPLACE", + "ref": null + } + ] + } + }, + { + "id": "poll", + "type": "schema", + "position": { + "x": 800, + "y": 780 + }, + "style": { + "width": 180 + }, + "data": { + "id": "poll", + "kind": "message", + "label": "Poll", + "sourceSymbol": "gnmi.Poll", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L237", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#35153-poll-subscriptions", + "fields": [] + } + }, + { + "id": "subscription-list", + "type": "schema", + "position": { + "x": 1050, + "y": 750 + }, + "style": { + "width": 390 + }, + "data": { + "id": "subscription-list", + "kind": "message", + "label": "SubscriptionList", + "sourceSymbol": "gnmi.SubscriptionList", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L264", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#3512-the-subscriptionlist-message", + "fields": [ + { + "id": "prefix", + "type": "Path", + "name": "prefix", + "ref": "path" + }, + { + "id": "subscription", + "type": "repeated Subscription", + "name": "subscription", + "ref": "subscription" + }, + { + "id": "qos", + "type": "QOSMarking", + "name": "qos", + "ref": "qos-marking" + }, + { + "id": "mode", + "type": "Mode", + "name": "mode", + "ref": "mode" + }, + { + "id": "allow-aggregation", + "type": "bool", + "name": "allow_aggregation", + "ref": null + }, + { + "id": "use-models", + "type": "repeated ModelData", + "name": "use_models", + "ref": "model-data" + }, + { + "id": "encoding", + "type": "Encoding", + "name": "encoding", + "ref": "encoding" + }, + { + "id": "updates-only", + "type": "bool", + "name": "updates_only", + "ref": null + }, + { + "id": "reserved-use-aliases", + "type": "reserved", + "name": "use_aliases / 3", + "ref": null, + "badge": "reserved" + } + ] + } + }, + { + "id": "qos-marking", + "type": "schema", + "position": { + "x": 990, + "y": 1210 + }, + "style": { + "width": 270 + }, + "data": { + "id": "qos-marking", + "kind": "message", + "label": "QOSMarking", + "sourceSymbol": "gnmi.QOSMarking", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L331", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#3512-the-subscriptionlist-message", + "fields": [ + { + "id": "marking", + "type": "uint32", + "name": "marking", + "ref": null + } + ] + } + }, + { + "id": "mode", + "type": "schema", + "position": { + "x": 990, + "y": 1390 + }, + "style": { + "width": 240 + }, + "data": { + "id": "mode", + "kind": "enum", + "label": "enum Mode", + "sourceSymbol": "gnmi.SubscriptionList.Mode", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L269", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#3512-the-subscriptionlist-message", + "fields": [ + { + "id": "stream", + "type": "0", + "name": "STREAM", + "ref": null + }, + { + "id": "once", + "type": "1", + "name": "ONCE", + "ref": null + }, + { + "id": "poll", + "type": "2", + "name": "POLL", + "ref": null + } + ] + } + }, + { + "id": "subscription", + "type": "schema", + "position": { + "x": 1460, + "y": 920 + }, + "style": { + "width": 360 + }, + "data": { + "id": "subscription", + "kind": "message", + "label": "Subscription", + "sourceSymbol": "gnmi.Subscription", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L300", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#3513-the-subscription-message", + "fields": [ + { + "id": "path", + "type": "Path", + "name": "path", + "ref": "path" + }, + { + "id": "mode", + "type": "SubscriptionMode", + "name": "mode", + "ref": "subscription-mode" + }, + { + "id": "sample-interval", + "type": "uint64", + "name": "sample_interval", + "ref": null + }, + { + "id": "suppress-redundant", + "type": "bool", + "name": "suppress_redundant", + "ref": null + }, + { + "id": "heartbeat-interval", + "type": "uint64", + "name": "heartbeat_interval", + "ref": null + } + ] + } + }, + { + "id": "subscription-mode", + "type": "schema", + "position": { + "x": 1460, + "y": 1250 + }, + "style": { + "width": 310 + }, + "data": { + "id": "subscription-mode", + "kind": "enum", + "label": "enum SubscriptionMode", + "sourceSymbol": "gnmi.SubscriptionMode", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L322", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#35152-stream-subscriptions", + "fields": [ + { + "id": "target-defined", + "type": "0", + "name": "TARGET_DEFINED", + "ref": null + }, + { + "id": "on-change", + "type": "1", + "name": "ON_CHANGE", + "ref": null + }, + { + "id": "sample", + "type": "2", + "name": "SAMPLE", + "ref": null + } + ] + } + }, + { + "id": "data-type", + "type": "schema", + "position": { + "x": 1660, + "y": 720 + }, + "style": { + "width": 280 + }, + "data": { + "id": "data-type", + "kind": "enum", + "label": "enum DataType", + "sourceSymbol": "gnmi.GetRequest.DataType", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L409", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#331-the-getrequest-message", + "fields": [ + { + "id": "all", + "type": "0", + "name": "ALL", + "ref": null + }, + { + "id": "config", + "type": "1", + "name": "CONFIG", + "ref": null + }, + { + "id": "state", + "type": "2", + "name": "STATE", + "ref": null + }, + { + "id": "operational", + "type": "3", + "name": "OPERATIONAL", + "ref": null + } + ] + } + }, + { + "id": "model-data", + "type": "schema", + "position": { + "x": 2410, + "y": 790 + }, + "style": { + "width": 300 + }, + "data": { + "id": "model-data", + "kind": "message", + "label": "ModelData", + "sourceSymbol": "gnmi.ModelData", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L464", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#261-the-modeldata-message", + "fields": [ + { + "id": "name", + "type": "string", + "name": "name", + "ref": null + }, + { + "id": "organization", + "type": "string", + "name": "organization", + "ref": null + }, + { + "id": "version", + "type": "string", + "name": "version", + "ref": null + } + ] + } + }, + { + "id": "encoding", + "type": "schema", + "position": { + "x": 2380, + "y": 1120 + }, + "style": { + "width": 260 + }, + "data": { + "id": "encoding", + "kind": "enum", + "label": "enum Encoding", + "sourceSymbol": "gnmi.Encoding", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L175", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#23-structured-data-types", + "fields": [ + { + "id": "json", + "type": "0", + "name": "JSON", + "ref": null + }, + { + "id": "bytes", + "type": "1", + "name": "BYTES", + "ref": null + }, + { + "id": "proto", + "type": "2", + "name": "PROTO", + "ref": null + }, + { + "id": "ascii", + "type": "3", + "name": "ASCII", + "ref": null + }, + { + "id": "json-ietf", + "type": "4", + "name": "JSON_IETF", + "ref": null + } + ] + } + }, + { + "id": "notification", + "type": "schema", + "position": { + "x": 1740, + "y": 1160 + }, + "style": { + "width": 350 + }, + "data": { + "id": "notification", + "kind": "message", + "label": "Notification", + "sourceSymbol": "gnmi.Notification", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L84", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#21-reusable-notification-message-format", + "fields": [ + { + "id": "timestamp", + "type": "int64", + "name": "timestamp", + "ref": null + }, + { + "id": "prefix", + "type": "Path", + "name": "prefix", + "ref": "path" + }, + { + "id": "update", + "type": "repeated Update", + "name": "update", + "ref": "update" + }, + { + "id": "delete", + "type": "repeated Path", + "name": "delete", + "ref": "path" + }, + { + "id": "atomic", + "type": "bool", + "name": "atomic", + "ref": null + }, + { + "id": "reserved-alias", + "type": "reserved", + "name": "alias / 3", + "ref": null, + "badge": "reserved" + } + ] + } + }, + { + "id": "update", + "type": "schema", + "position": { + "x": 1320, + "y": 1460 + }, + "style": { + "width": 330 + }, + "data": { + "id": "update", + "kind": "message", + "label": "Update", + "sourceSymbol": "gnmi.Update", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L100", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#21-reusable-notification-message-format", + "fields": [ + { + "id": "path", + "type": "Path", + "name": "path", + "ref": "path" + }, + { + "id": "value", + "type": "Value", + "name": "value", + "ref": "value", + "badge": "deprecated", + "deprecated": true + }, + { + "id": "val", + "type": "TypedValue", + "name": "val", + "ref": "typed-value" + }, + { + "id": "duplicates", + "type": "uint32", + "name": "duplicates", + "ref": null + } + ] + } + }, + { + "id": "path", + "type": "schema", + "position": { + "x": 1780, + "y": 1530 + }, + "style": { + "width": 360 + }, + "data": { + "id": "path", + "kind": "message", + "label": "Path", + "sourceSymbol": "gnmi.Path", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L142", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#222-paths", + "fields": [ + { + "id": "element", + "type": "repeated string", + "name": "element", + "ref": null, + "badge": "deprecated", + "deprecated": true + }, + { + "id": "origin", + "type": "string", + "name": "origin", + "ref": null + }, + { + "id": "elem", + "type": "repeated PathElem", + "name": "elem", + "ref": "path-elem" + }, + { + "id": "target", + "type": "string", + "name": "target", + "ref": null + } + ] + } + }, + { + "id": "path-elem", + "type": "schema", + "position": { + "x": 2210, + "y": 1600 + }, + "style": { + "width": 330 + }, + "data": { + "id": "path-elem", + "kind": "message", + "label": "PathElem", + "sourceSymbol": "gnmi.PathElem", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L155", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#222-paths", + "fields": [ + { + "id": "name", + "type": "string", + "name": "name", + "ref": null + }, + { + "id": "key", + "type": "map", + "name": "key", + "ref": null + } + ] + } + }, + { + "id": "value", + "type": "schema", + "position": { + "x": 700, + "y": 1570 + }, + "style": { + "width": 300 + }, + "data": { + "id": "value", + "kind": "message", + "label": "Value", + "sourceSymbol": "gnmi.Value", + "deprecated": true, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L163", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#223-node-values", + "badges": [ + "deprecated" + ], + "fields": [ + { + "id": "value", + "type": "bytes", + "name": "value", + "ref": null + }, + { + "id": "type", + "type": "Encoding", + "name": "type", + "ref": "encoding" + } + ] + } + }, + { + "id": "typed-value", + "type": "schema", + "position": { + "x": 980, + "y": 1780 + }, + "style": { + "width": 390 + }, + "data": { + "id": "typed-value", + "kind": "message", + "label": "TypedValue", + "sourceSymbol": "gnmi.TypedValue", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L109", + "fields": [ + { + "id": "string-val", + "type": "string", + "name": "string_val", + "ref": null, + "group": "oneof value" + }, + { + "id": "int-val", + "type": "int64", + "name": "int_val", + "ref": null, + "group": "oneof value" + }, + { + "id": "uint-val", + "type": "uint64", + "name": "uint_val", + "ref": null, + "group": "oneof value" + }, + { + "id": "bool-val", + "type": "bool", + "name": "bool_val", + "ref": null, + "group": "oneof value" + }, + { + "id": "bytes-val", + "type": "bytes", + "name": "bytes_val", + "ref": null, + "group": "oneof value" + }, + { + "id": "float-val", + "type": "float", + "name": "float_val", + "ref": null, + "group": "oneof value", + "badge": "deprecated", + "deprecated": true + }, + { + "id": "double-val", + "type": "double", + "name": "double_val", + "ref": null, + "group": "oneof value" + }, + { + "id": "decimal-val", + "type": "Decimal64", + "name": "decimal_val", + "ref": "decimal64", + "group": "oneof value", + "badge": "deprecated", + "deprecated": true + }, + { + "id": "leaflist-val", + "type": "ScalarArray", + "name": "leaflist_val", + "ref": "scalar-array", + "group": "oneof value" + }, + { + "id": "any-val", + "type": "google.protobuf.Any", + "name": "any_val", + "ref": "any", + "group": "oneof value" + }, + { + "id": "json-val", + "type": "bytes", + "name": "json_val", + "ref": null, + "group": "oneof value" + }, + { + "id": "json-ietf-val", + "type": "bytes", + "name": "json_ietf_val", + "ref": null, + "group": "oneof value" + }, + { + "id": "ascii-val", + "type": "string", + "name": "ascii_val", + "ref": null, + "group": "oneof value" + }, + { + "id": "proto-bytes", + "type": "bytes", + "name": "proto_bytes", + "ref": null, + "group": "oneof value" + } + ] + } + }, + { + "id": "decimal64", + "type": "schema", + "position": { + "x": 1430, + "y": 1780 + }, + "style": { + "width": 280 + }, + "data": { + "id": "decimal64", + "kind": "message", + "label": "Decimal64", + "sourceSymbol": "gnmi.Decimal64", + "deprecated": true, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L200", + "badges": [ + "deprecated" + ], + "fields": [ + { + "id": "digits", + "type": "int64", + "name": "digits", + "ref": null + }, + { + "id": "precision", + "type": "uint32", + "name": "precision", + "ref": null + } + ] + } + }, + { + "id": "scalar-array", + "type": "schema", + "position": { + "x": 1430, + "y": 1980 + }, + "style": { + "width": 330 + }, + "data": { + "id": "scalar-array", + "kind": "message", + "label": "ScalarArray", + "sourceSymbol": "gnmi.ScalarArray", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi/gnmi.proto#L208", + "fields": [ + { + "id": "element", + "type": "repeated TypedValue", + "name": "element", + "ref": "typed-value" + } + ] + } + }, + { + "id": "any", + "type": "schema", + "position": { + "x": 550, + "y": 1870 + }, + "style": { + "width": 330 + }, + "data": { + "id": "any", + "kind": "external", + "label": "google.protobuf.Any", + "protoUrl": "https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto", + "fields": [ + { + "id": "type-url", + "type": "string", + "name": "type_url" + }, + { + "id": "value", + "type": "bytes", + "name": "value" + } + ] + } + }, + { + "id": "extension", + "type": "schema", + "position": { + "x": 2020, + "y": 820 + }, + "style": { + "width": 360 + }, + "data": { + "id": "extension", + "kind": "message", + "label": "gnmi_ext.Extension", + "sourceSymbol": "gnmi_ext.Extension", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L29", + "specUrl": "https://github.com/openconfig/reference/blob/master/rpc/gnmi/gnmi-specification.md#27-extensions-to-gnmi", + "fields": [ + { + "id": "registered-ext", + "type": "RegisteredExtension", + "name": "registered_ext", + "ref": "registered-extension", + "group": "oneof ext" + }, + { + "id": "master-arbitration", + "type": "MasterArbitration", + "name": "master_arbitration", + "ref": "master-arbitration", + "group": "oneof ext" + }, + { + "id": "history", + "type": "History", + "name": "history", + "ref": "history", + "group": "oneof ext" + }, + { + "id": "commit", + "type": "Commit", + "name": "commit", + "ref": "commit", + "group": "oneof ext" + }, + { + "id": "depth", + "type": "Depth", + "name": "depth", + "ref": "depth", + "group": "oneof ext" + }, + { + "id": "config-subscription", + "type": "ConfigSubscription", + "name": "config_subscription", + "ref": "config-subscription", + "group": "oneof ext" + } + ] + } + }, + { + "id": "registered-extension", + "type": "schema", + "position": { + "x": 2510, + "y": 760 + }, + "style": { + "width": 390 + }, + "data": { + "id": "registered-extension", + "kind": "message", + "label": "gnmi_ext.RegisteredExtension", + "sourceSymbol": "gnmi_ext.RegisteredExtension", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L43", + "fields": [ + { + "id": "id", + "type": "ExtensionID", + "name": "id", + "ref": "extension-id" + }, + { + "id": "msg", + "type": "bytes", + "name": "msg", + "ref": null + } + ] + } + }, + { + "id": "extension-id", + "type": "schema", + "position": { + "x": 2940, + "y": 700 + }, + "style": { + "width": 310 + }, + "data": { + "id": "extension-id", + "kind": "enum", + "label": "enum gnmi_ext.ExtensionID", + "sourceSymbol": "gnmi_ext.ExtensionID", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L50", + "fields": [ + { + "id": "unset", + "type": "0", + "name": "EID_UNSET", + "ref": null + }, + { + "id": "experimental", + "type": "999", + "name": "EID_EXPERIMENTAL", + "ref": null + } + ] + } + }, + { + "id": "master-arbitration", + "type": "schema", + "position": { + "x": 2510, + "y": 1010 + }, + "style": { + "width": 390 + }, + "data": { + "id": "master-arbitration", + "kind": "message", + "label": "gnmi_ext.MasterArbitration", + "sourceSymbol": "gnmi_ext.MasterArbitration", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L65", + "fields": [ + { + "id": "role", + "type": "Role", + "name": "role", + "ref": "role" + }, + { + "id": "election-id", + "type": "Uint128", + "name": "election_id", + "ref": "uint128" + } + ] + } + }, + { + "id": "uint128", + "type": "schema", + "position": { + "x": 2940, + "y": 1040 + }, + "style": { + "width": 260 + }, + "data": { + "id": "uint128", + "kind": "message", + "label": "gnmi_ext.Uint128", + "sourceSymbol": "gnmi_ext.Uint128", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L71", + "fields": [ + { + "id": "high", + "type": "uint64", + "name": "high", + "ref": null + }, + { + "id": "low", + "type": "uint64", + "name": "low", + "ref": null + } + ] + } + }, + { + "id": "role", + "type": "schema", + "position": { + "x": 2940, + "y": 1210 + }, + "style": { + "width": 260 + }, + "data": { + "id": "role", + "kind": "message", + "label": "gnmi_ext.Role", + "sourceSymbol": "gnmi_ext.Role", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L77", + "fields": [ + { + "id": "id", + "type": "string", + "name": "id", + "ref": null + } + ] + } + }, + { + "id": "history", + "type": "schema", + "position": { + "x": 2510, + "y": 1260 + }, + "style": { + "width": 330 + }, + "data": { + "id": "history", + "kind": "message", + "label": "gnmi_ext.History", + "sourceSymbol": "gnmi_ext.History", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L86", + "fields": [ + { + "id": "snapshot-time", + "type": "int64", + "name": "snapshot_time", + "ref": null, + "group": "oneof request" + }, + { + "id": "range", + "type": "TimeRange", + "name": "range", + "ref": "time-range", + "group": "oneof request" + } + ] + } + }, + { + "id": "time-range", + "type": "schema", + "position": { + "x": 2940, + "y": 1390 + }, + "style": { + "width": 270 + }, + "data": { + "id": "time-range", + "kind": "message", + "label": "gnmi_ext.TimeRange", + "sourceSymbol": "gnmi_ext.TimeRange", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L93", + "fields": [ + { + "id": "start", + "type": "int64", + "name": "start", + "ref": null + }, + { + "id": "end", + "type": "int64", + "name": "end", + "ref": null + } + ] + } + }, + { + "id": "commit", + "type": "schema", + "position": { + "x": 2510, + "y": 1500 + }, + "style": { + "width": 390 + }, + "data": { + "id": "commit", + "kind": "message", + "label": "gnmi_ext.Commit", + "sourceSymbol": "gnmi_ext.Commit", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L104", + "fields": [ + { + "id": "id", + "type": "string", + "name": "id", + "ref": null + }, + { + "id": "commit", + "type": "CommitRequest", + "name": "commit", + "ref": "commit-request", + "group": "oneof action" + }, + { + "id": "confirm", + "type": "CommitConfirm", + "name": "confirm", + "ref": "commit-confirm", + "group": "oneof action" + }, + { + "id": "cancel", + "type": "CommitCancel", + "name": "cancel", + "ref": "commit-cancel", + "group": "oneof action" + }, + { + "id": "set-rollback-duration", + "type": "CommitSetRollbackDuration", + "name": "set_rollback_duration", + "ref": "commit-set-rollback-duration", + "group": "oneof action" + } + ] + } + }, + { + "id": "commit-request", + "type": "schema", + "position": { + "x": 2960, + "y": 1580 + }, + "style": { + "width": 360 + }, + "data": { + "id": "commit-request", + "kind": "message", + "label": "gnmi_ext.CommitRequest", + "sourceSymbol": "gnmi_ext.CommitRequest", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L129", + "fields": [ + { + "id": "rollback-duration", + "type": "google.protobuf.Duration", + "name": "rollback_duration", + "ref": "duration" + } + ] + } + }, + { + "id": "commit-confirm", + "type": "schema", + "position": { + "x": 2960, + "y": 1760 + }, + "style": { + "width": 300 + }, + "data": { + "id": "commit-confirm", + "kind": "message", + "label": "gnmi_ext.CommitConfirm", + "sourceSymbol": "gnmi_ext.CommitConfirm", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L136", + "fields": [] + } + }, + { + "id": "commit-cancel", + "type": "schema", + "position": { + "x": 2960, + "y": 1900 + }, + "style": { + "width": 300 + }, + "data": { + "id": "commit-cancel", + "kind": "message", + "label": "gnmi_ext.CommitCancel", + "sourceSymbol": "gnmi_ext.CommitCancel", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L140", + "fields": [] + } + }, + { + "id": "commit-set-rollback-duration", + "type": "schema", + "position": { + "x": 2960, + "y": 2040 + }, + "style": { + "width": 400 + }, + "data": { + "id": "commit-set-rollback-duration", + "kind": "message", + "label": "gnmi_ext.CommitSetRollbackDuration", + "sourceSymbol": "gnmi_ext.CommitSetRollbackDuration", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L144", + "fields": [ + { + "id": "rollback-duration", + "type": "google.protobuf.Duration", + "name": "rollback_duration", + "ref": "duration" + } + ] + } + }, + { + "id": "duration", + "type": "schema", + "position": { + "x": 3400, + "y": 1740 + }, + "style": { + "width": 300 + }, + "data": { + "id": "duration", + "kind": "external", + "label": "google.protobuf.Duration", + "protoUrl": "https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/duration.proto", + "fields": [ + { + "id": "seconds", + "type": "int64", + "name": "seconds" + }, + { + "id": "nanos", + "type": "int32", + "name": "nanos" + } + ] + } + }, + { + "id": "depth", + "type": "schema", + "position": { + "x": 3350, + "y": 850 + }, + "style": { + "width": 260 + }, + "data": { + "id": "depth", + "kind": "message", + "label": "gnmi_ext.Depth", + "sourceSymbol": "gnmi_ext.Depth", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L155", + "fields": [ + { + "id": "level", + "type": "uint32", + "name": "level", + "ref": null + } + ] + } + }, + { + "id": "config-subscription", + "type": "schema", + "position": { + "x": 3350, + "y": 1030 + }, + "style": { + "width": 400 + }, + "data": { + "id": "config-subscription", + "kind": "message", + "label": "gnmi_ext.ConfigSubscription", + "sourceSymbol": "gnmi_ext.ConfigSubscription", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L166", + "fields": [ + { + "id": "start", + "type": "ConfigSubscriptionStart", + "name": "start", + "ref": "config-subscription-start", + "group": "oneof action" + }, + { + "id": "sync-done", + "type": "ConfigSubscriptionSyncDone", + "name": "sync_done", + "ref": "config-subscription-sync-done", + "group": "oneof action" + } + ] + } + }, + { + "id": "config-subscription-start", + "type": "schema", + "position": { + "x": 3810, + "y": 980 + }, + "style": { + "width": 360 + }, + "data": { + "id": "config-subscription-start", + "kind": "message", + "label": "gnmi_ext.ConfigSubscriptionStart", + "sourceSymbol": "gnmi_ext.ConfigSubscriptionStart", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L178", + "fields": [] + } + }, + { + "id": "config-subscription-sync-done", + "type": "schema", + "position": { + "x": 3810, + "y": 1130 + }, + "style": { + "width": 400 + }, + "data": { + "id": "config-subscription-sync-done", + "kind": "message", + "label": "gnmi_ext.ConfigSubscriptionSyncDone", + "sourceSymbol": "gnmi_ext.ConfigSubscriptionSyncDone", + "deprecated": false, + "protoUrl": "https://github.com/openconfig/gnmi/blob/v0.14.1/proto/gnmi_ext/gnmi_ext.proto#L182", + "fields": [ + { + "id": "commit-confirm-id", + "type": "string", + "name": "commit_confirm_id", + "ref": null + }, + { + "id": "server-commit-id", + "type": "string", + "name": "server_commit_id", + "ref": null + }, + { + "id": "done", + "type": "bool", + "name": "done", + "ref": null + } + ] + } + }, + { + "id": "legend", + "type": "schema", + "position": { + "x": 2720, + "y": 1470 + }, + "style": { + "width": 410 + }, + "data": { + "id": "legend", + "kind": "legend", + "label": "Legend", + "fields": [ + { + "id": "proto", + "type": "file icon", + "name": "proto definition link" + }, + { + "id": "docs", + "type": "book icon", + "name": "documentation link" + }, + { + "id": "extension-note", + "type": "toggle", + "name": "extension relationship edges" + }, + { + "id": "pdf", + "type": "reference", + "name": "gnmi_0.7.0_map.pdf" + } + ] + } + } +]; + +export const mapEdges: MapEdge[] = [ + { + "id": "service-gnmi:capabilities->rpc-capabilities", + "source": "service-gnmi", + "sourceHandle": "capabilities", + "target": "rpc-capabilities", + "kind": "rpc", + "deprecated": false + }, + { + "id": "service-gnmi:get->rpc-get", + "source": "service-gnmi", + "sourceHandle": "get", + "target": "rpc-get", + "kind": "rpc", + "deprecated": false + }, + { + "id": "service-gnmi:set->rpc-set", + "source": "service-gnmi", + "sourceHandle": "set", + "target": "rpc-set", + "kind": "rpc", + "deprecated": false + }, + { + "id": "service-gnmi:subscribe->rpc-subscribe", + "source": "service-gnmi", + "sourceHandle": "subscribe", + "target": "rpc-subscribe", + "kind": "rpc", + "deprecated": false + }, + { + "id": "rpc-set:takes->set-request", + "source": "rpc-set", + "sourceHandle": "takes", + "target": "set-request", + "kind": "field", + "deprecated": false + }, + { + "id": "rpc-set:returns->set-response", + "source": "rpc-set", + "sourceHandle": "returns", + "target": "set-response", + "kind": "field", + "deprecated": false + }, + { + "id": "rpc-subscribe:takes->subscribe-request", + "source": "rpc-subscribe", + "sourceHandle": "takes", + "target": "subscribe-request", + "kind": "field", + "deprecated": false + }, + { + "id": "rpc-subscribe:returns->subscribe-response", + "source": "rpc-subscribe", + "sourceHandle": "returns", + "target": "subscribe-response", + "kind": "field", + "deprecated": false + }, + { + "id": "rpc-get:takes->get-request", + "source": "rpc-get", + "sourceHandle": "takes", + "target": "get-request", + "kind": "field", + "deprecated": false + }, + { + "id": "rpc-get:returns->get-response", + "source": "rpc-get", + "sourceHandle": "returns", + "target": "get-response", + "kind": "field", + "deprecated": false + }, + { + "id": "rpc-capabilities:takes->capability-request", + "source": "rpc-capabilities", + "sourceHandle": "takes", + "target": "capability-request", + "kind": "field", + "deprecated": false + }, + { + "id": "rpc-capabilities:returns->capability-response", + "source": "rpc-capabilities", + "sourceHandle": "returns", + "target": "capability-response", + "kind": "field", + "deprecated": false + }, + { + "id": "set-request:prefix->path", + "source": "set-request", + "sourceHandle": "prefix", + "target": "path", + "kind": "field", + "deprecated": false + }, + { + "id": "set-request:delete->path", + "source": "set-request", + "sourceHandle": "delete", + "target": "path", + "kind": "field", + "deprecated": false + }, + { + "id": "set-request:replace->update", + "source": "set-request", + "sourceHandle": "replace", + "target": "update", + "kind": "field", + "deprecated": false + }, + { + "id": "set-request:update->update", + "source": "set-request", + "sourceHandle": "update", + "target": "update", + "kind": "field", + "deprecated": false + }, + { + "id": "set-request:union-replace->update", + "source": "set-request", + "sourceHandle": "union-replace", + "target": "update", + "kind": "field", + "deprecated": false + }, + { + "id": "set-request:extension->extension", + "source": "set-request", + "sourceHandle": "extension", + "target": "extension", + "kind": "extension", + "deprecated": false + }, + { + "id": "set-response:prefix->path", + "source": "set-response", + "sourceHandle": "prefix", + "target": "path", + "kind": "field", + "deprecated": false + }, + { + "id": "set-response:response->update-result", + "source": "set-response", + "sourceHandle": "response", + "target": "update-result", + "kind": "field", + "deprecated": false + }, + { + "id": "set-response:message->error", + "source": "set-response", + "sourceHandle": "message", + "target": "error", + "kind": "field", + "deprecated": true + }, + { + "id": "set-response:extension->extension", + "source": "set-response", + "sourceHandle": "extension", + "target": "extension", + "kind": "extension", + "deprecated": false + }, + { + "id": "subscribe-request:subscribe->subscription-list", + "source": "subscribe-request", + "sourceHandle": "subscribe", + "target": "subscription-list", + "kind": "field", + "deprecated": false + }, + { + "id": "subscribe-request:poll->poll", + "source": "subscribe-request", + "sourceHandle": "poll", + "target": "poll", + "kind": "field", + "deprecated": false + }, + { + "id": "subscribe-request:extension->extension", + "source": "subscribe-request", + "sourceHandle": "extension", + "target": "extension", + "kind": "extension", + "deprecated": false + }, + { + "id": "subscribe-response:update->notification", + "source": "subscribe-response", + "sourceHandle": "update", + "target": "notification", + "kind": "field", + "deprecated": false + }, + { + "id": "subscribe-response:error->error", + "source": "subscribe-response", + "sourceHandle": "error", + "target": "error", + "kind": "field", + "deprecated": true + }, + { + "id": "subscribe-response:extension->extension", + "source": "subscribe-response", + "sourceHandle": "extension", + "target": "extension", + "kind": "extension", + "deprecated": false + }, + { + "id": "get-request:prefix->path", + "source": "get-request", + "sourceHandle": "prefix", + "target": "path", + "kind": "field", + "deprecated": false + }, + { + "id": "get-request:path->path", + "source": "get-request", + "sourceHandle": "path", + "target": "path", + "kind": "field", + "deprecated": false + }, + { + "id": "get-request:type->data-type", + "source": "get-request", + "sourceHandle": "type", + "target": "data-type", + "kind": "field", + "deprecated": false + }, + { + "id": "get-request:encoding->encoding", + "source": "get-request", + "sourceHandle": "encoding", + "target": "encoding", + "kind": "field", + "deprecated": false + }, + { + "id": "get-request:use-models->model-data", + "source": "get-request", + "sourceHandle": "use-models", + "target": "model-data", + "kind": "field", + "deprecated": false + }, + { + "id": "get-request:extension->extension", + "source": "get-request", + "sourceHandle": "extension", + "target": "extension", + "kind": "extension", + "deprecated": false + }, + { + "id": "get-response:notification->notification", + "source": "get-response", + "sourceHandle": "notification", + "target": "notification", + "kind": "field", + "deprecated": false + }, + { + "id": "get-response:error->error", + "source": "get-response", + "sourceHandle": "error", + "target": "error", + "kind": "field", + "deprecated": true + }, + { + "id": "get-response:extension->extension", + "source": "get-response", + "sourceHandle": "extension", + "target": "extension", + "kind": "extension", + "deprecated": false + }, + { + "id": "capability-request:extension->extension", + "source": "capability-request", + "sourceHandle": "extension", + "target": "extension", + "kind": "extension", + "deprecated": false + }, + { + "id": "capability-response:supported-models->model-data", + "source": "capability-response", + "sourceHandle": "supported-models", + "target": "model-data", + "kind": "field", + "deprecated": false + }, + { + "id": "capability-response:supported-encodings->encoding", + "source": "capability-response", + "sourceHandle": "supported-encodings", + "target": "encoding", + "kind": "field", + "deprecated": false + }, + { + "id": "capability-response:extension->extension", + "source": "capability-response", + "sourceHandle": "extension", + "target": "extension", + "kind": "extension", + "deprecated": false + }, + { + "id": "error:data->any", + "source": "error", + "sourceHandle": "data", + "target": "any", + "kind": "field", + "deprecated": false + }, + { + "id": "update-result:path->path", + "source": "update-result", + "sourceHandle": "path", + "target": "path", + "kind": "field", + "deprecated": false + }, + { + "id": "update-result:message->error", + "source": "update-result", + "sourceHandle": "message", + "target": "error", + "kind": "field", + "deprecated": true + }, + { + "id": "update-result:op->operation", + "source": "update-result", + "sourceHandle": "op", + "target": "operation", + "kind": "field", + "deprecated": false + }, + { + "id": "subscription-list:prefix->path", + "source": "subscription-list", + "sourceHandle": "prefix", + "target": "path", + "kind": "field", + "deprecated": false + }, + { + "id": "subscription-list:subscription->subscription", + "source": "subscription-list", + "sourceHandle": "subscription", + "target": "subscription", + "kind": "field", + "deprecated": false + }, + { + "id": "subscription-list:qos->qos-marking", + "source": "subscription-list", + "sourceHandle": "qos", + "target": "qos-marking", + "kind": "field", + "deprecated": false + }, + { + "id": "subscription-list:mode->mode", + "source": "subscription-list", + "sourceHandle": "mode", + "target": "mode", + "kind": "field", + "deprecated": false + }, + { + "id": "subscription-list:use-models->model-data", + "source": "subscription-list", + "sourceHandle": "use-models", + "target": "model-data", + "kind": "field", + "deprecated": false + }, + { + "id": "subscription-list:encoding->encoding", + "source": "subscription-list", + "sourceHandle": "encoding", + "target": "encoding", + "kind": "field", + "deprecated": false + }, + { + "id": "subscription:path->path", + "source": "subscription", + "sourceHandle": "path", + "target": "path", + "kind": "field", + "deprecated": false + }, + { + "id": "subscription:mode->subscription-mode", + "source": "subscription", + "sourceHandle": "mode", + "target": "subscription-mode", + "kind": "field", + "deprecated": false + }, + { + "id": "notification:prefix->path", + "source": "notification", + "sourceHandle": "prefix", + "target": "path", + "kind": "field", + "deprecated": false + }, + { + "id": "notification:update->update", + "source": "notification", + "sourceHandle": "update", + "target": "update", + "kind": "field", + "deprecated": false + }, + { + "id": "notification:delete->path", + "source": "notification", + "sourceHandle": "delete", + "target": "path", + "kind": "field", + "deprecated": false + }, + { + "id": "update:path->path", + "source": "update", + "sourceHandle": "path", + "target": "path", + "kind": "field", + "deprecated": false + }, + { + "id": "update:value->value", + "source": "update", + "sourceHandle": "value", + "target": "value", + "kind": "field", + "deprecated": true + }, + { + "id": "update:val->typed-value", + "source": "update", + "sourceHandle": "val", + "target": "typed-value", + "kind": "field", + "deprecated": false + }, + { + "id": "path:elem->path-elem", + "source": "path", + "sourceHandle": "elem", + "target": "path-elem", + "kind": "field", + "deprecated": false + }, + { + "id": "value:type->encoding", + "source": "value", + "sourceHandle": "type", + "target": "encoding", + "kind": "field", + "deprecated": false + }, + { + "id": "typed-value:decimal-val->decimal64", + "source": "typed-value", + "sourceHandle": "decimal-val", + "target": "decimal64", + "kind": "field", + "deprecated": true + }, + { + "id": "typed-value:leaflist-val->scalar-array", + "source": "typed-value", + "sourceHandle": "leaflist-val", + "target": "scalar-array", + "kind": "field", + "deprecated": false + }, + { + "id": "typed-value:any-val->any", + "source": "typed-value", + "sourceHandle": "any-val", + "target": "any", + "kind": "field", + "deprecated": false + }, + { + "id": "scalar-array:element->typed-value", + "source": "scalar-array", + "sourceHandle": "element", + "target": "typed-value", + "kind": "field", + "deprecated": false + }, + { + "id": "extension:registered-ext->registered-extension", + "source": "extension", + "sourceHandle": "registered-ext", + "target": "registered-extension", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "extension:master-arbitration->master-arbitration", + "source": "extension", + "sourceHandle": "master-arbitration", + "target": "master-arbitration", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "extension:history->history", + "source": "extension", + "sourceHandle": "history", + "target": "history", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "extension:commit->commit", + "source": "extension", + "sourceHandle": "commit", + "target": "commit", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "extension:depth->depth", + "source": "extension", + "sourceHandle": "depth", + "target": "depth", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "extension:config-subscription->config-subscription", + "source": "extension", + "sourceHandle": "config-subscription", + "target": "config-subscription", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "registered-extension:id->extension-id", + "source": "registered-extension", + "sourceHandle": "id", + "target": "extension-id", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "master-arbitration:role->role", + "source": "master-arbitration", + "sourceHandle": "role", + "target": "role", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "master-arbitration:election-id->uint128", + "source": "master-arbitration", + "sourceHandle": "election-id", + "target": "uint128", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "history:range->time-range", + "source": "history", + "sourceHandle": "range", + "target": "time-range", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "commit:commit->commit-request", + "source": "commit", + "sourceHandle": "commit", + "target": "commit-request", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "commit:confirm->commit-confirm", + "source": "commit", + "sourceHandle": "confirm", + "target": "commit-confirm", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "commit:cancel->commit-cancel", + "source": "commit", + "sourceHandle": "cancel", + "target": "commit-cancel", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "commit:set-rollback-duration->commit-set-rollback-duration", + "source": "commit", + "sourceHandle": "set-rollback-duration", + "target": "commit-set-rollback-duration", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "commit-request:rollback-duration->duration", + "source": "commit-request", + "sourceHandle": "rollback-duration", + "target": "duration", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "commit-set-rollback-duration:rollback-duration->duration", + "source": "commit-set-rollback-duration", + "sourceHandle": "rollback-duration", + "target": "duration", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "config-subscription:start->config-subscription-start", + "source": "config-subscription", + "sourceHandle": "start", + "target": "config-subscription-start", + "kind": "extension-detail", + "deprecated": false + }, + { + "id": "config-subscription:sync-done->config-subscription-sync-done", + "source": "config-subscription", + "sourceHandle": "sync-done", + "target": "config-subscription-sync-done", + "kind": "extension-detail", + "deprecated": false + } +]; + +export const mapBounds: MapBounds = { + "width": 4260, + "height": 2250 +}; + +export function getVisibleMap({ + showDeprecated = false, + showExtensions = true, +}: VisibleMapOptions = {}): VisibleMap { + const visibleNodes = mapNodes + .filter((node) => node.data.kind !== 'legend') + .filter((node) => showDeprecated || !node.data.deprecated) + .map((node) => ({ + ...node, + data: { + ...node.data, + fields: (node.data.fields ?? []).filter((field) => showDeprecated || !field.deprecated), + }, + })); + const visibleNodeIds = new Set(visibleNodes.map((node) => node.id)); + const visibleHandles = new Set( + visibleNodes.flatMap((node) => + (node.data.fields ?? []).map((field) => `${node.id}:${field.id}`), + ), + ); + const visibleEdges = mapEdges.filter((edge) => { + if (!showExtensions && edge.kind === 'extension') { + return false; + } + if (!showDeprecated && edge.deprecated) { + return false; + } + return ( + visibleNodeIds.has(edge.source) && + visibleNodeIds.has(edge.target) && + visibleHandles.has(`${edge.source}:${edge.sourceHandle}`) + ); + }); + + return { nodes: visibleNodes, edges: visibleEdges }; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..f7b11eb --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; +import './styles.css'; + +const rootElement = document.getElementById('root'); + +if (!rootElement) { + throw new Error('Root element not found'); +} + +createRoot(rootElement).render( + + + , +); diff --git a/src/mapLayout.ts b/src/mapLayout.ts new file mode 100644 index 0000000..12ef108 --- /dev/null +++ b/src/mapLayout.ts @@ -0,0 +1,797 @@ +import ELK, { type ElkExtendedEdge, type ElkNode } from 'elkjs/lib/elk.bundled.js'; +import type { MapEdge, MapField, MapNode, VisibleMap } from './gnmiMap'; + +export const nodeHeaderHeight = 36; +export const nodeBodyPadding = 8; +export const nodeBadgeHeight = 24; +export const baseFieldRowHeight = 32; +export const detailFieldRowHeight = 52; +export const layoutGapX = 8; +export const layoutGapY = 18; + +const maxLayoutPasses = 50; +const elk = new ELK(); +const nodeSpacing = 86; +const layerSpacing = 150; +const routePadding = 18; +const routeEndpointOffset = 34; +const routeOuterMargin = 160; +const routeBendPenalty = 42; + +export type RoutePoint = { + x: number; + y: number; +}; + +export type LayoutBox = { + id: string; + x: number; + y: number; + width: number; + height: number; +}; + +export type TargetHandleLayout = { + id: string; + edgeId: string; + y: number; +}; + +export type RoutedLayoutEdge = { + edge: MapEdge; + targetHandle: string; + routePoints: RoutePoint[]; +}; + +export type ReadableLayout = { + nodes: MapNode[]; + edges: RoutedLayoutEdge[]; + bounds: LayoutBounds; +}; + +export type ManualNodePositions = Record; + +export type LayoutBounds = { + x: number; + y: number; + width: number; + height: number; +}; + +export function mapNodeWidth(node: MapNode): number { + const width = node.style?.width; + + if (typeof width === 'number') { + return width; + } + + if (typeof width === 'string') { + const parsedWidth = Number.parseFloat(width); + return Number.isFinite(parsedWidth) ? parsedWidth : 320; + } + + return 320; +} + +export function mapFieldRowHeight(field?: MapField): number { + if (!field) { + return baseFieldRowHeight; + } + + return field.group || field.badge ? detailFieldRowHeight : baseFieldRowHeight; +} + +export function sourceHandleY(node: MapNode, sourceHandle: string): number { + const fields = node.data.fields ?? []; + let y = + nodeHeaderHeight + + (node.data.badges?.length ? nodeBadgeHeight : 0) + + nodeBodyPadding; + + for (const field of fields) { + const fieldHeight = mapFieldRowHeight(field); + if (field.id === sourceHandle) { + return y + fieldHeight / 2; + } + y += fieldHeight; + } + + return estimatedMapNodeHeight(node) / 2; +} + +export function estimatedMapNodeHeight(node: MapNode): number { + const fields = node.data.fields ?? []; + const badgeHeight = node.data.badges?.length ? nodeBadgeHeight : 0; + const fieldsHeight = fields.length + ? fields.reduce((height, field) => height + mapFieldRowHeight(field), 0) + : baseFieldRowHeight; + + return nodeHeaderHeight + badgeHeight + nodeBodyPadding * 2 + fieldsHeight; +} + +function boxesOverlap(first: LayoutBox, second: LayoutBox): boolean { + return ( + first.x < second.x + second.width + layoutGapX && + first.x + first.width + layoutGapX > second.x && + first.y < second.y + second.height + layoutGapY && + first.y + first.height + layoutGapY > second.y + ); +} + +export function improveNodeLayout(nodes: MapNode[]): MapNode[] { + const boxes = new Map( + nodes.map((node) => [ + node.id, + { + id: node.id, + x: node.position.x, + y: node.position.y, + width: mapNodeWidth(node), + height: estimatedMapNodeHeight(node), + }, + ]), + ); + + for (let pass = 0; pass < maxLayoutPasses; pass += 1) { + let moved = false; + const sortedBoxes = [...boxes.values()].sort((first, second) => { + if (first.y !== second.y) { + return first.y - second.y; + } + + return first.x - second.x; + }); + + for (let index = 0; index < sortedBoxes.length; index += 1) { + const anchor = sortedBoxes[index]; + + for (let nextIndex = index + 1; nextIndex < sortedBoxes.length; nextIndex += 1) { + const candidate = sortedBoxes[nextIndex]; + + if (candidate.y >= anchor.y + anchor.height + layoutGapY) { + break; + } + + if (!boxesOverlap(anchor, candidate)) { + continue; + } + + const nextY = anchor.y + anchor.height + layoutGapY; + if (candidate.y < nextY) { + candidate.y = nextY; + moved = true; + } + } + } + + if (!moved) { + break; + } + } + + return nodes.map((node) => { + const box = boxes.get(node.id); + if (!box || (box.x === node.position.x && box.y === node.position.y)) { + return node; + } + + return { + ...node, + position: { + x: box.x, + y: box.y, + }, + }; + }); +} + +export async function computeReadableNodeLayout(nodes: MapNode[], edges: MapEdge[]): Promise { + const graph: ElkNode = { + id: 'gnmi-map', + layoutOptions: { + 'elk.algorithm': 'layered', + 'elk.direction': 'RIGHT', + 'elk.edgeRouting': 'ORTHOGONAL', + 'elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF', + 'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP', + 'elk.layered.layering.strategy': 'NETWORK_SIMPLEX', + 'elk.layered.spacing.nodeNodeBetweenLayers': `${layerSpacing}`, + 'elk.spacing.nodeNode': `${nodeSpacing}`, + 'elk.spacing.edgeEdge': '26', + 'elk.spacing.edgeNode': '42', + 'elk.padding': '[top=40,left=40,bottom=40,right=40]', + }, + children: nodes.map((node) => ({ + id: node.id, + width: mapNodeWidth(node), + height: estimatedMapNodeHeight(node), + })), + edges: edges.map((edge) => ({ + id: edge.id, + sources: [edge.source], + targets: [edge.target], + })), + }; + + const result = await elk.layout(graph); + const layoutById = new Map((result.children ?? []).map((node) => [node.id, node])); + + return improveNodeLayout( + nodes.map((node) => { + const layoutNode = layoutById.get(node.id); + if (layoutNode?.x === undefined || layoutNode.y === undefined) { + return node; + } + + return { + ...node, + position: { + x: Math.round(layoutNode.x), + y: Math.round(layoutNode.y), + }, + }; + }), + ); +} + +export async function computeReadableLayout( + visibleMap: VisibleMap, + manualPositions: ManualNodePositions = {}, +): Promise { + const layoutNodes = await computeReadableNodeLayout(visibleMap.nodes, visibleMap.edges); + return routeReadableLayout(applyManualPositions(layoutNodes, manualPositions), visibleMap.edges); +} + +export function applyManualPositions( + nodes: MapNode[], + manualPositions: ManualNodePositions, +): MapNode[] { + return nodes.map((node) => { + const manualPosition = manualPositions[node.id]; + if (!manualPosition) { + return node; + } + + return { + ...node, + position: manualPosition, + }; + }); +} + +export function routeReadableLayout(nodes: MapNode[], edges: MapEdge[]): ReadableLayout { + const bounds = mapNodesBounds(nodes); + const boxes = new Map( + nodes.map((node) => [ + node.id, + { + id: node.id, + x: node.position.x, + y: node.position.y, + width: mapNodeWidth(node), + height: estimatedMapNodeHeight(node), + }, + ]), + ); + const nodesById = new Map(nodes.map((node) => [node.id, node])); + const targetHandlesByNode = targetHandles(nodes, edges, boxes); + const targetHandleByEdge = new Map( + [...targetHandlesByNode.values()] + .flat() + .map((handle) => [handle.edgeId, handle] as const), + ); + const nodesWithTargetHandles = nodes.map((node) => ({ + ...node, + data: { + ...node.data, + targetHandles: targetHandlesByNode.get(node.id) ?? [], + }, + })); + + return { + nodes: nodesWithTargetHandles, + edges: edges.map((edge) => { + const targetHandle = targetHandleByEdge.get(edge.id); + return { + edge, + targetHandle: targetHandle?.id ?? targetHandleId(edge), + routePoints: routeEdge(edge, nodesById, boxes, bounds, targetHandle), + }; + }), + bounds, + }; +} + +export function routeIntersectsNode( + routePoints: RoutePoint[], + node: MapNode, + padding = 0, +): boolean { + const box = expandedBox( + { + id: node.id, + x: node.position.x, + y: node.position.y, + width: mapNodeWidth(node), + height: estimatedMapNodeHeight(node), + }, + padding, + ); + + return routeSegments(routePoints).some(([start, end]) => segmentIntersectsBox(start, end, box)); +} + +export function mapNodesBounds(nodes: MapNode[]): LayoutBounds { + if (!nodes.length) { + return { x: 0, y: 0, width: 0, height: 0 }; + } + + const minX = Math.min(...nodes.map((node) => node.position.x)); + const minY = Math.min(...nodes.map((node) => node.position.y)); + const maxX = Math.max(...nodes.map((node) => node.position.x + mapNodeWidth(node))); + const maxY = Math.max(...nodes.map((node) => node.position.y + estimatedMapNodeHeight(node))); + + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; +} + +function targetHandles( + nodes: MapNode[], + edges: MapEdge[], + boxes: Map, +): Map { + const nodesById = new Map(nodes.map((node) => [node.id, node])); + const incoming = new Map(); + + for (const edge of edges) { + incoming.set(edge.target, [...(incoming.get(edge.target) ?? []), edge]); + } + + const handles = new Map(); + + for (const [nodeId, nodeEdges] of incoming) { + const node = nodesById.get(nodeId); + const box = boxes.get(nodeId); + if (!node || !box) { + continue; + } + + const sortedEdges = [...nodeEdges].sort((first, second) => { + const firstSource = boxes.get(first.source); + const secondSource = boxes.get(second.source); + if (!firstSource || !secondSource) { + return first.id.localeCompare(second.id); + } + + const yDiff = boxCenterY(firstSource) - boxCenterY(secondSource); + return yDiff || firstSource.x - secondSource.x || first.id.localeCompare(second.id); + }); + const nodeHeight = estimatedMapNodeHeight(node); + const top = Math.min(nodeHeaderHeight + 18, nodeHeight / 2); + const bottom = Math.max(top + 1, nodeHeight - 18); + + handles.set( + nodeId, + sortedEdges.map((edge, index) => { + const ratio = sortedEdges.length === 1 ? 0.5 : (index + 1) / (sortedEdges.length + 1); + return { + id: targetHandleId(edge), + edgeId: edge.id, + y: Math.round(top + (bottom - top) * ratio), + }; + }), + ); + } + + return handles; +} + +function routeEdge( + edge: MapEdge, + nodesById: Map, + boxes: Map, + bounds: LayoutBounds, + targetHandle?: TargetHandleLayout, +): RoutePoint[] { + const source = nodesById.get(edge.source); + const target = nodesById.get(edge.target); + const sourceBox = boxes.get(edge.source); + const targetBox = boxes.get(edge.target); + + if (!source || !target || !sourceBox || !targetBox) { + return []; + } + + const start: RoutePoint = { + x: sourceBox.x + sourceBox.width, + y: sourceBox.y + sourceHandleY(source, edge.sourceHandle), + }; + const end: RoutePoint = { + x: targetBox.x, + y: targetBox.y + (targetHandle?.y ?? targetBox.height / 2), + }; + const routeStart = { x: start.x + routeEndpointOffset, y: start.y }; + const routeEnd = { x: end.x - routeEndpointOffset, y: end.y }; + const obstacles = [...boxes.values()].map((box) => expandedBox(box, routePadding)); + const routed = findOrthogonalRoute(routeStart, routeEnd, obstacles, bounds); + + return compactRoute([ + start, + routeStart, + ...(routed ?? fallbackRoute(routeStart, routeEnd, bounds)).slice(1, -1), + routeEnd, + end, + ]); +} + +function findOrthogonalRoute( + start: RoutePoint, + end: RoutePoint, + obstacles: LayoutBox[], + bounds: LayoutBounds, +): RoutePoint[] | null { + const xCoordinates = uniqueSortedNumbers([ + start.x, + end.x, + bounds.x - routeOuterMargin, + bounds.x + bounds.width + routeOuterMargin, + ...obstacles.flatMap((box) => [box.x - routePadding, box.x + box.width + routePadding]), + ]); + const yCoordinates = uniqueSortedNumbers([ + start.y, + end.y, + bounds.y - routeOuterMargin, + bounds.y + bounds.height + routeOuterMargin, + ...obstacles.flatMap((box) => [box.y - routePadding, box.y + box.height + routePadding]), + ]); + const points: RoutePoint[] = []; + const pointIndex = new Map(); + + for (const y of yCoordinates) { + for (const x of xCoordinates) { + const point = { x, y }; + if (obstacles.some((box) => pointInsideBox(point, box))) { + continue; + } + + pointIndex.set(pointKey(point), points.length); + points.push(point); + } + } + + const startIndex = pointIndex.get(pointKey(start)); + const endIndex = pointIndex.get(pointKey(end)); + if (startIndex === undefined || endIndex === undefined) { + return null; + } + + const adjacency = buildRouteAdjacency(points, obstacles); + const path = shortestRoute(points, adjacency, startIndex, endIndex); + return path ? compactRoute(path.map((index) => points[index])) : null; +} + +function buildRouteAdjacency(points: RoutePoint[], obstacles: LayoutBox[]): number[][] { + const adjacency = Array.from({ length: points.length }, () => [] as number[]); + const rows = new Map(); + const columns = new Map(); + + points.forEach((point, index) => { + rows.set(point.y, [...(rows.get(point.y) ?? []), index]); + columns.set(point.x, [...(columns.get(point.x) ?? []), index]); + }); + + for (const row of rows.values()) { + row.sort((first, second) => points[first].x - points[second].x); + connectVisibleNeighbors(row, adjacency, points, obstacles); + } + + for (const column of columns.values()) { + column.sort((first, second) => points[first].y - points[second].y); + connectVisibleNeighbors(column, adjacency, points, obstacles); + } + + return adjacency; +} + +function connectVisibleNeighbors( + sortedIndexes: number[], + adjacency: number[][], + points: RoutePoint[], + obstacles: LayoutBox[], +): void { + for (let index = 0; index < sortedIndexes.length - 1; index += 1) { + const first = sortedIndexes[index]; + const second = sortedIndexes[index + 1]; + if (obstacles.some((box) => segmentIntersectsBox(points[first], points[second], box))) { + continue; + } + + adjacency[first].push(second); + adjacency[second].push(first); + } +} + +function shortestRoute( + points: RoutePoint[], + adjacency: number[][], + startIndex: number, + endIndex: number, +): number[] | null { + const directions = 3; + const directionStart = 0; + const directionHorizontal = 1; + const directionVertical = 2; + const totalStates = points.length * directions; + const distances = Array.from({ length: totalStates }, () => Number.POSITIVE_INFINITY); + const previous = Array<{ state: number; pointIndex: number } | null>(totalStates).fill(null); + const heap = new RouteHeap(); + const startState = routeState(startIndex, directionStart); + distances[startState] = 0; + heap.push({ state: startState, distance: 0, priority: 0 }); + + while (heap.size) { + const current = heap.pop(); + if (!current || current.distance !== distances[current.state]) { + continue; + } + + const currentPointIndex = Math.floor(current.state / directions); + const currentDirection = current.state % directions; + if (currentPointIndex === endIndex) { + return reconstructRoute(previous, current.state); + } + + for (const nextPointIndex of adjacency[currentPointIndex]) { + const nextDirection = + points[currentPointIndex].x === points[nextPointIndex].x + ? directionVertical + : directionHorizontal; + const bendCost = + currentDirection !== directionStart && currentDirection !== nextDirection + ? routeBendPenalty + : 0; + const nextState = routeState(nextPointIndex, nextDirection); + const nextDistance = + distances[current.state] + + manhattanDistance(points[currentPointIndex], points[nextPointIndex]) + + bendCost; + + if (nextDistance >= distances[nextState]) { + continue; + } + + distances[nextState] = nextDistance; + previous[nextState] = { state: current.state, pointIndex: currentPointIndex }; + heap.push({ + state: nextState, + distance: nextDistance, + priority: nextDistance + manhattanDistance(points[nextPointIndex], points[endIndex]), + }); + } + } + + return null; +} + +function reconstructRoute( + previous: Array<{ state: number; pointIndex: number } | null>, + endState: number, +): number[] { + const directions = 3; + const path = [Math.floor(endState / directions)]; + let currentState = endState; + + while (previous[currentState]) { + const currentPrevious = previous[currentState]; + if (!currentPrevious) { + break; + } + + path.push(currentPrevious.pointIndex); + currentState = currentPrevious.state; + } + + return path.reverse(); +} + +function routeState(pointIndex: number, direction: number): number { + return pointIndex * 3 + direction; +} + +function fallbackRoute(start: RoutePoint, end: RoutePoint, bounds: LayoutBounds): RoutePoint[] { + const y = + Math.abs(start.y - (bounds.y - routeOuterMargin)) < + Math.abs(start.y - (bounds.y + bounds.height + routeOuterMargin)) + ? bounds.y + bounds.height + routeOuterMargin + : bounds.y - routeOuterMargin; + + return compactRoute([start, { x: start.x, y }, { x: end.x, y }, end]); +} + +function compactRoute(points: RoutePoint[]): RoutePoint[] { + const deduped = points.filter((point, index) => { + const previous = points[index - 1]; + return !previous || previous.x !== point.x || previous.y !== point.y; + }); + const compacted: RoutePoint[] = []; + + for (const point of deduped) { + const previous = compacted[compacted.length - 1]; + const beforePrevious = compacted[compacted.length - 2]; + if ( + previous && + beforePrevious && + ((beforePrevious.x === previous.x && previous.x === point.x) || + (beforePrevious.y === previous.y && previous.y === point.y)) + ) { + compacted[compacted.length - 1] = point; + continue; + } + + compacted.push(point); + } + + return compacted; +} + +function routeSegments(points: RoutePoint[]): Array<[RoutePoint, RoutePoint]> { + const segments: Array<[RoutePoint, RoutePoint]> = []; + for (let index = 0; index < points.length - 1; index += 1) { + segments.push([points[index], points[index + 1]]); + } + return segments; +} + +function segmentIntersectsBox(start: RoutePoint, end: RoutePoint, box: LayoutBox): boolean { + if (start.y === end.y) { + const y = start.y; + if (y <= box.y || y >= box.y + box.height) { + return false; + } + + return rangesOverlap(start.x, end.x, box.x, box.x + box.width); + } + + if (start.x === end.x) { + const x = start.x; + if (x <= box.x || x >= box.x + box.width) { + return false; + } + + return rangesOverlap(start.y, end.y, box.y, box.y + box.height); + } + + return false; +} + +function rangesOverlap(firstStart: number, firstEnd: number, secondStart: number, secondEnd: number): boolean { + const firstMin = Math.min(firstStart, firstEnd); + const firstMax = Math.max(firstStart, firstEnd); + const secondMin = Math.min(secondStart, secondEnd); + const secondMax = Math.max(secondStart, secondEnd); + return firstMin < secondMax && firstMax > secondMin; +} + +function expandedBox(box: LayoutBox, padding: number): LayoutBox { + return { + id: box.id, + x: box.x - padding, + y: box.y - padding, + width: box.width + padding * 2, + height: box.height + padding * 2, + }; +} + +function pointInsideBox(point: RoutePoint, box: LayoutBox): boolean { + return ( + point.x > box.x && + point.x < box.x + box.width && + point.y > box.y && + point.y < box.y + box.height + ); +} + +function pointKey(point: RoutePoint): string { + return `${point.x}:${point.y}`; +} + +function uniqueSortedNumbers(values: number[]): number[] { + return [...new Set(values.map((value) => Math.round(value)))].sort((first, second) => first - second); +} + +function manhattanDistance(first: RoutePoint, second: RoutePoint): number { + return Math.abs(first.x - second.x) + Math.abs(first.y - second.y); +} + +function boxCenterY(box: LayoutBox): number { + return box.y + box.height / 2; +} + +function targetHandleId(edge: MapEdge): string { + return `target-${edge.id.replace(/[^A-Za-z0-9_-]/g, '-')}`; +} + +type HeapItem = { + state: number; + distance: number; + priority: number; +}; + +class RouteHeap { + private readonly items: HeapItem[] = []; + + get size(): number { + return this.items.length; + } + + push(item: HeapItem): void { + this.items.push(item); + this.bubbleUp(this.items.length - 1); + } + + pop(): HeapItem | undefined { + if (!this.items.length) { + return undefined; + } + + const top = this.items[0]; + const last = this.items.pop(); + if (last && this.items.length) { + this.items[0] = last; + this.bubbleDown(0); + } + + return top; + } + + private bubbleUp(index: number): void { + let currentIndex = index; + while (currentIndex > 0) { + const parentIndex = Math.floor((currentIndex - 1) / 2); + if (this.items[parentIndex].priority <= this.items[currentIndex].priority) { + return; + } + + [this.items[parentIndex], this.items[currentIndex]] = [ + this.items[currentIndex], + this.items[parentIndex], + ]; + currentIndex = parentIndex; + } + } + + private bubbleDown(index: number): void { + let currentIndex = index; + while (true) { + const leftIndex = currentIndex * 2 + 1; + const rightIndex = currentIndex * 2 + 2; + let smallestIndex = currentIndex; + + if ( + leftIndex < this.items.length && + this.items[leftIndex].priority < this.items[smallestIndex].priority + ) { + smallestIndex = leftIndex; + } + if ( + rightIndex < this.items.length && + this.items[rightIndex].priority < this.items[smallestIndex].priority + ) { + smallestIndex = rightIndex; + } + if (smallestIndex === currentIndex) { + return; + } + + [this.items[currentIndex], this.items[smallestIndex]] = [ + this.items[smallestIndex], + this.items[currentIndex], + ]; + currentIndex = smallestIndex; + } + } +} diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..91d5322 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,609 @@ +:root { + --page-bg: #f4f6f8; + --panel: #ffffff; + --panel-soft: #f8fafc; + --ink: #172033; + --muted: #677486; + --line: #cfd8e3; + --blue: #0b4b8f; + --blue-soft: #d9eaf9; + --teal: #16736b; + --teal-soft: #dcefeb; + --amber: #a15c03; + --amber-soft: #fbebd3; + --gray: #4d5a6b; + --focus: #2474c9; + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { + box-sizing: border-box; +} + +html, +body, +#root { + height: 100%; + margin: 0; +} + +body { + background: var(--page-bg); + color: var(--ink); +} + +button, +input { + font: inherit; +} + +a { + color: inherit; +} + +.app-shell { + display: grid; + grid-template-rows: auto 1fr; + height: 100%; + min-width: 0; +} + +.topbar { + z-index: 5; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + min-height: 76px; + padding: 12px 18px; + border-bottom: 1px solid var(--line); + background: rgba(255, 255, 255, 0.94); + backdrop-filter: blur(10px); +} + +.brand { + min-width: 178px; +} + +.brand-kicker, +.inspector-kicker { + display: block; + color: var(--muted); + font-size: 11px; + font-weight: 700; + line-height: 1.2; + text-transform: uppercase; +} + +.brand h1, +.inspector h2 { + margin: 2px 0 0; + font-size: 21px; + line-height: 1.2; +} + +.toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + gap: 8px; + min-width: 0; +} + +.search-box { + display: flex; + align-items: center; + gap: 8px; + width: min(360px, 44vw); + min-width: 210px; + height: 38px; + padding: 0 11px; + border: 1px solid var(--line); + border-radius: 7px; + background: var(--panel); + color: var(--muted); +} + +.search-box input { + min-width: 0; + width: 100%; + border: 0; + outline: 0; + background: transparent; + color: var(--ink); +} + +.tool-button { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + height: 38px; + padding: 0 12px; + border: 1px solid var(--line); + border-radius: 7px; + background: var(--panel); + color: var(--ink); + cursor: pointer; + font-size: 14px; + font-weight: 700; + text-decoration: none; +} + +.tool-button[data-tooltip]::after { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 20; + width: max-content; + max-width: 270px; + padding: 8px 10px; + border: 1px solid #27364a; + border-radius: 6px; + background: #172033; + color: #ffffff; + content: attr(data-tooltip); + font-size: 12px; + font-weight: 700; + line-height: 1.35; + opacity: 0; + pointer-events: none; + text-align: left; + transform: translateY(-2px); + transition: + opacity 140ms ease, + transform 140ms ease; + white-space: normal; +} + +.tool-button[data-tooltip]:hover::after, +.tool-button[data-tooltip]:focus-visible::after { + opacity: 1; + transform: translateY(0); +} + +.tool-button:hover, +.tool-button:focus-visible, +.tool-button.is-active { + border-color: var(--focus); + color: var(--focus); + outline: none; +} + +.tool-button:disabled { + border-color: var(--line); + color: var(--muted); + cursor: not-allowed; + opacity: 0.56; +} + +.map-stage { + position: relative; + display: grid; + grid-template-columns: minmax(0, 1fr) 330px; + min-height: 0; +} + +.react-flow { + width: 100%; + height: 100%; + background: + linear-gradient(90deg, rgba(11, 75, 143, 0.04), transparent 36%), + linear-gradient(0deg, rgba(22, 115, 107, 0.04), transparent 42%), + var(--page-bg); +} + +.react-flow__node { + font-family: inherit; +} + +.schema-node { + overflow: hidden; + border: 1px solid #b8c5d3; + border-radius: 8px; + background: var(--panel); + box-shadow: 0 8px 22px rgba(31, 41, 55, 0.09); + color: var(--ink); + transition: + box-shadow 160ms ease, + opacity 160ms ease, + transform 160ms ease; +} + +.schema-node.is-selected { + border-color: var(--focus); + box-shadow: + 0 0 0 3px rgba(36, 116, 201, 0.16), + 0 10px 28px rgba(31, 41, 55, 0.16); +} + +.schema-node.is-edge-endpoint { + border-color: #d21f3c; + box-shadow: + 0 0 0 3px rgba(210, 31, 60, 0.16), + 0 10px 28px rgba(31, 41, 55, 0.14); +} + +.schema-node.is-dimmed { + opacity: 0.22; +} + +.node-header { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + min-height: 36px; + padding: 8px 10px; + background: var(--teal); + color: #fff; +} + +.kind-service .node-header { + background: var(--blue); +} + +.kind-rpc .node-header { + background: #1c62a0; +} + +.kind-enum .node-header { + background: var(--amber); +} + +.kind-external .node-header, +.kind-legend .node-header { + background: var(--gray); +} + +.node-kind { + padding: 2px 6px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.16); + color: rgba(255, 255, 255, 0.9); + font-size: 10px; + font-weight: 800; + line-height: 1.2; + text-transform: uppercase; +} + +.node-header strong { + overflow: hidden; + font-size: 14px; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +.node-links { + display: inline-flex; + align-items: center; + gap: 5px; +} + +.node-links a { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: rgba(255, 255, 255, 0.14); +} + +.node-links a:hover, +.node-links a:focus-visible { + background: rgba(255, 255, 255, 0.25); + outline: none; +} + +.node-badges { + display: flex; + gap: 6px; + padding: 7px 10px 0; +} + +.node-badges span, +.field-badge, +.field-group { + display: inline-flex; + align-items: center; + min-height: 18px; + padding: 2px 6px; + border-radius: 999px; + font-size: 10px; + font-weight: 800; + line-height: 1; + text-transform: uppercase; +} + +.node-badges span, +.badge-deprecated { + background: #fee2df; + color: #9b1c15; +} + +.badge-optional { + background: var(--amber-soft); + color: #744200; +} + +.badge-reserved { + background: #e7ebf0; + color: #4d5a6b; +} + +.field-group { + background: var(--blue-soft); + color: #164675; +} + +.node-body { + padding: 8px; +} + +.field-row, +.empty-field { + position: relative; + display: grid; + grid-template-columns: minmax(86px, 0.9fr) minmax(84px, 1fr); + align-items: center; + column-gap: 8px; + row-gap: 5px; + min-height: 30px; + padding: 6px 9px; + border-radius: 6px; + font-size: 12px; + line-height: 1.25; +} + +.field-row:nth-child(odd) { + background: var(--panel-soft); +} + +.field-row.has-ref { + padding-right: 18px; +} + +.field-row.is-highlighted { + background: #fff5c7; + box-shadow: inset 0 0 0 1px #ecd676; +} + +.field-row.is-clickable { + cursor: pointer; +} + +.field-row.is-clickable:hover, +.field-row.is-clickable:focus-visible { + background: #edf6ff; + box-shadow: inset 0 0 0 1px rgba(36, 116, 201, 0.34); + outline: none; +} + +.field-row.is-edge-highlighted { + background: #ffe4e8; + box-shadow: inset 0 0 0 1px rgba(210, 31, 60, 0.44); +} + +.field-row.is-deprecated .field-type, +.field-row.is-deprecated .field-name { + text-decoration: line-through; + text-decoration-color: rgba(155, 28, 21, 0.55); +} + +.field-type { + overflow-wrap: anywhere; + color: #526071; + font-weight: 700; +} + +.field-name { + overflow-wrap: anywhere; + font-family: + "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + font-weight: 700; +} + +.field-group, +.field-badge { + grid-column: span 1; + justify-self: start; +} + +.empty-field { + display: flex; + color: var(--muted); + font-style: italic; +} + +.react-flow__handle { + width: 8px; + height: 8px; + border: 2px solid #fff; + background: #2d6f97; +} + +.node-target { + left: -5px; + opacity: 0; +} + +.field-handle { + right: -12px; + background: #2d6f97; +} + +.kind-enum .field-handle, +.kind-enum .react-flow__handle { + background: var(--amber); +} + +.flow-edge-rpc .react-flow__edge-path { + filter: drop-shadow(0 1px 1px rgba(11, 75, 143, 0.16)); +} + +.flow-edge .react-flow__edge-path { + transition: + opacity 160ms ease, + stroke 160ms ease, + stroke-width 160ms ease; +} + +.flow-edge-halo { + fill: none; + opacity: 0.72; + pointer-events: none; + stroke: #ffffff; + stroke-linecap: round; + stroke-linejoin: round; +} + +.flow-edge-bridge, +.flow-edge-bridge-gap { + fill: none; + pointer-events: none; + stroke-linecap: round; + stroke-linejoin: round; +} + +.flow-edge-bridge-gap { + stroke: var(--page-bg); + stroke-width: 8px; +} + +.flow-edge-bridge { + stroke-dasharray: none; +} + +.flow-edge.is-selected .react-flow__edge-path { + filter: drop-shadow(0 2px 3px rgba(210, 31, 60, 0.28)); +} + +.flow-edge.is-dimmed .react-flow__edge-path { + pointer-events: stroke; +} + +.react-flow__controls { + overflow: hidden; + border: 1px solid var(--line); + border-radius: 8px; + box-shadow: 0 8px 20px rgba(31, 41, 55, 0.12); +} + +.react-flow__controls-button { + border-bottom-color: var(--line); +} + +.inspector { + position: relative; + z-index: 4; + width: auto; + max-height: none; + overflow: auto; + padding: 16px; + border: 0; + border-left: 1px solid var(--line); + border-radius: 0; + background: rgba(255, 255, 255, 0.96); + box-shadow: -8px 0 20px rgba(31, 41, 55, 0.07); +} + +.inspector p { + margin: 10px 0 0; + color: var(--muted); + font-size: 14px; + line-height: 1.45; +} + +.inspector-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +.inspector-actions a { + display: inline-flex; + align-items: center; + gap: 7px; + min-height: 32px; + padding: 6px 9px; + border: 1px solid var(--line); + border-radius: 7px; + color: var(--blue); + font-size: 13px; + font-weight: 800; + text-decoration: none; +} + +.inspector-actions a:hover, +.inspector-actions a:focus-visible { + border-color: var(--blue); + outline: none; +} + +.inspector-fields { + display: grid; + gap: 6px; + margin-top: 14px; +} + +.inspector-field { + display: grid; + grid-template-columns: minmax(72px, 0.8fr) minmax(96px, 1fr); + gap: 8px; + padding: 7px 8px; + border-radius: 6px; + background: var(--panel-soft); + font-size: 12px; +} + +.inspector-field span { + overflow-wrap: anywhere; + color: var(--muted); + font-weight: 700; +} + +.inspector-field strong { + overflow-wrap: anywhere; + font-family: + "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; +} + +@media (max-width: 760px) { + .topbar { + align-items: stretch; + flex-direction: column; + gap: 10px; + } + + .toolbar { + justify-content: flex-start; + } + + .search-box { + width: 100%; + } + + .tool-button { + flex: 1 1 120px; + } + + .map-stage { + grid-template-columns: 1fr; + grid-template-rows: minmax(420px, 1fr) auto; + } + + .inspector { + position: relative; + width: auto; + max-height: 34vh; + border-top: 1px solid var(--line); + border-left: 0; + box-shadow: 0 -8px 20px rgba(31, 41, 55, 0.08); + } +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..2ab5f4e --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,4 @@ +/// + +declare module '*.css'; +declare module '@xyflow/react/dist/style.css'; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..de6c207 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["node", "react", "react-dom"] + }, + "include": ["src", "scripts", "vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..71bf3c2 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +function normalizeBasePath(basePath: string | undefined): string { + if (!basePath) { + return '/'; + } + + const withLeadingSlash = /^[a-z]+:\/\//i.test(basePath) || basePath.startsWith('/') + ? basePath + : `/${basePath}`; + + return withLeadingSlash.endsWith('/') ? withLeadingSlash : `${withLeadingSlash}/`; +} + +export default defineConfig({ + base: normalizeBasePath(process.env.BASE_PATH), + plugins: [react()], +});