Compare commits

...
22 Commits
Author SHA1 Message Date
LukeGus f3fae63591 chore: changelog for merged PRs 2026-11-06 20:51:56 -06:00
LukeGus e033025657 fix: only run shell keybinding defaults from the shell 2026-11-06 20:49:47 -06:00
4584aff32b feat: let plugins store a CA-signed certificate on a credential (#1764)
A credential that carries an SSH key can already hold a CA-signed
certificate, but the only way to fill it in is the credentials editor.
A plugin that issues certificates has no way to keep the credential in
step, so after a renewal it keeps presenting the previous certificate
and the connection fails much later with a generic authentication error.

ctx.credentials.setSshKeyCertificate stores one on a key credential the
acting user owns, or clears it with null. It needs credentials:write and
the core credentials.update permission, refuses anything that is not a
key credential because core never sends a certificate for the other auth
types, and is audited like the other ctx.credentials writes.

Co-authored-by: Stefan Wienert <stefan@piratopedia.de>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
2026-11-06 20:49:36 -06:00
b912a31e00 feat: add Cmd-B sidebar toggle shortcut (#1775)
Co-authored-by: Dr. Lukas Metzger <lukas@mba.lan>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-11-06 20:48:20 -06:00
alexcastgonzandClaude Sonnet 5.5 9778b6e56a fix(build): ship the arm64 and armv7l sharp binaries in the Linux builds (#1771)
The Linux job runs on x64, so npm ci only installs @img/sharp-linux-x64 and
@img/sharp-libvips-linux-x64. The arm64 and armv7l AppImages then carry x64
binaries and fail to load sharp on start. Install the arm packages at the
versions sharp declares, as the Windows and macOS jobs already do.

Fixes #1769

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 20:47:11 -06:00
ZacharyZcR 803a52c339 feat: add macOS menu bar icon options (#1779) 2026-11-06 20:46:38 -06:00
ZacharyZcR 3630d6c157 fix: expose host editing through the plugin shell bridge (#1778) 2026-11-06 20:46:28 -06:00
ZacharyZcR 1173f3f576 fix: prefer AES-128-GCM for efficient SSH group exchange (#1776) 2026-11-06 20:46:22 -06:00
Lukas 4fe23b562f fix: restore the hosts import/export button label (#1767) 2026-11-06 20:46:05 -06:00
Lukas 11b3811ba6 fix: send credentials with the web app manifest request (#1766) 2026-11-06 20:46:01 -06:00
LukeGus 17e57c9981 fix: format bundled plugin pins with prettier 2026-11-06 20:45:52 -06:00
LukeGus f3e0c0ebc2 fix: one border color for controls, small button icons snap to whole pixels 2026-11-06 20:03:43 -06:00
LukeGus cd81203df8 fix: tile lines no longer overlap section lines, tab row button groups fit 2026-11-06 19:17:10 -06:00
LukeGus 191d5b2e57 fix: hide empty tile stack sections so closed editors add no lines 2026-11-06 18:19:00 -06:00
LukeGus 76d41a2cbb chore: tier map for plugins that are not pinned yet 2026-11-06 18:18:51 -06:00
LukeGus 950e5398c9 docs: list default image plugins first, the rest in a dropdown 2026-11-06 18:18:47 -06:00
LukeGus 25fd8d0e58 feat: plugin list rows use host list status colors 2026-11-06 17:18:32 -06:00
LukeGus 33cbd77521 fix: add spacing around trailing buttons in tab strip 2026-11-06 16:39:07 -06:00
LukeGus 020f30541a feat: shared toolbar, footer and host dot pieces for plugin UI 2026-11-06 13:36:36 -06:00
LukeGus 3c6229671c feat: publish Flatpak to our own signed repo on each release 2026-11-06 11:07:17 -06:00
LukeGus 30db4f8024 feat: native drag of files out of the desktop app 2026-11-06 09:30:10 -06:00
LukeGus a055291fa4 chore: update translations and plugin pins 2026-11-06 15:11:47 +00:00
117 changed files with 3947 additions and 230 deletions

No files matched your search

+2 -84
View File
@@ -293,7 +293,7 @@ jobs:
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libfuse2 flatpak flatpak-builder imagemagick
sudo apt-get install -y libfuse2
- name: Install dependencies
run: |
@@ -301,6 +301,7 @@ jobs:
npm install --force @rollup/rollup-linux-x64-gnu
npm install --force @rollup/rollup-linux-arm64-gnu
npm install --force @rollup/rollup-linux-arm-gnueabihf
node scripts/install-linux-sharp.cjs
# Bundled plugins are counted as downloads in the plugin store, so a
# build only fetches them when the pins change.
@@ -426,89 +427,6 @@ jobs:
path: release/termix_linux_armv7l_portable.tar.gz
retention-days: 30
- name: Add Flathub repository
run: |
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
- name: Install Flatpak runtime and SDK
run: |
sudo flatpak install -y flathub org.freedesktop.Platform//24.08
sudo flatpak install -y flathub org.freedesktop.Sdk//24.08
sudo flatpak install -y flathub org.electronjs.Electron2.BaseApp//24.08
- name: Get version for Flatpak
id: flatpak-version
run: |
VERSION="${{ inputs.version_override }}"
if [ -z "$VERSION" ]; then
VERSION=$(node -p "require('./package.json').version")
fi
RELEASE_DATE=$(date +%Y-%m-%d)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT
- name: Prepare Flatpak files
run: |
VERSION="${{ steps.flatpak-version.outputs.version }}"
RELEASE_DATE="${{ steps.flatpak-version.outputs.release_date }}"
CHECKSUM_X64=$(sha256sum "release/termix_linux_x64_appimage.AppImage" | awk '{print $1}')
CHECKSUM_ARM64=$(sha256sum "release/termix_linux_arm64_appimage.AppImage" | awk '{print $1}')
mkdir -p flatpak-build
cp packaging/flatpak/com.karmaa.termix.yml flatpak-build/
cp packaging/flatpak/com.karmaa.termix.desktop flatpak-build/
cp packaging/flatpak/com.karmaa.termix.metainfo.xml flatpak-build/
cp public/icon.svg flatpak-build/com.karmaa.termix.svg
convert public/icon.png -resize 256x256 flatpak-build/icon-256.png
convert public/icon.png -resize 128x128 flatpak-build/icon-128.png
cd flatpak-build
sed -i "s|https://github.com/Termix-SSH/Termix/releases/download/vVERSION_PLACEHOLDER/termix_linux_x64_appimage.AppImage|file://$(realpath ../release/termix_linux_x64_appimage.AppImage)|g" com.karmaa.termix.yml
sed -i "s|https://github.com/Termix-SSH/Termix/releases/download/vVERSION_PLACEHOLDER/termix_linux_arm64_appimage.AppImage|file://$(realpath ../release/termix_linux_arm64_appimage.AppImage)|g" com.karmaa.termix.yml
sed -i "s/CHECKSUM_X64_PLACEHOLDER/$CHECKSUM_X64/g" com.karmaa.termix.yml
sed -i "s/CHECKSUM_ARM64_PLACEHOLDER/$CHECKSUM_ARM64/g" com.karmaa.termix.yml
sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" com.karmaa.termix.metainfo.xml
sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" com.karmaa.termix.metainfo.xml
- name: Build Flatpak bundle
run: |
cd flatpak-build
flatpak-builder --repo=repo --force-clean --disable-rofiles-fuse build-dir com.karmaa.termix.yml
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then
FLATPAK_ARCH="x86_64"
elif [ "$ARCH" = "aarch64" ]; then
FLATPAK_ARCH="aarch64"
else
FLATPAK_ARCH="$ARCH"
fi
flatpak build-bundle repo ../release/termix_linux_flatpak.flatpak com.karmaa.termix --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
- name: Create flatpakref file
run: |
VERSION="${{ steps.flatpak-version.outputs.version }}"
cp packaging/flatpak/com.karmaa.termix.flatpakref release/
sed -i "s|VERSION_PLACEHOLDER|v${VERSION}|g" release/com.karmaa.termix.flatpakref
- name: Upload Flatpak bundle
uses: actions/upload-artifact@v7
if: hashFiles('release/termix_linux_flatpak.flatpak') != '' && inputs.artifact_destination != 'none'
with:
name: termix_linux_flatpak
path: release/termix_linux_flatpak.flatpak
retention-days: 30
- name: Upload Flatpakref
uses: actions/upload-artifact@v7
if: hashFiles('release/com.karmaa.termix.flatpakref') != '' && inputs.artifact_destination != 'none'
with:
name: termix_linux_flatpakref
path: release/com.karmaa.termix.flatpakref
retention-days: 30
# Every official plugin, for installs without internet. No update files.
- name: Build Linux x64 full packages
if: inputs.artifact_destination != 'none'
+112
View File
@@ -0,0 +1,112 @@
name: Publish Flatpak
# Builds the Flatpak from a release's portable tarballs and adds the bundles to
# that release. Stable releases also replace the repo at flatpak.termix.site,
# which is what `flatpak update` reads.
on:
workflow_dispatch:
inputs:
release_tag:
description: "Release tag to package, like v26.11.1"
required: true
type: string
publish:
description: "Replace the live Flatpak repo"
required: true
default: true
type: boolean
workflow_call:
inputs:
release_tag:
description: "Release tag to package"
required: true
type: string
publish:
description: "Replace the live Flatpak repo"
required: true
type: boolean
env:
FLATPAK_REPO_URL: https://flatpak.termix.site
jobs:
build:
runs-on: blacksmith-8vcpu-ubuntu-2404
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
ref: ${{ inputs.release_tag }}
fetch-depth: 1
# A tag from before this workflow has no build script.
- name: Use the newest packaging files
env:
WORKFLOW_SHA: ${{ github.sha }}
run: |
git fetch --depth 1 origin "$WORKFLOW_SHA"
git checkout "$WORKFLOW_SHA" -- packaging/flatpak
- name: Install tools
run: |
sudo add-apt-repository -y ppa:flatpak/stable
sudo apt-get update
sudo apt-get install -y flatpak flatpak-builder imagemagick qemu-user-static
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
- name: Download release tarballs
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.release_tag }}
run: |
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir tarballs \
--pattern termix_linux_x64_portable.tar.gz \
--pattern termix_linux_arm64_portable.tar.gz
- name: Import signing key
env:
FLATPAK_GPG_KEY: ${{ secrets.FLATPAK_GPG_KEY }}
run: |
export GNUPGHOME="$RUNNER_TEMP/gnupg"
mkdir -m 700 "$GNUPGHOME"
printf '%s' "$FLATPAK_GPG_KEY" | gpg --batch --import
echo "FLATPAK_GPG_HOME=$GNUPGHOME" >> "$GITHUB_ENV"
echo "FLATPAK_GPG_KEY_ID=$(gpg --list-keys --with-colons | awk -F: '/^fpr/ { print $10; exit }')" >> "$GITHUB_ENV"
- name: Build
env:
TAG: ${{ inputs.release_tag }}
run: |
bash packaging/flatpak/build-repo.sh "${TAG#v}" tarballs out
chmod -R a+rX out/site
- name: Add bundles to the release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.release_tag }}
run: |
gh release upload "$TAG" out/*.flatpak out/com.karmaa.termix.flatpakref \
--repo "$GITHUB_REPOSITORY" --clobber
- name: Upload repo for Pages
if: inputs.publish
uses: actions/upload-pages-artifact@v5
with:
path: out/site
deploy:
needs: build
if: inputs.publish
runs-on: blacksmith-2vcpu-ubuntu-2404
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ env.FLATPAK_REPO_URL }}
steps:
- name: Deploy
uses: actions/deploy-pages@v5
+13
View File
@@ -543,6 +543,19 @@ jobs:
source_ref: ${{ needs.merge-to-main.outputs.build_ref || needs.translate.outputs.sha || needs.prep.outputs.sha }}
secrets: inherit
flatpak:
needs: [prep, electron-release]
if: ${{ always() && needs.electron-release.result == 'success' && needs.prep.outputs.mode != 'Dry run' }}
permissions:
contents: write
pages: write
id-token: write
uses: ./.github/workflows/flatpak.yml
with:
release_tag: ${{ needs.prep.outputs.release_tag }}
publish: ${{ needs.prep.outputs.channel == 'Stable' }}
secrets: inherit
electron-submit:
needs: [prep, merge-to-main, electron-release]
if: ${{ always() && needs.electron-release.result == 'success' && needs.prep.outputs.channel == 'Stable' && needs.prep.outputs.mode != 'Dry run' && needs.prep.outputs.mode != 'Skip submit' }}
+19
View File
@@ -4,31 +4,40 @@
### Added
- Plugins in the desktop app can drag files out to Explorer or Finder
- Termix actions: plugins can declare what they do once, and it works from the API with permission checks, dry runs, run history and approvals
- API keys can be limited to reading or running actions, when an admin makes them
- A click on a host can open its host page, when a plugin offers one
- Plugins can build pages of tiles you drag and resize, and add many copies of one dashboard card, each with its own settings
- A plugin a newer release ships is downloaded on its own when your image does not have it, and tried again on the next start when offline
- Plugins can decide who may connect to a host, so a host can need approved access before an SSH or remote desktop session opens
- When more than one plugin flags a host, its row shows one status dot with every reason in its tooltip
- Plugins that issue SSH certificates can save them on the SSH key login they were issued for
- Cmd+B shows or hides the sidebar, and can be changed in keybindings
- The macOS menu bar icon can be monochrome (the default), in color or hidden, from the Termix menu
### Changed
- Plugin cards and pages use the dashboard look everywhere, with shared lines between sections instead of separate boxes with gaps
- The Plugins tab and plugin pages use the same look
- List rows in plugins follow the host list status colors: accent for good, dark for offline or bad
- Line charts that cover a few minutes show seconds in their times
- Plugins a new release adds to the image start on only when they are recommended. The rest are installed and left off for an admin to turn on
- Host status, import and export, and credential tools now start on for new installs
- Sync now starts on in a new desktop app, so it can link to a server
- The first run plugin picker leaves plugins you did not choose off instead of removing them
- The Linux Flatpak now installs from flatpak.termix.site on x86_64 and arm64, and `flatpak update` keeps it current
- Dropdowns and text areas are the same size as the text fields next to them
- A page's own tabs sit above its search bar
- A plugin view that is still starting shows the same loading spinner as the rest of Termix
### Fixed
- The first run plugin picker no longer sets the default layout and themes to Remove. They always stay on
- Alerts with no icon no longer squeeze their text to one word per line
- Password fields with the show button are the same height as the other fields next to them
- Button groups next to tabs no longer touch the lines of the tab row
- The Secure your account setup step no longer shows up empty when no passkey or authenticator plugin is on
- An installed plugin update older than the copy in a newer image is removed at startup instead of logging an error every boot
- Canceling a download to the desktop app right as it starts no longer leaves a partial file behind
@@ -41,6 +50,16 @@
- The hint at the bottom of Browse and the stored data footer on plugin pages have the same padding as the rest of the page
- Sign in methods on the Security page have a line between them
- Chart axis labels with long units, like KiB/s, are no longer cut off
- Filters, view toggles and small buttons in a toolbar are the same height as the search box next to them
- Save and Cancel at the bottom of a form are the same height as other buttons
- State badges in tables no longer stretch across the whole column
- Text boxes, dropdowns, checkboxes and button groups share one border color
- Small icons in buttons sit centered instead of half a pixel off
- Installing Termix as an app works behind a proxy that needs a login cookie
- The hosts Import / Export button and menu entry show their name again instead of a text key
- SSH logins to servers that only offer group exchange no longer stall for up to a minute while checking a large key exchange group
- Plugin views can open the host editor again, so the terminal's Edit host button works after a failed connection
- The Linux arm64 and armv7l desktop builds start again instead of failing to load the image library
## 26.11.0
+37 -25
View File
@@ -55,35 +55,55 @@ What every install has, with or without plugins:
## Plugins
Plugins add everything else. The official ones ship with Termix, and admins can install, update or remove plugins from the Plugins tab. Each plugin has its own repo.
Plugins add everything else. Admins can install, update or remove plugins from the Plugins tab, and each plugin has its own repo. These ship in the default image:
| Plugin | What it does |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| [Alerts](https://github.com/Termix-SSH/Plugin-Alerts) | One inbox for every alert in Termix, with delivery to webhooks, ntfy, Discord and email. |
| [Classic Layout](https://github.com/Termix-SSH/Plugin-Classic-Layout) | The classic Termix layout with an icon rail, sidebar, tab bar and dock. |
| [Classic Themes](https://github.com/Termix-SSH/Plugin-Classic-Themes) | Light, dark and seven more color themes for Termix. |
| [Credential Tools](https://github.com/Termix-SSH/Plugin-Credential-Tools) | Generate SSH keys, check them and copy a public key to a host. |
| [Docker](https://github.com/Termix-SSH/Plugin-Docker) | Manage Docker and Podman containers on your hosts over SSH, with logs, stats and a console. |
| [File Manager](https://github.com/Termix-SSH/Plugin-File-Manager) | Browse, edit and move files over SFTP, including straight from one server to another. |
| [Host Metrics](https://github.com/Termix-SSH/Plugin-Host-Metrics) | Live CPU, memory, disk, network and GPU stats, plus tools to manage your hosts. |
| [Host Status](https://github.com/Termix-SSH/Plugin-Host-Status) | Shows if each host is online with a dot in the host list, checked by connecting to its port. |
| [Import and Export](https://github.com/Termix-SSH/Plugin-Import-Export) | Import hosts from a JSON file or an SSH config file and export them to share or back up. |
| [Operations Center](https://github.com/Termix-SSH/Plugin-Operations-Center) | Run, roll out, approve and track every plugin action across your hosts, with a page per host and tables across all of them. |
| [Passkeys](https://github.com/Termix-SSH/Plugin-WebAuthn) | Sign in with a passkey or security key instead of a password. |
| [Remote Desktop](https://github.com/Termix-SSH/Plugin-Remote-Desktop) | RDP, VNC, Apple Remote Desktop, RustDesk and Telnet sessions in your browser, with clipboard, file transfer, sharing and recording. |
| [Serial](https://github.com/Termix-SSH/Plugin-Serial) | Open a serial console to a router, switch or other device plugged into your computer. |
| [Single sign-on](https://github.com/Termix-SSH/Plugin-SSO) | Sign in with any OpenID Connect provider, GitHub or Google. |
| [Snippets](https://github.com/Termix-SSH/Plugin-Snippets) | Save the commands you run often and run them in a terminal or on many hosts in one click. |
| [SSH Terminal](https://github.com/Termix-SSH/Plugin-SSH-Terminal) | SSH terminal sessions with tabs, split screen, history, macros and a local terminal in the desktop app. |
| [Sync](https://github.com/Termix-SSH/Plugin-Sync) | Sync the desktop app with one or more servers, and manage other Termix servers as remote sites from a master. |
| [TOTP](https://github.com/Termix-SSH/Plugin-TOTP) | Two-factor sign in with a code from an authenticator app, with backup codes. |
| [Tunnels](https://github.com/Termix-SSH/Plugin-Tunnels) | SSH port forwarding that reconnects on its own, WireGuard gateways that publish private services, and client tunnels from the desktop app. |
| [Usage Statistics](https://github.com/Termix-SSH/Plugin-Telemetry) | Sends a small anonymous daily report so we can see how Termix is used. |
| [Workspaces](https://github.com/Termix-SSH/Plugin-Workspaces) | Save your open tabs and split layouts and reopen them in one click. |
<details>
<summary>All other official plugins (54)</summary>
These ship in the `-full` image tags (like `latest-full`) and the full desktop builds. On any other install, add them from the Plugins tab.
| Plugin | What it does |
| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| [ACME Certificates](https://github.com/Termix-SSH/Plugin-ACME-SSL) | Gets the Termix HTTPS certificate from Let's Encrypt or any ACME provider and renews it for you. |
| [AI Assistant](https://github.com/Termix-SSH/Plugin-AI) | An AI assistant that reads your setup and proposes changes for you to approve. |
| [Alerts](https://github.com/Termix-SSH/Plugin-Alerts) | One inbox for every alert in Termix, with delivery to webhooks, ntfy, Discord and email. |
| [Ansible](https://github.com/Termix-SSH/Plugin-Ansible) | Run Ansible playbooks and ad-hoc commands from a control host, with an inventory made from your Termix hosts. |
| [Audit Tools](https://github.com/Termix-SSH/Plugin-Audit-Tools) | Send the audit log to your SIEM and export it as CSV or NDJSON. |
| [Automations](https://github.com/Termix-SSH/Plugin-Automations) | Run steps on your hosts on a schedule or when something happens. |
| [Backups](https://github.com/Termix-SSH/Plugin-Backups) | Back up folders, databases and Docker data on your hosts with restic, then browse, download and restore. |
| [Certificates](https://github.com/Termix-SSH/Plugin-Certificates) | See, issue and renew TLS certificates on your hosts with certbot and acme.sh, and get an alert before they expire. |
| [Classic Layout](https://github.com/Termix-SSH/Plugin-Classic-Layout) | The classic Termix layout with an icon rail, sidebar, tab bar and dock. |
| [Classic Themes](https://github.com/Termix-SSH/Plugin-Classic-Themes) | Light, dark and seven more color themes for Termix. |
| [Cloud Providers](https://github.com/Termix-SSH/Plugin-Cloud-Providers) | Add hosts from AWS, Azure, Google Cloud, DigitalOcean, Hetzner and other clouds, and keep them in sync. |
| [Credential Tools](https://github.com/Termix-SSH/Plugin-Credential-Tools) | Generate SSH keys, check them and copy a public key to a host. |
| [Cron](https://github.com/Termix-SSH/Plugin-Cron) | See, add and run cron jobs on your hosts, with a schedule preview and the system cron files. |
| [Databases](https://github.com/Termix-SSH/Plugin-Databases) | Browse and edit MySQL, MariaDB, PostgreSQL and SQLite databases on your hosts from one tab. |
| [DNS](https://github.com/Termix-SSH/Plugin-DNS) | Manage DNS records and domains across Cloudflare, Route 53, Namecheap and other providers. |
| [Docker](https://github.com/Termix-SSH/Plugin-Docker) | Manage Docker and Podman containers on your hosts over SSH, with logs, stats and a console. |
| [File Manager](https://github.com/Termix-SSH/Plugin-File-Manager) | Browse, edit and move files over SFTP, including straight from one server to another. |
| [Firewall](https://github.com/Termix-SSH/Plugin-Firewall) | Manage UFW, firewalld, nftables and iptables rules on your hosts over SSH, with lockout protection. |
| [Fleets](https://github.com/Termix-SSH/Plugin-Fleets) | Group hosts into fleets and run commands, package actions and file transfers on all of them at once. |
| [Git](https://github.com/Termix-SSH/Plugin-Git) | See, pull, commit and push git repos on your hosts, and keep them up to date with auto pull. |
| [Homepage](https://github.com/Termix-SSH/Plugin-Homepage) | A drag and drop widget page for your servers and services, plus service links for the dashboard. |
| [Host Metrics](https://github.com/Termix-SSH/Plugin-Host-Metrics) | Live CPU, memory, disk, network and GPU stats, plus tools to manage your hosts. |
| [Host Status](https://github.com/Termix-SSH/Plugin-Host-Status) | Shows if each host is online with a dot in the host list, checked by connecting to its port. |
| [Hosts Config](https://github.com/Termix-SSH/Plugin-Hosts-Config) | Keep hosts, folders and sharing in a YAML or JSON file. Termix loads them as read only hosts and reloads when the file changes. |
| [Import and Export](https://github.com/Termix-SSH/Plugin-Import-Export) | Import hosts from a JSON file or an SSH config file and export them to share or back up. |
| [IP-KVM](https://github.com/Termix-SSH/Plugin-IP-KVM) | See and control machines through PiKVM, NanoKVM and JetKVM devices. |
| [Kubernetes](https://github.com/Termix-SSH/Plugin-Kubernetes) | Manage Kubernetes clusters from your hosts over SSH, with logs, metrics, YAML and a shell. |
| [LDAP](https://github.com/Termix-SSH/Plugin-LDAP) | Sign in with your LDAP or Active Directory username and password. |
@@ -93,39 +113,28 @@ Plugins add everything else. The official ones ship with Termix, and admins can
| [Network Topology](https://github.com/Termix-SSH/Plugin-Network-Topology) | Draws your hosts and the links between them as a live, interactive graph. |
| [Notes](https://github.com/Termix-SSH/Plugin-Notes) | Write markdown notes for each host, folder or yourself, with commands you can copy or send to the terminal. |
| [Object Storage](https://github.com/Termix-SSH/Plugin-Object-Storage) | Browse and manage S3-compatible object storage buckets and files. |
| [Operations Center](https://github.com/Termix-SSH/Plugin-Operations-Center) | Run, roll out, approve and track every plugin action across your hosts, with a page per host and tables across all of them. |
| [OPKSSH](https://github.com/Termix-SSH/Plugin-OPKSSH) | Connect to hosts with short-lived OpenPubkey SSH certificates after signing in with your identity provider. |
| [Out-of-band](https://github.com/Termix-SSH/Plugin-Out-Of-Band) | Power, sensors, event logs and consoles for server BMCs over Redfish, IPMI and Intel AMT. |
| [Packages](https://github.com/Termix-SSH/Plugin-Package-Manager) | See and install updates on your hosts, search, install and remove packages, and read the package history. |
| [Passkeys](https://github.com/Termix-SSH/Plugin-WebAuthn) | Sign in with a passkey or security key instead of a password. |
| [Privileged Access](https://github.com/Termix-SSH/Plugin-Privileged-Access) | Ask for approval before anyone connects to a privileged host, with access that ends on its own and a full audit trail. |
| [Process Manager](https://github.com/Termix-SSH/Plugin-Process-Manager) | See and control running processes, pm2 and supervisor apps on your hosts, read their logs and get alerts when they fail. |
| [Proxmox](https://github.com/Termix-SSH/Plugin-Proxmox) | Import Proxmox VE guests as hosts, keep them in sync and watch node stats. |
| [Remote Desktop](https://github.com/Termix-SSH/Plugin-Remote-Desktop) | RDP, VNC and Telnet sessions in your browser, with clipboard, file transfer, sharing and recording. |
| [Reverse Proxy](https://github.com/Termix-SSH/Plugin-Reverse-Proxy) | Reverse proxy for TCP, UDP, HTTP and HTTPS with free certificates and Termix login, managed per host. |
| [Samba Shares](https://github.com/Termix-SSH/Plugin-Samba) | Create and manage Samba (SMB) file shares, users and permissions on your hosts from one tab. |
| [Secret Sources](https://github.com/Termix-SSH/Plugin-Secret-Sources) | Pull host passwords and keys from 1Password when you connect instead of storing them in Termix. |
| [Secret Sources](https://github.com/Termix-SSH/Plugin-Secret-Sources) | Pull host passwords and keys from 1Password, Bitwarden, Infisical or HashiCorp Vault when you connect instead of storing them in Termix. |
| [Security Center](https://github.com/Termix-SSH/Plugin-Security-Center) | Audit SSH keys, rotate them and scan hosts for known vulnerabilities over SSH. |
| [Serial](https://github.com/Termix-SSH/Plugin-Serial) | Open a serial console to a router, switch or other device plugged into your computer. |
| [Services](https://github.com/Termix-SSH/Plugin-Service-Manager) | Manage services, timers and logs on your hosts over SSH. |
| [Session Recording](https://github.com/Termix-SSH/Plugin-Session-Recording) | Record terminal and remote desktop sessions and play them back or download them later. |
| [Session Recording](https://github.com/Termix-SSH/Plugin-Session-Recording) | Record terminal and remote desktop sessions, watch them live, and keep an audited, encrypted record of who did what. |
| [Session Sharing](https://github.com/Termix-SSH/Plugin-Session-Sharing) | Share live sessions by link or with another user, and present sessions to a group in meeting rooms. |
| [Simple Layout](https://github.com/Termix-SSH/Plugin-Simple-Layout) | A simple Termix layout for small setups with a top bar, a Home screen and only the basics. |
| [Single sign-on](https://github.com/Termix-SSH/Plugin-SSO) | Sign in with any OpenID Connect provider, GitHub or Google. |
| [Snippets](https://github.com/Termix-SSH/Plugin-Snippets) | Save the commands you run often and run them in a terminal or on many hosts in one click. |
| [SSH Certificate Signer](https://github.com/Termix-SSH/Plugin-Vault) | Connect to hosts with short-lived SSH certificates signed by HashiCorp Vault or OpenBao. |
| [SSH Terminal](https://github.com/Termix-SSH/Plugin-SSH-Terminal) | SSH terminal sessions with tabs, split screen, history, macros and a local terminal in the desktop app. |
| [Step CA](https://github.com/Termix-SSH/Plugin-Step-CA) | Connect to hosts with short-lived SSH certificates from a smallstep step-ca server. |
| [Storage Health](https://github.com/Termix-SSH/Plugin-Storage-Health) | Watch drive and array health over SSH: SMART, NVMe, ZFS, mdadm, btrfs and LVM, with history, alerts and self tests. |
| [Sync](https://github.com/Termix-SSH/Plugin-Sync) | Sync the desktop app with one or more servers, and manage other Termix servers as remote sites from a master. |
| [Tailscale](https://github.com/Termix-SSH/Plugin-Tailscale) | Add tailnet devices as hosts and connect with Tailscale SSH. |
| [Termix Identity](https://github.com/Termix-SSH/Plugin-Termix-Identity) | Publish your SSH public keys under a public handle and run your own SSH certificate authority. |
| [Tmux Monitor](https://github.com/Termix-SSH/Plugin-Tmux-Monitor) | Browse and control tmux sessions, windows and panes across your hosts. |
| [TOTP](https://github.com/Termix-SSH/Plugin-TOTP) | Two-factor sign in with a code from an authenticator app, with backup codes. |
| [Tunnels](https://github.com/Termix-SSH/Plugin-Tunnels) | SSH port forwarding that reconnects on its own, plus client tunnels from the desktop app. |
| [UPS Manager](https://github.com/Termix-SSH/Plugin-UPS-Manager) | Watch your UPS devices over NUT, apcupsd or SNMP, with history, alerts and self tests. |
| [Uptime](https://github.com/Termix-SSH/Plugin-Uptime) | Watch websites, ports, hosts and services, get alerts when they go down, and publish status pages. |
| [Usage Statistics](https://github.com/Termix-SSH/Plugin-Telemetry) | Sends a small anonymous daily report so we can see how Termix is used. |
| [Users](https://github.com/Termix-SSH/Plugin-User-Manager) | Manage the user accounts, groups, passwords and SSH keys on your hosts. |
| [Virtual Machines](https://github.com/Termix-SSH/Plugin-Virtual-Machines) | Create and manage KVM virtual machines on your Linux hosts over SSH with libvirt. |
| [Wake-on-LAN](https://github.com/Termix-SSH/Plugin-Wake-On-LAN) | Wake, shut down and track your machines from one power dashboard. |
@@ -133,7 +142,8 @@ Plugins add everything else. The official ones ship with Termix, and admins can
| [Web Endpoint](https://github.com/Termix-SSH/Plugin-Web-Endpoint) | Open a host's web UI inside Termix, directly or over an SSH tunnel. |
| [Web Servers](https://github.com/Termix-SSH/Plugin-Web-Servers) | Manage nginx, Apache and Caddy on your hosts: sites, config, logs and HTTPS, with every change tested first. |
| [WireGuard](https://github.com/Termix-SSH/Plugin-WireGuard) | Install WireGuard on your hosts and manage interfaces, clients and configs from one tab. |
| [Workspaces](https://github.com/Termix-SSH/Plugin-Workspaces) | Save your open tabs and split layouts and reopen them in one click. |
</details>
Want to build your own? Start from the [plugin template](https://github.com/Termix-SSH/Termix-Plugin-Template). Plugins you can install are listed in the [registry](https://github.com/Termix-SSH/Termix-Registry). Turn on the community registry in Plugins, Registries to install plugins made by other people, or add your own registry. See [submitting a plugin](https://docs.termix.site/develop/community-registry) and [hosting your own registry](https://docs.termix.site/develop/registries#host-your-own-registry).
@@ -178,6 +188,8 @@ Want to build your own? Start from the [plugin template](https://github.com/Term
See the [docs](https://docs.termix.site/install) for install steps on every platform.
On macOS, Termix, Menu Bar Icon picks a monochrome icon (the default), a color icon or no icon. The choice is saved on this device. With the icon hidden, open Termix from the Dock to change it back, and closing the last window quits the app.
Sample Docker Compose file. Remote desktop (RDP, VNC and Telnet) is built in, so nothing else needs to run next to it:
```yaml
+32
View File
@@ -0,0 +1,32 @@
{
"plugins": {
"ansible": { "tier": "full" },
"backups": { "tier": "full" },
"certificates": { "tier": "full" },
"cron": { "tier": "full" },
"dns": { "tier": "full" },
"firewall": { "tier": "full" },
"git": { "tier": "full" },
"ip-kvm": { "tier": "full" },
"log-search": { "tier": "full" },
"network-devices": { "tier": "full" },
"notes": { "tier": "full" },
"object-storage": { "tier": "full" },
"operations-center": {
"tier": "standard",
"onboarding": { "recommended": true, "consent": false }
},
"out-of-band": { "tier": "full" },
"package-manager": { "tier": "full" },
"privileged-access": { "tier": "full" },
"process-manager": { "tier": "full" },
"security-center": { "tier": "full" },
"storage-health": { "tier": "full" },
"ups-manager": { "tier": "full" },
"uptime": { "tier": "full" },
"user-manager": { "tier": "full" },
"virtual-machines": { "tier": "full" },
"web-servers": { "tier": "full" },
"wireguard": { "tier": "full" }
}
}
+5 -1
View File
@@ -32,7 +32,11 @@ function writeJson(filePath, value) {
}
}
const DEFAULTS = { defaultConnectionOrigin: "local", updateMode: "prompt" };
const DEFAULTS = {
defaultConnectionOrigin: "local",
updateMode: "prompt",
macTrayIcon: "monochrome",
};
function getDesktopSettings() {
return { ...DEFAULTS, ...readJson(dataPath("desktop-settings.json"), {}) };
+58 -3
View File
@@ -657,6 +657,12 @@ function createManagedTempFile(fileName, content, encoding = "utf8") {
return { tempId, path: filePath };
}
function dragIcon() {
// Windows cancels a drag with an empty icon
const buf = Buffer.alloc(16 * 16 * 4, 0x99);
return nativeImage.createFromBitmap(buf, { width: 16, height: 16 });
}
function cleanupManagedTempFile(tempId) {
const temp = tempFiles.get(tempId);
if (!temp) return;
@@ -1227,7 +1233,21 @@ if (!gotTheLock) {
});
}
function setMacTrayIcon(style) {
desktopSettings.saveDesktopSettings({ macTrayIcon: style });
if (tray && !tray.isDestroyed()) tray.destroy();
tray = null;
createTray();
createMenu();
if (style === "hidden" && mainWindow && !mainWindow.isDestroyed()) {
mainWindow.show();
mainWindow.focus();
}
}
function createTray() {
const macStyle = desktopSettings.getDesktopSettings().macTrayIcon;
if (process.platform === "darwin" && macStyle === "hidden") return;
try {
const { nativeImage } = require("electron");
@@ -1239,10 +1259,14 @@ function createTray() {
let trayIcon;
if (process.platform === "darwin") {
const iconPath = path.join(publicRoot, "icons", "16x16.png");
// The logo is full color on a solid tile. As a template image macOS
// keeps only its outline, which showed as an empty square.
const monochrome = macStyle !== "color";
const iconPath = path.join(
publicRoot,
"icons",
monochrome ? "trayTemplate.png" : "16x16.png",
);
trayIcon = nativeImage.createFromPath(iconPath);
trayIcon.setTemplateImage(monochrome);
} else if (process.platform === "win32") {
trayIcon = path.join(publicRoot, "icon.ico");
} else {
@@ -1961,6 +1985,23 @@ ipcMain.handle("start-drag-to-desktop", (event, dragData) => {
}
});
// Must run synchronously inside the renderer's dragstart, files must exist.
ipcMain.on("start-drag-files", (event, tempIds) => {
try {
const files = (Array.isArray(tempIds) ? tempIds : [])
.map((id) => tempFiles.get(id)?.path)
.filter((p) => p && fs.existsSync(p));
if (files.length === 0) return;
event.sender.startDrag({
file: files[0],
files,
icon: dragIcon(),
});
} catch (error) {
logToFile("Failed to start drag:", error.message);
}
});
ipcMain.handle("cleanup-temp-file", (_event, tempId) => {
try {
cleanupManagedTempFile(tempId);
@@ -1977,6 +2018,20 @@ function createMenu() {
label: app.name,
submenu: [
{ role: "about" },
{
label: "Menu Bar Icon",
submenu: [
["monochrome", "Monochrome"],
["color", "Color"],
["hidden", "Hidden"],
].map(([style, label]) => ({
label,
type: "radio",
checked:
desktopSettings.getDesktopSettings().macTrayIcon === style,
click: () => setMacTrayIcon(style),
})),
},
{ type: "separator" },
{ role: "services" },
{ type: "separator" },
+1
View File
@@ -91,6 +91,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
ipcRenderer.invoke("create-temp-folder", folderData),
startDragToDesktop: (dragData) =>
ipcRenderer.invoke("start-drag-to-desktop", dragData),
startDragFiles: (tempIds) => ipcRenderer.send("start-drag-files", tempIds),
cleanupTempFile: (tempId) => ipcRenderer.invoke("cleanup-temp-file", tempId),
// Native web views placed over an element of the page (WebContentsView).
+1 -1
View File
@@ -17,7 +17,7 @@
/>
<meta name="apple-mobile-web-app-title" content="Termix" />
<link rel="apple-touch-icon" href="icons/512x512.png" />
<link rel="manifest" href="manifest.json" />
<link rel="manifest" href="manifest.json" crossorigin="use-credentials" />
<title>Termix</title>
<style>
.toolbar-scrollbar {
+24
View File
@@ -1,5 +1,29 @@
# Changelog
## 1.1.2
Plugin API 1.15.0.
### Added
- `ctx.credentials.setSshKeyCertificate` stores an OpenSSH certificate on one of the acting user's SSH key credentials, or clears it with `null`
- `/ui` exports `ToolbarBand` (a toolbar row outside the PanelShell toolbar), `ToolbarSelect` (a small select for those rows) and `TOOLBAR_FIT` (the classes that make every control in a row 32px)
- `PANEL.section`, the one inner layout for a SectionCard body
- `TextAreaField` takes `fill`, so a config or log editor uses the rest of the height
- `registerHostBadge` takes `useDot`. Every plugin's dots merge into one dot on the host row, with the worst tone and each reason in its tooltip. `/ui` exports `HostDotGroup` for layouts that draw host rows
### Changed
- Controls in the PanelShell toolbar, tab band and footer are all 32px, so small buttons line up with search boxes and selects
- `Segmented` and `ViewToggle` match the height of inputs and buttons, and `Segmented` keeps its natural width
- `FormFooter` buttons are full height
- `PANEL.body` is `p-3`, the same padding `InlineView` gives a form
- A body with a pinned footer no longer draws a closing line just above the footer's own line
- The placeholder for a plugin view that is still starting uses `LoadingState`
- `termix-plugin validate` warns about UI drift: raw loading spinners, small back buttons, native selects, stretched filters, filled headers, capped log views and footers that scroll away
- A `ToolbarBand` right inside a `TileStack` leaves the line under it to the stack
- `ListBadge` and `StatusBadge` keep their own width in a table cell instead of filling it
## 1.1.1
Plugin API 1.15.0.
+26 -4
View File
@@ -1,6 +1,6 @@
# Termix plugin guide
SDK 1.1.1 | Plugin API 1.15.0
SDK 1.1.2 | Plugin API 1.15.0
This guide is for anyone, person or AI coding tool, building a Termix plugin. It ships inside
`@termix-ssh/plugin-sdk`, so the copy in your `node_modules` always matches the SDK you have installed.
@@ -354,6 +354,10 @@ router.post("/notes", async (req, res) => {
`allowManualCreate: () => false` blocks adding hosts by hand. Your own `ctx.hosts` writes still go
through. `ctx.hosts.setShares(hostId, grants)` replaces a host's whole grant list, and
`ctx.credentials.list()` finds the acting user's credentials by name.
- `ctx.credentials.setSshKeyCertificate(credentialId, certificate)` stores a CA-signed OpenSSH
certificate (a `-cert.pub` line, or `null` to clear it) on one of the acting user's SSH key
credentials. Core then offers key plus certificate on every connection through that credential, so a
plugin that issues certificates can keep the credential current instead of asking the user to paste it.
- Connect policy: with `hosts:connect-policy`, `ctx.hosts.connectPolicy.provide({ check(input) })` is asked
before every SSH config is built and every remote desktop login is handed out (`input` has `userId`,
`hostId`, `protocol`, `purpose`, `profile`, `interactive`, `origin`). Return `{ decision: "allow" }`,
@@ -782,9 +786,27 @@ exports with props is `ui.api.txt` in the Termix repo
`StatTile` or `MetricCard`, and use `TileFrame` for a titled section. Cards drop their own box inside
these. Forms keep `PANEL.body` padding inside their section. An `InlineView` form made of `SectionCard`s
takes `stacked`.
- Page views go in `PanelShell`'s `tabs` band as a `TabStrip`. `Segmented` is for filters and view modes.
A page with many sections uses `SideNav` on the left. Put the refresh button in the header with
`RefreshButton`, and the docs link with the `docs` prop.
- Page views go in `PanelShell`'s `tabs` band as a `TabStrip`. `Segmented` is for filters and view modes and
keeps its natural width, never stretched across the pane. A page with many sections uses `SideNav` on the
left. Put the refresh button in the header with `RefreshButton`, and the docs link with the `docs` prop.
- Toolbars: search, filters and buttons go in `PanelShell`'s `toolbar` slot, or in a `ToolbarBand` for a second
row. Both make every control in the row 32px, so a `size="sm"` button lines up with a search box. Use
`ToolbarSelect` for a small select there, never a native `<select>`. Don't build your own row with its own
padding.
- Padding: loose content in a page body (a hint, a single field) takes `PANEL.body`. A `SectionCard` body uses
`PANEL.section` for its fields.
- Headers have no background. Label a group with `GroupHeading`, never a row with `bg-muted`.
- Sub views: a detail or edit view opens as an `InlineView`, which has the one back button. Anywhere else use
`BackButton` in `PanelShell`'s `leading` slot. Never make a smaller back button.
- Save and Add always sit pinned at the bottom: pass `FormFooter` to `InlineView` or `PanelShell` as `footer`.
Inside a tab body, wrap the form in `<PanelShell chrome={false} footer={...}>`.
- Loading: `LoadingState` for anything still loading (`compact` in cards and rows). A spinner on its own is
only for a button or an activity like "working".
- Config and log editors fill the rest of the view: `TextAreaField fill`, or `min-h-0 flex-1` on a log, never a
fixed number of rows or a `max-h-[60vh]`.
- Host row dots: report a problem with `registerHostBadge({ useDot })` instead of drawing a dot. The layout
merges every plugin's dots into one.
- `termix-plugin validate` lists drift from these rules under `ui`. They are warnings, so fix what applies.
- States and data: `StatusBadge` and `StatusDot` (pass a state like "running" and the color follows),
`InfoGrid` for label and value pairs, `DataTable` for a plain table, `Pager` and `usePaged` under long lists.
- Tile pages use `TileGrid` with `TileFrame` around each tile and `TilePicker` beside it while editing. Slots are
@@ -51,6 +51,9 @@ export async function validate({ cwd }) {
for (const warning of docsWarnings(cwd, raw)) {
console.warn(` warn ${warning}`);
}
for (const warning of uiWarnings(cwd)) {
console.warn(` ui ${warning}`);
}
if (problems.length > 0) {
for (const problem of problems) console.error(` ${problem}`);
@@ -152,6 +155,92 @@ function walkSources(dir, out = []) {
return out;
}
/** Whether line i sits inside a still open Button within the lines before it. */
function insideButton(lines, i, reach = 12) {
const before = lines.slice(Math.max(0, i - reach), i + 1).join("\n");
const open = before.lastIndexOf("<Button");
const openLower = before.lastIndexOf("<button");
const start = Math.max(open, openLower);
if (start < 0) return false;
return !/<\/(Button|button)>/.test(before.slice(start));
}
const UI_RULES = [
{
test: (line) => /animate-spin|<Loader2\b/.test(line),
// Spinners inside a button, and activity spinners like "working", are fine.
skip: (lines, i) =>
insideButton(lines, i) ||
!/loading|Loading/.test(lines.slice(Math.max(0, i - 4), i + 4).join(" ")),
message: "raw spinner while loading, use LoadingState",
},
{
test: (line) => /<(ArrowLeft|ChevronLeft)\b/.test(line),
// Only real back buttons, not previous page or month arrows.
skip: (lines, i) =>
!insideButton(lines, i, 6) ||
!/back|onBack|close\b/i.test(
lines.slice(Math.max(0, i - 6), i + 4).join(" "),
),
message:
"hand made back button, use BackButton in PanelShell leading or InlineView",
},
{
test: (line) => /<select\b/.test(line),
message: "native select, use Select or SelectField",
},
{
test: (line) => /\[&>button\]:flex-1/.test(line),
message: "stretched Segmented, keep filters at their natural width",
},
{
test: (line) =>
/(^|[\s"'`])bg-(muted|card|accent)(\/\d+)?(?=[\s"'`]|$)/.test(line) &&
/\bborder-b\b/.test(line) &&
/(uppercase|GroupHeading|py-1\.5)/.test(line),
message: "filled section header, headers have no background",
},
{
test: (line) => /max-h-\[\d+vh\]/.test(line),
message: "viewer capped by screen height, let it fill the view",
},
{
test: (line) => /<FormFooter\b/.test(line),
skip: (lines, i) =>
/footer\s*=/i.test(lines.slice(Math.max(0, i - 4), i + 1).join(" ")),
message:
"FormFooter in the body, pass it to an InlineView or PanelShell footer (PanelShell chrome={false} inside a tab) so it stays pinned",
},
{
test: (line) =>
/\{t\(["'][\w.]*[Ll]oading["']\)\}/.test(line) && !/label=/.test(line),
message: "plain loading text, use LoadingState",
},
];
/**
* Drift from the shared UI kit in src/frontend: raw spinners, hand made back
* buttons, native selects, stretched filters, filled headers and footers that
* scroll away. Warnings only, since some cases are on purpose.
*/
export function uiWarnings(cwd) {
const warnings = [];
for (const file of walkSources(path.join(cwd, "src", "frontend"))) {
if (!file.endsWith(".tsx")) continue;
const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
const rel = path.relative(cwd, file).replace(/\\/g, "/");
lines.forEach((line, i) => {
if (/^\s*(\/\/|\*|import\b)/.test(line)) return;
for (const rule of UI_RULES) {
if (rule.test(line) && !(rule.skip && rule.skip(lines, i))) {
warnings.push(`${rel}:${i + 1} ${rule.message}`);
}
}
});
}
return warnings;
}
/**
* Docs gaps that do not block a release: no docs/index.md, or a process.env
* read the manifest does not list in env.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@termix-ssh/plugin-sdk",
"version": "1.1.1",
"version": "1.1.2",
"description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
"license": "Apache-2.0",
"repository": {
+12
View File
@@ -2789,6 +2789,18 @@ export interface PluginCredentials {
key: string,
passphrase?: string | null,
) => Promise<{ key: string; passphrase?: string }>;
/**
* Stores a CA-signed OpenSSH certificate next to an existing SSH key
* credential, so core offers key plus certificate when it connects through
* that credential. Only key credentials can carry one; core refuses the
* other auth types instead of dropping the certificate silently. Pass null
* to clear a stored one. Needs credentials:write and the core
* credentials.update permission. Audited.
*/
setSshKeyCertificate: (
credentialId: number,
certificate: string | null,
) => Promise<{ id: number; changed: boolean }>;
}
/**
+3
View File
@@ -112,6 +112,9 @@ export interface ElectronAPI {
error?: string;
}>;
/** Start a native drag of temp files. Call it synchronously in dragstart. */
startDragFiles?: (tempIds: string[]) => void;
cleanupTempFile: (tempId: string) => Promise<{
success: boolean;
error?: string;
+24 -3
View File
@@ -656,7 +656,7 @@ export interface HostProtocolContribution {
quickConnect?: { showDomain?: boolean };
}
export interface HostBadgeContribution {
export interface HostRowContribution {
id: string;
when: (host: PluginHostRecord) => boolean;
component: ComponentType<{ host: PluginHostRecord }>;
@@ -664,11 +664,32 @@ export interface HostBadgeContribution {
order?: number;
}
/** A problem shown as the host row's status dot. */
export interface HostDot {
tone: "bad" | "warn";
label: string;
}
export interface HostBadgeContribution {
id: string;
when: (host: PluginHostRecord) => boolean;
/** Drawn as is on the row. Leave out for a badge that is only a dot. */
component?: ComponentType<{ host: PluginHostRecord }>;
/**
* A hook that returns a problem or null. Every plugin's dots merge into one
* dot on the row, with the worst tone and each label in its tooltip, so two
* plugins reporting the same host never draw two dots. Core 26.11.1+.
*/
useDot?: (host: PluginHostRecord) => HostDot | null;
/** Lower comes first. Ties sort by id. */
order?: number;
}
/** The dot or stripe that shows a host's status on its row. */
export type HostStatusIndicatorContribution = HostBadgeContribution;
export type HostStatusIndicatorContribution = HostRowContribution;
/** A control in the host editor's toolbar. */
export type HostEditorToolbarItemContribution = HostBadgeContribution;
export type HostEditorToolbarItemContribution = HostRowContribution;
export interface HostSectionProps {
host: PluginHostRecord;
+1
View File
@@ -631,6 +631,7 @@ export const CORE_KEYBINDING_ACTIONS: readonly string[] = [
"previousTab",
"openCommandPalette",
"reconnectSession",
"toggleSidebar",
];
/**
+15
View File
@@ -355,6 +355,11 @@ export interface FakePluginContext {
credentialReads: Array<{ hostId: number; protocol: string }>;
/** Every ctx.credentials.createSshKey call, with the id it answered. */
createdSshKeys: Array<PluginSshKeyCredentialInput & { id: number }>;
/** Every ctx.credentials.setSshKeyCertificate call, in order. */
certificateWrites: Array<{
credentialId: number;
certificate: string | null;
}>;
/** Every ctx.notify.send call, with the actor it ran as. */
notifications: Array<{
actor: string | undefined;
@@ -576,6 +581,7 @@ export function createFakeContext(
[];
const credentialReads: FakePluginContext["credentialReads"] = [];
const createdSshKeys: FakePluginContext["createdSshKeys"] = [];
const certificateWrites: FakePluginContext["certificateWrites"] = [];
const notifications: FakePluginContext["notifications"] = [];
const notifyHub: FakePluginContext["notifyHub"] = { current: null };
const operationHandlers: FakePluginContext["operationHandlers"] = new Map();
@@ -1402,6 +1408,10 @@ export function createFakeContext(
publicKey: async (credentialId) =>
(options.sshKeyCredentials ?? []).find((k) => k.id === credentialId)
?.publicKey ?? null,
setSshKeyCertificate: async (credentialId, certificate) => {
certificateWrites.push({ credentialId, certificate });
return { id: credentialId, changed: true };
},
resolveKeyRefs: async (key, passphrase) => ({
key,
passphrase: passphrase ?? undefined,
@@ -1616,6 +1626,7 @@ export function createFakeContext(
externalClientLaunches,
credentialReads,
createdSshKeys,
certificateWrites,
notifications,
notifyHub,
operationHandlers,
@@ -2307,6 +2318,10 @@ export function createMockCtx(
require("credentials:use");
return ctx.credentials.publicKey(credentialId);
},
setSshKeyCertificate: async (credentialId, certificate) => {
require("credentials:write");
return ctx.credentials.setSshKeyCertificate(credentialId, certificate);
},
resolveKeyRefs: async (key, passphrase) => {
require("credentials:read");
return ctx.credentials.resolveKeyRefs(key, passphrase);
+6 -2
View File
@@ -149,6 +149,7 @@ HostDefaultsContextValue (type): HostDefaultsContextValue
HostDefaultsLevel (type): HostDefaultsLevel
HostDefaultsTarget (type): HostDefaultsTarget
HostDensity (type): HostDensity
HostDotGroup (value): ({ host, badges, className, }: { host: Host; badges: HostBadgeDef[]; className?: string; }) => Element
HostFeatureFields (value): ({ form, updateForm, }: HostFeatureFieldsProps) => Element
HostFeatureFieldsProps (type): HostFeatureFieldsProps
HostFolder (type): HostFolder
@@ -210,7 +211,7 @@ NoticePanel (value): ({ icon: Icon, title, children, action, tone, className, }:
NumberField (value): ({ label, value, onChange, placeholder, hint, error, min, max, disabled, className, }: { label: string; value: number; onChange: (value: number) => void; placeholder?: string; hint?: ReactNode; error?: string; min?: number; max?: number; disabled?: boolean; className?: string; }) => Element
OPEN_PLUGINS_EVENT (value): "termix:open-plugins"
OpenTabRecord (type): OpenTabRecord
PANEL (value): { readonly body: "p-2.5"; readonly gap: "gap-2"; readonly band: "px-3 py-2"; readonly cardHead: "px-3 py-2.5"; readonly header: "h-12.5"; }
PANEL (value): { readonly body: "p-3"; readonly section: "flex flex-col gap-3 py-3"; readonly gap: "gap-2"; readonly band: "px-3 py-2"; readonly cardHead: "px-3 py-2.5"; readonly header: "h-12.5"; }
PRESETS (value): Record<"simple" | "balanced" | "advanced", UiAreaPreferences>
PROMPT_BUTTON (value): "inline-flex h-7 items-center justify-center gap-1.5 border border-border px-3 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
PROMPT_DESTRUCTIVE_BUTTON (value): "inline-flex h-7 items-center justify-center gap-1.5 bg-destructive px-3 text-xs font-medium text-white transition-colors hover:bg-destructive/90 focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
@@ -309,6 +310,7 @@ SurfaceScope (value): ({ kind, onEditingChange, onClose, className, children, }:
Switch (value): ({ className, ...props }: SwitchProps & RefAttributes<HTMLButtonElement>) => Element
SwitchRow (value): ({ label, description, badge, checked, onChange, disabled, defaultKey, rowId, }: { label: string; description?: ReactNode; badge?: string; checked: boolean; onChange: (value: boolean) => void; disabled?: boolean; defaultKey?: string; rowId?: string; }) => Element
TILE_COLUMNS (value): 12
TOOLBAR_FIT (value): "[&_[data-slot=button][data-size=sm]]:h-8 [&_[data-slot=button][data-size=xs]]:h-8 [&_[data-slot=button][data-size=icon-sm]]:size-8 [&_[data-slot=button][data-size=icon-xs]]:size-8 [&_[data-slot=input]]:h-8 [&_[data-slot=select-trigger]]:h-8"
TOTPDialog (value): ({ isOpen, prompt, mode, waiting, onSubmit, onCancel, backgroundColor, }: TOTPDialogProps) => Element
Tab (type): Tab
TabShellCallbacks (type): TabShellCallbacks
@@ -318,7 +320,7 @@ TabStrip (value): ({ tabs, activeTab, onTabChange, isActive, variant, trailing,
TabStripItem (type): TabStripItem
TabType (type): TabType
TagInput (value): ({ tags, input, placeholder, onTagsChange, onInputChange, }: { tags: string[]; input: string; placeholder: string; onTagsChange: (tags: string[]) => void; onInputChange: (value: string) => void; }) => Element
TextAreaField (value): ({ label, value, onChange, placeholder, rows, hint, error, aside, mono, disabled, className, }: { label: string; value: string; onChange: (value: string) => void; placeholder?: string; rows?: number; hint?: ReactNode; error?: string; aside?: ReactNode; mono?: boolean; disabled?: boolean; className?: string; }) => Element
TextAreaField (value): ({ label, value, onChange, placeholder, rows, hint, error, aside, mono, disabled, fill, className, }: { label: string; value: string; onChange: (value: string) => void; placeholder?: string; rows?: number; hint?: ReactNode; error?: string; aside?: ReactNode; mono?: boolean; disabled?: boolean; fill?: boolean; className?: string; }) => Element
TextField (value): ({ label, value, onChange, placeholder, hint, error, aside, disabled, mono, className, }: { label: string; value: string; onChange: (value: string) => void; placeholder?: string; hint?: ReactNode; error?: string; aside?: ReactNode; disabled?: boolean; mono?: boolean; className?: string; }) => Element
Textarea (value): ForwardRefExoticComponent<TextareaProps & RefAttributes<HTMLTextAreaElement>>
TextareaProps (type): TextareaProps
@@ -335,6 +337,8 @@ TileSlot (type): TileSlot
TileStack (value): ({ className, children, }: { className?: string; children: ReactNode; }) => Element
TileWall (value): ({ size, minWidth, gridClassName, className, children, }: { size?: GridSize; minWidth?: number; gridClassName?: string; className?: string; children: ReactNode; }) => Element
TlsStatus (type): TlsStatus
ToolbarBand (value): ({ className, children, }: { className?: string; children: ReactNode; }) => Element
ToolbarSelect (value): <T extends string>({ value, onChange, options, label, disabled, className, }: { value: T; onChange: (value: NoInfer<T>) => void; options: { value: NoInfer<T>; label: string; }[]; label: string; disabled?: boolean; className?: string; }) => Element
ToolsTab (type): ToolsTab
Tooltip (value): ({ ...props }: TooltipProps) => Element
TooltipContent (value): ({ className, sideOffset, children, ...props }: TooltipContentProps & RefAttributes<HTMLDivElement>) => Element
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Builds a signed Flatpak repo and bundle from the release's portable tarballs.
# Usage: build-repo.sh <version> <tarball dir> <output dir>
# Env: FLATPAK_GPG_KEY_ID, FLATPAK_GPG_HOME, FLATPAK_REPO_URL, FLATPAK_ARCHES (optional)
set -euo pipefail
VERSION="$1"
TARBALLS="$(realpath "$2")"
mkdir -p "$3"
OUTPUT="$(realpath "$3")"
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
ARCHES="${FLATPAK_ARCHES:-x86_64 aarch64}"
APP_ID=com.karmaa.termix
WORK="$OUTPUT/.work"
rm -rf "$WORK"
mkdir -p "$WORK"
trap 'rm -rf "$WORK"' EXIT
cd "$WORK"
cp "$ROOT"/packaging/flatpak/$APP_ID.{yml,desktop,metainfo.xml} .
cp "$ROOT/public/icon.svg" "$APP_ID.svg"
convert "$ROOT/public/icon.png" -resize 256x256 icon-256.png
convert "$ROOT/public/icon.png" -resize 128x128 icon-128.png
cp "$TARBALLS/termix_linux_x64_portable.tar.gz" termix-x86_64.tar.gz
cp "$TARBALLS/termix_linux_arm64_portable.tar.gz" termix-aarch64.tar.gz
sed -i "s/VERSION_PLACEHOLDER/$VERSION/g; s/DATE_PLACEHOLDER/$(date +%Y-%m-%d)/g" "$APP_ID.metainfo.xml"
for arch in $ARCHES; do
flatpak-builder --user --arch="$arch" --install-deps-from=flathub \
--default-branch=stable --force-clean --disable-rofiles-fuse --repo=repo \
--gpg-sign="$FLATPAK_GPG_KEY_ID" --gpg-homedir="$FLATPAK_GPG_HOME" \
"build-$arch" "$APP_ID.yml"
done
flatpak build-update-repo --title=Termix --default-branch=stable \
--generate-static-deltas --prune \
--gpg-sign="$FLATPAK_GPG_KEY_ID" --gpg-homedir="$FLATPAK_GPG_HOME" repo
gpg --homedir "$FLATPAK_GPG_HOME" --export "$FLATPAK_GPG_KEY_ID" > termix.gpg
GPG_KEY="$(base64 -w0 termix.gpg)"
for arch in $ARCHES; do
name=termix_linux_flatpak.flatpak
[ "$arch" = x86_64 ] || name="termix_linux_${arch}_flatpak.flatpak"
flatpak build-bundle repo "$OUTPUT/$name" "$APP_ID" stable --arch="$arch" \
--repo-url="$FLATPAK_REPO_URL" --gpg-keys=termix.gpg \
--runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
done
mv repo "$OUTPUT/site"
for file in $APP_ID.flatpakref termix.flatpakrepo; do
sed "s|REPO_URL_PLACEHOLDER|$FLATPAK_REPO_URL|; s|GPG_KEY_PLACEHOLDER|$GPG_KEY|" \
"$ROOT/packaging/flatpak/$file" > "$OUTPUT/site/$file"
done
cp "$OUTPUT/site/$APP_ID.flatpakref" "$OUTPUT/"
@@ -1,10 +1,12 @@
[Flatpak Ref]
Name=Termix
Name=com.karmaa.termix
Branch=stable
Title=Termix
IsRuntime=false
Url=https://github.com/Termix-SSH/Termix/releases/download/VERSION_PLACEHOLDER/termix_linux_flatpak.flatpak
Url=REPO_URL_PLACEHOLDER
SuggestRemoteName=termix
RuntimeRepo=https://flathub.org/repo/flathub.flatpakrepo
GPGKey=GPG_KEY_PLACEHOLDER
Comment=Self-hosted, plugin-based server management.
Description=Termix is a free, open source, self-hosted platform for managing your servers, built on plugins. The core keeps your hosts, credentials, users and sharing, and plugins add the SSH terminal, remote desktop (RDP, VNC, Telnet), file manager, tunnels, Docker, metrics and more. It runs on web, desktop and mobile, and it is a free, self-hosted alternative to Termius.
Icon=https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/icon.png
@@ -7,7 +7,9 @@
<metadata_license>CC0-1.0</metadata_license>
<project_license>Apache-2.0</project_license>
<developer_name>bugattiguy527</developer_name>
<developer id="site.termix">
<name>Termix</name>
</developer>
<description>
<p>
@@ -59,9 +61,6 @@
</keywords>
<provides>
<binary>termix</binary>
</provides>
<requires>
<internet>always</internet>
+22 -35
View File
@@ -1,9 +1,9 @@
app-id: com.karmaa.termix
runtime: org.freedesktop.Platform
runtime-version: "24.08"
runtime-version: "26.08"
sdk: org.freedesktop.Sdk
base: org.electronjs.Electron2.BaseApp
base-version: "24.08"
base-version: "26.08"
command: run.sh
separate-locales: false
@@ -26,52 +26,39 @@ finish-args:
modules:
- name: termix
buildsystem: simple
build-options:
no-debuginfo: true
strip: false
build-commands:
- chmod +x termix.AppImage
- ./termix.AppImage --appimage-extract
- install -Dm755 squashfs-root/termix /app/bin/termix
- cp -r squashfs-root/resources /app/bin/
- cp -r squashfs-root/locales /app/bin/ || true
- cp squashfs-root/*.so /app/bin/ || true
- cp squashfs-root/*.pak /app/bin/ || true
- cp squashfs-root/*.bin /app/bin/ || true
- cp squashfs-root/*.dat /app/bin/ || true
- cp squashfs-root/*.json /app/bin/ || true
- |
cat > run.sh << 'EOF'
#!/bin/bash
export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID"
exec zypak-wrapper /app/bin/termix "$@"
EOF
- chmod +x run.sh
- rm -f app/chrome-sandbox
- mkdir -p /app/termix
- cp -a app/. /app/termix/
- install -Dm755 run.sh /app/bin/run.sh
- install -Dm644 com.karmaa.termix.desktop /app/share/applications/com.karmaa.termix.desktop
- install -Dm644 com.karmaa.termix.metainfo.xml /app/share/metainfo/com.karmaa.termix.metainfo.xml
- install -Dm644 com.karmaa.termix.svg /app/share/icons/hicolor/scalable/apps/com.karmaa.termix.svg
- install -Dm644 icon-256.png /app/share/icons/hicolor/256x256/apps/com.karmaa.termix.png || true
- install -Dm644 icon-128.png /app/share/icons/hicolor/128x128/apps/com.karmaa.termix.png || true
- install -Dm644 icon-256.png /app/share/icons/hicolor/256x256/apps/com.karmaa.termix.png
- install -Dm644 icon-128.png /app/share/icons/hicolor/128x128/apps/com.karmaa.termix.png
sources:
- type: file
url: https://github.com/Termix-SSH/Termix/releases/download/vVERSION_PLACEHOLDER/termix_linux_x64_appimage.AppImage
sha256: CHECKSUM_X64_PLACEHOLDER
dest-filename: termix.AppImage
- type: archive
path: termix-x86_64.tar.gz
dest: app
only-arches:
- x86_64
- type: file
url: https://github.com/Termix-SSH/Termix/releases/download/vVERSION_PLACEHOLDER/termix_linux_arm64_appimage.AppImage
sha256: CHECKSUM_ARM64_PLACEHOLDER
dest-filename: termix.AppImage
- type: archive
path: termix-aarch64.tar.gz
dest: app
only-arches:
- aarch64
- type: script
dest-filename: run.sh
commands:
- export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID"
- exec zypak-wrapper /app/termix/termix "$@"
- type: file
path: com.karmaa.termix.desktop
+7
View File
@@ -0,0 +1,7 @@
[Flatpak Repo]
Title=Termix
Url=REPO_URL_PLACEHOLDER
Homepage=https://github.com/Termix-SSH/Termix
Comment=Self-hosted, plugin-based server management.
Icon=https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/icon.png
GPGKey=GPG_KEY_PLACEHOLDER
Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="207 224 610 620">
<rect x="227" y="244" width="570" height="92" rx="46" fill="#000000"/>
<path d="M474 336H550V786A38 38 0 0 1 474 786V572.2L542 545V447L474 419.8ZM498 647.5V761.5H550V647.5Z" fill="#000000" fill-rule="evenodd"/>
<path d="M306 387.2L510 468.8V523.2L306 604.8V547.5L435.5 496L306 444.5Z" fill="#000000"/>
<rect x="530.5" y="679.5" width="213" height="50" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

+49
View File
@@ -10,6 +10,7 @@ import {
fetchArtifact,
extractArtifact,
pinsFromIndex,
parsePluginTiers,
buildBundledIndex,
findLocalPluginBuilds,
isLocalBuildStale,
@@ -253,6 +254,34 @@ describe("buildBundledIndex", () => {
});
});
describe("parsePluginTiers", () => {
it("reads tiers and onboarding", () => {
expect(
parsePluginTiers({
plugins: {
a: { tier: "full" },
b: { tier: "standard", onboarding: { recommended: true } },
},
}),
).toEqual({
tiers: {
a: { tier: "full" },
b: {
tier: "standard",
onboarding: { recommended: true, consent: false },
},
},
problems: [],
});
});
it("reports a bad tier", () => {
expect(
parsePluginTiers({ plugins: { a: { tier: "huge" } } }).problems,
).toEqual(["a tier must be one of minimal, standard, full"]);
});
});
describe("pinsFromIndex", () => {
const index = {
plugins: [
@@ -305,6 +334,26 @@ describe("pinsFromIndex", () => {
expect(pins[1].tier).toBe("full");
});
it("takes tier and onboarding for a new plugin from the tier map", () => {
const defaults = {
beta: {
tier: "standard",
onboarding: { recommended: true, consent: false },
},
alpha: { tier: "minimal" },
};
const pins = pinsFromIndex(
index,
null,
[{ id: "alpha", tier: "full" }],
null,
defaults,
);
expect(pins[0].tier).toBe("full");
expect(pins[1].tier).toBe("standard");
expect(pins[1].onboarding).toEqual({ recommended: true, consent: false });
});
it("skips releases whose engine.termix excludes the core version", () => {
const withEngine = {
plugins: [
+18 -5
View File
@@ -9,7 +9,7 @@
* so /^\/plugin-api\/docker/ counts the same as "/plugin-api/docker".
*
* Plugins live in their own repos, so the ids are the official ones below
* plus anything in docker/bundled-plugins.json. electron/ and vite.config.ts
* plus anything in docker/bundled-plugins.json and docker/plugin-tiers.json. electron/ and vite.config.ts
* are read as well.
*
* src/backend/tests, src/ui/tests and src/ui/locales are exempt, and so is
@@ -21,12 +21,17 @@
* One plugin id is also an SSH term: "totp" is the one-time-code prompt kind
* in keyboard-interactive auth, which the connect pipeline in
* src/backend/hosts/connect/ classifies. It is not the login plugin.
*
* A few ids are also plain core words: the host "notes" column, the data
* "backups" folder and the node "dns" module. On their own they are not
* counted, but their routes and dotted ids ("notes.list") still are.
*/
const fs = require("node:fs");
const path = require("node:path");
const SSH_TERMS = new Set(["totp"]);
const CORE_WORDS = new Set(["notes", "backups", "dns"]);
const OFFICIAL_IDS = [
"acme-ssl",
@@ -77,6 +82,7 @@ function paths(root) {
root,
src,
bundled: path.join(root, "docker", "bundled-plugins.json"),
tiers: path.join(root, "docker", "plugin-tiers.json"),
extra: [path.join(root, "electron"), path.join(root, "vite.config.ts")],
exempt: [
path.join(src, "backend", "upgrade"),
@@ -88,8 +94,15 @@ function paths(root) {
};
}
function pluginIds(bundledFile) {
function pluginIds(bundledFile, tiersFile) {
const ids = new Set(OFFICIAL_IDS);
if (tiersFile && fs.existsSync(tiersFile)) {
for (const id of Object.keys(
JSON.parse(fs.readFileSync(tiersFile, "utf8")).plugins ?? {},
)) {
ids.add(id);
}
}
if (bundledFile && fs.existsSync(bundledFile)) {
for (const entry of JSON.parse(fs.readFileSync(bundledFile, "utf8"))
.plugins ?? []) {
@@ -146,8 +159,8 @@ function regexLiterals(source) {
}
function scan(root = path.resolve(__dirname, "..")) {
const { src, bundled, extra, exempt, sshConnect } = paths(root);
const ids = pluginIds(bundled);
const { src, bundled, tiers, extra, exempt, sshConnect } = paths(root);
const ids = pluginIds(bundled, tiers);
const found = {};
const add = (file, what) => {
const key = path.relative(root, file).replaceAll("\\", "/");
@@ -167,7 +180,7 @@ function scan(root = path.resolve(__dirname, "..")) {
const inConnect = file.startsWith(sshConnect + path.sep);
for (const text of literals(source)) {
const sshTerm = inConnect && SSH_TERMS.has(text);
if (ids.has(text) && !sshTerm) add(file, text);
if (ids.has(text) && !sshTerm && !CORE_WORDS.has(text)) add(file, text);
for (const route of text.matchAll(
/\/plugin-(?:api|ws|assets)\/([a-z0-9-]+)/g,
)) {
+14
View File
@@ -86,6 +86,20 @@ describe("scan beyond src", () => {
expect(scan(dir)["src/ui/x.ts"]).toContain("/plugin-ws/extra-thing");
});
it("knows ids from plugin-tiers.json, but not core words alone", () => {
const dir = fixture({
"docker/plugin-tiers.json": JSON.stringify({
plugins: { "next-thing": { tier: "full" }, notes: { tier: "full" } },
}),
"src/ui/x.ts": 'const a = "next-thing";\nconst b = "notes";\n',
"src/ui/y.ts": 'const route = "/plugin-api/notes/list";\n',
});
expect(scan(dir)).toEqual({
"src/ui/x.ts": ["next-thing"],
"src/ui/y.ts": ["/plugin-api/notes"],
});
});
it("reads electron", () => {
const dir = fixture({
"electron/main.cjs": 'invoke("/plugin-api/tunnels/list");\n',
+4
View File
@@ -20,6 +20,10 @@ const TSCONFIG = path.join(ROOT, "tsconfig.app.json");
const ALLOWED = [
"SeamlessTileContext",
"ToolbarBand",
"ToolbarSelect",
"HostDotGroup",
"TOOLBAR_FIT",
"TileEditBar",
"TileStack",
"TileColumn",
+9
View File
@@ -63,6 +63,15 @@ writeFileSync(
copyFileSync(join(publicDir, "icon.ico"), join(iconsDir, "icon.ico"));
copyFileSync(join(publicDir, "icon.icns"), join(iconsDir, "icon.icns"));
// Transparent foreground from public/icon.svg; macOS colors this template.
const traySvg = readFileSync(join(iconsDir, "trayTemplate.svg"));
for (const scale of [1, 2]) {
await sharp(traySvg, { density: 72 * scale })
.resize(16 * scale, 16 * scale)
.png()
.toFile(join(iconsDir, `trayTemplate${scale === 2 ? "@2x" : ""}.png`));
}
console.log("Icons generated from public/icon-square.svg");
function buildIco(pngBuffers, sizes) {
+40
View File
@@ -0,0 +1,40 @@
const fs = require("node:fs");
const path = require("node:path");
const { execFileSync } = require("node:child_process");
function findPackage(start) {
let directory = path.dirname(start);
while (directory !== path.dirname(directory)) {
const manifest = path.join(directory, "package.json");
if (fs.existsSync(manifest)) return JSON.parse(fs.readFileSync(manifest));
directory = path.dirname(directory);
}
throw new Error("Could not locate the installed sharp package manifest");
}
// The build runs on x64 but ships arm64 and armv7l too, so npm ci alone
// only installs the x64 binaries.
function linuxSharpPackages(sharpPackage) {
return ["x64", "arm64", "arm"].flatMap((arch) =>
[`@img/sharp-linux-${arch}`, `@img/sharp-libvips-linux-${arch}`].map(
(name) => {
const version = sharpPackage.optionalDependencies?.[name];
if (!version) {
throw new Error(`sharp does not declare ${name} as a dependency`);
}
return `${name}@${version}`;
},
),
);
}
module.exports = { linuxSharpPackages };
if (require.main === module) {
const sharpPackage = findPackage(require.resolve("sharp"));
execFileSync(
"npm",
["install", "--force", "--no-save", ...linuxSharpPackages(sharpPackage)],
{ stdio: "inherit" },
);
}
+58
View File
@@ -0,0 +1,58 @@
import fs from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { describe, expect, it } from "vitest";
const require = createRequire(import.meta.url);
const { linuxSharpPackages } = require("./install-linux-sharp.cjs");
const lock = JSON.parse(
fs.readFileSync(path.join(__dirname, "../package-lock.json"), "utf8"),
);
const sharpManifest = lock.packages["node_modules/sharp"];
describe("Linux sharp packaging", () => {
// The build runs on x64 but ships arm64 and armv7l too, so npm ci alone
// only installs the x64 binaries and the arm AppImages cannot load sharp.
it("pins the sharp and libvips binaries of every shipped Linux architecture", () => {
const packages = linuxSharpPackages({
optionalDependencies: {
"@img/sharp-linux-x64": "1.0.0",
"@img/sharp-linux-arm64": "1.0.1",
"@img/sharp-linux-arm": "1.0.2",
"@img/sharp-libvips-linux-x64": "2.0.0",
"@img/sharp-libvips-linux-arm64": "2.0.1",
"@img/sharp-libvips-linux-arm": "2.0.2",
"@img/sharp-win32-x64": "9.9.9",
},
});
expect([...packages].sort()).toEqual([
"@img/sharp-libvips-linux-arm64@2.0.1",
"@img/sharp-libvips-linux-arm@2.0.2",
"@img/sharp-libvips-linux-x64@2.0.0",
"@img/sharp-linux-arm64@1.0.1",
"@img/sharp-linux-arm@1.0.2",
"@img/sharp-linux-x64@1.0.0",
]);
});
it("only names packages the installed sharp really declares", () => {
const packages = linuxSharpPackages(sharpManifest);
expect(packages).toHaveLength(6);
for (const spec of packages) {
expect(spec).toMatch(/^@img\/sharp-[a-z0-9-]+@\d+\.\d+\.\d+$/);
}
});
it("fails loudly when sharp stops declaring one of them", () => {
expect(() =>
linuxSharpPackages({
optionalDependencies: {
"@img/sharp-linux-x64": "1.0.0",
"@img/sharp-libvips-linux-x64": "2.0.0",
},
}),
).toThrow(/sharp-linux-arm64/);
});
});
+57 -5
View File
@@ -161,6 +161,46 @@ function loadBundledPlugins(root) {
return plugins;
}
/**
* docker/plugin-tiers.json: tier and onboarding for official plugins that are
* not pinned yet, used the first time `--all` pins them.
*/
function parsePluginTiers(raw) {
const problems = [];
const tiers = {};
const map = raw?.plugins;
if (!map || typeof map !== "object" || Array.isArray(map)) {
return { tiers, problems: ['plugin-tiers.json needs a "plugins" object'] };
}
for (const [id, entry] of Object.entries(map)) {
if (!ID.test(id) || !entry || typeof entry !== "object") {
problems.push(`${id} needs an object`);
continue;
}
if (!PROFILES.includes(entry.tier)) {
problems.push(`${id} tier must be one of ${PROFILES.join(", ")}`);
continue;
}
const before = problems.length;
const onboarding = parseOnboarding(entry.onboarding, id, problems);
if (problems.length > before) continue;
tiers[id] = { tier: entry.tier, ...(onboarding ? { onboarding } : {}) };
}
return { tiers, problems };
}
function loadPluginTiers(root) {
const file = path.join(root, "docker", "plugin-tiers.json");
if (!fs.existsSync(file)) return {};
const { tiers, problems } = parsePluginTiers(
JSON.parse(fs.readFileSync(file, "utf8")),
);
if (problems.length > 0) {
throw new Error(`docker/plugin-tiers.json:\n ${problems.join("\n ")}`);
}
return tiers;
}
/** Refuses an artifact whose bytes are not the ones the config pinned. */
function checkSha256(buffer, expected, id) {
const actual = crypto.createHash("sha256").update(buffer).digest("hex");
@@ -286,12 +326,19 @@ function pluginsForProfile(plugins, profile) {
* Pins the newest registry version of each wanted plugin. `ids` is the list
* to pin, or null for every plugin in the index. `current` is the existing
* pin list, so fields the registry does not know (onboarding, tier) carry over;
* a plugin new to the list starts in the full tier. coreVersion, when given,
* a plugin new to the list takes them from `defaults` (plugin-tiers.json),
* else starts in the full tier. coreVersion, when given,
* skips releases whose engine.termix range does not include it, so the image
* never ships a plugin its own core cannot run. With ids null, a plugin new to
* the list that has no usable release (only betas, say) is left out.
*/
function pinsFromIndex(index, ids, current = [], coreVersion = null) {
function pinsFromIndex(
index,
ids,
current = [],
coreVersion = null,
defaults = {},
) {
const plugins = Array.isArray(index?.plugins) ? index.plugins : [];
const byId = new Map(plugins.map((plugin) => [plugin.id, plugin]));
const currentById = new Map(current.map((entry) => [entry.id, entry]));
@@ -315,9 +362,12 @@ function pinsFromIndex(index, ids, current = [], coreVersion = null) {
if (ids === null && !currentById.has(id)) return null;
throw new Error(`${id} is not in the registry index`);
}
const onboarding = currentById.get(id)?.onboarding;
const defaultLook = currentById.get(id)?.defaultLook === true;
const tier = currentById.get(id)?.tier ?? "full";
const existing = currentById.get(id);
const onboarding = existing
? existing.onboarding
: defaults[id]?.onboarding;
const defaultLook = existing?.defaultLook === true;
const tier = existing?.tier ?? defaults[id]?.tier ?? "full";
return {
id,
source: "tmxplug",
@@ -485,6 +535,8 @@ module.exports = {
buildBundledIndex,
pinsFromIndex,
loadBundledPlugins,
parsePluginTiers,
loadPluginTiers,
checkSha256,
fetchArtifact,
extractArtifact,
+16 -3
View File
@@ -4,7 +4,7 @@
* re-released.
*
* node scripts/pin-bundled-plugins.cjs refresh the plugins already listed
* node scripts/pin-bundled-plugins.cjs --all also add plugins new to the index that have a stable release (tier full)
* node scripts/pin-bundled-plugins.cjs --all also add plugins new to the index that have a stable release (tier from docker/plugin-tiers.json, else full)
* --core-version X skip releases whose engine.termix excludes X
*
* The release workflow runs it with --all and the version being released.
@@ -14,6 +14,7 @@ const fs = require("node:fs");
const path = require("node:path");
const {
loadBundledPlugins,
loadPluginTiers,
pinsFromIndex,
} = require("./lib/bundled-plugins.cjs");
@@ -35,8 +36,20 @@ async function main() {
const current = loadBundledPlugins(root);
const ids = all ? null : current.map((plugin) => plugin.id);
const plugins = pinsFromIndex(index, ids, current, coreVersion);
fs.writeFileSync(configPath, `${JSON.stringify({ plugins }, null, 2)}\n`);
const plugins = pinsFromIndex(
index,
ids,
current,
coreVersion,
loadPluginTiers(root),
);
// Format like prettier so lint stays green after the release job pins
const prettier = require("prettier");
const text = await prettier.format(JSON.stringify({ plugins }, null, 2), {
...(await prettier.resolveConfig(configPath)),
filepath: configPath,
});
fs.writeFileSync(configPath, text);
console.log(
`Pinned ${plugins.length} plugin(s) in docker/bundled-plugins.json`,
);
+66
View File
@@ -85,6 +85,33 @@ function errorText(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
/**
* A certificate is read by sshd, not by a shell, so keep it to one line of a
* type sshd knows. A pasted private key, a PEM block or a second line would
* otherwise be stored happily and fail much later as an opaque authentication
* error on connect.
*/
function certificateValue(certificate: unknown): string | null {
if (certificate === null || certificate === undefined) return null;
if (typeof certificate !== "string") {
throw new Error("A certificate must be a string or null");
}
const value = certificate.trim();
if (!value) return null;
if (value.includes("\n")) {
throw new Error("A certificate must be a single line");
}
const type = value.split(/\s+/)[0];
if (
!/^(ssh-(ed25519|rsa)|ecdsa-sha2-nistp(256|384|521))-cert-v01@openssh\.com$/.test(
type,
)
) {
throw new Error("That is not an OpenSSH user certificate");
}
return value;
}
async function requireCorePermission(
userId: string,
permission: string,
@@ -225,6 +252,45 @@ export function createPluginCredentials({
}
},
setSshKeyCertificate: async (credentialId, certificate) => {
const details = `certificate of credential ${credentialId}`;
try {
await assertCapability(pluginId, "credentials:write", declared);
const userId = actingUser();
await requireCorePermission(userId, "credentials.update");
if (!Number.isInteger(credentialId) || credentialId <= 0) {
throw new Error("A credential id is required");
}
const value = certificateValue(certificate);
const { createCurrentCredentialRepository } =
await import("../database/repositories/factory.js");
const repository = createCurrentCredentialRepository();
const credential = await repository.findByIdForUser(
userId,
credentialId,
);
if (!credential) {
throw new Error("Credential not found");
}
// Core only sends a certificate for key auth; storing one on a password
// or agent credential would look saved and never be used.
if (credential.authType !== "key") {
throw new Error("Only SSH key credentials can carry a certificate");
}
const updated = await repository.updateForUser(userId, credentialId, {
certPublicKey: value,
});
await audit("credentials_set_certificate", details, { success: true });
return { id: credentialId, changed: Boolean(updated) };
} catch (error) {
await audit("credentials_set_certificate", details, {
success: false,
errorMessage: errorText(error),
});
throw error;
}
},
publicKey: async (credentialId) => {
const details = `public key of credential ${credentialId}`;
try {
+221
View File
@@ -0,0 +1,221 @@
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import vm from "node:vm";
import sharp from "sharp";
import { afterEach, describe, expect, it, vi } from "vitest";
const root = path.resolve(import.meta.dirname, "../../../..");
const source = readFileSync(path.join(root, "electron/main.cjs"), "utf8");
const temporary: string[] = [];
afterEach(() =>
temporary
.splice(0)
.forEach((dir) => rmSync(dir, { recursive: true, force: true })),
);
type MenuItem = {
label?: string;
checked?: boolean;
role?: string;
submenu?: MenuItem[];
click?: () => void;
};
function harness(platform = "darwin", saved?: string) {
const directory = mkdtempSync(path.join(os.tmpdir(), "termix-tray-"));
temporary.push(directory);
const settingsModule = { exports: {} };
vm.runInNewContext(
readFileSync(path.join(root, "electron/desktop-settings.cjs"), "utf8"),
{
module: settingsModule,
process,
require: (id: string) =>
id === "electron"
? { app: { getPath: () => directory } }
: id === "fs"
? fs
: path,
},
);
const settings = settingsModule.exports as {
getDesktopSettings: () => { macTrayIcon: string };
saveDesktopSettings: (value: { macTrayIcon: string }) => void;
};
if (saved) settings.saveDesktopSettings({ macTrayIcon: saved });
const images: { path: string; setTemplateImage: ReturnType<typeof vi.fn> }[] =
[];
const trays: { destroy: ReturnType<typeof vi.fn> }[] = [];
const app = { name: "Termix", quit: vi.fn() };
const mainWindow = {
show: vi.fn(),
focus: vi.fn(),
isDestroyed: () => false,
};
const createWindow = vi.fn();
const callbacks = new Map<string, () => void>();
const context = {
desktopSettings: settings,
process: { platform, env: {} },
path,
isDev: true,
appRoot: root,
tray: null,
mainWindow,
app,
require: () => ({
nativeImage: {
createFromPath: (p: string) => {
const image = { path: p, setTemplateImage: vi.fn() };
images.push(image);
return image;
},
},
}),
Tray: class {
destroyed = false;
destroy = vi.fn(() => {
this.destroyed = true;
});
constructor() {
trays.push(this);
}
isDestroyed() {
return this.destroyed;
}
setToolTip() {}
setContextMenu() {}
on() {}
},
Menu: {
buildFromTemplate: vi.fn((items: MenuItem[]) => items),
setApplicationMenu: vi.fn(),
},
logToFile: vi.fn(),
loadLinuxTrayIcon: vi.fn(() => "linux-icon"),
BrowserWindow: { getAllWindows: () => [] },
createWindow,
};
const functions =
source.slice(
source.indexOf("function setMacTrayIcon("),
source.indexOf("function loadLinuxTrayIcon("),
) +
source.slice(
source.indexOf("function createMenu("),
source.indexOf("app.whenReady()"),
);
const runtime = vm.runInNewContext(
functions + "\n({ createTray, createMenu });",
context,
) as { createTray: () => void; createMenu: () => void };
// Evaluate the real Dock activation and window-close policies, too.
const lifecycle = source.slice(
source.indexOf('app.on("window-all-closed"'),
source.indexOf('app.on("before-quit"'),
);
vm.runInNewContext(lifecycle, {
...context,
app: {
...app,
on: (event: string, fn: () => void) => callbacks.set(event, fn),
},
});
return { runtime, settings, images, trays, context, callbacks, createWindow };
}
describe("macOS menu bar icon", () => {
it("defaults to a transparent template, with an explicit color option", () => {
const h = harness();
h.runtime.createTray();
h.runtime.createMenu();
expect(h.images[0].path).toMatch(/trayTemplate\.png$/);
expect(h.images[0].setTemplateImage).toHaveBeenCalledWith(true);
const menu = h.context.Menu.setApplicationMenu.mock.calls.at(
-1,
)![0] as MenuItem[];
const options = menu[0].submenu!.find(
(item) => item.label === "Menu Bar Icon",
)!.submenu!;
expect(options[0].checked).toBe(true);
options[1].click!();
expect(h.trays[0].destroy).toHaveBeenCalledOnce();
expect(h.settings.getDesktopSettings().macTrayIcon).toBe("color");
expect(h.images.at(-1)!.path).toMatch(/16x16\.png$/);
expect(h.images.at(-1)!.setTemplateImage).toHaveBeenCalledWith(false);
});
it("hides immediately, persists the choice, and keeps the app menu available to restore it", () => {
const h = harness();
h.runtime.createTray();
h.runtime.createMenu();
const options = (
h.context.Menu.setApplicationMenu.mock.calls.at(-1)![0] as MenuItem[]
)[0].submenu![1].submenu!;
options[2].click!();
expect(h.trays[0].destroy).toHaveBeenCalledOnce();
expect(h.trays).toHaveLength(1);
expect(h.settings.getDesktopSettings().macTrayIcon).toBe("hidden");
expect(h.context.mainWindow.show).toHaveBeenCalledOnce();
const updated = (
h.context.Menu.setApplicationMenu.mock.calls.at(-1)![0] as MenuItem[]
)[0].submenu![1].submenu!;
expect(updated[2].checked).toBe(true);
updated[0].click!();
expect(h.trays).toHaveLength(2);
expect(h.images.at(-1)!.setTemplateImage).toHaveBeenCalledWith(true);
});
it("honors hidden on launch while preserving Dock activation and quitting after the last window closes", () => {
const h = harness("darwin", "hidden");
h.runtime.createTray();
h.runtime.createMenu();
expect(h.trays).toHaveLength(0);
h.callbacks.get("activate")!();
expect(h.createWindow).toHaveBeenCalledOnce();
h.callbacks.get("window-all-closed")!();
expect(h.context.app.quit).toHaveBeenCalledOnce();
expect(
(
h.context.Menu.setApplicationMenu.mock.calls[0][0] as MenuItem[]
)[0].submenu!.some((item) => item.role === "quit"),
).toBe(true);
});
it.each(["win32", "linux"])(
"does not hide or add macOS options on %s",
(platform) => {
const h = harness(platform, "hidden");
h.runtime.createTray();
h.runtime.createMenu();
expect(h.trays).toHaveLength(1);
expect(h.context.Menu.setApplicationMenu).not.toHaveBeenCalled();
},
);
it.each([1, 2])(
"ships a transparent, black template at %sx",
async (scale) => {
const file = path.join(
root,
`public/icons/trayTemplate${scale === 2 ? "@2x" : ""}.png`,
);
const { data, info } = await sharp(file)
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
expect([info.width, info.height]).toEqual([16 * scale, 16 * scale]);
const alpha = [];
for (let i = 0; i < data.length; i += 4) {
expect([data[i], data[i + 1], data[i + 2]]).toEqual([0, 0, 0]);
alpha.push(data[i + 3]);
}
expect(alpha).toContain(0);
expect(alpha.some((value) => value > 0)).toBe(true);
expect(alpha.filter((value) => value > 0).length).toBeLessThan(
alpha.length * 0.8,
);
},
);
});
@@ -17,6 +17,7 @@ const state = vi.hoisted(() => ({
resolverCalls: [] as Array<Record<string, unknown>>,
savedCredentials: [] as Array<Record<string, unknown>>,
userCredentials: new Map<string, Array<Record<string, unknown>>>(),
credentialUpdates: [] as Array<Record<string, unknown>>,
corePermissions: new Set<string>(["credentials.view", "credentials.create"]),
logins: new Map<string, Record<string, unknown>>(),
ownerDecrypts: 0,
@@ -33,6 +34,23 @@ vi.mock("../../database/repositories/factory.js", () => ({
createCurrentCredentialRepository: () => ({
listDecryptedByUserId: async (userId: string) =>
state.userCredentials.get(userId) ?? [],
findByIdForUser: async (userId: string, credentialId: number) =>
(state.userCredentials.get(userId) ?? []).find(
(row) => row.id === credentialId,
) ?? null,
updateForUser: async (
userId: string,
credentialId: number,
update: Record<string, unknown>,
) => {
const row = (state.userCredentials.get(userId) ?? []).find(
(entry) => entry.id === credentialId,
);
if (!row) return null;
Object.assign(row, update);
state.credentialUpdates.push({ userId, credentialId, update });
return row;
},
createEncryptedForUser: async (
userId: string,
data: Record<string, unknown>,
@@ -177,6 +195,7 @@ beforeEach(() => {
state.resolverCalls.length = 0;
state.savedCredentials.length = 0;
state.userCredentials.clear();
state.credentialUpdates.length = 0;
state.corePermissions = new Set(["credentials.view", "credentials.create"]);
state.hosts.set(7, { ...baseHost });
state.logins.clear();
@@ -641,3 +660,143 @@ describe("ctx.credentials.createSshKey", () => {
});
});
});
describe("ctx.credentials.setSshKeyCertificate", () => {
const certificate =
"ssh-ed25519-cert-v01@openssh.com AAAAC3NzaC1lZDI1NTE5AAAAIBtest termix:@owner:7";
beforeEach(() => {
state.corePermissions.add("credentials.update");
state.userCredentials.set("owner", [
{
id: 7,
userId: "owner",
authType: "key",
name: "Laptop",
certPublicKey: null,
},
{
id: 8,
userId: "owner",
authType: "password",
name: "Legacy",
certPublicKey: null,
},
]);
});
it("refuses without credentials:write and audits the refusal", async () => {
grants.set("demo", ["credentials:use"]);
const ctx = contextFor(["credentials:use"]);
await expect(
runAsActor("owner", "request", () =>
ctx.credentials.setSshKeyCertificate(7, certificate),
),
).rejects.toThrow(/credentials:write/);
expect(state.credentialUpdates).toHaveLength(0);
expect(auditEntries.at(-1)).toMatchObject({
action: "plugin_credentials_set_certificate",
success: false,
});
});
it("refuses without the core credentials.update permission", async () => {
grants.set("demo", ["credentials:write"]);
state.corePermissions.delete("credentials.update");
const ctx = contextFor(["credentials:write"]);
await expect(
runAsActor("owner", "request", () =>
ctx.credentials.setSshKeyCertificate(7, certificate),
),
).rejects.toThrow(/credentials.update/);
expect(state.credentialUpdates).toHaveLength(0);
});
it("stores the certificate on the actor's key credential", async () => {
grants.set("demo", ["credentials:write"]);
const ctx = contextFor(["credentials:write"]);
const result = await runAsActor("owner", "request", () =>
ctx.credentials.setSshKeyCertificate(7, certificate),
);
expect(result).toEqual({ id: 7, changed: true });
expect(state.credentialUpdates[0]).toMatchObject({
userId: "owner",
credentialId: 7,
update: { certPublicKey: certificate },
});
expect(state.userCredentials.get("owner")?.[0].certPublicKey).toBe(
certificate,
);
expect(auditEntries.at(-1)).toMatchObject({
action: "plugin_credentials_set_certificate",
success: true,
});
});
it("clears a stored certificate with null", async () => {
grants.set("demo", ["credentials:write"]);
state.userCredentials.get("owner")![0].certPublicKey = certificate;
const ctx = contextFor(["credentials:write"]);
const result = await runAsActor("owner", "request", () =>
ctx.credentials.setSshKeyCertificate(7, null),
);
expect(result).toEqual({ id: 7, changed: true });
expect(state.userCredentials.get("owner")?.[0].certPublicKey).toBeNull();
});
it("refuses a credential core never sends a certificate for", async () => {
grants.set("demo", ["credentials:write"]);
const ctx = contextFor(["credentials:write"]);
await expect(
runAsActor("owner", "request", () =>
ctx.credentials.setSshKeyCertificate(8, certificate),
),
).rejects.toThrow(/Only SSH key credentials/);
expect(state.credentialUpdates).toHaveLength(0);
});
it("refuses a credential the actor does not own", async () => {
grants.set("demo", ["credentials:write"]);
const ctx = contextFor(["credentials:write"]);
await expect(
runAsActor("owner", "request", () =>
ctx.credentials.setSshKeyCertificate(99, certificate),
),
).rejects.toThrow(/Credential not found/);
});
it("refuses a line that is not an OpenSSH user certificate", async () => {
grants.set("demo", ["credentials:write"]);
const ctx = contextFor(["credentials:write"]);
await expect(
runAsActor("owner", "request", () =>
ctx.credentials.setSshKeyCertificate(7, "ssh-ed25519 AAAAC3Nza"),
),
).rejects.toThrow(/not an OpenSSH user certificate/);
expect(state.credentialUpdates).toHaveLength(0);
});
it("refuses a multi-line paste", async () => {
grants.set("demo", ["credentials:write"]);
const ctx = contextFor(["credentials:write"]);
await expect(
runAsActor("owner", "request", () =>
ctx.credentials.setSshKeyCertificate(
7,
`${certificate}\n${certificate}`,
),
),
).rejects.toThrow(/single line/);
expect(state.credentialUpdates).toHaveLength(0);
});
});
@@ -0,0 +1,137 @@
import { generateKeyPairSync } from "node:crypto";
import net from "node:net";
import { Client, Server } from "ssh2";
import { describe, expect, it } from "vitest";
import { buildSSHAlgorithms } from "../../utils/ssh-algorithms.js";
// An SSH peer advertising only DH group exchange. Stop after the client's
// plaintext GEX request, before expensive prime validation or authentication.
async function requestedGroupBits(): Promise<number> {
const name = (value: string) => {
const data = Buffer.from(value);
const size = Buffer.alloc(4);
size.writeUInt32BE(data.length);
return Buffer.concat([size, data]);
};
const payload = Buffer.concat([
Buffer.from([20]),
Buffer.alloc(16),
...[
"diffie-hellman-group-exchange-sha256",
"rsa-sha2-256",
"aes256-gcm@openssh.com,aes128-gcm@openssh.com",
"aes256-gcm@openssh.com,aes128-gcm@openssh.com",
"hmac-sha2-256",
"hmac-sha2-256",
"none",
"none",
"",
"",
].map(name),
Buffer.alloc(5),
]);
const padding = 8 - ((payload.length + 5) % 8) + 8;
const header = Buffer.alloc(5);
header.writeUInt32BE(payload.length + padding + 1);
header[4] = padding;
const client = new Client();
let peer: net.Socket | undefined;
let timer: ReturnType<typeof setTimeout> | undefined;
const server = net.createServer();
try {
const result = new Promise<number>((resolve, reject) => {
timer = setTimeout(() => reject(new Error("No SSH group request")), 3000);
client.on("error", reject);
server.on("connection", (socket) => {
peer = socket;
let buffer = Buffer.alloc(0);
let banner = false;
socket.write(
Buffer.concat([
Buffer.from("SSH-2.0-OpenSSH_7.4\r\n"),
header,
payload,
Buffer.alloc(padding),
]),
);
socket.on("data", (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
if (!banner) {
const end = buffer.indexOf("\n");
if (end < 0) return;
buffer = buffer.subarray(end + 1);
banner = true;
}
while (buffer.length >= 5) {
const length = buffer.readUInt32BE(0) + 4;
if (buffer.length < length) return;
if (buffer[5] === 34) resolve(buffer.readUInt32BE(10));
buffer = buffer.subarray(length);
}
});
});
});
await new Promise<void>((resolve) =>
server.listen(0, "127.0.0.1", resolve),
);
client.connect({
host: "127.0.0.1",
port: (server.address() as net.AddressInfo).port,
username: "test",
algorithms: buildSSHAlgorithms(false),
});
return await result;
} finally {
clearTimeout(timer);
client.destroy();
peer?.destroy();
await new Promise<void>((resolve) => server.close(() => resolve()));
}
}
describe("SSH cipher preference", () => {
it("requests a 3072-bit group from a GEX-only peer offering both GCM ciphers", async () => {
expect(await requestedGroupBits()).toBe(3072);
});
it("still connects when a server only supports AES-256-GCM", async () => {
const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
const server = new Server(
{
hostKeys: [privateKey.export({ type: "pkcs1", format: "pem" })],
algorithms: { cipher: ["aes256-gcm@openssh.com"] },
},
(peer) => {
peer.on("authentication", (auth) => auth.accept());
},
);
const client = new Client();
try {
await new Promise<void>((resolve) =>
server.listen(0, "127.0.0.1", resolve),
);
await new Promise<void>((resolve, reject) => {
client.once("ready", resolve).once("error", reject);
client.connect({
host: "127.0.0.1",
port: (server.address() as net.AddressInfo).port,
username: "test",
readyTimeout: 2000,
algorithms: buildSSHAlgorithms(false),
});
});
} finally {
client.destroy();
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
it("keeps legacy algorithms opt-in", () => {
expect(buildSSHAlgorithms(false).kex).not.toContain(
"diffie-hellman-group-exchange-sha1",
);
expect(buildSSHAlgorithms(true).kex).toContain(
"diffie-hellman-group-exchange-sha1",
);
});
});
+3 -1
View File
@@ -97,8 +97,10 @@ export function buildSSHAlgorithms(
];
const cipher = filterCiphers([
"chacha20-poly1305@openssh.com",
"aes256-gcm@openssh.com",
// ssh2 sizes DH group exchange from the cipher key length. Prefer
// AES-128-GCM to avoid 8192-bit prime validation on GEX-only hosts.
"aes128-gcm@openssh.com",
"aes256-gcm@openssh.com",
"aes256-ctr",
"aes192-ctr",
"aes128-ctr",
+1 -1
View File
@@ -12,7 +12,7 @@ function Checkbox({
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-none border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"peer border-border dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-none border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
+1 -1
View File
@@ -165,7 +165,7 @@ export function HexColorPicker({
type="color"
value={safe}
onChange={(e) => onChange(e.target.value)}
className="h-8 w-10 shrink-0 cursor-pointer rounded-none border border-input bg-transparent p-0.5"
className="h-8 w-10 shrink-0 cursor-pointer rounded-none border border-border bg-transparent p-0.5"
/>
<Input
value={value}
+7 -3
View File
@@ -253,6 +253,7 @@ export function TextAreaField({
aside,
mono,
disabled,
fill,
className,
}: {
label: string;
@@ -265,6 +266,8 @@ export function TextAreaField({
aside?: ReactNode;
mono?: boolean;
disabled?: boolean;
/** Takes the rest of the height, for config and log editors. */
fill?: boolean;
className?: string;
}) {
return (
@@ -273,7 +276,7 @@ export function TextAreaField({
hint={hint}
error={error}
aside={aside}
className={className}
className={cn(fill && "min-h-0 flex-1", className)}
>
<textarea
rows={rows}
@@ -282,8 +285,9 @@ export function TextAreaField({
disabled={disabled}
onChange={(e) => onChange(e.target.value)}
className={cn(
"w-full resize-none border border-input bg-background px-3 py-2 text-xs text-foreground transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30",
"w-full resize-none border border-border bg-background px-3 py-2 text-xs text-foreground transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30",
mono && "font-mono text-[10px]",
fill && "min-h-40 flex-1",
)}
/>
</Field>
@@ -418,7 +422,7 @@ export function TagInput({
}) {
const { t } = useTranslation();
return (
<div className="flex min-h-8 flex-wrap items-center gap-1 border border-input bg-background px-2 py-1 focus-within:border-ring focus-within:ring-1 focus-within:ring-ring/50 dark:bg-input/30">
<div className="flex min-h-8 flex-wrap items-center gap-1 border border-border bg-background px-2 py-1 focus-within:border-ring focus-within:ring-1 focus-within:ring-ring/50 dark:bg-input/30">
{tags.map((tag) => (
<span
key={tag}
+1 -1
View File
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-none border border-input bg-transparent px-2.5 py-1 text-xs transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 md:text-xs dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
"h-8 w-full min-w-0 rounded-none border border-border bg-transparent px-2.5 py-1 text-xs transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 md:text-xs dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
type === "number" &&
"[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none",
className,
+17 -6
View File
@@ -4,6 +4,7 @@ import { ChevronRight, Plus, Trash2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
import { Button } from "@/components/button";
import { useStatusColorScheme } from "@/hooks/use-status-color-scheme";
export type ListRowTone =
"brand" | "muted" | "success" | "warning" | "destructive" | "none";
@@ -17,6 +18,16 @@ const STRIPE: Record<ListRowTone, string> = {
none: "bg-transparent",
};
// Accent scheme, like the host list: good is the accent, anything else is dim.
const ACCENT_STRIPE: Record<ListRowTone, string> = {
brand: "bg-accent-brand/60",
muted: "bg-muted-foreground/30",
success: "bg-accent-brand/60",
warning: "bg-muted-foreground/30",
destructive: "bg-muted-foreground/30",
none: "bg-transparent",
};
/** Scrolling list body. Rows sit flush, divided by their own borders. */
export function PanelList({
empty,
@@ -96,6 +107,7 @@ export function ListRow({
React.HTMLAttributes<HTMLDivElement>,
"title" | "onClick" | "children"
>) {
const stripes = useStatusColorScheme() === "status" ? STRIPE : ACCENT_STRIPE;
return (
<div
{...rest}
@@ -127,7 +139,7 @@ export function ListRow({
<div
className={cn(
"w-[3px] shrink-0",
selected ? "bg-accent-brand" : !color && STRIPE[tone],
selected ? "bg-accent-brand" : !color && stripes[tone],
)}
style={color && !selected ? { backgroundColor: color } : undefined}
/>
@@ -220,7 +232,7 @@ export function ListBadge({
return (
<span
className={cn(
"flex shrink-0 items-center gap-0.5 border px-1 py-px text-[9px] uppercase leading-none tracking-wider [&_svg]:size-2.5",
"inline-flex w-fit shrink-0 items-center gap-0.5 border px-1 py-px text-[9px] uppercase leading-none tracking-wider [&_svg]:size-2.5",
tone === "brand" &&
"border-accent-brand/30 bg-accent-brand/10 text-accent-brand",
tone === "warning" && "border-warning/30 bg-warning/10 text-warning",
@@ -382,7 +394,8 @@ export function AddButton({
className,
)}
>
<Plus className="size-3.5" />
{/* Whole pixels so the plus sits centered; the root font is 14px. */}
<Plus className="size-[12px]" />
{!compact && <span>{text}</span>}
</Button>
);
@@ -428,7 +441,6 @@ export function FormFooter({
{onDelete && (
<Button
variant="ghost"
size="sm"
onClick={onDelete}
disabled={saving}
className="text-destructive hover:bg-destructive/10 hover:text-destructive dark:hover:bg-destructive/10"
@@ -444,7 +456,7 @@ export function FormFooter({
))}
</div>
{onCancel && (
<Button variant="ghost" size="sm" onClick={onCancel} disabled={saving}>
<Button variant="ghost" onClick={onCancel} disabled={saving}>
{cancelLabel ?? t("common.cancel")}
</Button>
)}
@@ -454,7 +466,6 @@ export function FormFooter({
type={saveType}
form={form}
variant="outline"
size="sm"
onClick={onSave}
disabled={disabled || saving}
className="border-accent-brand/40 px-6 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand dark:border-accent-brand/40 dark:bg-transparent dark:hover:bg-accent-brand/10"
+2
View File
@@ -19,6 +19,7 @@ export function LoadingState({
return (
<div
role="status"
data-slot="loading-state"
className={cn(
"flex items-center gap-2 px-3 py-2 text-xs text-muted-foreground",
className,
@@ -32,6 +33,7 @@ export function LoadingState({
return (
<div
role="status"
data-slot="loading-state"
className={cn(
"flex flex-1 flex-col items-center justify-center gap-2 px-4 py-12 text-xs text-muted-foreground",
className,
+1 -1
View File
@@ -102,7 +102,7 @@ export function ManagerSearch({
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder ?? t("managerCard.filter")}
className="h-7 w-full border border-border bg-background pl-7 pr-2 text-xs outline-none focus:ring-1 focus:ring-ring"
className="h-8 w-full border border-border bg-background pl-7 pr-2 text-xs outline-none focus:ring-1 focus:ring-ring"
/>
</div>
{count != null && (
+90 -10
View File
@@ -16,6 +16,7 @@ import { cn } from "@/lib/utils";
import { Separator } from "@/components/separator";
import { DocsLink } from "@/components/docs-link";
import { useInSeamlessTile } from "@/components/tile-grid/context";
import { FooterClosed } from "@/components/tile-grid/edge";
import {
Select,
SelectContent,
@@ -26,13 +27,81 @@ import {
/** One spacing rhythm for every panel and tab. */
export const PANEL = {
body: "p-2.5",
body: "p-3",
section: "flex flex-col gap-3 py-3",
gap: "gap-2",
band: "px-3 py-2",
cardHead: "px-3 py-2.5",
header: "h-12.5",
} as const;
/** Makes every control in a toolbar row the same 32px height. */
export const TOOLBAR_FIT =
"[&_[data-slot=button][data-size=sm]]:h-8 [&_[data-slot=button][data-size=xs]]:h-8 [&_[data-slot=button][data-size=icon-sm]]:size-8 [&_[data-slot=button][data-size=icon-xs]]:size-8 [&_[data-slot=input]]:h-8 [&_[data-slot=select-trigger]]:h-8";
/** A small unlabelled select for toolbar rows, as tall as the other controls. */
export function ToolbarSelect<T extends string>({
value,
onChange,
options,
label,
disabled,
className,
}: {
value: T;
onChange: (value: NoInfer<T>) => void;
options: { value: NoInfer<T>; label: string }[];
/** Read by screen readers, since the select has no visible label. */
label: string;
disabled?: boolean;
className?: string;
}) {
return (
<Select
value={value}
onValueChange={(next) => onChange(next as T)}
disabled={disabled}
>
<SelectTrigger
aria-label={label}
className={cn("h-8 w-auto text-xs", className)}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
/** A toolbar row for controls outside the PanelShell toolbar slot. */
export function ToolbarBand({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) {
return (
<div
data-slot="toolbar-band"
className={cn(
"flex shrink-0 flex-wrap items-center gap-2 border-b border-border",
PANEL.band,
TOOLBAR_FIT,
className,
)}
>
{children}
</div>
);
}
export type PanelViewMode = "grid" | "list";
export type PanelDensity = "comfortable" | "compact";
@@ -112,18 +181,20 @@ export function PanelShell({
className,
)}
>
{children}
<FooterClosed closed={!!footer}>{children}</FooterClosed>
</div>
);
const bands = (
<>
{tabs && (
<div className="shrink-0 border-b border-border px-1">{tabs}</div>
<div className={`shrink-0 border-b border-border px-1 ${TOOLBAR_FIT}`}>
{tabs}
</div>
)}
{toolbar && (
<div
className={`flex shrink-0 items-center gap-2 border-b border-border ${PANEL.band}`}
className={`flex shrink-0 items-center gap-2 border-b border-border ${PANEL.band} ${TOOLBAR_FIT}`}
>
{toolbar}
</div>
@@ -132,7 +203,9 @@ export function PanelShell({
);
const foot = footer && (
<div className="flex shrink-0 items-center gap-2 border-t border-border px-3 py-2">
<div
className={`flex shrink-0 items-center gap-2 border-t border-border px-3 py-2 ${TOOLBAR_FIT}`}
>
{footer}
</div>
);
@@ -334,7 +407,7 @@ export function PanelSearch({
onEnter();
}
}}
className="h-8 w-full min-w-0 border border-input bg-transparent pl-8 pr-7 text-xs outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30"
className="h-8 w-full min-w-0 border border-border bg-transparent pl-8 pr-7 text-xs outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30"
/>
{value && (
<button
@@ -375,7 +448,11 @@ export function Segmented<T extends string>({
return (
<div
role="radiogroup"
className={cn("flex items-center border border-border", className)}
data-slot="segmented"
className={cn(
"flex w-fit shrink-0 items-center border border-border",
className,
)}
>
{options.map((option, i) => {
const active = option.value === value;
@@ -388,7 +465,7 @@ export function Segmented<T extends string>({
title={option.title ?? option.label}
onClick={() => onChange(option.value)}
className={cn(
"flex h-[30px] items-center justify-center gap-1.5 px-2.5 text-[11px] font-medium transition-colors focus-visible:relative focus-visible:z-10 focus-visible:ring-1 focus-visible:ring-ring",
"flex h-[calc(2rem-2px)] items-center justify-center gap-1.5 px-2.5 text-[11px] font-medium transition-colors focus-visible:relative focus-visible:z-10 focus-visible:ring-1 focus-visible:ring-ring",
i > 0 && "border-l border-border",
active
? "bg-accent-brand/10 text-accent-brand"
@@ -428,7 +505,10 @@ export function ViewToggle({
}) {
const { t } = useTranslation();
const toggle = (
<div className="flex items-center border border-border">
<div
data-slot="view-toggle"
className="flex items-center border border-border"
>
<ToggleButton
active={view === "grid"}
onClick={() => onView("grid")}
@@ -513,7 +593,7 @@ function ToggleButton({
aria-label={title}
aria-pressed={active}
className={cn(
"flex size-[30px] items-center justify-center transition-colors focus-visible:relative focus-visible:z-10 focus-visible:ring-1 focus-visible:ring-ring",
"flex size-[calc(2rem-2px)] items-center justify-center transition-colors focus-visible:relative focus-visible:z-10 focus-visible:ring-1 focus-visible:ring-ring",
bordered && "border-l border-border",
active
? "bg-accent-brand/10 text-accent-brand"
+1 -1
View File
@@ -35,7 +35,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-none border bg-transparent px-2.5 py-1 text-xs whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-8 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"border-border data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-none border bg-transparent px-2.5 py-1 text-xs whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-8 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
+1 -1
View File
@@ -154,7 +154,7 @@ function Select2({
aria-expanded={open}
aria-required={required}
className={cn(
"border-input bg-transparent dark:bg-input/30 dark:hover:bg-input/50 text-foreground flex h-8 w-full items-center justify-between gap-2 rounded-none border px-2.5 py-1 text-xs shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"border-border bg-transparent dark:bg-input/30 dark:hover:bg-input/50 text-foreground flex h-8 w-full items-center justify-between gap-2 rounded-none border px-2.5 py-1 text-xs shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
onKeyDown={(event) => {
+3 -1
View File
@@ -138,7 +138,9 @@ export function TabStrip({
</button>
)}
{trailing && (
<div className="flex shrink-0 items-center gap-1 px-1">{trailing}</div>
<div className="flex shrink-0 items-center gap-1 px-2 [&_[data-slot=button]]:!h-6 [&_[data-slot=button][data-size^=icon]]:!size-6 [&_[data-slot=input]]:!h-6 [&_[data-slot=select-trigger]]:!h-6 [&_[data-slot=segmented]_button]:!h-[calc(1.5rem-2px)]">
{trailing}
</div>
)}
</div>
);
+1 -1
View File
@@ -40,7 +40,7 @@ function TabsTrigger({
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-none border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] duration-200 focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-border dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-none border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] duration-200 focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
+1 -1
View File
@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-none border border-input bg-transparent dark:bg-input/30 px-2.5 py-2 text-xs shadow-xs transition-[color,box-shadow] duration-200 outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 disabled:pointer-events-none",
"flex min-h-[80px] w-full rounded-none border border-border bg-transparent dark:bg-input/30 px-2.5 py-2 text-xs shadow-xs transition-[color,box-shadow] duration-200 outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 disabled:pointer-events-none",
className,
)}
ref={ref}
+1 -1
View File
@@ -50,7 +50,7 @@ const SNAP = 8;
const SCROLL_EDGE = 48;
const styles = `
.tg-grid { position: relative; }
.tg-grid { position: relative; overflow-x: clip; }
.tg-tile { position: absolute; left: 0; top: 0; min-width: 0; display: flex; flex-direction: column; }
.tg-grid[data-animate="true"] .tg-tile:not([data-active]) { transition: transform 160ms ease, width 160ms ease, height 160ms ease; }
.tg-tile[data-active] { z-index: 40; }
+1 -1
View File
@@ -21,7 +21,7 @@ export function TileStack({
{Children.toArray(children).map((section, index) => (
<div
key={keyOf(section, index)}
className="min-w-0 shrink-0 border-b border-border"
className="min-w-0 shrink-0 border-b border-border empty:hidden [&>[data-slot=toolbar-band]]:border-b-0"
>
{section}
</div>
+24 -3
View File
@@ -28,6 +28,23 @@ export function EdgeOwned({ children }: { children: ReactNode }) {
);
}
/** True in a body with a pinned footer, whose top line already closes it. */
const FooterClosedContext = createContext(false);
export function FooterClosed({
closed,
children,
}: {
closed: boolean;
children: ReactNode;
}) {
return (
<FooterClosedContext.Provider value={closed}>
{children}
</FooterClosedContext.Provider>
);
}
/**
* Whether the element's end reaches the bottom of the visible scroll area,
* where a closing line would sit on the screen edge and only add noise.
@@ -65,7 +82,7 @@ function useReachesBottom(ref: React.RefObject<HTMLElement | null>) {
* Wraps tiles whose last row draws a line under itself. The line stays when
* there is room below and is clipped when the tiles reach the bottom of the
* screen, the same rule the tile grid uses. Inside a stack section the
* section owns the line, so it is always clipped.
* section owns the line, so it is always clipped, and so does a pinned footer.
*/
export function BottomEdge({
className,
@@ -77,11 +94,15 @@ export function BottomEdge({
children: ReactNode;
}) {
const owned = useContext(EdgeOwnedContext);
const closed = useContext(FooterClosedContext);
const ref = useRef<HTMLDivElement>(null);
const reaches = useReachesBottom(ref);
return (
<div ref={ref} className={cn("min-w-0 overflow-clip", className)}>
<div className={cn((owned || reaches) && "-mb-px", innerClassName)}>
// flow-root keeps the -1px margin inside, overflow clip alone lets it escape.
<div ref={ref} className={cn("flow-root min-w-0 overflow-clip", className)}>
<div
className={cn((owned || closed || reaches) && "-mb-px", innerClassName)}
>
{children}
</div>
</div>
+10
View File
@@ -318,6 +318,16 @@ html.fs-xl {
font-size: 20px;
}
/* Small icons in buttons snap to even pixels so they sit centered and sharp at every font size. */
:is(button, [role="button"]) svg:is(.size-3, .size-3\.5) {
width: round(nearest, 0.875rem, 2px);
height: round(nearest, 0.875rem, 2px);
}
:is(button, [role="button"]) svg.size-3 {
width: round(nearest, 0.75rem, 2px);
height: round(nearest, 0.75rem, 2px);
}
@supports (padding-bottom: env(safe-area-inset-bottom)) {
.safe-bottom {
padding-bottom: env(safe-area-inset-bottom);
+3 -1
View File
@@ -495,6 +495,7 @@
"deleteSelected": "Delete",
"exitSelection": "Exit selection",
"collapseBtn": "Collapse",
"importExportBtn": "Import / Export",
"hostStatusesRefreshed": "Host statuses refreshed",
"failedToRefreshHosts": "Failed to refresh hosts",
"movedHostTo": "Moved {{host}} to \"{{folder}}\"",
@@ -1053,7 +1054,8 @@
"actionNextTab": "Switch to next tab",
"actionPreviousTab": "Switch to previous tab",
"actionOpenCommandPalette": "Open quick search",
"actionReconnectSession": "Reconnect current session"
"actionReconnectSession": "Reconnect current session",
"actionToggleSidebar": "Toggle sidebar"
},
"userProfile": {
"preset_simple": "Simple",
+47
View File
@@ -609,6 +609,7 @@
"hostClickBehavior": "Klik op 'n gasheer",
"hostClickBehaviorDesc": "Wat gebeur wanneer jy op 'n gasheer klik wat reeds 'n oop tab het. Ctrl/Cmd-klik, middel-klik, of \"Maak oop in nuwe tab\" begin altyd 'n nuwe een.",
"hostClickNewTab": "Maak altyd 'n nuwe tab oop",
"hostClickHostPage": "Open die gasheerblad, as 'n invoegprogram een bied",
"hostClickFocusExisting": "Skakel oor na die oop tab",
"hostClickFocusExistingDoubleNew": "Klik om te skakel, dubbelklik vir 'n nuwe tab",
"openInNewTab": "Maak oop in nuwe tab",
@@ -1214,6 +1215,18 @@
"title": "Stuur waarskuwings",
"consequence": "Na jou of ander gebruikers, en deur die kanale wat hulle opgestel het."
},
"operations:provide": {
"title": "Bied aksies aan",
"consequence": "Sy aksies kan uitgevoer word outomaties, vanaf die CLI, die foon-app en ander plekke, deur mense wat toestemming het om dit te gebruik."
},
"operations:invoke": {
"title": "Voer aksies van ander invoegprogramme uit",
"consequence": "Handel namens die aangemelde gebruiker op die aksies wat in sy manifest gelys word. Goedkeuringsreëls geld steeds."
},
"operations:approve": {
"title": "Bepaal wat goedkeuring benodig",
"consequence": "Kan aksies laat uitvoer sonder goedkeuring, of dit blokkeer, vir elke gebruiker."
},
"notify:hub": {
"title": "Ontvang elke waarskuwing",
"consequence": "Bewaar en lewer die waarskuwings wat alle plugins stuur, insluitend hul inhoud."
@@ -1238,6 +1251,10 @@
"title": "Bestuur gasheer as slegs-lees",
"consequence": "Gasheer wat dit bestuur, kan nie deur iemand anders redigeer, verwyder of hergedeel word nie."
},
"hosts:connect-policy": {
"title": "Bepaal wie kan verbind",
"consequence": "Kan elke SSH- en afstandsbureaubladverbinding na enige gasheer blokkeer of toelaat."
},
"desktop:window": {
"title": "Maak burebladvensters oop",
"consequence": "Slegs in die Termix-burebladtoepassing."
@@ -1663,6 +1680,12 @@
"title": "Kolomme",
"auto": "Outo",
"count": "{{count}} breed"
},
"pager": {
"previous": "Vorige",
"next": "Volgende",
"page": "Bladsy {{page}}",
"pageOf": "Bladsy {{page}} van {{pages}}"
}
},
"fields": {
@@ -1681,5 +1704,29 @@
"admin": "Admin",
"user": "Gebruiker"
}
},
"tileGrid": {
"empty": "Nog geen teëls nie. Voeg 'n paar by vanaf die lys.",
"move": "Sleep om te beweeg",
"moveTile": "Beweeg {{tile}}",
"settings": "{{tile}}-instellings",
"remove": "Verwyder {{tile}}",
"width": "Wydte",
"height": "Hoogte",
"addTiles": "Voeg teëls by",
"closePicker": "Maak toe",
"search": "Soek teëls",
"noMatches": "Niets pas nie.",
"kind": {
"all": "Almal",
"card": "Kaarte",
"chart": "Grafieke",
"signal": "Status",
"action": "Aksies",
"resource": "Lyste",
"session": "Sessies"
},
"reset": "Stel terug",
"done": "Klaar"
}
}
+47
View File
@@ -633,6 +633,7 @@
"hostClickBehavior": "النقر على جهاز",
"hostClickBehaviorDesc": "ماذا يحدث عند النقر على جهاز لديه علامة تبويب مفتوحة بالفعل. النقر مع Ctrl/Cmd، أو النقر الأوسط، أو \"فتح في علامة تبويب جديدة\" يبدأ دائمًا علامة تبويب جديدة.",
"hostClickNewTab": "فتح علامة تبويب جديدة دائمًا",
"hostClickHostPage": "فتح صفحة المضيف، إذا كان الإضافة توفر واحدة",
"hostClickFocusExisting": "التبديل إلى علامة التبويب المفتوحة",
"hostClickFocusExistingDoubleNew": "النقر للتبديل، والنقر المزدوج لعلامة تبويب جديدة",
"openInNewTab": "فتح في علامة تبويب جديدة",
@@ -1282,6 +1283,18 @@
"title": "إرسال التنبيهات",
"consequence": "إليك أو لمستخدمين آخرين، وعبر القنوات التي قاموا بإعدادها."
},
"operations:provide": {
"title": "تقديم إجراءات",
"consequence": "يمكن تشغيل إجراءاتها من الأتمتة، وسطر الأوامر، وتطبيق الهاتف، وأماكن أخرى، من قبل الأشخاص المسموح لهم باستخدامها."
},
"operations:invoke": {
"title": "تشغيل إجراءات الإضافات الأخرى",
"consequence": "يعمل نيابة عن المستخدم المسجل الدخول على الإجراءات المدرجة في ملف الوصف الخاص به. لا تزال قواعد الموافقة سارية."
},
"operations:approve": {
"title": "تحديد ما يحتاج إلى موافقة",
"consequence": "يمكن السماح بتشغيل الإجراءات دون موافقة، أو حظرها، لكل مستخدم."
},
"notify:hub": {
"title": "استقبال كل التنبيهات",
"consequence": "يخزن ويوصل التنبيهات التي ترسلها جميع الإضافات، بما في ذلك محتوياتها."
@@ -1306,6 +1319,10 @@
"title": "إدارة المضيفين كقراءة فقط",
"consequence": "لا يمكن لأي شخص آخر تعديل أو حذف أو إعادة مشاركة المضيفين التي يديرها."
},
"hosts:connect-policy": {
"title": "تحديد من يمكنه الاتصال",
"consequence": "يمكن حظر أو السماح بكل اتصال SSH وسطح المكتب البعيد لأي مضيف."
},
"desktop:window": {
"title": "فتح نوافذ سطح المكتب",
"consequence": "فقط في تطبيق سطح المكتب الخاص بـ Termix."
@@ -1831,6 +1848,12 @@
"title": "الأعمدة",
"auto": "تلقائي",
"count": "عرض {{count}}"
},
"pager": {
"previous": "السابق",
"next": "التالي",
"page": "الصفحة {{page}}",
"pageOf": "الصفحة {{page}} من {{pages}}"
}
},
"fields": {
@@ -1861,5 +1884,29 @@
"admin": "مدير",
"user": "مستخدم"
}
},
"tileGrid": {
"empty": "لا توجد بلاطات بعد. أضف بعضها من القائمة.",
"move": "اسحب للتحريك",
"moveTile": "تحريك {{tile}}",
"settings": "إعدادات {{tile}}",
"remove": "إزالة {{tile}}",
"width": "العرض",
"height": "الارتفاع",
"addTiles": "إضافة بلاطات",
"closePicker": "إغلاق",
"search": "البحث في البلاطات",
"noMatches": "لا توجد نتائج مطابقة.",
"kind": {
"all": "الكل",
"card": "بطاقات",
"chart": "رسوم بيانية",
"signal": "الحالة",
"action": "إجراءات",
"resource": "قوائم",
"session": "جلسات"
},
"reset": "إعادة تعيين",
"done": "تم"
}
}
+47
View File
@@ -609,6 +609,7 @@
"hostClickBehavior": "Кликване върху хост",
"hostClickBehaviorDesc": "Какво се случва, когато кликнете върху хост, който вече има отворен раздел. Ctrl/Cmd клик, клик със средния бутон или „Отвори в нов раздел“ винаги стартират нов.",
"hostClickNewTab": "Винаги отваряне на нов раздел",
"hostClickHostPage": "Отваря страницата на хоста, ако плъгинът предлага такава",
"hostClickFocusExisting": "Превключване към отворения раздел",
"hostClickFocusExistingDoubleNew": "Клик за превключване, двоен клик за нов раздел",
"openInNewTab": "Отвори в нов раздел",
@@ -1214,6 +1215,18 @@
"title": "Изпращане на известия",
"consequence": "До вас или други потребители, и чрез каналите, които са настроили."
},
"operations:provide": {
"title": "Предлагане на действия",
"consequence": "Действията му могат да се изпълняват от Автоматизации, командния ред, мобилното приложение и други места от потребители, които имат разрешение за това."
},
"operations:invoke": {
"title": "Изпълнение на действия от други плъгини",
"consequence": "Действа от името на влезлия потребител върху действията, изброени в манифеста му. Правилата за одобрение все още важат."
},
"operations:approve": {
"title": "Определяне на това, което изисква одобрение",
"consequence": "Може да позволи действията да се изпълняват без одобрение или да ги блокира за всеки потребител."
},
"notify:hub": {
"title": "Получаване на всяко известие",
"consequence": "Съхранява и доставя известията, изпратени от всички плъгини, включително съдържанието им."
@@ -1238,6 +1251,10 @@
"title": "Управление на хостове само за четене",
"consequence": "Хостовете, които управлява, не могат да бъдат редактирани, изтривани или споделяни отново от никого друг."
},
"hosts:connect-policy": {
"title": "Определяне на това, кой може да се свързва",
"consequence": "Може да блокира или позволява всяко SSH и дистанционно работно свързване към всеки хост."
},
"desktop:window": {
"title": "Отваряне на прозорци на работния плот",
"consequence": "Само в десктоп приложението на Termix."
@@ -1663,6 +1680,12 @@
"title": "Колони",
"auto": "Автоматично",
"count": "ширина {{count}}"
},
"pager": {
"previous": "Предишна",
"next": "Следваща",
"page": "Страница {{page}}",
"pageOf": "Страница {{page}} от {{pages}}"
}
},
"fields": {
@@ -1681,5 +1704,29 @@
"admin": "Администратор",
"user": "Потребител"
}
},
"tileGrid": {
"empty": "Все още няма плочки. Добавете някои от списъка.",
"move": "Плъзнете, за да преместите",
"moveTile": "Преместване на {{tile}}",
"settings": "Настройки на {{tile}}",
"remove": "Премахване на {{tile}}",
"width": "Ширина",
"height": "Височина",
"addTiles": "Добавяне на плочки",
"closePicker": "Затваряне",
"search": "Търсене на плочки",
"noMatches": "Няма съвпадения.",
"kind": {
"all": "Всички",
"card": "Карти",
"chart": "Графики",
"signal": "Статус",
"action": "Действия",
"resource": "Списъци",
"session": "Сесии"
},
"reset": "Нулиране",
"done": "Готово"
}
}
+47
View File
@@ -609,6 +609,7 @@
"hostClickBehavior": "হোস্টে ক্লিক করা",
"hostClickBehaviorDesc": "যখন আপনি একটি খোলা ট্যাব সহ হোস্টে ক্লিক করেন তখন কী ঘটে। Ctrl/Cmd ক্লিক, মাঝের ক্লিক, বা \"নতুন ট্যাবে খুলুন\" সবসময় একটি নতুন ট্যাব শুরু করে।",
"hostClickNewTab": "সবসময় একটি নতুন ট্যাব খুলুন",
"hostClickHostPage": "প্লাগিন একটি পেজ অফার করলে হোস্ট পেজ খুলুন",
"hostClickFocusExisting": "খোলা ট্যাবে সুইচ করুন",
"hostClickFocusExistingDoubleNew": "সুইচ করতে ক্লিক করুন, নতুন ট্যাবের জন্য ডাবল ক্লিক করুন",
"openInNewTab": "নতুন ট্যাবে খুলুন",
@@ -1214,6 +1215,18 @@
"title": "অ্যালার্ট পাঠান",
"consequence": "আপনাকে বা অন্য ব্যবহারকারীদের কাছে, এবং তারা যে চ্যানেল সেটআপ করেছে তার মাধ্যমে।"
},
"operations:provide": {
"title": "অ্যাকশন অফার করুন",
"consequence": "এর অ্যাকশনগুলো অটোমেশন, CLI, ফোন অ্যাপ এবং অন্য জায়গা থেকে, যেসব ব্যবহারকারী এগুলো ব্যবহারের অনুমতি পেয়েছে, তারা চালিয়ে নিতে পারবে।"
},
"operations:invoke": {
"title": "অন্যান্য প্লাগিনের অ্যাকশন চালান",
"consequence": "এটি ম্যানিফেস্টে তালিকাভুক্ত অ্যাকশনগুলোর জন্য সাইন-ইন করা ব্যবহারকারীর হয়ে কাজ করে। অনুমোদনের নিয়মগুলো এখনও প্রযোজ্য।"
},
"operations:approve": {
"title": "কিসের অনুমোদন প্রয়োজন তা সিদ্ধান্ত নিন",
"consequence": "প্রতিটি ব্যবহারকারীর জন্য অ্যাকশনগুলো অনুমোদন ছাড়াই চালানোর বা ব্লক করার অনুমতি দিতে পারে।"
},
"notify:hub": {
"title": "প্রতিটি অ্যালার্ট গ্রহণ করুন",
"consequence": "সংরক্ষণ করে এবং ডেলিভারি দেয় সব প্লাগইন যে অ্যালার্ট পাঠায়, তাদের বিষয়বস্তু সহ।"
@@ -1238,6 +1251,10 @@
"title": "হোস্টগুলোকে রিড-অনলি হিসেবে পরিচালনা করুন",
"consequence": "এটি পরিচালিত হোস্টগুলোকে অন্য কেউ সম্পাদনা, মুছে ফেলা বা পুনরায় শেয়ার করতে পারবে না।"
},
"hosts:connect-policy": {
"title": "কে সংযোগ স্থাপন করতে পারবে তা সিদ্ধান্ত নিন",
"consequence": "যেকোনো হোস্টের প্রতিটি SSH এবং রিমোট ডেস্কটপ সংযোগ ব্লক বা অনুমোদন করতে পারে।"
},
"desktop:window": {
"title": "ডেস্কটপ উইন্ডো খুলুন",
"consequence": "শুধুমাত্র Termix ডেস্কটপ অ্যাপে।"
@@ -1663,6 +1680,12 @@
"title": "কলাম",
"auto": "স্বয়ংক্রিয়",
"count": "{{count}} প্রশস্ত"
},
"pager": {
"previous": "আগের",
"next": "পরবর্তী",
"page": "পেজ {{page}}",
"pageOf": "{{pages}} এর মধ্যে পেজ {{page}}"
}
},
"fields": {
@@ -1681,5 +1704,29 @@
"admin": "অ্যাডমিন",
"user": "ইউজার"
}
},
"tileGrid": {
"empty": "এখনো কোনো টাইল নেই। তালিকা থেকে কিছু যোগ করুন।",
"move": "সরানোর জন্য ড্র্যাগ করুন",
"moveTile": "{{tile}} সরান",
"settings": "{{tile}} সেটিংস",
"remove": "{{tile}} সরান",
"width": "প্রস্থ",
"height": "উচ্চতা",
"addTiles": "টাইল যোগ করুন",
"closePicker": "বন্ধ করুন",
"search": "টাইল খুঁজুন",
"noMatches": "কিছু মিলছে না।",
"kind": {
"all": "সব",
"card": "কার্ড",
"chart": "চার্ট",
"signal": "স্ট্যাটাস",
"action": "অ্যাকশন",
"resource": "তালিকা",
"session": "সেশন"
},
"reset": "রিসেট",
"done": "সম্পন্ন"
}
}
+47
View File
@@ -615,6 +615,7 @@
"hostClickBehavior": "En fer clic en un host",
"hostClickBehaviorDesc": "Què passa quan fas clic en un host que ja té una pestanya oberta. Ctrl/Cmd + clic, clic central o «Obre en una pestanya nova» sempre en inicien una de nova.",
"hostClickNewTab": "Obre sempre una pestanya nova",
"hostClickHostPage": "Obre la pàgina de l'amfitrió, si un complement en proporciona una",
"hostClickFocusExisting": "Canvia a la pestanya oberta",
"hostClickFocusExistingDoubleNew": "Clic per canviar, doble clic per a una pestanya nova",
"openInNewTab": "Obre en una pestanya nova",
@@ -1231,6 +1232,18 @@
"title": "Enviar alertes",
"consequence": "A vosaltres o a altres usuaris, i a través dels canals que hagin configurat."
},
"operations:provide": {
"title": "Ofereix accions",
"consequence": "Les seves accions es poden executar des d'Automatitzacions, la CLI, l'aplicació mòbil i altres llocs, per part de les persones autoritzades a utilitzar-les."
},
"operations:invoke": {
"title": "Executa accions d'altres complements",
"consequence": "Actua en nom de l'usuari autenticat sobre les accions que llista el seu manifest. Les regles d'aprovació continuen sent d'aplicació."
},
"operations:approve": {
"title": "Decideix què necessita aprovació",
"consequence": "Pot permetre que les accions s'executin sense aprovació o bloquejar-les, per a cada usuari."
},
"notify:hub": {
"title": "Rebre totes les alertes",
"consequence": "Emmagatzema i distribueix les alertes que envien tots els plugins, inclosos els seus continguts."
@@ -1255,6 +1268,10 @@
"title": "Gestionar els amfitrions com a només lectura",
"consequence": "Els amfitrions que gestiona no es poden editar, suprimir ni tornar a compartir per ningú més."
},
"hosts:connect-policy": {
"title": "Decideix qui pot connectar-se",
"consequence": "Pot bloquejar o permetre totes les connexions SSH i d'escriptori remot a qualsevol amfitrió."
},
"desktop:window": {
"title": "Obrir finestres d'escriptori",
"consequence": "Només a l'aplicació d'escriptori de Termix."
@@ -1705,6 +1722,12 @@
"title": "Columnes",
"auto": "Automàtic",
"count": "{{count}} d'ample"
},
"pager": {
"previous": "Anterior",
"next": "Següent",
"page": "Pàgina {{page}}",
"pageOf": "Pàgina {{page}} de {{pages}}"
}
},
"fields": {
@@ -1726,5 +1749,29 @@
"admin": "Administrador",
"user": "Usuari"
}
},
"tileGrid": {
"empty": "Encara no hi ha cap mosaic. Afegeix-ne alguns de la llista.",
"move": "Arrossega per moure",
"moveTile": "Mou {{tile}}",
"settings": "Configuració de {{tile}}",
"remove": "Elimina {{tile}}",
"width": "Amplada",
"height": "Alçada",
"addTiles": "Afegeix mosaics",
"closePicker": "Tanca",
"search": "Cerca mosaics",
"noMatches": "Cap resultat coincideix.",
"kind": {
"all": "Tots",
"card": "Targetes",
"chart": "Gràfics",
"signal": "Estat",
"action": "Accions",
"resource": "Llistes",
"session": "Sessions"
},
"reset": "Restableix",
"done": "Fet"
}
}
+47
View File
@@ -621,6 +621,7 @@
"hostClickBehavior": "Kliknutí na hostitele",
"hostClickBehaviorDesc": "Co se stane, když kliknete na hostitele, který již má otevřenou kartu. Kliknutí s Ctrl/Cmd, prostředním tlačítkem nebo možnost „Otevřít v nové kartě“ vždy vytvoří novou kartu.",
"hostClickNewTab": "Vždy otevřít novou kartu",
"hostClickHostPage": "Otevřít stránku hostitele, pokud ji plugin nabízí",
"hostClickFocusExisting": "Přepnout na otevřenou kartu",
"hostClickFocusExistingDoubleNew": "Kliknutím přepnout, dvojklikem otevřít novou kartu",
"openInNewTab": "Otevřít v nové kartě",
@@ -1248,6 +1249,18 @@
"title": "Odesílat upozornění",
"consequence": "Vám nebo ostatním uživatelům a prostřednictvím kanálů, které nastavili."
},
"operations:provide": {
"title": "Nabízet akce",
"consequence": "Jeho akce mohou spouštět povolení uživatelé z automatizací, příkazového řádku, mobilní aplikace a dalších míst."
},
"operations:invoke": {
"title": "Spouštět akce jiných pluginů",
"consequence": "Koná se jménem přihlášeného uživatele u akcí uvedených v manifestu. Pravidla schvalování se nadále uplatňují."
},
"operations:approve": {
"title": "Rozhodovat, co vyžaduje schválení",
"consequence": "Může povolit spuštění akcí bez schválení nebo je blokovat pro každého uživatele."
},
"notify:hub": {
"title": "Přijímat všechna upozornění",
"consequence": "Ukládá a doručuje upozornění, která odesílají všechny pluginy, včetně jejich obsahu."
@@ -1272,6 +1285,10 @@
"title": "Spravovat hostitele jako pouze pro čtení",
"consequence": "Hostitele, které spravuje, nemůže nikdo jiný upravovat, mazat ani znovu sdílet."
},
"hosts:connect-policy": {
"title": "Rozhodovat, kdo se může připojit",
"consequence": "Může blokovat nebo povolit každé SSH a vzdálené plošné připojení k libovolnému hostiteli."
},
"desktop:window": {
"title": "Otevírat okna plochy",
"consequence": "Pouze v desktopové aplikaci Termix."
@@ -1747,6 +1764,12 @@
"title": "Sloupce",
"auto": "Automaticky",
"count": "šířka {{count}}"
},
"pager": {
"previous": "Předchozí",
"next": "Další",
"page": "Strana {{page}}",
"pageOf": "Strana {{page}} z {{pages}}"
}
},
"fields": {
@@ -1771,5 +1794,29 @@
"admin": "Administrátor",
"user": "Uživatel"
}
},
"tileGrid": {
"empty": "Zatím žádné dlaždice. Přidejte některé ze seznamu.",
"move": "Tažením přesunout",
"moveTile": "Přesunout {{tile}}",
"settings": "Nastavení {{tile}}",
"remove": "Odebrat {{tile}}",
"width": "Šířka",
"height": "Výška",
"addTiles": "Přidat dlaždice",
"closePicker": "Zavřít",
"search": "Hledat dlaždice",
"noMatches": "Žádné shody.",
"kind": {
"all": "Vše",
"card": "Karty",
"chart": "Grafy",
"signal": "Stav",
"action": "Akce",
"resource": "Seznamy",
"session": "Relace"
},
"reset": "Obnovit",
"done": "Hotovo"
}
}
+47
View File
@@ -609,6 +609,7 @@
"hostClickBehavior": "Klik på en vært",
"hostClickBehaviorDesc": "Hvad sker der, når du klikker på en vært, der allerede har et åbent fanen. Ctrl/Cmd-klik, mellemklik eller \"Åbn i ny fane\" starter altid en ny.",
"hostClickNewTab": "Åbn altid en ny fane",
"hostClickHostPage": "Åbn værtsiden, hvis et plugin tilbyder en",
"hostClickFocusExisting": "Skift til den åbne fane",
"hostClickFocusExistingDoubleNew": "Klik for at skifte, dobbeltklik for en ny fane",
"openInNewTab": "Åbn i ny fane",
@@ -1214,6 +1215,18 @@
"title": "Send advarsler",
"consequence": "Til dig eller andre brugere, og gennem de kanaler, de har opsat."
},
"operations:provide": {
"title": "Tilbyd handlinger",
"consequence": "Dets handlinger kan udføres fra automatiseringer, CLI, mobilappen og andre steder af personer, der har tilladelse til at bruge dem."
},
"operations:invoke": {
"title": "Udfør andre plugins' handlinger",
"consequence": "Handler på vegne af den loggede bruger for de handlinger, der er angivet i dets manifest. Godkendelsesregler gælder stadig."
},
"operations:approve": {
"title": "Bestem, hvad der kræver godkendelse",
"consequence": "Kan lade handlinger køre uden godkendelse eller blokere dem for alle brugere."
},
"notify:hub": {
"title": "Modtag alle advarsler",
"consequence": "Gemmer og leverer de advarsler, alle plugins sender, inklusive deres indhold."
@@ -1238,6 +1251,10 @@
"title": "Administrer værter som skrivebeskyttet",
"consequence": "Værter, den administrerer, kan ikke redigeres, slettes eller deles videre af andre."
},
"hosts:connect-policy": {
"title": "Bestem, hvem der kan oprette forbindelse",
"consequence": "Kan blokere eller tillade alle SSH- og fjernskrivebordsforbindelser til enhver vært."
},
"desktop:window": {
"title": "Åbn skrivebordsvinduer",
"consequence": "Kun i Termix-skivebordsappen."
@@ -1663,6 +1680,12 @@
"title": "Søjler",
"auto": "Automatisk",
"count": "{{count}} bred"
},
"pager": {
"previous": "Forrige",
"next": "Næste",
"page": "Side {{page}}",
"pageOf": "Side {{page}} af {{pages}}"
}
},
"fields": {
@@ -1681,5 +1704,29 @@
"admin": "Administrator",
"user": "Bruger"
}
},
"tileGrid": {
"empty": "Ingen fliser endnu. Tilføj nogle fra listen.",
"move": "Træk for at flytte",
"moveTile": "Flyt {{tile}}",
"settings": "{{tile}}-indstillinger",
"remove": "Fjern {{tile}}",
"width": "Bredde",
"height": "Højde",
"addTiles": "Tilføj fliser",
"closePicker": "Luk",
"search": "Søg i fliser",
"noMatches": "Intet matcher.",
"kind": {
"all": "Alle",
"card": "Kort",
"chart": "Diagrammer",
"signal": "Status",
"action": "Handlinger",
"resource": "Lister",
"session": "Sessioner"
},
"reset": "Nulstil",
"done": "Færdig"
}
}
+47
View File
@@ -609,6 +609,7 @@
"hostClickBehavior": "Klick auf einen Host",
"hostClickBehaviorDesc": "Was passiert, wenn du auf einen Host klickst, der bereits einen offenen Tab hat. Strg/Cmd-Klick, Mittelklick oder „In neuem Tab öffnen“ starten immer einen neuen Tab.",
"hostClickNewTab": "Immer einen neuen Tab öffnen",
"hostClickHostPage": "Host-Seite öffnen, wenn ein Plugin eine anbietet",
"hostClickFocusExisting": "Zum offenen Tab wechseln",
"hostClickFocusExistingDoubleNew": "Klick zum Wechseln, Doppelklick für neuen Tab",
"openInNewTab": "In neuem Tab öffnen",
@@ -1214,6 +1215,18 @@
"title": "Benachrichtigungen senden",
"consequence": "An Sie oder andere Benutzer und über die von ihnen eingerichteten Kanäle."
},
"operations:provide": {
"title": "Aktionen anbieten",
"consequence": "Seine Aktionen können von autorisierten Personen über Automatisierungen, die CLI, die Handy-App und andere Stellen ausgeführt werden."
},
"operations:invoke": {
"title": "Aktionen anderer Plugins ausführen",
"consequence": "Führt im Namen des angemeldeten Benutzers die in seinem Manifest aufgeführten Aktionen aus. Genehmigungsregeln gelten weiterhin."
},
"operations:approve": {
"title": "Entscheiden, was eine Genehmigung benötigt",
"consequence": "Kann Aktionen für jeden Benutzer ohne Genehmigung ausführen oder blockieren."
},
"notify:hub": {
"title": "Alle Benachrichtigungen empfangen",
"consequence": "Speichert und liefert die Benachrichtigungen aller Plugins aus, einschließlich deren Inhalt."
@@ -1238,6 +1251,10 @@
"title": "Hosts schreibgeschützt verwalten",
"consequence": "Die verwalteten Hosts können von niemandem anderem bearbeitet, gelöscht oder erneut geteilt werden."
},
"hosts:connect-policy": {
"title": "Entscheiden, wer sich verbinden darf",
"consequence": "Kann jede SSH- und Remotedesktopverbindung zu jedem Host blockieren oder zulassen."
},
"desktop:window": {
"title": "Desktop-Fenster öffnen",
"consequence": "Nur in der Termix-Desktop-App."
@@ -1663,6 +1680,12 @@
"title": "Spalten",
"auto": "Automatisch",
"count": "{{count}} Spalten breit"
},
"pager": {
"previous": "Zurück",
"next": "Weiter",
"page": "Seite {{page}}",
"pageOf": "Seite {{page}} von {{pages}}"
}
},
"fields": {
@@ -1681,5 +1704,29 @@
"admin": "Administrator",
"user": "Benutzer"
}
},
"tileGrid": {
"empty": "Noch keine Kacheln. Füge einige aus der Liste hinzu.",
"move": "Zum Verschieben ziehen",
"moveTile": "{{tile}} verschieben",
"settings": "{{tile}}-Einstellungen",
"remove": "{{tile}} entfernen",
"width": "Breite",
"height": "Höhe",
"addTiles": "Kacheln hinzufügen",
"closePicker": "Schließen",
"search": "Kacheln durchsuchen",
"noMatches": "Keine Übereinstimmungen.",
"kind": {
"all": "Alle",
"card": "Karten",
"chart": "Diagramme",
"signal": "Status",
"action": "Aktionen",
"resource": "Listen",
"session": "Sitzungen"
},
"reset": "Zurücksetzen",
"done": "Fertig"
}
}
+47
View File
@@ -609,6 +609,7 @@
"hostClickBehavior": "Κλικ σε host",
"hostClickBehaviorDesc": "Τι συμβαίνει όταν κάνετε κλικ σε έναν host που έχει ήδη ανοιχτή καρτέλα. Το Ctrl/Cmd κλικ, το κλικ με το μεσαίο πλήκτρο ή η επιλογή «Άνοιγμα σε νέα καρτέλα» ξεκινούν πάντα μια νέα.",
"hostClickNewTab": "Άνοιγμα πάντα νέας καρτέλας",
"hostClickHostPage": "Άνοιγμα της σελίδας του host, όταν ένα plugin προσφέρει μία",
"hostClickFocusExisting": "Μετάβαση στην ανοιχτή καρτέλα",
"hostClickFocusExistingDoubleNew": "Κλικ για μετάβαση, διπλό κλικ για νέα καρτέλα",
"openInNewTab": "Άνοιγμα σε νέα καρτέλα",
@@ -1214,6 +1215,18 @@
"title": "Αποστολή ειδοποιήσεων",
"consequence": "Σε εσάς ή άλλους χρήστες, και μέσω των καναλιών που έχουν ρυθμίσει."
},
"operations:provide": {
"title": "Προσφορά ενεργειών",
"consequence": "Οι ενέργειές του μπορούν να εκτελεστούν από Αυτοματισμούς, τη γραμμή εντολών (CLI), την εφαρμογή για κινητά και άλλες θέσεις, από χρήστες που έχουν άδεια χρήσης τους."
},
"operations:invoke": {
"title": "Εκτέλεση ενεργειών άλλων plugins",
"consequence": "Δρα για τον συνδεδεμένο χρήστη στις ενέργειες που αναφέρει το manifest του. Οι κανόνες έγκρισης εξακολουθούν να ισχύουν."
},
"operations:approve": {
"title": "Απόφαση για το τι χρειάζεται έγκριση",
"consequence": "Μπορεί να επιτρέπει την εκτέλεση ενεργειών χωρίς έγκριση ή να τις μπλοκάρει, για κάθε χρήστη."
},
"notify:hub": {
"title": "Λήψη κάθε ειδοποίησης",
"consequence": "Αποθηκεύει και παραδίδει τις ειδοποιήσεις που στέλνουν όλα τα plugins, συμπεριλαμβανομένου του περιεχομένου τους."
@@ -1238,6 +1251,10 @@
"title": "Διαχείριση hosts ως μόνο για ανάγνωση",
"consequence": "Οι hosts που διαχειρίζεται δεν μπορούν να επεξεργαστούν, να διαγραφούν ή να κοινοποιηθούν ξανά από κανέναν άλλον."
},
"hosts:connect-policy": {
"title": "Απόφαση για το ποιος μπορεί να συνδεθεί",
"consequence": "Μπορεί να μπλοκάρει ή να επιτρέπει κάθε σύνδεση SSH και απομακρυσμένης επιφάνειας εργασίας σε οποιοδήποτε host."
},
"desktop:window": {
"title": "Άνοιγμα παραθύρων επιφάνειας εργασίας",
"consequence": "Μόνο στην εφαρμογή επιφάνειας εργασίας Termix."
@@ -1663,6 +1680,12 @@
"title": "Στήλες",
"auto": "Αυτόματο",
"count": "Πλάτος {{count}}"
},
"pager": {
"previous": "Προηγούμενο",
"next": "Επόμενο",
"page": "Σελίδα {{page}}",
"pageOf": "Σελίδα {{page}} από {{pages}}"
}
},
"fields": {
@@ -1681,5 +1704,29 @@
"admin": "Διαχειριστής",
"user": "Χρήστης"
}
},
"tileGrid": {
"empty": "Δεν υπάρχουν ακόμη πλακάκια. Προσθέστε μερικά από τη λίστα.",
"move": "Σύρετε για μετακίνηση",
"moveTile": "Μετακίνηση του {{tile}}",
"settings": "Ρυθμίσεις του {{tile}}",
"remove": "Αφαίρεση του {{tile}}",
"width": "Πλάτος",
"height": "Ύψος",
"addTiles": "Προσθήκη πλακακιών",
"closePicker": "Κλείσιμο",
"search": "Αναζήτηση πλακακιών",
"noMatches": "Κανένα αποτέλεσμα.",
"kind": {
"all": "Όλα",
"card": "Κάρτες",
"chart": "Γράφημα",
"signal": "Κατάσταση",
"action": "Ενέργειες",
"resource": "Λίστες",
"session": "Συνεδρίες"
},
"reset": "Επαναφορά",
"done": "Τέλος"
}
}
+47
View File
@@ -615,6 +615,7 @@
"hostClickBehavior": "Al hacer clic en un host",
"hostClickBehaviorDesc": "Qué ocurre al hacer clic en un host que ya tiene una pestaña abierta. Ctrl/Cmd + clic, clic central o «Abrir en nueva pestaña» siempre inician una nueva.",
"hostClickNewTab": "Abrir siempre una nueva pestaña",
"hostClickHostPage": "Abre la página del host, si un complemento la ofrece",
"hostClickFocusExisting": "Cambiar a la pestaña abierta",
"hostClickFocusExistingDoubleNew": "Clic para cambiar, doble clic para nueva pestaña",
"openInNewTab": "Abrir en nueva pestaña",
@@ -1231,6 +1232,18 @@
"title": "Enviar alertas",
"consequence": "A ti u otros usuarios, y a través de los canales que hayan configurado."
},
"operations:provide": {
"title": "Ofrecer acciones",
"consequence": "Sus acciones se pueden ejecutar desde Automatizaciones, la CLI, la app móvil y otros lugares, por los usuarios autorizados para usarlas."
},
"operations:invoke": {
"title": "Ejecutar acciones de otros complementos",
"consequence": "Actúa en nombre del usuario conectado sobre las acciones que enumera su manifiesto. Las reglas de aprobación siguen aplicándose."
},
"operations:approve": {
"title": "Decidir qué requiere aprobación",
"consequence": "Puede permitir que las acciones se ejecuten sin aprobación o bloquearlas, para cada usuario."
},
"notify:hub": {
"title": "Recibir todas las alertas",
"consequence": "Almacena y entrega las alertas que envían todos los complementos, incluido su contenido."
@@ -1255,6 +1268,10 @@
"title": "Gestionar hosts como de solo lectura",
"consequence": "Los hosts que gestiona no pueden ser editados, eliminados ni compartidos por nadie más."
},
"hosts:connect-policy": {
"title": "Decidir quién puede conectarse",
"consequence": "Puede bloquear o permitir cualquier conexión SSH y de escritorio remoto a cualquier host."
},
"desktop:window": {
"title": "Abrir ventanas de escritorio",
"consequence": "Solo en la aplicación de escritorio de Termix."
@@ -1705,6 +1722,12 @@
"title": "Columnas",
"auto": "Automático",
"count": "{{count}} de ancho"
},
"pager": {
"previous": "Anterior",
"next": "Siguiente",
"page": "Página {{page}}",
"pageOf": "Página {{page}} de {{pages}}"
}
},
"fields": {
@@ -1726,5 +1749,29 @@
"admin": "Administrador",
"user": "Usuario"
}
},
"tileGrid": {
"empty": "Aún no hay widgets. Añade algunos desde la lista.",
"move": "Arrastra para mover",
"moveTile": "Mover {{tile}}",
"settings": "Configuración de {{tile}}",
"remove": "Eliminar {{tile}}",
"width": "Ancho",
"height": "Alto",
"addTiles": "Añadir widgets",
"closePicker": "Cerrar",
"search": "Buscar widgets",
"noMatches": "No hay coincidencias.",
"kind": {
"all": "Todos",
"card": "Tarjetas",
"chart": "Gráficos",
"signal": "Estado",
"action": "Acciones",
"resource": "Listas",
"session": "Sesiones"
},
"reset": "Restablecer",
"done": "Listo"
}
}
+47
View File
@@ -609,6 +609,7 @@
"hostClickBehavior": "Hosti klõpsamine",
"hostClickBehaviorDesc": "Mis juhtub, kui klõpsad hostil, millel on juba avatud vahekaart. Ctrl/Cmd klõps, kesklõpu klõps või „Ava uuel vahekaardil” alustavad alati uut.",
"hostClickNewTab": "Ava alati uus vahekaart",
"hostClickHostPage": "Ava peremehe leht, kui pistmikul on selline võimalus",
"hostClickFocusExisting": "Lülitu avatud vahekaardile",
"hostClickFocusExistingDoubleNew": "Klõpsa lülitumiseks, kahekordne klõps uue vahekaardi jaoks",
"openInNewTab": "Ava uuel vahekaardil",
@@ -1214,6 +1215,18 @@
"title": "Hoiatuste saatmine",
"consequence": "Sulle või teistele kasutajatele ning nende poolt seadistatud kanalite kaudu."
},
"operations:provide": {
"title": "Toimingute pakkumine",
"consequence": "Selle toiminguid saavad käivitada automatsioonid, käsurea liides, mobiilirakendus ja muud kohad, kui kasutajal on selleks õigus."
},
"operations:invoke": {
"title": "Teiste pistmike toimingute käivitamine",
"consequence": "Toimib sisseloginud kasutaja nimel oma manifestis loetletud toimingute puhul. Kinnituse reeglid kehtivad endiselt."
},
"operations:approve": {
"title": "Määra, mis vajab kinnitust",
"consequence": "Võib lubada toiminguid käivitada ilma kinnitust või blokeerida neid igale kasutajale."
},
"notify:hub": {
"title": "Kõigi hoiatuste vastuvõtmine",
"consequence": "Salvestab ja edastab kõikide pluginatelt saadetud hoiatused, sealhulgas nende sisu."
@@ -1238,6 +1251,10 @@
"title": "Hostide haldamine kirjutuskaitstult",
"consequence": "Ülejäänud kasutajad ei saa selle haldatavaid hoste muuta, kustutada ega uuesti jagada."
},
"hosts:connect-policy": {
"title": "Määra, kes saab ühendust luua",
"consequence": "Võib blokeerida või lubada igat SSH ja kauglaua ühendust mis tahes peremeheni."
},
"desktop:window": {
"title": "Töölaua akende avamine",
"consequence": "Ainult Termixi töölauarakenduses."
@@ -1663,6 +1680,12 @@
"title": "Veerud",
"auto": "Automaatne",
"count": "{{count}} veeru laiune"
},
"pager": {
"previous": "Eelmine",
"next": "Järgmine",
"page": "Lehekülg {{page}}",
"pageOf": "Lehekülg {{page}} / {{pages}}"
}
},
"fields": {
@@ -1681,5 +1704,29 @@
"admin": "Administraator",
"user": "Kasutaja"
}
},
"tileGrid": {
"empty": "Plaatseid pole veel. Lisa neid nimekirjast.",
"move": "Lohista, et liigutada",
"moveTile": "Liiguta {{tile}}",
"settings": "{{tile}} seaded",
"remove": "Eemalda {{tile}}",
"width": "Laius",
"height": "Kõrgus",
"addTiles": "Lisa plaate",
"closePicker": "Sulge",
"search": "Otsi plaate",
"noMatches": "Ühtegi sobivat tulemust ei leitud.",
"kind": {
"all": "Kõik",
"card": "Kaardid",
"chart": "Diagrammid",
"signal": "Olek",
"action": "Toimingud",
"resource": "Nimekirjad",
"session": "Seansid"
},
"reset": "Lähtesta",
"done": "Valmis"
}
}
+47
View File
@@ -609,6 +609,7 @@
"hostClickBehavior": "Isännän napsauttaminen",
"hostClickBehaviorDesc": "Mitä tapahtuu, kun napsautat isäntää, jolla on jo avoin välilehti. Ctrl/Cmd-napsautus, keskinapsautus tai \"Avaa uudella välilehdellä\" aloittavat aina uuden.",
"hostClickNewTab": "Avaa aina uusi välilehti",
"hostClickHostPage": "Avaa isäntäpalvelimen sivu, jos lisäosa tarjoaa sellaisen",
"hostClickFocusExisting": "Vaihda avoimeen välilehteen",
"hostClickFocusExistingDoubleNew": "Napsauta vaihtaaksesi, kaksoisnapsauta uuden välilehden avaamiseksi",
"openInNewTab": "Avaa uudella välilehdellä",
@@ -1214,6 +1215,18 @@
"title": "Lähettää hälytyksiä",
"consequence": "Sinulle tai muille käyttäjille ja heidän asettamiensa kanavien kautta."
},
"operations:provide": {
"title": "Tarjoa toimintoja",
"consequence": "Toimintoja voidaan suorittaa Automaatioissa, komentoriviltä, puhelinsovelluksesta ja muista paikoista käyttäjien toimesta, joilla on siihen oikeus."
},
"operations:invoke": {
"title": "Suorita muiden lisäosien toimintoja",
"consequence": "Toimii kirjautuneen käyttäjän puolesta sen manifestissa lueteltujen toimintojen osalta. Hyväksyntäsäännöt ovat edelleen voimassa."
},
"operations:approve": {
"title": "Päätä, mikä vaatii hyväksynnän",
"consequence": "Voittaa sallita toimintojen suorittaminen ilman hyväksyntää tai estää ne kaikille käyttäjille."
},
"notify:hub": {
"title": "Vastaanottaa kaikki hälytykset",
"consequence": "Tallentaa ja toimittaa kaikki lisäosien lähettämät hälytykset, mukaan lukien niiden sisällöt."
@@ -1238,6 +1251,10 @@
"title": "Hallinnoi isäntiä vain lukuoikeudella",
"consequence": "Sen hallinnoimia isäntiä ei voi muokata, poistaa tai jakaa uudelleen kenenkään toisen toimesta."
},
"hosts:connect-policy": {
"title": "Päätä, kenen on mahdollista muodostaa yhteys",
"consequence": "Voii estää tai sallia kaikki SSH- ja etätyöpöytäyhteydet mihin tahansa isäntäpalvelimeen."
},
"desktop:window": {
"title": "Avata työpöytäikkunoita",
"consequence": "Vain Termix-työpöytäsovelluksessa."
@@ -1663,6 +1680,12 @@
"title": "Sarakkeet",
"auto": "Automaattinen",
"count": "{{count}} saraketta leveä"
},
"pager": {
"previous": "Edellinen",
"next": "Seuraava",
"page": "Sivu {{page}}",
"pageOf": "Sivu {{page}} / {{pages}}"
}
},
"fields": {
@@ -1681,5 +1704,29 @@
"admin": "Ylläpitäjä",
"user": "Käyttäjä"
}
},
"tileGrid": {
"empty": "Ei vielä laattoja. Lisää niitä listasta.",
"move": "Vedä siirtääksesi",
"moveTile": "Siirrä {{tile}}",
"settings": "{{tile}}-asetukset",
"remove": "Poista {{tile}}",
"width": "Leveys",
"height": "Korkeus",
"addTiles": "Lisää laattoja",
"closePicker": "Sulje",
"search": "Hae laattoja",
"noMatches": "Ei osumia.",
"kind": {
"all": "Kaikki",
"card": "Kortit",
"chart": "Kaaviot",
"signal": "Tila",
"action": "Toiminnot",
"resource": "Listat",
"session": "Istunnot"
},
"reset": "Palauta",
"done": "Valmis"
}
}
+47
View File
@@ -615,6 +615,7 @@
"hostClickBehavior": "Cliquer sur un hôte",
"hostClickBehaviorDesc": "Que se passe-t-il lorsque vous cliquez sur un hôte qui possède déjà un onglet ouvert. Ctrl/Cmd + clic, clic du milieu ou « Ouvrir dans un nouvel onglet » ouvrent toujours un nouvel onglet.",
"hostClickNewTab": "Toujours ouvrir un nouvel onglet",
"hostClickHostPage": "Ouvrir la page de l'hôte, si un plugin en propose une",
"hostClickFocusExisting": "Basculer vers l'onglet ouvert",
"hostClickFocusExistingDoubleNew": "Clic pour basculer, double-clic pour un nouvel onglet",
"openInNewTab": "Ouvrir dans un nouvel onglet",
@@ -1231,6 +1232,18 @@
"title": "Envoyer des alertes",
"consequence": "À vous ou à d'autres utilisateurs, et via les canaux qu'ils ont configurés."
},
"operations:provide": {
"title": "Proposer des actions",
"consequence": "Ses actions peuvent être exécutées depuis les automatisations, l'interface en ligne de commande, l'application mobile et d'autres endroits, par les utilisateurs autorisés à les utiliser."
},
"operations:invoke": {
"title": "Exécuter les actions d'autres plugins",
"consequence": "Agit au nom de l'utilisateur connecté sur les actions listées dans son manifeste. Les règles d'approbation s'appliquent toujours."
},
"operations:approve": {
"title": "Décider ce qui nécessite une approbation",
"consequence": "Peut permettre aux actions de s'exécuter sans approbation, ou les bloquer, pour chaque utilisateur."
},
"notify:hub": {
"title": "Recevoir toutes les alertes",
"consequence": "Stocke et distribue les alertes envoyées par tous les plugins, y compris leur contenu."
@@ -1255,6 +1268,10 @@
"title": "Gérer les hôtes en lecture seule",
"consequence": "Les hôtes qu'il gère ne peuvent pas être modifiés, supprimés ou partagés à nouveau par quiconque."
},
"hosts:connect-policy": {
"title": "Décider qui peut se connecter",
"consequence": "Peut bloquer ou autoriser chaque connexion SSH et bureau à distance vers n'importe quel hôte."
},
"desktop:window": {
"title": "Ouvrir des fenêtres de bureau",
"consequence": "Uniquement dans l'application de bureau Termix."
@@ -1705,6 +1722,12 @@
"title": "Colonnes",
"auto": "Auto",
"count": "{{count}} de large"
},
"pager": {
"previous": "Précédent",
"next": "Suivant",
"page": "Page {{page}}",
"pageOf": "Page {{page}} sur {{pages}}"
}
},
"fields": {
@@ -1726,5 +1749,29 @@
"admin": "Administrateur",
"user": "Utilisateur"
}
},
"tileGrid": {
"empty": "Aucune tuile pour le moment. Ajoutez-en depuis la liste.",
"move": "Glisser pour déplacer",
"moveTile": "Déplacer {{tile}}",
"settings": "Paramètres de {{tile}}",
"remove": "Supprimer {{tile}}",
"width": "Largeur",
"height": "Hauteur",
"addTiles": "Ajouter des tuiles",
"closePicker": "Fermer",
"search": "Rechercher des tuiles",
"noMatches": "Aucun résultat.",
"kind": {
"all": "Tout",
"card": "Cartes",
"chart": "Graphiques",
"signal": "Statut",
"action": "Actions",
"resource": "Listes",
"session": "Sessions"
},
"reset": "Réinitialiser",
"done": "Terminé"
}
}
+47
View File
@@ -615,6 +615,7 @@
"hostClickBehavior": "לחיצה על מארח",
"hostClickBehaviorDesc": "מה קורה כשלוחצים על מארח שכבר יש לו לשונית פתוחה. לחיצה עם Ctrl/Cmd, לחיצה באמצע, או \"פתח בלשונית חדשה\" תמיד יפתחו לשונית חדשה.",
"hostClickNewTab": "תמיד פתח לשונית חדשה",
"hostClickHostPage": "פתח את דף המארח, כאשר תוסף מציע אחד כזה",
"hostClickFocusExisting": "עבור ללשונית הפתוחה",
"hostClickFocusExistingDoubleNew": "לחיצה לעבור, לחיצה כפולה ללשונית חדשה",
"openInNewTab": "פתח בלשונית חדשה",
@@ -1231,6 +1232,18 @@
"title": "שליחת התראות",
"consequence": "אליך או למשתמשים אחרים, דרך הערוצים שהם הגדירו."
},
"operations:provide": {
"title": "הצעת פעולות",
"consequence": "הפעולות שלו ניתנות להפעלה מאוטומציות, שורת הפקודה, אפליקציית הטלפון ומקומות אחרים, על ידי אנשים שמונו להשתמש בהן."
},
"operations:invoke": {
"title": "הפעלת פעולות של תוספים אחרים",
"consequence": "פועל בשם המשתמש המחובר על הפעולות המופיעות במניפסט שלו. כללי אישור עדיין חלים."
},
"operations:approve": {
"title": "החלטה על מה דורש אישור",
"consequence": "יכול לאפשר לפעולות להיבצע ללא אישור, או לחסום אותן, עבור כל משתמש."
},
"notify:hub": {
"title": "קבלת כל ההתראות",
"consequence": "שומר ומעביר את כל ההתראות שהתוספים שולחים, כולל התוכן שלהן."
@@ -1255,6 +1268,10 @@
"title": "ניהול מארחים כקריאה בלבד",
"consequence": "המארחים שבהם הוא מנהל לא יכולים לערוך, למחוק או לשתף מחדש על ידי אף אחד אחר."
},
"hosts:connect-policy": {
"title": "החלטה על מי יכול להתחבר",
"consequence": "יכול לחסום או לאשר כל חיבור SSH ודסקטופ מרוחק לכל מארח."
},
"desktop:window": {
"title": "פתיחת חלונות שולחן עבודה",
"consequence": "רק באפליקציית שולחן העבודה של Termix."
@@ -1705,6 +1722,12 @@
"title": "עמודות",
"auto": "אוטומטי",
"count": "רוחב של {{count}}"
},
"pager": {
"previous": "קודם",
"next": "הבא",
"page": "עמוד {{page}}",
"pageOf": "עמוד {{page}} מתוך {{pages}}"
}
},
"fields": {
@@ -1726,5 +1749,29 @@
"admin": "מנהל",
"user": "משתמש"
}
},
"tileGrid": {
"empty": "אין אריחים עדיין. הוסף כמה מהרשימה.",
"move": "גרור להזזה",
"moveTile": "הזז את {{tile}}",
"settings": "הגדרות של {{tile}}",
"remove": "הסר את {{tile}}",
"width": "רוחב",
"height": "גובה",
"addTiles": "הוסף אריחים",
"closePicker": "סגור",
"search": "חפש אריחים",
"noMatches": "אין תוצאות תואמות.",
"kind": {
"all": "הכל",
"card": "כרטיסיות",
"chart": "תרשימים",
"signal": "סטטוס",
"action": "פעולות",
"resource": "רשימות",
"session": "סשנים"
},
"reset": "אפס",
"done": "בוצע"
}
}
+47
View File
@@ -609,6 +609,7 @@
"hostClickBehavior": "होस्ट पर क्लिक करना",
"hostClickBehaviorDesc": "जब आप पहले से खुले टैब वाले होस्ट पर क्लिक करते हैं तो क्या होता है। Ctrl/Cmd क्लिक, मध्य क्लिक, या \"नए टैब में खोलें\" हमेशा एक नया टैब शुरू करते हैं।",
"hostClickNewTab": "हमेशा नया टैब खोलें",
"hostClickHostPage": "जब कोई प्लगइन होस्ट पेज प्रदान करता है, तो उसे खोलें",
"hostClickFocusExisting": "खुले टैब पर जाएं",
"hostClickFocusExistingDoubleNew": "स्विच करने के लिए क्लिक करें, नए टैब के लिए डबल क्लिक करें",
"openInNewTab": "नए टैब में खोलें",
@@ -1214,6 +1215,18 @@
"title": "अलर्ट भेजें",
"consequence": "आपको या अन्य उपयोगकर्ताओं को, और उन चैनलों के माध्यम से जो वे सेट करते हैं।"
},
"operations:provide": {
"title": "कार्रवाइयाँ प्रदान करें",
"consequence": "इसकी कार्रवाइयाँ ऑटोमेशन, CLI, फ़ोन ऐप और अन्य स्थानों से उन लोगों द्वारा चलाई जा सकती हैं जिन्हें उनका उपयोग करने की अनुमति है।"
},
"operations:invoke": {
"title": "अन्य प्लगइन्स की कार्रवाइयाँ चलाएँ",
"consequence": "अपने मैनिफ़ेस्ट में सूचीबद्ध कार्रवाइयों पर साइन-इन उपयोगकर्ता की ओर से कार्य करता है। अनुमोदन नियम अभी भी लागू होते हैं।"
},
"operations:approve": {
"title": "तय करें कि किसके लिए अनुमोदन की आवश्यकता है",
"consequence": "हर उपयोगकर्ता के लिए कार्रवाइयों को बिना अनुमोदन के चलने दे सकता है या उन्हें ब्लॉक कर सकता है।"
},
"notify:hub": {
"title": "हर अलर्ट प्राप्त करें",
"consequence": "सभी प्लगइन द्वारा भेजे गए अलर्ट को स्टोर और डिलीवर करता है, जिसमें उनका सामग्री भी शामिल है।"
@@ -1238,6 +1251,10 @@
"title": "होस्ट्स को केवल-पठनीय (read-only) के रूप में प्रबंधित करें",
"consequence": "इसके द्वारा प्रबंधित होस्ट्स को कोई अन्य व्यक्ति संपादित, हटा या पुनः साझा नहीं कर सकता।"
},
"hosts:connect-policy": {
"title": "तय करें कि कौन कनेक्ट कर सकता है",
"consequence": "किसी भी होस्ट से हर SSH और रिमोट डेस्कटॉप कनेक्शन को ब्लॉक या अनुमति दे सकता है।"
},
"desktop:window": {
"title": "डेस्कटॉप विंडो खोलें",
"consequence": "केवल Termix डेस्कटॉप ऐप में।"
@@ -1663,6 +1680,12 @@
"title": "कॉलम",
"auto": "स्वचालित",
"count": "{{count}} चौड़ा"
},
"pager": {
"previous": "पिछला",
"next": "अगला",
"page": "पेज {{page}}",
"pageOf": "{{pages}} में से पेज {{page}}"
}
},
"fields": {
@@ -1681,5 +1704,29 @@
"admin": "एडमिन",
"user": "उपयोगकर्ता"
}
},
"tileGrid": {
"empty": "अभी तक कोई टाइल नहीं। सूची से कुछ जोड़ें।",
"move": "स्थानांतरित करने के लिए खींचें",
"moveTile": "{{tile}} को स्थानांतरित करें",
"settings": "{{tile}} सेटिंग्स",
"remove": "{{tile}} को हटाएँ",
"width": "चौड़ाई",
"height": "ऊँचाई",
"addTiles": "टाइलें जोड़ें",
"closePicker": "बंद करें",
"search": "टाइलें खोजें",
"noMatches": "कुछ भी मेल नहीं खाता।",
"kind": {
"all": "सभी",
"card": "कार्ड",
"chart": "चार्ट",
"signal": "स्थिति",
"action": "कार्रवाइयाँ",
"resource": "सूचियाँ",
"session": "सत्र"
},
"reset": "रीसेट करें",
"done": "पूर्ण"
}
}
+47
View File
@@ -615,6 +615,7 @@
"hostClickBehavior": "Klik na host",
"hostClickBehaviorDesc": "Što se događa kada kliknete na host koji već ima otvoren tab. Ctrl/Cmd klik, srednji klik ili „Otvori u novom tabu“ uvijek otvaraju novi tab.",
"hostClickNewTab": "Uvijek otvori novi tab",
"hostClickHostPage": "Otvori stranicu hosta, ako je plugin nudi",
"hostClickFocusExisting": "Prebaci se na otvoreni tab",
"hostClickFocusExistingDoubleNew": "Klik za prebacivanje, dvostruki klik za novi tab",
"openInNewTab": "Otvori u novom tabu",
@@ -1231,6 +1232,18 @@
"title": "Slanje obavijesti",
"consequence": "Vama ili drugim korisnicima, putem kanala koje su oni postavili."
},
"operations:provide": {
"title": "Nudi radnje",
"consequence": "Njegove radnje mogu se pokretati iz Automatizacija, CLI sučelja, mobilne aplikacije i drugih mjesta, od strane korisnika koji imaju dopuštenje za njihovo korištenje."
},
"operations:invoke": {
"title": "Pokreće radnje drugih pluginova",
"consequence": "Djeluje u ime prijavljenog korisnika na radnje navedene u njegovom manifestu. Pravila odobrenja i dalje vrijede."
},
"operations:approve": {
"title": "Određuje što zahtijeva odobrenje",
"consequence": "Može dopustiti radnjama da se pokrenu bez odobrenja ili ih blokirati za svakog korisnika."
},
"notify:hub": {
"title": "Primanje svih obavijesti",
"consequence": "Sprema i dostavlja sve obavijesti koje šalju dodaci, uključujući njihov sadržaj."
@@ -1255,6 +1268,10 @@
"title": "Upravljanje domaćinima kao samo za čitanje",
"consequence": "Domaćine koje upravlja ne mogu uređivati, brisati niti dijeliti drugi korisnici."
},
"hosts:connect-policy": {
"title": "Određuje tko se može povezati",
"consequence": "Može blokirati ili dopustiti svaku SSH i daljinsku desktop vezu s bilo kojim hostom."
},
"desktop:window": {
"title": "Otvaranje prozora radne površine",
"consequence": "Samo u Termix aplikaciji za radnu površinu."
@@ -1705,6 +1722,12 @@
"title": "Stupci",
"auto": "Automatski",
"count": "Širina: {{count}}"
},
"pager": {
"previous": "Prethodna",
"next": "Sljedeća",
"page": "Stranica {{page}}",
"pageOf": "Stranica {{page}} od {{pages}}"
}
},
"fields": {
@@ -1726,5 +1749,29 @@
"admin": "Administrator",
"user": "Korisnik"
}
},
"tileGrid": {
"empty": "Još nema pločica. Dodajte ih s popisa.",
"move": "Povucite za pomicanje",
"moveTile": "Premjesti {{tile}}",
"settings": "Postavke za {{tile}}",
"remove": "Ukloni {{tile}}",
"width": "Širina",
"height": "Visina",
"addTiles": "Dodaj pločice",
"closePicker": "Zatvori",
"search": "Pretraži pločice",
"noMatches": "Nema rezultata.",
"kind": {
"all": "Sve",
"card": "Kartice",
"chart": "Grafikon",
"signal": "Status",
"action": "Radnje",
"resource": "Popisi",
"session": "Sesije"
},
"reset": "Poništi",
"done": "Gotovo"
}
}
+47
View File
@@ -609,6 +609,7 @@
"hostClickBehavior": "Hostra kattintás",
"hostClickBehaviorDesc": "Mi történjen, ha egy már megnyitott fület tartalmazó hostra kattint. A Ctrl/Cmd+kattintás, középső kattintás vagy az \"Új fülben megnyitás\" mindig újat indít.",
"hostClickNewTab": "Mindig új fülben megnyitás",
"hostClickHostPage": "Megnyitja a host oldalát, ha egy bővítmény biztosít ilyet",
"hostClickFocusExisting": "Váltás a megnyitott fülre",
"hostClickFocusExistingDoubleNew": "Kattintás a váltáshoz, dupla kattintás új fülhöz",
"openInNewTab": "Új fülben megnyitás",
@@ -1214,6 +1215,18 @@
"title": "Riasztások küldése",
"consequence": "Neked vagy más felhasználóknak, és az általuk beállított csatornákon keresztül."
},
"operations:provide": {
"title": "Műveletek biztosítása",
"consequence": "A műveleteit az automatizálásokból, a CLI-ből, a mobilalkalmazásból és más helyekről futtathatják azok, akiknek engedélyük van a használatukra."
},
"operations:invoke": {
"title": "Más bővítmények műveleteinek futtatása",
"consequence": "A bejelentkezett felhasználó nevében hajtja végre a manifestjében felsorolt műveleteket. A jóváhagyási szabályok továbbra is érvényesek."
},
"operations:approve": {
"title": "Jóváhagyási követelmények meghatározása",
"consequence": "Engedélyezheti, hogy a műveletek jóváhagyás nélkül fussanak, vagy blokkolhatja azokat minden felhasználó számára."
},
"notify:hub": {
"title": "Minden riasztás fogadása",
"consequence": "Tárolja és kézbesíti az összes plugin által küldött riasztást, beleértve azok tartalmát is."
@@ -1238,6 +1251,10 @@
"title": "Hostok kezelése csak olvashatóként",
"consequence": "A kezelt hostokat senki más nem szerkesztheti, törölheti vagy újra megoszthatja."
},
"hosts:connect-policy": {
"title": "Csatlakozási jogosultságok meghatározása",
"consequence": "Blokkolhatja vagy engedélyezheti minden SSH és távoli asztali kapcsolatot bármely host felé."
},
"desktop:window": {
"title": "Asztali ablakok megnyitása",
"consequence": "Csak a Termix asztali alkalmazásban."
@@ -1663,6 +1680,12 @@
"title": "Oszlopok",
"auto": "Automatikus",
"count": "{{count}} oszlop széles"
},
"pager": {
"previous": "Előző",
"next": "Következő",
"page": "{{page}}. oldal",
"pageOf": "{{page}}. oldal a {{pages}}-ból"
}
},
"fields": {
@@ -1681,5 +1704,29 @@
"admin": "Adminisztrátor",
"user": "Felhasználó"
}
},
"tileGrid": {
"empty": "Még nincsenek csempék. Adj hozzá néhányat a listából.",
"move": "Húzd a mozgatáshoz",
"moveTile": "{{tile}} mozgatása",
"settings": "{{tile}} beállításai",
"remove": "{{tile}} eltávolítása",
"width": "Szélesség",
"height": "Magasság",
"addTiles": "Csempék hozzáadása",
"closePicker": "Bezárás",
"search": "Csempék keresése",
"noMatches": "Nincs találat.",
"kind": {
"all": "Összes",
"card": "Kártyák",
"chart": "Diagramok",
"signal": "Státusz",
"action": "Műveletek",
"resource": "Listák",
"session": "Munkamenetek"
},
"reset": "Visszaállítás",
"done": "Kész"
}
}
+47
View File
@@ -603,6 +603,7 @@
"hostClickBehavior": "Mengklik host",
"hostClickBehaviorDesc": "Apa yang terjadi saat Anda mengklik host yang sudah memiliki tab terbuka. Klik Ctrl/Cmd, klik tengah, atau \"Buka di tab baru\" selalu memulai yang baru.",
"hostClickNewTab": "Selalu buka tab baru",
"hostClickHostPage": "Buka halaman host, jika plugin menyediakan",
"hostClickFocusExisting": "Beralih ke tab yang terbuka",
"hostClickFocusExistingDoubleNew": "Klik untuk beralih, klik dua kali untuk tab baru",
"openInNewTab": "Buka di tab baru",
@@ -1197,6 +1198,18 @@
"title": "Mengirim peringatan",
"consequence": "Ke Anda atau pengguna lain, dan melalui saluran yang mereka atur."
},
"operations:provide": {
"title": "Menawarkan tindakan",
"consequence": "Tindakannya dapat dijalankan dari Otomasi, CLI, aplikasi ponsel, dan tempat lain, oleh pengguna yang diizinkan."
},
"operations:invoke": {
"title": "Jalankan tindakan plugin lain",
"consequence": "Bertindak atas nama pengguna yang masuk untuk tindakan yang tercantum dalam manifestnya. Aturan persetujuan tetap berlaku."
},
"operations:approve": {
"title": "Tentukan apa yang memerlukan persetujuan",
"consequence": "Dapat mengizinkan tindakan berjalan tanpa persetujuan, atau memblokirnya, untuk setiap pengguna."
},
"notify:hub": {
"title": "Menerima setiap peringatan",
"consequence": "Menyimpan dan mengirimkan semua peringatan yang dikirim plugin, termasuk isinya."
@@ -1221,6 +1234,10 @@
"title": "Kelola host sebagai baca saja",
"consequence": "Host yang dikelolanya tidak dapat diedit, dihapus, atau dibagikan ulang oleh siapa pun."
},
"hosts:connect-policy": {
"title": "Tentukan siapa yang dapat terhubung",
"consequence": "Dapat memblokir atau mengizinkan setiap koneksi SSH dan desktop jarak jauh ke host apa pun."
},
"desktop:window": {
"title": "Membuka jendela desktop",
"consequence": "Hanya di aplikasi desktop Termix."
@@ -1621,6 +1638,12 @@
"title": "Kolom",
"auto": "Otomatis",
"count": "Lebar {{count}}"
},
"pager": {
"previous": "Sebelumnya",
"next": "Selanjutnya",
"page": "Halaman {{page}}",
"pageOf": "Halaman {{page}} dari {{pages}}"
}
},
"fields": {
@@ -1636,5 +1659,29 @@
"admin": "Admin",
"user": "Pengguna"
}
},
"tileGrid": {
"empty": "Belum ada ubin. Tambahkan beberapa dari daftar.",
"move": "Seret untuk memindahkan",
"moveTile": "Pindahkan {{tile}}",
"settings": "Pengaturan {{tile}}",
"remove": "Hapus {{tile}}",
"width": "Lebar",
"height": "Tinggi",
"addTiles": "Tambahkan ubin",
"closePicker": "Tutup",
"search": "Cari ubin",
"noMatches": "Tidak ada yang cocok.",
"kind": {
"all": "Semua",
"card": "Kartu",
"chart": "Bagan",
"signal": "Status",
"action": "Tindakan",
"resource": "Daftar",
"session": "Sesi"
},
"reset": "Setel ulang",
"done": "Selesai"
}
}
+47
View File
@@ -615,6 +615,7 @@
"hostClickBehavior": "Cliccando su un host",
"hostClickBehaviorDesc": "Cosa succede quando clicchi su un host che ha già una scheda aperta. Ctrl/Cmd + clic, clic centrale o \"Apri in nuova scheda\" avviano sempre una nuova scheda.",
"hostClickNewTab": "Apri sempre una nuova scheda",
"hostClickHostPage": "Apri la pagina dell'host, se un plugin ne offre una",
"hostClickFocusExisting": "Passa alla scheda aperta",
"hostClickFocusExistingDoubleNew": "Clic per passare, doppio clic per una nuova scheda",
"openInNewTab": "Apri in nuova scheda",
@@ -1231,6 +1232,18 @@
"title": "Invia avvisi",
"consequence": "A te o ad altri utenti, e attraverso i canali da loro configurati."
},
"operations:provide": {
"title": "Offri azioni",
"consequence": "Le sue azioni possono essere eseguite da Automazioni, dalla CLI, dall'app mobile e da altri luoghi, da parte degli utenti autorizzati."
},
"operations:invoke": {
"title": "Esegui azioni di altri plugin",
"consequence": "Agisce per l'utente autenticato sulle azioni elencate nel suo manifesto. Le regole di approvazione rimangono valide."
},
"operations:approve": {
"title": "Decidi cosa richiede approvazione",
"consequence": "Può consentire l'esecuzione delle azioni senza approvazione o bloccarle, per ogni utente."
},
"notify:hub": {
"title": "Ricevi ogni avviso",
"consequence": "Memorizza e distribuisce gli avvisi inviati da tutti i plugin, inclusi i loro contenuti."
@@ -1255,6 +1268,10 @@
"title": "Gestisci gli host in sola lettura",
"consequence": "Gli host gestiti non possono essere modificati, eliminati o condivisi nuovamente da nessun altro."
},
"hosts:connect-policy": {
"title": "Decidi chi può connettersi",
"consequence": "Può bloccare o consentire ogni connessione SSH e desktop remoto a qualsiasi host."
},
"desktop:window": {
"title": "Apri finestre desktop",
"consequence": "Solo nell'app desktop di Termix."
@@ -1705,6 +1722,12 @@
"title": "Colonne",
"auto": "Automatico",
"count": "{{count}} colonne"
},
"pager": {
"previous": "Precedente",
"next": "Successivo",
"page": "Pagina {{page}}",
"pageOf": "Pagina {{page}} di {{pages}}"
}
},
"fields": {
@@ -1726,5 +1749,29 @@
"admin": "Amministratore",
"user": "Utente"
}
},
"tileGrid": {
"empty": "Nessun tile ancora. Aggiungine alcuni dall'elenco.",
"move": "Trascina per spostare",
"moveTile": "Sposta {{tile}}",
"settings": "Impostazioni di {{tile}}",
"remove": "Rimuovi {{tile}}",
"width": "Larghezza",
"height": "Altezza",
"addTiles": "Aggiungi tile",
"closePicker": "Chiudi",
"search": "Cerca tile",
"noMatches": "Nessun risultato.",
"kind": {
"all": "Tutti",
"card": "Schede",
"chart": "Grafici",
"signal": "Stato",
"action": "Azioni",
"resource": "Elenchi",
"session": "Sessioni"
},
"reset": "Reimposta",
"done": "Fatto"
}
}
+47
View File
@@ -603,6 +603,7 @@
"hostClickBehavior": "ホストをクリックしたとき",
"hostClickBehaviorDesc": "既にタブが開いているホストをクリックしたときの動作を指定します。Ctrl/Cmd + クリック、中央クリック、または「新しいタブで開く」は常に新しいタブを開きます。",
"hostClickNewTab": "常に新しいタブで開く",
"hostClickHostPage": "プラグインが提供している場合、ホストページを開きます",
"hostClickFocusExisting": "開いているタブに切り替える",
"hostClickFocusExistingDoubleNew": "クリックで切り替え、ダブルクリックで新しいタブ",
"openInNewTab": "新しいタブで開く",
@@ -1197,6 +1198,18 @@
"title": "アラートの送信",
"consequence": "あなたや他のユーザーへ、設定されたチャネルを通じて送信します。"
},
"operations:provide": {
"title": "アクションを提供する",
"consequence": "そのアクションは、自動化、CLI、モバイルアプリ、その他の場所から、使用が許可されたユーザーによって実行できます。"
},
"operations:invoke": {
"title": "他のプラグインのアクションを実行する",
"consequence": "マニフェストにリストされたアクションに対して、サインインしているユーザーに代わって動作します。承認ルールは依然として適用されます。"
},
"operations:approve": {
"title": "承認が必要なものを決定する",
"consequence": "すべてのユーザーに対して、アクションを承認なしで実行できるようにするか、ブロックするかを制御できます。"
},
"notify:hub": {
"title": "すべてのアラートを受信",
"consequence": "すべてのプラグインが送信するアラート(内容を含む)を保存・配信します。"
@@ -1221,6 +1234,10 @@
"title": "ホストを読み取り専用として管理",
"consequence": "管理対象のホストは、他のユーザーによって編集、削除、再共有できません。"
},
"hosts:connect-policy": {
"title": "接続を許可するユーザーを決定する",
"consequence": "任意のホストへのすべてのSSHおよびリモートデスクトップ接続をブロックまたは許可できます。"
},
"desktop:window": {
"title": "デスクトップウィンドウを開く",
"consequence": "Termix デスクトップアプリでのみ可能です。"
@@ -1621,6 +1638,12 @@
"title": "カラム",
"auto": "自動",
"count": "{{count}} カラム"
},
"pager": {
"previous": "前へ",
"next": "次へ",
"page": "{{page}}ページ目",
"pageOf": "{{pages}}ページ中{{page}}ページ目"
}
},
"fields": {
@@ -1636,5 +1659,29 @@
"admin": "管理者",
"user": "ユーザー"
}
},
"tileGrid": {
"empty": "まだタイルがありません。リストからいくつか追加してください。",
"move": "ドラッグして移動",
"moveTile": "{{tile}}を移動",
"settings": "{{tile}}の設定",
"remove": "{{tile}}を削除",
"width": "幅",
"height": "高さ",
"addTiles": "タイルを追加",
"closePicker": "閉じる",
"search": "タイルを検索",
"noMatches": "一致するものはありません。",
"kind": {
"all": "すべて",
"card": "カード",
"chart": "チャート",
"signal": "ステータス",
"action": "アクション",
"resource": "リスト",
"session": "セッション"
},
"reset": "リセット",
"done": "完了"
}
}
+47
View File
@@ -603,6 +603,7 @@
"hostClickBehavior": "호스트 클릭 시 동작",
"hostClickBehaviorDesc": "이미 열린 탭이 있는 호스트를 클릭했을 때의 동작을 설정합니다. Ctrl/Cmd 클릭, 마우스 가운데 클릭 또는 \"새 탭에서 열기\"는 항상 새 탭을 엽니다.",
"hostClickNewTab": "항상 새 탭에서 열기",
"hostClickHostPage": "플러그인이 제공하면 호스트 페이지 열기",
"hostClickFocusExisting": "열린 탭으로 전환",
"hostClickFocusExistingDoubleNew": "클릭 시 전환, 더블 클릭 시 새 탭",
"openInNewTab": "새 탭에서 열기",
@@ -1197,6 +1198,18 @@
"title": "알림 전송",
"consequence": "사용자나 다른 사용자에게 설정된 채널을 통해 알림을 보냅니다."
},
"operations:provide": {
"title": "작업 제공",
"consequence": "사용이 허용된 사용자는 자동화, CLI, 모바일 앱 및 기타 위치에서 이 작업을 실행할 수 있습니다."
},
"operations:invoke": {
"title": "다른 플러그인의 작업 실행",
"consequence": "매니페스트에 나열된 작업에 대해 로그인한 사용자를 대신하여 행동합니다. 승인 규칙은 여전히 적용됩니다."
},
"operations:approve": {
"title": "승인 필요 여부 결정",
"consequence": "모든 사용자에게 대해 작업을 승인 없이 실행하거나 차단할 수 있습니다."
},
"notify:hub": {
"title": "모든 알림 수신",
"consequence": "모든 플러그인이 보내는 알림(내용 포함)을 저장하고 전달합니다."
@@ -1221,6 +1234,10 @@
"title": "호스트를 읽기 전용으로 관리",
"consequence": "관리하는 호스트는 다른 사용자가 수정, 삭제 또는 재공유할 수 없습니다."
},
"hosts:connect-policy": {
"title": "연결 권한 결정",
"consequence": "모든 호스트에 대한 SSH 및 원격 데스크톱 연결을 차단하거나 허용할 수 있습니다."
},
"desktop:window": {
"title": "데스크톱 창 열기",
"consequence": "Termix 데스크톱 앱에서만 가능합니다."
@@ -1621,6 +1638,12 @@
"title": "열",
"auto": "자동",
"count": "{{count}}열 너비"
},
"pager": {
"previous": "이전",
"next": "다음",
"page": "{{page}}페이지",
"pageOf": "{{pages}} 중 {{page}}페이지"
}
},
"fields": {
@@ -1636,5 +1659,29 @@
"admin": "관리자",
"user": "사용자"
}
},
"tileGrid": {
"empty": "타일이 없습니다. 목록에서 추가하세요.",
"move": "드래그하여 이동",
"moveTile": "{{tile}} 이동",
"settings": "{{tile}} 설정",
"remove": "{{tile}} 제거",
"width": "너비",
"height": "높이",
"addTiles": "타일 추가",
"closePicker": "닫기",
"search": "타일 검색",
"noMatches": "일치하는 항목이 없습니다.",
"kind": {
"all": "전체",
"card": "카드",
"chart": "차트",
"signal": "상태",
"action": "작업",
"resource": "목록",
"session": "세션"
},
"reset": "초기화",
"done": "완료"
}
}
+47
View File
@@ -621,6 +621,7 @@
"hostClickBehavior": "Paspaudus serverį",
"hostClickBehaviorDesc": "Kas nutinka, kai paspaudžiate serverį, kurio skirtukas jau atidarytas. Paspaudus su Ctrl/Cmd, vidiniu pelės mygtuku arba pasirinkus „Atidaryti naujame skirtuke“, visada atidaromas naujas skirtukas.",
"hostClickNewTab": "Visada atidaryti naują skirtuką",
"hostClickHostPage": "Atidaryti serverio puslapį, jei priedas jį siūlo",
"hostClickFocusExisting": "Perjungti į atidarytą skirtuką",
"hostClickFocusExistingDoubleNew": "Paspaudus – perjungti, dukart paspaudus – naujas skirtukas",
"openInNewTab": "Atidaryti naujame skirtuke",
@@ -1248,6 +1249,18 @@
"title": "Siųsti įspėjimus",
"consequence": "Jums arba kitiems vartotojams, per nustatytus kanalus."
},
"operations:provide": {
"title": "Siūlyti veiksmus",
"consequence": "Jo veiksmus gali vykdyti Automations, CLI, telefono programėlė ir kitur, naudotojai, kuriems tai leidžiama."
},
"operations:invoke": {
"title": "Vykdyti kitų priedų veiksmus",
"consequence": "Veikia prisijungusio naudotojo vardu su manifeste išvardytais veiksmais. Toliau taikomos patvirtinimo taisyklės."
},
"operations:approve": {
"title": "Nustatyti, kas reikalauja patvirtinimo",
"consequence": "Gali leisti veiksmus vykdyti be patvirtinimo arba juos blokuoti visiems naudotojams."
},
"notify:hub": {
"title": "Gauti visus įspėjimus",
"consequence": "Laiko ir pristato visus įskiepių siunčiamus įspėjimus, įskaitant jų turinį."
@@ -1272,6 +1285,10 @@
"title": "Tvarkyti serverius kaip tik skaitymo režimu",
"consequence": "Serverių, kuriuos jis tvarko, negalima redaguoti, ištrinti ar dalintis kitais būdais."
},
"hosts:connect-policy": {
"title": "Nustatyti, kas gali prisijungti",
"consequence": "Gali blokuoti arba leisti visus SSH ir nuotolinio darbalaukio prisijungimus prie bet kurio serverio."
},
"desktop:window": {
"title": "Atidaryti darbalaukio langus",
"consequence": "Tik Termix darbalaukio programoje."
@@ -1747,6 +1764,12 @@
"title": "Stulpeliai",
"auto": "Automatiškai",
"count": "{{count}} pločio"
},
"pager": {
"previous": "Ankstesnis",
"next": "Kitas",
"page": "{{page}} puslapis",
"pageOf": "{{page}} iš {{pages}} puslapio"
}
},
"fields": {
@@ -1771,5 +1794,29 @@
"admin": "Administratorius",
"user": "Vartotojas"
}
},
"tileGrid": {
"empty": "Dar nėra plytelių. Pridėkite keletą iš sąrašo.",
"move": "Vilkite, kad perkeltumėte",
"moveTile": "Perkelti „{{tile}}“",
"settings": "„{{tile}}“ nustatymai",
"remove": "Pašalinti „{{tile}}“",
"width": "Plotis",
"height": "Aukštis",
"addTiles": "Pridėti plyteles",
"closePicker": "Uždaryti",
"search": "Ieškoti plytelių",
"noMatches": "Nėra sutapimų.",
"kind": {
"all": "Visos",
"card": "Kortelės",
"chart": "Grafikai",
"signal": "Būsena",
"action": "Veiksmai",
"resource": "Sąrašai",
"session": "Sesijos"
},
"reset": "Atstatyti",
"done": "Atlikta"
}
}
+47
View File
@@ -615,6 +615,7 @@
"hostClickBehavior": "Klikšķinot uz mezgla",
"hostClickBehaviorDesc": "Kas notiek, kad noklikšķināt uz mezgla, kuram jau ir atvērta cilne. Ctrl/Cmd klikšķis, vidējā pogas klikšķis vai \"Atvērt jaunā cilnē\" vienmēr atver jaunu.",
"hostClickNewTab": "Vienmēr atvērt jaunā cilnē",
"hostClickHostPage": "Atvērt resursa lapu, ja spraudnis to piedāvā",
"hostClickFocusExisting": "Pārslēgties uz atvērto cilni",
"hostClickFocusExistingDoubleNew": "Klikšķiniet, lai pārslēgtos, dubultklikšķis jaunai cilnei",
"openInNewTab": "Atvērt jaunā cilnē",
@@ -1231,6 +1232,18 @@
"title": "Nosūtīt brīdinājumus",
"consequence": "Jums vai citiem lietotājiem, caur kanāliem, ko tie ir iestatījuši."
},
"operations:provide": {
"title": "Piedāvāt darbības",
"consequence": "Tās darbības var izpildīt no Automatizācijām, komandrindas rīka, tālruņa lietotnes un citām vietām, personām, kurām ir atļauts tās izmantot."
},
"operations:invoke": {
"title": "Izpildīt citu spraudņu darbības",
"consequence": "Rīkojas no pieteiktā lietotāja vārda attiecībā uz darbībām, kas uzskaitītas tā manifestā. Apstiprināšanas noteikumi joprojām ir spēkā."
},
"operations:approve": {
"title": "Noteikt, kam nepieciešams apstiprinājums",
"consequence": "Var ļaut darbībām izpildīties bez apstiprinājuma vai bloķēt tās katram lietotājam."
},
"notify:hub": {
"title": "Saņemt katru brīdinājumu",
"consequence": "Glabā un piegādā visus brīdinājumus, ko nosūta visi spraudņi, ieskaitot to saturu."
@@ -1255,6 +1268,10 @@
"title": "Pārvaldīt resursdatorus kā lasāmus",
"consequence": "Tos pārvaldītos resursdatorus neviens cits nevar rediģēt, dzēst vai atkārtoti kopīgot."
},
"hosts:connect-policy": {
"title": "Noteikt, kam ir atļauts izveidot savienojumu",
"consequence": "Var bloķēt vai atļaut katru SSH un attālinātā darbvirsas savienojumu ar jebkuru resursu."
},
"desktop:window": {
"title": "Atvērt darbvirsmas logus",
"consequence": "Tikai Termix darbvirsmas lietotnē."
@@ -1705,6 +1722,12 @@
"title": "Kolonnas",
"auto": "Automātiski",
"count": "{{count}} kolonnu platums"
},
"pager": {
"previous": "Iepriekšējais",
"next": "Nākamais",
"page": "{{page}}. lapa",
"pageOf": "{{page}}. lapa no {{pages}}"
}
},
"fields": {
@@ -1726,5 +1749,29 @@
"admin": "Administrators",
"user": "Lietotājs"
}
},
"tileGrid": {
"empty": "Vēl nav flīžu. Pievienojiet dažas no saraksta.",
"move": "Velciet, lai pārvietotu",
"moveTile": "Pārvietot {{tile}}",
"settings": "{{tile}} iestatījumi",
"remove": "Noņemt {{tile}}",
"width": "Platums",
"height": "Augstums",
"addTiles": "Pievienot flīzes",
"closePicker": "Aizvērt",
"search": "Meklēt flīzes",
"noMatches": "Nav atrasts nekas.",
"kind": {
"all": "Visas",
"card": "Kartiņas",
"chart": "Diagrammas",
"signal": "Statuss",
"action": "Darbības",
"resource": "Saraksti",
"session": "Sesijas"
},
"reset": "Atiestatīt",
"done": "Gatavs"
}
}
+47
View File
@@ -603,6 +603,7 @@
"hostClickBehavior": "Mengklik hos",
"hostClickBehaviorDesc": "Apa yang berlaku apabila anda mengklik hos yang sudah mempunyai tab terbuka. Klik Ctrl/Cmd, klik tengah, atau \"Buka dalam tab baru\" sentiasa memulakan yang baru.",
"hostClickNewTab": "Sentiasa buka tab baru",
"hostClickHostPage": "Buka halaman hos, jika plugin menawarkan satu",
"hostClickFocusExisting": "Tukar ke tab yang terbuka",
"hostClickFocusExistingDoubleNew": "Klik untuk tukar, klik dua kali untuk tab baru",
"openInNewTab": "Buka dalam tab baru",
@@ -1197,6 +1198,18 @@
"title": "Hantar amaran",
"consequence": "Kepada anda atau pengguna lain, dan melalui saluran yang mereka tetapkan."
},
"operations:provide": {
"title": "Tawar tindakan",
"consequence": "Tindakannya boleh dijalankan dari Automasi, CLI, aplikasi telefon dan tempat lain, oleh orang yang dibenarkan menggunakannya."
},
"operations:invoke": {
"title": "Jalankan tindakan plugin lain",
"consequence": "Bertindak bagi pengguna yang log masuk pada tindakan yang disenaraikan dalam manifestnya. Peraturan kelulusan masih terpakai."
},
"operations:approve": {
"title": "Tentukan apa yang memerlukan kelulusan",
"consequence": "Boleh membiarkan tindakan berjalan tanpa kelulusan, atau menyekatnya, untuk setiap pengguna."
},
"notify:hub": {
"title": "Terima setiap amaran",
"consequence": "Menyimpan dan menghantar semua amaran yang dihantar oleh plugin, termasuk kandungannya."
@@ -1221,6 +1234,10 @@
"title": "Urus hos sebagai baca sahaja",
"consequence": "Hos yang diuruskan tidak boleh diedit, dipadam atau dikongsi semula oleh sesiapa pun."
},
"hosts:connect-policy": {
"title": "Tentukan siapa yang boleh menyambung",
"consequence": "Boleh menyekat atau membenarkan setiap sambungan SSH dan desktop jauh ke mana-mana hos."
},
"desktop:window": {
"title": "Buka tetingkap desktop",
"consequence": "Hanya dalam aplikasi desktop Termix."
@@ -1621,6 +1638,12 @@
"title": "Lajur",
"auto": "Auto",
"count": "Lebar {{count}}"
},
"pager": {
"previous": "Sebelumnya",
"next": "Seterusnya",
"page": "Halaman {{page}}",
"pageOf": "Halaman {{page}} daripada {{pages}}"
}
},
"fields": {
@@ -1636,5 +1659,29 @@
"admin": "Admin",
"user": "Pengguna"
}
},
"tileGrid": {
"empty": "Tiada jubin lagi. Tambah beberapa daripada senarai.",
"move": "Seret untuk bergerak",
"moveTile": "Pindah {{tile}}",
"settings": "Tetapan {{tile}}",
"remove": "Buang {{tile}}",
"width": "Lebar",
"height": "Tinggi",
"addTiles": "Tambah jubin",
"closePicker": "Tutup",
"search": "Cari jubin",
"noMatches": "Tiada padanan.",
"kind": {
"all": "Semua",
"card": "Kad",
"chart": "Carta",
"signal": "Status",
"action": "Tindakan",
"resource": "Senarai",
"session": "Sesi"
},
"reset": "Set semula",
"done": "Selesai"
}
}
+47
View File
@@ -609,6 +609,7 @@
"hostClickBehavior": "Klik op een host",
"hostClickBehaviorDesc": "Wat er gebeurt als je op een host klikt die al een geopend tabblad heeft. Ctrl/Cmd-klik, middelklik of \"Openen in nieuw tabblad\" starten altijd een nieuw tabblad.",
"hostClickNewTab": "Altijd een nieuw tabblad openen",
"hostClickHostPage": "Open de hostpagina, als een plugin die aanbiedt",
"hostClickFocusExisting": "Schakel naar het geopende tabblad",
"hostClickFocusExistingDoubleNew": "Klik om te schakelen, dubbelklik voor een nieuw tabblad",
"openInNewTab": "Openen in nieuw tabblad",
@@ -1214,6 +1215,18 @@
"title": "Verstuur meldingen",
"consequence": "Naar jou of andere gebruikers, via de kanalen die zij hebben ingesteld."
},
"operations:provide": {
"title": "Acties aanbieden",
"consequence": "De acties kunnen worden uitgevoerd vanuit Automatiseringen, de CLI, de mobiele app en andere plaatsen, door gebruikers die daar toestemming voor hebben."
},
"operations:invoke": {
"title": "Acties van andere plugins uitvoeren",
"consequence": "Handelt namens de ingelogde gebruiker voor de acties die in het manifest staan vermeld. Goedkeuringsregels blijven van toepassing."
},
"operations:approve": {
"title": "Bepalen wat goedkeuring vereist",
"consequence": "Kan acties zonder goedkeuring laten uitvoeren of blokkeren, voor elke gebruiker."
},
"notify:hub": {
"title": "Ontvang elke melding",
"consequence": "Bewaart en levert de meldingen die alle plugins versturen, inclusief de inhoud."
@@ -1238,6 +1251,10 @@
"title": "Hosts als alleen-lezen beheren",
"consequence": "Hosts die hierdoor worden beheerd, kunnen door niemand anders worden bewerkt, verwijderd of opnieuw gedeeld."
},
"hosts:connect-policy": {
"title": "Bepalen wie kan verbinden",
"consequence": "Kan elke SSH- en remote desktopverbinding met elke host blokkeren of toestaan."
},
"desktop:window": {
"title": "Open desktopvensters",
"consequence": "Alleen in de Termix-desktopapp."
@@ -1663,6 +1680,12 @@
"title": "Kolommen",
"auto": "Automatisch",
"count": "{{count}} kolommen breed"
},
"pager": {
"previous": "Vorige",
"next": "Volgende",
"page": "Pagina {{page}}",
"pageOf": "Pagina {{page}} van {{pages}}"
}
},
"fields": {
@@ -1681,5 +1704,29 @@
"admin": "Beheerder",
"user": "Gebruiker"
}
},
"tileGrid": {
"empty": "Nog geen tegels. Voeg er een paar toe uit de lijst.",
"move": "Slepen om te verplaatsen",
"moveTile": "Verplaats {{tile}}",
"settings": "Instellingen voor {{tile}}",
"remove": "Verwijder {{tile}}",
"width": "Breedte",
"height": "Hoogte",
"addTiles": "Tegels toevoegen",
"closePicker": "Sluiten",
"search": "Zoek tegels",
"noMatches": "Geen resultaten gevonden.",
"kind": {
"all": "Alles",
"card": "Kaarten",
"chart": "Grafieken",
"signal": "Status",
"action": "Acties",
"resource": "Lijsten",
"session": "Sessies"
},
"reset": "Resetten",
"done": "Klaar"
}
}
+47
View File
@@ -609,6 +609,7 @@
"hostClickBehavior": "Å klikke på en vertsmaskin",
"hostClickBehaviorDesc": "Hva som skjer når du klikker på en vertsmaskin som allerede har en åpen fane. Ctrl/Cmd-klikk, midtklikk eller «Åpne i ny fane» starter alltid en ny.",
"hostClickNewTab": "Alltid åpne en ny fane",
"hostClickHostPage": "Åpne vertsiden, hvis et programtillegg tilbyr en",
"hostClickFocusExisting": "Bytt til den åpne fanen",
"hostClickFocusExistingDoubleNew": "Klikk for å bytte, dobbeltklikk for ny fane",
"openInNewTab": "Åpne i ny fane",
@@ -1214,6 +1215,18 @@
"title": "Send varsler",
"consequence": "Til deg eller andre brukere, og gjennom kanalene de har satt opp."
},
"operations:provide": {
"title": "Tilby handlinger",
"consequence": "Handlingene kan kjøres fra automatiseringer, kommandolinjen, mobilappen og andre steder, av brukere som har tillatelse til å bruke dem."
},
"operations:invoke": {
"title": "Kjør andre programtilleggs handlinger",
"consequence": "Utfører handlinger på vegne av den innloggede brukeren, basert på listen i manifestet. Godkjenningsregler gjelder fortsatt."
},
"operations:approve": {
"title": "Avgjør hva som krever godkjenning",
"consequence": "Kan la handlinger kjøre uten godkjenning, eller blokkere dem, for alle brukere."
},
"notify:hub": {
"title": "Motta alle varsler",
"consequence": "Lagrer og leverer varslene alle plugins sender, inkludert innholdet deres."
@@ -1238,6 +1251,10 @@
"title": "Administrere vertsmaskiner som skrivebeskyttet",
"consequence": "Vertsmaskinene den administrerer kan ikke redigeres, slettes eller deles på nytt av noen andre."
},
"hosts:connect-policy": {
"title": "Avgjør hvem som kan koble til",
"consequence": "Kan blokkere eller tillate alle SSH- og fjernskrivebordsforbindelser til enhver vert."
},
"desktop:window": {
"title": "Åpne skrivebordsvinduer",
"consequence": "Kun i Termix-skrivebordsappen."
@@ -1663,6 +1680,12 @@
"title": "Kolonner",
"auto": "Auto",
"count": "{{count}} bred"
},
"pager": {
"previous": "Forrige",
"next": "Neste",
"page": "Side {{page}}",
"pageOf": "Side {{page}} av {{pages}}"
}
},
"fields": {
@@ -1681,5 +1704,29 @@
"admin": "Administrator",
"user": "Bruker"
}
},
"tileGrid": {
"empty": "Ingen fliser ennå. Legg til noen fra listen.",
"move": "Dra for å flytte",
"moveTile": "Flytt {{tile}}",
"settings": "Innstillinger for {{tile}}",
"remove": "Fjern {{tile}}",
"width": "Bredde",
"height": "Høyde",
"addTiles": "Legg til fliser",
"closePicker": "Lukk",
"search": "Søk etter fliser",
"noMatches": "Ingen treff.",
"kind": {
"all": "Alle",
"card": "Kort",
"chart": "Diagrammer",
"signal": "Status",
"action": "Handlinger",
"resource": "Lister",
"session": "Økter"
},
"reset": "Tilbakestill",
"done": "Ferdig"
}
}
+47
View File
@@ -621,6 +621,7 @@
"hostClickBehavior": "Kliknięcie hosta",
"hostClickBehaviorDesc": "Co się stanie, gdy klikniesz hosta, który ma już otwartą kartę. Kliknięcie z Ctrl/Cmd, kliknięcie środkowym przyciskiem lub opcja „Otwórz w nowej karcie” zawsze tworzą nową kartę.",
"hostClickNewTab": "Zawsze otwieraj nową kartę",
"hostClickHostPage": "Otwórz stronę hosta, jeśli wtyczka ją oferuje",
"hostClickFocusExisting": "Przełącz na otwartą kartę",
"hostClickFocusExistingDoubleNew": "Kliknij, aby przełączyć, podwójne kliknięcie dla nowej karty",
"openInNewTab": "Otwórz w nowej karcie",
@@ -1248,6 +1249,18 @@
"title": "Wysyłanie alertów",
"consequence": "Do Ciebie lub innych użytkowników, przez skonfigurowane przez nich kanały."
},
"operations:provide": {
"title": "Oferuj akcje",
"consequence": "Jej akcje mogą być uruchamiane z Automatyzacji, CLI, aplikacji mobilnej i innych miejsc przez użytkowników, którzy mają do nich uprawnienia."
},
"operations:invoke": {
"title": "Uruchamiaj akcje innych wtyczek",
"consequence": "Działa w imieniu zalogowanego użytkownika dla akcji wymienionych w jej manifeście. Zasady zatwierdzania nadal obowiązują."
},
"operations:approve": {
"title": "Decyduj, co wymaga zatwierdzenia",
"consequence": "Może pozwalać na uruchamianie akcji bez zatwierdzenia lub je blokować dla każdego użytkownika."
},
"notify:hub": {
"title": "Otrzymywanie wszystkich alertów",
"consequence": "Przechowuje i dostarcza alerty wysyłane przez wszystkie wtyczki, w tym ich treść."
@@ -1272,6 +1285,10 @@
"title": "Zarządzaj hostami w trybie tylko do odczytu",
"consequence": "Hosty nimi zarządzane nie mogą być edytowane, usuwane ani udostępniane ponownie przez innych użytkowników."
},
"hosts:connect-policy": {
"title": "Decyduj, kto może się łączyć",
"consequence": "Może blokować lub zezwalać na każde połączenie SSH i zdalnego pulpitu z dowolnym hostem."
},
"desktop:window": {
"title": "Otwieranie okien pulpitu",
"consequence": "Tylko w aplikacji pulpitu Termix."
@@ -1747,6 +1764,12 @@
"title": "Kolumny",
"auto": "Automatycznie",
"count": "Szerokość: {{count}}"
},
"pager": {
"previous": "Poprzednia",
"next": "Następna",
"page": "Strona {{page}}",
"pageOf": "Strona {{page}} z {{pages}}"
}
},
"fields": {
@@ -1771,5 +1794,29 @@
"admin": "Administrator",
"user": "Użytkownik"
}
},
"tileGrid": {
"empty": "Brak kafelków. Dodaj kilka z listy.",
"move": "Przeciągnij, aby przenieść",
"moveTile": "Przenieś {{tile}}",
"settings": "Ustawienia {{tile}}",
"remove": "Usuń {{tile}}",
"width": "Szerokość",
"height": "Wysokość",
"addTiles": "Dodaj kafelki",
"closePicker": "Zamknij",
"search": "Szukaj kafelków",
"noMatches": "Brak wyników.",
"kind": {
"all": "Wszystkie",
"card": "Karty",
"chart": "Wykresy",
"signal": "Status",
"action": "Akcje",
"resource": "Listy",
"session": "Sesje"
},
"reset": "Resetuj",
"done": "Gotowe"
}
}
+47
View File
@@ -615,6 +615,7 @@
"hostClickBehavior": "Ao clicar em um host",
"hostClickBehaviorDesc": "O que acontece ao clicar em um host que já possui uma aba aberta. Ctrl/Cmd + clique, clique do meio ou \"Abrir em nova aba\" sempre iniciam uma nova.",
"hostClickNewTab": "Sempre abrir uma nova aba",
"hostClickHostPage": "Abre a página do host, quando um plugin oferece uma",
"hostClickFocusExisting": "Alternar para a aba aberta",
"hostClickFocusExistingDoubleNew": "Clique para alternar, clique duplo para nova aba",
"openInNewTab": "Abrir em nova aba",
@@ -1231,6 +1232,18 @@
"title": "Enviar alertas",
"consequence": "Para você ou outros usuários, e pelos canais que eles configuraram."
},
"operations:provide": {
"title": "Oferecer ações",
"consequence": "Suas ações podem ser executadas a partir de Automações, da CLI, do app para celular e outros lugares, por pessoas autorizadas a usá-las."
},
"operations:invoke": {
"title": "Executar ações de outros plugins",
"consequence": "Age em nome do usuário conectado nas ações listadas em seu manifesto. As regras de aprovação ainda se aplicam."
},
"operations:approve": {
"title": "Decidir o que precisa de aprovação",
"consequence": "Pode permitir que ações sejam executadas sem aprovação ou bloqueá-las, para todos os usuários."
},
"notify:hub": {
"title": "Receber todos os alertas",
"consequence": "Armazena e entrega os alertas enviados por todos os plugins, incluindo seu conteúdo."
@@ -1255,6 +1268,10 @@
"title": "Gerenciar hosts como somente leitura",
"consequence": "Os hosts gerenciados não podem ser editados, excluídos ou compartilhados novamente por ninguém."
},
"hosts:connect-policy": {
"title": "Decidir quem pode se conectar",
"consequence": "Pode bloquear ou permitir todas as conexões SSH e de área de trabalho remota a qualquer host."
},
"desktop:window": {
"title": "Abrir janelas de desktop",
"consequence": "Apenas no aplicativo desktop do Termix."
@@ -1705,6 +1722,12 @@
"title": "Colunas",
"auto": "Automático",
"count": "{{count}} colunas"
},
"pager": {
"previous": "Anterior",
"next": "Próximo",
"page": "Página {{page}}",
"pageOf": "Página {{page}} de {{pages}}"
}
},
"fields": {
@@ -1726,5 +1749,29 @@
"admin": "Administrador",
"user": "Usuário"
}
},
"tileGrid": {
"empty": "Nenhum bloco ainda. Adicione alguns da lista.",
"move": "Arraste para mover",
"moveTile": "Mover {{tile}}",
"settings": "Configurações de {{tile}}",
"remove": "Remover {{tile}}",
"width": "Largura",
"height": "Altura",
"addTiles": "Adicionar blocos",
"closePicker": "Fechar",
"search": "Pesquisar blocos",
"noMatches": "Nada corresponde.",
"kind": {
"all": "Todos",
"card": "Cartões",
"chart": "Gráficos",
"signal": "Status",
"action": "Ações",
"resource": "Listas",
"session": "Sessões"
},
"reset": "Redefinir",
"done": "Concluído"
}
}
+47
View File
@@ -615,6 +615,7 @@
"hostClickBehavior": "Ao clicar num host",
"hostClickBehaviorDesc": "O que acontece ao clicar num host que já tem uma aba aberta. Ctrl/Cmd + clique, clique do meio ou \"Abrir em nova aba\" iniciam sempre uma nova.",
"hostClickNewTab": "Abrir sempre uma nova aba",
"hostClickHostPage": "Abrir a página do host, quando um plugin a disponibilizar",
"hostClickFocusExisting": "Mudar para a aba aberta",
"hostClickFocusExistingDoubleNew": "Clique para mudar, duplo clique para nova aba",
"openInNewTab": "Abrir em nova aba",
@@ -1231,6 +1232,18 @@
"title": "Enviar alertas",
"consequence": "Para si ou para outros utilizadores, e através dos canais que configuraram."
},
"operations:provide": {
"title": "Oferecer ações",
"consequence": "As suas ações podem ser executadas a partir de Automações, da CLI, da aplicação móvel e noutros locais, por pessoas autorizadas a utilizá-las."
},
"operations:invoke": {
"title": "Executar ações de outros plugins",
"consequence": "Atua em nome do utilizador autenticado nas ações listadas no seu manifesto. As regras de aprovação continuam a aplicar-se."
},
"operations:approve": {
"title": "Decidir o que necessita de aprovação",
"consequence": "Pode permitir que ações sejam executadas sem aprovação ou bloqueá-las, para todos os utilizadores."
},
"notify:hub": {
"title": "Receber todos os alertas",
"consequence": "Armazena e entrega os alertas enviados por todos os plugins, incluindo o seu conteúdo."
@@ -1255,6 +1268,10 @@
"title": "Gerir hosts como apenas leitura",
"consequence": "Os hosts que gere não podem ser editados, eliminados ou partilhados novamente por ninguém."
},
"hosts:connect-policy": {
"title": "Decidir quem pode ligar",
"consequence": "Pode bloquear ou permitir todas as ligações SSH e de ambiente de trabalho remoto a qualquer host."
},
"desktop:window": {
"title": "Abrir janelas de ambiente de trabalho",
"consequence": "Apenas na aplicação de ambiente de trabalho Termix."
@@ -1705,6 +1722,12 @@
"title": "Colunas",
"auto": "Automático",
"count": "{{count}} de largura"
},
"pager": {
"previous": "Anterior",
"next": "Seguinte",
"page": "Página {{page}}",
"pageOf": "Página {{page}} de {{pages}}"
}
},
"fields": {
@@ -1726,5 +1749,29 @@
"admin": "Administrador",
"user": "Utilizador"
}
},
"tileGrid": {
"empty": "Ainda não há widgets. Adicione alguns a partir da lista.",
"move": "Arrastar para mover",
"moveTile": "Mover {{tile}}",
"settings": "Definições de {{tile}}",
"remove": "Remover {{tile}}",
"width": "Largura",
"height": "Altura",
"addTiles": "Adicionar widgets",
"closePicker": "Fechar",
"search": "Pesquisar widgets",
"noMatches": "Nenhum resultado.",
"kind": {
"all": "Todos",
"card": "Cartões",
"chart": "Gráficos",
"signal": "Estado",
"action": "Ações",
"resource": "Listas",
"session": "Sessões"
},
"reset": "Repor",
"done": "Concluído"
}
}
+47
View File
@@ -615,6 +615,7 @@
"hostClickBehavior": "Click pe o gazdă",
"hostClickBehaviorDesc": "Ce se întâmplă când dai click pe o gazdă care are deja un tab deschis. Ctrl/Cmd click, click cu butonul din mijloc sau „Deschide într-un tab nou” pornesc întotdeauna unul nou.",
"hostClickNewTab": "Deschide întotdeauna un tab nou",
"hostClickHostPage": "Deschide pagina gazdei, dacă un plugin oferă una",
"hostClickFocusExisting": "Comută la tab-ul deschis",
"hostClickFocusExistingDoubleNew": "Click pentru a comuta, dublu click pentru un tab nou",
"openInNewTab": "Deschide într-un tab nou",
@@ -1231,6 +1232,18 @@
"title": "Trimite alerte",
"consequence": "Către tine sau către alți utilizatori, prin canalele configurate de aceștia."
},
"operations:provide": {
"title": "Oferă acțiuni",
"consequence": "Acțiunile sale pot fi executate din Automatizări, linia de comandă, aplicația mobilă și alte locații, de către persoanele autorizate să le folosească."
},
"operations:invoke": {
"title": "Execută acțiuni ale altor plugin-uri",
"consequence": "Acționează în numele utilizatorului autentificat pentru acțiunile listate în manifestul său. Regulile de aprobare se aplică în continuare."
},
"operations:approve": {
"title": "Stabilește ce necesită aprobare",
"consequence": "Poate permite ca acțiunile să se execute fără aprobare sau le poate bloca, pentru fiecare utilizator."
},
"notify:hub": {
"title": "Primește fiecare alertă",
"consequence": "Stochează și livrează alertele trimise de toate plugin-urile, inclusiv conținutul acestora."
@@ -1255,6 +1268,10 @@
"title": "Gestionează gazdele ca doar pentru citire",
"consequence": "Gazdele pe care le gestionează nu pot fi editate, șterse sau redistribuite de nimeni altcineva."
},
"hosts:connect-policy": {
"title": "Stabilește cine se poate conecta",
"consequence": "Poate bloca sau permite fiecare conexiune SSH și desktop la distanță către orice gazdă."
},
"desktop:window": {
"title": "Deschide ferestre desktop",
"consequence": "Doar în aplicația desktop Termix."
@@ -1705,6 +1722,12 @@
"title": "Coloane",
"auto": "Automat",
"count": "{{count}} coloane"
},
"pager": {
"previous": "Anterior",
"next": "Următor",
"page": "Pagina {{page}}",
"pageOf": "Pagina {{page}} din {{pages}}"
}
},
"fields": {
@@ -1726,5 +1749,29 @@
"admin": "Administrator",
"user": "Utilizator"
}
},
"tileGrid": {
"empty": "Încă nu există plăcuțe. Adaugă unele din listă.",
"move": "Trage pentru a muta",
"moveTile": "Mută {{tile}}",
"settings": "Setări {{tile}}",
"remove": "Elimină {{tile}}",
"width": "Lățime",
"height": "Înălțime",
"addTiles": "Adaugă plăcuțe",
"closePicker": "Închide",
"search": "Caută plăcuțe",
"noMatches": "Niciun rezultat.",
"kind": {
"all": "Toate",
"card": "Carduri",
"chart": "Grafice",
"signal": "Stare",
"action": "Acțiuni",
"resource": "Liste",
"session": "Sesiuni"
},
"reset": "Resetează",
"done": "Gata"
}
}
+47
View File
@@ -621,6 +621,7 @@
"hostClickBehavior": "Клик по хосту",
"hostClickBehaviorDesc": "Что происходит при клике по хосту, у которого уже открыта вкладка. Ctrl/Cmd + клик, клик средней кнопкой мыши или «Открыть в новой вкладке» всегда открывают новую.",
"hostClickNewTab": "Всегда открывать новую вкладку",
"hostClickHostPage": "Открыть страницу хоста, если плагин её предоставляет",
"hostClickFocusExisting": "Переключиться на открытую вкладку",
"hostClickFocusExistingDoubleNew": "Клик для переключения, двойной клик для новой вкладки",
"openInNewTab": "Открыть в новой вкладке",
@@ -1248,6 +1249,18 @@
"title": "Отправка уведомлений",
"consequence": "Вам или другим пользователям через настроенные ими каналы."
},
"operations:provide": {
"title": "Предоставлять действия",
"consequence": "Его действия могут выполняться из Автоматизаций, CLI, мобильного приложения и других мест пользователями, имеющими на это право."
},
"operations:invoke": {
"title": "Выполнять действия других плагинов",
"consequence": "Выполняет действия из своего манифеста от имени текущего пользователя. Правила одобрения по-прежнему применяются."
},
"operations:approve": {
"title": "Определять, что требует одобрения",
"consequence": "Может разрешать выполнение действий без одобрения или блокировать их для любого пользователя."
},
"notify:hub": {
"title": "Получение всех уведомлений",
"consequence": "Хранит и доставляет все уведомления, отправленные другими плагинами, включая их содержимое."
@@ -1272,6 +1285,10 @@
"title": "Управление хостами в режиме только для чтения",
"consequence": "Хосты, которыми он управляет, не могут быть изменены, удалены или повторно переданы другим пользователям."
},
"hosts:connect-policy": {
"title": "Определять, кто может подключаться",
"consequence": "Может блокировать или разрешать любые SSH-подключения и подключения удалённого рабочего стола к любому хосту."
},
"desktop:window": {
"title": "Открытие окон рабочего стола",
"consequence": "Только в настольном приложении Termix."
@@ -1747,6 +1764,12 @@
"title": "Колонки",
"auto": "Авто",
"count": "{{count}} колонок"
},
"pager": {
"previous": "Назад",
"next": "Вперёд",
"page": "Страница {{page}}",
"pageOf": "Страница {{page}} из {{pages}}"
}
},
"fields": {
@@ -1771,5 +1794,29 @@
"admin": "Администратор",
"user": "Пользователь"
}
},
"tileGrid": {
"empty": "Виджетов пока нет. Добавьте их из списка.",
"move": "Перетащите для перемещения",
"moveTile": "Переместить {{tile}}",
"settings": "Настройки {{tile}}",
"remove": "Удалить {{tile}}",
"width": "Ширина",
"height": "Высота",
"addTiles": "Добавить виджеты",
"closePicker": "Закрыть",
"search": "Поиск виджетов",
"noMatches": "Ничего не найдено.",
"kind": {
"all": "Все",
"card": "Карточки",
"chart": "Графики",
"signal": "Статус",
"action": "Действия",
"resource": "Списки",
"session": "Сессии"
},
"reset": "Сбросить",
"done": "Готово"
}
}
+47
View File
@@ -621,6 +621,7 @@
"hostClickBehavior": "Kliknutie na hostiteľa",
"hostClickBehaviorDesc": "Čo sa stane, keď kliknete na hostiteľa, ktorý už má otvorený panel. Kliknutie s Ctrl/Cmd, stredným tlačidlom alebo možnosť „Otvoriť v novom paneli“ vždy spustí nový.",
"hostClickNewTab": "Vždy otvoriť nový panel",
"hostClickHostPage": "Otvoriť stránku hostiteľa, ak ju plugin ponúka",
"hostClickFocusExisting": "Prepnúť na otvorený panel",
"hostClickFocusExistingDoubleNew": "Kliknutím prepnúť, dvojklikom otvoriť nový panel",
"openInNewTab": "Otvoriť v novom paneli",
@@ -1248,6 +1249,18 @@
"title": "Posielať upozornenia",
"consequence": "Vám alebo iným používateľom a cez kanály, ktoré si nastavili."
},
"operations:provide": {
"title": "Ponúkať akcie",
"consequence": "Jeho akcie môžu spúšťať automatizácie, príkazový riadok, mobilná aplikácia a ďalšie miesta, a to používateľmi, ktorí majú povolenie na ich použitie."
},
"operations:invoke": {
"title": "Spúšťať akcie iných pluginov",
"consequence": "Koná v mene prihláseného používateľa pri akciách uvedených v jeho manifeste. Stále sa uplatňujú pravidlá schvaľovania."
},
"operations:approve": {
"title": "Rozhodnúť, čo vyžaduje schválenie",
"consequence": "Môže povoliť spúšťanie akcií bez schválenia alebo ich blokovať pre každého používateľa."
},
"notify:hub": {
"title": "Prijať každé upozornenie",
"consequence": "Ukladá a doručuje upozornenia, ktoré posielajú všetky pluginy, vrátane ich obsahu."
@@ -1272,6 +1285,10 @@
"title": "Spravovať hostov ako len na čítanie",
"consequence": "Hostov, ktoré spravuje, nemôže nikto iný upravovať, mazať ani zdieľať."
},
"hosts:connect-policy": {
"title": "Rozhodnúť, kto sa môže pripojiť",
"consequence": "Môže blokovať alebo povoliť každé SSH pripojenie a pripojenie na vzdialenú plochu k akémukoľvek hostiteľovi."
},
"desktop:window": {
"title": "Otvoriť okná plochy",
"consequence": "Len v desktopovej aplikácii Termix."
@@ -1747,6 +1764,12 @@
"title": "Stĺpce",
"auto": "Automaticky",
"count": "Šírka {{count}}"
},
"pager": {
"previous": "Predchádzajúca",
"next": "Nasledujúca",
"page": "Strana {{page}}",
"pageOf": "Strana {{page}} z {{pages}}"
}
},
"fields": {
@@ -1771,5 +1794,29 @@
"admin": "Administrátor",
"user": "Používateľ"
}
},
"tileGrid": {
"empty": "Zatiaľ žiadne dlaždice. Pridajte niektoré zo zoznamu.",
"move": "Ťahaním presunúť",
"moveTile": "Presunúť {{tile}}",
"settings": "Nastavenia {{tile}}",
"remove": "Odstrániť {{tile}}",
"width": "Šírka",
"height": "Výška",
"addTiles": "Pridať dlaždice",
"closePicker": "Zavrieť",
"search": "Hľadať dlaždice",
"noMatches": "Žiadne zhody.",
"kind": {
"all": "Všetky",
"card": "Karty",
"chart": "Grafy",
"signal": "Stav",
"action": "Akcie",
"resource": "Zoznamy",
"session": "Sedenia"
},
"reset": "Obnoviť",
"done": "Hotovo"
}
}
+47
View File
@@ -621,6 +621,7 @@
"hostClickBehavior": "Klik na gostitelja",
"hostClickBehaviorDesc": "Kaj se zgodi, ko klikneš na gostitelja, ki že ima odprt zavihek. Ctrl/Cmd klik, sredinski klik ali »Odpri v novem zavihku« vedno odprejo nov zavihek.",
"hostClickNewTab": "Vedno odpri nov zavihek",
"hostClickHostPage": "Odpri stran gostitelja, če vtičnik ponuja takšno",
"hostClickFocusExisting": "Preklopi na odprt zavihek",
"hostClickFocusExistingDoubleNew": "Klik za preklop, dvojni klik za nov zavihek",
"openInNewTab": "Odpri v novem zavihku",
@@ -1248,6 +1249,18 @@
"title": "Pošiljanje opozoril",
"consequence": "Vam ali drugim uporabnikom, prek kanalov, ki so jih nastavili."
},
"operations:provide": {
"title": "Ponudi ukaze",
"consequence": "Njegove ukaze lahko izvajajo avtomatizacije, ukazna vrstica, mobilna aplikacija in drugi deli programa, in sicer uporabniki, ki imajo za to dovoljenje."
},
"operations:invoke": {
"title": "Izvajaj ukaze drugih vtičnikov",
"consequence": "Ureja ukaze, navedene v njegovem manifestu, v imenu prijavljenega uporabnika. Pravila odobritve se še vedno uporabljajo."
},
"operations:approve": {
"title": "Odloči, kaj zahteva odobritev",
"consequence": "Lahko omogoči izvajanje ukazov brez odobritve ali jih blokira za vse uporabnike."
},
"notify:hub": {
"title": "Prejemanje vseh opozoril",
"consequence": "Shranjuje in dostavlja opozorila, ki jih pošiljajo vsi vtičniki, vključno z njihovo vsebino."
@@ -1272,6 +1285,10 @@
"title": "Upravljanje gostiteljev kot samo za branje",
"consequence": "Gostiteljev, ki jih upravlja, ne morejo urejati, brisati ali ponovno deliti drugi uporabniki."
},
"hosts:connect-policy": {
"title": "Odloči, kdo se lahko poveže",
"consequence": "Lahko blokira ali dovoli vsako SSH in daljinsko namizno povezavo do katerega koli gostitelja."
},
"desktop:window": {
"title": "Odpiranje oken namizja",
"consequence": "Samo v namizni aplikaciji Termix."
@@ -1747,6 +1764,12 @@
"title": "Stolpci",
"auto": "Samodejno",
"count": "{{count}} širok"
},
"pager": {
"previous": "Prejšnja",
"next": "Naslednja",
"page": "Stran {{page}}",
"pageOf": "Stran {{page}} od {{pages}}"
}
},
"fields": {
@@ -1771,5 +1794,29 @@
"admin": "Upravitelj",
"user": "Uporabnik"
}
},
"tileGrid": {
"empty": "Še ni ploščic. Dodajte jih s seznama.",
"move": "Povlecite za premik",
"moveTile": "Premakni {{tile}}",
"settings": "Nastavitve za {{tile}}",
"remove": "Odstrani {{tile}}",
"width": "Širina",
"height": "Višina",
"addTiles": "Dodaj ploščice",
"closePicker": "Zapri",
"search": "Išči ploščice",
"noMatches": "Ni ujemajočih se rezultatov.",
"kind": {
"all": "Vse",
"card": "Kartice",
"chart": "Grafikon",
"signal": "Stanje",
"action": "Ukazi",
"resource": "Seznami",
"session": "Seje"
},
"reset": "Ponastavi",
"done": "Končano"
}
}
+47
View File
@@ -615,6 +615,7 @@
"hostClickBehavior": "Клик на хост",
"hostClickBehaviorDesc": "Шта се дешава када кликнеш на хост који већ има отворену картицу. Ctrl/Cmd клик, средњи клик или „Отвори у новој картици” увек отварају нову.",
"hostClickNewTab": "Увек отвори нову картицу",
"hostClickHostPage": "Отвори страницу хоста, ако је понуди додатак",
"hostClickFocusExisting": "Пређи на отворену картицу",
"hostClickFocusExistingDoubleNew": "Клик за прелазак, двоклик за нову картицу",
"openInNewTab": "Отвори у новој картици",
@@ -1231,6 +1232,18 @@
"title": "Шаљите упозорења",
"consequence": "Вам или другим корисницима, и кроз канале које су они поставили."
},
"operations:provide": {
"title": "Нуди радње",
"consequence": "Његове радње могу се покренути из Аутоматизација, CLI-ја, мобилне апликације и других места, од стране корисника који имају дозволе за њихово коришћење."
},
"operations:invoke": {
"title": "Покрени радње других додатака",
"consequence": "Делује у име пријављеног корисника на радње наведене у манифесту. Правила одобрења и даље важе."
},
"operations:approve": {
"title": "Одреди шта захтева одобрење",
"consequence": "Може дозволити радње без одобрења или их блокирати, за сваког корисника."
},
"notify:hub": {
"title": "Примите сва упозорења",
"consequence": "Чува и доставља упозорења које шаљу сви плугини, укључујући њихов садржај."
@@ -1255,6 +1268,10 @@
"title": "Управљање домаћинима као само за читање",
"consequence": "Домаћине које управља не могу да уређују, бришу или поново деле други корисници."
},
"hosts:connect-policy": {
"title": "Одреди ко се може повезати",
"consequence": "Може блокирати или дозволити сваку SSH и удаљену десктоп везу са било којим хостом."
},
"desktop:window": {
"title": "Отворите прозоре десктопа",
"consequence": "Само у Termix десктоп апликацији."
@@ -1705,6 +1722,12 @@
"title": "Колоне",
"auto": "Аутоматски",
"count": "{{count}} широка"
},
"pager": {
"previous": "Претходна",
"next": "Следећа",
"page": "Страна {{page}}",
"pageOf": "Страна {{page}} од {{pages}}"
}
},
"fields": {
@@ -1726,5 +1749,29 @@
"admin": "Администратор",
"user": "Корисник"
}
},
"tileGrid": {
"empty": "Још нема плочица. Додајте неке са листе.",
"move": "Превуци за померање",
"moveTile": "Помери {{tile}}",
"settings": "Подешавања за {{tile}}",
"remove": "Уклони {{tile}}",
"width": "Ширина",
"height": "Висина",
"addTiles": "Додај плочице",
"closePicker": "Затвори",
"search": "Претражи плочице",
"noMatches": "Нема поклопања.",
"kind": {
"all": "Све",
"card": "Картице",
"chart": "Графикони",
"signal": "Статус",
"action": "Радње",
"resource": "Листе",
"session": "Сесије"
},
"reset": "Ресетуј",
"done": "Завршено"
}
}
+47
View File
@@ -609,6 +609,7 @@
"hostClickBehavior": "När du klickar på en värd",
"hostClickBehaviorDesc": "Vad som händer när du klickar på en värd som redan har en öppen flik. Ctrl/Cmd-klick, mellanklick eller \"Öppna i ny flik\" startar alltid en ny.",
"hostClickNewTab": "Öppna alltid en ny flik",
"hostClickHostPage": "Öppna värdens sida, om ett tillägg erbjuder en",
"hostClickFocusExisting": "Byt till den öppna fliken",
"hostClickFocusExistingDoubleNew": "Klicka för att byta, dubbelklicka för ny flik",
"openInNewTab": "Öppna i ny flik",
@@ -1214,6 +1215,18 @@
"title": "Skicka aviseringar",
"consequence": "Till dig eller andra användare, och via de kanaler de har konfigurerat."
},
"operations:provide": {
"title": "Erbjud åtgärder",
"consequence": "Dess åtgärder kan köras från automatiseringar, kommandoraden, mobilappen och andra platser, av personer som har tillstånd att använda dem."
},
"operations:invoke": {
"title": "Kör andra tilläggs åtgärder",
"consequence": "Utför åtgärder för den inloggade användaren enligt dess manifest. Godkännanderegler gäller fortfarande."
},
"operations:approve": {
"title": "Bestäm vad som kräver godkännande",
"consequence": "Kan låta åtgärder köras utan godkännande eller blockera dem, för varje användare."
},
"notify:hub": {
"title": "Ta emot alla aviseringar",
"consequence": "Lagrar och levererar de aviseringar alla plugins skickar, inklusive deras innehåll."
@@ -1238,6 +1251,10 @@
"title": "Hantera värdar som skrivskyddade",
"consequence": "Värdar som den hanterar kan inte redigeras, raderas eller delas om av någon annan."
},
"hosts:connect-policy": {
"title": "Bestäm vem som kan ansluta",
"consequence": "Kan blockera eller tillåta varje SSH- och fjärrskrivbordsanslutning till vilken värd som helst."
},
"desktop:window": {
"title": "Öppna skrivbordsfönster",
"consequence": "Endast i Termix-skrivbordsappen."
@@ -1663,6 +1680,12 @@
"title": "Kolumner",
"auto": "Automatiskt",
"count": "{{count}} bred"
},
"pager": {
"previous": "Föregående",
"next": "Nästa",
"page": "Sida {{page}}",
"pageOf": "Sida {{page}} av {{pages}}"
}
},
"fields": {
@@ -1681,5 +1704,29 @@
"admin": "Administratör",
"user": "Användare"
}
},
"tileGrid": {
"empty": "Inga kakel ännu. Lägg till några från listan.",
"move": "Dra för att flytta",
"moveTile": "Flytta {{tile}}",
"settings": "Inställningar för {{tile}}",
"remove": "Ta bort {{tile}}",
"width": "Bredd",
"height": "Höjd",
"addTiles": "Lägg till kakel",
"closePicker": "Stäng",
"search": "Sök bland kakel",
"noMatches": "Inga träffar.",
"kind": {
"all": "Alla",
"card": "Kort",
"chart": "Diagram",
"signal": "Status",
"action": "Åtgärder",
"resource": "Listor",
"session": "Sessioner"
},
"reset": "Återställ",
"done": "Klar"
}
}
+47
View File
@@ -603,6 +603,7 @@
"hostClickBehavior": "เมื่อคลิกโฮสต์",
"hostClickBehaviorDesc": "สิ่งที่เกิดขึ้นเมื่อคุณคลิกโฮสต์ที่มีแท็บเปิดอยู่แล้ว การคลิกพร้อม Ctrl/Cmd คลิกกลาง หรือ \"เปิดในแท็บใหม่\" จะเริ่มแท็บใหม่เสมอ",
"hostClickNewTab": "เปิดแท็บใหม่เสมอ",
"hostClickHostPage": "เปิดหน้าโฮสต์ เมื่อปลั๊กอินมีหน้าดังกล่าว",
"hostClickFocusExisting": "สลับไปยังแท็บที่เปิดอยู่",
"hostClickFocusExistingDoubleNew": "คลิกเพื่อสลับ ดับเบิลคลิกเพื่อเปิดแท็บใหม่",
"openInNewTab": "เปิดในแท็บใหม่",
@@ -1197,6 +1198,18 @@
"title": "ส่งการแจ้งเตือน",
"consequence": "ส่งถึงคุณหรือผู้ใช้รายอื่น ผ่านช่องทางที่พวกเขาตั้งค่าไว้"
},
"operations:provide": {
"title": "เสนอการดำเนินการ",
"consequence": "การดำเนินการสามารถเรียกใช้ได้จากอัตโนมัติ, CLI, แอปมือถือ และที่อื่นๆ โดยผู้ใช้ที่ได้รับอนุญาต"
},
"operations:invoke": {
"title": "เรียกใช้การดำเนินการของปลั๊กอินอื่น",
"consequence": "ดำเนินการแทนผู้ใช้ที่เข้าสู่ระบบ ตามรายการใน manifest กฎการอนุมัติยังคงบังคับใช้"
},
"operations:approve": {
"title": "กำหนดสิ่งที่ต้องได้รับการอนุมัติ",
"consequence": "อนุญาตให้ดำเนินการโดยไม่ต้องอนุมัติ หรือบล็อกการดำเนินการ สำหรับผู้ใช้ทุกคน"
},
"notify:hub": {
"title": "รับทุกการแจ้งเตือน",
"consequence": "จัดเก็บและส่งต่อการแจ้งเตือนที่ปลั๊กอินทั้งหมดส่งมา รวมถึงเนื้อหาของการแจ้งเตือน"
@@ -1221,6 +1234,10 @@
"title": "จัดการโฮสต์แบบอ่านอย่างเดียว",
"consequence": "โฮสต์ที่จัดการโดยปลั๊กอินนี้ไม่สามารถแก้ไข ลบ หรือแชร์ต่อโดยผู้อื่นได้"
},
"hosts:connect-policy": {
"title": "กำหนดว่าใครสามารถเชื่อมต่อได้",
"consequence": "สามารถบล็อกหรืออนุญาตการเชื่อมต่อ SSH และเดสก์ท็อปรระยะไกลไปยังโฮสต์ใดๆ"
},
"desktop:window": {
"title": "เปิดหน้าต่างเดสก์ท็อป",
"consequence": "เฉพาะในแอปเดสก์ท็อปของ Termix"
@@ -1621,6 +1638,12 @@
"title": "คอลัมน์",
"auto": "อัตโนมัติ",
"count": "กว้าง {{count}} คอลัมน์"
},
"pager": {
"previous": "ก่อนหน้า",
"next": "ถัดไป",
"page": "หน้า {{page}}",
"pageOf": "หน้า {{page}} จาก {{pages}}"
}
},
"fields": {
@@ -1636,5 +1659,29 @@
"admin": "ผู้ดูแลระบบ",
"user": "ผู้ใช้"
}
},
"tileGrid": {
"empty": "ยังไม่มีไทล์ เพิ่มจากลิสต์",
"move": "ลากเพื่อเคลื่อนย้าย",
"moveTile": "เคลื่อนย้าย {{tile}}",
"settings": "การตั้งค่า {{tile}}",
"remove": "ลบ {{tile}}",
"width": "ความกว้าง",
"height": "ความสูง",
"addTiles": "เพิ่มไทล์",
"closePicker": "ปิด",
"search": "ค้นหาไทล์",
"noMatches": "ไม่พบผลลัพธ์",
"kind": {
"all": "ทั้งหมด",
"card": "การ์ด",
"chart": "แผนภูมิ",
"signal": "สถานะ",
"action": "การดำเนินการ",
"resource": "ลิสต์",
"session": "เซสชัน"
},
"reset": "รีเซ็ต",
"done": "เสร็จสิ้น"
}
}
+47
View File
@@ -609,6 +609,7 @@
"hostClickBehavior": "Ana bilgisayara tıklama",
"hostClickBehaviorDesc": "Zaten açık bir sekmesi olan bir ana bilgisayara tıkladığınızda ne olacağını belirler. Ctrl/Cmd + tıklama, orta tıklama veya \"Yeni sekmede aç\" her zaman yeni bir sekme başlatır.",
"hostClickNewTab": "Her zaman yeni bir sekme aç",
"hostClickHostPage": "Bir eklenti sunuyorsa ana bilgisayar sayfasını aç",
"hostClickFocusExisting": "Açık sekmeye geç",
"hostClickFocusExistingDoubleNew": "Geçmek için tıkla, yeni sekme için çift tıkla",
"openInNewTab": "Yeni sekmede aç",
@@ -1214,6 +1215,18 @@
"title": "Uyarılar gönderebilir",
"consequence": "Size veya diğer kullanıcılara ve ayarladıkları kanallar üzerinden."
},
"operations:provide": {
"title": "İşlem sunma",
"consequence": "İzin verilen kişiler tarafından Otomasyonlar, CLI, telefon uygulaması ve diğer yerlerden çalıştırılabilir."
},
"operations:invoke": {
"title": "Diğer eklentilerin işlemlerini çalıştır",
"consequence": "Manifestosunda listelenen işlemlerde oturum açmış kullanıcı adına hareket eder. Onay kuralları hâlâ geçerlidir."
},
"operations:approve": {
"title": "Onaya neyin ihtiyacı olduğunu belirle",
"consequence": "Her kullanıcı için işlemlerin onaysız çalışmasına izin verebilir veya engelleyebilir."
},
"notify:hub": {
"title": "Tüm uyarıları alabilir",
"consequence": "Tüm eklentilerin gönderdiği uyarıları, içerikleri dahil olmak üzere depolar ve iletir."
@@ -1238,6 +1251,10 @@
"title": "Ana bilgisayarları salt okunur olarak yönet",
"consequence": "Yönettiği ana bilgisayarlar başka kimse tarafından düzenlenemez, silinemez veya yeniden paylaşılamaz."
},
"hosts:connect-policy": {
"title": "Bağlanabilen kişileri belirle",
"consequence": "Herhangi bir ana bilgisayara yapılan her SSH ve uzaktan masaüstü bağlantısını engelleyebilir veya izin verebilir."
},
"desktop:window": {
"title": "Masaüstü pencereleri açabilir",
"consequence": "Sadece Termix masaüstü uygulamasında."
@@ -1663,6 +1680,12 @@
"title": "Sütunlar",
"auto": "Otomatik",
"count": "{{count}} geniş"
},
"pager": {
"previous": "Önceki",
"next": "Sonraki",
"page": "{{page}}. sayfa",
"pageOf": "{{pages}} sayfadaki {{page}}. sayfa"
}
},
"fields": {
@@ -1681,5 +1704,29 @@
"admin": "Yönetici",
"user": "Kullanıcı"
}
},
"tileGrid": {
"empty": "Henüz karo yok. Listeden bazıları ekleyin.",
"move": "Taşımak için sürükleyin",
"moveTile": "{{tile}} taşı",
"settings": "{{tile}} ayarları",
"remove": "{{tile}} kaldır",
"width": "Genişlik",
"height": "Yükseklik",
"addTiles": "Karo ekle",
"closePicker": "Kapat",
"search": "Karoları ara",
"noMatches": "Hiçbir şey eşleşmiyor.",
"kind": {
"all": "Tümü",
"card": "Kartlar",
"chart": "Grafikler",
"signal": "Durum",
"action": "İşlemler",
"resource": "Listeler",
"session": "Oturumlar"
},
"reset": "Sıfırla",
"done": "Tamam"
}
}
+47
View File
@@ -621,6 +621,7 @@
"hostClickBehavior": "Клік по хосту",
"hostClickBehaviorDesc": "Що відбувається, коли ви клікаєте на хост, для якого вже відкрито вкладку. Ctrl/Cmd + клік, клік середньою кнопкою або «Відкрити у новій вкладці» завжди відкривають нову вкладку.",
"hostClickNewTab": "Завжди відкривати нову вкладку",
"hostClickHostPage": "Відкрити сторінку хоста, якщо плагін її надає",
"hostClickFocusExisting": "Перемикатися на відкриту вкладку",
"hostClickFocusExistingDoubleNew": "Клік для перемикання, подвійний клік для нової вкладки",
"openInNewTab": "Відкрити у новій вкладці",
@@ -1248,6 +1249,18 @@
"title": "Надсилання сповіщень",
"consequence": "Вам або іншим користувачам через налаштовані ними канали."
},
"operations:provide": {
"title": "Надавати дії",
"consequence": "Їхні дії можна виконувати з автоматизацій, командного рядка, мобільного додатка та інших місць користувачам, яким це дозволено."
},
"operations:invoke": {
"title": "Виконувати дії інших плагінів",
"consequence": "Діє від імені увійшовшого користувача для дій, перелічених у його маніфесті. Правила схвалення все ще застосовуються."
},
"operations:approve": {
"title": "Визначати, що потребує схвалення",
"consequence": "Може дозволяти виконувати дії без схвалення або блокувати їх для кожного користувача."
},
"notify:hub": {
"title": "Отримання всіх сповіщень",
"consequence": "Зберігає та доставляє всі сповіщення, надіслані плагінами, включаючи їхній вміст."
@@ -1272,6 +1285,10 @@
"title": "Керування хостами в режимі «тільки для читання»",
"consequence": "Хости, якими він керує, не можна редагувати, видаляти або повторно поширювати іншим користувачам."
},
"hosts:connect-policy": {
"title": "Визначати, хто може підключатися",
"consequence": "Може блокувати або дозволяти будь-які підключення SSH та віддаленого робочого столу до будь-якого хоста."
},
"desktop:window": {
"title": "Відкриття вікон робочого столу",
"consequence": "Тільки в десктопному додатку Termix."
@@ -1747,6 +1764,12 @@
"title": "Стовпці",
"auto": "Авто",
"count": "{{count}} стовпців"
},
"pager": {
"previous": "Назад",
"next": "Далі",
"page": "Сторінка {{page}}",
"pageOf": "Сторінка {{page}} з {{pages}}"
}
},
"fields": {
@@ -1771,5 +1794,29 @@
"admin": "Адміністратор",
"user": "Користувач"
}
},
"tileGrid": {
"empty": "Ще немає плиток. Додайте їх зі списку.",
"move": "Перетягніть для переміщення",
"moveTile": "Перемістити {{tile}}",
"settings": "Налаштування {{tile}}",
"remove": "Видалити {{tile}}",
"width": "Ширина",
"height": "Висота",
"addTiles": "Додати плитки",
"closePicker": "Закрити",
"search": "Пошук плиток",
"noMatches": "Нічого не знайдено.",
"kind": {
"all": "Усі",
"card": "Картки",
"chart": "Діаграми",
"signal": "Статус",
"action": "Дії",
"resource": "Списки",
"session": "Сесії"
},
"reset": "Скинути",
"done": "Готово"
}
}
+47
View File
@@ -603,6 +603,7 @@
"hostClickBehavior": "Khi nhấp vào host",
"hostClickBehaviorDesc": "Điều xảy ra khi bạn nhấp vào một host đã có tab đang mở. Nhấp Ctrl/Cmd, nhấp chuột giữa hoặc \"Mở trong tab mới\" luôn luôn mở một tab mới.",
"hostClickNewTab": "Luôn mở tab mới",
"hostClickHostPage": "Mở trang chủ cơ, nếu plugin cung cấp",
"hostClickFocusExisting": "Chuyển sang tab đang mở",
"hostClickFocusExistingDoubleNew": "Nhấp để chuyển, nhấp đúp để mở tab mới",
"openInNewTab": "Mở trong tab mới",
@@ -1197,6 +1198,18 @@
"title": "Gửi cảnh báo",
"consequence": "Đến bạn hoặc người dùng khác, và qua các kênh họ đã thiết lập."
},
"operations:provide": {
"title": "Cung cấp thao tác",
"consequence": "Các thao tác của nó có thể được chạy từ Tự động hóa, CLI, ứng dụng điện thoại và các nơi khác, bởi những người được phép sử dụng."
},
"operations:invoke": {
"title": "Chạy thao tác của plugin khác",
"consequence": "Thay mặt người dùng đang đăng nhập thực hiện các thao tác được liệt kê trong manifest của plugin. Quy tắc phê duyệt vẫn được áp dụng."
},
"operations:approve": {
"title": "Quyết định thao tác nào cần phê duyệt",
"consequence": "Có thể cho phép thao tác chạy mà không cần phê duyệt, hoặc chặn chúng, đối với mọi người dùng."
},
"notify:hub": {
"title": "Nhận mọi cảnh báo",
"consequence": "Lưu trữ và phân phối các cảnh báo mà tất cả plugin gửi, bao gồm cả nội dung của chúng."
@@ -1221,6 +1234,10 @@
"title": "Quản lý máy chủ ở chế độ chỉ đọc",
"consequence": "Các máy chủ do nó quản lý không thể bị chỉnh sửa, xóa hoặc chia sẻ lại bởi bất kỳ ai khác."
},
"hosts:connect-policy": {
"title": "Quyết định ai có thể kết nối",
"consequence": "Có thể chặn hoặc cho phép mọi kết nối SSH và máy tính từ xa đến bất kỳ host nào."
},
"desktop:window": {
"title": "Mở cửa sổ máy tính để bàn",
"consequence": "Chỉ trong ứng dụng máy tính để bàn Termix."
@@ -1621,6 +1638,12 @@
"title": "Cột",
"auto": "Tự động",
"count": "Rộng {{count}}"
},
"pager": {
"previous": "Trước",
"next": "Tiếp",
"page": "Trang {{page}}",
"pageOf": "Trang {{page}} trong tổng số {{pages}}"
}
},
"fields": {
@@ -1636,5 +1659,29 @@
"admin": "Quản trị viên",
"user": "Người dùng"
}
},
"tileGrid": {
"empty": "Chưa có ô nào. Thêm một số từ danh sách.",
"move": "Kéo để di chuyển",
"moveTile": "Di chuyển {{tile}}",
"settings": "Cài đặt {{tile}}",
"remove": "Xóa {{tile}}",
"width": "Chiều rộng",
"height": "Chiều cao",
"addTiles": "Thêm ô",
"closePicker": "Đóng",
"search": "Tìm kiếm ô",
"noMatches": "Không có kết quả phù hợp.",
"kind": {
"all": "Tất cả",
"card": "Thẻ",
"chart": "Biểu đồ",
"signal": "Trạng thái",
"action": "Thao tác",
"resource": "Danh sách",
"session": "Phiên làm việc"
},
"reset": "Đặt lại",
"done": "Xong"
}
}
+47
View File
@@ -603,6 +603,7 @@
"hostClickBehavior": "点击主机",
"hostClickBehaviorDesc": "点击已打开标签页的主机时的行为。Ctrl/Cmd 点击、中键点击或“在新标签页中打开”始终会打开新标签页。",
"hostClickNewTab": "始终打开新标签页",
"hostClickHostPage": "当插件提供时,打开主机页面",
"hostClickFocusExisting": "切换到已打开的标签页",
"hostClickFocusExistingDoubleNew": "单击切换,双击打开新标签页",
"openInNewTab": "在新标签页中打开",
@@ -1197,6 +1198,18 @@
"title": "发送警报",
"consequence": "发送给您或其他用户,并通过他们设置的渠道发送。"
},
"operations:provide": {
"title": "提供操作",
"consequence": "其操作可由自动化、CLI、手机应用及其他位置中获授权的用户执行。"
},
"operations:invoke": {
"title": "运行其他插件的操作",
"consequence": "代表已登录用户执行其清单中列出的操作。审批规则仍然适用。"
},
"operations:approve": {
"title": "决定哪些操作需要审批",
"consequence": "可为所有用户允许操作无需审批直接运行,或阻止它们。"
},
"notify:hub": {
"title": "接收所有警报",
"consequence": "存储并分发所有插件发送的警报,包括其内容。"
@@ -1221,6 +1234,10 @@
"title": "以只读方式管理主机",
"consequence": "由其管理的主机无法被其他人编辑、删除或重新共享。"
},
"hosts:connect-policy": {
"title": "决定谁可以连接",
"consequence": "可阻止或允许对任何主机的所有 SSH 和远程桌面连接。"
},
"desktop:window": {
"title": "打开桌面窗口",
"consequence": "仅在 Termix 桌面应用中。"
@@ -1621,6 +1638,12 @@
"title": "列数",
"auto": "自动",
"count": "{{count}} 列宽"
},
"pager": {
"previous": "上一页",
"next": "下一页",
"page": "第 {{page}} 页",
"pageOf": "第 {{page}} 页,共 {{pages}} 页"
}
},
"fields": {
@@ -1636,5 +1659,29 @@
"admin": "管理员",
"user": "用户"
}
},
"tileGrid": {
"empty": "暂无卡片。从列表中添加一些。",
"move": "拖拽移动",
"moveTile": "移动 {{tile}}",
"settings": "{{tile}} 设置",
"remove": "移除 {{tile}}",
"width": "宽度",
"height": "高度",
"addTiles": "添加卡片",
"closePicker": "关闭",
"search": "搜索卡片",
"noMatches": "无匹配结果。",
"kind": {
"all": "全部",
"card": "卡片",
"chart": "图表",
"signal": "状态",
"action": "操作",
"resource": "列表",
"session": "会话"
},
"reset": "重置",
"done": "完成"
}
}
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff. Show more