mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-27 21:55:24 +00:00
v26.10.5 (#1710)
* feat: add an auto or fixed columns picker to card grids * feat: add safe mode that only runs layout and theme plugins * docs: add simple layout to plugin table * fix: ask for local network access in the macOS app * feat: allow plugin registries on listed private hosts (#1705) * chore: add private registry hosts to the changelog * feat: use the proxy for plugin registry downloads * fix: allow changing connection origin on shared hosts * chore: update translations and plugin pins * chore: lint, format, and bump version to 26.10.5 --------- Co-authored-by: Lukas <80832688+0skater0@users.noreply.github.com>
This commit is contained in:
1 parent
7b7b17e31f
commit
96b8520fb4
96 files changed
+1885
-474
No files matched your search
@@ -41,9 +41,15 @@ on:
|
||||
default: ""
|
||||
|
||||
jobs:
|
||||
# One image per plugin profile (docker/bundled-plugins.json). standard keeps
|
||||
# the plain tags; minimal and full add a suffix.
|
||||
build:
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
profile: [standard, minimal, full]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
@@ -80,12 +86,17 @@ jobs:
|
||||
TAGS=()
|
||||
ALL_TAGS=()
|
||||
|
||||
SUFFIX=""
|
||||
if [ "${{ matrix.profile }}" != "standard" ]; then
|
||||
SUFFIX="-${{ matrix.profile }}"
|
||||
fi
|
||||
|
||||
if [ "$BUILD_TYPE" = "Production" ]; then
|
||||
TAGS+=("$VERSION" "latest")
|
||||
TAGS+=("$VERSION$SUFFIX" "latest$SUFFIX")
|
||||
elif [ "$BUILD_TYPE" = "Beta" ]; then
|
||||
TAGS+=("beta" "$VERSION")
|
||||
TAGS+=("beta$SUFFIX" "$VERSION$SUFFIX")
|
||||
else
|
||||
TAGS+=("dev-$VERSION")
|
||||
TAGS+=("dev-$VERSION$SUFFIX")
|
||||
fi
|
||||
|
||||
for tag in "${TAGS[@]}"; do
|
||||
@@ -124,6 +135,7 @@ jobs:
|
||||
tags: ${{ env.ALL_TAGS }}
|
||||
build-args: |
|
||||
BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
|
||||
PLUGIN_PROFILE=${{ matrix.profile }}
|
||||
labels: |
|
||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
org.opencontainers.image.revision=${{ env.SOURCE_SHA }}
|
||||
|
||||
@@ -161,9 +161,10 @@ jobs:
|
||||
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "prev_ref=$PREV" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Translates new or changed strings with the Termix LLM server and commits
|
||||
# them to the dev branch, so the beta or stable build includes them. If the
|
||||
# server is down it warns and the release goes on with what is there.
|
||||
# Translates new or changed strings with the Termix LLM server and pins the
|
||||
# bundled plugins to their newest registry releases, then commits both to the
|
||||
# dev branch so the beta or stable build includes them. If the server is down
|
||||
# it warns and the release goes on with what is there.
|
||||
translate:
|
||||
needs: [prep]
|
||||
if: ${{ needs.prep.outputs.skip != 'true' && needs.prep.outputs.mode != 'Overwrite release' }}
|
||||
@@ -191,19 +192,26 @@ jobs:
|
||||
languages: src/ui/locales/languages.json
|
||||
api-key: ${{ secrets.TERMIX_LLM_API_KEY }}
|
||||
|
||||
- name: Commit translations
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
# Newest stable plugin release whose engine.termix allows this version.
|
||||
- name: Pin bundled plugins
|
||||
run: node scripts/pin-bundled-plugins.cjs --all --core-version "${{ needs.prep.outputs.version }}"
|
||||
|
||||
- name: Commit translations and plugin pins
|
||||
id: commit
|
||||
run: |
|
||||
git add src/ui/locales
|
||||
git add src/ui/locales docker/bundled-plugins.json
|
||||
if git diff --cached --quiet; then
|
||||
echo "No translation changes."
|
||||
elif [ "${{ needs.prep.outputs.mode }}" = "Dry run" ]; then
|
||||
echo "DRY RUN: would commit these translation changes:"
|
||||
echo "DRY RUN: would commit these translation and pin changes:"
|
||||
git --no-pager diff --cached --stat
|
||||
else
|
||||
git config user.name "LukeGus"
|
||||
git config user.email "bugattiguy527@gmail.com"
|
||||
git commit -m "chore: update translations"
|
||||
git commit -m "chore: update translations and plugin pins"
|
||||
git push origin HEAD:"${{ needs.prep.outputs.dev_branch }}"
|
||||
fi
|
||||
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
## 26.10.5
|
||||
|
||||
### Added
|
||||
|
||||
- Safe mode: start the server with TERMIX_SAFE_MODE=1 to run only layout and theme plugins, so a broken plugin can be turned off
|
||||
- TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS lets plugin registries live on a private address, like a registry in your LAN
|
||||
- Plugin registry browsing and downloads use HTTP_PROXY and HTTPS_PROXY when they are set
|
||||
- Docker image sizes: the default image now ships the most used plugins, the minimal tag ships only the look plugins and the full tag ships every official plugin
|
||||
- Plugins an older image shipped and you had on are downloaded from the registry on the first start of a smaller image, so nothing goes missing
|
||||
|
||||
### Changed
|
||||
|
||||
- Card grids in the Plugins tab and in plugins can show Auto (as many as fit) or a set number of cards per row
|
||||
|
||||
### Fixed
|
||||
|
||||
- The macOS app asks for Local Network access, so it can reach servers on your LAN
|
||||
- The desktop app can change the connection origin of a host shared with you, instead of failing to save
|
||||
|
||||
## 26.10.4
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -93,6 +93,7 @@ Plugins add everything else. The official ones ship with Termix, and admins can
|
||||
| [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 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 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. |
|
||||
|
||||
+15
-4
@@ -34,9 +34,20 @@ WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build:backend:core
|
||||
|
||||
# Stage 3b: Stage the bundled plugins for one profile (minimal, standard or
|
||||
# full, see docker/bundled-plugins.json). Only this stage and the final one
|
||||
# differ between profiles, so the other stages are built once.
|
||||
FROM deps AS plugins
|
||||
WORKDIR /app
|
||||
ARG PLUGIN_PROFILE=standard
|
||||
COPY scripts/build-plugins.cjs ./scripts/build-plugins.cjs
|
||||
COPY scripts/lib/bundled-plugins.cjs ./scripts/lib/bundled-plugins.cjs
|
||||
COPY docker/bundled-plugins.json ./docker/bundled-plugins.json
|
||||
# Bundled plugin downloads show in the plugin store, so keep them between builds.
|
||||
RUN --mount=type=cache,target=/app/node_modules/.cache/termix-bundled-plugins \
|
||||
npm run build:backend
|
||||
node scripts/build-plugins.cjs --profile "$PLUGIN_PROFILE"
|
||||
|
||||
# Stage 4: Download OPKSSH binary for the target platform so the image works offline
|
||||
FROM node:24-slim AS opkssh-downloader
|
||||
@@ -117,9 +128,9 @@ COPY --chown=node:node --from=frontend-builder /app/dist /app/html
|
||||
|
||||
COPY --chown=node:node --from=production-deps /app/node_modules /app/node_modules
|
||||
COPY --chown=node:node --from=backend-builder /app/dist/backend ./dist/backend
|
||||
# Plugins pinned in docker/bundled-plugins.json, resolved as dist/plugins by
|
||||
# getBundledPluginsDir().
|
||||
COPY --chown=node:node --from=backend-builder /app/dist/plugins ./dist/plugins
|
||||
# Plugins of the chosen profile pinned in docker/bundled-plugins.json, resolved
|
||||
# as dist/plugins by getBundledPluginsDir().
|
||||
COPY --chown=node:node --from=plugins /app/dist/plugins ./dist/plugins
|
||||
# Target of node_modules/@termix-ssh/plugin-sdk, which core and the plugins import.
|
||||
COPY --chown=node:node --from=backend-builder /app/packages/plugin-sdk/package.json ./packages/plugin-sdk/package.json
|
||||
COPY --chown=node:node --from=backend-builder /app/packages/plugin-sdk/dist ./packages/plugin-sdk/dist
|
||||
|
||||
+152
-60
@@ -3,20 +3,23 @@
|
||||
{
|
||||
"id": "acme-ssl",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-ACME-SSL/releases/download/v1.0.2/acme-ssl-1.0.2.tmxplug",
|
||||
"sha256": "157d0f96dc42136ff64e618b6904db7862079cd31c1f1cf5f19846b1d127b00f"
|
||||
"url": "https://github.com/Termix-SSH/Plugin-ACME-SSL/releases/download/v1.1.0/acme-ssl-1.1.0.tmxplug",
|
||||
"sha256": "fb75541fae110007b45ed69d58aa4b9b6f6a79b6b1b762c6e54b635aa7cb6e4a",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "ai",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-AI/releases/download/v1.2.0/ai-1.2.0.tmxplug",
|
||||
"sha256": "3b486133af78ae5168e6f5b8a4c71f3db3403cfdae537ed1ca8d6cc16035d8dd"
|
||||
"url": "https://github.com/Termix-SSH/Plugin-AI/releases/download/v1.3.0/ai-1.3.0.tmxplug",
|
||||
"sha256": "87ad17324cec6036dc224135fee342250c37413dccb2963f5f6b554d4ab8f5a6",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "alerts",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Alerts/releases/download/v1.0.2/alerts-1.0.2.tmxplug",
|
||||
"sha256": "50768e77019276343a5d162c85d081659fb89d1cc3733ac45cd0db4436918faa",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Alerts/releases/download/v1.1.0/alerts-1.1.0.tmxplug",
|
||||
"sha256": "a7ce9e20ce3c15f77c15479b4901563adf3c793e24a3aacd7226950491f0434c",
|
||||
"tier": "standard",
|
||||
"onboarding": {
|
||||
"recommended": true,
|
||||
"consent": false
|
||||
@@ -26,19 +29,22 @@
|
||||
"id": "audit-tools",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Audit-Tools/releases/download/v1.0.0/audit-tools-1.0.0.tmxplug",
|
||||
"sha256": "131479c294d692b1c0ed4d6cec273730a3a67a28ecd44825ffd36bade9aa863b"
|
||||
"sha256": "131479c294d692b1c0ed4d6cec273730a3a67a28ecd44825ffd36bade9aa863b",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "automations",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Automations/releases/download/v1.0.3/automations-1.0.3.tmxplug",
|
||||
"sha256": "b5e5b39a053010b72ad1ba03ea6bb05afbbb2b789a86e0e98634d9ada543d02d"
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Automations/releases/download/v1.2.0/automations-1.2.0.tmxplug",
|
||||
"sha256": "2ec9c718d73a094be3b59121aea049f0237e5783f53d193c4ea24a524584aeea",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "classic-layout",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Classic-Layout/releases/download/v1.0.1/classic-layout-1.0.1.tmxplug",
|
||||
"sha256": "6b2d9acd77ef4b33fa0761c1aea7ed3c6171c325ee8febcedff94b5b2bc7be34",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Classic-Layout/releases/download/v1.0.2/classic-layout-1.0.2.tmxplug",
|
||||
"sha256": "cf54f9b2e81c33b7e58cd3a21e203805f08ba37b3bc78412a08eb21b9fef8f14",
|
||||
"tier": "minimal",
|
||||
"defaultLook": true
|
||||
},
|
||||
{
|
||||
@@ -46,19 +52,36 @@
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Classic-Themes/releases/download/v1.0.0/classic-themes-1.0.0.tmxplug",
|
||||
"sha256": "919283f95701d41727c826637a7d349bd6a646abd61f9ac9017149bd0bee782c",
|
||||
"tier": "minimal",
|
||||
"defaultLook": true
|
||||
},
|
||||
{
|
||||
"id": "cloud-providers",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Cloud-Providers/releases/download/v1.1.0/cloud-providers-1.1.0.tmxplug",
|
||||
"sha256": "7f61eafc3b99bdf1328fffa95791db1d27f8e1b683e5a5925603b5840057a9c7",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "credential-tools",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Credential-Tools/releases/download/v1.0.0/credential-tools-1.0.0.tmxplug",
|
||||
"sha256": "413eff710b5c832a2575a890bbab1c90d28ea55f10cd7a7b94b851d51dd77dce"
|
||||
"sha256": "413eff710b5c832a2575a890bbab1c90d28ea55f10cd7a7b94b851d51dd77dce",
|
||||
"tier": "standard"
|
||||
},
|
||||
{
|
||||
"id": "databases",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Databases/releases/download/v1.1.0/databases-1.1.0.tmxplug",
|
||||
"sha256": "4d9504cde705688a6f1f7706139d4e4c49b2276eccc67538fdee8251f29920b8",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "docker",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Docker/releases/download/v1.1.0/docker-1.1.0.tmxplug",
|
||||
"sha256": "da32dfbe9d4cffec5d80c4a1037a79e7591a3d16c50f882db6a146322a50c2b7",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Docker/releases/download/v1.2.1/docker-1.2.1.tmxplug",
|
||||
"sha256": "b2ca797465d88cfb549a04c3f0161708669def22a5ca73cb107879847c2db8ff",
|
||||
"tier": "standard",
|
||||
"onboarding": {
|
||||
"recommended": true,
|
||||
"consent": false
|
||||
@@ -67,8 +90,9 @@
|
||||
{
|
||||
"id": "file-manager",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-File-Manager/releases/download/v1.1.0/file-manager-1.1.0.tmxplug",
|
||||
"sha256": "807e9ffa80d6054a5911522721a555e0daa3e50f6edffdebe97efe7c91aea7a2",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-File-Manager/releases/download/v1.1.1/file-manager-1.1.1.tmxplug",
|
||||
"sha256": "ebba0b7d14facb3354f22a00e059e9f5a69e233458cb24985bf4ed5e5f8d2abd",
|
||||
"tier": "standard",
|
||||
"onboarding": {
|
||||
"recommended": true,
|
||||
"consent": false
|
||||
@@ -78,19 +102,22 @@
|
||||
"id": "fleets",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Fleets/releases/download/v1.0.2/fleets-1.0.2.tmxplug",
|
||||
"sha256": "50dab38e1c6c95fdb4d0544ce50cdf84dae84789e64aa34ea9d04fdac3a38c59"
|
||||
"sha256": "50dab38e1c6c95fdb4d0544ce50cdf84dae84789e64aa34ea9d04fdac3a38c59",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "homepage",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Homepage/releases/download/v1.1.0/homepage-1.1.0.tmxplug",
|
||||
"sha256": "5a9d5d2cc337fcee5590ea957e35ddeea5a71596aeb59410bf1420065b9b5b00"
|
||||
"sha256": "5a9d5d2cc337fcee5590ea957e35ddeea5a71596aeb59410bf1420065b9b5b00",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "host-metrics",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Host-Metrics/releases/download/v1.1.0/host-metrics-1.1.0.tmxplug",
|
||||
"sha256": "4f1d422ec1d0565c79f94fa814ee0864d556d7367793b11035e68239e49e297f",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Host-Metrics/releases/download/v1.2.0/host-metrics-1.2.0.tmxplug",
|
||||
"sha256": "8522cd23b558c3209821cee6d177daeb80f439b8ff652c577c5e4955315cc7a3",
|
||||
"tier": "standard",
|
||||
"onboarding": {
|
||||
"recommended": true,
|
||||
"consent": false
|
||||
@@ -100,93 +127,142 @@
|
||||
"id": "host-status",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Host-Status/releases/download/v1.0.0/host-status-1.0.0.tmxplug",
|
||||
"sha256": "1ba62577dc7373a2a2787bab7efdb89b544a30a8003b1de91ce51f372fc4f170"
|
||||
"sha256": "1ba62577dc7373a2a2787bab7efdb89b544a30a8003b1de91ce51f372fc4f170",
|
||||
"tier": "standard"
|
||||
},
|
||||
{
|
||||
"id": "hosts-config",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Hosts-Config/releases/download/v1.0.0/hosts-config-1.0.0.tmxplug",
|
||||
"sha256": "60a2b2a6d9ba13bad12a511c93af70a30a10fbc560066b6419acf533d806a3ad"
|
||||
"sha256": "60a2b2a6d9ba13bad12a511c93af70a30a10fbc560066b6419acf533d806a3ad",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "import-export",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Import-Export/releases/download/v1.0.0/import-export-1.0.0.tmxplug",
|
||||
"sha256": "6670b6c147e81eed0ddfd249f9a7c9564f1ab50e45f2771161d2e30c47f6fd58"
|
||||
"sha256": "6670b6c147e81eed0ddfd249f9a7c9564f1ab50e45f2771161d2e30c47f6fd58",
|
||||
"tier": "standard"
|
||||
},
|
||||
{
|
||||
"id": "kubernetes",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Kubernetes/releases/download/v1.0.1/kubernetes-1.0.1.tmxplug",
|
||||
"sha256": "31d6b200579d326d07244712713ae291ff473270d44b478a884d07c863a394f0",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "ldap",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-LDAP/releases/download/v1.0.2/ldap-1.0.2.tmxplug",
|
||||
"sha256": "9ef269ad281f1c68cea53e0d43ccb7a5d4f949f11b30b22eac3d01047b20eb03"
|
||||
"sha256": "9ef269ad281f1c68cea53e0d43ccb7a5d4f949f11b30b22eac3d01047b20eb03",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "login-security",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Login-Security/releases/download/v1.0.0/login-security-1.0.0.tmxplug",
|
||||
"sha256": "b21dff5722e72a02022d563ef27130e8b56a53e273f1668fade4ec7d030374aa"
|
||||
"sha256": "b21dff5722e72a02022d563ef27130e8b56a53e273f1668fade4ec7d030374aa",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "network-topology",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Network-Topology/releases/download/v1.0.2/network-topology-1.0.2.tmxplug",
|
||||
"sha256": "8d32340a0d0f9674b872da5d3d53e90f529b80dfe0d73ab294eed00e565fbb56"
|
||||
"sha256": "8d32340a0d0f9674b872da5d3d53e90f529b80dfe0d73ab294eed00e565fbb56",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "opkssh",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-OPKSSH/releases/download/v1.0.2/opkssh-1.0.2.tmxplug",
|
||||
"sha256": "4bb8f1ac5d39bd18ac3d0fe82ddbf2f26ab4c01bd75e6f7a073d65c2e9aef219"
|
||||
"sha256": "4bb8f1ac5d39bd18ac3d0fe82ddbf2f26ab4c01bd75e6f7a073d65c2e9aef219",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "proxmox",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Proxmox/releases/download/v1.0.2/proxmox-1.0.2.tmxplug",
|
||||
"sha256": "b92be2644b0c8121732103b922069a6cb3cf00caad4e6549f7a8cb471bda5e4e"
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Proxmox/releases/download/v1.1.0/proxmox-1.1.0.tmxplug",
|
||||
"sha256": "8937d239ad40033d69fba59085fe8896ee25c61951c21f66ff98531fd4c63cbf",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "remote-desktop",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Remote-Desktop/releases/download/v1.2.0/remote-desktop-1.2.0.tmxplug",
|
||||
"sha256": "0de40896bfeb4b1a702c3ce815355b7b7b0bb4bfe424f33e905bb5a0209d02f6",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Remote-Desktop/releases/download/v1.2.1/remote-desktop-1.2.1.tmxplug",
|
||||
"sha256": "ad244f31016f0ea5df5c303bf6f48af35c6c8e52b12098d381c8296626b73de7",
|
||||
"tier": "standard",
|
||||
"onboarding": {
|
||||
"recommended": true,
|
||||
"consent": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "reverse-proxy",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Reverse-Proxy/releases/download/v1.1.0/reverse-proxy-1.1.0.tmxplug",
|
||||
"sha256": "c8a9851da7ca6291c1f47cf6ab235da5057455a86b4ed0dc1c1a3026afb30aa7",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "samba",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Samba/releases/download/v1.0.0/samba-1.0.0.tmxplug",
|
||||
"sha256": "6e131ccdca998a9d0802becdd439a88305b64fcd7f07335bec9c7ed6791c0f53",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "secret-sources",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Secret-Sources/releases/download/v1.0.2/secret-sources-1.0.2.tmxplug",
|
||||
"sha256": "82e2b1e95b0837dd3e5fee277be4eea2d1ce68567b0e204941c48150f415d134"
|
||||
"sha256": "82e2b1e95b0837dd3e5fee277be4eea2d1ce68567b0e204941c48150f415d134",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "serial",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Serial/releases/download/v1.0.2/serial-1.0.2.tmxplug",
|
||||
"sha256": "6ede7e8726a7312429473f34a46f93ba6657c5b7ebc12bed18f80ec62c211715",
|
||||
"tier": "standard",
|
||||
"onboarding": {
|
||||
"recommended": "desktop",
|
||||
"consent": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "service-manager",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Service-Manager/releases/download/v1.1.0/service-manager-1.1.0.tmxplug",
|
||||
"sha256": "82b1c70abd97ed535f217535c667fe6c48e5e01e2bf8037a89b58b80d35206bf",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "session-recording",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Session-Recording/releases/download/v1.0.2/session-recording-1.0.2.tmxplug",
|
||||
"sha256": "55ace6897710ce9a63f24e3d736a9e533cac557be0f3d98974956c94cf417d1c"
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Session-Recording/releases/download/v1.1.0/session-recording-1.1.0.tmxplug",
|
||||
"sha256": "31cd78d67e061242f8776849a2fae5f84ab48c7c765d487b32c5e1693c54b46a",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "session-sharing",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Session-Sharing/releases/download/v1.0.2/session-sharing-1.0.2.tmxplug",
|
||||
"sha256": "6408e7949a9549ac5ec94b35205729099138e730df1598d50f4bb618d8d90804"
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Session-Sharing/releases/download/v1.1.0/session-sharing-1.1.0.tmxplug",
|
||||
"sha256": "2988b39221d93b3dbc5e2a81e104e5dc01ce264afb1e66df3407492658ab9fdc",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "simple-layout",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Simple-Layout/releases/download/v1.0.1/simple-layout-1.0.1.tmxplug",
|
||||
"sha256": "838f08050335b44b62295b87006b50d1db1029699bfe6a35fa75808d4aea2cde",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "snippets",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Snippets/releases/download/v1.0.2/snippets-1.0.2.tmxplug",
|
||||
"sha256": "cc1f5d74cf97f9a9c8f800fccf481c8a000e6fa516bc260ce24a7249d4f4c8e5",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Snippets/releases/download/v1.1.0/snippets-1.1.0.tmxplug",
|
||||
"sha256": "e2d7d1ebe5bdf2986ffb4961ac6f6d32b392b02c30a64a333a7eeb2ce6a46bb3",
|
||||
"tier": "standard",
|
||||
"onboarding": {
|
||||
"recommended": true,
|
||||
"consent": false
|
||||
@@ -195,8 +271,9 @@
|
||||
{
|
||||
"id": "ssh-terminal",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-SSH-Terminal/releases/download/v1.1.0/ssh-terminal-1.1.0.tmxplug",
|
||||
"sha256": "dacdbadb3c6a0d7014b1de4f62b499f80abf358c7799a6e6326674660b055e4a",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-SSH-Terminal/releases/download/v1.1.1/ssh-terminal-1.1.1.tmxplug",
|
||||
"sha256": "5cfb381a2de10b7a6fab108d79645bb1e6e7316fb5e4150c4529a3e76a2193d8",
|
||||
"tier": "standard",
|
||||
"onboarding": {
|
||||
"recommended": true,
|
||||
"consent": false
|
||||
@@ -205,8 +282,9 @@
|
||||
{
|
||||
"id": "sso",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-SSO/releases/download/v1.0.3/sso-1.0.3.tmxplug",
|
||||
"sha256": "87b03c0789d981dbc806162d73dfcc2c624150375277efd2ae40ab9d7657d48e",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-SSO/releases/download/v1.0.4/sso-1.0.4.tmxplug",
|
||||
"sha256": "2aad33177085fbc92fe06cd007facffbfc8f42f9c9fa688ad5f37d68f62093d9",
|
||||
"tier": "standard",
|
||||
"onboarding": {
|
||||
"recommended": false,
|
||||
"consent": false,
|
||||
@@ -217,25 +295,29 @@
|
||||
"id": "step-ca",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Step-CA/releases/download/v1.0.2/step-ca-1.0.2.tmxplug",
|
||||
"sha256": "685b644fe44c2d3fa007baee16e8ea8a7d5204f039ebc8aa65e8093ad93adf8a"
|
||||
"sha256": "685b644fe44c2d3fa007baee16e8ea8a7d5204f039ebc8aa65e8093ad93adf8a",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "sync",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Sync/releases/download/v1.0.0/sync-1.0.0.tmxplug",
|
||||
"sha256": "12ced05523f7c1a9151b5e680726bd31f53888e8bb0047d7bd12a2570ea168bf"
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Sync/releases/download/v1.0.1/sync-1.0.1.tmxplug",
|
||||
"sha256": "66a301fd04fb8d2a3ece18dfb230069cf78e1138ee78941dc511dfbd8ee4f91e",
|
||||
"tier": "standard"
|
||||
},
|
||||
{
|
||||
"id": "tailscale",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Tailscale/releases/download/v1.0.2/tailscale-1.0.2.tmxplug",
|
||||
"sha256": "5c7b00d6b6b0186d833302e39a9d31b92ea92d4542e9e2dab550b95837a59d97"
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Tailscale/releases/download/v1.1.0/tailscale-1.1.0.tmxplug",
|
||||
"sha256": "8784a77a1fc1b0011d8243d9520574300df31bc40c31f86339868f4c9b76d1bf",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "telemetry",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Telemetry/releases/download/v1.0.2/telemetry-1.0.2.tmxplug",
|
||||
"sha256": "5ffdc58b05cd8b37fd0a207f99d2d3a256164130c950032501c499f869edb9ec",
|
||||
"tier": "standard",
|
||||
"onboarding": {
|
||||
"recommended": true,
|
||||
"consent": true
|
||||
@@ -245,19 +327,22 @@
|
||||
"id": "termix-identity",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Termix-Identity/releases/download/v1.0.2/termix-identity-1.0.2.tmxplug",
|
||||
"sha256": "ac6c90a48b4f1e9cff6773a5fd267b2bf7319d2fc7e2aa2cd4004cec82f0f927"
|
||||
"sha256": "ac6c90a48b4f1e9cff6773a5fd267b2bf7319d2fc7e2aa2cd4004cec82f0f927",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "tmux-monitor",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Tmux-Monitor/releases/download/v1.0.4/tmux-monitor-1.0.4.tmxplug",
|
||||
"sha256": "42b8a2284e68a321c1736984f5c641b1857c87c6a65ce8cf694c6bc946de2444"
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Tmux-Monitor/releases/download/v1.1.0/tmux-monitor-1.1.0.tmxplug",
|
||||
"sha256": "3cdc54a34e1ca30c4e3c217bbc612e40ef3cd3e4d5de704ea92d914b32d98006",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "totp",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-TOTP/releases/download/v1.0.2/totp-1.0.2.tmxplug",
|
||||
"sha256": "94a0772660fa8e4dfd9afc15b68d5bc9fe02fe28a1acfd35e65a57c304d460c0",
|
||||
"tier": "standard",
|
||||
"onboarding": {
|
||||
"recommended": true,
|
||||
"consent": false
|
||||
@@ -266,8 +351,9 @@
|
||||
{
|
||||
"id": "tunnels",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Tunnels/releases/download/v1.1.0/tunnels-1.1.0.tmxplug",
|
||||
"sha256": "b1e4f1b6dd2bbd15a9a240ae1c1d381111c3609258c8946d19493a83f861ea1e",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Tunnels/releases/download/v1.2.1/tunnels-1.2.1.tmxplug",
|
||||
"sha256": "2871a8d31854e1a31416c4afe5839099a4c489e34d9d31573659f73b2515f159",
|
||||
"tier": "standard",
|
||||
"onboarding": {
|
||||
"recommended": true,
|
||||
"consent": false
|
||||
@@ -277,31 +363,36 @@
|
||||
"id": "vault",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Vault/releases/download/v1.0.2/vault-1.0.2.tmxplug",
|
||||
"sha256": "48ef402d030912a836489edf0fdfcaf7929cbd67c21f16a2079cb4925134cc28"
|
||||
"sha256": "48ef402d030912a836489edf0fdfcaf7929cbd67c21f16a2079cb4925134cc28",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "wake-on-lan",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Wake-On-LAN/releases/download/v1.0.2/wake-on-lan-1.0.2.tmxplug",
|
||||
"sha256": "3e9cf9a133b4dd96ec5f2b9fbf2d6a3ff7e08f8dfbf1e86b8d45de0c2f024f42"
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Wake-On-LAN/releases/download/v1.1.0/wake-on-lan-1.1.0.tmxplug",
|
||||
"sha256": "1afb708c12631ededec40938aed3ab587bc9a519215fa5e93350b1f0a843d2a8",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "warpgate",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Warpgate/releases/download/v1.0.2/warpgate-1.0.2.tmxplug",
|
||||
"sha256": "5c2c1c9aef40dbbf98424d02303f2ee77e6e869334247881e8aa0fcd8c415b47"
|
||||
"sha256": "5c2c1c9aef40dbbf98424d02303f2ee77e6e869334247881e8aa0fcd8c415b47",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "web-endpoint",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Web-Endpoint/releases/download/v1.0.3/web-endpoint-1.0.3.tmxplug",
|
||||
"sha256": "014cbf23c82cab20e485bc4f4c9b8baa779aee9bb3ed644b41f22a9b1be95687"
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Web-Endpoint/releases/download/v1.1.0/web-endpoint-1.1.0.tmxplug",
|
||||
"sha256": "79e691abc456b17c8ce651017b47097a7ff71e94267e668341548a05b911a92c",
|
||||
"tier": "full"
|
||||
},
|
||||
{
|
||||
"id": "webauthn",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-WebAuthn/releases/download/v1.1.0/webauthn-1.1.0.tmxplug",
|
||||
"sha256": "83a61a530db0273f9fef2080d6aa027a82ef8374fb4e0f158a8559a2770299de",
|
||||
"tier": "standard",
|
||||
"onboarding": {
|
||||
"recommended": true,
|
||||
"consent": false
|
||||
@@ -310,8 +401,9 @@
|
||||
{
|
||||
"id": "workspaces",
|
||||
"source": "tmxplug",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Workspaces/releases/download/v1.0.2/workspaces-1.0.2.tmxplug",
|
||||
"sha256": "97519a01fbb2eb2eecf21d7b9ab9028c740457bcff9fc6b075fb6facbc5ccb6b",
|
||||
"url": "https://github.com/Termix-SSH/Plugin-Workspaces/releases/download/v1.1.0/workspaces-1.1.0.tmxplug",
|
||||
"sha256": "7189d767c3f7cddfb59366f6cceeca27a0b29e41da1dcc89d57a25317a0fe4bf",
|
||||
"tier": "standard",
|
||||
"onboarding": {
|
||||
"recommended": true,
|
||||
"consent": false
|
||||
|
||||
@@ -128,6 +128,10 @@
|
||||
"category": "public.app-category.developer-tools",
|
||||
"hardenedRuntime": true,
|
||||
"gatekeeperAssess": false,
|
||||
"extendInfo": {
|
||||
"NSLocalNetworkUsageDescription": "Termix connects to servers and hosts on your local network.",
|
||||
"NSBonjourServices": ["_http._tcp", "_https._tcp", "_ssh._tcp"]
|
||||
},
|
||||
"entitlements": "packaging/build/entitlements.mac.plist",
|
||||
"entitlementsInherit": "packaging/build/entitlements.mac.inherit.plist",
|
||||
"type": "distribution",
|
||||
@@ -153,6 +157,8 @@
|
||||
"artifactName": "termix_macos_${arch}_mas.${ext}",
|
||||
"extendInfo": {
|
||||
"ITSAppUsesNonExemptEncryption": false,
|
||||
"NSLocalNetworkUsageDescription": "Termix connects to servers and hosts on your local network.",
|
||||
"NSBonjourServices": ["_http._tcp", "_https._tcp", "_ssh._tcp"],
|
||||
"NSAppleEventsUsageDescription": "Termix needs access to control other applications for terminal operations."
|
||||
}
|
||||
},
|
||||
|
||||
+12
-1
@@ -298,7 +298,7 @@
|
||||
{
|
||||
"name": "HTTP_PROXY",
|
||||
"group": "proxy",
|
||||
"description": "Proxy for outbound HTTP requests, like registry downloads and webhooks."
|
||||
"description": "Proxy for outbound HTTP requests, like plugin registry downloads and webhooks."
|
||||
},
|
||||
{
|
||||
"name": "HTTPS_PROXY",
|
||||
@@ -353,6 +353,11 @@
|
||||
"group": "plugins",
|
||||
"description": "Where plugin download counts are read from. Defaults to stats.json next to the registry index."
|
||||
},
|
||||
{
|
||||
"name": "TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS",
|
||||
"group": "plugins",
|
||||
"description": "Comma-separated host names or IP addresses that plugin registry downloads may reach on a private address, like a registry in your LAN. List bare hosts: entries with a scheme, port or path are ignored. Matched by name only, on any port, so only list names your own DNS answers for. With HTTPS_PROXY set, add them to NO_PROXY too."
|
||||
},
|
||||
{
|
||||
"name": "TERMIX_REQUIRE_SIGNED_PLUGINS",
|
||||
"group": "plugins",
|
||||
@@ -365,6 +370,12 @@
|
||||
"default": "false",
|
||||
"description": "Start everyone in the layout repair view instead of the chosen layout. Use it when a layout plugin breaks the app."
|
||||
},
|
||||
{
|
||||
"name": "TERMIX_SAFE_MODE",
|
||||
"group": "plugins",
|
||||
"default": "false",
|
||||
"description": "Start only layout and theme plugins. Every other plugin stays off until a restart without it, so you can turn off a plugin that breaks the server or the app."
|
||||
},
|
||||
{
|
||||
"name": "TERMIX_BUNDLED_PLUGINS_DIR",
|
||||
"group": "plugins",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "termix",
|
||||
"version": "26.10.4",
|
||||
"version": "26.10.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "termix",
|
||||
"version": "26.10.4",
|
||||
"version": "26.10.5",
|
||||
"hasInstallScript": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "26.10.4",
|
||||
"version": "26.10.5",
|
||||
"description": "Self-hosted, plugin-based server management.",
|
||||
"author": "Karmaa",
|
||||
"main": "electron/main.cjs",
|
||||
@@ -31,7 +31,8 @@
|
||||
"dev:docker": "node scripts/dev.mjs docker",
|
||||
"dev:frontend": "vite",
|
||||
"build": "npm run build:sdk && vite build && node scripts/stamp-sw-build-info.cjs && tsc -p tsconfig.node.json && node -e \"require('fs').copyFileSync('src/backend/package.json','dist/backend/package.json')\" && npm run build:plugins",
|
||||
"build:backend": "npm run build:sdk && tsc -p tsconfig.node.json && node -e \"require('fs').copyFileSync('src/backend/package.json','dist/backend/package.json')\" && npm run build:plugins",
|
||||
"build:backend:core": "npm run build:sdk && tsc -p tsconfig.node.json && node -e \"require('fs').copyFileSync('src/backend/package.json','dist/backend/package.json')\"",
|
||||
"build:backend": "npm run build:backend:core && npm run build:plugins",
|
||||
"generate:openapi": "npm run build:sdk && node scripts/generate-openapi.mjs",
|
||||
"electron:rebuild": "electron-rebuild -f -o better-sqlite3,@serialport/bindings-cpp,node-pty",
|
||||
"build:win-portable": "npm run build && npm run electron:rebuild && electron-builder --win --dir",
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.20
|
||||
|
||||
Plugin API 1.12.0.
|
||||
|
||||
### Added
|
||||
|
||||
- `columns` and `onColumns` on `ViewToggle` show a cards per row picker: Auto fits as many as the pane has room for, or pick 1 to 6
|
||||
- `size` and `minCardWidth` on `DataView`, so a card grid follows that pick, and the `GridSize` type
|
||||
- `usePluginPanelView` also keeps `columns`, the cards per row pick, with `setColumns`
|
||||
|
||||
## 1.0.19
|
||||
|
||||
Plugin API 1.11.0.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Termix plugin guide
|
||||
|
||||
SDK 1.0.19 | Plugin API 1.11.0
|
||||
SDK 1.0.20 | Plugin API 1.12.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.
|
||||
@@ -643,6 +643,9 @@ exports with props is `ui.api.txt` in the Termix repo
|
||||
`PanePrompt` for small prompts.
|
||||
- Every tab or panel sits in `PanelShell`. Lists use `PanelList` and `ListRow`, add with `AddButton`,
|
||||
forms end with `FormFooter`, empty lists show `EmptyState`.
|
||||
- Card grids use `DataView` with `ViewToggle`. Pass `columns` and `onColumns` to `ViewToggle` and the same
|
||||
value as `size` to `DataView`, so users can pick Auto (as many as fit) or a fixed number of cards per
|
||||
row. `usePluginPanelView` keeps the choice as `columns`. Older Termix versions ignore both props.
|
||||
- Forms: `TextField`, `NumberField`, `SelectField`, `SwitchRow`, `SecretField`, `SettingRow` and friends.
|
||||
- Host sessions use `ConnectionGate` for the whole connect flow (host keys, TOTP, passphrases, errors).
|
||||
- Mobile: check `useIsMobile()` and give small screens a layout that works. The desktop app always uses the
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@termix-ssh/plugin-sdk",
|
||||
"version": "1.0.19",
|
||||
"version": "1.0.20",
|
||||
"description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -1711,28 +1711,49 @@ export function usePluginUiPreferences<
|
||||
return bridge.usePluginUiPreferences(bridge.usePluginId()) as never;
|
||||
}
|
||||
|
||||
type PanelGridSize = "auto" | "1" | "2" | "3" | "4" | "5" | "6";
|
||||
const PANEL_GRID_SIZES: readonly string[] = [
|
||||
"auto",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
];
|
||||
|
||||
/**
|
||||
* Grid or list and row density for one of this plugin's views, kept in its
|
||||
* Appearance area as `<prefix>viewMode` and `<prefix>density`. Declare those
|
||||
* keys in contributes.uiPresets so each preset level can pick a default.
|
||||
* Grid or list, row density and cards per row for one of this plugin's
|
||||
* views, kept in its Appearance area as `<prefix>viewMode`, `<prefix>density`
|
||||
* and `<prefix>columns`. Declare those keys in contributes.uiPresets so each
|
||||
* preset level can pick a default. Hand columns to ViewToggle and DataView.
|
||||
*/
|
||||
export function usePluginPanelView(prefix = ""): {
|
||||
view: "grid" | "list";
|
||||
density: "comfortable" | "compact";
|
||||
compact: boolean;
|
||||
columns: PanelGridSize;
|
||||
setView: (view: "grid" | "list") => void;
|
||||
setDensity: (density: "comfortable" | "compact") => void;
|
||||
setColumns: (columns: PanelGridSize) => void;
|
||||
} {
|
||||
const { values, set } = usePluginUiPreferences();
|
||||
const view = values[`${prefix}viewMode`] === "list" ? "list" : "grid";
|
||||
const density =
|
||||
values[`${prefix}density`] === "compact" ? "compact" : "comfortable";
|
||||
const saved = values[`${prefix}columns`];
|
||||
const columns =
|
||||
typeof saved === "string" && PANEL_GRID_SIZES.includes(saved)
|
||||
? (saved as PanelGridSize)
|
||||
: "auto";
|
||||
return {
|
||||
view,
|
||||
density,
|
||||
compact: density === "compact",
|
||||
columns,
|
||||
setView: (next) => set(`${prefix}viewMode`, next),
|
||||
setDensity: (next) => set(`${prefix}density`, next),
|
||||
setColumns: (next) => set(`${prefix}columns`, next),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export {
|
||||
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
||||
* version by the server.
|
||||
*/
|
||||
export const PLUGIN_API_VERSION = "1.11.0";
|
||||
export const PLUGIN_API_VERSION = "1.12.0";
|
||||
|
||||
/** The API major, the default engine.api for a new plugin. */
|
||||
export const SUPPORTED_PLUGIN_API_VERSION = String(
|
||||
|
||||
@@ -80,7 +80,7 @@ DEFAULT_FOLDER_COLOR (value): "#f59145"
|
||||
DEFAULT_FOLDER_ICON (value): "folder"
|
||||
DashboardCardId (type): DashboardCardId
|
||||
DataColumn (type): DataColumn<T>
|
||||
DataView (value): <T>({ items, view, density, getKey, renderCard, columns, listColumns, onRowClick, isRowActive, empty, className, }: { items: T[]; view: PanelViewMode; density?: PanelDensity; getKey: (item: T) => string; renderCard: (item: T) => ReactNode; columns: GridColumns; listColumns: DataColumn<T>[]; onRowClick?: (item: T) => void; isRowActive?: (item: T) => boolean; empty: ReactNode; className?: string; }) => Element
|
||||
DataView (value): <T>({ items, view, density, getKey, renderCard, columns, size, minCardWidth, listColumns, onRowClick, isRowActive, empty, className, }: { items: T[]; view: PanelViewMode; density?: PanelDensity; getKey: (item: T) => string; renderCard: (item: T) => ReactNode; columns: GridColumns; size?: GridSize; minCardWidth?: number; listColumns: DataColumn<T>[]; onRowClick?: (item: T) => void; isRowActive?: (item: T) => boolean; empty: ReactNode; className?: string; }) => Element
|
||||
DefaultOverrides (type): DefaultOverrides
|
||||
DefaultsOnly (value): ({ settingKey, children, }: { settingKey: string; children: ReactNode; }) => Element
|
||||
DesktopUpdateMode (type): DesktopUpdateMode
|
||||
@@ -131,6 +131,7 @@ GridCardCatalogEntry (type): GridCardCatalogEntry
|
||||
GridColSpan (type): GridColSpan
|
||||
GridColumns (type): GridColumns
|
||||
GridLayout (type): GridLayout
|
||||
GridSize (type): GridSize
|
||||
GridSlot (type): GridSlot
|
||||
GroupHeading (value): ({ title, count, action, className, }: { title: string; count?: number; action?: ReactNode; className?: string; }) => Element
|
||||
HexColorPicker (value): ({ value, onChange, }: { value: string; onChange: (hex: string) => void; }) => Element
|
||||
@@ -321,7 +322,7 @@ UserRole (type): UserRole
|
||||
UserSecondFactorSummary (type): UserSecondFactorSummary
|
||||
VersionBadge (value): ({ status, releaseUrl, className, }: VersionBadgeProps) => Element
|
||||
VersionInfo (type): VersionInfo
|
||||
ViewToggle (value): ({ view, onView, density, onDensity, }: { view: PanelViewMode; onView: (next: PanelViewMode) => void; density?: PanelDensity; onDensity?: (next: PanelDensity) => void; }) => Element
|
||||
ViewToggle (value): ({ view, onView, density, onDensity, columns, onColumns, }: { view: PanelViewMode; onView: (next: PanelViewMode) => void; density?: PanelDensity; onDensity?: (next: PanelDensity) => void; columns?: GridSize; onColumns?: (next: GridSize) => void; }) => Element
|
||||
WebSocketConnectionTarget (type): WebSocketConnectionTarget
|
||||
WidgetTitle (value): ({ title, icon, fallback }: WidgetTitleProps) => Element
|
||||
actionOrder (value): (action: HostActionDef) => number
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
*
|
||||
* getBundledPluginsDir() in src/backend/plugins/paths.ts resolves
|
||||
* dist/backend/backend/plugins -> dist/plugins, which is where this writes.
|
||||
* --profile minimal|standard|full (or TERMIX_PLUGIN_PROFILE, default standard)
|
||||
* picks which tiers of the file are staged.
|
||||
* Each .tmxplug is downloaded (or read from a path), checked against its
|
||||
* pinned sha256 and unpacked.
|
||||
*
|
||||
@@ -25,13 +27,16 @@ const {
|
||||
localSdkBuildCommand,
|
||||
copyLocalBuild,
|
||||
buildBundledIndex,
|
||||
resolveProfile,
|
||||
pluginsForProfile,
|
||||
} = require("./lib/bundled-plugins.cjs");
|
||||
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const destination = path.join(root, "dist", "plugins");
|
||||
|
||||
async function main() {
|
||||
const plugins = loadBundledPlugins(root);
|
||||
const profile = resolveProfile();
|
||||
const plugins = pluginsForProfile(loadBundledPlugins(root), profile);
|
||||
const flag = process.argv.indexOf("--local");
|
||||
const localArg =
|
||||
flag !== -1 ? process.argv[flag + 1] : process.env.TERMIX_LOCAL_PLUGINS;
|
||||
@@ -74,7 +79,7 @@ async function main() {
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Bundled ${plugins.length + extra.length} plugin(s): ${[...pinned, ...extra].join(", ")}`,
|
||||
`Bundled ${plugins.length + extra.length} plugin(s) (${profile}): ${[...pinned, ...extra].join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
localSdkBuildCommand,
|
||||
rebuildLocalPlugins,
|
||||
copyLocalBuild,
|
||||
resolveProfile,
|
||||
pluginsForProfile,
|
||||
} from "./lib/bundled-plugins.cjs";
|
||||
|
||||
const cleanups: Array<() => void> = [];
|
||||
@@ -263,10 +265,52 @@ describe("pinsFromIndex", () => {
|
||||
source: "tmxplug",
|
||||
url: "https://example.test/beta-1.1.0.tmxplug",
|
||||
sha256: "b".repeat(64),
|
||||
tier: "full",
|
||||
});
|
||||
expect(parseBundledPlugins({ plugins: pins }).problems).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps the tier of an existing pin", () => {
|
||||
const pins = pinsFromIndex(index, null, [
|
||||
{ id: "alpha", tier: "standard" },
|
||||
]);
|
||||
expect(pins[0].tier).toBe("standard");
|
||||
expect(pins[1].tier).toBe("full");
|
||||
});
|
||||
|
||||
it("skips releases whose engine.termix excludes the core version", () => {
|
||||
const withEngine = {
|
||||
plugins: [
|
||||
{
|
||||
id: "gamma",
|
||||
versions: [
|
||||
{
|
||||
version: "2.0.0",
|
||||
url: "https://example.test/gamma-2.0.0.tmxplug",
|
||||
sha256: "d".repeat(64),
|
||||
engine: { api: "^1", termix: ">=26.10.6" },
|
||||
},
|
||||
{
|
||||
version: "1.0.0",
|
||||
url: "https://example.test/gamma-1.0.0.tmxplug",
|
||||
sha256: "e".repeat(64),
|
||||
engine: { api: "^1", termix: ">=26.10.0" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(pinsFromIndex(withEngine, null, [], "26.10.5")[0].url).toBe(
|
||||
"https://example.test/gamma-1.0.0.tmxplug",
|
||||
);
|
||||
expect(pinsFromIndex(withEngine, null, [], "26.10.6")[0].url).toBe(
|
||||
"https://example.test/gamma-2.0.0.tmxplug",
|
||||
);
|
||||
expect(pinsFromIndex(withEngine, null)[0].url).toBe(
|
||||
"https://example.test/gamma-2.0.0.tmxplug",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the onboarding defaults of an existing pin", () => {
|
||||
const current = [
|
||||
{ id: "beta", onboarding: { recommended: true, consent: false } },
|
||||
@@ -511,3 +555,61 @@ describe("default look", () => {
|
||||
expect(fs.existsSync(path.join(out, "themes", "paper.css"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin profiles", () => {
|
||||
const entry = (id: string, tier?: string) => ({
|
||||
id,
|
||||
source: "tmxplug",
|
||||
url: `https://x/${id}.tmxplug`,
|
||||
sha256: SHA,
|
||||
...(tier ? { tier } : {}),
|
||||
});
|
||||
const { plugins } = parseBundledPlugins({
|
||||
plugins: [
|
||||
entry("look", "minimal"),
|
||||
entry("term", "standard"),
|
||||
entry("extra", "full"),
|
||||
entry("untiered"),
|
||||
],
|
||||
});
|
||||
const ids = (profile: string) =>
|
||||
pluginsForProfile(plugins, profile).map((p: { id: string }) => p.id);
|
||||
|
||||
it("ships each tier and every smaller one", () => {
|
||||
expect(ids("minimal")).toEqual(["look"]);
|
||||
expect(ids("standard")).toEqual(["look", "term"]);
|
||||
expect(ids("full")).toEqual(["look", "term", "extra", "untiered"]);
|
||||
});
|
||||
|
||||
it("rejects an unknown tier", () => {
|
||||
const { problems } = parseBundledPlugins({ plugins: [entry("x", "huge")] });
|
||||
expect(problems[0]).toMatch(/tier must be one of/);
|
||||
});
|
||||
|
||||
it("reads the profile from the flag, then the environment", () => {
|
||||
expect(resolveProfile(["node", "x"], {})).toBe("standard");
|
||||
expect(
|
||||
resolveProfile(["node", "x"], { TERMIX_PLUGIN_PROFILE: "full" }),
|
||||
).toBe("full");
|
||||
expect(
|
||||
resolveProfile(["node", "x", "--profile", "minimal"], {
|
||||
TERMIX_PLUGIN_PROFILE: "full",
|
||||
}),
|
||||
).toBe("minimal");
|
||||
expect(() =>
|
||||
resolveProfile(["node", "x", "--profile", "tiny"], {}),
|
||||
).toThrow(/unknown plugin profile/);
|
||||
});
|
||||
|
||||
it("keeps the real file valid with a default look in the minimal tier", () => {
|
||||
const real = loadBundledPlugins(path.resolve(__dirname, ".."));
|
||||
const minimal = pluginsForProfile(real, "minimal");
|
||||
expect(minimal.length).toBeGreaterThan(0);
|
||||
expect(minimal.every((p: { defaultLook: boolean }) => p.defaultLook)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(pluginsForProfile(real, "standard").length).toBeLessThan(
|
||||
real.length,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -151,6 +151,7 @@ const ALLOWED = [
|
||||
"GridCardCatalogEntry",
|
||||
"GridColSpan",
|
||||
"GridColumns",
|
||||
"GridSize",
|
||||
"GridLayout",
|
||||
"GridSlot",
|
||||
"GroupHeading",
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
* environment variables is set, for a server configured before first boot.
|
||||
* Plugins without it start unchecked.
|
||||
*
|
||||
* "tier" says which image profile ships the plugin: "minimal" (only what the
|
||||
* app cannot start without, the default look), "standard" (the default image)
|
||||
* or "full" (everything else). A profile includes its own tier and every
|
||||
* smaller one. A plugin with no tier counts as "full".
|
||||
*
|
||||
* "defaultLook": true marks the plugin whose layout (and that layout's
|
||||
* theme) a fresh install shows. Core finds the default look this way, so it
|
||||
* never names a plugin. Such a plugin always starts enabled and cannot be
|
||||
@@ -31,6 +36,8 @@ const ID = /^[a-z0-9][a-z0-9-]*$/;
|
||||
const ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
|
||||
const ONBOARDING_KEYS = new Set(["recommended", "consent", "enabledByEnv"]);
|
||||
const SHA256 = /^[0-9a-f]{64}$/;
|
||||
const PROFILES = ["minimal", "standard", "full"];
|
||||
const DEFAULT_PROFILE = "standard";
|
||||
|
||||
function parseOnboarding(value, id, problems) {
|
||||
if (value === undefined) return null;
|
||||
@@ -122,6 +129,9 @@ function parseBundledPlugins(raw) {
|
||||
) {
|
||||
problems.push(`${entry.id} defaultLook must be a boolean`);
|
||||
}
|
||||
if (entry.tier !== undefined && !PROFILES.includes(entry.tier)) {
|
||||
problems.push(`${entry.id} tier must be one of ${PROFILES.join(", ")}`);
|
||||
}
|
||||
if (problems.length > before) continue;
|
||||
plugins.push({
|
||||
id: entry.id,
|
||||
@@ -131,6 +141,7 @@ function parseBundledPlugins(raw) {
|
||||
sha256: entry.sha256,
|
||||
onboarding,
|
||||
defaultLook: entry.defaultLook === true,
|
||||
tier: entry.tier ?? "full",
|
||||
});
|
||||
} else {
|
||||
problems.push(`${entry.id} has an unknown source "${entry.source}"`);
|
||||
@@ -241,12 +252,35 @@ function buildBundledIndex(plugins, stagedIds) {
|
||||
return { version: 1, plugins: out };
|
||||
}
|
||||
|
||||
/** The profile to build: --profile, then TERMIX_PLUGIN_PROFILE, then standard. */
|
||||
function resolveProfile(argv = process.argv, env = process.env) {
|
||||
const flag = argv.indexOf("--profile");
|
||||
const value =
|
||||
(flag !== -1 ? argv[flag + 1] : env.TERMIX_PLUGIN_PROFILE) ||
|
||||
DEFAULT_PROFILE;
|
||||
if (!PROFILES.includes(value)) {
|
||||
throw new Error(
|
||||
`unknown plugin profile "${value}", use ${PROFILES.join(", ")}`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** The plugins a profile ships: its own tier and every smaller one. */
|
||||
function pluginsForProfile(plugins, profile) {
|
||||
const rank = PROFILES.indexOf(profile);
|
||||
return plugins.filter((plugin) => PROFILES.indexOf(plugin.tier) <= rank);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) carry over.
|
||||
* 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,
|
||||
* skips releases whose engine.termix range does not include it, so the image
|
||||
* never ships a plugin its own core cannot run.
|
||||
*/
|
||||
function pinsFromIndex(index, ids, current = []) {
|
||||
function pinsFromIndex(index, ids, current = [], coreVersion = null) {
|
||||
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]));
|
||||
@@ -257,15 +291,25 @@ function pinsFromIndex(index, ids, current = []) {
|
||||
const latest = [...(listed?.versions ?? []), ...(listed?.gated ?? [])]
|
||||
.filter((entry) => semver.valid(entry.version))
|
||||
.filter((entry) => !semver.prerelease(entry.version))
|
||||
.filter(
|
||||
(entry) =>
|
||||
!coreVersion ||
|
||||
!entry.engine?.termix ||
|
||||
semver.satisfies(coreVersion, entry.engine.termix, {
|
||||
includePrerelease: true,
|
||||
}),
|
||||
)
|
||||
.sort((a, b) => semver.rcompare(a.version, b.version))[0];
|
||||
if (!latest) 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";
|
||||
return {
|
||||
id,
|
||||
source: "tmxplug",
|
||||
url: latest.url,
|
||||
sha256: latest.sha256,
|
||||
tier,
|
||||
...(onboarding ? { onboarding } : {}),
|
||||
...(defaultLook ? { defaultLook } : {}),
|
||||
};
|
||||
@@ -409,6 +453,9 @@ function copyLocalBuild(repo, destination) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PROFILES,
|
||||
resolveProfile,
|
||||
pluginsForProfile,
|
||||
findLocalPluginBuilds,
|
||||
isLocalBuildStale,
|
||||
localSdkBuildCommand,
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
* re-released.
|
||||
*
|
||||
* node scripts/pin-bundled-plugins.cjs refresh the plugins already listed
|
||||
* node scripts/pin-bundled-plugins.cjs --all list every plugin in the index
|
||||
* node scripts/pin-bundled-plugins.cjs --all also add plugins new to the index (tier full)
|
||||
* --core-version X skip releases whose engine.termix excludes X
|
||||
*
|
||||
* The release workflow runs it with --all and the version being released.
|
||||
*/
|
||||
|
||||
const fs = require("node:fs");
|
||||
@@ -22,6 +25,8 @@ const configPath = path.join(root, "docker", "bundled-plugins.json");
|
||||
|
||||
async function main() {
|
||||
const all = process.argv.includes("--all");
|
||||
const flag = process.argv.indexOf("--core-version");
|
||||
const coreVersion = flag !== -1 ? process.argv[flag + 1] : null;
|
||||
const response = await fetch(INDEX_URL);
|
||||
if (!response.ok) {
|
||||
throw new Error(`registry index download failed with ${response.status}`);
|
||||
@@ -30,7 +35,7 @@ async function main() {
|
||||
|
||||
const current = loadBundledPlugins(root);
|
||||
const ids = all ? null : current.map((plugin) => plugin.id);
|
||||
const plugins = pinsFromIndex(index, ids, current);
|
||||
const plugins = pinsFromIndex(index, ids, current, coreVersion);
|
||||
fs.writeFileSync(configPath, `${JSON.stringify({ plugins }, null, 2)}\n`);
|
||||
console.log(
|
||||
`Pinned ${plugins.length} plugin(s) in docker/bundled-plugins.json`,
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
safeLayoutForced,
|
||||
unavailableLooks,
|
||||
} from "../../plugins/looks.js";
|
||||
import { safeModeOn } from "../../plugins/safe-mode.js";
|
||||
|
||||
const router = express.Router();
|
||||
const authManager = AuthManager.getInstance();
|
||||
@@ -36,7 +37,7 @@ async function adminDefaults() {
|
||||
* /user-preferences/look:
|
||||
* get:
|
||||
* summary: Get the layout and theme to show the current user
|
||||
* description: The user's own choice, the admin defaults (a layout no installed plugin brings reads as null), the default look of the bundled plugins, whether TERMIX_SAFE_LAYOUT forces the repair view, and installed layout or theme plugins that could not load. The browser resolves the final look from these.
|
||||
* description: The user's own choice, the admin defaults (a layout no installed plugin brings reads as null), the default look of the bundled plugins, whether TERMIX_SAFE_LAYOUT forces the repair view, whether TERMIX_SAFE_MODE runs only layout and theme plugins, and installed layout or theme plugins that could not load. The browser resolves the final look from these.
|
||||
* tags:
|
||||
* - User Preferences
|
||||
* responses:
|
||||
@@ -75,6 +76,8 @@ async function adminDefaults() {
|
||||
* nullable: true
|
||||
* safeLayout:
|
||||
* type: boolean
|
||||
* safeMode:
|
||||
* type: boolean
|
||||
* unavailable:
|
||||
* type: array
|
||||
* items:
|
||||
@@ -112,6 +115,7 @@ router.get("/", authenticateJWT, async (req: Request, res: Response) => {
|
||||
defaults: { ...defaults, layout: known(defaults.layout) },
|
||||
defaultLook: resolveDefaultLook(loader),
|
||||
safeLayout: safeLayoutForced(),
|
||||
safeMode: safeModeOn(),
|
||||
unavailable: unavailableLooks(loader),
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { getPluginRuntime } from "../../plugins/index.js";
|
||||
import { resolvePluginDesktopMode } from "@termix-ssh/plugin-sdk/manifest";
|
||||
import { describePluginFrontend } from "../../plugins/assets.js";
|
||||
import { isDefaultLookPlugin } from "../../plugins/bundled-index.js";
|
||||
import { mayRunInSafeMode } from "../../plugins/safe-mode.js";
|
||||
import { invalidatePluginPermissionCache } from "../../plugins/permissions.js";
|
||||
import { getPluginPublicHttpRoutes } from "../../plugins/http.js";
|
||||
import { getPluginPublicWsRoutes } from "../../plugins/ws.js";
|
||||
@@ -249,6 +250,9 @@ export async function listPreLoginPlugins(): Promise<
|
||||
* only run on that server and stay off here on purpose, and, for
|
||||
* admins, `managedByServer` marks plugins whose on/off state the server
|
||||
* decides.
|
||||
*
|
||||
* While TERMIX_SAFE_MODE is on, an enabled plugin that safe mode keeps
|
||||
* off reports `state: "safe-mode"`.
|
||||
* tags:
|
||||
* - Plugins
|
||||
* responses:
|
||||
@@ -354,7 +358,12 @@ router.get("/", authenticateJWT, async (req: Request, res: Response) => {
|
||||
name: record.name,
|
||||
version: record.version,
|
||||
enabled: record.state === "enabled",
|
||||
state: loaded?.state ?? record.state,
|
||||
state:
|
||||
record.state === "enabled" &&
|
||||
loaded?.state !== "active" &&
|
||||
!mayRunInSafeMode(record.id, loader.list())
|
||||
? "safe-mode"
|
||||
: (loaded?.state ?? record.state),
|
||||
runsOnServer: linked && desktopMode === "server",
|
||||
contributes,
|
||||
icon,
|
||||
@@ -936,7 +945,7 @@ router.delete(
|
||||
* 404:
|
||||
* description: No such plugin.
|
||||
* 409:
|
||||
* description: This desktop is linked to a server, which decides whether the plugin runs.
|
||||
* description: This desktop is linked to a server, which decides whether the plugin runs, or safe mode is on and the plugin is not a layout or theme.
|
||||
*/
|
||||
router.patch(
|
||||
"/:id/state",
|
||||
@@ -1050,6 +1059,8 @@ router.patch(
|
||||
* description: The caller lacks admin.plugins.manage.
|
||||
* 404:
|
||||
* description: No such plugin, or it is not on disk.
|
||||
* 409:
|
||||
* description: Safe mode is on and the plugin is not a layout or theme.
|
||||
* 500:
|
||||
* description: The plugin failed to start again.
|
||||
*/
|
||||
@@ -1073,6 +1084,13 @@ router.post(
|
||||
res.status(404).json({ error: "Plugin is not present on disk" });
|
||||
return;
|
||||
}
|
||||
if (!mayRunInSafeMode(pluginId, loader.list())) {
|
||||
res.status(409).json({
|
||||
error: "Safe mode is on, so only layout and theme plugins can start",
|
||||
code: "SAFE_MODE",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await repository.update(pluginId, {
|
||||
state: "enabled",
|
||||
|
||||
@@ -14,3 +14,24 @@ export function startablePluginIds(
|
||||
.map((record) => record.id),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugins that shipped in an earlier image and were left on, but whose code is
|
||||
* not in this one (a smaller image tag). They get fetched from the registry.
|
||||
*/
|
||||
export function missingShippedPluginIds(
|
||||
records: { id: string; tier: string; state: string }[],
|
||||
presentIds: ReadonlySet<string>,
|
||||
ignored: ReadonlySet<string>,
|
||||
): string[] {
|
||||
return records
|
||||
.filter(
|
||||
(record) =>
|
||||
record.tier === "bundled" &&
|
||||
record.state !== "disabled" &&
|
||||
!presentIds.has(record.id) &&
|
||||
!ignored.has(record.id),
|
||||
)
|
||||
.map((record) => record.id)
|
||||
.sort();
|
||||
}
|
||||
@@ -23,6 +23,11 @@ import { setHostProtocolSource } from "../hosts/protocol-auth/registry.js";
|
||||
import { setSecretResolverOwnerSource } from "../hosts/connect/secret-resolver-registry.js";
|
||||
import { recordConflict } from "./conflicts.js";
|
||||
import { startablePluginIds } from "./boot-state.js";
|
||||
import {
|
||||
mayRunInSafeMode,
|
||||
safeModeOn,
|
||||
safeModePluginIds,
|
||||
} from "./safe-mode.js";
|
||||
import { setPluginImpersonationCheck } from "../utils/auth-manager.js";
|
||||
import { setKeybindingActionSource } from "../database/routes/keybinding-validation.js";
|
||||
|
||||
@@ -237,6 +242,10 @@ export async function initializePlugins(
|
||||
if (loaded.length === 0) return [];
|
||||
|
||||
const fresh = await seedPlugins(loaded, options.freshInstall === true);
|
||||
if (!fresh) {
|
||||
const { restoreMissingPlugins } = await import("./manage.js");
|
||||
loaded.push(...(await restoreMissingPlugins()));
|
||||
}
|
||||
await syncCapabilityGrants(loaded);
|
||||
if (loaded.some((plugin) => plugin.source === "bundled")) {
|
||||
const { offerPluginSetup } = await import("./onboarding.js");
|
||||
@@ -248,8 +257,17 @@ export async function initializePlugins(
|
||||
const records = await createCurrentPluginRepository().listAll();
|
||||
const enabled = startablePluginIds(records);
|
||||
|
||||
const safe = safeModeOn() ? safeModePluginIds(loaded) : null;
|
||||
if (safe) {
|
||||
pluginLogger.warn(
|
||||
"Safe mode is on (TERMIX_SAFE_MODE). Only layout and theme plugins start",
|
||||
{ operation: "plugin_init" },
|
||||
);
|
||||
}
|
||||
|
||||
const candidates = loaded
|
||||
.filter((plugin) => enabled.has(plugin.id))
|
||||
.filter((plugin) => !safe || safe.has(plugin.id))
|
||||
.map((plugin) => plugin.id);
|
||||
|
||||
for (const pluginId of candidates) {
|
||||
@@ -539,6 +557,8 @@ export async function activatePlugin(pluginId: string): Promise<void> {
|
||||
// Before activate: a plugin's own activate() may already want to check one
|
||||
// of its permissions.
|
||||
const plugin = pluginLoader.get(pluginId);
|
||||
// Stays enabled and starts on the next boot without safe mode.
|
||||
if (!mayRunInSafeMode(pluginId, pluginLoader.list())) return;
|
||||
if (plugin) await registerPluginPermissions(plugin);
|
||||
|
||||
await pluginLoader.activate(pluginId);
|
||||
|
||||
@@ -18,6 +18,7 @@ import path from "node:path";
|
||||
import semver from "semver";
|
||||
import { pluginLogger } from "../utils/logger.js";
|
||||
import { isDefaultLookPlugin } from "./bundled-index.js";
|
||||
import { mayRunInSafeMode } from "./safe-mode.js";
|
||||
import {
|
||||
activatePlugin,
|
||||
deactivatePlugin,
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
registerLoadedPlugins,
|
||||
unloadPlugin,
|
||||
} from "./index.js";
|
||||
import { missingShippedPluginIds } from "./boot-state.js";
|
||||
import type { LoadedPlugin, PluginLoader } from "./loader.js";
|
||||
import {
|
||||
getBundledPluginsDir,
|
||||
@@ -763,6 +765,13 @@ export async function setPluginStateUnlocked(
|
||||
if (!loader.get(pluginId)) {
|
||||
throw new PluginManageError("Plugin not found", 404, "NOT_FOUND");
|
||||
}
|
||||
if (enabled && !mayRunInSafeMode(pluginId, loader.list())) {
|
||||
throw new PluginManageError(
|
||||
"Safe mode is on, so only layout and theme plugins can start",
|
||||
409,
|
||||
"SAFE_MODE",
|
||||
);
|
||||
}
|
||||
const plan = await planStateChange(pluginId, enabled);
|
||||
|
||||
if (enabled) {
|
||||
@@ -959,7 +968,11 @@ export function installPlugin(
|
||||
return serialized(async () => {
|
||||
const { createCurrentPluginRepository } = await repos();
|
||||
const repository = createCurrentPluginRepository();
|
||||
if (await repository.findById(pluginId)) {
|
||||
// A record with no code is a plugin an older image shipped: install fills it in.
|
||||
if (
|
||||
(await repository.findById(pluginId)) &&
|
||||
getPluginRuntime().loader.get(pluginId)
|
||||
) {
|
||||
throw new PluginManageError(
|
||||
"Plugin is already installed",
|
||||
409,
|
||||
@@ -1018,6 +1031,63 @@ export function installPlugin(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called at boot before plugins start. A plugin the database has on but the
|
||||
* image no longer ships (an update to a smaller image tag) is fetched from the
|
||||
* official registry, so nothing an install used goes missing. One that cannot
|
||||
* be fetched stays as it was and is tried again on the next start.
|
||||
*/
|
||||
export function restoreMissingPlugins(): Promise<LoadedPlugin[]> {
|
||||
return serialized(async () => {
|
||||
const { createCurrentPluginRepository } = await repos();
|
||||
const repository = createCurrentPluginRepository();
|
||||
const { loader } = getPluginRuntime();
|
||||
const ids = missingShippedPluginIds(
|
||||
await repository.listAll(),
|
||||
new Set(loader.list().map((plugin) => plugin.id)),
|
||||
await readUninstalled(),
|
||||
);
|
||||
const restored: LoadedPlugin[] = [];
|
||||
for (const id of ids) {
|
||||
try {
|
||||
const record = await repository.findById(id);
|
||||
const { source, release } = await requireRelease(
|
||||
id,
|
||||
record?.pinnedVersion ?? undefined,
|
||||
channelOf(record),
|
||||
OFFICIAL_REGISTRY_ID,
|
||||
);
|
||||
const plugin = await swapIn(id, { release, source });
|
||||
await repository.update(id, {
|
||||
registryId: source.id,
|
||||
tier: tierFor(plugin, source),
|
||||
});
|
||||
await grantDeclared(plugin, null);
|
||||
restored.push(plugin);
|
||||
pluginLogger.info(
|
||||
`Restored ${id}@${release.version} from the registry`,
|
||||
{
|
||||
operation: "plugin_restore_missing",
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
pluginLogger.warn(`Could not restore ${id}: ${message}`, {
|
||||
operation: "plugin_restore_missing",
|
||||
});
|
||||
// The registry is down or offline, so the rest would fail the same way.
|
||||
if (
|
||||
error instanceof PluginManageError &&
|
||||
error.code === "REGISTRY_UNREACHABLE"
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return restored;
|
||||
});
|
||||
}
|
||||
|
||||
export function updatePlugin(
|
||||
pluginId: string,
|
||||
options: InstallOptions,
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import { isIP } from "node:net";
|
||||
import path from "node:path";
|
||||
import semver from "semver";
|
||||
import {
|
||||
@@ -23,7 +24,11 @@ import {
|
||||
youtubeVideoId,
|
||||
} from "@termix-ssh/plugin-sdk/manifest";
|
||||
import { getLocalVersion } from "../utils/app-version.js";
|
||||
import { safeOutboundFetch } from "../utils/safe-outbound-fetch.js";
|
||||
import {
|
||||
isBlockedAddress,
|
||||
safeOutboundFetch,
|
||||
} from "../utils/safe-outbound-fetch.js";
|
||||
import { fetchWithProxy, getProxyAgent } from "../utils/proxy-agent.js";
|
||||
import { pluginLogger } from "../utils/logger.js";
|
||||
import {
|
||||
COMMUNITY_PLUGIN_KEYS,
|
||||
@@ -278,15 +283,72 @@ export function canRunRelease(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hosts the admin lets registry fetches reach on a private address, like a
|
||||
* registry in the LAN. Matched by host name on any port, also when another
|
||||
* index or a redirect points there; every other host stays blocked.
|
||||
*/
|
||||
export function getRegistryPrivateHosts(): string[] {
|
||||
return (process.env.TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS ?? "")
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
.map(normalizePrivateHost)
|
||||
.filter((host): host is string => host !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* The host in the form safeOutboundFetch compares against, or undefined when
|
||||
* the entry carries more than a host, like a scheme, port, path or user.
|
||||
*/
|
||||
function normalizePrivateHost(entry: string): string | undefined {
|
||||
const candidate = isIP(entry) === 6 ? `[${entry}]` : entry;
|
||||
if (/[/?#@\\]|:\d*$/.test(candidate)) return undefined;
|
||||
try {
|
||||
return new URL(`http://${candidate}`).hostname.replace(/^\[|\]$/g, "");
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* With HTTP_PROXY or HTTPS_PROXY set, registry traffic goes through the proxy,
|
||||
* which does its own name lookup. Only the URL itself is checked here: web
|
||||
* pages only, no logins in it, and no private address typed in as a literal.
|
||||
*/
|
||||
function fetchThroughProxy(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
privateHosts: readonly string[],
|
||||
): Promise<Response> {
|
||||
const parsed = new URL(url);
|
||||
const host = parsed.hostname.replace(/^\[|\]$/g, "");
|
||||
if (
|
||||
!["http:", "https:"].includes(parsed.protocol) ||
|
||||
parsed.username ||
|
||||
parsed.password
|
||||
) {
|
||||
throw new Error("Invalid outbound URL");
|
||||
}
|
||||
if (isIP(host) && isBlockedAddress(host) && !privateHosts.includes(host)) {
|
||||
throw new Error("Private destinations are not allowed");
|
||||
}
|
||||
return fetchWithProxy(url, options);
|
||||
}
|
||||
|
||||
/** Follows redirects by hand so each hop still gets the outbound checks. */
|
||||
async function fetchFollowing(url: string): Promise<Response> {
|
||||
const privateHosts = getRegistryPrivateHosts();
|
||||
let current = url;
|
||||
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
||||
const response = await safeOutboundFetch(current, {
|
||||
const options: RequestInit = {
|
||||
method: "GET",
|
||||
redirect: "manual",
|
||||
headers: { "User-Agent": "Termix" },
|
||||
});
|
||||
};
|
||||
const response = getProxyAgent(current)
|
||||
? await fetchThroughProxy(current, options, privateHosts)
|
||||
: await safeOutboundFetch(current, options, privateHosts);
|
||||
if (response.status >= 300 && response.status < 400) {
|
||||
const location = response.headers.get("location");
|
||||
if (!location) throw new Error(`redirect without a location: ${current}`);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { PluginManifest } from "./manifest.js";
|
||||
|
||||
/** TERMIX_SAFE_MODE=1 starts only the plugins that draw the app. */
|
||||
export function safeModeOn(): boolean {
|
||||
const value = process.env.TERMIX_SAFE_MODE?.trim().toLowerCase();
|
||||
return value === "1" || value === "true";
|
||||
}
|
||||
|
||||
function isLookPlugin(manifest: PluginManifest): boolean {
|
||||
return (
|
||||
(manifest.contributes?.layouts?.length ?? 0) > 0 ||
|
||||
(manifest.contributes?.themes?.length ?? 0) > 0
|
||||
);
|
||||
}
|
||||
|
||||
/** Layout and theme plugins, plus the plugins they need to start. */
|
||||
export function safeModePluginIds(
|
||||
plugins: { id: string; manifest: PluginManifest }[],
|
||||
): Set<string> {
|
||||
const byId = new Map(plugins.map((plugin) => [plugin.id, plugin]));
|
||||
const allowed = new Set<string>();
|
||||
const add = (id: string) => {
|
||||
const plugin = byId.get(id);
|
||||
if (!plugin || allowed.has(id)) return;
|
||||
allowed.add(id);
|
||||
for (const dependency of Object.keys(plugin.manifest.dependencies ?? {})) {
|
||||
add(dependency);
|
||||
}
|
||||
};
|
||||
for (const plugin of plugins) {
|
||||
if (isLookPlugin(plugin.manifest)) add(plugin.id);
|
||||
}
|
||||
return allowed;
|
||||
}
|
||||
|
||||
/** Whether a plugin may run now. Always true outside safe mode. */
|
||||
export function mayRunInSafeMode(
|
||||
pluginId: string,
|
||||
plugins: { id: string; manifest: PluginManifest }[],
|
||||
): boolean {
|
||||
return !safeModeOn() || safeModePluginIds(plugins).has(pluginId);
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import { createCurrentRepositoryContext } from "../database/repositories/factory
|
||||
* On a linked desktop, hosts and credentials someone shared with the account
|
||||
* are read-only copies kept in step by sync. Editing or deleting one here
|
||||
* would be undone by the next sync, so it is refused with a clear reason.
|
||||
* A host's connection origin only means something on this device, so that one
|
||||
* field can still be changed.
|
||||
*/
|
||||
export function rejectSharedCopyWrites(
|
||||
kind: "host" | "credential",
|
||||
@@ -25,6 +27,19 @@ export function rejectSharedCopyWrites(
|
||||
.where(eq(table.id, Number(match[1])))
|
||||
.limit(1);
|
||||
if (row?.sharedSource) {
|
||||
const origin = req.body?.connectionOrigin;
|
||||
if (
|
||||
kind === "host" &&
|
||||
req.method === "PUT" &&
|
||||
(origin === "local" || origin === "remote")
|
||||
) {
|
||||
await createCurrentRepositoryContext()
|
||||
.drizzle.update(hosts)
|
||||
.set({ connectionOrigin: origin })
|
||||
.where(eq(hosts.id, Number(match[1])));
|
||||
res.json({ id: Number(match[1]), connectionOrigin: origin });
|
||||
return;
|
||||
}
|
||||
res.status(403).json({
|
||||
error: "This was shared with you and is managed on the server",
|
||||
code: "SHARED_COPY",
|
||||
|
||||
@@ -102,6 +102,7 @@ beforeEach(async () => {
|
||||
state.defaultLookIds = [];
|
||||
state.installed = [];
|
||||
delete process.env.TERMIX_SAFE_LAYOUT;
|
||||
delete process.env.TERMIX_SAFE_MODE;
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
@@ -113,6 +114,7 @@ beforeEach(async () => {
|
||||
|
||||
afterEach(async () => {
|
||||
delete process.env.TERMIX_SAFE_LAYOUT;
|
||||
delete process.env.TERMIX_SAFE_MODE;
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
});
|
||||
|
||||
@@ -138,6 +140,7 @@ describe("GET /user-preferences/look", () => {
|
||||
defaults: { layout: null, theme: null },
|
||||
defaultLook: { layout: null, theme: null },
|
||||
safeLayout: false,
|
||||
safeMode: false,
|
||||
unavailable: [],
|
||||
});
|
||||
});
|
||||
@@ -159,6 +162,7 @@ describe("GET /user-preferences/look", () => {
|
||||
{ id: "other", version: null, layouts: [], themes: [], error: "y" },
|
||||
];
|
||||
process.env.TERMIX_SAFE_LAYOUT = "1";
|
||||
process.env.TERMIX_SAFE_MODE = "1";
|
||||
|
||||
const { body } = await getLook();
|
||||
expect(body).toMatchObject({
|
||||
@@ -167,6 +171,7 @@ describe("GET /user-preferences/look", () => {
|
||||
defaults: { layout: "admin-layout", theme: "gruvbox" },
|
||||
defaultLook: { layout: "shipped", theme: "ink" },
|
||||
safeLayout: true,
|
||||
safeMode: true,
|
||||
});
|
||||
expect(body.unavailable.map((plugin: { id: string }) => plugin.id)).toEqual(
|
||||
["broken"],
|
||||
|
||||
@@ -488,6 +488,26 @@ describe("plugins route", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("reports an enabled plugin that safe mode keeps off", async () => {
|
||||
state.plugins.set("sample-plugin", makePlugin({ state: "enabled" }));
|
||||
|
||||
const read = async () =>
|
||||
(
|
||||
(await (await fetch(`${baseUrl}/plugins`)).json()) as Array<{
|
||||
state: string;
|
||||
enabled: boolean;
|
||||
}>
|
||||
)[0];
|
||||
|
||||
expect((await read()).state).not.toBe("safe-mode");
|
||||
process.env.TERMIX_SAFE_MODE = "1";
|
||||
try {
|
||||
expect(await read()).toMatchObject({ state: "safe-mode", enabled: true });
|
||||
} finally {
|
||||
delete process.env.TERMIX_SAFE_MODE;
|
||||
}
|
||||
});
|
||||
|
||||
it("grants a declared capability", async () => {
|
||||
state.plugins.set("sample-plugin", makePlugin());
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { startablePluginIds } from "../../plugins/boot-state.js";
|
||||
import {
|
||||
missingShippedPluginIds,
|
||||
startablePluginIds,
|
||||
} from "../../plugins/boot-state.js";
|
||||
|
||||
describe("startablePluginIds", () => {
|
||||
it("starts enabled plugins and retries ones that failed or were blocked", () => {
|
||||
@@ -13,3 +16,25 @@ describe("startablePluginIds", () => {
|
||||
expect([...ids].sort()).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("missingShippedPluginIds", () => {
|
||||
const records = [
|
||||
{ id: "gone", tier: "bundled", state: "enabled" },
|
||||
{ id: "here", tier: "bundled", state: "enabled" },
|
||||
{ id: "off", tier: "bundled", state: "disabled" },
|
||||
{ id: "failed", tier: "bundled", state: "failed" },
|
||||
{ id: "community", tier: "community", state: "enabled" },
|
||||
{ id: "removed", tier: "bundled", state: "enabled" },
|
||||
];
|
||||
|
||||
it("lists shipped plugins that are on but not in the image", () => {
|
||||
expect(
|
||||
missingShippedPluginIds(records, new Set(["here"]), new Set(["removed"])),
|
||||
).toEqual(["failed", "gone"]);
|
||||
});
|
||||
|
||||
it("finds nothing when every plugin is present", () => {
|
||||
const present = new Set(records.map((record) => record.id));
|
||||
expect(missingShippedPluginIds(records, present, new Set())).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -621,6 +621,26 @@ describe("dependencies", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("safe mode", () => {
|
||||
afterEach(() => {
|
||||
delete process.env.TERMIX_SAFE_MODE;
|
||||
});
|
||||
|
||||
it("refuses to start a plugin that is not a layout or theme, but still stops one", async () => {
|
||||
createFixturePlugin({ id: "base", root: bundled });
|
||||
const loader = await boot();
|
||||
await manage.setPluginState("base", false);
|
||||
|
||||
process.env.TERMIX_SAFE_MODE = "1";
|
||||
await expect(manage.setPluginState("base", true)).rejects.toMatchObject({
|
||||
status: 409,
|
||||
code: "SAFE_MODE",
|
||||
});
|
||||
expect(loader.get("base")?.state).toBe("stopped");
|
||||
expect(db.rows.get("base")?.state).toBe("disabled");
|
||||
});
|
||||
});
|
||||
|
||||
describe("reloading a bundled plugin for the dev runner", () => {
|
||||
it("loads the rebuilt copy and restarts what depends on it", async () => {
|
||||
createFixturePlugin({ id: "base", root: bundled });
|
||||
|
||||
@@ -8,10 +8,23 @@ import * as tar from "tar";
|
||||
const fetchMock = vi.hoisted(() => vi.fn());
|
||||
const testKey = vi.hoisted(() => ({ value: null as null | object }));
|
||||
|
||||
vi.mock("../../utils/safe-outbound-fetch.js", () => ({
|
||||
vi.mock("../../utils/safe-outbound-fetch.js", async (importOriginal) => ({
|
||||
...(await importOriginal<
|
||||
typeof import("../../utils/safe-outbound-fetch.js")
|
||||
>()),
|
||||
safeOutboundFetch: fetchMock,
|
||||
}));
|
||||
|
||||
const proxyMock = vi.hoisted(() => ({
|
||||
on: false,
|
||||
fetch: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/proxy-agent.js", () => ({
|
||||
getProxyAgent: () => (proxyMock.on ? {} : undefined),
|
||||
fetchWithProxy: proxyMock.fetch,
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/app-version.js", () => ({
|
||||
getLocalVersion: () => "26.10.3",
|
||||
}));
|
||||
@@ -43,9 +56,11 @@ vi.mock("../../plugins/trust.js", async (importOriginal) => {
|
||||
import {
|
||||
communitySource,
|
||||
downloadRelease,
|
||||
fetchIndexAt,
|
||||
fetchRegistryIndex,
|
||||
fetchRegistryStats,
|
||||
findRelease,
|
||||
getRegistryPrivateHosts,
|
||||
latestOnChannel,
|
||||
getStatsUrl,
|
||||
canRunRelease,
|
||||
@@ -409,6 +424,91 @@ describe("fetchRegistryIndex", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("registry fetches with a proxy", () => {
|
||||
afterEach(() => {
|
||||
proxyMock.on = false;
|
||||
proxyMock.fetch.mockReset();
|
||||
});
|
||||
|
||||
it("goes through the proxy when one is configured", async () => {
|
||||
proxyMock.on = true;
|
||||
proxyMock.fetch.mockResolvedValueOnce(
|
||||
response(JSON.stringify({ plugins: [] })),
|
||||
);
|
||||
await fetchIndexAt("https://registry.example/index.json");
|
||||
expect(proxyMock.fetch).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still refuses a private address typed in as a literal", async () => {
|
||||
proxyMock.on = true;
|
||||
await expect(fetchIndexAt("https://127.0.0.1/index.json")).rejects.toThrow(
|
||||
"Private destinations are not allowed",
|
||||
);
|
||||
expect(proxyMock.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("private registry hosts", () => {
|
||||
afterEach(() => {
|
||||
delete process.env.TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS;
|
||||
});
|
||||
|
||||
it("reads a comma separated list of host names", () => {
|
||||
process.env.TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS =
|
||||
" GitLab.lan ,, 192.168.1.5 ";
|
||||
expect(getRegistryPrivateHosts()).toEqual(["gitlab.lan", "192.168.1.5"]);
|
||||
delete process.env.TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS;
|
||||
expect(getRegistryPrivateHosts()).toEqual([]);
|
||||
});
|
||||
|
||||
it("brings each entry into the form the fetch compares against", () => {
|
||||
process.env.TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS =
|
||||
"GitLab.LAN,[FD00::0001],fd00::2,bücher.lan";
|
||||
expect(getRegistryPrivateHosts()).toEqual([
|
||||
"gitlab.lan",
|
||||
"fd00::1",
|
||||
"fd00::2",
|
||||
"xn--bcher-kva.lan",
|
||||
]);
|
||||
});
|
||||
|
||||
it("drops entries with more than a host", () => {
|
||||
process.env.TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS =
|
||||
"gitlab.lan:8443,https://git.lan,[fd00::1]:80,nas.lan";
|
||||
expect(getRegistryPrivateHosts()).toEqual(["nas.lan"]);
|
||||
});
|
||||
|
||||
it("allows the listed hosts on every hop", async () => {
|
||||
process.env.TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS = "gitlab.lan";
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(
|
||||
response("", 302, { location: "https://gitlab.lan/raw/index.json" }),
|
||||
)
|
||||
.mockResolvedValueOnce(response(JSON.stringify({ plugins: [] })));
|
||||
await fetchIndexAt("https://gitlab.lan/index.json");
|
||||
expect(fetchMock.mock.calls.map((call) => call[2])).toEqual([
|
||||
["gitlab.lan"],
|
||||
["gitlab.lan"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("still blocks a redirect to a private host that is not listed", async () => {
|
||||
process.env.TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS = "gitlab.lan";
|
||||
const actual = await vi.importActual<
|
||||
typeof import("../../utils/safe-outbound-fetch.js")
|
||||
>("../../utils/safe-outbound-fetch.js");
|
||||
fetchMock
|
||||
.mockResolvedValueOnce(
|
||||
response("", 302, { location: "https://127.0.0.1/index.json" }),
|
||||
)
|
||||
.mockImplementationOnce(actual.safeOutboundFetch);
|
||||
await expect(fetchIndexAt("https://gitlab.lan/index.json")).rejects.toThrow(
|
||||
"Private destinations are not allowed",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/** A real .tmxplug holding only a manifest, enough for the checks. */
|
||||
function pack(manifest: Record<string, unknown>): Buffer {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-pack-"));
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
mayRunInSafeMode,
|
||||
safeModeOn,
|
||||
safeModePluginIds,
|
||||
} from "../../plugins/safe-mode.js";
|
||||
import type { PluginManifest } from "../../plugins/manifest.js";
|
||||
|
||||
function plugin(id: string, manifest: Partial<PluginManifest> = {}) {
|
||||
return { id, manifest: { id, ...manifest } as PluginManifest };
|
||||
}
|
||||
|
||||
const plugins = [
|
||||
plugin("layout", {
|
||||
contributes: { layouts: [{ id: "classic" }] },
|
||||
dependencies: { helper: "^1.0.0" },
|
||||
} as Partial<PluginManifest>),
|
||||
plugin("themes", {
|
||||
contributes: { themes: [{ id: "dark" }] },
|
||||
} as Partial<PluginManifest>),
|
||||
plugin("helper"),
|
||||
plugin("docker"),
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.TERMIX_SAFE_MODE;
|
||||
});
|
||||
|
||||
describe("safeModeOn", () => {
|
||||
it("reads 1 and true, anything else is off", () => {
|
||||
expect(safeModeOn()).toBe(false);
|
||||
process.env.TERMIX_SAFE_MODE = "1";
|
||||
expect(safeModeOn()).toBe(true);
|
||||
process.env.TERMIX_SAFE_MODE = " TRUE ";
|
||||
expect(safeModeOn()).toBe(true);
|
||||
process.env.TERMIX_SAFE_MODE = "0";
|
||||
expect(safeModeOn()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("safeModePluginIds", () => {
|
||||
it("keeps layout and theme plugins and what they depend on", () => {
|
||||
expect([...safeModePluginIds(plugins)].sort()).toEqual([
|
||||
"helper",
|
||||
"layout",
|
||||
"themes",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mayRunInSafeMode", () => {
|
||||
it("lets every plugin run when safe mode is off", () => {
|
||||
expect(mayRunInSafeMode("docker", plugins)).toBe(true);
|
||||
});
|
||||
|
||||
it("only lets look plugins run when safe mode is on", () => {
|
||||
process.env.TERMIX_SAFE_MODE = "1";
|
||||
expect(mayRunInSafeMode("docker", plugins)).toBe(false);
|
||||
expect(mayRunInSafeMode("layout", plugins)).toBe(true);
|
||||
expect(mayRunInSafeMode("helper", plugins)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const h = vi.hoisted(() => ({
|
||||
row: { sharedSource: '{"owner":"x"}' } as { sharedSource: string | null },
|
||||
updates: [] as unknown[],
|
||||
}));
|
||||
|
||||
vi.mock("../../database/repositories/factory.js", () => ({
|
||||
createCurrentRepositoryContext: () => ({
|
||||
drizzle: {
|
||||
select: () => ({
|
||||
from: () => ({ where: () => ({ limit: async () => [h.row] }) }),
|
||||
}),
|
||||
update: () => ({
|
||||
set: (values: unknown) => ({
|
||||
where: async () => {
|
||||
h.updates.push(values);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const { rejectSharedCopyWrites } =
|
||||
await import("../../sync/shared-copy-guard.js");
|
||||
|
||||
function run(body: Record<string, unknown>) {
|
||||
const guard = rejectSharedCopyWrites("host", /^\/db\/host\/(\d+)$/);
|
||||
const res = {
|
||||
code: 0,
|
||||
payload: undefined as unknown,
|
||||
status(code: number) {
|
||||
this.code = code;
|
||||
return this;
|
||||
},
|
||||
json(payload: unknown) {
|
||||
this.payload = payload;
|
||||
return this;
|
||||
},
|
||||
};
|
||||
const next = vi.fn();
|
||||
return guard(
|
||||
{ method: "PUT", path: "/db/host/5", body } as never,
|
||||
res as never,
|
||||
next,
|
||||
).then(() => ({ res, next }));
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.ELECTRON_EMBEDDED = "true";
|
||||
h.row = { sharedSource: '{"owner":"x"}' };
|
||||
h.updates = [];
|
||||
});
|
||||
|
||||
describe("shared copy guard", () => {
|
||||
it("refuses edits to a shared copy", async () => {
|
||||
const { res, next } = await run({ name: "new" });
|
||||
expect(res.code).toBe(403);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
expect(h.updates).toEqual([]);
|
||||
});
|
||||
|
||||
it("still lets the connection origin change on this device", async () => {
|
||||
const { res, next } = await run({ name: "new", connectionOrigin: "local" });
|
||||
expect(res.code).toBe(0);
|
||||
expect(res.payload).toMatchObject({ connectionOrigin: "local" });
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
expect(h.updates).toEqual([{ connectionOrigin: "local" }]);
|
||||
});
|
||||
|
||||
it("passes hosts that are not shared copies", async () => {
|
||||
h.row = { sharedSource: null };
|
||||
const { next } = await run({ name: "new" });
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,8 @@ export interface LookResponse {
|
||||
defaultLook: { layout: string | null; theme: string | null };
|
||||
/** TERMIX_SAFE_LAYOUT is set on the server. */
|
||||
safeLayout: boolean;
|
||||
/** TERMIX_SAFE_MODE is set: only layout and theme plugins run. */
|
||||
safeMode?: boolean;
|
||||
unavailable: UnavailableLook[];
|
||||
}
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ export interface PluginSummary {
|
||||
name: string;
|
||||
version: string;
|
||||
enabled: boolean;
|
||||
/** enabled | disabled | blocked | failed, or the loader's live state. */
|
||||
/** enabled | disabled | blocked | failed | safe-mode, or the loader's live state. */
|
||||
state: string;
|
||||
/** A linked desktop leaves this plugin to its server. */
|
||||
runsOnServer?: boolean;
|
||||
|
||||
@@ -2,6 +2,8 @@ import type React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
PANEL,
|
||||
gridSizeStyle,
|
||||
type GridSize,
|
||||
type PanelDensity,
|
||||
type PanelViewMode,
|
||||
} from "@/components/panel-layout";
|
||||
@@ -67,6 +69,8 @@ export function DataView<T>({
|
||||
getKey,
|
||||
renderCard,
|
||||
columns,
|
||||
size,
|
||||
minCardWidth,
|
||||
listColumns,
|
||||
onRowClick,
|
||||
isRowActive,
|
||||
@@ -78,7 +82,12 @@ export function DataView<T>({
|
||||
density?: PanelDensity;
|
||||
getKey: (item: T) => string;
|
||||
renderCard: (item: T) => React.ReactNode;
|
||||
/** Breakpoint columns, used when no size is given. */
|
||||
columns: GridColumns;
|
||||
/** Columns the user picked with ViewToggle. Wins over columns. */
|
||||
size?: GridSize;
|
||||
/** Narrowest card in auto mode, in px. */
|
||||
minCardWidth?: number;
|
||||
listColumns: DataColumn<T>[];
|
||||
onRowClick?: (item: T) => void;
|
||||
isRowActive?: (item: T) => boolean;
|
||||
@@ -93,9 +102,19 @@ export function DataView<T>({
|
||||
|
||||
if (view === "grid") {
|
||||
return (
|
||||
<div className={cn("grid", PANEL.gap, gridClass(columns), className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"grid",
|
||||
PANEL.gap,
|
||||
!size && gridClass(columns),
|
||||
className,
|
||||
)}
|
||||
style={size ? gridSizeStyle(size, minCardWidth) : undefined}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<div key={getKey(item)}>{renderCard(item)}</div>
|
||||
<div key={getKey(item)} className="flex flex-col *:flex-1">
|
||||
{renderCard(item)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import { Children, Fragment, isValidElement, useRef } from "react";
|
||||
import type React from "react";
|
||||
import { ArrowLeft, Grid3X3, List, Rows3, Search, X } from "lucide-react";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Columns3,
|
||||
Grid3X3,
|
||||
List,
|
||||
Rows3,
|
||||
Search,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Separator } from "@/components/separator";
|
||||
import { DocsLink } from "@/components/docs-link";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/select";
|
||||
|
||||
/** One spacing rhythm for every panel and tab. */
|
||||
export const PANEL = {
|
||||
@@ -19,6 +34,37 @@ export const PANEL = {
|
||||
export type PanelViewMode = "grid" | "list";
|
||||
export type PanelDensity = "comfortable" | "compact";
|
||||
|
||||
/** Cards per row. "auto" fits as many as the pane has room for. */
|
||||
export type GridSize = "auto" | "1" | "2" | "3" | "4" | "5" | "6";
|
||||
|
||||
export const GRID_SIZES: readonly GridSize[] = [
|
||||
"auto",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
];
|
||||
|
||||
// Sized off the pane's own width, not the window. Cards keep a min width,
|
||||
// so narrow panes drop columns on their own.
|
||||
export function gridSizeStyle(
|
||||
size: GridSize,
|
||||
minCardWidth = 320,
|
||||
gap = "0.5rem",
|
||||
): React.CSSProperties {
|
||||
if (size === "auto") {
|
||||
return {
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(min(100%, ${minCardWidth}px), 1fr))`,
|
||||
};
|
||||
}
|
||||
const n = Number(size);
|
||||
return {
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(max(min(100%, 220px), calc((100% - ${n - 1} * ${gap}) / ${n} - 0.5px)), 1fr))`,
|
||||
};
|
||||
}
|
||||
|
||||
/** The header, toolbar band and body every tab and panel draws. */
|
||||
export function PanelShell({
|
||||
chrome = true,
|
||||
@@ -308,20 +354,25 @@ export function Segmented<T extends string>({
|
||||
);
|
||||
}
|
||||
|
||||
/** Grid or list, plus the compact rows toggle. */
|
||||
/** Grid or list, plus the compact rows toggle and the cards per row picker. */
|
||||
export function ViewToggle({
|
||||
view,
|
||||
onView,
|
||||
density,
|
||||
onDensity,
|
||||
columns,
|
||||
onColumns,
|
||||
}: {
|
||||
view: PanelViewMode;
|
||||
onView: (next: PanelViewMode) => void;
|
||||
density?: PanelDensity;
|
||||
onDensity?: (next: PanelDensity) => void;
|
||||
/** Cards per row in grid view. Pass with onColumns to show the picker. */
|
||||
columns?: GridSize;
|
||||
onColumns?: (next: GridSize) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
const toggle = (
|
||||
<div className="flex items-center border border-border">
|
||||
<ToggleButton
|
||||
active={view === "grid"}
|
||||
@@ -356,6 +407,34 @@ export function ViewToggle({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!columns || !onColumns || view !== "grid") return toggle;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={columns} onValueChange={(v) => onColumns(v as GridSize)}>
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="w-auto gap-1.5 text-xs"
|
||||
title={t("panel.columns.title")}
|
||||
aria-label={t("panel.columns.title")}
|
||||
>
|
||||
<Columns3 className="size-3.5 text-muted-foreground" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{GRID_SIZES.map((size) => (
|
||||
<SelectItem key={size} value={size} className="text-xs">
|
||||
{size === "auto"
|
||||
? t("panel.columns.auto")
|
||||
: t("panel.columns.count", { count: Number(size) })}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{toggle}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleButton({
|
||||
|
||||
+12
-7
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} is ready to install",
|
||||
"readyHint": "It installs the next time Termix restarts."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Safe mode",
|
||||
"description": "Only layout and theme plugins are running. Restart the server without TERMIX_SAFE_MODE to turn the rest back on."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Close panel",
|
||||
"moveSide": "Move panel",
|
||||
@@ -1311,7 +1315,8 @@
|
||||
"disabled": "Disabled",
|
||||
"failed": "Stopped unexpectedly",
|
||||
"blocked": "Waiting on another plugin",
|
||||
"server": "Runs on your server"
|
||||
"server": "Runs on your server",
|
||||
"safeMode": "Off in safe mode"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Running",
|
||||
@@ -1325,11 +1330,6 @@
|
||||
"updated": "Recently updated",
|
||||
"category": "Category"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Columns",
|
||||
"auto": "Auto",
|
||||
"count": "{{count}} wide"
|
||||
},
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
@@ -1654,7 +1654,12 @@
|
||||
"gridView": "Grid",
|
||||
"listView": "List",
|
||||
"compactRows": "Compact rows",
|
||||
"comfortableRows": "Comfortable rows"
|
||||
"comfortableRows": "Comfortable rows",
|
||||
"columns": {
|
||||
"title": "Columns",
|
||||
"auto": "Auto",
|
||||
"count": "{{count}} wide"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Saved. Type to replace it."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} is gereed om te installeer",
|
||||
"readyHint": "Dit word die volgende keer wat Termix herbegin geïnstalleer."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Veilige modus",
|
||||
"description": "Slegs uitleg- en tema-plug-ins word uitgevoer. Herbegin die bediener sonder TERMIX_SAFE_MODE om die res weer aan te skakel."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Maak paneel toe",
|
||||
"moveSide": "Skuif paneel",
|
||||
@@ -1098,6 +1102,8 @@
|
||||
"resetEveryone": "Stel die verstek vir almal terug",
|
||||
"update": "Dateer {{name}} op na {{version}}",
|
||||
"disable": "Skakel {{name}} af",
|
||||
"enable": "Skakel {{name}} aan",
|
||||
"startAgain": "Begin {{name}} weer",
|
||||
"install": "Vind lêe en temas",
|
||||
"installTitle": "Lêe en temas",
|
||||
"installButton": "Installeer",
|
||||
@@ -1309,7 +1315,8 @@
|
||||
"disabled": "Afgeskakel",
|
||||
"failed": "Onverwags gestop",
|
||||
"blocked": "Wag op 'n ander plugin",
|
||||
"server": "Loop op jou bediener"
|
||||
"server": "Loop op jou bediener",
|
||||
"safeMode": "Af in veilige modus"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Aktief",
|
||||
@@ -1323,11 +1330,6 @@
|
||||
"updated": "Onlangs opgedateer",
|
||||
"category": "Kategorie"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Kolomme",
|
||||
"auto": "Outo",
|
||||
"count": "{{count}} breed"
|
||||
},
|
||||
"table": {
|
||||
"name": "Naam",
|
||||
"description": "Beskrywing",
|
||||
@@ -1652,7 +1654,12 @@
|
||||
"gridView": "Rooster",
|
||||
"listView": "Lys",
|
||||
"compactRows": "Kompakte rye",
|
||||
"comfortableRows": "Gerieflike rye"
|
||||
"comfortableRows": "Gerieflike rye",
|
||||
"columns": {
|
||||
"title": "Kolomme",
|
||||
"auto": "Outo",
|
||||
"count": "{{count}} breed"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Opgeslaan. Tik om dit te vervang."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} جاهز للتثبيت",
|
||||
"readyHint": "سيتم تثبيته في المرة القادمة التي يعاد فيها تشغيل Termix."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "الوضع الآمن",
|
||||
"description": "لا تعمل سوى إضافات التخطيط والمظهر. أعد تشغيل الخادم بدون TERMIX_SAFE_MODE لإعادة تشغيل الباقي."
|
||||
},
|
||||
"companion": {
|
||||
"close": "إغلاق اللوحة",
|
||||
"moveSide": "نقل اللوحة",
|
||||
@@ -1166,6 +1170,8 @@
|
||||
"resetEveryone": "إعادة تعيين الافتراضي للجميع",
|
||||
"update": "تحديث {{name}} إلى {{version}}",
|
||||
"disable": "إيقاف {{name}}",
|
||||
"enable": "تفعيل {{name}}",
|
||||
"startAgain": "إعادة تشغيل {{name}}",
|
||||
"install": "البحث عن التخطيطات والسمات",
|
||||
"installTitle": "التخطيطات والسمات",
|
||||
"installButton": "تثبيت",
|
||||
@@ -1377,7 +1383,8 @@
|
||||
"disabled": "معطلة",
|
||||
"failed": "توقفت بشكل غير متوقع",
|
||||
"blocked": "في انتظار إضافة أخرى",
|
||||
"server": "يعمل على خادمك"
|
||||
"server": "يعمل على خادمك",
|
||||
"safeMode": "معطل في الوضع الآمن"
|
||||
},
|
||||
"groups": {
|
||||
"running": "تعمل",
|
||||
@@ -1391,11 +1398,6 @@
|
||||
"updated": "تم تحديثها مؤخراً",
|
||||
"category": "الفئة"
|
||||
},
|
||||
"columns": {
|
||||
"title": "الأعمدة",
|
||||
"auto": "تلقائي",
|
||||
"count": "عرض {{count}}"
|
||||
},
|
||||
"table": {
|
||||
"name": "الاسم",
|
||||
"description": "الوصف",
|
||||
@@ -1820,7 +1822,12 @@
|
||||
"gridView": "شبكة",
|
||||
"listView": "قائمة",
|
||||
"compactRows": "صفوف مدمجة",
|
||||
"comfortableRows": "صفوف مريحة"
|
||||
"comfortableRows": "صفوف مريحة",
|
||||
"columns": {
|
||||
"title": "الأعمدة",
|
||||
"auto": "تلقائي",
|
||||
"count": "عرض {{count}}"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "تم الحفظ. اكتب لاستبداله."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} е готов за инсталация",
|
||||
"readyHint": "Ще се инсталира следващия път, когато Termix се рестартира."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Режим на безопасност",
|
||||
"description": "Само плъгините за подредба и тема са активни. Рестартирайте сървъра без променливата TERMIX_SAFE_MODE, за да активирате останалите."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Затвори панела",
|
||||
"moveSide": "Премести панела",
|
||||
@@ -1098,6 +1102,8 @@
|
||||
"resetEveryone": "Върни настройките по подразбиране за всички",
|
||||
"update": "Актуализирай {{name}} до {{version}}",
|
||||
"disable": "Изключи {{name}}",
|
||||
"enable": "Активиране на {{name}}",
|
||||
"startAgain": "Стартиране на {{name}} отново",
|
||||
"install": "Намери подредби и теми",
|
||||
"installTitle": "Подредби и теми",
|
||||
"installButton": "Инсталирай",
|
||||
@@ -1309,7 +1315,8 @@
|
||||
"disabled": "Изключена",
|
||||
"failed": "Спряна неочаквано",
|
||||
"blocked": "Чака друга добавка",
|
||||
"server": "Работи на вашия сървър"
|
||||
"server": "Работи на вашия сървър",
|
||||
"safeMode": "Изключен в режим на безопасност"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Работещи",
|
||||
@@ -1323,11 +1330,6 @@
|
||||
"updated": "Наскоро актуализирани",
|
||||
"category": "Категория"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Колони",
|
||||
"auto": "Автоматично",
|
||||
"count": "{{count}} колони"
|
||||
},
|
||||
"table": {
|
||||
"name": "Име",
|
||||
"description": "Описание",
|
||||
@@ -1652,7 +1654,12 @@
|
||||
"gridView": "Мрежа",
|
||||
"listView": "Списък",
|
||||
"compactRows": "Компактни редове",
|
||||
"comfortableRows": "Комфортни редове"
|
||||
"comfortableRows": "Комфортни редове",
|
||||
"columns": {
|
||||
"title": "Колони",
|
||||
"auto": "Автоматично",
|
||||
"count": "ширина {{count}}"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Запазено. Въведете ново, за да го замените."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} ইনস্টল করার জন্য প্রস্তুত",
|
||||
"readyHint": "এটি পরবর্তীবার Termix রিস্টার্ট করলে ইনস্টল হবে।"
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "সেফ মোড",
|
||||
"description": "শুধুমাত্র লেআউট এবং থিম প্লাগইন চলছে। বাকিগুলো আবার চালু করতে TERMIX_SAFE_MODE ছাড়া সার্ভারটি রিস্টার্ট করুন।"
|
||||
},
|
||||
"companion": {
|
||||
"close": "প্যানেল বন্ধ করুন",
|
||||
"moveSide": "প্যানেল সরান",
|
||||
@@ -1098,6 +1102,8 @@
|
||||
"resetEveryone": "সবার জন্য ডিফল্ট রিসেট করুন",
|
||||
"update": "{{name}} কে {{version}} এ আপডেট করুন",
|
||||
"disable": "{{name}} বন্ধ করুন",
|
||||
"enable": "{{name}} চালু করুন",
|
||||
"startAgain": "{{name}} আবার শুরু করুন",
|
||||
"install": "লেআউট এবং থিম খুঁজুন",
|
||||
"installTitle": "লেআউট এবং থিম",
|
||||
"installButton": "ইনস্টল করুন",
|
||||
@@ -1309,7 +1315,8 @@
|
||||
"disabled": "নিষ্ক্রিয়",
|
||||
"failed": "অপ্রত্যাশিতভাবে থেমে গেছে",
|
||||
"blocked": "অন্য প্লাগইনের জন্য অপেক্ষা করছে",
|
||||
"server": "আপনার সার্ভারে চলে"
|
||||
"server": "আপনার সার্ভারে চলে",
|
||||
"safeMode": "সেফ মোডে বন্ধ"
|
||||
},
|
||||
"groups": {
|
||||
"running": "চলমান",
|
||||
@@ -1323,11 +1330,6 @@
|
||||
"updated": "সাম্প্রতিক আপডেট",
|
||||
"category": "ক্যাটাগরি"
|
||||
},
|
||||
"columns": {
|
||||
"title": "কলাম",
|
||||
"auto": "স্বয়ংক্রিয়",
|
||||
"count": "{{count}} প্রশস্ত"
|
||||
},
|
||||
"table": {
|
||||
"name": "নাম",
|
||||
"description": "বিবরণ",
|
||||
@@ -1652,7 +1654,12 @@
|
||||
"gridView": "গ্রিড",
|
||||
"listView": "তালিকা",
|
||||
"compactRows": "কমপ্যাক্ট সারি",
|
||||
"comfortableRows": "আরামদায়ক সারি"
|
||||
"comfortableRows": "আরামদায়ক সারি",
|
||||
"columns": {
|
||||
"title": "কলাম",
|
||||
"auto": "স্বয়ংক্রিয়",
|
||||
"count": "{{count}} প্রশস্ত"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "সংরক্ষিত হয়েছে। এটি প্রতিস্থাপন করতে টাইপ করুন।"
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} està llest per instal·lar-se",
|
||||
"readyHint": "S'instal·larà la propera vegada que reiniciïs Termix."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Mode segur",
|
||||
"description": "Només s'estan executant els complements de disseny i tema. Reinicia el servidor sense TERMIX_SAFE_MODE per activar la resta."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Tanca el panell",
|
||||
"moveSide": "Mou el panell",
|
||||
@@ -1115,6 +1119,8 @@
|
||||
"resetEveryone": "Restableix el predeterminat per a tothom",
|
||||
"update": "Actualitza {{name}} a {{version}}",
|
||||
"disable": "Desactiva {{name}}",
|
||||
"enable": "Activar {{name}}",
|
||||
"startAgain": "Reiniciar {{name}}",
|
||||
"install": "Cerca dissenys i temes",
|
||||
"installTitle": "Disseny i temes",
|
||||
"installButton": "Instal·la",
|
||||
@@ -1326,7 +1332,8 @@
|
||||
"disabled": "Desactivat",
|
||||
"failed": "Aturat inesperadament",
|
||||
"blocked": "Esperant un altre plugin",
|
||||
"server": "S'executa al teu servidor"
|
||||
"server": "S'executa al teu servidor",
|
||||
"safeMode": "Desactivat en mode segur"
|
||||
},
|
||||
"groups": {
|
||||
"running": "En execució",
|
||||
@@ -1340,11 +1347,6 @@
|
||||
"updated": "Actualitzats recentment",
|
||||
"category": "Categoria"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Columnes",
|
||||
"auto": "Automàtic",
|
||||
"count": "{{count}} d'amplada"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nom",
|
||||
"description": "Descripció",
|
||||
@@ -1694,7 +1696,12 @@
|
||||
"gridView": "Graella",
|
||||
"listView": "Llista",
|
||||
"compactRows": "Files compactes",
|
||||
"comfortableRows": "Files còmodes"
|
||||
"comfortableRows": "Files còmodes",
|
||||
"columns": {
|
||||
"title": "Columnes",
|
||||
"auto": "Automàtic",
|
||||
"count": "{{count}} d'ample"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Desat. Escriu per substituir-lo."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} je připraven k instalaci",
|
||||
"readyHint": "Nainstaluje se při příštím restartu Termixu."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Režim nouzového spuštění",
|
||||
"description": "Spouštějí se pouze zásuvné moduly rozložení a motivu. Restartujte server bez proměnné TERMIX_SAFE_MODE, abyste znovu povolili zbytek."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Zavřít panel",
|
||||
"moveSide": "Přesunout panel",
|
||||
@@ -1132,6 +1136,8 @@
|
||||
"resetEveryone": "Obnovit výchozí nastavení pro všechny",
|
||||
"update": "Aktualizovat {{name}} na {{version}}",
|
||||
"disable": "Vypnout {{name}}",
|
||||
"enable": "Zapnout {{name}}",
|
||||
"startAgain": "Spustit {{name}} znovu",
|
||||
"install": "Hledat rozložení a motivy",
|
||||
"installTitle": "Rozložení a motivy",
|
||||
"installButton": "Instalovat",
|
||||
@@ -1343,7 +1349,8 @@
|
||||
"disabled": "Zakázáno",
|
||||
"failed": "Neočekávaně zastaveno",
|
||||
"blocked": "Čeká na jiný plugin",
|
||||
"server": "Běží na vašem serveru"
|
||||
"server": "Běží na vašem serveru",
|
||||
"safeMode": "Vypnuto v režimu nouzového spuštění"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Spuštěné",
|
||||
@@ -1357,11 +1364,6 @@
|
||||
"updated": "Nedávno aktualizované",
|
||||
"category": "Kategorie"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Sloupce",
|
||||
"auto": "Automaticky",
|
||||
"count": "Šířka {{count}}"
|
||||
},
|
||||
"table": {
|
||||
"name": "Název",
|
||||
"description": "Popis",
|
||||
@@ -1736,7 +1738,12 @@
|
||||
"gridView": "Mřížka",
|
||||
"listView": "Seznam",
|
||||
"compactRows": "Kompaktní řádky",
|
||||
"comfortableRows": "Pohodlné řádky"
|
||||
"comfortableRows": "Pohodlné řádky",
|
||||
"columns": {
|
||||
"title": "Sloupce",
|
||||
"auto": "Automaticky",
|
||||
"count": "šířka {{count}}"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Uloženo. Napište pro nahrazení."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} er klar til installation",
|
||||
"readyHint": "Den installeres næste gang Termix genstartes."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Sikker tilstand",
|
||||
"description": "Kun layout- og tema-plugins kører. Genstart serveren uden TERMIX_SAFE_MODE for at aktivere de øvrige plugins igen."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Luk panel",
|
||||
"moveSide": "Flyt panel",
|
||||
@@ -1098,6 +1102,8 @@
|
||||
"resetEveryone": "Nulstil standardindstillingen for alle",
|
||||
"update": "Opdater {{name}} til {{version}}",
|
||||
"disable": "Sluk {{name}}",
|
||||
"enable": "Aktivér {{name}}",
|
||||
"startAgain": "Start {{name}} igen",
|
||||
"install": "Find layouts og temaer",
|
||||
"installTitle": "Layouts og temaer",
|
||||
"installButton": "Installer",
|
||||
@@ -1309,7 +1315,8 @@
|
||||
"disabled": "Deaktiveret",
|
||||
"failed": "Stoppet uventet",
|
||||
"blocked": "Venter på et andet plugin",
|
||||
"server": "Kører på din server"
|
||||
"server": "Kører på din server",
|
||||
"safeMode": "Deaktiveret i sikker tilstand"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Kører",
|
||||
@@ -1323,11 +1330,6 @@
|
||||
"updated": "Nyligt opdateret",
|
||||
"category": "Kategori"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Kolonner",
|
||||
"auto": "Auto",
|
||||
"count": "{{count}} bred"
|
||||
},
|
||||
"table": {
|
||||
"name": "Navn",
|
||||
"description": "Beskrivelse",
|
||||
@@ -1652,7 +1654,12 @@
|
||||
"gridView": "Gitter",
|
||||
"listView": "Liste",
|
||||
"compactRows": "Kompakte rækker",
|
||||
"comfortableRows": "Komfortable rækker"
|
||||
"comfortableRows": "Komfortable rækker",
|
||||
"columns": {
|
||||
"title": "Søjler",
|
||||
"auto": "Automatisk",
|
||||
"count": "{{count}} bred"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Gemt. Skriv for at erstatte det."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} ist bereit zur Installation",
|
||||
"readyHint": "Es wird beim nächsten Neustart von Termix installiert."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Sicherer Modus",
|
||||
"description": "Nur Layout- und Themen-Plugins sind aktiv. Starten Sie den Server ohne TERMIX_SAFE_MODE neu, um die restlichen Plugins wieder zu aktivieren."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Panel schließen",
|
||||
"moveSide": "Panel verschieben",
|
||||
@@ -1098,6 +1102,8 @@
|
||||
"resetEveryone": "Standard für alle zurücksetzen",
|
||||
"update": "{{name}} auf {{version}} aktualisieren",
|
||||
"disable": "{{name}} deaktivieren",
|
||||
"enable": "{{name}} aktivieren",
|
||||
"startAgain": "{{name}} neu starten",
|
||||
"install": "Layouts und Themes finden",
|
||||
"installTitle": "Layouts und Themes",
|
||||
"installButton": "Installieren",
|
||||
@@ -1309,7 +1315,8 @@
|
||||
"disabled": "Deaktiviert",
|
||||
"failed": "Unerwartet gestoppt",
|
||||
"blocked": "Wartet auf ein anderes Plugin",
|
||||
"server": "Läuft auf deinem Server"
|
||||
"server": "Läuft auf deinem Server",
|
||||
"safeMode": "Im sicheren Modus deaktiviert"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Aktiv",
|
||||
@@ -1323,11 +1330,6 @@
|
||||
"updated": "Kürzlich aktualisiert",
|
||||
"category": "Kategorie"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Spalten",
|
||||
"auto": "Auto",
|
||||
"count": "{{count}} Spalten breit"
|
||||
},
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"description": "Beschreibung",
|
||||
@@ -1652,7 +1654,12 @@
|
||||
"gridView": "Raster",
|
||||
"listView": "Liste",
|
||||
"compactRows": "Kompakte Zeilen",
|
||||
"comfortableRows": "Bequeme Zeilen"
|
||||
"comfortableRows": "Bequeme Zeilen",
|
||||
"columns": {
|
||||
"title": "Spalten",
|
||||
"auto": "Automatisch",
|
||||
"count": "{{count}} Spalten breit"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Gespeichert. Tippen, um es zu ersetzen."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Το Termix {{version}} είναι έτοιμο για εγκατάσταση",
|
||||
"readyHint": "Εγκαθίσταται την επόμενη φορά που θα επανεκκινήσει το Termix."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Λειτουργία ασφαλείας",
|
||||
"description": "Τρέχουν μόνο οι προσθήκες διάταξης και θέματος. Επανεκκινήστε τον διακομιστή χωρίς το TERMIX_SAFE_MODE για να ενεργοποιήσετε ξανά τις υπόλοιπες."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Κλείσιμο πάνελ",
|
||||
"moveSide": "Μετακίνηση πάνελ",
|
||||
@@ -1098,6 +1102,8 @@
|
||||
"resetEveryone": "Επαναφορά της προεπιλογής για όλους",
|
||||
"update": "Ενημέρωση του {{name}} στην έκδοση {{version}}",
|
||||
"disable": "Απενεργοποίηση του {{name}}",
|
||||
"enable": "Ενεργοποίηση {{name}}",
|
||||
"startAgain": "Εκκίνηση {{name}} ξανά",
|
||||
"install": "Εύρεση layouts και θεμάτων",
|
||||
"installTitle": "Layouts και θέματα",
|
||||
"installButton": "Εγκατάσταση",
|
||||
@@ -1309,7 +1315,8 @@
|
||||
"disabled": "Απενεργοποιημένο",
|
||||
"failed": "Διακόπηκε απροσδόκητα",
|
||||
"blocked": "Περιμένει άλλο plugin",
|
||||
"server": "Τρέχει στον διακομιστή σας"
|
||||
"server": "Τρέχει στον διακομιστή σας",
|
||||
"safeMode": "Ανενεργό στη λειτουργία ασφαλείας"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Εν ενεργεία",
|
||||
@@ -1323,11 +1330,6 @@
|
||||
"updated": "Πρόσφατα ενημερωμένα",
|
||||
"category": "Κατηγορία"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Στήλες",
|
||||
"auto": "Αυτόματο",
|
||||
"count": "{{count}} στήλες"
|
||||
},
|
||||
"table": {
|
||||
"name": "Όνομα",
|
||||
"description": "Περιγραφή",
|
||||
@@ -1652,7 +1654,12 @@
|
||||
"gridView": "Πλέγμα",
|
||||
"listView": "Λίστα",
|
||||
"compactRows": "Συμπαγείς γραμμές",
|
||||
"comfortableRows": "Άνετες γραμμές"
|
||||
"comfortableRows": "Άνετες γραμμές",
|
||||
"columns": {
|
||||
"title": "Στήλες",
|
||||
"auto": "Αυτόματο",
|
||||
"count": "Πλάτος {{count}}"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Αποθηκεύτηκε. Πληκτρολογήστε για να το αντικαταστήσετε."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} está listo para instalar",
|
||||
"readyHint": "Se instalará la próxima vez que reinicies Termix."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Modo seguro",
|
||||
"description": "Solo se están ejecutando los complementos de diseño y tema. Reinicia el servidor sin TERMIX_SAFE_MODE para volver a activar el resto."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Cerrar panel",
|
||||
"moveSide": "Mover panel",
|
||||
@@ -1115,6 +1119,8 @@
|
||||
"resetEveryone": "Restablecer el valor predeterminado para todos",
|
||||
"update": "Actualizar {{name}} a {{version}}",
|
||||
"disable": "Desactivar {{name}}",
|
||||
"enable": "Activar {{name}}",
|
||||
"startAgain": "Reiniciar {{name}}",
|
||||
"install": "Buscar diseños y temas",
|
||||
"installTitle": "Diseños y temas",
|
||||
"installButton": "Instalar",
|
||||
@@ -1326,7 +1332,8 @@
|
||||
"disabled": "Desactivado",
|
||||
"failed": "Detenido inesperadamente",
|
||||
"blocked": "Esperando a otro complemento",
|
||||
"server": "Se ejecuta en tu servidor"
|
||||
"server": "Se ejecuta en tu servidor",
|
||||
"safeMode": "Desactivado en modo seguro"
|
||||
},
|
||||
"groups": {
|
||||
"running": "En ejecución",
|
||||
@@ -1340,11 +1347,6 @@
|
||||
"updated": "Actualizados recientemente",
|
||||
"category": "Categoría"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Columnas",
|
||||
"auto": "Automático",
|
||||
"count": "{{count}} de ancho"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nombre",
|
||||
"description": "Descripción",
|
||||
@@ -1694,7 +1696,12 @@
|
||||
"gridView": "Cuadrícula",
|
||||
"listView": "Lista",
|
||||
"compactRows": "Filas compactas",
|
||||
"comfortableRows": "Filas cómodas"
|
||||
"comfortableRows": "Filas cómodas",
|
||||
"columns": {
|
||||
"title": "Columnas",
|
||||
"auto": "Automático",
|
||||
"count": "{{count}} de ancho"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Guardado. Escribe para reemplazarlo."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} on paigaldamiseks valmis",
|
||||
"readyHint": "See paigaldatakse järgmisel Termixi taaskäivitamisel."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Turvarežiim",
|
||||
"description": "Töötavad ainult paigutuse ja teema lisad. Taaskäivitage server ilma TERMIX_SAFE_MODE määramata, et ülejäänud lisad uuesti lubada."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Sulge paneel",
|
||||
"moveSide": "Liiguta paneeli",
|
||||
@@ -1098,6 +1102,8 @@
|
||||
"resetEveryone": "Taasta vaikimisi seadistus kõigile",
|
||||
"update": "Uuenda {{name}} versioonile {{version}}",
|
||||
"disable": "Lülita {{name}} välja",
|
||||
"enable": "Lülita {{name}} sisse",
|
||||
"startAgain": "Käivita {{name}} uuesti",
|
||||
"install": "Otsi paigutusi ja teemasid",
|
||||
"installTitle": "Paigutused ja teemad",
|
||||
"installButton": "Paigalda",
|
||||
@@ -1309,7 +1315,8 @@
|
||||
"disabled": "Keelatud",
|
||||
"failed": "Peatati ootamatult",
|
||||
"blocked": "Ootab teist pistikprogrammi",
|
||||
"server": "Töötab sinu serveris"
|
||||
"server": "Töötab sinu serveris",
|
||||
"safeMode": "Turvarežiimis välja lülitatud"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Käimas",
|
||||
@@ -1323,11 +1330,6 @@
|
||||
"updated": "Hiljuti uuendatud",
|
||||
"category": "Kategooria"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Veerud",
|
||||
"auto": "Automaatne",
|
||||
"count": "{{count}} lai"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nimi",
|
||||
"description": "Kirjeldus",
|
||||
@@ -1652,7 +1654,12 @@
|
||||
"gridView": "Ruudustik",
|
||||
"listView": "Loend",
|
||||
"compactRows": "Kompaktsead read",
|
||||
"comfortableRows": "Mugavad read"
|
||||
"comfortableRows": "Mugavad read",
|
||||
"columns": {
|
||||
"title": "Veerud",
|
||||
"auto": "Automaatne",
|
||||
"count": "{{count}} veeru laiune"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Salvestatud. Tüüpi, et asendada."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} on valmis asennettavaksi",
|
||||
"readyHint": "Se asennetaan seuraavan kerran, kun Termix käynnistetään uudelleen."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Turvatoiminto",
|
||||
"description": "Vain asettelun ja teeman lisäosat ovat käytössä. Käynnistä palvelin uudelleen ilman TERMIX_SAFE_MODE-muuttujaa ottaaksesi muut lisäosat takaisin käyttöön."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Sulje paneeli",
|
||||
"moveSide": "Siirrä paneeli",
|
||||
@@ -1098,6 +1102,8 @@
|
||||
"resetEveryone": "Palauta oletus kaikille",
|
||||
"update": "Päivitä {{name}} versioon {{version}}",
|
||||
"disable": "Sammuta {{name}}",
|
||||
"enable": "Ota {{name}} käyttöön",
|
||||
"startAgain": "Käynnistä {{name}} uudelleen",
|
||||
"install": "Etsi asetteluja ja teemoja",
|
||||
"installTitle": "Asettelut ja teemat",
|
||||
"installButton": "Asenna",
|
||||
@@ -1309,7 +1315,8 @@
|
||||
"disabled": "Poistettu käytöstä",
|
||||
"failed": "Pysähtyi odottamatta",
|
||||
"blocked": "Odottaa toista lisäosaa",
|
||||
"server": "Käynnistyy palvelimellasi"
|
||||
"server": "Käynnistyy palvelimellasi",
|
||||
"safeMode": "Pois käytöstä turvatoiminnossa"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Käynnissä",
|
||||
@@ -1323,11 +1330,6 @@
|
||||
"updated": "Viimeksi päivitetty",
|
||||
"category": "Kategoria"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Sarakkeet",
|
||||
"auto": "Automaattinen",
|
||||
"count": "{{count}} saraketta leveä"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nimi",
|
||||
"description": "Kuvaus",
|
||||
@@ -1652,7 +1654,12 @@
|
||||
"gridView": "Ruudukko",
|
||||
"listView": "Lista",
|
||||
"compactRows": "Tiiviit rivit",
|
||||
"comfortableRows": "Mukavat rivit"
|
||||
"comfortableRows": "Mukavat rivit",
|
||||
"columns": {
|
||||
"title": "Sarakkeet",
|
||||
"auto": "Automaattinen",
|
||||
"count": "{{count}} saraketta leveä"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Tallennettu. Kirjoita korvataksesi sen."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} est prêt à être installé",
|
||||
"readyHint": "Il sera installé au prochain redémarrage de Termix."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Mode sans échec",
|
||||
"description": "Seuls les plugins de mise en page et de thème sont actifs. Redémarrez le serveur sans la variable TERMIX_SAFE_MODE pour réactiver les autres."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Fermer le panneau",
|
||||
"moveSide": "Déplacer le panneau",
|
||||
@@ -1115,6 +1119,8 @@
|
||||
"resetEveryone": "Réinitialiser la valeur par défaut pour tout le monde",
|
||||
"update": "Mettre à jour {{name}} vers {{version}}",
|
||||
"disable": "Désactiver {{name}}",
|
||||
"enable": "Activer {{name}}",
|
||||
"startAgain": "Relancer {{name}}",
|
||||
"install": "Trouver des dispositions et des thèmes",
|
||||
"installTitle": "Dispositions et thèmes",
|
||||
"installButton": "Installer",
|
||||
@@ -1326,7 +1332,8 @@
|
||||
"disabled": "Désactivé",
|
||||
"failed": "Arrêt inattendu",
|
||||
"blocked": "En attente d'un autre plugin",
|
||||
"server": "Exécute sur votre serveur"
|
||||
"server": "Exécute sur votre serveur",
|
||||
"safeMode": "Désactivé en mode sans échec"
|
||||
},
|
||||
"groups": {
|
||||
"running": "En cours d'exécution",
|
||||
@@ -1340,11 +1347,6 @@
|
||||
"updated": "Récemment mis à jour",
|
||||
"category": "Catégorie"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Colonnes",
|
||||
"auto": "Auto",
|
||||
"count": "{{count}} de large"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nom",
|
||||
"description": "Description",
|
||||
@@ -1694,7 +1696,12 @@
|
||||
"gridView": "Grille",
|
||||
"listView": "Liste",
|
||||
"compactRows": "Lignes compactes",
|
||||
"comfortableRows": "Lignes confortables"
|
||||
"comfortableRows": "Lignes confortables",
|
||||
"columns": {
|
||||
"title": "Colonnes",
|
||||
"auto": "Auto",
|
||||
"count": "{{count}} de large"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Enregistré. Tapez pour le remplacer."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} מוכן להתקנה",
|
||||
"readyHint": "הוא מותקן בפעם הבאה ש-Termix מופעל מחדש."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "מצב בטוח",
|
||||
"description": "רק תוספי פריסה ועיצוב פועלים. הפעל מחדש את השרת ללא המשתנה TERMIX_SAFE_MODE כדי להפעיל מחדש את שאר התוספים."
|
||||
},
|
||||
"companion": {
|
||||
"close": "סגור לוח",
|
||||
"moveSide": "הזז לוח",
|
||||
@@ -1115,6 +1119,8 @@
|
||||
"resetEveryone": "אפס את ברירת המחדל לכולם",
|
||||
"update": "עדכן את {{name}} ל-{{version}}",
|
||||
"disable": "כבה את {{name}}",
|
||||
"enable": "הפעל את {{name}}",
|
||||
"startAgain": "הפעל מחדש את {{name}}",
|
||||
"install": "חפש פריסות וערכות נושא",
|
||||
"installTitle": "פריסות וערכות נושא",
|
||||
"installButton": "התקן",
|
||||
@@ -1326,7 +1332,8 @@
|
||||
"disabled": "מושבת",
|
||||
"failed": "הופסק ללא ציפייה",
|
||||
"blocked": "ממתין לתוסף אחר",
|
||||
"server": "רץ על השרת שלך"
|
||||
"server": "רץ על השרת שלך",
|
||||
"safeMode": "כבוי במצב בטוח"
|
||||
},
|
||||
"groups": {
|
||||
"running": "פעילים",
|
||||
@@ -1340,11 +1347,6 @@
|
||||
"updated": "עודכן לאחרונה",
|
||||
"category": "קטגוריה"
|
||||
},
|
||||
"columns": {
|
||||
"title": "עמודות",
|
||||
"auto": "אוטומטי",
|
||||
"count": "רוחב של {{count}}"
|
||||
},
|
||||
"table": {
|
||||
"name": "שם",
|
||||
"description": "תיאור",
|
||||
@@ -1694,7 +1696,12 @@
|
||||
"gridView": "רשת",
|
||||
"listView": "רשימה",
|
||||
"compactRows": "שורות צפופות",
|
||||
"comfortableRows": "שורות נוחות"
|
||||
"comfortableRows": "שורות נוחות",
|
||||
"columns": {
|
||||
"title": "עמודות",
|
||||
"auto": "אוטומטי",
|
||||
"count": "רוחב של {{count}}"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "נשמר. הקלד כדי להחליף אותו."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} इंस्टॉल करने के लिए तैयार है",
|
||||
"readyHint": "यह अगली बार जब Termix पुनरारंभ होगा तब इंस्टॉल होगा।"
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "सुरक्षित मोड",
|
||||
"description": "केवल लेआउट और थीम प्लगइन चल रहे हैं। बाकी सब कुछ वापस चालू करने के लिए TERMIX_SAFE_MODE के बिना सर्वर को पुनः आरंभ करें।"
|
||||
},
|
||||
"companion": {
|
||||
"close": "पैनल बंद करें",
|
||||
"moveSide": "पैनल स्थानांतरित करें",
|
||||
@@ -1098,6 +1102,8 @@
|
||||
"resetEveryone": "सभी के लिए डिफ़ॉल्ट रीसेट करें",
|
||||
"update": "{{name}} को {{version}} पर अपडेट करें",
|
||||
"disable": "{{name}} बंद करें",
|
||||
"enable": "{{name}} चालू करें",
|
||||
"startAgain": "{{name}} को फिर से शुरू करें",
|
||||
"install": "लेआउट और थीम खोजें",
|
||||
"installTitle": "लेआउट और थीम",
|
||||
"installButton": "इंस्टॉल करें",
|
||||
@@ -1309,7 +1315,8 @@
|
||||
"disabled": "अक्षम",
|
||||
"failed": "अप्रत्याशित रूप से रुका",
|
||||
"blocked": "दूसरे प्लगइन का इंतज़ार कर रहा है",
|
||||
"server": "आपके सर्वर पर चलता है"
|
||||
"server": "आपके सर्वर पर चलता है",
|
||||
"safeMode": "सुरक्षित मोड में बंद"
|
||||
},
|
||||
"groups": {
|
||||
"running": "चल रहे",
|
||||
@@ -1323,11 +1330,6 @@
|
||||
"updated": "हाल ही में अपडेट किया गया",
|
||||
"category": "श्रेणी"
|
||||
},
|
||||
"columns": {
|
||||
"title": "कॉलम",
|
||||
"auto": "स्वचालित",
|
||||
"count": "{{count}} चौड़ा"
|
||||
},
|
||||
"table": {
|
||||
"name": "नाम",
|
||||
"description": "विवरण",
|
||||
@@ -1652,7 +1654,12 @@
|
||||
"gridView": "ग्रिड",
|
||||
"listView": "सूची",
|
||||
"compactRows": "संक्षिप्त पंक्तियाँ",
|
||||
"comfortableRows": "आरामदायक पंक्तियाँ"
|
||||
"comfortableRows": "आरामदायक पंक्तियाँ",
|
||||
"columns": {
|
||||
"title": "कॉलम",
|
||||
"auto": "स्वचालित",
|
||||
"count": "{{count}} चौड़ा"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "सहेजा गया। इसे बदलने के लिए टाइप करें।"
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} je spreman za instalaciju",
|
||||
"readyHint": "Instalirat će se sljedeći put kada se Termix ponovno pokrene."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Sigurni način rada",
|
||||
"description": "Pokreću se samo dodaci za raspored i temu. Ponovno pokrenite poslužitelj bez postavke TERMIX_SAFE_MODE da biste ponovno omogućili ostale dodatke."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Zatvori panel",
|
||||
"moveSide": "Pomakni panel",
|
||||
@@ -1115,6 +1119,8 @@
|
||||
"resetEveryone": "Vrati zadano za sve",
|
||||
"update": "Ažuriraj {{name}} na {{version}}",
|
||||
"disable": "Isključi {{name}}",
|
||||
"enable": "Uključi {{name}}",
|
||||
"startAgain": "Ponovno pokreni {{name}}",
|
||||
"install": "Pronađi rasporede i teme",
|
||||
"installTitle": "Rasporedi i teme",
|
||||
"installButton": "Instaliraj",
|
||||
@@ -1326,7 +1332,8 @@
|
||||
"disabled": "Onemogućeno",
|
||||
"failed": "Neočekivano zaustavljeno",
|
||||
"blocked": "Čeka drugi dodatak",
|
||||
"server": "Radi na vašem poslužitelju"
|
||||
"server": "Radi na vašem poslužitelju",
|
||||
"safeMode": "Isključeno u sigurnom načinu rada"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Pokrenuto",
|
||||
@@ -1340,11 +1347,6 @@
|
||||
"updated": "Nedavno ažurirano",
|
||||
"category": "Kategorija"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Stupci",
|
||||
"auto": "Automatski",
|
||||
"count": "{{count}} široko"
|
||||
},
|
||||
"table": {
|
||||
"name": "Naziv",
|
||||
"description": "Opis",
|
||||
@@ -1694,7 +1696,12 @@
|
||||
"gridView": "Mreža",
|
||||
"listView": "Popis",
|
||||
"compactRows": "Kompaktni redovi",
|
||||
"comfortableRows": "Prostrani redovi"
|
||||
"comfortableRows": "Prostrani redovi",
|
||||
"columns": {
|
||||
"title": "Stupci",
|
||||
"auto": "Automatski",
|
||||
"count": "Širina: {{count}}"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Spremljeno. Upišite za zamjenu."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "A Termix {{version}} verziója készen áll a telepítésre",
|
||||
"readyHint": "A következő újraindításkor települ."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Biztonságos mód",
|
||||
"description": "Csak a elrendezés- és témabővítmények futnak. Indítsa újra a szervert a TERMIX_SAFE_MODE nélkül, hogy a többit is újra bekapcsolja."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Panel bezárása",
|
||||
"moveSide": "Panel áthelyezése",
|
||||
@@ -1098,6 +1102,8 @@
|
||||
"resetEveryone": "Az alapértelmezett visszaállítása mindenkinek",
|
||||
"update": "{{name}} frissítése a {{version}} verzióra",
|
||||
"disable": "{{name}} kikapcsolása",
|
||||
"enable": "{{name}} bekapcsolása",
|
||||
"startAgain": "{{name}} újraindítása",
|
||||
"install": "Elrendezések és témák keresése",
|
||||
"installTitle": "Elrendezések és témák",
|
||||
"installButton": "Telepítés",
|
||||
@@ -1309,7 +1315,8 @@
|
||||
"disabled": "Letiltva",
|
||||
"failed": "Váratlanul leállt",
|
||||
"blocked": "Várakozás egy másik bővítményre",
|
||||
"server": "A szerverén fut"
|
||||
"server": "A szerverén fut",
|
||||
"safeMode": "Kikapcsolva biztonságos módban"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Fut",
|
||||
@@ -1323,11 +1330,6 @@
|
||||
"updated": "Legutóbb frissítve",
|
||||
"category": "Kategória"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Oszlopok",
|
||||
"auto": "Automatikus",
|
||||
"count": "{{count}} oszlop széles"
|
||||
},
|
||||
"table": {
|
||||
"name": "Név",
|
||||
"description": "Leírás",
|
||||
@@ -1652,7 +1654,12 @@
|
||||
"gridView": "Rács",
|
||||
"listView": "Lista",
|
||||
"compactRows": "Kompakt sorok",
|
||||
"comfortableRows": "Kényelmes sorok"
|
||||
"comfortableRows": "Kényelmes sorok",
|
||||
"columns": {
|
||||
"title": "Oszlopok",
|
||||
"auto": "Automatikus",
|
||||
"count": "{{count}} oszlop széles"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Mentve. Írjon be új értéket a helyettesítéshez."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} siap untuk diinstal",
|
||||
"readyHint": "Ini akan diinstal saat Termix dimulai ulang berikutnya."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Mode aman",
|
||||
"description": "Hanya plugin tata letak dan tema yang berjalan. Mulai ulang server tanpa TERMIX_SAFE_MODE untuk mengaktifkan kembali yang lainnya."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Tutup panel",
|
||||
"moveSide": "Pindahkan panel",
|
||||
@@ -1081,6 +1085,8 @@
|
||||
"resetEveryone": "Atur ulang default untuk semua orang",
|
||||
"update": "Perbarui {{name}} ke {{version}}",
|
||||
"disable": "Matikan {{name}}",
|
||||
"enable": "Aktifkan {{name}}",
|
||||
"startAgain": "Mulai ulang {{name}}",
|
||||
"install": "Cari tata letak dan tema",
|
||||
"installTitle": "Tata letak dan tema",
|
||||
"installButton": "Instal",
|
||||
@@ -1292,7 +1298,8 @@
|
||||
"disabled": "Dinonaktifkan",
|
||||
"failed": "Berhenti secara tak terduga",
|
||||
"blocked": "Menunggu plugin lain",
|
||||
"server": "Berjalan di server Anda"
|
||||
"server": "Berjalan di server Anda",
|
||||
"safeMode": "Nonaktif di mode aman"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Berjalan",
|
||||
@@ -1306,11 +1313,6 @@
|
||||
"updated": "Baru diperbarui",
|
||||
"category": "Kategori"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Kolom",
|
||||
"auto": "Otomatis",
|
||||
"count": "Lebar {{count}}"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nama",
|
||||
"description": "Deskripsi",
|
||||
@@ -1610,7 +1612,12 @@
|
||||
"gridView": "Grid",
|
||||
"listView": "Daftar",
|
||||
"compactRows": "Baris ringkas",
|
||||
"comfortableRows": "Baris nyaman"
|
||||
"comfortableRows": "Baris nyaman",
|
||||
"columns": {
|
||||
"title": "Kolom",
|
||||
"auto": "Otomatis",
|
||||
"count": "Lebar {{count}}"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Tersimpan. Ketik untuk menggantinya."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} è pronto per l'installazione",
|
||||
"readyHint": "Verrà installato al prossimo riavvio di Termix."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Modalità sicura",
|
||||
"description": "Sono attivi solo i plugin per layout e temi. Riavvia il server senza TERMIX_SAFE_MODE per riattivare gli altri."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Chiudi pannello",
|
||||
"moveSide": "Sposta pannello",
|
||||
@@ -1115,6 +1119,8 @@
|
||||
"resetEveryone": "Reimposta il predefinito per tutti",
|
||||
"update": "Aggiorna {{name}} a {{version}}",
|
||||
"disable": "Disattiva {{name}}",
|
||||
"enable": "Attiva {{name}}",
|
||||
"startAgain": "Riavvia {{name}}",
|
||||
"install": "Trova layout e temi",
|
||||
"installTitle": "Layout e temi",
|
||||
"installButton": "Installa",
|
||||
@@ -1326,7 +1332,8 @@
|
||||
"disabled": "Disabilitato",
|
||||
"failed": "Arrestato inaspettatamente",
|
||||
"blocked": "In attesa di un altro plugin",
|
||||
"server": "Eseguito sul tuo server"
|
||||
"server": "Eseguito sul tuo server",
|
||||
"safeMode": "Disattivato in modalità sicura"
|
||||
},
|
||||
"groups": {
|
||||
"running": "In esecuzione",
|
||||
@@ -1340,11 +1347,6 @@
|
||||
"updated": "Aggiornati di recente",
|
||||
"category": "Categoria"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Colonne",
|
||||
"auto": "Automatico",
|
||||
"count": "Larghezza {{count}}"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nome",
|
||||
"description": "Descrizione",
|
||||
@@ -1694,7 +1696,12 @@
|
||||
"gridView": "Griglia",
|
||||
"listView": "Elenco",
|
||||
"compactRows": "Righe compatte",
|
||||
"comfortableRows": "Righe spaziose"
|
||||
"comfortableRows": "Righe spaziose",
|
||||
"columns": {
|
||||
"title": "Colonne",
|
||||
"auto": "Automatico",
|
||||
"count": "{{count}} colonne"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Salvato. Digita per sostituirlo."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} のインストール準備ができました",
|
||||
"readyHint": "Termix を再起動したときにインストールされます。"
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "セーフモード",
|
||||
"description": "レイアウトとテーマのプラグインのみが実行されています。TERMIX_SAFE_MODE を設定せずにサーバーを再起動すると、他のプラグインが再度有効になります。"
|
||||
},
|
||||
"companion": {
|
||||
"close": "パネルを閉じる",
|
||||
"moveSide": "パネルを移動",
|
||||
@@ -1081,6 +1085,8 @@
|
||||
"resetEveryone": "すべてのユーザーのデフォルトをリセット",
|
||||
"update": "{{name}} を {{version}} に更新",
|
||||
"disable": "{{name}} を無効にする",
|
||||
"enable": "{{name}} を有効にする",
|
||||
"startAgain": "{{name}} を再起動する",
|
||||
"install": "レイアウトとテーマを探す",
|
||||
"installTitle": "レイアウトとテーマ",
|
||||
"installButton": "インストール",
|
||||
@@ -1292,7 +1298,8 @@
|
||||
"disabled": "無効",
|
||||
"failed": "予期せず停止",
|
||||
"blocked": "他のプラグインを待機中",
|
||||
"server": "サーバー上で実行"
|
||||
"server": "サーバー上で実行",
|
||||
"safeMode": "セーフモードで無効"
|
||||
},
|
||||
"groups": {
|
||||
"running": "実行中",
|
||||
@@ -1306,11 +1313,6 @@
|
||||
"updated": "最近更新",
|
||||
"category": "カテゴリ"
|
||||
},
|
||||
"columns": {
|
||||
"title": "列",
|
||||
"auto": "自動",
|
||||
"count": "{{count}} 列"
|
||||
},
|
||||
"table": {
|
||||
"name": "名前",
|
||||
"description": "説明",
|
||||
@@ -1610,7 +1612,12 @@
|
||||
"gridView": "グリッド",
|
||||
"listView": "リスト",
|
||||
"compactRows": "コンパクトな行",
|
||||
"comfortableRows": "ゆったりした行"
|
||||
"comfortableRows": "ゆったりした行",
|
||||
"columns": {
|
||||
"title": "カラム",
|
||||
"auto": "自動",
|
||||
"count": "{{count}} カラム"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "保存しました。入力して置き換えてください。"
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} 설치 준비 완료",
|
||||
"readyHint": "Termix가 다음에 재시작될 때 설치됩니다."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "안전 모드",
|
||||
"description": "레이아웃 및 테마 플러그인만 실행 중입니다. TERMIX_SAFE_MODE 없이 서버를 다시 시작하여 나머지 기능을 다시 활성화하세요."
|
||||
},
|
||||
"companion": {
|
||||
"close": "패널 닫기",
|
||||
"moveSide": "패널 이동",
|
||||
@@ -1081,6 +1085,8 @@
|
||||
"resetEveryone": "모두의 기본값 초기화",
|
||||
"update": "{{name}}을(를) {{version}}(으)로 업데이트",
|
||||
"disable": "{{name}} 끄기",
|
||||
"enable": "{{name}} 켜기",
|
||||
"startAgain": "{{name}} 다시 시작",
|
||||
"install": "레이아웃 및 테마 찾기",
|
||||
"installTitle": "레이아웃 및 테마",
|
||||
"installButton": "설치",
|
||||
@@ -1292,7 +1298,8 @@
|
||||
"disabled": "비활성화됨",
|
||||
"failed": "예기치 않게 중지됨",
|
||||
"blocked": "다른 플러그인 대기 중",
|
||||
"server": "서버에서 실행 중"
|
||||
"server": "서버에서 실행 중",
|
||||
"safeMode": "안전 모드에서 꺼짐"
|
||||
},
|
||||
"groups": {
|
||||
"running": "실행 중",
|
||||
@@ -1306,11 +1313,6 @@
|
||||
"updated": "최근 업데이트순",
|
||||
"category": "카테고리순"
|
||||
},
|
||||
"columns": {
|
||||
"title": "열",
|
||||
"auto": "자동",
|
||||
"count": "{{count}}열 너비"
|
||||
},
|
||||
"table": {
|
||||
"name": "이름",
|
||||
"description": "설명",
|
||||
@@ -1610,7 +1612,12 @@
|
||||
"gridView": "그리드",
|
||||
"listView": "목록",
|
||||
"compactRows": "간결한 행",
|
||||
"comfortableRows": "넓은 행"
|
||||
"comfortableRows": "넓은 행",
|
||||
"columns": {
|
||||
"title": "열",
|
||||
"auto": "자동",
|
||||
"count": "{{count}}열 너비"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "저장되었습니다. 입력하여 교체하세요."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} paruošta įdiegimui",
|
||||
"readyHint": "Ji bus įdiegta kitą kartą paleidus Termix iš naujo."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Saugus režimas",
|
||||
"description": "Veikia tik išdėstymo ir temos priedai. Paleiskite serverį iš naujo be kintamojo TERMIX_SAFE_MODE, kad įjungtumėte likusius priedus."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Uždaryti skydelį",
|
||||
"moveSide": "Perkelti skydelį",
|
||||
@@ -1132,6 +1136,8 @@
|
||||
"resetEveryone": "Atstatyti numatytąjį visiems",
|
||||
"update": "Atnaujinti {{name}} į {{version}}",
|
||||
"disable": "Išjungti {{name}}",
|
||||
"enable": "Įjungti „{{name}}“",
|
||||
"startAgain": "Paleisti „{{name}}“ iš naujo",
|
||||
"install": "Ieškoti išdėstymų ir temų",
|
||||
"installTitle": "Išdėstymai ir temos",
|
||||
"installButton": "Įdiegti",
|
||||
@@ -1343,7 +1349,8 @@
|
||||
"disabled": "Išjungta",
|
||||
"failed": "Nenumatomai sustabdyta",
|
||||
"blocked": "Laukiama kito priedo",
|
||||
"server": "Veikia jūsų serveryje"
|
||||
"server": "Veikia jūsų serveryje",
|
||||
"safeMode": "Išjungta saugiu režimu"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Veikia",
|
||||
@@ -1357,11 +1364,6 @@
|
||||
"updated": "Neseniai atnaujinti",
|
||||
"category": "Kategorija"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Stulpeliai",
|
||||
"auto": "Automatiškai",
|
||||
"count": "{{count}} pločio"
|
||||
},
|
||||
"table": {
|
||||
"name": "Pavadinimas",
|
||||
"description": "Aprašymas",
|
||||
@@ -1736,7 +1738,12 @@
|
||||
"gridView": "Tinklelis",
|
||||
"listView": "Sąrašas",
|
||||
"compactRows": "Kompaktiškos eilutės",
|
||||
"comfortableRows": "Patogios eilutės"
|
||||
"comfortableRows": "Patogios eilutės",
|
||||
"columns": {
|
||||
"title": "Stulpeliai",
|
||||
"auto": "Automatiškai",
|
||||
"count": "{{count}} pločio"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Išsaugota. Įveskite, kad pakeistumėte."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} ir gatavs instalēšanai",
|
||||
"readyHint": "Tas tiks instalēts nākamajā reizi, kad Termix tiks restartēts."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Drošais režīms",
|
||||
"description": "Darbojas tikai izkārtojuma un temas spraudņi. Restartējiet serveri bez TERMIX_SAFE_MODE, lai atkal ieslēgtu pārējos."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Aizvērt paneli",
|
||||
"moveSide": "Pārvietot paneli",
|
||||
@@ -1115,6 +1119,8 @@
|
||||
"resetEveryone": "Atiestatīt noklusējumu visiem",
|
||||
"update": "Atjaunināt {{name}} uz {{version}}",
|
||||
"disable": "Izslēgt {{name}}",
|
||||
"enable": "Ieslēgt {{name}}",
|
||||
"startAgain": "Atkārtoti palaist {{name}}",
|
||||
"install": "Atrast izkārtojumus un tēmas",
|
||||
"installTitle": "Izkārtojumi un tēmas",
|
||||
"installButton": "Instalēt",
|
||||
@@ -1326,7 +1332,8 @@
|
||||
"disabled": "Atspējots",
|
||||
"failed": "Gaidāms apstājās",
|
||||
"blocked": "Gaida citu spraudni",
|
||||
"server": "Darbojas jūsu serverī"
|
||||
"server": "Darbojas jūsu serverī",
|
||||
"safeMode": "Izslēgts drošajā režīmā"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Darbojas",
|
||||
@@ -1340,11 +1347,6 @@
|
||||
"updated": "Nesen atjaunināti",
|
||||
"category": "Kategorija"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Kolonnas",
|
||||
"auto": "Automātiski",
|
||||
"count": "{{count}} kolonnu platums"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nosaukums",
|
||||
"description": "Apraksts",
|
||||
@@ -1694,7 +1696,12 @@
|
||||
"gridView": "Režģis",
|
||||
"listView": "Saraksts",
|
||||
"compactRows": "Kompaktas rindas",
|
||||
"comfortableRows": "Plašas rindas"
|
||||
"comfortableRows": "Plašas rindas",
|
||||
"columns": {
|
||||
"title": "Kolonnas",
|
||||
"auto": "Automātiski",
|
||||
"count": "{{count}} kolonnu platums"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Saglabāts. Ierakstiet, lai to aizvietotu."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} sedia untuk dipasang",
|
||||
"readyHint": "Ia akan dipasang pada kali seterusnya Termix dimulakan semula."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Modus selamat",
|
||||
"description": "Hanya plugin susun atur dan tema yang sedang berjalan. Mulakan semula pelayan tanpa TERMIX_SAFE_MODE untuk menghidupkan kembali yang lain."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Tutup panel",
|
||||
"moveSide": "Pindahkan panel",
|
||||
@@ -1081,6 +1085,8 @@
|
||||
"resetEveryone": "Set semula lalai untuk semua orang",
|
||||
"update": "Kemas kini {{name}} ke {{version}}",
|
||||
"disable": "Matikan {{name}}",
|
||||
"enable": "Hidupkan {{name}}",
|
||||
"startAgain": "Mulakan {{name}} semula",
|
||||
"install": "Cari susun atur dan tema",
|
||||
"installTitle": "Susun atur dan tema",
|
||||
"installButton": "Pasang",
|
||||
@@ -1292,7 +1298,8 @@
|
||||
"disabled": "Dilumpuhkan",
|
||||
"failed": "Berhenti secara tidak dijangka",
|
||||
"blocked": "Menunggu plugin lain",
|
||||
"server": "Berjalan pada pelayan anda"
|
||||
"server": "Berjalan pada pelayan anda",
|
||||
"safeMode": "Mati dalam modus selamat"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Berjalan",
|
||||
@@ -1306,11 +1313,6 @@
|
||||
"updated": "Dikemas kini baru-baru ini",
|
||||
"category": "Kategori"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Lajur",
|
||||
"auto": "Auto",
|
||||
"count": "Lebar {{count}}"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nama",
|
||||
"description": "Keterangan",
|
||||
@@ -1610,7 +1612,12 @@
|
||||
"gridView": "Grid",
|
||||
"listView": "Senarai",
|
||||
"compactRows": "Baris padat",
|
||||
"comfortableRows": "Baris selesa"
|
||||
"comfortableRows": "Baris selesa",
|
||||
"columns": {
|
||||
"title": "Lajur",
|
||||
"auto": "Auto",
|
||||
"count": "Lebar {{count}}"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Disimpan. Taip untuk menggantikannya."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} is klaar om te installeren",
|
||||
"readyHint": "Het wordt geïnstalleerd de volgende keer dat Termix herstart."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Veilige modus",
|
||||
"description": "Alleen layout- en thema-plugins zijn actief. Herstart de server zonder TERMIX_SAFE_MODE om de rest weer in te schakelen."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Sluit paneel",
|
||||
"moveSide": "Verplaats paneel",
|
||||
@@ -1098,6 +1102,8 @@
|
||||
"resetEveryone": "Standaard voor iedereen terugzetten",
|
||||
"update": "Update {{name}} naar {{version}}",
|
||||
"disable": "Zet {{name}} uit",
|
||||
"enable": "{{name}} inschakelen",
|
||||
"startAgain": "{{name}} opnieuw starten",
|
||||
"install": "Lay-outs en thema's zoeken",
|
||||
"installTitle": "Lay-outs en thema's",
|
||||
"installButton": "Installeren",
|
||||
@@ -1309,7 +1315,8 @@
|
||||
"disabled": "Uitgeschakeld",
|
||||
"failed": "Onverwacht gestopt",
|
||||
"blocked": "Wachten op een andere plugin",
|
||||
"server": "Wordt uitgevoerd op je server"
|
||||
"server": "Wordt uitgevoerd op je server",
|
||||
"safeMode": "Uitgeschakeld in veilige modus"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Actief",
|
||||
@@ -1323,11 +1330,6 @@
|
||||
"updated": "Recent bijgewerkt",
|
||||
"category": "Categorie"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Kolommen",
|
||||
"auto": "Automatisch",
|
||||
"count": "{{count}} kolommen breed"
|
||||
},
|
||||
"table": {
|
||||
"name": "Naam",
|
||||
"description": "Beschrijving",
|
||||
@@ -1652,7 +1654,12 @@
|
||||
"gridView": "Raster",
|
||||
"listView": "Lijst",
|
||||
"compactRows": "Compacte rijen",
|
||||
"comfortableRows": "Ruime rijen"
|
||||
"comfortableRows": "Ruime rijen",
|
||||
"columns": {
|
||||
"title": "Kolommen",
|
||||
"auto": "Automatisch",
|
||||
"count": "{{count}} kolommen breed"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Opgeslagen. Typ om het te vervangen."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} er klar til installasjon",
|
||||
"readyHint": "Den installeres neste gang Termix starter på nytt."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Sikker modus",
|
||||
"description": "Kun utforming- og tematillegg kjører. Start serveren på nytt uten TERMIX_SAFE_MODE for å slå på resten igjen."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Lukk panel",
|
||||
"moveSide": "Flytt panel",
|
||||
@@ -1098,6 +1102,8 @@
|
||||
"resetEveryone": "Tilbakestill standard for alle",
|
||||
"update": "Oppdater {{name}} til {{version}}",
|
||||
"disable": "Slå av {{name}}",
|
||||
"enable": "Slå på {{name}}",
|
||||
"startAgain": "Start {{name}} på nytt",
|
||||
"install": "Finn oppsett og temaer",
|
||||
"installTitle": "Oppsett og temaer",
|
||||
"installButton": "Installer",
|
||||
@@ -1309,7 +1315,8 @@
|
||||
"disabled": "Deaktivert",
|
||||
"failed": "Stoppet uventet",
|
||||
"blocked": "Venter på et annet plugin",
|
||||
"server": "Kjører på serveren din"
|
||||
"server": "Kjører på serveren din",
|
||||
"safeMode": "Av i sikker modus"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Kjører",
|
||||
@@ -1323,11 +1330,6 @@
|
||||
"updated": "Nylig oppdatert",
|
||||
"category": "Kategori"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Kolonner",
|
||||
"auto": "Auto",
|
||||
"count": "{{count}} bred"
|
||||
},
|
||||
"table": {
|
||||
"name": "Navn",
|
||||
"description": "Beskrivelse",
|
||||
@@ -1652,7 +1654,12 @@
|
||||
"gridView": "Rutenett",
|
||||
"listView": "Liste",
|
||||
"compactRows": "Kompakte rader",
|
||||
"comfortableRows": "Komfortable rader"
|
||||
"comfortableRows": "Komfortable rader",
|
||||
"columns": {
|
||||
"title": "Kolonner",
|
||||
"auto": "Auto",
|
||||
"count": "{{count}} bred"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Lagret. Skriv for å erstatte den."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} jest gotowy do instalacji",
|
||||
"readyHint": "Zostanie zainstalowany przy następnym ponownym uruchomieniu Termix."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Tryb awaryjny",
|
||||
"description": "Uruchomione są tylko wtyczki układu i motywu. Uruchom ponownie serwer bez zmiennej środowiskowej TERMIX_SAFE_MODE, aby włączyć pozostałe wtyczki."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Zamknij panel",
|
||||
"moveSide": "Przenieś panel",
|
||||
@@ -1132,6 +1136,8 @@
|
||||
"resetEveryone": "Przywróć domyślne ustawienia dla wszystkich",
|
||||
"update": "Aktualizuj {{name}} do wersji {{version}}",
|
||||
"disable": "Wyłącz {{name}}",
|
||||
"enable": "Włącz {{name}}",
|
||||
"startAgain": "Uruchom ponownie {{name}}",
|
||||
"install": "Szukaj układów i motywów",
|
||||
"installTitle": "Układy i motywy",
|
||||
"installButton": "Zainstaluj",
|
||||
@@ -1343,7 +1349,8 @@
|
||||
"disabled": "Wyłączone",
|
||||
"failed": "Zatrzymane nieoczekiwanie",
|
||||
"blocked": "Oczekiwanie na inną wtyczkę",
|
||||
"server": "Uruchamiane na Twoim serwerze"
|
||||
"server": "Uruchamiane na Twoim serwerze",
|
||||
"safeMode": "Wyłączone w trybie awaryjnym"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Uruchomione",
|
||||
@@ -1357,11 +1364,6 @@
|
||||
"updated": "Ostatnio zaktualizowane",
|
||||
"category": "Kategoria"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Kolumny",
|
||||
"auto": "Automatycznie",
|
||||
"count": "{{count}} kolumn"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nazwa",
|
||||
"description": "Opis",
|
||||
@@ -1736,7 +1738,12 @@
|
||||
"gridView": "Siatka",
|
||||
"listView": "Lista",
|
||||
"compactRows": "Zwięzłe wiersze",
|
||||
"comfortableRows": "Przestrzenne wiersze"
|
||||
"comfortableRows": "Przestrzenne wiersze",
|
||||
"columns": {
|
||||
"title": "Kolumny",
|
||||
"auto": "Automatycznie",
|
||||
"count": "Szerokość: {{count}}"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Zapisano. Wpisz nową wartość, aby ją zmienić."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} está pronto para instalação",
|
||||
"readyHint": "Será instalado na próxima vez que o Termix reiniciar."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Modo de segurança",
|
||||
"description": "Apenas os plugins de layout e tema estão ativos. Reinicie o servidor sem a variável TERMIX_SAFE_MODE para reativar os demais."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Fechar painel",
|
||||
"moveSide": "Mover painel",
|
||||
@@ -1115,6 +1119,8 @@
|
||||
"resetEveryone": "Restaurar o padrão para todos",
|
||||
"update": "Atualizar {{name}} para {{version}}",
|
||||
"disable": "Desativar {{name}}",
|
||||
"enable": "Ativar {{name}}",
|
||||
"startAgain": "Reiniciar {{name}}",
|
||||
"install": "Encontrar layouts e temas",
|
||||
"installTitle": "Layouts e temas",
|
||||
"installButton": "Instalar",
|
||||
@@ -1326,7 +1332,8 @@
|
||||
"disabled": "Desativado",
|
||||
"failed": "Parado inesperadamente",
|
||||
"blocked": "Aguardando outro plugin",
|
||||
"server": "Executa no seu servidor"
|
||||
"server": "Executa no seu servidor",
|
||||
"safeMode": "Desativado no modo de segurança"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Em execução",
|
||||
@@ -1340,11 +1347,6 @@
|
||||
"updated": "Atualizados recentemente",
|
||||
"category": "Categoria"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Colunas",
|
||||
"auto": "Automático",
|
||||
"count": "{{count}} de largura"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nome",
|
||||
"description": "Descrição",
|
||||
@@ -1694,7 +1696,12 @@
|
||||
"gridView": "Grade",
|
||||
"listView": "Lista",
|
||||
"compactRows": "Linhas compactas",
|
||||
"comfortableRows": "Linhas confortáveis"
|
||||
"comfortableRows": "Linhas confortáveis",
|
||||
"columns": {
|
||||
"title": "Colunas",
|
||||
"auto": "Automático",
|
||||
"count": "{{count}} colunas"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Salvo. Digite para substituí-lo."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "O Termix {{version}} está pronto para instalar",
|
||||
"readyHint": "Será instalado na próxima reinicialização do Termix."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Modo seguro",
|
||||
"description": "Apenas os plugins de layout e tema estão em execução. Reinicie o servidor sem a variável TERMIX_SAFE_MODE para reativar os restantes."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Fechar painel",
|
||||
"moveSide": "Mover painel",
|
||||
@@ -1115,6 +1119,8 @@
|
||||
"resetEveryone": "Repor a predefinição para todos",
|
||||
"update": "Atualizar {{name}} para {{version}}",
|
||||
"disable": "Desativar {{name}}",
|
||||
"enable": "Ativar {{name}}",
|
||||
"startAgain": "Reiniciar {{name}}",
|
||||
"install": "Procurar layouts e temas",
|
||||
"installTitle": "Layouts e temas",
|
||||
"installButton": "Instalar",
|
||||
@@ -1326,7 +1332,8 @@
|
||||
"disabled": "Desativado",
|
||||
"failed": "Parado inesperadamente",
|
||||
"blocked": "À espera de outro plugin",
|
||||
"server": "Executa no seu servidor"
|
||||
"server": "Executa no seu servidor",
|
||||
"safeMode": "Desativado no modo seguro"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Em execução",
|
||||
@@ -1340,11 +1347,6 @@
|
||||
"updated": "Atualizados recentemente",
|
||||
"category": "Categoria"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Colunas",
|
||||
"auto": "Automático",
|
||||
"count": "{{count}} de largura"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nome",
|
||||
"description": "Descrição",
|
||||
@@ -1694,7 +1696,12 @@
|
||||
"gridView": "Grelha",
|
||||
"listView": "Lista",
|
||||
"compactRows": "Linhas compactas",
|
||||
"comfortableRows": "Linhas confortáveis"
|
||||
"comfortableRows": "Linhas confortáveis",
|
||||
"columns": {
|
||||
"title": "Colunas",
|
||||
"auto": "Automático",
|
||||
"count": "{{count}} de largura"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Guardado. Escreva para substituir."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} este gata de instalare",
|
||||
"readyHint": "Se instalează data viitoare când Termix se repornește."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Mod sigur",
|
||||
"description": "Doar plugin-urile de aspect și temă sunt active. Repornește serverul fără variabila TERMIX_SAFE_MODE pentru a reactiva restul."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Închide panoul",
|
||||
"moveSide": "Mută panoul",
|
||||
@@ -1115,6 +1119,8 @@
|
||||
"resetEveryone": "Resetează implicitul pentru toți",
|
||||
"update": "Actualizează {{name}} la {{version}}",
|
||||
"disable": "Dezactivează {{name}}",
|
||||
"enable": "Activează {{name}}",
|
||||
"startAgain": "Pornește din nou {{name}}",
|
||||
"install": "Găsește aspecte și teme",
|
||||
"installTitle": "Aspecte și teme",
|
||||
"installButton": "Instalează",
|
||||
@@ -1326,7 +1332,8 @@
|
||||
"disabled": "Dezactivat",
|
||||
"failed": "Oprit neașteptat",
|
||||
"blocked": "Așteaptă un alt plugin",
|
||||
"server": "Rulează pe serverul tău"
|
||||
"server": "Rulează pe serverul tău",
|
||||
"safeMode": "Oprit în modul sigur"
|
||||
},
|
||||
"groups": {
|
||||
"running": "În execuție",
|
||||
@@ -1340,11 +1347,6 @@
|
||||
"updated": "Actualizate recent",
|
||||
"category": "Categorie"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Coloane",
|
||||
"auto": "Automat",
|
||||
"count": "{{count}} coloane"
|
||||
},
|
||||
"table": {
|
||||
"name": "Nume",
|
||||
"description": "Descriere",
|
||||
@@ -1694,7 +1696,12 @@
|
||||
"gridView": "Grilă",
|
||||
"listView": "Listă",
|
||||
"compactRows": "Rânduri compacte",
|
||||
"comfortableRows": "Rânduri confortabile"
|
||||
"comfortableRows": "Rânduri confortabile",
|
||||
"columns": {
|
||||
"title": "Coloane",
|
||||
"auto": "Automat",
|
||||
"count": "{{count}} coloane"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Salvat. Tastează pentru a-l înlocui."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} готова к установке",
|
||||
"readyHint": "Она будет установлена при следующем перезапуске Termix."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Безопасный режим",
|
||||
"description": "Работают только плагины макета и темы. Перезапустите сервер без флага TERMIX_SAFE_MODE, чтобы включить остальные плагины."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Закрыть панель",
|
||||
"moveSide": "Переместить панель",
|
||||
@@ -1132,6 +1136,8 @@
|
||||
"resetEveryone": "Сбросить настройки по умолчанию для всех",
|
||||
"update": "Обновить {{name}} до версии {{version}}",
|
||||
"disable": "Отключить {{name}}",
|
||||
"enable": "Включить {{name}}",
|
||||
"startAgain": "Запустить {{name}} заново",
|
||||
"install": "Найти макеты и темы",
|
||||
"installTitle": "Макеты и темы",
|
||||
"installButton": "Установить",
|
||||
@@ -1343,7 +1349,8 @@
|
||||
"disabled": "Отключен",
|
||||
"failed": "Неожиданно остановлен",
|
||||
"blocked": "Ожидание другого плагина",
|
||||
"server": "Работает на вашем сервере"
|
||||
"server": "Работает на вашем сервере",
|
||||
"safeMode": "Отключено в безопасном режиме"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Запущенные",
|
||||
@@ -1357,11 +1364,6 @@
|
||||
"updated": "Недавно обновленные",
|
||||
"category": "Категория"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Столбцы",
|
||||
"auto": "Авто",
|
||||
"count": "{{count}} столбцов"
|
||||
},
|
||||
"table": {
|
||||
"name": "Имя",
|
||||
"description": "Описание",
|
||||
@@ -1736,7 +1738,12 @@
|
||||
"gridView": "Сетка",
|
||||
"listView": "Список",
|
||||
"compactRows": "Компактные строки",
|
||||
"comfortableRows": "Удобные строки"
|
||||
"comfortableRows": "Удобные строки",
|
||||
"columns": {
|
||||
"title": "Колонки",
|
||||
"auto": "Авто",
|
||||
"count": "{{count}} колонок"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Сохранено. Введите новый текст, чтобы заменить."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} je pripravený na inštaláciu",
|
||||
"readyHint": "Nainštaluje sa pri ďalšom reštarte Termixu."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Režim bezpečnosti",
|
||||
"description": "Bežia len zásuvné moduly rozloženia a témy. Reštartujte server bez premennej TERMIX_SAFE_MODE, aby ste ostatné moduly opäť povolili."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Zatvoriť panel",
|
||||
"moveSide": "Presunúť panel",
|
||||
@@ -1132,6 +1136,8 @@
|
||||
"resetEveryone": "Obnoviť predvolené nastavenie pre všetkých",
|
||||
"update": "Aktualizovať {{name}} na verziu {{version}}",
|
||||
"disable": "Vypnúť {{name}}",
|
||||
"enable": "Povoliť {{name}}",
|
||||
"startAgain": "Spustiť {{name}} znova",
|
||||
"install": "Nájsť rozloženia a témy",
|
||||
"installTitle": "Rozloženia a témy",
|
||||
"installButton": "Nainštalovať",
|
||||
@@ -1343,7 +1349,8 @@
|
||||
"disabled": "Zakázané",
|
||||
"failed": "Neočakávane zastavené",
|
||||
"blocked": "Čaká na iný plugin",
|
||||
"server": "Beží na vašom serveri"
|
||||
"server": "Beží na vašom serveri",
|
||||
"safeMode": "Vypnuté v režime bezpečnosti"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Bežiaci",
|
||||
@@ -1357,11 +1364,6 @@
|
||||
"updated": "Nedávno aktualizované",
|
||||
"category": "Kategória"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Stĺpce",
|
||||
"auto": "Automaticky",
|
||||
"count": "{{count}} stĺpcov široké"
|
||||
},
|
||||
"table": {
|
||||
"name": "Názov",
|
||||
"description": "Popis",
|
||||
@@ -1736,7 +1738,12 @@
|
||||
"gridView": "Mriežka",
|
||||
"listView": "Zoznam",
|
||||
"compactRows": "Kompaktné riadky",
|
||||
"comfortableRows": "Pohodlné riadky"
|
||||
"comfortableRows": "Pohodlné riadky",
|
||||
"columns": {
|
||||
"title": "Stĺpce",
|
||||
"auto": "Automaticky",
|
||||
"count": "Šírka {{count}}"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Uložené. Pre nahradenie zadajte novú hodnotu."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} je pripravljen za namestitev",
|
||||
"readyHint": "Namesti se naslednjič, ko se Termix ponovno zažene."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Varen način",
|
||||
"description": "Zagnani so le vtičniki za postavitev in temo. Ponovno zaženite strežnik brez TERMIX_SAFE_MODE, da ponovno vklopite ostale."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Zapri ploščo",
|
||||
"moveSide": "Premakni ploščo",
|
||||
@@ -1132,6 +1136,8 @@
|
||||
"resetEveryone": "Ponastavi privzeto za vse",
|
||||
"update": "Posodobi {{name}} na {{version}}",
|
||||
"disable": "Izklopi {{name}}",
|
||||
"enable": "Vklopi {{name}}",
|
||||
"startAgain": "Znova zaženi {{name}}",
|
||||
"install": "Poišči razporede in teme",
|
||||
"installTitle": "Razporedi in teme",
|
||||
"installButton": "Namesti",
|
||||
@@ -1343,7 +1349,8 @@
|
||||
"disabled": "Onemogočen",
|
||||
"failed": "Nepričakovano ustavljen",
|
||||
"blocked": "Čaka na drug vtičnik",
|
||||
"server": "Zažene se na vašem strežniku"
|
||||
"server": "Zažene se na vašem strežniku",
|
||||
"safeMode": "Izklopljeno v varnem načinu"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Delujoči",
|
||||
@@ -1357,11 +1364,6 @@
|
||||
"updated": "Nedavno posodobljeni",
|
||||
"category": "Kategorija"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Stolpci",
|
||||
"auto": "Samodejno",
|
||||
"count": "{{count}} širok"
|
||||
},
|
||||
"table": {
|
||||
"name": "Ime",
|
||||
"description": "Opis",
|
||||
@@ -1736,7 +1738,12 @@
|
||||
"gridView": "Mreža",
|
||||
"listView": "Seznam",
|
||||
"compactRows": "Kompaktni vrstice",
|
||||
"comfortableRows": "Udobne vrstice"
|
||||
"comfortableRows": "Udobne vrstice",
|
||||
"columns": {
|
||||
"title": "Stolpci",
|
||||
"auto": "Samodejno",
|
||||
"count": "{{count}} širok"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Shranjeno. Vnesite za zamenjavo."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} је спреман за инсталацију",
|
||||
"readyHint": "Инсталира се следећи пут када се Termix поново покрене."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Безбедан режим",
|
||||
"description": "Раде само додаци за распоред и тему. Поново покрените сервер без TERMIX_SAFE_MODE да бисте укључили остале."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Затвори панел",
|
||||
"moveSide": "Помери панел",
|
||||
@@ -1115,6 +1119,8 @@
|
||||
"resetEveryone": "Ресетуј подразумевано за све",
|
||||
"update": "Ажурирај {{name}} на {{version}}",
|
||||
"disable": "Искључи {{name}}",
|
||||
"enable": "Укључи {{name}}",
|
||||
"startAgain": "Поново покрените {{name}}",
|
||||
"install": "Пронађи распоред и теме",
|
||||
"installTitle": "Распореди и теме",
|
||||
"installButton": "Инсталирај",
|
||||
@@ -1326,7 +1332,8 @@
|
||||
"disabled": "Онемогућено",
|
||||
"failed": "Неочекивано заустављено",
|
||||
"blocked": "Чека на други додатак",
|
||||
"server": "Ради на вашем серверу"
|
||||
"server": "Ради на вашем серверу",
|
||||
"safeMode": "Искључено у безбедном режиму"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Покренуто",
|
||||
@@ -1340,11 +1347,6 @@
|
||||
"updated": "Недавно ажурирано",
|
||||
"category": "Категорија"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Колоне",
|
||||
"auto": "Аутоматски",
|
||||
"count": "{{count}} широка"
|
||||
},
|
||||
"table": {
|
||||
"name": "Име",
|
||||
"description": "Опис",
|
||||
@@ -1694,7 +1696,12 @@
|
||||
"gridView": "Мрежа",
|
||||
"listView": "Листа",
|
||||
"compactRows": "Компактни редови",
|
||||
"comfortableRows": "Удобни редови"
|
||||
"comfortableRows": "Удобни редови",
|
||||
"columns": {
|
||||
"title": "Колоне",
|
||||
"auto": "Аутоматски",
|
||||
"count": "{{count}} широка"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Сачувано. Унесите да бисте га заменили."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} är redo att installeras",
|
||||
"readyHint": "Den installeras nästa gång Termix startas om."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Säkerhetsläge",
|
||||
"description": "Endast layout- och temapluggar är aktiva. Starta om servern utan TERMIX_SAFE_MODE för att aktivera de övriga igen."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Stäng panel",
|
||||
"moveSide": "Flytta panel",
|
||||
@@ -1098,6 +1102,8 @@
|
||||
"resetEveryone": "Återställ standarden för alla",
|
||||
"update": "Uppdatera {{name}} till {{version}}",
|
||||
"disable": "Stäng av {{name}}",
|
||||
"enable": "Aktivera {{name}}",
|
||||
"startAgain": "Starta {{name}} igen",
|
||||
"install": "Hitta layouter och teman",
|
||||
"installTitle": "Layouter och teman",
|
||||
"installButton": "Installera",
|
||||
@@ -1309,7 +1315,8 @@
|
||||
"disabled": "Inaktiverad",
|
||||
"failed": "Stoppades oväntat",
|
||||
"blocked": "Väntar på ett annat plugin",
|
||||
"server": "Kör på din server"
|
||||
"server": "Kör på din server",
|
||||
"safeMode": "Avstängd i säkert läge"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Körs",
|
||||
@@ -1323,11 +1330,6 @@
|
||||
"updated": "Nyligen uppdaterade",
|
||||
"category": "Kategori"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Kolumner",
|
||||
"auto": "Auto",
|
||||
"count": "{{count}} breda"
|
||||
},
|
||||
"table": {
|
||||
"name": "Namn",
|
||||
"description": "Beskrivning",
|
||||
@@ -1652,7 +1654,12 @@
|
||||
"gridView": "Rutnät",
|
||||
"listView": "Lista",
|
||||
"compactRows": "Kompakta rader",
|
||||
"comfortableRows": "Bekväma rader"
|
||||
"comfortableRows": "Bekväma rader",
|
||||
"columns": {
|
||||
"title": "Kolumner",
|
||||
"auto": "Automatiskt",
|
||||
"count": "{{count}} bred"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Sparat. Skriv för att ersätta det."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix เวอร์ชัน {{version}} พร้อมติดตั้ง",
|
||||
"readyHint": "จะติดตั้งเมื่อ Termix รีสตาร์ทครั้งถัดไป"
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "โหมดปลอดภัย",
|
||||
"description": "กำลังทำงานเฉพาะปลั๊กอินเลย์เอาต์และธีมเท่านั้น โปรดรีสตาร์ทเซิร์ฟเวอร์โดยยกเลิกการตั้งค่า TERMIX_SAFE_MODE เพื่อเปิดใช้งานส่วนที่เหลือ"
|
||||
},
|
||||
"companion": {
|
||||
"close": "ปิดแผง",
|
||||
"moveSide": "ย้ายแผง",
|
||||
@@ -1081,6 +1085,8 @@
|
||||
"resetEveryone": "รีเซ็ตค่าเริ่มต้นสำหรับทุกคน",
|
||||
"update": "อัปเดต {{name}} เป็น {{version}}",
|
||||
"disable": "ปิดใช้งาน {{name}}",
|
||||
"enable": "เปิดใช้งาน {{name}}",
|
||||
"startAgain": "เริ่ม {{name}} อีกครั้ง",
|
||||
"install": "ค้นหาการจัดวางและธีม",
|
||||
"installTitle": "การจัดวางและธีม",
|
||||
"installButton": "ติดตั้ง",
|
||||
@@ -1292,7 +1298,8 @@
|
||||
"disabled": "ปิดใช้งาน",
|
||||
"failed": "หยุดทำงานโดยไม่คาดคิด",
|
||||
"blocked": "กำลังรอปลั๊กอินอื่น",
|
||||
"server": "ทำงานบนเซิร์ฟเวอร์ของคุณ"
|
||||
"server": "ทำงานบนเซิร์ฟเวอร์ของคุณ",
|
||||
"safeMode": "ปิดใช้งานในโหมดปลอดภัย"
|
||||
},
|
||||
"groups": {
|
||||
"running": "กำลังทำงาน",
|
||||
@@ -1306,11 +1313,6 @@
|
||||
"updated": "อัปเดตล่าสุด",
|
||||
"category": "หมวดหมู่"
|
||||
},
|
||||
"columns": {
|
||||
"title": "คอลัมน์",
|
||||
"auto": "อัตโนมัติ",
|
||||
"count": "กว้าง {{count}} คอลัมน์"
|
||||
},
|
||||
"table": {
|
||||
"name": "ชื่อ",
|
||||
"description": "คำอธิบาย",
|
||||
@@ -1610,7 +1612,12 @@
|
||||
"gridView": "ตาราง",
|
||||
"listView": "รายการ",
|
||||
"compactRows": "แถวแบบกะทัดรัด",
|
||||
"comfortableRows": "แถวแบบสบายตา"
|
||||
"comfortableRows": "แถวแบบสบายตา",
|
||||
"columns": {
|
||||
"title": "คอลัมน์",
|
||||
"auto": "อัตโนมัติ",
|
||||
"count": "กว้าง {{count}} คอลัมน์"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "บันทึกแล้ว พิมพ์เพื่อแทนที่"
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} yüklenmeye hazır",
|
||||
"readyHint": "Termix yeniden başladığında yüklenir."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Güvenli mod",
|
||||
"description": "Sadece düzen ve tema eklentileri çalışıyor. Geri kalanını tekrar açmak için sunucuyu TERMIX_SAFE_MODE olmadan yeniden başlatın."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Paneli kapat",
|
||||
"moveSide": "Paneli taşı",
|
||||
@@ -1098,6 +1102,8 @@
|
||||
"resetEveryone": "Herkes için varsayılanı sıfırla",
|
||||
"update": "{{name}}'i {{version}} sürümüne güncelle",
|
||||
"disable": "{{name}}'i kapat",
|
||||
"enable": "{{name}}'i aç",
|
||||
"startAgain": "{{name}}'i yeniden başlat",
|
||||
"install": "Düzen ve temaları bul",
|
||||
"installTitle": "Düzenler ve temalar",
|
||||
"installButton": "Yükle",
|
||||
@@ -1309,7 +1315,8 @@
|
||||
"disabled": "Devre dışı",
|
||||
"failed": "Beklenmedik şekilde durdu",
|
||||
"blocked": "Başka bir eklentiyi bekliyor",
|
||||
"server": "Sunucunuzda çalışır"
|
||||
"server": "Sunucunuzda çalışır",
|
||||
"safeMode": "Güvenli modda kapalı"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Çalışıyor",
|
||||
@@ -1323,11 +1330,6 @@
|
||||
"updated": "Son güncellenen",
|
||||
"category": "Kategori"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Sütunlar",
|
||||
"auto": "Otomatik",
|
||||
"count": "{{count}} geniş"
|
||||
},
|
||||
"table": {
|
||||
"name": "Ad",
|
||||
"description": "Açıklama",
|
||||
@@ -1652,7 +1654,12 @@
|
||||
"gridView": "Izgara",
|
||||
"listView": "Liste",
|
||||
"compactRows": "Sıkı satırlar",
|
||||
"comfortableRows": "Rahat satırlar"
|
||||
"comfortableRows": "Rahat satırlar",
|
||||
"columns": {
|
||||
"title": "Sütunlar",
|
||||
"auto": "Otomatik",
|
||||
"count": "{{count}} geniş"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Kaydedildi. Değiştirmek için yazın."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} готова до встановлення",
|
||||
"readyHint": "Вона встановиться під час наступного перезапуску Termix."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Безпечний режим",
|
||||
"description": "Працюють лише плагіни макету та теми. Перезапустіть сервер без змінної TERMIX_SAFE_MODE, щоб увімкнути решту плагінів."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Закрити панель",
|
||||
"moveSide": "Перемістити панель",
|
||||
@@ -1132,6 +1136,8 @@
|
||||
"resetEveryone": "Скинути значення за замовчуванням для всіх",
|
||||
"update": "Оновити {{name}} до {{version}}",
|
||||
"disable": "Вимкнути {{name}}",
|
||||
"enable": "Увімкнути {{name}}",
|
||||
"startAgain": "Запустити {{name}} знову",
|
||||
"install": "Знайти макети та теми",
|
||||
"installTitle": "Макети та теми",
|
||||
"installButton": "Встановити",
|
||||
@@ -1343,7 +1349,8 @@
|
||||
"disabled": "Вимкнено",
|
||||
"failed": "Зупинено несподівано",
|
||||
"blocked": "Очікує на інший плагін",
|
||||
"server": "Працює на вашому сервері"
|
||||
"server": "Працює на вашому сервері",
|
||||
"safeMode": "Вимкнено в безпечному режимі"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Запущено",
|
||||
@@ -1357,11 +1364,6 @@
|
||||
"updated": "Нещодавно оновлено",
|
||||
"category": "Категорія"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Стовпці",
|
||||
"auto": "Авто",
|
||||
"count": "{{count}} стовпців"
|
||||
},
|
||||
"table": {
|
||||
"name": "Назва",
|
||||
"description": "Опис",
|
||||
@@ -1736,7 +1738,12 @@
|
||||
"gridView": "Сітка",
|
||||
"listView": "Список",
|
||||
"compactRows": "Компактні рядки",
|
||||
"comfortableRows": "Зручні рядки"
|
||||
"comfortableRows": "Зручні рядки",
|
||||
"columns": {
|
||||
"title": "Стовпці",
|
||||
"auto": "Авто",
|
||||
"count": "{{count}} стовпців"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Збережено. Введіть текст, щоб замінити його."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} đã sẵn sàng để cài đặt",
|
||||
"readyHint": "Nó sẽ được cài đặt khi Termix khởi động lại lần tới."
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "Chế độ an toàn",
|
||||
"description": "Chỉ có các plugin bố cục và giao diện đang chạy. Khởi động lại máy chủ mà không có TERMIX_SAFE_MODE để bật lại các plugin còn lại."
|
||||
},
|
||||
"companion": {
|
||||
"close": "Đóng bảng",
|
||||
"moveSide": "Di chuyển bảng",
|
||||
@@ -1081,6 +1085,8 @@
|
||||
"resetEveryone": "Đặt lại mặc định cho mọi người",
|
||||
"update": "Cập nhật {{name}} lên {{version}}",
|
||||
"disable": "Tắt {{name}}",
|
||||
"enable": "Bật {{name}}",
|
||||
"startAgain": "Khởi động lại {{name}}",
|
||||
"install": "Tìm bố cục và giao diện",
|
||||
"installTitle": "Bố cục và giao diện",
|
||||
"installButton": "Cài đặt",
|
||||
@@ -1292,7 +1298,8 @@
|
||||
"disabled": "Đã tắt",
|
||||
"failed": "Dừng bất ngờ",
|
||||
"blocked": "Đang chờ plugin khác",
|
||||
"server": "Chạy trên máy chủ của bạn"
|
||||
"server": "Chạy trên máy chủ của bạn",
|
||||
"safeMode": "Tắt trong chế độ an toàn"
|
||||
},
|
||||
"groups": {
|
||||
"running": "Đang chạy",
|
||||
@@ -1306,11 +1313,6 @@
|
||||
"updated": "Mới cập nhật",
|
||||
"category": "Danh mục"
|
||||
},
|
||||
"columns": {
|
||||
"title": "Cột",
|
||||
"auto": "Tự động",
|
||||
"count": "Rộng {{count}}"
|
||||
},
|
||||
"table": {
|
||||
"name": "Tên",
|
||||
"description": "Mô tả",
|
||||
@@ -1610,7 +1612,12 @@
|
||||
"gridView": "Lưới",
|
||||
"listView": "Danh sách",
|
||||
"compactRows": "Hàng gọn",
|
||||
"comfortableRows": "Hàng rộng"
|
||||
"comfortableRows": "Hàng rộng",
|
||||
"columns": {
|
||||
"title": "Cột",
|
||||
"auto": "Tự động",
|
||||
"count": "Rộng {{count}}"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "Đã lưu. Nhập để thay thế."
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} 已准备好安装",
|
||||
"readyHint": "下次重启 Termix 时将进行安装。"
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "安全模式",
|
||||
"description": "仅运行布局和主题插件。重启服务器并移除 TERMIX_SAFE_MODE 环境变量以恢复其他插件。"
|
||||
},
|
||||
"companion": {
|
||||
"close": "关闭面板",
|
||||
"moveSide": "移动面板",
|
||||
@@ -1081,6 +1085,8 @@
|
||||
"resetEveryone": "重置所有人的默认设置",
|
||||
"update": "将 {{name}} 更新到 {{version}}",
|
||||
"disable": "关闭 {{name}}",
|
||||
"enable": "启用 {{name}}",
|
||||
"startAgain": "重新启动 {{name}}",
|
||||
"install": "查找布局和主题",
|
||||
"installTitle": "布局和主题",
|
||||
"installButton": "安装",
|
||||
@@ -1292,7 +1298,8 @@
|
||||
"disabled": "已禁用",
|
||||
"failed": "意外停止",
|
||||
"blocked": "等待其他插件",
|
||||
"server": "在您的服务器上运行"
|
||||
"server": "在您的服务器上运行",
|
||||
"safeMode": "安全模式下已关闭"
|
||||
},
|
||||
"groups": {
|
||||
"running": "正在运行",
|
||||
@@ -1306,11 +1313,6 @@
|
||||
"updated": "最近更新",
|
||||
"category": "类别"
|
||||
},
|
||||
"columns": {
|
||||
"title": "列数",
|
||||
"auto": "自动",
|
||||
"count": "{{count}} 列宽"
|
||||
},
|
||||
"table": {
|
||||
"name": "名称",
|
||||
"description": "描述",
|
||||
@@ -1610,7 +1612,12 @@
|
||||
"gridView": "网格",
|
||||
"listView": "列表",
|
||||
"compactRows": "紧凑行",
|
||||
"comfortableRows": "宽松行"
|
||||
"comfortableRows": "宽松行",
|
||||
"columns": {
|
||||
"title": "列数",
|
||||
"auto": "自动",
|
||||
"count": "{{count}} 列宽"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "已保存。输入以替换。"
|
||||
|
||||
@@ -235,6 +235,10 @@
|
||||
"ready": "Termix {{version}} 已準備好安裝",
|
||||
"readyHint": "下次重新啟動 Termix 時將進行安裝。"
|
||||
},
|
||||
"safeMode": {
|
||||
"title": "安全模式",
|
||||
"description": "目前僅執行版面配置與主題外掛。請在移除 TERMIX_SAFE_MODE 環境變數後重新啟動伺服器,以恢復其他外掛。"
|
||||
},
|
||||
"companion": {
|
||||
"close": "關閉面板",
|
||||
"moveSide": "移動面板",
|
||||
@@ -1081,6 +1085,8 @@
|
||||
"resetEveryone": "重設所有人的預設值",
|
||||
"update": "將 {{name}} 更新至 {{version}}",
|
||||
"disable": "關閉 {{name}}",
|
||||
"enable": "啟用 {{name}}",
|
||||
"startAgain": "重新啟動 {{name}}",
|
||||
"install": "尋找版面配置與主題",
|
||||
"installTitle": "版面配置與主題",
|
||||
"installButton": "安裝",
|
||||
@@ -1292,7 +1298,8 @@
|
||||
"disabled": "已停用",
|
||||
"failed": "意外停止",
|
||||
"blocked": "等待其他外掛",
|
||||
"server": "在您的伺服器上執行"
|
||||
"server": "在您的伺服器上執行",
|
||||
"safeMode": "安全模式下已停用"
|
||||
},
|
||||
"groups": {
|
||||
"running": "執行中",
|
||||
@@ -1306,11 +1313,6 @@
|
||||
"updated": "最近更新",
|
||||
"category": "類別"
|
||||
},
|
||||
"columns": {
|
||||
"title": "欄數",
|
||||
"auto": "自動",
|
||||
"count": "寬度 {{count}}"
|
||||
},
|
||||
"table": {
|
||||
"name": "名稱",
|
||||
"description": "說明",
|
||||
@@ -1610,7 +1612,12 @@
|
||||
"gridView": "網格",
|
||||
"listView": "清單",
|
||||
"compactRows": "緊湊列",
|
||||
"comfortableRows": "寬鬆列"
|
||||
"comfortableRows": "寬鬆列",
|
||||
"columns": {
|
||||
"title": "欄位",
|
||||
"auto": "自動",
|
||||
"count": "寬度 {{count}}"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"secretSaved": "已儲存。輸入以取代。"
|
||||
|
||||
@@ -221,6 +221,8 @@
|
||||
"desktopUpdates.manualHint": "48b49fd6307c",
|
||||
"desktopUpdates.ready": "faadfb86e8e8",
|
||||
"desktopUpdates.readyHint": "dd55ccd0ee8d",
|
||||
"safeMode.title": "c730d398556a",
|
||||
"safeMode.description": "f6746478af76",
|
||||
"companion.close": "fa0b7b3ac0f0",
|
||||
"companion.moveSide": "4e7610d6ac13",
|
||||
"companion.resize": "c3689b810f1d",
|
||||
@@ -1005,6 +1007,8 @@
|
||||
"looks.repair.resetEveryone": "219daf273b31",
|
||||
"looks.repair.update": "9930e068ecbe",
|
||||
"looks.repair.disable": "f5be6c90fa8d",
|
||||
"looks.repair.enable": "507cd8f53166",
|
||||
"looks.repair.startAgain": "24dbed327ec1",
|
||||
"looks.repair.install": "904997c1d819",
|
||||
"looks.repair.installTitle": "378817c80794",
|
||||
"looks.repair.installButton": "fd6c3ebf7bef",
|
||||
@@ -1124,6 +1128,7 @@
|
||||
"plugins.manager.status.failed": "71f9a28d7960",
|
||||
"plugins.manager.status.blocked": "911f8f3cbad4",
|
||||
"plugins.manager.status.server": "212cbe47e65e",
|
||||
"plugins.manager.status.safeMode": "e850fafec9cd",
|
||||
"plugins.manager.groups.running": "73989d9c5926",
|
||||
"plugins.manager.groups.notRunning": "2a2cd4b515b7",
|
||||
"plugins.manager.groups.onServer": "63ca7153c2d1",
|
||||
@@ -1132,9 +1137,6 @@
|
||||
"plugins.manager.sort.name": "709a23220f2c",
|
||||
"plugins.manager.sort.updated": "5f0500ccf919",
|
||||
"plugins.manager.sort.category": "a3c686e711e4",
|
||||
"plugins.manager.columns.title": "cf723c59b62a",
|
||||
"plugins.manager.columns.auto": "c614ba7c453c",
|
||||
"plugins.manager.columns.count": "e8f2c5983611",
|
||||
"plugins.manager.table.name": "709a23220f2c",
|
||||
"plugins.manager.table.description": "55f8ebc805e6",
|
||||
"plugins.manager.table.version": "2da600bf9404",
|
||||
@@ -1392,6 +1394,9 @@
|
||||
"panel.listView": "a1fffaaafb7c",
|
||||
"panel.compactRows": "b17f552f3b98",
|
||||
"panel.comfortableRows": "4c5b39191ead",
|
||||
"panel.columns.title": "cf723c59b62a",
|
||||
"panel.columns.auto": "c614ba7c453c",
|
||||
"panel.columns.count": "e8f2c5983611",
|
||||
"fields.secretSaved": "81d793988e7a",
|
||||
"manage.needsAttention": "75674c819d3e",
|
||||
"manage.hostCount": "4279c8d92ede",
|
||||
|
||||
@@ -130,6 +130,7 @@ function serverAllowsFrontend(summary: PluginSummary): boolean {
|
||||
summary.enabled &&
|
||||
summary.state !== "failed" &&
|
||||
summary.state !== "blocked" &&
|
||||
summary.state !== "safe-mode" &&
|
||||
!!summary.frontend
|
||||
);
|
||||
}
|
||||
|
||||
@@ -289,6 +289,7 @@ export {
|
||||
PanelShell,
|
||||
Segmented,
|
||||
ViewToggle,
|
||||
type GridSize,
|
||||
type PanelDensity,
|
||||
type PanelViewMode,
|
||||
} from "@/components/panel-layout";
|
||||
|
||||
@@ -74,7 +74,9 @@ export function unregisteredViewStatus(
|
||||
return { status: getPluginStoreState().loaded ? "missing" : "loading" };
|
||||
}
|
||||
const serverState = owner.summary.state;
|
||||
if (!owner.summary.enabled) return { status: "disabled", owner };
|
||||
if (!owner.summary.enabled || serverState === "safe-mode") {
|
||||
return { status: "disabled", owner };
|
||||
}
|
||||
if (
|
||||
owner.frontend === "failed" ||
|
||||
owner.frontend === "blocked" ||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
Check,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Columns3,
|
||||
Download,
|
||||
Globe,
|
||||
Library,
|
||||
@@ -37,7 +36,9 @@ import {
|
||||
GroupHeading,
|
||||
PANEL,
|
||||
PanelShell,
|
||||
GRID_SIZES,
|
||||
ViewToggle,
|
||||
type GridSize,
|
||||
type PanelViewMode,
|
||||
} from "@/components/panel-layout";
|
||||
import { DataView, type DataColumn } from "@/components/data-view";
|
||||
@@ -72,9 +73,6 @@ import { DocsLink } from "@/components/docs-link";
|
||||
|
||||
type Section = "installed" | "browse" | "updates" | "registries";
|
||||
|
||||
const GRID_SIZES = ["auto", "2", "3", "4", "5", "6"] as const;
|
||||
type GridSize = (typeof GRID_SIZES)[number];
|
||||
|
||||
interface Layout {
|
||||
view: PanelViewMode;
|
||||
columns: GridSize;
|
||||
@@ -364,54 +362,16 @@ function LayoutControls({
|
||||
onView: (next: PanelViewMode) => void;
|
||||
onColumns: (next: GridSize) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
{layout.view === "grid" && (
|
||||
<Select
|
||||
value={layout.columns}
|
||||
onValueChange={(v) => onColumns(v as GridSize)}
|
||||
>
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="w-auto gap-1.5 text-xs"
|
||||
title={t("plugins.manager.columns.title")}
|
||||
aria-label={t("plugins.manager.columns.title")}
|
||||
>
|
||||
<Columns3 className="size-3.5 text-muted-foreground" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{GRID_SIZES.map((size) => (
|
||||
<SelectItem key={size} value={size} className="text-xs">
|
||||
{size === "auto"
|
||||
? t("plugins.manager.columns.auto")
|
||||
: t("plugins.manager.columns.count", { count: Number(size) })}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<ViewToggle view={layout.view} onView={onView} />
|
||||
</>
|
||||
<ViewToggle
|
||||
view={layout.view}
|
||||
onView={onView}
|
||||
columns={layout.columns}
|
||||
onColumns={onColumns}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Sized off the tab's own width, not the window. Cards keep a min width,
|
||||
// so narrow panes drop columns on their own.
|
||||
function gridStyle(columns: GridSize): React.CSSProperties {
|
||||
if (columns === "auto") {
|
||||
return {
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(min(100%, 400px), 1fr))",
|
||||
};
|
||||
}
|
||||
const n = Number(columns);
|
||||
const gaps = (n - 1) * 12;
|
||||
return {
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(max(220px, calc((100% - ${gaps}px) / ${n} - 0.5px)), 1fr))`,
|
||||
};
|
||||
}
|
||||
|
||||
function PluginCollection({
|
||||
plugins,
|
||||
layout,
|
||||
@@ -440,13 +400,17 @@ function PluginCollection({
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="grid gap-3" style={gridStyle(layout.columns)}>
|
||||
{plugins.map((p) => (
|
||||
<div key={p.id} className="flex flex-col *:flex-1">
|
||||
{renderCard(p)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<DataView
|
||||
items={plugins}
|
||||
view="grid"
|
||||
getKey={(p) => p.id}
|
||||
renderCard={renderCard}
|
||||
columns={{}}
|
||||
size={layout.columns}
|
||||
minCardWidth={400}
|
||||
listColumns={listColumns}
|
||||
empty={null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1133,7 +1097,9 @@ function InstalledCard({
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{plugin.status === "server"
|
||||
? t("plugins.manager.status.server")
|
||||
: t("plugins.manager.status.disabled")}
|
||||
: plugin.status === "safeMode"
|
||||
? t("plugins.manager.status.safeMode")
|
||||
: t("plugins.manager.status.disabled")}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
@@ -1226,11 +1192,15 @@ function StatusCell({ plugin }: { plugin: PluginEntry }) {
|
||||
? t("plugins.manager.status.failed")
|
||||
: plugin.status === "server"
|
||||
? t("plugins.manager.status.server")
|
||||
: !running
|
||||
? t("plugins.manager.status.disabled")
|
||||
: plugin.pinnedVersion
|
||||
? t("plugins.manager.pinnedTo", { version: plugin.pinnedVersion })
|
||||
: t("plugins.manager.status.running");
|
||||
: plugin.status === "safeMode"
|
||||
? t("plugins.manager.status.safeMode")
|
||||
: !running
|
||||
? t("plugins.manager.status.disabled")
|
||||
: plugin.pinnedVersion
|
||||
? t("plugins.manager.pinnedTo", {
|
||||
version: plugin.pinnedVersion,
|
||||
})
|
||||
: t("plugins.manager.status.running");
|
||||
return (
|
||||
<span
|
||||
className="flex min-w-0 items-center gap-1.5 text-muted-foreground"
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from "@/lib/issue-url";
|
||||
|
||||
export type PluginStatus =
|
||||
"running" | "disabled" | "failed" | "blocked" | "server";
|
||||
"running" | "disabled" | "failed" | "blocked" | "server" | "safeMode";
|
||||
|
||||
/** One row of the Plugins tab: what is installed merged with the registry. */
|
||||
export interface PluginEntry {
|
||||
@@ -121,6 +121,7 @@ export function pluginStatus(summary: PluginSummary): PluginStatus {
|
||||
if (summary.state === "failed") return "failed";
|
||||
if (summary.state === "blocked") return "blocked";
|
||||
if (!summary.enabled) return "disabled";
|
||||
if (summary.state === "safe-mode") return "safeMode";
|
||||
return "running";
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { useLookState } from "@/look/look-store";
|
||||
|
||||
const TOAST_ID = "safe-mode";
|
||||
|
||||
/** Tells everyone the server runs in safe mode, until they close it. */
|
||||
export function SafeModeNotice() {
|
||||
const { t } = useTranslation();
|
||||
const safeMode = useLookState().response?.safeMode === true;
|
||||
|
||||
useEffect(() => {
|
||||
if (!safeMode) return;
|
||||
toast.warning(t("safeMode.title"), {
|
||||
id: TOAST_ID,
|
||||
description: t("safeMode.description"),
|
||||
duration: Infinity,
|
||||
closeButton: true,
|
||||
});
|
||||
}, [safeMode, t]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { ServerStatusProvider } from "@/lib/ServerStatusContext";
|
||||
import { ComponentSlot } from "@/shell/ActionSlot";
|
||||
import { OnboardingHost } from "@/onboarding/OnboardingHost";
|
||||
import { DesktopUpdateToasts } from "@/shell/DesktopUpdateToasts";
|
||||
import { SafeModeNotice } from "@/shell/SafeModeNotice";
|
||||
import { TabPortals } from "@/shell/stage/TabPortals";
|
||||
import { LayoutHost } from "@/shell/layout/LayoutHost";
|
||||
import { ShellControllerProvider } from "@/shell/controller/ShellControllerProvider";
|
||||
@@ -25,6 +26,7 @@ export function ShellRoot({
|
||||
<TabPortals />
|
||||
<ComponentSlot slotId="shell.overlay" />
|
||||
<OnboardingHost />
|
||||
<SafeModeNotice />
|
||||
{isElectron() && <DesktopUpdateToasts />}
|
||||
</ShellControllerProvider>
|
||||
</ServerStatusProvider>
|
||||
|
||||
@@ -34,6 +34,24 @@ describe("DataView", () => {
|
||||
expect(screen.queryByText("Name")).toBeNull();
|
||||
});
|
||||
|
||||
it("uses the picked size over breakpoint columns", () => {
|
||||
const { container } = render(
|
||||
<DataView
|
||||
items={items}
|
||||
view="grid"
|
||||
getKey={(item) => item.id}
|
||||
renderCard={(item) => <div>card {item.name}</div>}
|
||||
columns={{ base: 1, md: 2 }}
|
||||
size="3"
|
||||
listColumns={[]}
|
||||
empty={null}
|
||||
/>,
|
||||
);
|
||||
const grid = container.firstElementChild as HTMLElement;
|
||||
expect(grid.className).not.toContain("md:grid-cols-2");
|
||||
expect(grid.style.gridTemplateColumns).toContain("/ 3");
|
||||
});
|
||||
|
||||
it("renders a header and clickable rows in list mode", () => {
|
||||
const onRowClick = vi.fn();
|
||||
renderView("list", onRowClick);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
PanelShell,
|
||||
Segmented,
|
||||
ViewToggle,
|
||||
gridSizeStyle,
|
||||
} from "@/components/panel-layout";
|
||||
|
||||
afterEach(cleanup);
|
||||
@@ -72,6 +73,34 @@ describe("ViewToggle", () => {
|
||||
fireEvent.click(screen.getByLabelText("panel.compactRows"));
|
||||
expect(onDensity).toHaveBeenCalledWith("compact");
|
||||
});
|
||||
|
||||
it("shows the columns picker only in grid view", () => {
|
||||
const props = { onView: vi.fn(), columns: "auto" as const };
|
||||
const { rerender } = render(
|
||||
<ViewToggle view="grid" {...props} onColumns={vi.fn()} />,
|
||||
);
|
||||
expect(screen.getByLabelText("panel.columns.title")).toBeTruthy();
|
||||
|
||||
rerender(<ViewToggle view="list" {...props} onColumns={vi.fn()} />);
|
||||
expect(screen.queryByLabelText("panel.columns.title")).toBeNull();
|
||||
|
||||
rerender(<ViewToggle view="grid" {...props} />);
|
||||
expect(screen.queryByLabelText("panel.columns.title")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("gridSizeStyle", () => {
|
||||
it("fits auto cards to the pane", () => {
|
||||
expect(gridSizeStyle("auto", 280).gridTemplateColumns).toBe(
|
||||
"repeat(auto-fill, minmax(min(100%, 280px), 1fr))",
|
||||
);
|
||||
});
|
||||
|
||||
it("splits the width for a fixed count", () => {
|
||||
expect(gridSizeStyle("4").gridTemplateColumns).toContain(
|
||||
"(100% - 3 * 0.5rem) / 4",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Facts", () => {
|
||||
|
||||
@@ -236,6 +236,13 @@ describe("plugin loader", () => {
|
||||
expect(railIds()).not.toContain("headless-panel");
|
||||
});
|
||||
|
||||
it("skips plugins safe mode keeps off", async () => {
|
||||
modules.paused = railPlugin("paused");
|
||||
plugins = [summary("paused", { state: "safe-mode" })];
|
||||
await syncPlugins();
|
||||
expect(railIds()).not.toContain("paused-panel");
|
||||
});
|
||||
|
||||
it("keeps every plugin when the pre-login list lands after sign-in", async () => {
|
||||
modules.login = railPlugin("login");
|
||||
modules.alpha = railPlugin("alpha");
|
||||
|
||||
@@ -183,17 +183,13 @@ describe("PluginsTab", () => {
|
||||
it("switches to a table and remembers it", async () => {
|
||||
await renderTab();
|
||||
expect(screen.queryByRole("table")).toBeNull();
|
||||
expect(
|
||||
screen.getByLabelText(en.plugins.manager.columns.title),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByLabelText(en.panel.columns.title)).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByLabelText(en.panel.listView));
|
||||
|
||||
expect(screen.getByRole("table")).toBeTruthy();
|
||||
expect(screen.getByText(en.plugins.manager.table.version)).toBeTruthy();
|
||||
expect(
|
||||
screen.queryByLabelText(en.plugins.manager.columns.title),
|
||||
).toBeNull();
|
||||
expect(screen.queryByLabelText(en.panel.columns.title)).toBeNull();
|
||||
expect(localStorage.getItem("pluginsTab.view")).toBe("list");
|
||||
|
||||
cleanup();
|
||||
|
||||
@@ -157,6 +157,7 @@ describe("pluginStatus", () => {
|
||||
"disabled",
|
||||
);
|
||||
expect(pluginStatus(summary())).toBe("running");
|
||||
expect(pluginStatus(summary({ state: "safe-mode" }))).toBe("safeMode");
|
||||
expect(pluginStatus(summary({ state: "failed", runsOnServer: true }))).toBe(
|
||||
"server",
|
||||
);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
|
||||
const toast = vi.hoisted(() => ({ warning: vi.fn() }));
|
||||
vi.mock("sonner", () => ({ toast }));
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
const look = vi.hoisted(() => ({ safeMode: false }));
|
||||
vi.mock("@/look/look-store", () => ({
|
||||
useLookState: () => ({
|
||||
loaded: true,
|
||||
response: { safeMode: look.safeMode },
|
||||
}),
|
||||
}));
|
||||
|
||||
const { SafeModeNotice } = await import("@/shell/SafeModeNotice");
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
toast.warning.mockReset();
|
||||
});
|
||||
|
||||
describe("SafeModeNotice", () => {
|
||||
it("shows nothing when safe mode is off", () => {
|
||||
look.safeMode = false;
|
||||
render(<SafeModeNotice />);
|
||||
expect(toast.warning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows a lasting warning when the server runs in safe mode", () => {
|
||||
look.safeMode = true;
|
||||
render(<SafeModeNotice />);
|
||||
expect(toast.warning).toHaveBeenCalledWith(
|
||||
"safeMode.title",
|
||||
expect.objectContaining({
|
||||
id: "safe-mode",
|
||||
description: "safeMode.description",
|
||||
duration: Infinity,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user