mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 07:35:24 +00:00
release-2.9.0 (#1532)
* fix(macos): skip single-instance lock in Mac App Store builds (#1454)
fix(macos): skip single-instance lock in Mac App Store builds
* Fix/backend optional sharp (#1455)
Fix/backend optional sharp
* feat: issue #1218 (#1218)
https://github.com/Termix-SSH/Support/issues/1218
* feat: Add option to use saved global custom theme under Connection Defaults (#1209)
https://github.com/Termix-SSH/Support/issues/1209
* feat: Add Fleet Sharing functionality to the UI (#1228)
https://github.com/Termix-SSH/Support/issues/1228
* feat: Add a list view to the Docker management page (#1237)
https://github.com/Termix-SSH/Support/issues/1237
* feat: issue #1264 (#1264)
https://github.com/Termix-SSH/Support/issues/1264
* feat: 2FA With FortiToken (#1288)
https://github.com/Termix-SSH/Support/issues/1288
* feat: Allow to disable showing paths to folders (#1274)
https://github.com/Termix-SSH/Support/issues/1274
* feat: Sync Network Graph between Desktop and Remote Server (#1245)
https://github.com/Termix-SSH/Support/issues/1245
* feat: Version Number on offline servers (#1291)
https://github.com/Termix-SSH/Support/issues/1291
* feat: Support OrbStack Docker socket path on macOS / Fix "Docker is not installed" on mac... (#1302)
https://github.com/Termix-SSH/Support/issues/1302
* feat: Sidebar host click should focus existing tab instead of opening a new connection (#1289)
https://github.com/Termix-SSH/Support/issues/1289
* fix: Search in side bar shows overlapping hosts when grouped by tags (#1303)
https://github.com/Termix-SSH/Support/issues/1303
* fix: I can't send a file on its own. (#1304)
https://github.com/Termix-SSH/Support/issues/1304
* fix: sudo password not autofilling (#1248)
https://github.com/Termix-SSH/Support/issues/1248
* fix: node-MainThread (#1300)
https://github.com/Termix-SSH/Support/issues/1300
* fix: Tunnel authentication with shared credentials (#1295)
https://github.com/Termix-SSH/Support/issues/1295
* fix: host key updates not syncing, metrics 404 race on first connect
Host key writes never bumped updatedAt so sync never picked them up.
Also retry the first metrics fetch briefly instead of failing right away.
* chore: increment ver
* feat: add category and icon fields to plugin manifest schema
* ci(deps): bump the github-actions group with 2 updates (#1458)
Bumps the github-actions group with 2 updates: [crowdin/github-action](https://github.com/crowdin/github-action) and [actions/github-script](https://github.com/actions/github-script).
Updates `crowdin/github-action` from 2 to 3
- [Release notes](https://github.com/crowdin/github-action/releases)
- [Commits](https://github.com/crowdin/github-action/compare/v2...v3)
Updates `actions/github-script` from 7 to 9
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v9)
---
updated-dependencies:
- dependency-name: crowdin/github-action
dependency-version: '3'
dependency-type: direct:production
update-type: version-update:semver-major
dependency-group: github-actions
- dependency-name: actions/github-script
dependency-version: '9'
dependency-type: direct:production
update-type: version-update:semver-major
dependency-group: github-actions
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps-dev): bump the dev-patch-updates group with 13 updates (#1459)
Bumps the dev-patch-updates group with 13 updates:
| Package | From | To |
| --- | --- | --- |
| [@codemirror/commands](https://github.com/codemirror/commands) | `6.11.0` | `6.11.1` |
| [@codemirror/search](https://github.com/codemirror/search) | `6.7.1` | `6.7.2` |
| [@codemirror/view](https://github.com/codemirror/view) | `6.43.9` | `6.43.12` |
| [@testing-library/dom](https://github.com/testing-library/dom-testing-library) | `10.4.1` | `10.4.2` |
| [@testing-library/react](https://github.com/testing-library/react-testing-library) | `16.3.2` | `16.3.3` |
| [@testing-library/user-event](https://github.com/testing-library/user-event) | `14.6.5` | `14.6.7` |
| [@types/ssh2](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/ssh2) | `1.15.5` | `1.15.6` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.1.0` | `6.1.1` |
| [cytoscape](https://github.com/cytoscape/cytoscape.js) | `3.34.1` | `3.34.3` |
| [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.4` | `0.5.7` |
| [i18next](https://github.com/i18next/i18next) | `26.4.0` | `26.4.2` |
| [prettier](https://github.com/prettier/prettier) | `3.9.6` | `3.9.8` |
| [react-i18next](https://github.com/i18next/react-i18next) | `17.0.12` | `17.0.14` |
Updates `@codemirror/commands` from 6.11.0 to 6.11.1
- [Changelog](https://github.com/codemirror/commands/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/commands/commits)
Updates `@codemirror/search` from 6.7.1 to 6.7.2
- [Changelog](https://github.com/codemirror/search/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/search/commits)
Updates `@codemirror/view` from 6.43.9 to 6.43.12
- [Changelog](https://github.com/codemirror/view/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/view/commits)
Updates `@testing-library/dom` from 10.4.1 to 10.4.2
- [Release notes](https://github.com/testing-library/dom-testing-library/releases)
- [Changelog](https://github.com/testing-library/dom-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/dom-testing-library/compare/v10.4.1...v10.4.2)
Updates `@testing-library/react` from 16.3.2 to 16.3.3
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v16.3.2...v16.3.3)
Updates `@testing-library/user-event` from 14.6.5 to 14.6.7
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v14.6.5...v14.6.7)
Updates `@types/ssh2` from 1.15.5 to 1.15.6
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/ssh2)
Updates `@vitejs/plugin-react` from 6.1.0 to 6.1.1
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.1.1/packages/plugin-react)
Updates `cytoscape` from 3.34.1 to 3.34.3
- [Release notes](https://github.com/cytoscape/cytoscape.js/releases)
- [Commits](https://github.com/cytoscape/cytoscape.js/compare/v3.34.1...v3.34.3)
Updates `eslint-plugin-react-refresh` from 0.5.4 to 0.5.7
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.5.4...v0.5.7)
Updates `i18next` from 26.4.0 to 26.4.2
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.4.0...v26.4.2)
Updates `prettier` from 3.9.6 to 3.9.8
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.9.6...3.9.8)
Updates `react-i18next` from 17.0.12 to 17.0.14
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.0.12...v17.0.14)
---
updated-dependencies:
- dependency-name: "@codemirror/commands"
dependency-version: 6.11.1
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: "@codemirror/search"
dependency-version: 6.7.2
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: "@codemirror/view"
dependency-version: 6.43.12
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: "@testing-library/dom"
dependency-version: 10.4.2
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: "@testing-library/react"
dependency-version: 16.3.3
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: "@testing-library/user-event"
dependency-version: 14.6.7
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: "@types/ssh2"
dependency-version: 1.15.6
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: "@vitejs/plugin-react"
dependency-version: 6.1.1
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: cytoscape
dependency-version: 3.34.3
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: eslint-plugin-react-refresh
dependency-version: 0.5.7
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: i18next
dependency-version: 26.4.2
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: prettier
dependency-version: 3.9.8
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
- dependency-name: react-i18next
dependency-version: 17.0.14
dependency-type: direct:development
update-type: version-update:semver-patch
dependency-group: dev-patch-updates
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps): bump the prod-patch-updates group with 6 updates (#1461)
Bumps the prod-patch-updates group with 6 updates:
| Package | From | To |
| --- | --- | --- |
| [@tanstack/react-virtual](https://github.com/TanStack/virtual/tree/HEAD/packages/react-virtual) | `3.14.10` | `3.14.13` |
| [compression](https://github.com/expressjs/compression) | `1.8.1` | `1.8.2` |
| [jose](https://github.com/panva/jose) | `6.2.9` | `6.2.12` |
| [jszip](https://github.com/Stuk/jszip) | `3.10.1` | `3.10.2` |
| [socks](https://github.com/JoshGlazebrook/socks) | `2.8.9` | `2.8.10` |
| [undici](https://github.com/nodejs/undici) | `8.10.0` | `8.10.2` |
Updates `@tanstack/react-virtual` from 3.14.10 to 3.14.13
- [Release notes](https://github.com/TanStack/virtual/releases)
- [Changelog](https://github.com/TanStack/virtual/blob/main/packages/react-virtual/CHANGELOG.md)
- [Commits](https://github.com/TanStack/virtual/commits/@tanstack/react-virtual@3.14.13/packages/react-virtual)
Updates `compression` from 1.8.1 to 1.8.2
- [Release notes](https://github.com/expressjs/compression/releases)
- [Changelog](https://github.com/expressjs/compression/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/compression/compare/v1.8.1...v1.8.2)
Updates `jose` from 6.2.9 to 6.2.12
- [Release notes](https://github.com/panva/jose/releases)
- [Changelog](https://github.com/panva/jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/panva/jose/compare/v6.2.9...v6.2.12)
Updates `jszip` from 3.10.1 to 3.10.2
- [Changelog](https://github.com/Stuk/jszip/blob/main/CHANGES.md)
- [Commits](https://github.com/Stuk/jszip/compare/v3.10.1...v3.10.2)
Updates `socks` from 2.8.9 to 2.8.10
- [Release notes](https://github.com/JoshGlazebrook/socks/releases)
- [Commits](https://github.com/JoshGlazebrook/socks/compare/2.8.9...2.8.10)
Updates `undici` from 8.10.0 to 8.10.2
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v8.10.0...v8.10.2)
---
updated-dependencies:
- dependency-name: "@tanstack/react-virtual"
dependency-version: 3.14.13
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: prod-patch-updates
- dependency-name: compression
dependency-version: 1.8.2
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: prod-patch-updates
- dependency-name: jose
dependency-version: 6.2.12
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: prod-patch-updates
- dependency-name: jszip
dependency-version: 3.10.2
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: prod-patch-updates
- dependency-name: socks
dependency-version: 2.8.10
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: prod-patch-updates
- dependency-name: undici
dependency-version: 8.10.2
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: prod-patch-updates
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* feat: add plugin worker protocol, bootstrap and plugin_storage table
Worker-side ctx proxy plus the message envelope it talks over, and the
per-plugin key/value table behind ctx.storage with migrations for all
three dialects. Rows cascade with the plugin so uninstalling leaves
nothing behind.
* feat: add allowlisted host view for plugins
An allowlist rather than a list of fields to strip. A blocklist rots:
the moment a new secret-bearing column lands on SSHHost it starts
leaking silently. With an allowlist a new field stays invisible to
plugins until someone adds it here on purpose.
* feat: add plugin loader with manifest validation and crash restart
Validates an unpacked plugin, spawns a worker_threads worker and manages
activate/deactivate/crash-restart with backoff, disabling after three
tries.
The crash counter clears only after a plugin stays up past a stability
window, not on activation. Clearing it on activation let a plugin that
activates cleanly and then dies restart forever, because every attempt
looked like the first.
loader.ts documents what the worker boundary does and does not buy you.
Node's permission model is process-wide, not per-worker, so it cannot be
enabled for plugin workers alone and is not claimed here.
* feat: route internal events through a shared plugin event bus
Formalises three ad-hoc publish paths that already existed:
automation-events.ts, metrics/automation-bridge.ts and the
hostSessionStatus singleton, now the host.session.status topic.
Both invariants the old code relied on are preserved. Publishing stays
fire-and-forget, so a failing subscriber cannot disturb a metrics poll or
a host delete. The automations engine subscribes to the bus at scheduler
start instead of being imported ad hoc, which keeps the edge
one-directional: automations may import repositories, hosts modules must
not import automations.
* feat: add plugin ctx broker with permission gate and audit
Every ctx call a plugin makes is checked against its granted
capabilities, performed by the main thread, then audited. A capability
must be both declared in the manifest and granted in the database, so
widening a plugin's reach always needs a new manifest the user can see,
never just a database row.
Audit attribution is set by the broker, not the plugin. The plugin
supplies only the details, so it cannot forge the actor.
fix: stop plugin workers seeing plaintext credentials in error messages
A failing ctx.ssh.connect handed the worker the raw error from deep in
the SSH stack, which embeds the connect config. The worker received, in
full:
Authentication failed for root using password <SECRET>
where <SECRET> was the host's real plaintext password. Any plugin with
ssh.exec could read it by catching the error, defeating the whole point
of the handle-based ssh API.
Errors crossing the boundary are now sanitised. Only PluginFacingError
and PluginPermissionError, both authored here, reach a plugin verbatim;
anything else becomes a generic message and the real one goes to the
log. This uses a marker class rather than matching on message text,
because matching would start leaking again the moment an upstream error
happened to contain a familiar phrase.
Covered by credential-isolation.test.ts, which plants a sentinel in
every secret-bearing field and asserts it appears in no message the
worker ever receives.
* feat: dispatch plugin HTTP routes and wire the runtime into startup
ctx.http.route now registers a real router on the /plugin-api/:pluginId
dispatcher, replacing its stub 404. A plugin never touches the Express
req/res: it gets a plain summary and returns a plain object, so sockets
and session cookies stay on this side of the boundary. Authorization and
cookie headers are not forwarded.
Plugins load last in the start-up sequence, after the server is fully
wired, and are terminated before the database on shutdown so none can
outlive it. A plugin that fails to load cannot stop the backend.
fix: correct the nginx plugin-api location regex
The pattern was ^/plugin-api/(/.*)?$, which needs a double slash and so
never matched /plugin-api/<id>/<route>. Every other block in the file
uses ^/prefix(/.*)?$. Left alone, plugin routes would work in dev and
404 only behind Docker.
* fix: update stale SFTP transfer test for single file destination paths
The test still asserted transferToHost was called with the destination
directory "/srv". Commit 7727fa09 (#1304) deliberately changed a single
file transfer to send the full destination path including the filename,
so the correct expectation is "/srv/remote-1.txt". The code was right
and the test was left behind.
* feat: ship the ssh terminal as a first-party in-process plugin
Adds a hardcoded process:transport-owner tier so the terminal can own its
WebSocket server and ssh2 clients, which cannot cross the worker boundary.
* feat: ship docker as a first-party in-process plugin
Moves docker backend and frontend into plugins/docker/, alongside
ssh-terminal. Adds a host editor tab seam, a dashboard card registry,
and a build step to compile plugin backends into dist.
* feat: ship host metrics as a first-party in-process plugin
* fix: harden plugin auth and add per-request identity for ctx calls
Require auth on /plugin-api, gate plugin enable/grant/revoke routes
behind admin.plugins.manage, and thread the calling user through
worker ctx.hosts/ctx.ssh calls instead of always using the install
owner. Also adds the plugin permissions grant/revoke UI.
* feat: ship the AI assistant as a first-party in-process plugin
Moves src/backend/ai and its routes into plugins/ai, mounted through a
small /ai dispatcher instead of its own port. Moves the ai-enabled and
ai-private-endpoints settings routes out of user-settings-routes.ts
into the plugin too, so it has no leftover core dependents.
* fix: remove compiled plugin JS left behind in plugins/ after build
copy-bundled-plugins.cjs compiled and rewrote plugin backend TS in
place, copied it to dist/plugins, but never deleted the .js it had
just emitted next to the .ts sources. Those files shadowed the real
source for vitest/tsx and had import paths rewritten for the dist
layout, so tests failed with "Cannot find module" once a local build
had run. Affects docker, host-metrics and ai, not just this one.
* feat: add a permission-gated backend service registry for plugins
* feat: add a permission-gated UI action registry and action slots for plugins
Moves the terminal toolbar's hardwired AI button into a slot the ssh-terminal
plugin offers and the ai plugin fills, gated on ai.services.use.
* feat: add cross-plugin secret references gated by the provider's RBAC permission
* feat: move proxmox discovery/import/sync into a first-party plugin
* feat: move remote desktop (RDP/VNC/Telnet) into a first-party plugin
Relocates guacamole backend/frontend source and tests into
plugins/remote-desktop, following the ssh-terminal/docker pattern.
* feat: wire remote-desktop plugin into core and rename i18n keys
Repoints core consumers (dispatcher, collab, session-sharing, tab
utils) at the plugin, adds isTabTypeAvailable gating for rdp/vnc/
telnet, widens hostCapability to accept an array, and renames the
guacamole i18n namespace to remoteDesktop.
* feat: move fleets into a first-party plugin
* feat: move automations into a first-party plugin
* feat: remove legacy alert rules, superseded by automations
Alert rules already auto-migrate into automations on boot and the old
engine stands down after. Deletes the rule/firing tables, routes, UI,
and AI tools; keeps notification channels since automations still use
them. Also fixes automations' notify step, which still called the
deleted repository.
* feat: move network topology into a first-party plugin
* feat: move tailscale device discovery and settings into a plugin
Adds plugins/tailscale/ with a devices sidebar tab, admin settings
section, and re-auth dialog. Host Metrics tailscale manager card and
SSH re-auth banner parsing stay in core.
* feat: move workspaces into a first-party plugin
* feat: move web endpoint into a first-party plugin
* feat: collapse plugin runtime to one tier behind a plugin SDK
* feat: make each plugin an npm workspace built by a termix-plugin CLI
* feat: give plugins their own tables, migrations and sync entities
* feat: serve plugin http and websockets from one mount instead of ports
* feat: namespace plugin permissions and keep them valid when a plugin is off
* feat: give plugins their own settings and one settings screen
* feat: load plugin frontends at runtime and remove plugin ids from the shell
* fix: mock the ai status fetch in its activate test
* feat: add auth extension points and one ssh connect pipeline
* feat: rebuild workspaces as the reference plugin
* fix: gate second factors after SSO/LDAP behind an admin setting
* feat: convert network-topology to a full plugin (B1)
* feat: move snippets backend and tables into a plugin (B2)
* feat: point core frontend snippets pickers at the plugin (B2)
* feat: add the snippets plugin (B2)
* feat: wire ai, automations, fleets and ssh-terminal to snippets (B2)
* feat: finish the tailscale plugin, including SSH auth (B3)
* feat: finish the fleets plugin, add ctx.hosts and host-commands SDK (B4)
* chore: update proxmox and host-metrics locale files for B5
* chore: regenerate drizzle migration journals for B5
* feat: add ctx.hosts create/update to the SDK, host-metrics drops proxmox
* feat: move proxmox node stats and collectors into the proxmox plugin
* feat: drop proxmox host columns, use plugin settings instead
* feat: finish the fleets plugin, add ctx.hosts and host-commands SDK (B4)
* feat: convert file manager into a plugin
* feat: convert ssh tunnels into a plugin
* feat: finish the web-endpoint plugin, add ctx.desktop (B8)
* feat: move the terminal fully into the ssh-terminal plugin (B9)
* feat: convert tmux monitoring into a plugin (B10)
* feat: convert serial console into the serial plugin
* feat: move session sharing and collab rooms into a plugin (B12)
* feat: turn session recording into a plugin (B13)
* feat: finish the remote-desktop plugin (B14)
* feat: finish the host-metrics plugin, move host status into core (B16)
* feat: finish the docker plugin (B15)
* feat: finish the automations plugin (B17)
* feat: finish the ai plugin (B18)
* feat: finish the homepage plugin, move service links and canvas out of core (B19)
* feat: convert wake-on-lan into a plugin (B20)
* feat: convert secret-sources into a plugin (B20)
* feat: move totp and passkeys into plugins (C1)
* feat: move sso and ldap logins into plugins (C2)
* feat: move opkssh and warpgate into plugins (C3)
* feat: move step-ca into a plugin (C4)
* feat: move vault ssh signing into a plugin (C5)
* feat: move termix identity into a plugin (C6)
* feat: move acme certificate automation into a plugin (C7)
* fix: finish 2.9.0 plugin moves and get every check green
* chore: lock the plugin boundary and remove 2.9.0 leftovers
* fix: harden the plugin runtime after the 2.9.0 security review
* feat: add plugin packing, signing, pinned trust and tmxplug bundling
* test: prove 2.8 to 2.9.0 upgrade is lossless and fix what it caught
* fix: give bundled plugin backends a real require for CJS deps
* feat: improve UI loading time and jitter
* feat: show credential sharing inline in the sidebar like host sharing
* feat: tailscale add host and copy ip, fix outbound fetch hang and plugin ESM warning
* feat: unify alerts into one plugin, channels, rules and inbox
* feat: rebuild desktop sync as linked server accounts, fix packaged plugin bundles and UI scaling
* fix: set blacksmith builder cache-key to stop broken gha cache fallback
* fix: link plugin-sdk workspace before npm ci in docker deps stage
* fix: link every plugin workspace before npm ci in docker deps stage
* fix(host-metrics): exclude image and firmware filesystems (#1469)
* fix(host-metrics): exclude image and firmware filesystems
* fix(ci): build plugin SDK before dependent checks
* fix(mysql): keep plugin settings scope indexes within limits
* test(mysql): refresh reviewed plugin upgrade SQL
* fix(hosts): normalize imported jump host references (#1470)
* fix(hosts): normalize imported jump host references
* fix(ci): build plugin SDK before dependent checks
* fix(mysql): keep plugin settings scope indexes within limits
* test(mysql): refresh reviewed plugin upgrade SQL
* fix(ci): build plugin SDK and repair MySQL settings migration (#1471)
* fix(ci): build plugin SDK before dependent checks
* fix(mysql): keep plugin settings scope indexes within limits
* test(mysql): refresh reviewed plugin upgrade SQL
* fix(file-manager): keep inline rename consistent (#1476)
* fix(ci): build plugin SDK before dependent checks
* fix(mysql): keep plugin settings scope indexes within limits
* test(mysql): refresh reviewed plugin upgrade SQL
* fix(file-manager): keep inline rename consistent
* fix(dashboard): reuse translated host status labels (#1475)
* fix(ci): build plugin SDK before dependent checks
* fix(mysql): keep plugin settings scope indexes within limits
* test(mysql): refresh reviewed plugin upgrade SQL
* fix(dashboard): reuse translated host status labels
* docs(api): clarify local and remote version fields (#1474)
* fix(ci): build plugin SDK before dependent checks
* fix(mysql): keep plugin settings scope indexes within limits
* test(mysql): refresh reviewed plugin upgrade SQL
* docs(api): clarify local and remote version fields
* fix(file-manager): preserve selected backgrounds on hover (#1473)
* fix(ci): build plugin SDK before dependent checks
* fix(mysql): keep plugin settings scope indexes within limits
* test(mysql): refresh reviewed plugin upgrade SQL
* fix(file-manager): preserve selection feedback on hover
* test(ui): await dialog focus cleanup before teardown
* fix(file-manager): restore Ctrl/Cmd+F file search (#1472)
* fix(ci): build plugin SDK before dependent checks
* fix(mysql): keep plugin settings scope indexes within limits
* test(mysql): refresh reviewed plugin upgrade SQL
* fix(file-manager): focus file search with Ctrl or Cmd F
* fix: keep rounded-full circular in production builds
* fix(file-manager): cancel uploads and stop remote writes (#1477)
* fix(file-manager): cancel uploads and stop remote writes
* test(css): exercise the configured PostCSS plugin chain
* fix(file-manager): apply compact density to directory tree (#1478)
* fix(file-manager): apply compact density to directory tree
* test(css): exercise the configured PostCSS plugin chain
* fix(file-manager): preserve font sizes in compact mode (#1480)
* fix(file-manager): preserve font sizes in compact mode
* test(css): exercise the configured PostCSS plugin chain
* fix(file-manager): prioritize file names in narrow lists (#1483)
* fix(file-manager): prioritize file names in narrow lists
* test(css): exercise the configured PostCSS plugin chain
* test(identity): preserve full Ed25519 public keys in fixtures
* test(remote-desktop): always mutate ciphertext in tamper test
* fix(remote-desktop): probe the saved host guacd endpoint (#1481)
* fix(remote-desktop): probe the saved host guacd endpoint
* test(css): exercise the configured PostCSS plugin chain
* test(identity): preserve full Ed25519 public keys in fixtures
* fix(homepage): check allowlisted private service reachability (#1479)
* fix(homepage): check allowlisted private service reachability
* test(css): exercise the configured PostCSS plugin chain
* fix(remote-desktop): stop unsolicited clipboard read prompts (#1482)
* fix(remote-desktop): read clipboard only on explicit paste
* test(css): exercise the configured PostCSS plugin chain
* test(identity): preserve full Ed25519 public keys in fixtures
* test(remote-desktop): always mutate ciphertext in tamper test
* chore: simplify plugin readmes, changelogs and manifest descriptions
* fix: keep plugins loaded after signing in from the login screen
* fix(file-manager): accept OS file drops over existing rows (#1484)
* fix(file-manager): accept OS file drops over existing rows
* test(file-manager): target the file label in drop tests
* test(css): exercise the configured PostCSS plugin chain
* test(identity): preserve full Ed25519 public keys in fixtures
* test(remote-desktop): always mutate ciphertext in tamper test
* fix(file-manager): pipeline SFTP download reads (#1485)
* feat: add plugin sdk build to beta release
* fix: make link-store secret check immune to random base64 matches
* fix(ldap): preserve the directory-provided bind DN (#1486)
* fix: compute beta changes across dev branches and move the beta tag
* chore: untrack plugin sdk ARCHITECTURE.md
* chore: remove references to plugin sdk ARCHITECTURE.md
* feat: merge host editor terminal tabs into one owned by ssh-terminal
* feat: add descriptions to plugin host enable toggles
* fix(file-manager): show download progress in file viewers (#1487)
* fix(electron): signal backend readiness over IPC (#1488)
* fix(split-screen): allow layout creation without randomUUID (#1490)
* fix(split-screen): allow layout creation without randomUUID
* test(credentials): avoid flaky Ed25519 key generation
* fix(sidebar): remeasure visible rows after density changes (#1489)
* fix(sidebar): remeasure visible rows after density changes
* test(credentials): avoid flaky Ed25519 key generation
* feat(split-screen): redesign split screen with free-form panes and pane picker
* feat(sidebar): add host click behavior setting, default to always opening a new tab
* feat(snippets): redesign panel to match the host list
* fix(session-recording): stop session logs panel from refetching in a loop
* feat(snippets): bring back sharing, target hosts and drag reorder
* feat(ssh-terminal): add toolbar position, start state, display and fade host settings
* feat(ssh-terminal): add host setting to turn off password prompt auto-fill
* fix(ai): stop streams getting cut off and keep tool steps across messages
* feat: improve quick connect system using plugin api
* fix: proxmox guest sync, startup snippet sync, drop dead cloudflare ssh transport
* feat: move terminal settings, look and ssh tools into ssh-terminal plugin
* feat: move snippets, keybinding actions and macros out of core into plugins
* feat: store rdp, vnc and telnet logins in a generic per-protocol table
* feat: type the plugin host record, move plugin strings out of core and add lint checks
* feat: replace core homepage widget registry with generic extension points
* feat: plugins only see their own host settings in the browser
* feat: harden plugin api for separate repos, signed updates and generic external login
* fix: keep plugin css below core utilities and version plugin assets by their css
* fix: load and save plugin host settings in the editor and tighten settings row spacing
* feat: redesign user host defaults system
* fix(remote-desktop): release held keys on focus loss (#1498)
* fix(session-sharing): replace local presentation after takeover (#1500)
* fix(file-manager): refresh disk usage with directory listing (#1503)
* fix(ssh-terminal): preserve input order during local echo reconciliation (#1505)
* feat(remote-desktop): expose display zoom controls for RDP (#1514)
* fix(file-manager): restore text selection and copying in Markdown previews (#1518)
* fix(sidebar): keep compact host addresses visible (#1524)
* fix(hosts): preserve plugin settings when reopening the editor (#1499)
* fix(ssh-terminal): close owner sockets when presentations unmount (#1501)
* fix(session-sharing): synchronize shared SSH terminal dimensions (#1502)
* fix(session-sharing): show participant presence in member terminals (#1504)
* fix(ai): sync provider configuration and keys across linked devices (#1506)
* feat(ssh-terminal): add a duplicate tab action (#1510)
* feat(file-manager): allow disabling move-to-trash confirmation (#1511)
* feat(file-manager): default to the remote login directory (#1512)
* feat(homepage): synchronize canvas layouts across devices (#1515)
* fix(shortcuts): prevent accidental double-Shift palette activation (#1519)
* feat(tmux-monitor): add a unified multi-host view (#1520)
* fix(electron): allow explicit quit with active connections (#1522)
* feat: reconnect all disconnected terminal tabs (#1523)
* fix(hosts): preserve recording and sharing settings in the dev editor (#1509)
* fix(database): preserve RDP domain during startup backfill (#1497)
* feat(sidebar): add keyboard type-ahead search for hosts (#1516)
* feat(automations): add per-host maintenance mode and recurring schedules (#1521)
* feat(tmux): add a per-host mouse toggle and preserve remote configuration (#1513)
* fix(session-sharing): use the linked server for desktop meetings and links (#1507)
* fix(tunnels): support TOTP for client tunnels and jump hosts (#1508)
* fix: type errors and lint failures from merged plugin prs
* feat(installer): make the Windows desktop shortcut optional (#1517)
* fix: ship the plugin sdk and serialport in desktop builds
* fix: prefer ipv4 when resolving ssh host names (#1365)
* fix: readable terminal selection with the light theme (#1319)
* fix: build desktop session share links on the linked server (#1350)
* fix: show meeting guests to members and hide the roster from guests (#1353)
* fix: reliable file manager saves with sudo prompt and no base64 guessing (#1335)
* fix: show disk usage for the mount being browsed (#1347)
* fix: load the linux tray icon reliably and log tray failures (#1332)
* fix: cover the gnome close path in the desktop quit test (#1332)
* fix: prompt for jump host TOTP in terminal, file manager and metrics (#1312)
* feat: optional auto reconnect for dropped ssh sessions (#1321)
* feat: tighter compact host rows with inline tags (#1325)
* feat: bring back the alert feed homepage widget
* feat: pick target terminals for snippets again
* feat: share button on connection tabs again
* feat: recent servers list when linking the desktop app
* feat: list the fixed app shortcuts in keyboard shortcuts
* fix: translate hardcoded toasts and drop em dashes from ui text
* fix: translate leftover hardcoded ui text
* fix: toolbar files button opens at the shell directory again
* fix: show the remote desktop note under connection origin again
* chore: openapi docs for host metrics manager routes
* chore: openapi docs for undocumented routes and fix two broken doc blocks
* fix: declare terminal.listSessions use for snippet targets
* revert: drop the alert feed widget, the notification center replaces it
* feat: restore the alert feed homepage widget
* fix: quick connect fills the host address from plugin auth editors like tailscale
* fix: docker container logs use the base background color
* chore: fix dependabot alerts and apply safe npm upgrades
* fix: proxmox stats tab reads its switch from plugin host settings
* fix: count plugin ssh logins toward host status and stop showing reachable as a warning
* fix: hide the built-in source pill in the host editor
* fix: host status shows online when the port answers and only warns on a failed login
* fix: drop the top border on the split layout menu
* fix: host defaults tabs sit where the host editor puts them and skip plugins with nothing to default
* fix: remote desktop auth method labels and notes for vnc and telnet
* fix: add description to the web endpoint enable switch
* fix: crowdin sync and pretranslate for plugin locales, run plugin tests on release
* feat: move telemetry out of core into a usage statistics plugin
* fix: recent activity card no longer cuts off host names early
* fix: remove duplicate top border on split screen menu
* fix: redesign session log player controls, fix tooltip overlap and drop dot separators
* fix: file manager and sftp text showing raw i18n keys
* fix: onboarding rerunning for existing users after upgrading
* fix: fleet detail buttons overflowing the sidebar
* fix: session log player scrolling off screen horizontally
* fix: network graph lag when adding hosts and on every render
* fix: maintenance index name, shared host read only view and badge polling
* fix: meeting tabs, workspace tab merging, rail panels as tabs and general ui lag
* chore: update release notes
* fix: remote desktop tabs reconnecting on every tab switch
* fix: build plugin sdk before linting in release workflow
* chore: lint, format, and bump version to 2.9.0
* chore: skip crowdin pre-translate in release for now
* chore: sync Crowdin translations for 2.9.0
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Koi <38512047+datlt4@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
This commit is contained in:
4405 files changed
+1341026
-289722
No files matched your search
@@ -0,0 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.0
|
||||
|
||||
First release, bundled with Termix 2.9.0.
|
||||
|
||||
Shows your hosts and how they connect as an interactive graph. Hosts can be grouped, and each user's graph is saved.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Network Topology
|
||||
|
||||
Show your hosts and how they connect as an interactive graph.
|
||||
|
||||
## Features
|
||||
|
||||
- Show your hosts and the links between them as an interactive graph.
|
||||
- Put hosts into groups, and groups inside other groups.
|
||||
- Import and export the graph as JSON.
|
||||
- Add the graph to the dashboard as a card.
|
||||
- Each user's graph is saved and syncs between the desktop app and a server.
|
||||
|
||||
## Permissions
|
||||
|
||||
- `network-topology.use`: Use the network graph. Admins and users have it by default.
|
||||
|
||||
## Services
|
||||
|
||||
Provides to other plugins:
|
||||
|
||||
- `network-topology.graph`: read a user's graph.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm run build # build into dist/
|
||||
npm run test # run this plugin's tests
|
||||
npm run typecheck # type-check this plugin
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Add Host",
|
||||
"addGroup": "Add Group",
|
||||
"addLink": "Add Link",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
"resetView": "Reset View",
|
||||
"selectHost": "Select Host",
|
||||
"chooseHost": "Choose a host...",
|
||||
"parentGroup": "Parent Group",
|
||||
"noGroup": "No Group",
|
||||
"groupName": "Group Name",
|
||||
"color": "Color",
|
||||
"source": "Source",
|
||||
"target": "Target",
|
||||
"moveToGroup": "Move to Group",
|
||||
"selectGroup": "Select group...",
|
||||
"addConnection": "Add Connection",
|
||||
"hostDetails": "Host Details",
|
||||
"removeFromGroup": "Remove from Group",
|
||||
"addHostHere": "Add Host Here",
|
||||
"editGroup": "Edit Group",
|
||||
"delete": "Delete",
|
||||
"add": "Add",
|
||||
"create": "Create",
|
||||
"move": "Move",
|
||||
"connect": "Connect",
|
||||
"createGroup": "Create Group",
|
||||
"selectSourcePlaceholder": "Select Source...",
|
||||
"selectTargetPlaceholder": "Select Target...",
|
||||
"invalidFile": "Invalid File",
|
||||
"hostAlreadyExists": "Host is already in the topology",
|
||||
"connectionExists": "Connection already exists",
|
||||
"unknown": "Unknown",
|
||||
"name": "Name",
|
||||
"ip": "IP",
|
||||
"status": "Status",
|
||||
"failedToAddNode": "Failed to add node",
|
||||
"sourceDifferentFromTarget": "Source and target must be different",
|
||||
"exportJSON": "Export JSON",
|
||||
"importJSON": "Import JSON",
|
||||
"serverStats": "Host Metrics",
|
||||
"noNodes": "No nodes yet"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Network Graph"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Network Graph"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} nodes"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "View docs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Voeg gasheer by",
|
||||
"addGroup": "Voeg Groep by",
|
||||
"addLink": "Voeg skakel by",
|
||||
"zoomIn": "Inzoem",
|
||||
"zoomOut": "Uitzoom",
|
||||
"resetView": "Herstel aansig",
|
||||
"selectHost": "Kies gasheer",
|
||||
"chooseHost": "Kies 'n gasheer...",
|
||||
"parentGroup": "Ouergroep",
|
||||
"noGroup": "Geen Groep",
|
||||
"groupName": "Groepnaam",
|
||||
"color": "Kleur",
|
||||
"source": "Bron",
|
||||
"target": "Teiken",
|
||||
"moveToGroup": "Skuif na Groep",
|
||||
"selectGroup": "Kies groep...",
|
||||
"addConnection": "Voeg verbinding by",
|
||||
"hostDetails": "Gasheerbesonderhede",
|
||||
"removeFromGroup": "Verwyder uit Groep",
|
||||
"addHostHere": "Voeg gasheer hier by",
|
||||
"editGroup": "Wysig Groep",
|
||||
"delete": "Vee uit",
|
||||
"add": "Voeg by",
|
||||
"create": "Skep",
|
||||
"move": "Beweeg",
|
||||
"connect": "Verbind",
|
||||
"createGroup": "Skep Groep",
|
||||
"selectSourcePlaceholder": "Kies Bron...",
|
||||
"selectTargetPlaceholder": "Kies Teiken...",
|
||||
"invalidFile": "Ongeldige lêer",
|
||||
"hostAlreadyExists": "Gasheer is reeds in die topologie",
|
||||
"connectionExists": "Verbinding bestaan reeds",
|
||||
"unknown": "Onbekend",
|
||||
"name": "Naam",
|
||||
"ip": "IP-adres",
|
||||
"status": "Status",
|
||||
"failedToAddNode": "Kon nie nodus byvoeg nie",
|
||||
"sourceDifferentFromTarget": "Bron en teiken moet verskillend wees",
|
||||
"exportJSON": "Voer JSON uit",
|
||||
"importJSON": "Voer JSON in",
|
||||
"serverStats": "Gasheermetrieke",
|
||||
"noNodes": "Geen nodusse nog nie"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Netwerkgrafiek"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Netwerkgrafiek"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} nodusse"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Gebruik netwerkgrafiek",
|
||||
"description": "Bekyk en wysig die netwerktopologiegrafiek."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Bekyk dokumente"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "إضافة مضيف",
|
||||
"addGroup": "إضافة مجموعة",
|
||||
"addLink": "إضافة رابط",
|
||||
"zoomIn": "تكبير",
|
||||
"zoomOut": "تصغير",
|
||||
"resetView": "إعادة ضبط العرض",
|
||||
"selectHost": "تحديد مضيف",
|
||||
"chooseHost": "اختر مضيفًا...",
|
||||
"parentGroup": "المجموعة الأم",
|
||||
"noGroup": "بدون مجموعة",
|
||||
"groupName": "اسم المجموعة",
|
||||
"color": "اللون",
|
||||
"source": "المصدر",
|
||||
"target": "الهدف",
|
||||
"moveToGroup": "نقل إلى مجموعة",
|
||||
"selectGroup": "تحديد مجموعة...",
|
||||
"addConnection": "إضافة اتصال",
|
||||
"hostDetails": "تفاصيل المضيف",
|
||||
"removeFromGroup": "إزالة من المجموعة",
|
||||
"addHostHere": "إضافة مضيف هنا",
|
||||
"editGroup": "تعديل المجموعة",
|
||||
"delete": "حذف",
|
||||
"add": "إضافة",
|
||||
"create": "إنشاء",
|
||||
"move": "نقل",
|
||||
"connect": "اتصل",
|
||||
"createGroup": "إنشاء مجموعة",
|
||||
"selectSourcePlaceholder": "تحديد المصدر...",
|
||||
"selectTargetPlaceholder": "تحديد الهدف...",
|
||||
"invalidFile": "ملف غير صالح",
|
||||
"hostAlreadyExists": "المضيف موجود بالفعل في الطوبولوجيا",
|
||||
"connectionExists": "الاتصال موجود بالفعل",
|
||||
"unknown": "غير معروف",
|
||||
"name": "الاسم",
|
||||
"ip": "IP",
|
||||
"status": "الحالة",
|
||||
"failedToAddNode": "فشل في إضافة العقدة",
|
||||
"sourceDifferentFromTarget": "يجب أن يكون المصدر والهدف مختلفين",
|
||||
"exportJSON": "تصدير JSON",
|
||||
"importJSON": "استيراد JSON",
|
||||
"serverStats": "مقاييس المضيف",
|
||||
"noNodes": "لا توجد عُقد بعد"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "رسم بياني للشبكة"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "الرسم البياني للشبكة"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} عقدة"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "استخدم الرسم البياني للشبكة",
|
||||
"description": "عرض وتعديل مخطط طوبولوجيا الشبكة."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "عرض المستندات"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Добавяне на хост",
|
||||
"addGroup": "Добавяне на група",
|
||||
"addLink": "Добавяне на връзка",
|
||||
"zoomIn": "Увеличаване на мащаба",
|
||||
"zoomOut": "Намаляване на мащаба",
|
||||
"resetView": "Нулиране на изгледа",
|
||||
"selectHost": "Изберете хост",
|
||||
"chooseHost": "Изберете хост...",
|
||||
"parentGroup": "Родителска група",
|
||||
"noGroup": "Няма група",
|
||||
"groupName": "Име на групата",
|
||||
"color": "Цвят",
|
||||
"source": "Източник",
|
||||
"target": "Цел",
|
||||
"moveToGroup": "Преместване в група",
|
||||
"selectGroup": "Изберете група...",
|
||||
"addConnection": "Добавяне на връзка",
|
||||
"hostDetails": "Детайли за хоста",
|
||||
"removeFromGroup": "Премахване от групата",
|
||||
"addHostHere": "Добавете хост тук",
|
||||
"editGroup": "Редактиране на група",
|
||||
"delete": "Изтриване",
|
||||
"add": "Добавяне",
|
||||
"create": "Създаване",
|
||||
"move": "Преместване",
|
||||
"connect": "Свържете се",
|
||||
"createGroup": "Създаване на група",
|
||||
"selectSourcePlaceholder": "Изберете източник...",
|
||||
"selectTargetPlaceholder": "Изберете цел...",
|
||||
"invalidFile": "Невалиден файл",
|
||||
"hostAlreadyExists": "Хостът вече е в топологията",
|
||||
"connectionExists": "Връзката вече съществува",
|
||||
"unknown": "Неизвестен",
|
||||
"name": "Име",
|
||||
"ip": "ИП",
|
||||
"status": "Статус",
|
||||
"failedToAddNode": "Добавянето на възел не бе успешно",
|
||||
"sourceDifferentFromTarget": "Източникът и целта трябва да са различни",
|
||||
"exportJSON": "Експортиране на JSON",
|
||||
"importJSON": "Импортиране на JSON",
|
||||
"serverStats": "Метрики на хоста",
|
||||
"noNodes": "Все още няма възли"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Мрежова графика"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Мрежова графика"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} възли"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Използвайте мрежова графика",
|
||||
"description": "Преглед и редактиране на графиката на мрежовата топология."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Преглед на документи"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Add Host",
|
||||
"addGroup": "Add Group",
|
||||
"addLink": "Add Link",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
"resetView": "Reset View",
|
||||
"selectHost": "Select Host",
|
||||
"chooseHost": "Choose a host...",
|
||||
"parentGroup": "Parent Group",
|
||||
"noGroup": "No Group",
|
||||
"groupName": "Group Name",
|
||||
"color": "Color",
|
||||
"source": "Source",
|
||||
"target": "Target",
|
||||
"moveToGroup": "Move to Group",
|
||||
"selectGroup": "Select group...",
|
||||
"addConnection": "Add Connection",
|
||||
"hostDetails": "Host Details",
|
||||
"removeFromGroup": "Remove from Group",
|
||||
"addHostHere": "Add Host Here",
|
||||
"editGroup": "Edit Group",
|
||||
"delete": "Delete",
|
||||
"add": "Add",
|
||||
"create": "Create",
|
||||
"move": "Move",
|
||||
"connect": "Connect",
|
||||
"createGroup": "Create Group",
|
||||
"selectSourcePlaceholder": "Select Source...",
|
||||
"selectTargetPlaceholder": "Select Target...",
|
||||
"invalidFile": "Invalid File",
|
||||
"hostAlreadyExists": "Host is already in the topology",
|
||||
"connectionExists": "Connection already exists",
|
||||
"unknown": "Unknown",
|
||||
"name": "Name",
|
||||
"ip": "IP",
|
||||
"status": "Status",
|
||||
"failedToAddNode": "Failed to add node",
|
||||
"sourceDifferentFromTarget": "Source and target must be different",
|
||||
"exportJSON": "Export JSON",
|
||||
"importJSON": "Import JSON",
|
||||
"serverStats": "Host Metrics",
|
||||
"noNodes": "No nodes yet"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Network Graph"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Network Graph"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} nodes"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "View docs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Afegeix amfitrió",
|
||||
"addGroup": "Afegeix un grup",
|
||||
"addLink": "Afegeix un enllaç",
|
||||
"zoomIn": "Ampliar zoom",
|
||||
"zoomOut": "Allunyar",
|
||||
"resetView": "Restableix la vista",
|
||||
"selectHost": "Selecciona l'amfitrió",
|
||||
"chooseHost": "Trieu un amfitrió...",
|
||||
"parentGroup": "Grup de pares",
|
||||
"noGroup": "Sense grup",
|
||||
"groupName": "Nom del grup",
|
||||
"color": "Color",
|
||||
"source": "Font",
|
||||
"target": "Objectiu",
|
||||
"moveToGroup": "Moure al grup",
|
||||
"selectGroup": "Selecciona el grup...",
|
||||
"addConnection": "Afegeix una connexió",
|
||||
"hostDetails": "Detalls de l'amfitrió",
|
||||
"removeFromGroup": "Elimina del grup",
|
||||
"addHostHere": "Afegeix l'amfitrió aquí",
|
||||
"editGroup": "Edita el grup",
|
||||
"delete": "Suprimeix",
|
||||
"add": "Afegeix",
|
||||
"create": "Crea",
|
||||
"move": "Moure",
|
||||
"connect": "Connecta",
|
||||
"createGroup": "Crea un grup",
|
||||
"selectSourcePlaceholder": "Selecciona la font...",
|
||||
"selectTargetPlaceholder": "Selecciona l'objectiu...",
|
||||
"invalidFile": "Fitxer no vàlid",
|
||||
"hostAlreadyExists": "L'amfitrió ja és a la topologia",
|
||||
"connectionExists": "La connexió ja existeix",
|
||||
"unknown": "Desconegut",
|
||||
"name": "Nom",
|
||||
"ip": "IP",
|
||||
"status": "Estat",
|
||||
"failedToAddNode": "No s'ha pogut afegir el node",
|
||||
"sourceDifferentFromTarget": "La font i la destinació han de ser diferents",
|
||||
"exportJSON": "Exporta JSON",
|
||||
"importJSON": "Importa JSON",
|
||||
"serverStats": "Mètriques de l'amfitrió",
|
||||
"noNodes": "Encara no hi ha nodes"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Gràfic de xarxa"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Gràfic de xarxa"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "nodes {{count}}"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Utilitza el gràfic de xarxa",
|
||||
"description": "Visualitza i edita el gràfic de topologia de xarxa."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Veure documents"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Přidat hostitele",
|
||||
"addGroup": "Přidat skupinu",
|
||||
"addLink": "Přidat odkaz",
|
||||
"zoomIn": "Přiblížit",
|
||||
"zoomOut": "Oddálit",
|
||||
"resetView": "Obnovit zobrazení",
|
||||
"selectHost": "Vyberte hostitele",
|
||||
"chooseHost": "Vyberte si hostitele...",
|
||||
"parentGroup": "Nadřazená skupina",
|
||||
"noGroup": "Žádná skupina",
|
||||
"groupName": "Název skupiny",
|
||||
"color": "Barva",
|
||||
"source": "Zdroj",
|
||||
"target": "Cíl",
|
||||
"moveToGroup": "Přesunout do skupiny",
|
||||
"selectGroup": "Vyberte skupinu...",
|
||||
"addConnection": "Přidat připojení",
|
||||
"hostDetails": "Podrobnosti o hostiteli",
|
||||
"removeFromGroup": "Odebrat ze skupiny",
|
||||
"addHostHere": "Přidat hostitele zde",
|
||||
"editGroup": "Upravit skupinu",
|
||||
"delete": "Vymazat",
|
||||
"add": "Přidat",
|
||||
"create": "Vytvořit",
|
||||
"move": "Pohyb",
|
||||
"connect": "Připojit",
|
||||
"createGroup": "Vytvořit skupinu",
|
||||
"selectSourcePlaceholder": "Vyberte zdroj...",
|
||||
"selectTargetPlaceholder": "Vyberte cíl...",
|
||||
"invalidFile": "Neplatný soubor",
|
||||
"hostAlreadyExists": "Hostitel je již v topologii",
|
||||
"connectionExists": "Připojení již existuje",
|
||||
"unknown": "Neznámý",
|
||||
"name": "Jméno",
|
||||
"ip": "IP adresa",
|
||||
"status": "Postavení",
|
||||
"failedToAddNode": "Nepodařilo se přidat uzel",
|
||||
"sourceDifferentFromTarget": "Zdroj a cíl se musí lišit",
|
||||
"exportJSON": "Exportovat JSON",
|
||||
"importJSON": "Importovat JSON",
|
||||
"serverStats": "Metriky hostitele",
|
||||
"noNodes": "Zatím žádné uzly"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Síťový graf"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Síťový graf"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} uzlů"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Použít síťový graf",
|
||||
"description": "Zobrazte a upravte graf topologie sítě."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Zobrazit dokumenty"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Tilføj vært",
|
||||
"addGroup": "Tilføj gruppe",
|
||||
"addLink": "Tilføj link",
|
||||
"zoomIn": "Zoom ind",
|
||||
"zoomOut": "Zoom ud",
|
||||
"resetView": "Nulstil visning",
|
||||
"selectHost": "Vælg vært",
|
||||
"chooseHost": "Vælg en vært...",
|
||||
"parentGroup": "Forældregruppe",
|
||||
"noGroup": "Ingen gruppe",
|
||||
"groupName": "Gruppenavn",
|
||||
"color": "Farve",
|
||||
"source": "Kilde",
|
||||
"target": "Mål",
|
||||
"moveToGroup": "Flyt til gruppe",
|
||||
"selectGroup": "Vælg gruppe...",
|
||||
"addConnection": "Tilføj forbindelse",
|
||||
"hostDetails": "Værtsoplysninger",
|
||||
"removeFromGroup": "Fjern fra gruppe",
|
||||
"addHostHere": "Tilføj vært her",
|
||||
"editGroup": "Rediger gruppe",
|
||||
"delete": "Slet",
|
||||
"add": "Tilføje",
|
||||
"create": "Skabe",
|
||||
"move": "Flytte",
|
||||
"connect": "Forbinde",
|
||||
"createGroup": "Opret gruppe",
|
||||
"selectSourcePlaceholder": "Vælg kilde...",
|
||||
"selectTargetPlaceholder": "Vælg mål...",
|
||||
"invalidFile": "Ugyldig fil",
|
||||
"hostAlreadyExists": "Værten er allerede i topologien",
|
||||
"connectionExists": "Forbindelsen findes allerede",
|
||||
"unknown": "Ukendt",
|
||||
"name": "Navn",
|
||||
"ip": "IP-adresse",
|
||||
"status": "Status",
|
||||
"failedToAddNode": "Kunne ikke tilføje node",
|
||||
"sourceDifferentFromTarget": "Kilde og mål skal være forskellige",
|
||||
"exportJSON": "Eksportér JSON",
|
||||
"importJSON": "Importér JSON",
|
||||
"serverStats": "Værtsmålinger",
|
||||
"noNodes": "Ingen noder endnu"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Netværksgraf"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Netværksgraf"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} noder"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Brug netværksgraf",
|
||||
"description": "Se og rediger netværkstopologigrafen."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Se dokumenter"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Host hinzufügen",
|
||||
"addGroup": "Gruppe hinzufügen",
|
||||
"addLink": "Link hinzufügen",
|
||||
"zoomIn": "Vergrößern",
|
||||
"zoomOut": "Verkleinern",
|
||||
"resetView": "Ansicht zurücksetzen",
|
||||
"selectHost": "Host auswählen",
|
||||
"chooseHost": "Host auswählen...",
|
||||
"parentGroup": "Übergeordnete Gruppe",
|
||||
"noGroup": "Keine Gruppe",
|
||||
"groupName": "Gruppenname",
|
||||
"color": "Farbe",
|
||||
"source": "Quelle",
|
||||
"target": "Ziel",
|
||||
"moveToGroup": "In Gruppe verschieben",
|
||||
"selectGroup": "Gruppe auswählen...",
|
||||
"addConnection": "Verbindung hinzufügen",
|
||||
"hostDetails": "Host-Details",
|
||||
"removeFromGroup": "Aus Gruppe entfernen",
|
||||
"addHostHere": "Host hier hinzufügen",
|
||||
"editGroup": "Gruppe bearbeiten",
|
||||
"delete": "Löschen",
|
||||
"add": "Hinzufügen",
|
||||
"create": "Erstellen",
|
||||
"move": "Verschieben",
|
||||
"connect": "Verbinden",
|
||||
"createGroup": "Gruppe erstellen",
|
||||
"selectSourcePlaceholder": "Quelle auswählen...",
|
||||
"selectTargetPlaceholder": "Ziel auswählen...",
|
||||
"invalidFile": "Ungültige Datei",
|
||||
"hostAlreadyExists": "Host ist bereits in der Topologie",
|
||||
"connectionExists": "Verbindung ist bereits vorhanden",
|
||||
"unknown": "Unbekannt",
|
||||
"name": "Name",
|
||||
"ip": "IP",
|
||||
"status": "Status",
|
||||
"failedToAddNode": "Knoten konnte nicht hinzugefügt werden",
|
||||
"sourceDifferentFromTarget": "Quelle und Ziel müssen unterschiedlich sein",
|
||||
"exportJSON": "JSON exportieren",
|
||||
"importJSON": "JSON importieren",
|
||||
"serverStats": "Host-Metriken",
|
||||
"noNodes": "Noch keine Knoten"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Netzwerkdiagramm"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Netzwerkdiagramm"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} Knoten"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Netzwerkdiagramm verwenden",
|
||||
"description": "Netzwerktopologiediagramm anzeigen und bearbeiten."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Dokumentation anzeigen"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Προσθήκη κεντρικού υπολογιστή",
|
||||
"addGroup": "Προσθήκη ομάδας",
|
||||
"addLink": "Προσθήκη συνδέσμου",
|
||||
"zoomIn": "Μεγέθυνση",
|
||||
"zoomOut": "Σμίκρυνση",
|
||||
"resetView": "Επαναφορά προβολής",
|
||||
"selectHost": "Επιλέξτε κεντρικό υπολογιστή",
|
||||
"chooseHost": "Επιλέξτε έναν οικοδεσπότη...",
|
||||
"parentGroup": "Γονική ομάδα",
|
||||
"noGroup": "Καμία ομάδα",
|
||||
"groupName": "Όνομα ομάδας",
|
||||
"color": "Χρώμα",
|
||||
"source": "Πηγή",
|
||||
"target": "Στόχος",
|
||||
"moveToGroup": "Μετακίνηση σε ομάδα",
|
||||
"selectGroup": "Επιλογή ομάδας...",
|
||||
"addConnection": "Προσθήκη σύνδεσης",
|
||||
"hostDetails": "Στοιχεία κεντρικού υπολογιστή",
|
||||
"removeFromGroup": "Αφαίρεση από την ομάδα",
|
||||
"addHostHere": "Προσθήκη κεντρικού υπολογιστή εδώ",
|
||||
"editGroup": "Επεξεργασία ομάδας",
|
||||
"delete": "Διαγράφω",
|
||||
"add": "Προσθέτω",
|
||||
"create": "Δημιουργώ",
|
||||
"move": "Κίνηση",
|
||||
"connect": "Συνδέω",
|
||||
"createGroup": "Δημιουργία ομάδας",
|
||||
"selectSourcePlaceholder": "Επιλογή Πηγής...",
|
||||
"selectTargetPlaceholder": "Επιλογή Στόχου...",
|
||||
"invalidFile": "Μη έγκυρο αρχείο",
|
||||
"hostAlreadyExists": "Ο κεντρικός υπολογιστής βρίσκεται ήδη στην τοπολογία",
|
||||
"connectionExists": "Η σύνδεση υπάρχει ήδη",
|
||||
"unknown": "Αγνωστος",
|
||||
"name": "Ονομα",
|
||||
"ip": "Πνευματική Ιδιοκτησία",
|
||||
"status": "Κατάσταση",
|
||||
"failedToAddNode": "Αποτυχία προσθήκης κόμβου",
|
||||
"sourceDifferentFromTarget": "Η πηγή και ο στόχος πρέπει να είναι διαφορετικοί",
|
||||
"exportJSON": "Εξαγωγή JSON",
|
||||
"importJSON": "Εισαγωγή JSON",
|
||||
"serverStats": "Μετρήσεις κεντρικού υπολογιστή",
|
||||
"noNodes": "Δεν υπάρχουν ακόμη κόμβοι"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Γράφημα δικτύου"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Γράφημα δικτύου"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} κόμβοι"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Χρήση γραφήματος δικτύου",
|
||||
"description": "Προβολή και επεξεργασία του γραφήματος τοπολογίας δικτύου."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Προβολή εγγράφων"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Añadir host",
|
||||
"addGroup": "Añadir grupo",
|
||||
"addLink": "Añadir enlace",
|
||||
"zoomIn": "Acercar",
|
||||
"zoomOut": "Alejar",
|
||||
"resetView": "Restablecer vista",
|
||||
"selectHost": "Seleccionar host",
|
||||
"chooseHost": "Elegir un host...",
|
||||
"parentGroup": "Grupo padre",
|
||||
"noGroup": "Sin grupo",
|
||||
"groupName": "Nombre del grupo",
|
||||
"color": "Color",
|
||||
"source": "Origen",
|
||||
"target": "Destino",
|
||||
"moveToGroup": "Mover al grupo",
|
||||
"selectGroup": "Seleccionar grupo...",
|
||||
"addConnection": "Añadir conexión",
|
||||
"hostDetails": "Detalles del host",
|
||||
"removeFromGroup": "Quitar del grupo",
|
||||
"addHostHere": "Añadir host aquí",
|
||||
"editGroup": "Editar grupo",
|
||||
"delete": "Eliminar",
|
||||
"add": "Añadir",
|
||||
"create": "Crear",
|
||||
"move": "Mover",
|
||||
"connect": "Conectar",
|
||||
"createGroup": "Crear grupo",
|
||||
"selectSourcePlaceholder": "Seleccionar origen...",
|
||||
"selectTargetPlaceholder": "Seleccionar destino...",
|
||||
"invalidFile": "Archivo no válido",
|
||||
"hostAlreadyExists": "El host ya está en la topología",
|
||||
"connectionExists": "La conexión ya existe",
|
||||
"unknown": "Desconocido",
|
||||
"name": "Nombre",
|
||||
"ip": "IP",
|
||||
"status": "Estado",
|
||||
"failedToAddNode": "Error al añadir nodo",
|
||||
"sourceDifferentFromTarget": "El origen y el destino deben ser diferentes",
|
||||
"exportJSON": "Exportar JSON",
|
||||
"importJSON": "Importar JSON",
|
||||
"serverStats": "Métricas del host",
|
||||
"noNodes": "Aún no hay nodos"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Gráfico de red"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Gráfico de red"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} nodos"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Utilice un gráfico de red.",
|
||||
"description": "Visualice y edite el gráfico de topología de red."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Ver documentación"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Lisää isäntä",
|
||||
"addGroup": "Lisää ryhmä",
|
||||
"addLink": "Lisää linkki",
|
||||
"zoomIn": "Lähennä",
|
||||
"zoomOut": "Loitonna",
|
||||
"resetView": "Palauta näkymä",
|
||||
"selectHost": "Valitse isäntä",
|
||||
"chooseHost": "Valitse isäntä...",
|
||||
"parentGroup": "Yläryhmä",
|
||||
"noGroup": "Ei ryhmää",
|
||||
"groupName": "Ryhmän nimi",
|
||||
"color": "Väri",
|
||||
"source": "Lähde",
|
||||
"target": "Kohde",
|
||||
"moveToGroup": "Siirrä ryhmään",
|
||||
"selectGroup": "Valitse ryhmä...",
|
||||
"addConnection": "Lisää yhteys",
|
||||
"hostDetails": "Isännän tiedot",
|
||||
"removeFromGroup": "Poista ryhmästä",
|
||||
"addHostHere": "Lisää isäntä tähän",
|
||||
"editGroup": "Muokkaa ryhmää",
|
||||
"delete": "Poistaa",
|
||||
"add": "Lisätä",
|
||||
"create": "Luoda",
|
||||
"move": "Liikkua",
|
||||
"connect": "Yhdistä",
|
||||
"createGroup": "Luo ryhmä",
|
||||
"selectSourcePlaceholder": "Valitse lähde...",
|
||||
"selectTargetPlaceholder": "Valitse kohde...",
|
||||
"invalidFile": "Virheellinen tiedosto",
|
||||
"hostAlreadyExists": "Isäntä on jo topologiassa",
|
||||
"connectionExists": "Yhteys on jo olemassa",
|
||||
"unknown": "Tuntematon",
|
||||
"name": "Nimi",
|
||||
"ip": "IP-osoite",
|
||||
"status": "Status",
|
||||
"failedToAddNode": "Solmun lisääminen epäonnistui",
|
||||
"sourceDifferentFromTarget": "Lähteen ja kohteen on oltava eri",
|
||||
"exportJSON": "Vie JSON",
|
||||
"importJSON": "Tuo JSON",
|
||||
"serverStats": "Isännän mittarit",
|
||||
"noNodes": "Ei vielä solmuja"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Verkkokaavio"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Verkkokaavio"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} solmut"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Käytä verkkokaaviota",
|
||||
"description": "Tarkastele ja muokkaa verkon topologiakaaviota."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Näytä dokumentit"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Ajouter un hôte",
|
||||
"addGroup": "Ajouter un groupe",
|
||||
"addLink": "Ajouter un lien",
|
||||
"zoomIn": "Zoom avant",
|
||||
"zoomOut": "Zoom arrière",
|
||||
"resetView": "Réinitialiser la vue",
|
||||
"selectHost": "Sélectionner l'hôte",
|
||||
"chooseHost": "Choisir un hôte...",
|
||||
"parentGroup": "Groupe parent",
|
||||
"noGroup": "Aucun groupe",
|
||||
"groupName": "Nom du groupe",
|
||||
"color": "Couleur",
|
||||
"source": "Source",
|
||||
"target": "Cible",
|
||||
"moveToGroup": "Déplacer vers le groupe",
|
||||
"selectGroup": "Sélectionner un groupe...",
|
||||
"addConnection": "Ajouter une connexion",
|
||||
"hostDetails": "Détails de l'hôte",
|
||||
"removeFromGroup": "Retirer du groupe",
|
||||
"addHostHere": "Ajouter un hôte ici",
|
||||
"editGroup": "Modifier le groupe",
|
||||
"delete": "Supprimer",
|
||||
"add": "Ajouter",
|
||||
"create": "Créer",
|
||||
"move": "Déplacer",
|
||||
"connect": "Connecter",
|
||||
"createGroup": "Créer un groupe",
|
||||
"selectSourcePlaceholder": "Sélectionner la source...",
|
||||
"selectTargetPlaceholder": "Sélectionner la cible...",
|
||||
"invalidFile": "Fichier non valide",
|
||||
"hostAlreadyExists": "L'hôte est déjà dans la topologie",
|
||||
"connectionExists": "La connexion existe déjà",
|
||||
"unknown": "Inconnu",
|
||||
"name": "Nom",
|
||||
"ip": "IP",
|
||||
"status": "Statut",
|
||||
"failedToAddNode": "Échec de l'ajout du nœud",
|
||||
"sourceDifferentFromTarget": "La source et la cible doivent être différentes",
|
||||
"exportJSON": "Exporter JSON",
|
||||
"importJSON": "Importer JSON",
|
||||
"serverStats": "Métriques de l'hôte",
|
||||
"noNodes": "Aucun nœud pour l'instant"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Graphique réseau"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Graphique réseau"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} nœuds"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Utiliser un graphe de réseau",
|
||||
"description": "Afficher et modifier le graphique de topologie du réseau."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Voir la documentation"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "הוסף מארח",
|
||||
"addGroup": "הוסף קבוצה",
|
||||
"addLink": "הוסף קישור",
|
||||
"zoomIn": "לְהִתְמַקֵד",
|
||||
"zoomOut": "התקרבות",
|
||||
"resetView": "איפוס תצוגה",
|
||||
"selectHost": "בחר מארח",
|
||||
"chooseHost": "בחר מארח...",
|
||||
"parentGroup": "קבוצת הורים",
|
||||
"noGroup": "אין קבוצה",
|
||||
"groupName": "שם הקבוצה",
|
||||
"color": "צֶבַע",
|
||||
"source": "מָקוֹר",
|
||||
"target": "יַעַד",
|
||||
"moveToGroup": "העבר לקבוצה",
|
||||
"selectGroup": "בחירת קבוצה...",
|
||||
"addConnection": "הוסף חיבור",
|
||||
"hostDetails": "פרטי המארח",
|
||||
"removeFromGroup": "הסר מהקבוצה",
|
||||
"addHostHere": "הוסף מארח כאן",
|
||||
"editGroup": "עריכת קבוצה",
|
||||
"delete": "לִמְחוֹק",
|
||||
"add": "לְהוֹסִיף",
|
||||
"create": "לִיצוֹר",
|
||||
"move": "מַהֲלָך",
|
||||
"connect": "לְחַבֵּר",
|
||||
"createGroup": "צור קבוצה",
|
||||
"selectSourcePlaceholder": "בחר מקור...",
|
||||
"selectTargetPlaceholder": "בחר יעד...",
|
||||
"invalidFile": "קובץ לא חוקי",
|
||||
"hostAlreadyExists": "המארח כבר נמצא בטופולוגיה",
|
||||
"connectionExists": "החיבור כבר קיים",
|
||||
"unknown": "לֹא יְדוּעַ",
|
||||
"name": "שֵׁם",
|
||||
"ip": "IP",
|
||||
"status": "סטָטוּס",
|
||||
"failedToAddNode": "נכשל בהוספת הצומת",
|
||||
"sourceDifferentFromTarget": "המקור והיעד חייבים להיות שונים",
|
||||
"exportJSON": "ייצוא JSON",
|
||||
"importJSON": "ייבוא JSON",
|
||||
"serverStats": "מדדי מארח",
|
||||
"noNodes": "עדיין אין צמתים"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "גרף הרשת"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "גרף הרשת"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "צמתים {{count}}"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "השתמש בגרף הרשת",
|
||||
"description": "הצג וערוך את גרף טופולוגיית הרשת."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "הצג מסמכים"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "होस्ट जोड़ें",
|
||||
"addGroup": "समूह जोड़ें",
|
||||
"addLink": "लिंक जोड़ें",
|
||||
"zoomIn": "ज़ूम इन",
|
||||
"zoomOut": "ज़ूम आउट",
|
||||
"resetView": "रीसेट करें",
|
||||
"selectHost": "होस्ट चुनें",
|
||||
"chooseHost": "कोई होस्ट चुनें...",
|
||||
"parentGroup": "मूल समूह",
|
||||
"noGroup": "कोई समूह नहीं",
|
||||
"groupName": "समूह का नाम",
|
||||
"color": "रंग",
|
||||
"source": "स्रोत",
|
||||
"target": "लक्ष्य",
|
||||
"moveToGroup": "समूह में ले जाएँ",
|
||||
"selectGroup": "समूह चुनें...",
|
||||
"addConnection": "कनेक्शन जोड़ें",
|
||||
"hostDetails": "होस्ट विवरण",
|
||||
"removeFromGroup": "समूह से निकालें",
|
||||
"addHostHere": "यहाँ होस्ट जोड़ें",
|
||||
"editGroup": "समूह संपादित करें",
|
||||
"delete": "हटाएँ",
|
||||
"add": "जोड़ें",
|
||||
"create": "बनाएँ",
|
||||
"move": "ले जाएँ",
|
||||
"connect": "कनेक्ट करें",
|
||||
"createGroup": "समूह बनाएँ",
|
||||
"selectSourcePlaceholder": "स्रोत चुनें...",
|
||||
"selectTargetPlaceholder": "लक्ष्य चुनें...",
|
||||
"invalidFile": "अमान्य फ़ाइल",
|
||||
"hostAlreadyExists": "होस्ट पहले से टोपोलॉजी में मौजूद है",
|
||||
"connectionExists": "कनेक्शन पहले से मौजूद है",
|
||||
"unknown": "अज्ञात",
|
||||
"name": "नाम",
|
||||
"ip": "IP",
|
||||
"status": "स्थिति",
|
||||
"failedToAddNode": "नोड जोड़ने में विफल",
|
||||
"sourceDifferentFromTarget": "स्रोत और लक्ष्य भिन्न होने चाहिए",
|
||||
"exportJSON": "JSON निर्यात करें",
|
||||
"importJSON": "JSON आयात करें",
|
||||
"serverStats": "होस्ट मेट्रिक्स",
|
||||
"noNodes": "अभी तक कोई नोड नहीं"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "नेटवर्क ग्राफ़"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "नेटवर्क ग्राफ़"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} नोड्स"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "दस्तावेज़ देखें"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Gazdagép hozzáadása",
|
||||
"addGroup": "Csoport hozzáadása",
|
||||
"addLink": "Link hozzáadása",
|
||||
"zoomIn": "Nagyítás",
|
||||
"zoomOut": "Kicsinyítés",
|
||||
"resetView": "Nézet visszaállítása",
|
||||
"selectHost": "Válasszon gazdagépet",
|
||||
"chooseHost": "Válassz egy tárhelyszolgáltatót...",
|
||||
"parentGroup": "Szülőcsoport",
|
||||
"noGroup": "Nincs csoport",
|
||||
"groupName": "Csoport neve",
|
||||
"color": "Szín",
|
||||
"source": "Forrás",
|
||||
"target": "Cél",
|
||||
"moveToGroup": "Áthelyezés csoportba",
|
||||
"selectGroup": "Csoport kiválasztása...",
|
||||
"addConnection": "Kapcsolat hozzáadása",
|
||||
"hostDetails": "Gazdagép adatai",
|
||||
"removeFromGroup": "Eltávolítás a csoportból",
|
||||
"addHostHere": "Gazdagép hozzáadása ide",
|
||||
"editGroup": "Csoport szerkesztése",
|
||||
"delete": "Töröl",
|
||||
"add": "Hozzáadás",
|
||||
"create": "Teremt",
|
||||
"move": "Mozog",
|
||||
"connect": "Csatlakozás",
|
||||
"createGroup": "Csoport létrehozása",
|
||||
"selectSourcePlaceholder": "Forrás kiválasztása...",
|
||||
"selectTargetPlaceholder": "Cél kiválasztása...",
|
||||
"invalidFile": "Érvénytelen fájl",
|
||||
"hostAlreadyExists": "A gazdagép már szerepel a topológiában",
|
||||
"connectionExists": "A kapcsolat már létezik",
|
||||
"unknown": "Ismeretlen",
|
||||
"name": "Név",
|
||||
"ip": "IP-cím",
|
||||
"status": "Állapot",
|
||||
"failedToAddNode": "Nem sikerült hozzáadni a csomópontot",
|
||||
"sourceDifferentFromTarget": "A forrásnak és a célnak eltérőnek kell lennie",
|
||||
"exportJSON": "JSON exportálása",
|
||||
"importJSON": "JSON importálása",
|
||||
"serverStats": "Gazdagép-metrikák",
|
||||
"noNodes": "Még nincsenek csomópontok"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Hálózati gráf"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Hálózati gráf"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} csomópontok"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Hálózati gráf használata",
|
||||
"description": "A hálózati topológia grafikonjának megtekintése és szerkesztése."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Dokumentumok megtekintése"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Add Host",
|
||||
"addGroup": "Add Group",
|
||||
"addLink": "Add Link",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
"resetView": "Reset View",
|
||||
"selectHost": "Select Host",
|
||||
"chooseHost": "Choose a host...",
|
||||
"parentGroup": "Parent Group",
|
||||
"noGroup": "No Group",
|
||||
"groupName": "Group Name",
|
||||
"color": "Color",
|
||||
"source": "Source",
|
||||
"target": "Target",
|
||||
"moveToGroup": "Move to Group",
|
||||
"selectGroup": "Select group...",
|
||||
"addConnection": "Add Connection",
|
||||
"hostDetails": "Host Details",
|
||||
"removeFromGroup": "Remove from Group",
|
||||
"addHostHere": "Add Host Here",
|
||||
"editGroup": "Edit Group",
|
||||
"delete": "Delete",
|
||||
"add": "Add",
|
||||
"create": "Create",
|
||||
"move": "Move",
|
||||
"connect": "Connect",
|
||||
"createGroup": "Create Group",
|
||||
"selectSourcePlaceholder": "Select Source...",
|
||||
"selectTargetPlaceholder": "Select Target...",
|
||||
"invalidFile": "Invalid File",
|
||||
"hostAlreadyExists": "Host is already in the topology",
|
||||
"connectionExists": "Connection already exists",
|
||||
"unknown": "Unknown",
|
||||
"name": "Name",
|
||||
"ip": "IP",
|
||||
"status": "Status",
|
||||
"failedToAddNode": "Failed to add node",
|
||||
"sourceDifferentFromTarget": "Source and target must be different",
|
||||
"exportJSON": "Export JSON",
|
||||
"importJSON": "Import JSON",
|
||||
"serverStats": "Host Metrics",
|
||||
"noNodes": "No nodes yet"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Network Graph"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Network Graph"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} nodes"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "View docs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Aggiungi host",
|
||||
"addGroup": "Aggiungi gruppo",
|
||||
"addLink": "Aggiungi collegamento",
|
||||
"zoomIn": "Ingrandisci",
|
||||
"zoomOut": "Riduci",
|
||||
"resetView": "Reimposta vista",
|
||||
"selectHost": "Seleziona host",
|
||||
"chooseHost": "Scegli un host...",
|
||||
"parentGroup": "Gruppo padre",
|
||||
"noGroup": "Nessun gruppo",
|
||||
"groupName": "Nome gruppo",
|
||||
"color": "Colore",
|
||||
"source": "Origine",
|
||||
"target": "Destinazione",
|
||||
"moveToGroup": "Sposta nel gruppo",
|
||||
"selectGroup": "Seleziona gruppo...",
|
||||
"addConnection": "Aggiungi connessione",
|
||||
"hostDetails": "Dettagli host",
|
||||
"removeFromGroup": "Rimuovi dal gruppo",
|
||||
"addHostHere": "Aggiungi host qui",
|
||||
"editGroup": "Modifica gruppo",
|
||||
"delete": "Elimina",
|
||||
"add": "Aggiungi",
|
||||
"create": "Crea",
|
||||
"move": "Sposta",
|
||||
"connect": "Connetti",
|
||||
"createGroup": "Crea gruppo",
|
||||
"selectSourcePlaceholder": "Seleziona origine...",
|
||||
"selectTargetPlaceholder": "Seleziona destinazione...",
|
||||
"invalidFile": "File non valido",
|
||||
"hostAlreadyExists": "L'host è già presente nella topologia",
|
||||
"connectionExists": "La connessione esiste già",
|
||||
"unknown": "Sconosciuto",
|
||||
"name": "Nome",
|
||||
"ip": "IP",
|
||||
"status": "Stato",
|
||||
"failedToAddNode": "Impossibile aggiungere il nodo",
|
||||
"sourceDifferentFromTarget": "Origine e destinazione devono essere diverse",
|
||||
"exportJSON": "Esporta JSON",
|
||||
"importJSON": "Importa JSON",
|
||||
"serverStats": "Metriche host",
|
||||
"noNodes": "Nessun nodo al momento"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Grafico di rete"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Grafico di rete"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} nodi"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Utilizzare un grafico di rete",
|
||||
"description": "Visualizza e modifica il grafico della topologia di rete."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Visualizza documentazione"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "ホストを追加",
|
||||
"addGroup": "グループを追加",
|
||||
"addLink": "リンクを追加",
|
||||
"zoomIn": "拡大",
|
||||
"zoomOut": "縮小",
|
||||
"resetView": "表示をリセット",
|
||||
"selectHost": "ホストを選択",
|
||||
"chooseHost": "ホストを選択...",
|
||||
"parentGroup": "親グループ",
|
||||
"noGroup": "グループなし",
|
||||
"groupName": "グループ名",
|
||||
"color": "色",
|
||||
"source": "ソース",
|
||||
"target": "ターゲット",
|
||||
"moveToGroup": "グループに移動",
|
||||
"selectGroup": "グループを選択...",
|
||||
"addConnection": "接続を追加",
|
||||
"hostDetails": "ホスト詳細",
|
||||
"removeFromGroup": "グループから削除",
|
||||
"addHostHere": "ここにホストを追加",
|
||||
"editGroup": "グループを編集",
|
||||
"delete": "削除",
|
||||
"add": "追加",
|
||||
"create": "作成",
|
||||
"move": "移動",
|
||||
"connect": "接続",
|
||||
"createGroup": "グループを作成",
|
||||
"selectSourcePlaceholder": "ソースを選択...",
|
||||
"selectTargetPlaceholder": "ターゲットを選択...",
|
||||
"invalidFile": "無効なファイル",
|
||||
"hostAlreadyExists": "ホストはトポロジーに既に存在します",
|
||||
"connectionExists": "接続は既に存在しています",
|
||||
"unknown": "不明",
|
||||
"name": "名前",
|
||||
"ip": "IP",
|
||||
"status": "ステータス",
|
||||
"failedToAddNode": "ノードの追加に失敗しました",
|
||||
"sourceDifferentFromTarget": "ソースとターゲットは異なる必要があります",
|
||||
"exportJSON": "JSON エクスポート",
|
||||
"importJSON": "JSON インポート",
|
||||
"serverStats": "ホストメトリクス",
|
||||
"noNodes": "ノードはまだありません"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "ネットワークグラフ"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "ネットワークグラフ"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} ノード"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "ネットワークグラフを使用する",
|
||||
"description": "ネットワークトポロジーグラフを表示および編集します。"
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "ドキュメントを表示"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "호스트 추가",
|
||||
"addGroup": "그룹 추가",
|
||||
"addLink": "링크 추가",
|
||||
"zoomIn": "확대",
|
||||
"zoomOut": "축소",
|
||||
"resetView": "보기 초기화",
|
||||
"selectHost": "호스트 선택",
|
||||
"chooseHost": "호스트 선택...",
|
||||
"parentGroup": "상위 그룹",
|
||||
"noGroup": "그룹 없음",
|
||||
"groupName": "그룹 이름",
|
||||
"color": "색상",
|
||||
"source": "소스",
|
||||
"target": "대상",
|
||||
"moveToGroup": "그룹으로 이동",
|
||||
"selectGroup": "그룹 선택...",
|
||||
"addConnection": "연결 추가",
|
||||
"hostDetails": "호스트 세부 정보",
|
||||
"removeFromGroup": "그룹에서 제거",
|
||||
"addHostHere": "여기에 호스트 추가",
|
||||
"editGroup": "그룹 편집",
|
||||
"delete": "삭제",
|
||||
"add": "추가",
|
||||
"create": "생성",
|
||||
"move": "이동",
|
||||
"connect": "연결",
|
||||
"createGroup": "그룹 생성",
|
||||
"selectSourcePlaceholder": "소스 선택...",
|
||||
"selectTargetPlaceholder": "대상 선택...",
|
||||
"invalidFile": "잘못된 파일",
|
||||
"hostAlreadyExists": "호스트가 이미 토폴로지에 있습니다.",
|
||||
"connectionExists": "연결이 이미 존재합니다.",
|
||||
"unknown": "알 수 없음",
|
||||
"name": "이름",
|
||||
"ip": "IP",
|
||||
"status": "상태",
|
||||
"failedToAddNode": "노드를 추가하지 못했습니다.",
|
||||
"sourceDifferentFromTarget": "소스와 대상은 달라야 합니다.",
|
||||
"exportJSON": "JSON 내보내기",
|
||||
"importJSON": "JSON 가져오기",
|
||||
"serverStats": "호스트 메트릭",
|
||||
"noNodes": "아직 노드 없음"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "네트워크 그래프"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "네트워크 그래프"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}}개 노드"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "네트워크 그래프를 사용하세요",
|
||||
"description": "네트워크 토폴로지 그래프를 보고 편집할 수 있습니다."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "문서 보기"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Voeg een host toe",
|
||||
"addGroup": "Groep toevoegen",
|
||||
"addLink": "Link toevoegen",
|
||||
"zoomIn": "Inzoomen",
|
||||
"zoomOut": "Uitzoomen",
|
||||
"resetView": "Weergave opnieuw instellen",
|
||||
"selectHost": "Selecteer host",
|
||||
"chooseHost": "Kies een host...",
|
||||
"parentGroup": "Oudergroep",
|
||||
"noGroup": "Geen groep",
|
||||
"groupName": "Groepsnaam",
|
||||
"color": "Kleur",
|
||||
"source": "Bron",
|
||||
"target": "Doel",
|
||||
"moveToGroup": "Verplaatsen naar groep",
|
||||
"selectGroup": "Selecteer groep...",
|
||||
"addConnection": "Verbinding toevoegen",
|
||||
"hostDetails": "Gegevens van de gastheer",
|
||||
"removeFromGroup": "Verwijderen uit groep",
|
||||
"addHostHere": "Voeg hier de host toe",
|
||||
"editGroup": "Groep bewerken",
|
||||
"delete": "Verwijderen",
|
||||
"add": "Toevoegen",
|
||||
"create": "Creëren",
|
||||
"move": "Beweging",
|
||||
"connect": "Verbinden",
|
||||
"createGroup": "Groep aanmaken",
|
||||
"selectSourcePlaceholder": "Selecteer bron...",
|
||||
"selectTargetPlaceholder": "Doel selecteren...",
|
||||
"invalidFile": "Ongeldig bestand",
|
||||
"hostAlreadyExists": "De host bevindt zich al in de topologie.",
|
||||
"connectionExists": "De verbinding bestaat al",
|
||||
"unknown": "Onbekend",
|
||||
"name": "Naam",
|
||||
"ip": "IP",
|
||||
"status": "Status",
|
||||
"failedToAddNode": "Het toevoegen van het knooppunt is mislukt.",
|
||||
"sourceDifferentFromTarget": "Bron en doel moeten verschillend zijn.",
|
||||
"exportJSON": "Exporteer JSON",
|
||||
"importJSON": "JSON importeren",
|
||||
"serverStats": "Hoststatistieken",
|
||||
"noNodes": "Nog geen knooppunten"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Netwerkgrafiek"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Netwerkgrafiek"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} knooppunten"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Gebruik een netwerkgrafiek",
|
||||
"description": "Bekijk en bewerk de netwerktopologiegrafiek."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Documenten bekijken"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Legg til vert",
|
||||
"addGroup": "Add Group",
|
||||
"addLink": "Add Link",
|
||||
"zoomIn": "Zoom inn",
|
||||
"zoomOut": "Zoom ut",
|
||||
"resetView": "Reset View",
|
||||
"selectHost": "Select Host",
|
||||
"chooseHost": "Choose a host...",
|
||||
"parentGroup": "Parent Group",
|
||||
"noGroup": "No Group",
|
||||
"groupName": "Group Name",
|
||||
"color": "Color",
|
||||
"source": "Source",
|
||||
"target": "Target",
|
||||
"moveToGroup": "Move to Group",
|
||||
"selectGroup": "Select group...",
|
||||
"addConnection": "Add Connection",
|
||||
"hostDetails": "Host Details",
|
||||
"removeFromGroup": "Remove from Group",
|
||||
"addHostHere": "Add Host Here",
|
||||
"editGroup": "Edit Group",
|
||||
"delete": "Slett",
|
||||
"add": "Legg til",
|
||||
"create": "Create",
|
||||
"move": "Flytt",
|
||||
"connect": "Koble til",
|
||||
"createGroup": "Create Group",
|
||||
"selectSourcePlaceholder": "Select Source...",
|
||||
"selectTargetPlaceholder": "Select Target...",
|
||||
"invalidFile": "Invalid File",
|
||||
"hostAlreadyExists": "Host is already in the topology",
|
||||
"connectionExists": "Connection already exists",
|
||||
"unknown": "Ukjent",
|
||||
"name": "Navn",
|
||||
"ip": "IP",
|
||||
"status": "Status",
|
||||
"failedToAddNode": "Failed to add node",
|
||||
"sourceDifferentFromTarget": "Source and target must be different",
|
||||
"exportJSON": "Export JSON",
|
||||
"importJSON": "Importer JSON",
|
||||
"serverStats": "Host Metrics",
|
||||
"noNodes": "No nodes yet"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Network Graph"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Network Graph"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} nodes"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "View docs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Dodaj hosta",
|
||||
"addGroup": "Dodaj grupę",
|
||||
"addLink": "Dodaj link",
|
||||
"zoomIn": "Powiększ",
|
||||
"zoomOut": "Oddal",
|
||||
"resetView": "Resetuj widok",
|
||||
"selectHost": "Wybierz hosta",
|
||||
"chooseHost": "Wybierz hosta...",
|
||||
"parentGroup": "Grupa nadrzędna",
|
||||
"noGroup": "Brak grupy",
|
||||
"groupName": "Nazwa grupy",
|
||||
"color": "Kolor",
|
||||
"source": "Źródło",
|
||||
"target": "Cel",
|
||||
"moveToGroup": "Przenieś do grupy",
|
||||
"selectGroup": "Wybierz grupę...",
|
||||
"addConnection": "Dodaj połączenie",
|
||||
"hostDetails": "Szczegóły hosta",
|
||||
"removeFromGroup": "Usuń z grupy",
|
||||
"addHostHere": "Dodaj hosta tutaj",
|
||||
"editGroup": "Edytuj grupę",
|
||||
"delete": "Usuń",
|
||||
"add": "Dodaj",
|
||||
"create": "Utwórz",
|
||||
"move": "Przenieś",
|
||||
"connect": "Połącz",
|
||||
"createGroup": "Utwórz grupę",
|
||||
"selectSourcePlaceholder": "Wybierz źródło...",
|
||||
"selectTargetPlaceholder": "Wybierz cel...",
|
||||
"invalidFile": "Nieprawidłowy plik",
|
||||
"hostAlreadyExists": "Host jest już w topologii",
|
||||
"connectionExists": "Połączenie już istnieje",
|
||||
"unknown": "Nieznany",
|
||||
"name": "Nazwa",
|
||||
"ip": "IP",
|
||||
"status": "Status",
|
||||
"failedToAddNode": "Nie udało się dodać węzła",
|
||||
"sourceDifferentFromTarget": "Źródło i cel muszą być różne",
|
||||
"exportJSON": "Eksportuj JSON",
|
||||
"importJSON": "Importuj JSON",
|
||||
"serverStats": "Metryki hosta",
|
||||
"noNodes": "Brak węzłów"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Graf sieci"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Graf sieci"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} węzłów"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Zobacz dokumentację"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Adicionar host",
|
||||
"addGroup": "Add Group",
|
||||
"addLink": "Add Link",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
"resetView": "Reset View",
|
||||
"selectHost": "Select Host",
|
||||
"chooseHost": "Choose a host...",
|
||||
"parentGroup": "Parent Group",
|
||||
"noGroup": "No Group",
|
||||
"groupName": "Group Name",
|
||||
"color": "Color",
|
||||
"source": "Source",
|
||||
"target": "Target",
|
||||
"moveToGroup": "Move to Group",
|
||||
"selectGroup": "Select group...",
|
||||
"addConnection": "Add Connection",
|
||||
"hostDetails": "Host Details",
|
||||
"removeFromGroup": "Remove from Group",
|
||||
"addHostHere": "Add Host Here",
|
||||
"editGroup": "Edit Group",
|
||||
"delete": "Apagar",
|
||||
"add": "Adicionar",
|
||||
"create": "Criar",
|
||||
"move": "Move",
|
||||
"connect": "Conectar",
|
||||
"createGroup": "Create Group",
|
||||
"selectSourcePlaceholder": "Select Source...",
|
||||
"selectTargetPlaceholder": "Select Target...",
|
||||
"invalidFile": "Invalid File",
|
||||
"hostAlreadyExists": "Host is already in the topology",
|
||||
"connectionExists": "Connection already exists",
|
||||
"unknown": "Unknown",
|
||||
"name": "Name",
|
||||
"ip": "IP",
|
||||
"status": "Status",
|
||||
"failedToAddNode": "Failed to add node",
|
||||
"sourceDifferentFromTarget": "Source and target must be different",
|
||||
"exportJSON": "Export JSON",
|
||||
"importJSON": "Import JSON",
|
||||
"serverStats": "Métricas do Host",
|
||||
"noNodes": "No nodes yet"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Gráfico da Rede"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Gráfico da Rede"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} nodes"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Documentação"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Adicionar Host",
|
||||
"addGroup": "Adicionar Grupo",
|
||||
"addLink": "Adicionar Ligação",
|
||||
"zoomIn": "Ampliar",
|
||||
"zoomOut": "Reduzir",
|
||||
"resetView": "Repor Vista",
|
||||
"selectHost": "Selecionar Host",
|
||||
"chooseHost": "Escolher um host...",
|
||||
"parentGroup": "Grupo Pai",
|
||||
"noGroup": "Nenhum grupo",
|
||||
"groupName": "Nome do Grupo",
|
||||
"color": "Cor",
|
||||
"source": "Origem",
|
||||
"target": "Destino",
|
||||
"moveToGroup": "Mover para Grupo",
|
||||
"selectGroup": "Selecionar grupo...",
|
||||
"addConnection": "Adicionar Ligação",
|
||||
"hostDetails": "Detalhes do Host",
|
||||
"removeFromGroup": "Remover do Grupo",
|
||||
"addHostHere": "Adicionar Host Aqui",
|
||||
"editGroup": "Editar Grupo",
|
||||
"delete": "Eliminar",
|
||||
"add": "Adicionar",
|
||||
"create": "Criar",
|
||||
"move": "Mover",
|
||||
"connect": "Ligar",
|
||||
"createGroup": "Criar Grupo",
|
||||
"selectSourcePlaceholder": "Selecionar Origem...",
|
||||
"selectTargetPlaceholder": "Selecionar Destino...",
|
||||
"invalidFile": "Ficheiro Inválido",
|
||||
"hostAlreadyExists": "O host já está na topologia.",
|
||||
"connectionExists": "A ligação já existe",
|
||||
"unknown": "Desconhecido",
|
||||
"name": "Nome",
|
||||
"ip": "IP",
|
||||
"status": "Estado",
|
||||
"failedToAddNode": "Falha ao adicionar nó",
|
||||
"sourceDifferentFromTarget": "A origem e o destino devem ser diferentes",
|
||||
"exportJSON": "Exportar JSON",
|
||||
"importJSON": "Importar JSON",
|
||||
"serverStats": "Métricas do host",
|
||||
"noNodes": "Ainda sem nós"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Gráfico de rede"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Gráfico de Rede"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} nós"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Ver documentação"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Adăugați gazdă",
|
||||
"addGroup": "Adăugați un grup",
|
||||
"addLink": "Adăugați link",
|
||||
"zoomIn": "Mărire",
|
||||
"zoomOut": "Micșorare",
|
||||
"resetView": "Resetare vizualizare",
|
||||
"selectHost": "Selectați gazda",
|
||||
"chooseHost": "Alege o gazdă...",
|
||||
"parentGroup": "Grupul de părinți",
|
||||
"noGroup": "Fără grup",
|
||||
"groupName": "Numele grupului",
|
||||
"color": "Culoare",
|
||||
"source": "Sursă",
|
||||
"target": "Ţintă",
|
||||
"moveToGroup": "Mutare în grup",
|
||||
"selectGroup": "Selectați grupul...",
|
||||
"addConnection": "Adăugați o conexiune",
|
||||
"hostDetails": "Detalii gazdă",
|
||||
"removeFromGroup": "Eliminare din grup",
|
||||
"addHostHere": "Adăugați gazda aici",
|
||||
"editGroup": "Editați grupul",
|
||||
"delete": "Şterge",
|
||||
"add": "Adăuga",
|
||||
"create": "Crea",
|
||||
"move": "Mişcare",
|
||||
"connect": "Conectează-te",
|
||||
"createGroup": "Creare grup",
|
||||
"selectSourcePlaceholder": "Selectați sursa...",
|
||||
"selectTargetPlaceholder": "Selectați țintă...",
|
||||
"invalidFile": "Fișier nevalid",
|
||||
"hostAlreadyExists": "Gazda se află deja în topologie",
|
||||
"connectionExists": "Conexiunea există deja",
|
||||
"unknown": "Necunoscut",
|
||||
"name": "Nume",
|
||||
"ip": "IP-ul",
|
||||
"status": "Stare",
|
||||
"failedToAddNode": "Nu s-a putut adăuga nodul",
|
||||
"sourceDifferentFromTarget": "Sursa și ținta trebuie să fie diferite",
|
||||
"exportJSON": "Export JSON",
|
||||
"importJSON": "Import JSON",
|
||||
"serverStats": "Metrici ale gazdei",
|
||||
"noNodes": "Încă nu există noduri"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Grafic de rețea"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Grafic de rețea"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "noduri {{count}}"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Folosește graficul de rețea",
|
||||
"description": "Vizualizați și editați graficul topologiei rețelei."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Vizualizați documentele"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Добавить хост",
|
||||
"addGroup": "Добавить группу",
|
||||
"addLink": "Добавить связь",
|
||||
"zoomIn": "Приблизить",
|
||||
"zoomOut": "Отдалить",
|
||||
"resetView": "Сбросить вид",
|
||||
"selectHost": "Выбрать хост",
|
||||
"chooseHost": "Выберите хост...",
|
||||
"parentGroup": "Родительская группа",
|
||||
"noGroup": "Без группы",
|
||||
"groupName": "Имя группы",
|
||||
"color": "Цвет",
|
||||
"source": "Источник",
|
||||
"target": "Цель",
|
||||
"moveToGroup": "Переместить в группу",
|
||||
"selectGroup": "Выберите группу...",
|
||||
"addConnection": "Добавить соединение",
|
||||
"hostDetails": "Сведения о хосте",
|
||||
"removeFromGroup": "Удалить из группы",
|
||||
"addHostHere": "Добавить хост сюда",
|
||||
"editGroup": "Редактировать группу",
|
||||
"delete": "Удалить",
|
||||
"add": "Добавить",
|
||||
"create": "Создать",
|
||||
"move": "Переместить",
|
||||
"connect": "Соединить",
|
||||
"createGroup": "Создать группу",
|
||||
"selectSourcePlaceholder": "Выберите источник...",
|
||||
"selectTargetPlaceholder": "Выберите цель...",
|
||||
"invalidFile": "Неверный файл",
|
||||
"hostAlreadyExists": "Хост уже присутствует в топологии",
|
||||
"connectionExists": "Соединение уже существует",
|
||||
"unknown": "Неизвестно",
|
||||
"name": "Имя",
|
||||
"ip": "IP",
|
||||
"status": "Статус",
|
||||
"failedToAddNode": "Не удалось добавить узел",
|
||||
"sourceDifferentFromTarget": "Источник и цель должны различаться",
|
||||
"exportJSON": "Экспорт JSON",
|
||||
"importJSON": "Импорт JSON",
|
||||
"serverStats": "Метрики хоста",
|
||||
"noNodes": "Пока нет узлов"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Сетевой график"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Сетевой график"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} узлов"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Документация"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Add Host",
|
||||
"addGroup": "Add Group",
|
||||
"addLink": "Add Link",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
"resetView": "Reset View",
|
||||
"selectHost": "Select Host",
|
||||
"chooseHost": "Choose a host...",
|
||||
"parentGroup": "Parent Group",
|
||||
"noGroup": "No Group",
|
||||
"groupName": "Group Name",
|
||||
"color": "Color",
|
||||
"source": "Source",
|
||||
"target": "Target",
|
||||
"moveToGroup": "Move to Group",
|
||||
"selectGroup": "Select group...",
|
||||
"addConnection": "Add Connection",
|
||||
"hostDetails": "Host Details",
|
||||
"removeFromGroup": "Remove from Group",
|
||||
"addHostHere": "Add Host Here",
|
||||
"editGroup": "Edit Group",
|
||||
"delete": "Delete",
|
||||
"add": "Add",
|
||||
"create": "Create",
|
||||
"move": "Move",
|
||||
"connect": "Connect",
|
||||
"createGroup": "Create Group",
|
||||
"selectSourcePlaceholder": "Select Source...",
|
||||
"selectTargetPlaceholder": "Select Target...",
|
||||
"invalidFile": "Invalid File",
|
||||
"hostAlreadyExists": "Host is already in the topology",
|
||||
"connectionExists": "Connection already exists",
|
||||
"unknown": "Unknown",
|
||||
"name": "Name",
|
||||
"ip": "IP",
|
||||
"status": "Status",
|
||||
"failedToAddNode": "Failed to add node",
|
||||
"sourceDifferentFromTarget": "Source and target must be different",
|
||||
"exportJSON": "Export JSON",
|
||||
"importJSON": "Import JSON",
|
||||
"serverStats": "Host Metrics",
|
||||
"noNodes": "No nodes yet"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Network Graph"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Network Graph"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} nodes"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "View docs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Add Host",
|
||||
"addGroup": "Add Group",
|
||||
"addLink": "Add Link",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
"resetView": "Reset View",
|
||||
"selectHost": "Select Host",
|
||||
"chooseHost": "Choose a host...",
|
||||
"parentGroup": "Parent Group",
|
||||
"noGroup": "No Group",
|
||||
"groupName": "Group Name",
|
||||
"color": "Color",
|
||||
"source": "Source",
|
||||
"target": "Target",
|
||||
"moveToGroup": "Move to Group",
|
||||
"selectGroup": "Select group...",
|
||||
"addConnection": "Add Connection",
|
||||
"hostDetails": "Host Details",
|
||||
"removeFromGroup": "Remove from Group",
|
||||
"addHostHere": "Add Host Here",
|
||||
"editGroup": "Edit Group",
|
||||
"delete": "Delete",
|
||||
"add": "Add",
|
||||
"create": "Create",
|
||||
"move": "Move",
|
||||
"connect": "Connect",
|
||||
"createGroup": "Create Group",
|
||||
"selectSourcePlaceholder": "Select Source...",
|
||||
"selectTargetPlaceholder": "Select Target...",
|
||||
"invalidFile": "Invalid File",
|
||||
"hostAlreadyExists": "Host is already in the topology",
|
||||
"connectionExists": "Connection already exists",
|
||||
"unknown": "Unknown",
|
||||
"name": "Name",
|
||||
"ip": "IP",
|
||||
"status": "Status",
|
||||
"failedToAddNode": "Failed to add node",
|
||||
"sourceDifferentFromTarget": "Source and target must be different",
|
||||
"exportJSON": "Export JSON",
|
||||
"importJSON": "Import JSON",
|
||||
"serverStats": "Host Metrics",
|
||||
"noNodes": "No nodes yet"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Network Graph"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Network Graph"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} nodes"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "View docs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Add Host",
|
||||
"addGroup": "Add Group",
|
||||
"addLink": "Add Link",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
"resetView": "Reset View",
|
||||
"selectHost": "Select Host",
|
||||
"chooseHost": "Choose a host...",
|
||||
"parentGroup": "Parent Group",
|
||||
"noGroup": "No Group",
|
||||
"groupName": "Group Name",
|
||||
"color": "Color",
|
||||
"source": "Source",
|
||||
"target": "Target",
|
||||
"moveToGroup": "Move to Group",
|
||||
"selectGroup": "Select group...",
|
||||
"addConnection": "Add Connection",
|
||||
"hostDetails": "Host Details",
|
||||
"removeFromGroup": "Remove from Group",
|
||||
"addHostHere": "Add Host Here",
|
||||
"editGroup": "Edit Group",
|
||||
"delete": "Delete",
|
||||
"add": "Add",
|
||||
"create": "Create",
|
||||
"move": "Move",
|
||||
"connect": "Connect",
|
||||
"createGroup": "Create Group",
|
||||
"selectSourcePlaceholder": "Select Source...",
|
||||
"selectTargetPlaceholder": "Select Target...",
|
||||
"invalidFile": "Invalid File",
|
||||
"hostAlreadyExists": "Host is already in the topology",
|
||||
"connectionExists": "Connection already exists",
|
||||
"unknown": "Unknown",
|
||||
"name": "Name",
|
||||
"ip": "IP",
|
||||
"status": "Status",
|
||||
"failedToAddNode": "Failed to add node",
|
||||
"sourceDifferentFromTarget": "Source and target must be different",
|
||||
"exportJSON": "Export JSON",
|
||||
"importJSON": "Import JSON",
|
||||
"serverStats": "Host Metrics",
|
||||
"noNodes": "No nodes yet"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Network Graph"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Network Graph"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} nodes"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "View docs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Sunucu Ekle",
|
||||
"addGroup": "Grup Ekle",
|
||||
"addLink": "Bağlantı Ekle",
|
||||
"zoomIn": "Yakınlaştır",
|
||||
"zoomOut": "Uzaklaştır",
|
||||
"resetView": "Görünümü Sıfırla",
|
||||
"selectHost": "Sunucu Seç",
|
||||
"chooseHost": "Bir sunucu seçin...",
|
||||
"parentGroup": "Üst Grup",
|
||||
"noGroup": "Grup Yok",
|
||||
"groupName": "Grup Adı",
|
||||
"color": "Renk",
|
||||
"source": "Kaynak",
|
||||
"target": "Hedef",
|
||||
"moveToGroup": "Gruba Taşı",
|
||||
"selectGroup": "Grup seçin...",
|
||||
"addConnection": "Bağlantı Ekle",
|
||||
"hostDetails": "Sunucu Detayları",
|
||||
"removeFromGroup": "Gruptan Kaldır",
|
||||
"addHostHere": "Buraya Sunucu Ekle",
|
||||
"editGroup": "Grubu Düzenle",
|
||||
"delete": "Sil",
|
||||
"add": "Ekle",
|
||||
"create": "Oluştur",
|
||||
"move": "Taşı",
|
||||
"connect": "Bağlan",
|
||||
"createGroup": "Grup Oluştur",
|
||||
"selectSourcePlaceholder": "Kaynak Seçin...",
|
||||
"selectTargetPlaceholder": "Hedef Seçin...",
|
||||
"invalidFile": "Geçersiz Dosya",
|
||||
"hostAlreadyExists": "Sunucu zaten topolojide var",
|
||||
"connectionExists": "Bağlantı zaten var",
|
||||
"unknown": "Bilinmiyor",
|
||||
"name": "Ad",
|
||||
"ip": "IP",
|
||||
"status": "Durum",
|
||||
"failedToAddNode": "Düğüm eklenemedi",
|
||||
"sourceDifferentFromTarget": "Kaynak ve hedef farklı olmalıdır",
|
||||
"exportJSON": "JSON Dışa Aktar",
|
||||
"importJSON": "JSON İçe Aktar",
|
||||
"serverStats": "Sunucu Metrikleri",
|
||||
"noNodes": "Henüz düğüm yok"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Ağ Grafiği"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Ağ Grafiği"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} düğüm"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Dokümanları görüntüle"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Add Host",
|
||||
"addGroup": "Add Group",
|
||||
"addLink": "Add Link",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
"resetView": "Reset View",
|
||||
"selectHost": "Select Host",
|
||||
"chooseHost": "Choose a host...",
|
||||
"parentGroup": "Parent Group",
|
||||
"noGroup": "No Group",
|
||||
"groupName": "Group Name",
|
||||
"color": "Color",
|
||||
"source": "Source",
|
||||
"target": "Target",
|
||||
"moveToGroup": "Move to Group",
|
||||
"selectGroup": "Select group...",
|
||||
"addConnection": "Add Connection",
|
||||
"hostDetails": "Host Details",
|
||||
"removeFromGroup": "Remove from Group",
|
||||
"addHostHere": "Add Host Here",
|
||||
"editGroup": "Edit Group",
|
||||
"delete": "Delete",
|
||||
"add": "Add",
|
||||
"create": "Create",
|
||||
"move": "Move",
|
||||
"connect": "Connect",
|
||||
"createGroup": "Create Group",
|
||||
"selectSourcePlaceholder": "Select Source...",
|
||||
"selectTargetPlaceholder": "Select Target...",
|
||||
"invalidFile": "Invalid File",
|
||||
"hostAlreadyExists": "Host is already in the topology",
|
||||
"connectionExists": "Connection already exists",
|
||||
"unknown": "Unknown",
|
||||
"name": "Name",
|
||||
"ip": "IP",
|
||||
"status": "Status",
|
||||
"failedToAddNode": "Failed to add node",
|
||||
"sourceDifferentFromTarget": "Source and target must be different",
|
||||
"exportJSON": "Export JSON",
|
||||
"importJSON": "Import JSON",
|
||||
"serverStats": "Host Metrics",
|
||||
"noNodes": "No nodes yet"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Network Graph"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Network Graph"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} nodes"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "View docs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "Thêm máy chủ",
|
||||
"addGroup": "Thêm nhóm",
|
||||
"addLink": "Thêm liên kết",
|
||||
"zoomIn": "Phóng to",
|
||||
"zoomOut": "Thu nhỏ",
|
||||
"resetView": "Đặt lại khung nhìn",
|
||||
"selectHost": "Chọn máy chủ",
|
||||
"chooseHost": "Chọn một máy chủ...",
|
||||
"parentGroup": "Nhóm cha",
|
||||
"noGroup": "Không nhóm",
|
||||
"groupName": "Tên nhóm",
|
||||
"color": "Màu sắc",
|
||||
"source": "Nguồn",
|
||||
"target": "Đích",
|
||||
"moveToGroup": "Di chuyển vào nhóm",
|
||||
"selectGroup": "Chọn nhóm...",
|
||||
"addConnection": "Thêm kết nối",
|
||||
"hostDetails": "Chi tiết máy chủ",
|
||||
"removeFromGroup": "Xóa khỏi nhóm",
|
||||
"addHostHere": "Thêm máy chủ tại đây",
|
||||
"editGroup": "Sửa nhóm",
|
||||
"delete": "Xóa",
|
||||
"add": "Thêm",
|
||||
"create": "Tạo",
|
||||
"move": "Di chuyển",
|
||||
"connect": "Kết nối",
|
||||
"createGroup": "Tạo nhóm",
|
||||
"selectSourcePlaceholder": "Chọn nguồn...",
|
||||
"selectTargetPlaceholder": "Chọn đích...",
|
||||
"invalidFile": "Tệp không hợp lệ",
|
||||
"hostAlreadyExists": "Máy chủ đã có trong cấu trúc liên kết",
|
||||
"connectionExists": "Kết nối đã tồn tại",
|
||||
"unknown": "Không xác định",
|
||||
"name": "Tên",
|
||||
"ip": "IP",
|
||||
"status": "Trạng thái",
|
||||
"failedToAddNode": "Không thể thêm nút",
|
||||
"sourceDifferentFromTarget": "Nguồn và đích phải khác nhau",
|
||||
"exportJSON": "Xuất JSON",
|
||||
"importJSON": "Nhập JSON",
|
||||
"serverStats": "Số liệu máy chủ",
|
||||
"noNodes": "Chưa có nút nào"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "Biểu đồ mạng"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "Biểu đồ Mạng"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} node"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "Xem tài liệu"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "添加主机",
|
||||
"addGroup": "添加分组",
|
||||
"addLink": "添加链接",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "缩小",
|
||||
"resetView": "重置视图",
|
||||
"selectHost": "选择主机",
|
||||
"chooseHost": "选择主机...",
|
||||
"parentGroup": "父分组",
|
||||
"noGroup": "无分组",
|
||||
"groupName": "分组名称",
|
||||
"color": "颜色",
|
||||
"source": "源",
|
||||
"target": "目标",
|
||||
"moveToGroup": "移至分组",
|
||||
"selectGroup": "选择分组...",
|
||||
"addConnection": "添加连接",
|
||||
"hostDetails": "主机详情",
|
||||
"removeFromGroup": "移出群组",
|
||||
"addHostHere": "在此添加主机",
|
||||
"editGroup": "编辑群组",
|
||||
"delete": "删除",
|
||||
"add": "添加",
|
||||
"create": "创建",
|
||||
"move": "移动",
|
||||
"connect": "连接",
|
||||
"createGroup": "创建群组",
|
||||
"selectSourcePlaceholder": "选择源...",
|
||||
"selectTargetPlaceholder": "选择目标...",
|
||||
"invalidFile": "无效文件",
|
||||
"hostAlreadyExists": "主机已在拓扑中",
|
||||
"connectionExists": "连接已存在",
|
||||
"unknown": "未知",
|
||||
"name": "名称",
|
||||
"ip": "IP",
|
||||
"status": "状态",
|
||||
"failedToAddNode": "添加节点失败",
|
||||
"sourceDifferentFromTarget": "源和目标必须不同",
|
||||
"exportJSON": "导出 JSON",
|
||||
"importJSON": "导入 JSON",
|
||||
"serverStats": "主机指标",
|
||||
"noNodes": "尚无节点"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "网络拓扑图"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "网络图表"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} 个节点"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "查看文档"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"networkGraph": {
|
||||
"addHost": "新增主機",
|
||||
"addGroup": "新增群組",
|
||||
"addLink": "新增連結",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "縮小",
|
||||
"resetView": "重置檢視",
|
||||
"selectHost": "選取主機",
|
||||
"chooseHost": "選擇主機...",
|
||||
"parentGroup": "父群組",
|
||||
"noGroup": "無群組",
|
||||
"groupName": "群組名稱",
|
||||
"color": "顏色",
|
||||
"source": "來源",
|
||||
"target": "目標",
|
||||
"moveToGroup": "移至群組",
|
||||
"selectGroup": "選取群組...",
|
||||
"addConnection": "新增連線",
|
||||
"hostDetails": "主機詳細資料",
|
||||
"removeFromGroup": "從群組中移除",
|
||||
"addHostHere": "在此新增主機",
|
||||
"editGroup": "編輯群組",
|
||||
"delete": "刪除",
|
||||
"add": "新增",
|
||||
"create": "建立",
|
||||
"move": "移動",
|
||||
"connect": "連線",
|
||||
"createGroup": "建立群組",
|
||||
"selectSourcePlaceholder": "選取來源...",
|
||||
"selectTargetPlaceholder": "選取目標...",
|
||||
"invalidFile": "無效的檔案",
|
||||
"hostAlreadyExists": "主機已存在於拓撲中",
|
||||
"connectionExists": "連線已存在",
|
||||
"unknown": "未知",
|
||||
"name": "名稱",
|
||||
"ip": "IP",
|
||||
"status": "狀態",
|
||||
"failedToAddNode": "無法新增節點",
|
||||
"sourceDifferentFromTarget": "來源與目標必須不同",
|
||||
"exportJSON": "匯出 JSON",
|
||||
"importJSON": "匯入 JSON",
|
||||
"serverStats": "主機指標",
|
||||
"noNodes": "尚無節點"
|
||||
},
|
||||
"nav": {
|
||||
"networkGraph": "網路圖表"
|
||||
},
|
||||
"dashboard": {
|
||||
"networkGraph": "網路圖表"
|
||||
},
|
||||
"dashboardTab": {
|
||||
"nodes": "{{count}} 個節點"
|
||||
},
|
||||
"permissions": {
|
||||
"use": {
|
||||
"title": "Use network graph",
|
||||
"description": "View and edit the network topology graph."
|
||||
}
|
||||
},
|
||||
"hosts": {
|
||||
"docsLink": "檢視文件"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"id": "network-topology",
|
||||
"name": "Network Topology",
|
||||
"version": "1.0.0",
|
||||
"description": "Show your hosts and how they connect as an interactive graph.",
|
||||
"author": {
|
||||
"name": "Termix"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/Termix-SSH/Termix",
|
||||
"category": "Infrastructure",
|
||||
"icon": "Network",
|
||||
"engine": {
|
||||
"termix": ">=2.9.0",
|
||||
"api": "1"
|
||||
},
|
||||
"capabilities": ["db:own", "network:serve", "ui:surface"],
|
||||
"provides": [
|
||||
{
|
||||
"service": "network-topology.graph",
|
||||
"version": "1.0.0",
|
||||
"permission": "network-topology.use"
|
||||
}
|
||||
],
|
||||
"contributes": {
|
||||
"tabs": [
|
||||
{
|
||||
"id": "network_graph",
|
||||
"titleKey": "nav.networkGraph",
|
||||
"icon": "Network",
|
||||
"openFrom": ["rail", "palette"]
|
||||
}
|
||||
],
|
||||
"dashboardCards": [
|
||||
{
|
||||
"id": "network_graph",
|
||||
"titleKey": "dashboard.networkGraph",
|
||||
"icon": "Network"
|
||||
}
|
||||
],
|
||||
"permissions": [
|
||||
{
|
||||
"name": "use",
|
||||
"titleKey": "permissions.use.title",
|
||||
"descriptionKey": "permissions.use.description",
|
||||
"defaultRoles": ["admin", "user"]
|
||||
}
|
||||
],
|
||||
"syncEntities": ["networkTopology"]
|
||||
},
|
||||
"locales": "locales",
|
||||
"platforms": ["linux", "win32", "darwin"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
-- network-topology 0001: adopt_network_topology
|
||||
-- Generated by termix-plugin migrations. Review before committing.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `network_topology` (
|
||||
`id` int AUTO_INCREMENT PRIMARY KEY,
|
||||
`user_id` varchar(255) NOT NULL,
|
||||
`topology` text,
|
||||
`created_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
`updated_at` text NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
);
|
||||
RENAME TABLE `network_topology` TO `p_network_topology_graphs`;
|
||||
@@ -0,0 +1,12 @@
|
||||
-- network-topology 0001: adopt_network_topology
|
||||
-- Generated by termix-plugin migrations. Review before committing.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "network_topology" (
|
||||
"id" serial PRIMARY KEY,
|
||||
"user_id" varchar(255) NOT NULL,
|
||||
"topology" text,
|
||||
"created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE
|
||||
);
|
||||
ALTER TABLE "network_topology" RENAME TO "p_network_topology_graphs";
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"version": 1,
|
||||
"tables": {
|
||||
"graphs": {
|
||||
"columns": {
|
||||
"id": {
|
||||
"type": "id",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"userId": {
|
||||
"type": "refUser",
|
||||
"length": 255,
|
||||
"notNull": true
|
||||
},
|
||||
"topology": {
|
||||
"type": "text"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "timestamp",
|
||||
"notNull": true,
|
||||
"defaultNow": true
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "timestamp",
|
||||
"notNull": true,
|
||||
"defaultNow": true
|
||||
}
|
||||
},
|
||||
"indexes": [],
|
||||
"adopts": "network_topology"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
-- network-topology 0001: adopt_network_topology
|
||||
-- Generated by termix-plugin migrations. Review before committing.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "network_topology" (
|
||||
"id" integer PRIMARY KEY AUTOINCREMENT,
|
||||
"user_id" text NOT NULL,
|
||||
"topology" text,
|
||||
"created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE
|
||||
);
|
||||
ALTER TABLE "network_topology" RENAME TO "p_network_topology_graphs";
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@termix-plugin/network-topology",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "termix-plugin build",
|
||||
"test": "termix-plugin test",
|
||||
"typecheck": "tsc -p .",
|
||||
"validate": "termix-plugin validate"
|
||||
},
|
||||
"dependencies": {
|
||||
"cytoscape": "^3.34.3",
|
||||
"lucide-react": "^1.33.0",
|
||||
"react-cytoscapejs": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@termix/plugin-sdk": "^1.0.0",
|
||||
"@types/node": "^26.2.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"better-sqlite3": "^13.0.3",
|
||||
"jsdom": "^30.0.1",
|
||||
"vitest": "^4.1.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@termix/plugin-sdk": "^1.0.0",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"express": "^5.2.1",
|
||||
"react": "^19.2.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { Router } from "express";
|
||||
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||
import { graphs } from "./tables.js";
|
||||
import { createGraphRepository } from "./repository.js";
|
||||
import { registerGraphRoutes } from "./routes.js";
|
||||
import { mapTopologyHostIds } from "./topology-host-ids.js";
|
||||
|
||||
/** What other plugins get from ctx.services.get("network-topology.graph"). */
|
||||
export interface NetworkTopologyService {
|
||||
/** The calling user's saved topology, or null if they have none. */
|
||||
get: () => Promise<unknown | null>;
|
||||
}
|
||||
|
||||
export async function activate(ctx: PluginContext) {
|
||||
const table = await ctx.db.define(graphs);
|
||||
const repo = createGraphRepository(ctx.db, table);
|
||||
|
||||
registerGraphRoutes(ctx.http.router<Router>(), repo, ctx);
|
||||
|
||||
ctx.sync.registerEntity({
|
||||
type: "networkTopology",
|
||||
table,
|
||||
order: 100,
|
||||
singleton: true,
|
||||
// Host ids live inside the topology JSON rather than in a column, so the
|
||||
// generic reference machinery cannot reach them.
|
||||
serialize: async (row, resolveSyncId) => ({
|
||||
...row,
|
||||
topology: await mapTopologyHostIds(
|
||||
row.topology as string | null | undefined,
|
||||
async (id) => {
|
||||
const numericId = Number(id);
|
||||
if (!Number.isInteger(numericId)) return null;
|
||||
return resolveSyncId("hosts", numericId);
|
||||
},
|
||||
),
|
||||
}),
|
||||
deserialize: async (row, resolveId) => ({
|
||||
...row,
|
||||
topology: await mapTopologyHostIds(
|
||||
row.topology as string | null | undefined,
|
||||
async (syncId) => {
|
||||
const id = await resolveId("hosts", syncId);
|
||||
return id === null ? null : String(id);
|
||||
},
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
const service: NetworkTopologyService = {
|
||||
get: async () => {
|
||||
const userId = ctx.currentActor();
|
||||
if (!userId) return null;
|
||||
const record = await repo.findByUserId(userId);
|
||||
if (!record?.topology) return null;
|
||||
try {
|
||||
return JSON.parse(record.topology);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
ctx.services.provide("network-topology.graph", service);
|
||||
|
||||
ctx.log.info(
|
||||
"Network Topology routes mounted at /plugin-api/network-topology",
|
||||
);
|
||||
}
|
||||
|
||||
export async function deactivate() {
|
||||
// Everything above was registered through ctx and is disposed by core.
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { PluginDatabase } from "@termix/plugin-sdk/backend";
|
||||
|
||||
export interface GraphRecord {
|
||||
id: number;
|
||||
userId: string;
|
||||
topology: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// The table comes from ctx.db.define, which the SDK hands back untyped, and
|
||||
// the drizzle handle is the server's own. Typed at this module's edge instead.
|
||||
type Table = any;
|
||||
type Drizzle = any;
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export type GraphRepository = ReturnType<typeof createGraphRepository>;
|
||||
|
||||
/** Per-user network topology storage, one row per user. */
|
||||
export function createGraphRepository(db: PluginDatabase, table: Table) {
|
||||
const client = () => db.client<Drizzle>();
|
||||
|
||||
return {
|
||||
async findByUserId(userId: string): Promise<GraphRecord | null> {
|
||||
const drizzle = await client();
|
||||
const rows = await drizzle
|
||||
.select()
|
||||
.from(table)
|
||||
.where(eq(table.userId, userId))
|
||||
.limit(1);
|
||||
return (rows[0] as GraphRecord) ?? null;
|
||||
},
|
||||
|
||||
async upsertForUser(userId: string, topology: string): Promise<void> {
|
||||
const drizzle = await client();
|
||||
const now = new Date().toISOString();
|
||||
const rows = await drizzle
|
||||
.select()
|
||||
.from(table)
|
||||
.where(eq(table.userId, userId))
|
||||
.limit(1);
|
||||
|
||||
if (rows[0]) {
|
||||
await drizzle
|
||||
.update(table)
|
||||
.set({ topology, updatedAt: now })
|
||||
.where(eq(table.userId, userId));
|
||||
} else {
|
||||
await drizzle
|
||||
.insert(table)
|
||||
.values({ userId, topology, createdAt: now, updatedAt: now });
|
||||
}
|
||||
await db.persist();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { Request, Response, Router } from "express";
|
||||
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||
import type { GraphRepository } from "./repository.js";
|
||||
|
||||
function actor(ctx: PluginContext): string {
|
||||
// Core's plugin router authenticates every request and runs it as that user.
|
||||
return ctx.currentActor() as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mounts the graph routes on the plugin's router, which core serves at
|
||||
* /plugin-api/network-topology with auth in front.
|
||||
*/
|
||||
export function registerGraphRoutes(
|
||||
router: Router,
|
||||
repo: GraphRepository,
|
||||
ctx: PluginContext,
|
||||
): void {
|
||||
router.use(ctx.rbac.require("use") as never);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/network-topology:
|
||||
* get:
|
||||
* summary: Get network topology for authenticated user
|
||||
* description: Retrieves the saved network topology graph (nodes and edges) for the current user. Returns null if no topology exists.
|
||||
* tags:
|
||||
* - Network Topology
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Network topology retrieved successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* nullable: true
|
||||
* properties:
|
||||
* nodes:
|
||||
* type: array
|
||||
* description: Array of graph nodes (hosts and groups)
|
||||
* edges:
|
||||
* type: array
|
||||
* description: Array of graph edges (connections)
|
||||
*/
|
||||
router.get("/", async (req: Request, res: Response) => {
|
||||
const userId = actor(ctx);
|
||||
try {
|
||||
const record = await repo.findByUserId(userId);
|
||||
const topology = record?.topology ? JSON.parse(record.topology) : null;
|
||||
res.json(topology);
|
||||
} catch (err) {
|
||||
ctx.log.error(
|
||||
"Failed to fetch network topology",
|
||||
err instanceof Error ? err : new Error(String(err)),
|
||||
);
|
||||
res.status(500).json({ error: "Failed to fetch network topology" });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/network-topology:
|
||||
* post:
|
||||
* summary: Save network topology for authenticated user
|
||||
* description: Saves or updates the network topology graph. Uses upsert logic - creates new record if none exists, updates existing record otherwise.
|
||||
* tags:
|
||||
* - Network Topology
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - topology
|
||||
* properties:
|
||||
* topology:
|
||||
* type: object
|
||||
* description: Network topology data containing nodes and edges
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Topology saved successfully
|
||||
* 400:
|
||||
* description: Invalid topology data
|
||||
*/
|
||||
router.post("/", async (req: Request, res: Response) => {
|
||||
const userId = actor(ctx);
|
||||
const { topology } = req.body ?? {};
|
||||
if (!topology) {
|
||||
return res.status(400).json({ error: "Topology data is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const topologyStr =
|
||||
typeof topology === "string" ? topology : JSON.stringify(topology);
|
||||
await repo.upsertForUser(userId, topologyStr);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
ctx.log.error(
|
||||
"Failed to save network topology",
|
||||
err instanceof Error ? err : new Error(String(err)),
|
||||
);
|
||||
res.status(500).json({ error: "Failed to save network topology" });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
adoptLegacyTable,
|
||||
defineTable,
|
||||
id,
|
||||
refUser,
|
||||
text,
|
||||
timestamp,
|
||||
} from "@termix/plugin-sdk/db";
|
||||
|
||||
/**
|
||||
* One row per user, holding their saved graph as a JSON blob.
|
||||
*
|
||||
* Adopted from core's network_topology, so the column names are the legacy
|
||||
* ones: the rename carries the existing rows across.
|
||||
*/
|
||||
export const graphs = adoptLegacyTable(
|
||||
"network_topology",
|
||||
defineTable("graphs", {
|
||||
id: id(),
|
||||
userId: refUser(),
|
||||
topology: text(),
|
||||
createdAt: timestamp().notNull().defaultNow(),
|
||||
updatedAt: timestamp().notNull().defaultNow(),
|
||||
}),
|
||||
);
|
||||
|
||||
export const tables = [graphs];
|
||||
@@ -0,0 +1,72 @@
|
||||
interface TopologyNode {
|
||||
data?: { id?: string; [key: string]: unknown };
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface TopologyEdge {
|
||||
data?: { source?: string; target?: string; [key: string]: unknown };
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface Topology {
|
||||
nodes?: TopologyNode[];
|
||||
edges?: TopologyEdge[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Node/edge ids in a topology are either a host's local numeric id (as a
|
||||
* string) or a client-generated group-<timestamp> id. Only the former is
|
||||
* meaningful across sides, so translate just those through the mapper and
|
||||
* leave group ids untouched. Nodes/edges that fail to resolve (host deleted,
|
||||
* or not synced to this side yet) are dropped rather than left dangling.
|
||||
*/
|
||||
export async function mapTopologyHostIds(
|
||||
topologyJson: string | null | undefined,
|
||||
mapId: (id: string) => Promise<string | null>,
|
||||
): Promise<string | null> {
|
||||
if (!topologyJson) return topologyJson ?? null;
|
||||
|
||||
let topology: Topology;
|
||||
try {
|
||||
topology = JSON.parse(topologyJson);
|
||||
} catch {
|
||||
return topologyJson;
|
||||
}
|
||||
|
||||
const isGroupId = (id: string) => id.startsWith("group-");
|
||||
const idMap = new Map<string, string | null>();
|
||||
|
||||
const resolve = async (id: string): Promise<string | null> => {
|
||||
if (isGroupId(id)) return id;
|
||||
if (!idMap.has(id)) idMap.set(id, await mapId(id));
|
||||
return idMap.get(id) ?? null;
|
||||
};
|
||||
|
||||
const nodes: TopologyNode[] = [];
|
||||
for (const node of topology.nodes ?? []) {
|
||||
const id = node.data?.id;
|
||||
if (typeof id !== "string") continue;
|
||||
const mapped = await resolve(id);
|
||||
if (mapped === null) continue;
|
||||
nodes.push({ ...node, data: { ...node.data, id: mapped } });
|
||||
}
|
||||
|
||||
const nodeIds = new Set(nodes.map((n) => n.data?.id));
|
||||
const edges: TopologyEdge[] = [];
|
||||
for (const edge of topology.edges ?? []) {
|
||||
const source = edge.data?.source;
|
||||
const target = edge.data?.target;
|
||||
if (typeof source !== "string" || typeof target !== "string") continue;
|
||||
const mappedSource = await resolve(source);
|
||||
const mappedTarget = await resolve(target);
|
||||
if (mappedSource === null || mappedTarget === null) continue;
|
||||
if (!nodeIds.has(mappedSource) || !nodeIds.has(mappedTarget)) continue;
|
||||
edges.push({
|
||||
...edge,
|
||||
data: { ...edge.data, source: mappedSource, target: mappedTarget },
|
||||
});
|
||||
}
|
||||
|
||||
return JSON.stringify({ ...topology, nodes, edges });
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,58 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { Network } from "lucide-react";
|
||||
import type {
|
||||
DashboardCardProps,
|
||||
TabProps,
|
||||
TermixApp,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { NetworkGraphCard } from "./NetworkGraphCard";
|
||||
|
||||
const VIEW_ID = "network_graph";
|
||||
|
||||
function GraphTab({ isVisible }: TabProps) {
|
||||
return <NetworkGraphCard embedded={false} isVisible={isVisible} />;
|
||||
}
|
||||
|
||||
function GraphCard({ isVisible, shell }: DashboardCardProps) {
|
||||
// A stable callback lets the memoized graph skip dashboard re-renders.
|
||||
const shellRef = useRef(shell);
|
||||
useEffect(() => {
|
||||
shellRef.current = shell;
|
||||
});
|
||||
const openInNewTab = useCallback(
|
||||
() => shellRef.current.openSingletonTab(VIEW_ID),
|
||||
[],
|
||||
);
|
||||
return (
|
||||
<NetworkGraphCard
|
||||
embedded={true}
|
||||
isVisible={isVisible}
|
||||
onOpenInNewTab={openInNewTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function activate(app: TermixApp): void {
|
||||
app.registerTab(VIEW_ID, GraphTab, {
|
||||
icon: Network,
|
||||
titleKey: "nav.networkGraph",
|
||||
singleton: true,
|
||||
hostless: true,
|
||||
});
|
||||
|
||||
app.registerRailItem({
|
||||
id: VIEW_ID,
|
||||
icon: Network,
|
||||
titleKey: "nav.networkGraph",
|
||||
kind: "tab",
|
||||
after: "local-terminal",
|
||||
permission: "use",
|
||||
});
|
||||
|
||||
app.registerDashboardCard({
|
||||
id: VIEW_ID,
|
||||
titleKey: "dashboard.networkGraph",
|
||||
defaultHeight: 350,
|
||||
component: GraphCard,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { PluginApiClient } from "@termix/plugin-sdk/frontend";
|
||||
|
||||
export interface NetworkTopologyNode {
|
||||
data: {
|
||||
id: string;
|
||||
label?: string;
|
||||
ip?: string;
|
||||
status?: string;
|
||||
tags?: string[];
|
||||
parent?: string;
|
||||
color?: string;
|
||||
/** Absent on nodes; callers tell nodes from edges by testing these. */
|
||||
source?: undefined;
|
||||
target?: undefined;
|
||||
};
|
||||
position?: { x: number; y: number };
|
||||
}
|
||||
|
||||
export interface NetworkTopologyEdge {
|
||||
data: {
|
||||
id?: string;
|
||||
source: string;
|
||||
target: string;
|
||||
label?: undefined;
|
||||
ip?: undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NetworkTopologyData {
|
||||
nodes: NetworkTopologyNode[];
|
||||
edges: NetworkTopologyEdge[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The topology routes, through the plugin's own client. Paths are relative
|
||||
* to /plugin-api/network-topology/, which the client already points at.
|
||||
*/
|
||||
export function createNetworkTopologyApi(api: PluginApiClient) {
|
||||
return {
|
||||
get: async (): Promise<NetworkTopologyData | null> => {
|
||||
const response = await api.get<NetworkTopologyData | null>("/");
|
||||
return response.data;
|
||||
},
|
||||
save: async (
|
||||
topology: NetworkTopologyData,
|
||||
): Promise<{ success: boolean }> => {
|
||||
const response = await api.post<{ success: boolean }>("/", {
|
||||
topology,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type NetworkTopologyApi = ReturnType<typeof createNetworkTopologyApi>;
|
||||
@@ -0,0 +1,81 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import express from "express";
|
||||
import {
|
||||
createMockCtx,
|
||||
createTestDb,
|
||||
type TestDb,
|
||||
} from "@termix/plugin-sdk/testing";
|
||||
import { PluginCapabilityError } from "@termix/plugin-sdk/backend";
|
||||
import { activate } from "../../src/backend/index.js";
|
||||
import { manifest, pluginDir, startServer, type TestServer } from "./helpers";
|
||||
|
||||
let db: TestDb | null = null;
|
||||
let server: TestServer | null = null;
|
||||
|
||||
afterEach(async () => {
|
||||
db?.close();
|
||||
db = null;
|
||||
await server?.close();
|
||||
server = null;
|
||||
});
|
||||
|
||||
describe("network-topology activate", () => {
|
||||
it("registers a singleton sync entity with the JSON blob serializer", async () => {
|
||||
server = await startServer();
|
||||
const [entity] = server.mock.syncEntities;
|
||||
expect(entity.type).toBe("networkTopology");
|
||||
expect(entity.singleton).toBe(true);
|
||||
expect(entity.serialize).toBeTypeOf("function");
|
||||
expect(entity.deserialize).toBeTypeOf("function");
|
||||
});
|
||||
|
||||
it("offers the user's saved graph as a service", async () => {
|
||||
server = await startServer();
|
||||
await server.request("POST", "/", {
|
||||
body: { topology: { nodes: [{ data: { id: "1" } }], edges: [] } },
|
||||
});
|
||||
|
||||
const service = server.mock.services.get("network-topology.graph") as {
|
||||
get: () => Promise<unknown>;
|
||||
};
|
||||
const result = await server.mock.actAs("user-1", () => service.get());
|
||||
expect(result).toEqual({ nodes: [{ data: { id: "1" } }], edges: [] });
|
||||
});
|
||||
|
||||
it("returns null from the service when the user has no saved graph", async () => {
|
||||
server = await startServer();
|
||||
const service = server.mock.services.get("network-topology.graph") as {
|
||||
get: () => Promise<unknown>;
|
||||
};
|
||||
const result = await server.mock.actAs("user-1", () => service.get());
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("fails closed without db:own", async () => {
|
||||
db = await createTestDb(pluginDir);
|
||||
const mock = createMockCtx({
|
||||
pluginId: manifest.id,
|
||||
manifest,
|
||||
capabilities: ["network:serve", "ui:surface"],
|
||||
db: db.database,
|
||||
router: () => express.Router(),
|
||||
});
|
||||
await expect(activate(mock.ctx)).rejects.toBeInstanceOf(
|
||||
PluginCapabilityError,
|
||||
);
|
||||
});
|
||||
|
||||
it("fails closed without network:serve", async () => {
|
||||
db = await createTestDb(pluginDir);
|
||||
const mock = createMockCtx({
|
||||
pluginId: manifest.id,
|
||||
manifest,
|
||||
capabilities: ["db:own", "ui:surface"],
|
||||
db: db.database,
|
||||
router: () => express.Router(),
|
||||
});
|
||||
await expect(activate(mock.ctx)).rejects.toBeInstanceOf(
|
||||
PluginCapabilityError,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import http from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import express, { type Router } from "express";
|
||||
import {
|
||||
createMockCtx,
|
||||
createTestDb,
|
||||
type MockPluginContext,
|
||||
type TestDb,
|
||||
} from "@termix/plugin-sdk/testing";
|
||||
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
|
||||
import manifestJson from "../../manifest.json";
|
||||
|
||||
export const pluginDir = fileURLToPath(new URL("../..", import.meta.url));
|
||||
export const manifest = manifestJson as unknown as PluginManifest;
|
||||
|
||||
export interface TestServer {
|
||||
db: TestDb;
|
||||
mock: MockPluginContext;
|
||||
request: (
|
||||
method: string,
|
||||
path: string,
|
||||
options?: { user?: string; body?: unknown },
|
||||
// Route bodies vary per test; asserting on them is the point.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) => Promise<{ status: number; body: any }>;
|
||||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The plugin activated against a real in-memory database, its router mounted
|
||||
* on an express app the way core mounts it: JSON parsed, the acting user set
|
||||
* from the request, everything else left to the plugin.
|
||||
*/
|
||||
export async function startServer(
|
||||
options: { permissions?: string[]; users?: string[] } = {},
|
||||
): Promise<TestServer> {
|
||||
const db = await createTestDb(pluginDir);
|
||||
for (const user of options.users ?? ["user-1", "user-2"]) {
|
||||
db.sqlite
|
||||
.prepare("INSERT INTO users (id, username) VALUES (?, ?)")
|
||||
.run(user, user);
|
||||
}
|
||||
|
||||
let router: Router | null = null;
|
||||
const mock = createMockCtx({
|
||||
pluginId: manifest.id,
|
||||
manifest,
|
||||
capabilities: manifest.capabilities,
|
||||
db: db.database,
|
||||
router: () => (router = express.Router()),
|
||||
permissions: options.permissions ?? ["network-topology.use"],
|
||||
});
|
||||
|
||||
const { activate } = await import("../../src/backend/index.js");
|
||||
await activate(mock.ctx);
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use((req, _res, next) => {
|
||||
mock.setActor(req.header("x-test-user") ?? undefined);
|
||||
next();
|
||||
});
|
||||
app.use((req, res, next) => router!(req, res, next));
|
||||
|
||||
const server = http.createServer(app);
|
||||
await new Promise<void>((resolve) => server.listen(0, resolve));
|
||||
const { port } = server.address() as AddressInfo;
|
||||
|
||||
return {
|
||||
db,
|
||||
mock,
|
||||
async request(method, path, { user = "user-1", body: given } = {}) {
|
||||
const body = method === "GET" ? undefined : given;
|
||||
const response = await fetch(`http://127.0.0.1:${port}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
"x-test-user": user,
|
||||
...(body !== undefined ? { "content-type": "application/json" } : {}),
|
||||
},
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
const text = await response.text();
|
||||
return { status: response.status, body: text ? JSON.parse(text) : null };
|
||||
},
|
||||
async close() {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
db.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createTestDb, type TestDb } from "@termix/plugin-sdk/testing";
|
||||
import { pluginDir } from "./helpers";
|
||||
|
||||
// network_topology as core's SQLite bootstrap created it before 2.9.0.
|
||||
const LEGACY_DDL = `
|
||||
CREATE TABLE network_topology (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
topology TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`;
|
||||
|
||||
let db: TestDb | null = null;
|
||||
|
||||
afterEach(() => {
|
||||
db?.close();
|
||||
db = null;
|
||||
});
|
||||
|
||||
const tableExists = (name: string) =>
|
||||
!!db!.sqlite
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
||||
.get(name);
|
||||
|
||||
describe("adopting network_topology", () => {
|
||||
it("keeps every existing row when the legacy table is there", async () => {
|
||||
db = await createTestDb(pluginDir, {
|
||||
before: (sqlite) => {
|
||||
sqlite.exec("INSERT INTO users (id) VALUES ('user-1'), ('user-2')");
|
||||
sqlite.exec(LEGACY_DDL);
|
||||
sqlite
|
||||
.prepare(
|
||||
"INSERT INTO network_topology (user_id, topology, updated_at) VALUES (?, ?, ?)",
|
||||
)
|
||||
.run(
|
||||
"user-1",
|
||||
'{"nodes":[{"data":{"id":"1"}}],"edges":[]}',
|
||||
"2026-01-01T00:00:00.000Z",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
expect(db.applied).toEqual(["0001_adopt_network_topology"]);
|
||||
expect(tableExists("network_topology")).toBe(false);
|
||||
expect(tableExists("p_network_topology_graphs")).toBe(true);
|
||||
|
||||
const rows = db.sqlite
|
||||
.prepare(
|
||||
"SELECT user_id, topology, updated_at FROM p_network_topology_graphs ORDER BY id",
|
||||
)
|
||||
.all();
|
||||
expect(rows).toEqual([
|
||||
{
|
||||
user_id: "user-1",
|
||||
topology: '{"nodes":[{"data":{"id":"1"}}],"edges":[]}',
|
||||
updated_at: "2026-01-01T00:00:00.000Z",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("creates the table fresh when there is no legacy one", async () => {
|
||||
db = await createTestDb(pluginDir);
|
||||
|
||||
expect(tableExists("network_topology")).toBe(false);
|
||||
expect(tableExists("p_network_topology_graphs")).toBe(true);
|
||||
expect(
|
||||
db.sqlite.prepare("SELECT * FROM p_network_topology_graphs").all(),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("removes a user's topology when the user is deleted", async () => {
|
||||
db = await createTestDb(pluginDir, {
|
||||
before: (sqlite) => {
|
||||
sqlite.exec("INSERT INTO users (id) VALUES ('user-1'), ('user-2')");
|
||||
},
|
||||
});
|
||||
const insert = db.sqlite.prepare(
|
||||
"INSERT INTO p_network_topology_graphs (user_id, topology) VALUES (?, ?)",
|
||||
);
|
||||
insert.run("user-1", "{}");
|
||||
insert.run("user-2", "{}");
|
||||
|
||||
db.sqlite.exec("DELETE FROM users WHERE id = 'user-1'");
|
||||
|
||||
const remaining = (
|
||||
db.sqlite
|
||||
.prepare("SELECT user_id FROM p_network_topology_graphs")
|
||||
.all() as { user_id: string }[]
|
||||
).map((row) => row.user_id);
|
||||
expect(remaining).toEqual(["user-2"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { createTestDb, type TestDb } from "@termix/plugin-sdk/testing";
|
||||
import { graphs } from "../../src/backend/tables.js";
|
||||
import {
|
||||
createGraphRepository,
|
||||
type GraphRepository,
|
||||
} from "../../src/backend/repository.js";
|
||||
import { pluginDir } from "./helpers";
|
||||
|
||||
let db: TestDb;
|
||||
let repo: GraphRepository;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = await createTestDb(pluginDir, {
|
||||
before: (sqlite) =>
|
||||
sqlite.exec("INSERT INTO users (id) VALUES ('user-1'), ('user-2')"),
|
||||
});
|
||||
repo = createGraphRepository(db.database, await db.database.define(graphs));
|
||||
});
|
||||
|
||||
afterEach(() => db.close());
|
||||
|
||||
describe("graph repository", () => {
|
||||
it("finds, creates, and updates topology by user id", async () => {
|
||||
expect(await repo.findByUserId("user-1")).toBeNull();
|
||||
|
||||
await repo.upsertForUser(
|
||||
"user-1",
|
||||
JSON.stringify({ nodes: [{ id: "host-1" }], edges: [] }),
|
||||
);
|
||||
expect(await repo.findByUserId("user-1")).toMatchObject({
|
||||
userId: "user-1",
|
||||
topology: '{"nodes":[{"id":"host-1"}],"edges":[]}',
|
||||
});
|
||||
|
||||
await repo.upsertForUser(
|
||||
"user-1",
|
||||
JSON.stringify({ nodes: [], edges: [{ id: "edge-1" }] }),
|
||||
);
|
||||
expect(await repo.findByUserId("user-1")).toMatchObject({
|
||||
userId: "user-1",
|
||||
topology: '{"nodes":[],"edges":[{"id":"edge-1"}]}',
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps rows for different users separate", async () => {
|
||||
await repo.upsertForUser("user-1", '{"nodes":[]}');
|
||||
await repo.upsertForUser("user-2", '{"nodes":["a"]}');
|
||||
|
||||
expect((await repo.findByUserId("user-1"))?.topology).toBe('{"nodes":[]}');
|
||||
expect((await repo.findByUserId("user-2"))?.topology).toBe(
|
||||
'{"nodes":["a"]}',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { startServer, type TestServer } from "./helpers";
|
||||
|
||||
let server: TestServer;
|
||||
|
||||
beforeEach(async () => {
|
||||
server = await startServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.close();
|
||||
});
|
||||
|
||||
describe("GET /", () => {
|
||||
it("returns null when the user has no saved topology", async () => {
|
||||
const res = await server.request("GET", "/");
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toBeNull();
|
||||
});
|
||||
|
||||
it("returns the saved topology", async () => {
|
||||
await server.request("POST", "/", {
|
||||
body: { topology: { nodes: [{ data: { id: "host-1" } }], edges: [] } },
|
||||
});
|
||||
|
||||
const res = await server.request("GET", "/");
|
||||
expect(res.body).toEqual({
|
||||
nodes: [{ data: { id: "host-1" } }],
|
||||
edges: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps each user's topology separate", async () => {
|
||||
await server.request("POST", "/", {
|
||||
user: "user-1",
|
||||
body: { topology: { nodes: [{ data: { id: "a" } }], edges: [] } },
|
||||
});
|
||||
|
||||
const res = await server.request("GET", "/", { user: "user-2" });
|
||||
expect(res.body).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /", () => {
|
||||
it("saves and overwrites the topology", async () => {
|
||||
const first = await server.request("POST", "/", {
|
||||
body: { topology: { nodes: [], edges: [] } },
|
||||
});
|
||||
expect(first.body).toEqual({ success: true });
|
||||
|
||||
await server.request("POST", "/", {
|
||||
body: { topology: { nodes: [{ data: { id: "a" } }], edges: [] } },
|
||||
});
|
||||
|
||||
const res = await server.request("GET", "/");
|
||||
expect(res.body).toEqual({ nodes: [{ data: { id: "a" } }], edges: [] });
|
||||
});
|
||||
|
||||
it("rejects a request with no topology", async () => {
|
||||
const res = await server.request("POST", "/", { body: {} });
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe("permissions", () => {
|
||||
it("refuses every route without network-topology.use", async () => {
|
||||
const denied = await startServer({ permissions: [] });
|
||||
try {
|
||||
expect((await denied.request("GET", "/")).status).toBe(403);
|
||||
expect(
|
||||
(
|
||||
await denied.request("POST", "/", {
|
||||
body: { topology: { nodes: [], edges: [] } },
|
||||
})
|
||||
).status,
|
||||
).toBe(403);
|
||||
} finally {
|
||||
await denied.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
renderWithApp,
|
||||
type RenderedPluginApp,
|
||||
} from "@termix/plugin-sdk/testing";
|
||||
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
|
||||
import * as plugin from "../../src/frontend/index";
|
||||
import manifestJson from "../../manifest.json";
|
||||
import locales from "../../locales/en.json";
|
||||
|
||||
const manifest = manifestJson as unknown as PluginManifest;
|
||||
|
||||
const declared = (views?: Array<{ id: string }>) =>
|
||||
(views ?? []).map((view) => view.id).sort();
|
||||
|
||||
let rendered: RenderedPluginApp | null = null;
|
||||
|
||||
afterEach(async () => {
|
||||
await rendered?.deactivate();
|
||||
rendered = null;
|
||||
});
|
||||
|
||||
describe(`${manifest.id} activate`, () => {
|
||||
it("activates and registers only views its manifest declares", async () => {
|
||||
rendered = await renderWithApp(plugin, { manifest, locales });
|
||||
const contributes = manifest.contributes as unknown as Record<
|
||||
string,
|
||||
Array<{ id: string }> | undefined
|
||||
>;
|
||||
const tabs = declared(contributes.tabs);
|
||||
// A rail panel may reuse one of the plugin's tab ids.
|
||||
const panels = [...declared(contributes.panels), ...tabs];
|
||||
const cards = declared(contributes.dashboardCards);
|
||||
for (const id of rendered.registered.tabs()) expect(tabs).toContain(id);
|
||||
for (const id of rendered.registered.panels()) expect(panels).toContain(id);
|
||||
for (const id of rendered.registered.dashboardCards()) {
|
||||
expect(cards).toContain(id);
|
||||
}
|
||||
});
|
||||
|
||||
it("removes everything it registered on deactivate", async () => {
|
||||
const app = await renderWithApp(plugin, { manifest, locales });
|
||||
await app.deactivate();
|
||||
expect(app.registered.tabs()).toEqual([]);
|
||||
expect(app.registered.panels()).toEqual([]);
|
||||
expect(app.registered.railItems()).toEqual([]);
|
||||
expect(app.registered.hostActions()).toEqual([]);
|
||||
expect(app.registered.hostEditorSections()).toEqual([]);
|
||||
expect(app.registered.dashboardCards()).toEqual([]);
|
||||
expect(app.registered.settingsComponents()).toEqual([]);
|
||||
expect(app.registered.actions()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@termix/plugin-sdk/tsconfig.plugin.json",
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset";
|
||||
|
||||
export default pluginVitestConfig(import.meta.url);
|
||||
Reference in new issue
Block a user