mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: move snippets, keybinding actions and macros out of core into plugins
This commit is contained in:
1 parent
d9bba10612
commit
e0c30a4e6f
262 files changed
+9091
-10744
No files matched your search
@@ -23,6 +23,9 @@
|
||||
"network:serve",
|
||||
"ui:surface"
|
||||
],
|
||||
"optionalDependencies": {
|
||||
"snippets": "^1.0.0"
|
||||
},
|
||||
"provides": [
|
||||
{
|
||||
"service": "fleets.access",
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
export interface CommandHostVars {
|
||||
ip?: string;
|
||||
username?: string;
|
||||
port?: number | string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const INPUT_PATTERN =
|
||||
/\$\{INPUT_(\d+)(?::([^}$]+))?\}|\$INPUT_(\d+)(?![a-zA-Z0-9_])/g;
|
||||
|
||||
function replaceVar(content: string, name: string, value?: string): string {
|
||||
if (value === undefined) return content;
|
||||
const pattern = new RegExp(`\\$\\{?${name}\\}?`, "g");
|
||||
return content.replace(pattern, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* $HOST/$USER/$PORT/$NAME/$INPUT_n substitution for an ad hoc fleet command,
|
||||
* typed directly into the "run on fleet" box rather than loaded from a saved
|
||||
* snippet. Mirrors the snippets plugin's own resolveSnippetCommand and the
|
||||
* frontend's src/ui/lib/snippet-variables.ts - kept as a small standalone
|
||||
* copy rather than a cross-plugin dependency for one pure function with no
|
||||
* snippet data of its own to fetch.
|
||||
*/
|
||||
export function resolveCommandVariables(
|
||||
content: string,
|
||||
host: CommandHostVars | null,
|
||||
inputValues: Record<string, string> = {},
|
||||
): string {
|
||||
let resolved = content;
|
||||
|
||||
resolved = replaceVar(resolved, "HOST", host?.ip);
|
||||
resolved = replaceVar(resolved, "USER", host?.username);
|
||||
resolved = replaceVar(
|
||||
resolved,
|
||||
"PORT",
|
||||
host?.port !== undefined ? String(host.port) : undefined,
|
||||
);
|
||||
resolved = replaceVar(resolved, "NAME", host?.name);
|
||||
|
||||
resolved = resolved.replace(
|
||||
INPUT_PATTERN,
|
||||
(
|
||||
fullMatch,
|
||||
braceDigits: string | undefined,
|
||||
_label,
|
||||
plainDigits: string | undefined,
|
||||
) => {
|
||||
const key = `INPUT_${braceDigits ?? plainDigits}`;
|
||||
return key in inputValues ? inputValues[key] : fullMatch;
|
||||
},
|
||||
);
|
||||
|
||||
return resolved;
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
ElevationError,
|
||||
} from "@termix/plugin-sdk/host-commands";
|
||||
import type { FleetRepository } from "./repository.js";
|
||||
import { resolveCommandVariables } from "./command-variables.js";
|
||||
|
||||
const FLEET_TRANSFER_MAX_BYTES = 200 * 1024 * 1024;
|
||||
const upload = multer({
|
||||
@@ -74,6 +73,35 @@ function parseShareTargets(
|
||||
return targets;
|
||||
}
|
||||
|
||||
interface CommandVariables {
|
||||
resolve: (
|
||||
content: string,
|
||||
host: {
|
||||
ip?: string;
|
||||
username?: string;
|
||||
port?: number | string;
|
||||
name?: string;
|
||||
} | null,
|
||||
inputValues?: Record<string, string>,
|
||||
) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* $HOST, $USER, $PORT, $NAME and $INPUT_n in an ad hoc fleet command, through
|
||||
* the snippets plugin, which owns the syntax. Without it the command runs as
|
||||
* typed.
|
||||
*/
|
||||
export function resolveCommandVariables(
|
||||
ctx: Pick<PluginContext, "registry">,
|
||||
command: string,
|
||||
host: Parameters<CommandVariables["resolve"]>[1],
|
||||
inputValues: Record<string, string> = {},
|
||||
): string {
|
||||
const variables =
|
||||
ctx.registry.consume<CommandVariables>("snippets.variables");
|
||||
return variables ? variables.resolve(command, host, inputValues) : command;
|
||||
}
|
||||
|
||||
function actor(ctx: PluginContext): string {
|
||||
return ctx.currentActor() as string;
|
||||
}
|
||||
@@ -884,6 +912,7 @@ export function registerFleetRoutes(
|
||||
}
|
||||
|
||||
const resolvedCommand = resolveCommandVariables(
|
||||
ctx,
|
||||
command,
|
||||
{
|
||||
ip: fullHost.ip,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import {
|
||||
getErrorMessage,
|
||||
extractSnippetInputs,
|
||||
type SnippetInput,
|
||||
} from "./helpers.js";
|
||||
import { getErrorMessage, type SnippetInput } from "./helpers.js";
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import {
|
||||
invokeAction,
|
||||
useTranslation,
|
||||
useHosts,
|
||||
type PluginHostRecord,
|
||||
@@ -792,8 +789,16 @@ function RunCommandTab({ api, fleetId }: { api: FleetsApi; fleetId: number }) {
|
||||
const [running, setRunning] = useState(false);
|
||||
const [results, setResults] = useState<FleetHostResult[] | null>(null);
|
||||
|
||||
// The snippets plugin owns $INPUT_n parsing; without it the command
|
||||
// runs as typed.
|
||||
useEffect(() => {
|
||||
setInputs(extractSnippetInputs(command));
|
||||
let cancelled = false;
|
||||
void invokeAction("snippets.extractInputs", command).then((found) => {
|
||||
if (!cancelled) setInputs(Array.isArray(found) ? found : []);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [command]);
|
||||
|
||||
async function handleRun() {
|
||||
|
||||
@@ -21,24 +21,3 @@ export interface SnippetInput {
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const INPUT_PATTERN =
|
||||
/\$\{INPUT_(\d+)(?::([^}$]+))?\}|\$INPUT_(\d+)(?![a-zA-Z0-9_])/g;
|
||||
|
||||
/**
|
||||
* Finds the $INPUT_n placeholders in a command, for the fleet run box's
|
||||
* input fields. Mirrors src/ui/lib/snippet-variables.ts extractSnippetInputs.
|
||||
*/
|
||||
export function extractSnippetInputs(content: string): SnippetInput[] {
|
||||
const seen = new Map<string, SnippetInput>();
|
||||
let match: RegExpExecArray | null;
|
||||
INPUT_PATTERN.lastIndex = 0;
|
||||
while ((match = INPUT_PATTERN.exec(content)) !== null) {
|
||||
const digits = match[1] ?? match[3];
|
||||
const key = `INPUT_${digits}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.set(key, { key, label: match[2]?.trim() || `Input ${digits}` });
|
||||
}
|
||||
}
|
||||
return Array.from(seen.values());
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveCommandVariables } from "../../src/backend/routes.js";
|
||||
|
||||
function registry(value?: unknown) {
|
||||
return {
|
||||
registry: {
|
||||
provide: () => {},
|
||||
revoke: () => false,
|
||||
consume: <T>() => value as T | undefined,
|
||||
},
|
||||
} as never;
|
||||
}
|
||||
|
||||
describe("resolveCommandVariables", () => {
|
||||
it("fills variables through the snippets plugin's resolver", () => {
|
||||
const ctx = registry({
|
||||
resolve: (content: string, host: { ip?: string } | null) =>
|
||||
content.replace("$HOST", host?.ip ?? ""),
|
||||
});
|
||||
expect(resolveCommandVariables(ctx, "ping $HOST", { ip: "10.0.0.1" })).toBe(
|
||||
"ping 10.0.0.1",
|
||||
);
|
||||
});
|
||||
|
||||
it("runs the command as typed while snippets is off", () => {
|
||||
expect(
|
||||
resolveCommandVariables(registry(), "ping $HOST", { ip: "10.0.0.1" }),
|
||||
).toBe("ping $HOST");
|
||||
});
|
||||
});
|
||||
@@ -54,10 +54,6 @@
|
||||
"processesRunning": "running",
|
||||
"loginStats": "SSH Login Statistics",
|
||||
"noRecentLoginData": "No recent login data",
|
||||
"executingQuickAction": "Executing {{name}}...",
|
||||
"quickActionSuccess": "{{name}} completed successfully",
|
||||
"quickActionFailed": "{{name}} failed",
|
||||
"quickActionError": "Failed to execute {{name}}",
|
||||
"ports": {
|
||||
"title": "Listening Ports",
|
||||
"protocol": "Protocol",
|
||||
@@ -182,8 +178,7 @@
|
||||
"wgLastHandshake": "Last handshake",
|
||||
"wgHandshakeNever": "Never",
|
||||
"wgTransfer": "Transfer"
|
||||
},
|
||||
"snippetsUnavailable": "Snippets are turned off, so quick actions cannot run."
|
||||
}
|
||||
},
|
||||
"metricsHistory": {
|
||||
"title": "Metrics History",
|
||||
@@ -219,12 +214,6 @@
|
||||
"addExcludedMount": "Add",
|
||||
"visibleWidgets": "Visible Widgets",
|
||||
"widgetsMovedToHostMetrics": "Cards are now added, arranged, and resized directly in the Host Metrics tab. Open Host Metrics for this host and use Customize to choose which cards are shown.",
|
||||
"quickActionsLabel": "Quick Actions",
|
||||
"quickActionsToolbar": "Quick actions appear as buttons in the Host Metrics toolbar for one-click command execution.",
|
||||
"noQuickActions": "No quick actions yet.",
|
||||
"buttonLabel": "Button label",
|
||||
"selectSnippetPlaceholder": "Select snippet...",
|
||||
"addActionBtn": "Add Action",
|
||||
"hostNotFound": "Host not found",
|
||||
"loadingHost": "Loading host..."
|
||||
},
|
||||
@@ -269,15 +258,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Disk"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Cancel",
|
||||
"confirmRunButton": "Run",
|
||||
"confirmRunMessage": "Run \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Connected"
|
||||
},
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "hardloop",
|
||||
"loginStats": "SSH-aanmeldingstatistieke",
|
||||
"noRecentLoginData": "Geen onlangse aanmelddata nie",
|
||||
"executingQuickAction": "Voer tans {{name}} uit ...",
|
||||
"quickActionSuccess": "{{name}} suksesvol voltooi",
|
||||
"quickActionFailed": "{{name}} het misluk",
|
||||
"quickActionError": "Kon nie {{name}} uitvoer nie",
|
||||
"ports": {
|
||||
"title": "Luisterpoorte",
|
||||
"protocol": "Protokol",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Voeg by",
|
||||
"visibleWidgets": "Sigbare Widgets",
|
||||
"widgetsMovedToHostMetrics": "Kaarte word nou direk in die Gasheermetrieke-oortjie bygevoeg, gerangskik en herskaal. Maak Gasheermetrieke vir hierdie gasheer oop en gebruik Pasmaak om te kies watter kaarte gewys word.",
|
||||
"quickActionsLabel": "Vinnige Aksies",
|
||||
"quickActionsToolbar": "Vinnige aksies verskyn as knoppies in die Host Metrics-nutsbalk vir die uitvoering van opdragte met een klik.",
|
||||
"noQuickActions": "Geen vinnige aksies nog nie.",
|
||||
"buttonLabel": "Knoppie-etiket",
|
||||
"selectSnippetPlaceholder": "Kies uittreksel...",
|
||||
"addActionBtn": "Voeg Aksie by",
|
||||
"hostNotFound": "Gasheer nie gevind nie",
|
||||
"loadingHost": "Laai gasheer..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Skyf"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Kanselleer",
|
||||
"confirmRunButton": "Hardloop",
|
||||
"confirmRunMessage": "Begin \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Verbonde"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "قيد التشغيل",
|
||||
"loginStats": "إحصائيات تسجيل الدخول عبر SSH",
|
||||
"noRecentLoginData": "لا توجد بيانات تسجيل دخول حديثة",
|
||||
"executingQuickAction": "جارٍ تنفيذ {{name}}...",
|
||||
"quickActionSuccess": "تم إكمال {{name}} بنجاح",
|
||||
"quickActionFailed": "فشل {{name}}",
|
||||
"quickActionError": "فشل تنفيذ {{name}}",
|
||||
"ports": {
|
||||
"title": "المنافذ المستمعة",
|
||||
"protocol": "البروتوكول",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "يضيف",
|
||||
"visibleWidgets": "الأدوات المرئية",
|
||||
"widgetsMovedToHostMetrics": "تتم الآن إضافة البطاقات وترتيبها وتغيير حجمها مباشرةً في علامة تبويب مقاييس المضيف. افتح مقاييس المضيف لهذا المضيف واستخدم تخصيص لاختيار البطاقات التي ستظهر.",
|
||||
"quickActionsLabel": "إجراءات سريعة",
|
||||
"quickActionsToolbar": "تظهر الإجراءات السريعة كأزرار في شريط أدوات مقاييس المضيف لتنفيذ الأوامر بنقرة واحدة.",
|
||||
"noQuickActions": "لا توجد إجراءات سريعة بعد.",
|
||||
"buttonLabel": "تسمية الزر",
|
||||
"selectSnippetPlaceholder": "اختر مقتطفًا...",
|
||||
"addActionBtn": "إضافة إجراء",
|
||||
"hostNotFound": "المضيف غير موجود",
|
||||
"loadingHost": "جارٍ تحميل المضيف..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "القرص"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "إلغاء",
|
||||
"confirmRunButton": "تشغيل",
|
||||
"confirmRunMessage": "تشغيل \"{{name}}\"؟"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "متصل"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "бягане",
|
||||
"loginStats": "Статистика за SSH вход",
|
||||
"noRecentLoginData": "Няма скорошни данни за вход",
|
||||
"executingQuickAction": "Изпълнява се {{name}}...",
|
||||
"quickActionSuccess": "{{name}} завършено успешно",
|
||||
"quickActionFailed": "{{name}} неуспешно",
|
||||
"quickActionError": "Неуспешно изпълнение на {{name}}",
|
||||
"ports": {
|
||||
"title": "Портове за слушане",
|
||||
"protocol": "Протокол",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Добавяне",
|
||||
"visibleWidgets": "Видими джаджи",
|
||||
"widgetsMovedToHostMetrics": "Картите вече се добавят, подреждат и преоразмеряват директно в раздела „Показатели за хоста“. Отворете „Показатели за хоста“ за този хост и използвайте „Персонализиране“, за да изберете кои карти да се показват.",
|
||||
"quickActionsLabel": "Бързи действия",
|
||||
"quickActionsToolbar": "Бързите действия се показват като бутони в лентата с инструменти Host Metrics за изпълнение на команди с едно щракване.",
|
||||
"noQuickActions": "Все още няма бързи действия.",
|
||||
"buttonLabel": "Етикет на бутона",
|
||||
"selectSnippetPlaceholder": "Изберете фрагмент...",
|
||||
"addActionBtn": "Добавяне на действие",
|
||||
"hostNotFound": "Хостът не е намерен",
|
||||
"loadingHost": "Зареждане на хоста..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Диск"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Отказ",
|
||||
"confirmRunButton": "Бягане",
|
||||
"confirmRunMessage": "Изпълни „{{name}}“?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Свързан"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "দৌড়ানো",
|
||||
"loginStats": "SSH লগইন পরিসংখ্যান",
|
||||
"noRecentLoginData": "সাম্প্রতিক কোনো লগইন ডেটা নেই",
|
||||
"executingQuickAction": "{{name}} কার্যকর করা হচ্ছে ...",
|
||||
"quickActionSuccess": "{{name}} সফলভাবে সম্পন্ন হয়েছে",
|
||||
"quickActionFailed": "{{name}} ব্যর্থ হয়েছে",
|
||||
"quickActionError": "{{name}} কার্যকর করতে ব্যর্থ হয়েছে",
|
||||
"ports": {
|
||||
"title": "লিসেনিং পোর্ট",
|
||||
"protocol": "প্রোটোকল",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "যোগ করুন",
|
||||
"visibleWidgets": "দৃশ্যমান উইজেট",
|
||||
"widgetsMovedToHostMetrics": "এখন কার্ডগুলি সরাসরি হোস্ট মেট্রিক্স ট্যাবে যোগ, সাজানো এবং আকার পরিবর্তন করা যায়। এই হোস্টের জন্য হোস্ট মেট্রিক্স খুলুন এবং কোন কার্ডগুলি দেখানো হবে তা বেছে নিতে কাস্টমাইজ ব্যবহার করুন।",
|
||||
"quickActionsLabel": "দ্রুত পদক্ষেপ",
|
||||
"quickActionsToolbar": "এক ক্লিকে কমান্ড কার্যকর করার জন্য কুইক অ্যাকশনগুলো হোস্ট মেট্রিক্স টুলবারে বাটন হিসেবে প্রদর্শিত হয়।",
|
||||
"noQuickActions": "এখনো কোনো দ্রুত পদক্ষেপ নেওয়া হয়নি।",
|
||||
"buttonLabel": "বোতাম লেবেল",
|
||||
"selectSnippetPlaceholder": "স্নিপেট নির্বাচন করুন...",
|
||||
"addActionBtn": "অ্যাকশন যোগ করুন",
|
||||
"hostNotFound": "হোস্ট খুঁজে পাওয়া যায়নি",
|
||||
"loadingHost": "হোস্ট লোড হচ্ছে..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "ডিস্ক"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "বাতিল করুন",
|
||||
"confirmRunButton": "দৌড়",
|
||||
"confirmRunMessage": "\"{{name}} \" চালান?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "সংযুক্ত"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "corrent",
|
||||
"loginStats": "Estadístiques d'inici de sessió SSH",
|
||||
"noRecentLoginData": "Sense dades d'inici de sessió recents",
|
||||
"executingQuickAction": "Executant {{name}}...",
|
||||
"quickActionSuccess": "{{name}} s'ha completat correctament",
|
||||
"quickActionFailed": "{{name}} ha fallat",
|
||||
"quickActionError": "No s'ha pogut executar {{name}}",
|
||||
"ports": {
|
||||
"title": "Ports d'escolta",
|
||||
"protocol": "Protocol",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Afegeix",
|
||||
"visibleWidgets": "Widgets visibles",
|
||||
"widgetsMovedToHostMetrics": "Ara les targetes s'afegeixen, s'organitzen i es canvien de mida directament a la pestanya Mètriques de l'amfitrió. Obriu Mètriques de l'amfitrió per a aquest amfitrió i utilitzeu Personalitza per triar quines targetes es mostren.",
|
||||
"quickActionsLabel": "Accions ràpides",
|
||||
"quickActionsToolbar": "Les accions ràpides apareixen com a botons a la barra d'eines de mètriques de l'amfitrió per executar ordres amb un sol clic.",
|
||||
"noQuickActions": "Encara no hi ha accions ràpides.",
|
||||
"buttonLabel": "Etiqueta del botó",
|
||||
"selectSnippetPlaceholder": "Selecciona un fragment...",
|
||||
"addActionBtn": "Afegeix una acció",
|
||||
"hostNotFound": "No s'ha trobat l'amfitrió",
|
||||
"loadingHost": "S'està carregant l'amfitrió..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Disc"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Cancel·la",
|
||||
"confirmRunButton": "Corre",
|
||||
"confirmRunMessage": "Executar \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Connectat"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "běh",
|
||||
"loginStats": "Statistiky přihlášení SSH",
|
||||
"noRecentLoginData": "Žádné nedávné přihlašovací údaje",
|
||||
"executingQuickAction": "Spouštění {{name}}...",
|
||||
"quickActionSuccess": "{{name}} úspěšně dokončeno",
|
||||
"quickActionFailed": "{{name}} selhalo",
|
||||
"quickActionError": "Nepodařilo se spustit {{name}}",
|
||||
"ports": {
|
||||
"title": "Poslouchající porty",
|
||||
"protocol": "Protokol",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Přidat",
|
||||
"visibleWidgets": "Viditelné widgety",
|
||||
"widgetsMovedToHostMetrics": "Karty se nyní přidávají, uspořádávají a mění jejich velikost přímo na kartě Metriky hostitele. Otevřete Metriky hostitele pro tohoto hostitele a pomocí možnosti Přizpůsobit vyberte, které karty se zobrazí.",
|
||||
"quickActionsLabel": "Rychlé akce",
|
||||
"quickActionsToolbar": "Rychlé akce se zobrazují jako tlačítka v panelu nástrojů Metriky hostitele pro spuštění příkazů jedním kliknutím.",
|
||||
"noQuickActions": "Zatím žádné rychlé akce.",
|
||||
"buttonLabel": "Štítek tlačítka",
|
||||
"selectSnippetPlaceholder": "Vybrat úryvek...",
|
||||
"addActionBtn": "Přidat akci",
|
||||
"hostNotFound": "Hostitel nenalezen",
|
||||
"loadingHost": "Načítání hostitele..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Disk"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Zrušit",
|
||||
"confirmRunButton": "Běh",
|
||||
"confirmRunMessage": "Spustit „{{name}}“?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Připojeno"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "løb",
|
||||
"loginStats": "SSH-loginstatistik",
|
||||
"noRecentLoginData": "Ingen nylige logindata",
|
||||
"executingQuickAction": "Udfører {{name}}...",
|
||||
"quickActionSuccess": "{{name}} gennemført",
|
||||
"quickActionFailed": "{{name}} mislykkedes",
|
||||
"quickActionError": "Kunne ikke udføre {{name}}",
|
||||
"ports": {
|
||||
"title": "Lytteporte",
|
||||
"protocol": "Protokol",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Tilføje",
|
||||
"visibleWidgets": "Synlige widgets",
|
||||
"widgetsMovedToHostMetrics": "Kort tilføjes, arrangeres og skaleres nu direkte i fanen Værtsmål. Åbn Værtsmål for denne vært, og brug Tilpas til at vælge, hvilke kort der vises.",
|
||||
"quickActionsLabel": "Hurtige handlinger",
|
||||
"quickActionsToolbar": "Hurtige handlinger vises som knapper i værktøjslinjen Host Metrics til udførelse af kommandoer med et enkelt klik.",
|
||||
"noQuickActions": "Ingen hurtige handlinger endnu.",
|
||||
"buttonLabel": "Knapmærke",
|
||||
"selectSnippetPlaceholder": "Vælg uddrag...",
|
||||
"addActionBtn": "Tilføj handling",
|
||||
"hostNotFound": "Værten blev ikke fundet",
|
||||
"loadingHost": "Indlæser vært..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Disk"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Ophæve",
|
||||
"confirmRunButton": "Løbe",
|
||||
"confirmRunMessage": "Kør \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Forbundet"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "laufend",
|
||||
"loginStats": "SSH-Anmeldestatistiken",
|
||||
"noRecentLoginData": "Keine aktuellen Anmeldedaten",
|
||||
"executingQuickAction": "{{name}} wird ausgeführt...",
|
||||
"quickActionSuccess": "{{name}} erfolgreich abgeschlossen",
|
||||
"quickActionFailed": "{{name}} fehlgeschlagen",
|
||||
"quickActionError": "Fehler beim Ausführen von {{name}}",
|
||||
"ports": {
|
||||
"title": "Lauschende Ports",
|
||||
"protocol": "Protokoll",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Hinzufügen",
|
||||
"visibleWidgets": "Sichtbare Widgets",
|
||||
"widgetsMovedToHostMetrics": "Karten werden jetzt direkt im Tab „Host-Metriken“ hinzugefügt, angeordnet und in der Größe verändert. Öffnen Sie die Host-Metriken für diesen Host und verwenden Sie „Anpassen“, um auszuwählen, welche Karten angezeigt werden.",
|
||||
"quickActionsLabel": "Schnellaktionen",
|
||||
"quickActionsToolbar": "Schnellaktionen erscheinen als Schaltflächen in der Host-Metriken-Symbolleiste zur Ausführung von Befehlen mit einem Klick.",
|
||||
"noQuickActions": "Noch keine Schnellaktionen.",
|
||||
"buttonLabel": "Schaltflächenbeschriftung",
|
||||
"selectSnippetPlaceholder": "Snippet auswählen...",
|
||||
"addActionBtn": "Aktion hinzufügen",
|
||||
"hostNotFound": "Host nicht gefunden",
|
||||
"loadingHost": "Host wird geladen..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Datenträger"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Abbrechen",
|
||||
"confirmRunButton": "Ausführen",
|
||||
"confirmRunMessage": "„{{name}}“ ausführen?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Verbunden"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "τρέξιμο",
|
||||
"loginStats": "Στατιστικά στοιχεία σύνδεσης SSH",
|
||||
"noRecentLoginData": "Δεν υπάρχουν πρόσφατα δεδομένα σύνδεσης",
|
||||
"executingQuickAction": "Εκτέλεση {{name}}...",
|
||||
"quickActionSuccess": "{{name}} ολοκληρώθηκε με επιτυχία",
|
||||
"quickActionFailed": "{{name}} απέτυχε",
|
||||
"quickActionError": "Αποτυχία εκτέλεσης {{name}}",
|
||||
"ports": {
|
||||
"title": "Θύρες ακρόασης",
|
||||
"protocol": "Πρωτόκολλο",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Προσθέτω",
|
||||
"visibleWidgets": "Ορατά γραφικά στοιχεία",
|
||||
"widgetsMovedToHostMetrics": "Οι κάρτες προστίθενται, ταξινομούνται και αλλάζουν μέγεθος απευθείας στην καρτέλα Μετρήσεις κεντρικού υπολογιστή. Ανοίξτε τις Μετρήσεις κεντρικού υπολογιστή για αυτόν τον κεντρικό υπολογιστή και χρησιμοποιήστε την Προσαρμογή για να επιλέξετε ποιες κάρτες θα εμφανίζονται.",
|
||||
"quickActionsLabel": "Γρήγορες ενέργειες",
|
||||
"quickActionsToolbar": "Οι γρήγορες ενέργειες εμφανίζονται ως κουμπιά στη γραμμή εργαλείων Μετρήσεων κεντρικού υπολογιστή για εκτέλεση εντολών με ένα κλικ.",
|
||||
"noQuickActions": "Δεν υπάρχουν ακόμη γρήγορες ενέργειες.",
|
||||
"buttonLabel": "Ετικέτα κουμπιού",
|
||||
"selectSnippetPlaceholder": "Επιλογή αποσπάσματος...",
|
||||
"addActionBtn": "Προσθήκη ενέργειας",
|
||||
"hostNotFound": "Δεν βρέθηκε κεντρικός υπολογιστής",
|
||||
"loadingHost": "Φόρτωση κεντρικού υπολογιστή..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Δίσκος"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Ματαίωση",
|
||||
"confirmRunButton": "Τρέξιμο",
|
||||
"confirmRunMessage": "Εκτέλεση \"{{name}}\";"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Συνδεδεμένος"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "en ejecución",
|
||||
"loginStats": "Estadísticas de inicio de sesión SSH",
|
||||
"noRecentLoginData": "No hay datos de inicio de sesión recientes",
|
||||
"executingQuickAction": "Ejecutando {{name}}...",
|
||||
"quickActionSuccess": "{{name}} completado correctamente",
|
||||
"quickActionFailed": "{{name}} falló",
|
||||
"quickActionError": "Error al ejecutar {{name}}",
|
||||
"ports": {
|
||||
"title": "Puertos en escucha",
|
||||
"protocol": "Protocolo",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Agregar",
|
||||
"visibleWidgets": "Widgets visibles",
|
||||
"widgetsMovedToHostMetrics": "Ahora las tarjetas se añaden, organizan y redimensionan directamente en la pestaña Métricas del host. Abre Métricas del host para este host y usa Personalizar para elegir qué tarjetas se muestran.",
|
||||
"quickActionsLabel": "Acciones rápidas",
|
||||
"quickActionsToolbar": "Las acciones rápidas aparecen como botones en la barra de herramientas de Métricas del host para la ejecución de comandos con un solo clic.",
|
||||
"noQuickActions": "Aún no hay acciones rápidas.",
|
||||
"buttonLabel": "Etiqueta del botón",
|
||||
"selectSnippetPlaceholder": "Seleccionar fragmento...",
|
||||
"addActionBtn": "Añadir acción",
|
||||
"hostNotFound": "Host no encontrado",
|
||||
"loadingHost": "Cargando host..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Disco"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Cancelar",
|
||||
"confirmRunButton": "Ejecutar",
|
||||
"confirmRunMessage": "¿Ejecutar \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Conectado"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "juokseminen",
|
||||
"loginStats": "SSH-kirjautumistilastot",
|
||||
"noRecentLoginData": "Ei viimeaikaisia kirjautumistietoja",
|
||||
"executingQuickAction": "Suoritetaan {{name}}...",
|
||||
"quickActionSuccess": "{{name}} suoritettu onnistuneesti",
|
||||
"quickActionFailed": "{{name}} epäonnistui",
|
||||
"quickActionError": "Suoritus epäonnistui {{name}}",
|
||||
"ports": {
|
||||
"title": "Kuunteluportit",
|
||||
"protocol": "Protokolla",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Lisätä",
|
||||
"visibleWidgets": "Näkyvät widgetit",
|
||||
"widgetsMovedToHostMetrics": "Kortteja lisätään, järjestetään ja niiden kokoa muutetaan nyt suoraan Isännän mittarit -välilehdellä. Avaa tämän isännän Isännän mittarit ja valitse näytettävät kortit Mukauta-toiminnolla.",
|
||||
"quickActionsLabel": "Pikatoiminnot",
|
||||
"quickActionsToolbar": "Pikatoiminnot näkyvät painikkeina isäntämetriikkatyökalurivillä komentojen suorittamista varten yhdellä napsautuksella.",
|
||||
"noQuickActions": "Ei vielä nopeita toimia.",
|
||||
"buttonLabel": "Painikkeen nimi",
|
||||
"selectSnippetPlaceholder": "Valitse katkelma...",
|
||||
"addActionBtn": "Lisää toiminto",
|
||||
"hostNotFound": "Isäntää ei löydy",
|
||||
"loadingHost": "Ladataan isäntää..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Levy"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Peruuttaa",
|
||||
"confirmRunButton": "Juokse",
|
||||
"confirmRunMessage": "Suorittaa \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Yhdistetty"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "en cours",
|
||||
"loginStats": "Statistiques de connexion SSH",
|
||||
"noRecentLoginData": "Aucune donnée de connexion récente",
|
||||
"executingQuickAction": "Exécution de {{name}}...",
|
||||
"quickActionSuccess": "{{name}} terminé avec succès",
|
||||
"quickActionFailed": "Échec de {{name}}",
|
||||
"quickActionError": "Échec de l'exécution de {{name}}",
|
||||
"ports": {
|
||||
"title": "Ports en écoute",
|
||||
"protocol": "Protocole",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Ajouter",
|
||||
"visibleWidgets": "Widgets visibles",
|
||||
"widgetsMovedToHostMetrics": "Les cartes sont désormais ajoutées, organisées et redimensionnées directement dans l'onglet Métriques de l'hôte. Ouvrez les métriques de cet hôte et utilisez Personnaliser pour choisir les cartes à afficher.",
|
||||
"quickActionsLabel": "Actions rapides",
|
||||
"quickActionsToolbar": "Les actions rapides apparaissent sous forme de boutons dans la barre d'outils Métriques de l'hôte pour une exécution en un clic.",
|
||||
"noQuickActions": "Aucune action rapide pour le moment.",
|
||||
"buttonLabel": "Étiquette du bouton",
|
||||
"selectSnippetPlaceholder": "Sélectionner un snippet...",
|
||||
"addActionBtn": "Ajouter une action",
|
||||
"hostNotFound": "Hôte introuvable",
|
||||
"loadingHost": "Chargement de l'hôte..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Disque"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Annuler",
|
||||
"confirmRunButton": "Exécuter",
|
||||
"confirmRunMessage": "Exécuter « {{name}} » ?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Connecté"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "רִיצָה",
|
||||
"loginStats": "סטטיסטיקות כניסה ל-SSH",
|
||||
"noRecentLoginData": "אין נתוני התחברות אחרונים",
|
||||
"executingQuickAction": "מבצע {{name}}...",
|
||||
"quickActionSuccess": "{{name}} הושלם בהצלחה",
|
||||
"quickActionFailed": "{{name}} נכשל",
|
||||
"quickActionError": "נכשל בביצוע {{name}}",
|
||||
"ports": {
|
||||
"title": "יציאות האזנה",
|
||||
"protocol": "פּרוֹטוֹקוֹל",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "לְהוֹסִיף",
|
||||
"visibleWidgets": "ווידג'טים גלויים",
|
||||
"widgetsMovedToHostMetrics": "כעת ניתן להוסיף, לסידור ולשנות את גודל הכרטיסים ישירות בכרטיסייה 'מדדי מארח'. פתחו את 'מדדי מארח' עבור מארח זה והשתמשו באפשרות 'התאמה אישית' כדי לבחור אילו כרטיסים יוצגו.",
|
||||
"quickActionsLabel": "פעולות מהירות",
|
||||
"quickActionsToolbar": "פעולות מהירות מופיעות ככפתורים בסרגל הכלים של Host Metrics לביצוע פקודה בלחיצה אחת.",
|
||||
"noQuickActions": "אין עדיין פעולות מהירות.",
|
||||
"buttonLabel": "תווית כפתור",
|
||||
"selectSnippetPlaceholder": "בחר קטע...",
|
||||
"addActionBtn": "הוסף פעולה",
|
||||
"hostNotFound": "המארח לא נמצא",
|
||||
"loadingHost": "טוען מארח..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "דִיסק"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "לְבַטֵל",
|
||||
"confirmRunButton": "לָרוּץ",
|
||||
"confirmRunMessage": "להפעיל את \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "מְחוּבָּר"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "चल रहे",
|
||||
"loginStats": "SSH लॉगिन आँकड़े",
|
||||
"noRecentLoginData": "कोई हालिया लॉगिन डेटा नहीं",
|
||||
"executingQuickAction": "{{name}} निष्पादित हो रहा है...",
|
||||
"quickActionSuccess": "{{name}} सफलतापूर्वक पूर्ण हुआ",
|
||||
"quickActionFailed": "{{name}} विफल रहा",
|
||||
"quickActionError": "{{name}} निष्पादित करने में विफल",
|
||||
"ports": {
|
||||
"title": "लिसनिंग पोर्ट",
|
||||
"protocol": "प्रोटोकॉल",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "जोड़ना",
|
||||
"visibleWidgets": "दृश्यमान विजेट्स",
|
||||
"widgetsMovedToHostMetrics": "कार्ड अब सीधे होस्ट मेट्रिक्स टैब में जोड़े, व्यवस्थित और आकार बदले जा सकते हैं। इस होस्ट के लिए होस्ट मेट्रिक्स खोलें और यह चुनने के लिए कस्टमाइज़ का उपयोग करें कि कौन से कार्ड दिखाए जाएँ।",
|
||||
"quickActionsLabel": "त्वरित कार्रवाइयाँ",
|
||||
"quickActionsToolbar": "त्वरित कार्रवाइयाँ एक-क्लिक कमांड निष्पादन के लिए होस्ट मेट्रिक्स टूलबार में बटन के रूप में दिखाई देती हैं।",
|
||||
"noQuickActions": "अभी तक कोई त्वरित कार्रवाई नहीं।",
|
||||
"buttonLabel": "बटन लेबल",
|
||||
"selectSnippetPlaceholder": "स्निपेट चुनें...",
|
||||
"addActionBtn": "कार्रवाई जोड़ें",
|
||||
"hostNotFound": "होस्ट नहीं मिला",
|
||||
"loadingHost": "होस्ट लोड हो रहा है..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "डिस्क"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "रद्द करें",
|
||||
"confirmRunButton": "चलाएँ",
|
||||
"confirmRunMessage": "\"{{name}}\" चलाएँ?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "कनेक्टेड"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "futás",
|
||||
"loginStats": "SSH bejelentkezési statisztikák",
|
||||
"noRecentLoginData": "Nincsenek friss bejelentkezési adatok",
|
||||
"executingQuickAction": "{{name}} végrehajtása ...",
|
||||
"quickActionSuccess": "{{name}} sikeresen befejeződött",
|
||||
"quickActionFailed": "{{name}} sikertelen",
|
||||
"quickActionError": "Nem sikerült végrehajtani a {{name}} parancsot.",
|
||||
"ports": {
|
||||
"title": "Figyelő portok",
|
||||
"protocol": "Jegyzőkönyv",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Hozzáadás",
|
||||
"visibleWidgets": "Látható widgetek",
|
||||
"widgetsMovedToHostMetrics": "A kártyák mostantól közvetlenül a Gazdagép metrikái lapon adhatók hozzá, rendezhetők el és méretezhetők át. Nyissa meg a Gazdagép metrikái oldalt ehhez a gazdagéphez, és a Testreszabás gombbal válassza ki, hogy mely kártyák jelenjenek meg.",
|
||||
"quickActionsLabel": "Gyors műveletek",
|
||||
"quickActionsToolbar": "A gyors műveletek gombokként jelennek meg a Host Metrics eszköztáron, így egyetlen kattintással végrehajthatók a parancsok.",
|
||||
"noQuickActions": "Még nincsenek gyors intézkedések.",
|
||||
"buttonLabel": "Gomb felirata",
|
||||
"selectSnippetPlaceholder": "Válasszon részletet...",
|
||||
"addActionBtn": "Művelet hozzáadása",
|
||||
"hostNotFound": "Nem található gazdagép",
|
||||
"loadingHost": "Gazdagép betöltése..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Korong"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Mégsem",
|
||||
"confirmRunButton": "Fut",
|
||||
"confirmRunMessage": "Futtassa a \"{{name}} \" programot?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Csatlakoztatva"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "berlari",
|
||||
"loginStats": "Statistik Login SSH",
|
||||
"noRecentLoginData": "Tidak ada data login terbaru.",
|
||||
"executingQuickAction": "Menjalankan {{name}}...",
|
||||
"quickActionSuccess": "{{name}} berhasil diselesaikan",
|
||||
"quickActionFailed": "{{name}} gagal",
|
||||
"quickActionError": "Gagal mengeksekusi {{name}}",
|
||||
"ports": {
|
||||
"title": "Port Pendengaran",
|
||||
"protocol": "Protokol",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Menambahkan",
|
||||
"visibleWidgets": "Widget yang Terlihat",
|
||||
"widgetsMovedToHostMetrics": "Kartu sekarang ditambahkan, diatur, dan diubah ukurannya langsung di tab Metrik Host. Buka Metrik Host untuk host ini dan gunakan Sesuaikan untuk memilih kartu mana yang akan ditampilkan.",
|
||||
"quickActionsLabel": "Tindakan Cepat",
|
||||
"quickActionsToolbar": "Tindakan cepat muncul sebagai tombol di bilah alat Metrik Host untuk eksekusi perintah sekali klik.",
|
||||
"noQuickActions": "Belum ada tindakan cepat.",
|
||||
"buttonLabel": "Label tombol",
|
||||
"selectSnippetPlaceholder": "Pilih cuplikan...",
|
||||
"addActionBtn": "Tambahkan Tindakan",
|
||||
"hostNotFound": "Host tidak ditemukan",
|
||||
"loadingHost": "Memuat host..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Disk"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Membatalkan",
|
||||
"confirmRunButton": "Berlari",
|
||||
"confirmRunMessage": "Jalankan \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Terhubung"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "in esecuzione",
|
||||
"loginStats": "Statistiche di accesso SSH",
|
||||
"noRecentLoginData": "Nessun dato di accesso recente",
|
||||
"executingQuickAction": "Esecuzione di {{name}}...",
|
||||
"quickActionSuccess": "{{name}} completato con successo",
|
||||
"quickActionFailed": "{{name}} non riuscito",
|
||||
"quickActionError": "Impossibile eseguire {{name}}",
|
||||
"ports": {
|
||||
"title": "Porte in ascolto",
|
||||
"protocol": "Protocollo",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Aggiungere",
|
||||
"visibleWidgets": "Widget visibili",
|
||||
"widgetsMovedToHostMetrics": "Le schede ora vengono aggiunte, disposte e ridimensionate direttamente nella scheda Metriche host. Apri Metriche host per questo host e usa Personalizza per scegliere quali schede mostrare.",
|
||||
"quickActionsLabel": "Azioni Rapide",
|
||||
"quickActionsToolbar": "Le azioni rapide appaiono come pulsanti nella barra delle metriche host per l'esecuzione di comandi con un clic.",
|
||||
"noQuickActions": "Nessuna azione rapida ancora.",
|
||||
"buttonLabel": "Etichetta pulsante",
|
||||
"selectSnippetPlaceholder": "Seleziona snippet...",
|
||||
"addActionBtn": "Aggiungi Azione",
|
||||
"hostNotFound": "Host non trovato",
|
||||
"loadingHost": "Caricamento host..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Disco"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Annulla",
|
||||
"confirmRunButton": "Esegui",
|
||||
"confirmRunMessage": "Eseguire \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Connesso"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "実行中",
|
||||
"loginStats": "SSHログイン統計",
|
||||
"noRecentLoginData": "最近のログインデータがありません",
|
||||
"executingQuickAction": "{{name}} を実行中...",
|
||||
"quickActionSuccess": "{{name}} が正常に完了しました",
|
||||
"quickActionFailed": "{{name}} が失敗しました",
|
||||
"quickActionError": "{{name}} の実行に失敗しました",
|
||||
"ports": {
|
||||
"title": "リスニングポート",
|
||||
"protocol": "プロトコル",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "追加",
|
||||
"visibleWidgets": "表示するウィジェット",
|
||||
"widgetsMovedToHostMetrics": "カードは、ホストメトリクスタブで直接追加、配置、サイズ変更できるようになりました。このホストのホストメトリクスを開き、カスタマイズを使用して表示するカードを選択してください。",
|
||||
"quickActionsLabel": "クイックアクション",
|
||||
"quickActionsToolbar": "クイックアクションはホストメトリクスツールバーにボタンとして表示され、ワンクリックでコマンドを実行できます。",
|
||||
"noQuickActions": "クイックアクションはまだありません",
|
||||
"buttonLabel": "ボタンラベル",
|
||||
"selectSnippetPlaceholder": "スニペットを選択...",
|
||||
"addActionBtn": "アクションを追加",
|
||||
"hostNotFound": "ホストが見つかりません",
|
||||
"loadingHost": "ホストを読み込み中..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "ディスク"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "キャンセル",
|
||||
"confirmRunButton": "実行",
|
||||
"confirmRunMessage": "「{{name}}」を実行しますか?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "接続中"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "실행 중",
|
||||
"loginStats": "SSH 로그인 통계",
|
||||
"noRecentLoginData": "최근 로그인 데이터가 없습니다.",
|
||||
"executingQuickAction": "{{name}} 실행 중...",
|
||||
"quickActionSuccess": "{{name}}이(가) 성공적으로 완료되었습니다.",
|
||||
"quickActionFailed": "{{name}} 실패",
|
||||
"quickActionError": "{{name}} 실행에 실패했습니다.",
|
||||
"ports": {
|
||||
"title": "수신 대기 포트",
|
||||
"protocol": "프로토콜",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "추가하다",
|
||||
"visibleWidgets": "표시 위젯",
|
||||
"widgetsMovedToHostMetrics": "카드는 이제 호스트 메트릭 탭에서 직접 추가, 정렬 및 크기 조정됩니다. 이 호스트의 호스트 메트릭을 열고 사용자 정의를 사용하여 표시할 카드를 선택하세요.",
|
||||
"quickActionsLabel": "빠른 작업",
|
||||
"quickActionsToolbar": "빠른 작업은 한 번의 클릭으로 명령을 실행할 수 있도록 호스트 메트릭 도구 모음에 버튼으로 표시됩니다.",
|
||||
"noQuickActions": "아직 빠른 작업이 없습니다.",
|
||||
"buttonLabel": "버튼 레이블",
|
||||
"selectSnippetPlaceholder": "스니펫 선택...",
|
||||
"addActionBtn": "작업 추가",
|
||||
"hostNotFound": "호스트를 찾을 수 없습니다",
|
||||
"loadingHost": "호스트 로드 중..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "디스크"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "취소",
|
||||
"confirmRunButton": "실행",
|
||||
"confirmRunMessage": "\"{{name}}\"을(를) 실행하시겠습니까?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "연결됨"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "rennen",
|
||||
"loginStats": "SSH-aanmeldingsstatistieken",
|
||||
"noRecentLoginData": "Geen recente inloggegevens",
|
||||
"executingQuickAction": "Het uitvoeren van {{name}}...",
|
||||
"quickActionSuccess": "{{name}} succesvol voltooid",
|
||||
"quickActionFailed": "{{name}} mislukt",
|
||||
"quickActionError": "Het uitvoeren van {{name}} is mislukt.",
|
||||
"ports": {
|
||||
"title": "Luisterpoorten",
|
||||
"protocol": "Protocol",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Toevoegen",
|
||||
"visibleWidgets": "Zichtbare widgets",
|
||||
"widgetsMovedToHostMetrics": "Kaarten kunnen nu rechtstreeks in het tabblad Hoststatistieken worden toegevoegd, gerangschikt en van formaat worden veranderd. Open Hoststatistieken voor deze host en gebruik Aanpassen om te kiezen welke kaarten worden weergegeven.",
|
||||
"quickActionsLabel": "Snelle acties",
|
||||
"quickActionsToolbar": "Snelle acties verschijnen als knoppen in de Host Metrics-werkbalk, zodat u opdrachten met één klik kunt uitvoeren.",
|
||||
"noQuickActions": "Nog geen snelle acties ondernomen.",
|
||||
"buttonLabel": "Knoplabel",
|
||||
"selectSnippetPlaceholder": "Selecteer fragment...",
|
||||
"addActionBtn": "Actie toevoegen",
|
||||
"hostNotFound": "Host niet gevonden",
|
||||
"loadingHost": "Host wordt geladen..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Schijf"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Annuleren",
|
||||
"confirmRunButton": "Loop",
|
||||
"confirmRunMessage": "Run \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Aangesloten"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "løping",
|
||||
"loginStats": "SSH-påloggingsstatistikk",
|
||||
"noRecentLoginData": "Ingen nylige påloggingsdata",
|
||||
"executingQuickAction": "Utfører {{name}}...",
|
||||
"quickActionSuccess": "{{name}} fullført",
|
||||
"quickActionFailed": "{{name}} mislyktes",
|
||||
"quickActionError": "Kunne ikke utføre {{name}}",
|
||||
"ports": {
|
||||
"title": "Lytteporter",
|
||||
"protocol": "Protokoll",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Legge til",
|
||||
"visibleWidgets": "Synlige widgeter",
|
||||
"widgetsMovedToHostMetrics": "Kort legges nå til, arrangeres og endres i størrelse direkte i fanen Vertsmålinger. Åpne Vertsmålinger for denne verten og bruk Tilpass for å velge hvilke kort som vises.",
|
||||
"quickActionsLabel": "Hurtighandlinger",
|
||||
"quickActionsToolbar": "Hurtighandlinger vises som knapper i verktøylinjen for vertsmålinger for utførelse av kommandoer med ett klikk.",
|
||||
"noQuickActions": "Ingen hurtighandlinger ennå.",
|
||||
"buttonLabel": "Knappetikett",
|
||||
"selectSnippetPlaceholder": "Velg utdrag...",
|
||||
"addActionBtn": "Legg til handling",
|
||||
"hostNotFound": "Verten ble ikke funnet",
|
||||
"loadingHost": "Laster inn vert ..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Disk"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Kansellere",
|
||||
"confirmRunButton": "Løp",
|
||||
"confirmRunMessage": "Kjør \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Tilkoblet"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "działa",
|
||||
"loginStats": "Statystyki logowania SSH",
|
||||
"noRecentLoginData": "Brak ostatnich danych logowania",
|
||||
"executingQuickAction": "Wykonywanie {{name}}...",
|
||||
"quickActionSuccess": "{{name}} ukończono pomyślnie",
|
||||
"quickActionFailed": "{{name}} nie powiodło się",
|
||||
"quickActionError": "Nie udało się wykonać {{name}}",
|
||||
"ports": {
|
||||
"title": "Porty nasłuchujące",
|
||||
"protocol": "Protokół",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Dodaj",
|
||||
"visibleWidgets": "Widoczne widżety",
|
||||
"widgetsMovedToHostMetrics": "Karty dodaje się, układa i skaluje bezpośrednio na karcie „Metryki hosta”. Otwórz Metryki hosta dla tego hosta i użyj opcji „Dostosuj”, aby wybrać wyświetlane karty.",
|
||||
"quickActionsLabel": "Szybkie akcje",
|
||||
"quickActionsToolbar": "Szybkie akcje pojawiają się jako przyciski na pasku narzędzi Metryki hosta, umożliwiając wykonanie polecenia jednym kliknięciem.",
|
||||
"noQuickActions": "Brak szybkich akcji.",
|
||||
"buttonLabel": "Etykieta przycisku",
|
||||
"selectSnippetPlaceholder": "Wybierz snippet...",
|
||||
"addActionBtn": "Dodaj akcję",
|
||||
"hostNotFound": "Nie znaleziono hosta",
|
||||
"loadingHost": "Ładowanie hosta..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Dysk"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Anuluj",
|
||||
"confirmRunButton": "Uruchom",
|
||||
"confirmRunMessage": "Uruchomić „{{name}}”?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Połączono"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "correndo",
|
||||
"loginStats": "Estatísticas de login SSH",
|
||||
"noRecentLoginData": "Nenhum dado de login recente",
|
||||
"executingQuickAction": "Executando {{name}}...",
|
||||
"quickActionSuccess": "{{name}} concluído com sucesso",
|
||||
"quickActionFailed": "{{name}} falhou",
|
||||
"quickActionError": "Falha ao executar {{name}}",
|
||||
"ports": {
|
||||
"title": "Portas de escuta",
|
||||
"protocol": "Protocolo",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Adicionar",
|
||||
"visibleWidgets": "Widgets Visíveis",
|
||||
"widgetsMovedToHostMetrics": "Agora, os cartões podem ser adicionados, organizados e redimensionados diretamente na guia Métricas do Host. Abra as Métricas do Host para este host e use a opção Personalizar para escolher quais cartões serão exibidos.",
|
||||
"quickActionsLabel": "Ações rápidas",
|
||||
"quickActionsToolbar": "As ações rápidas aparecem como botões na barra de ferramentas do Host Metrics para execução de comandos com um único clique.",
|
||||
"noQuickActions": "Nenhuma ação imediata ainda.",
|
||||
"buttonLabel": "Rótulo do botão",
|
||||
"selectSnippetPlaceholder": "Selecione o trecho...",
|
||||
"addActionBtn": "Adicionar ação",
|
||||
"hostNotFound": "Host não encontrado",
|
||||
"loadingHost": "Carregando host..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Disco"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Cancelar",
|
||||
"confirmRunButton": "Correr",
|
||||
"confirmRunMessage": "Executar \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Conectado"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "em execução",
|
||||
"loginStats": "Estatísticas de início de sessão SSH",
|
||||
"noRecentLoginData": "Sem dados de início de sessão recentes",
|
||||
"executingQuickAction": "A executar {{name}}...",
|
||||
"quickActionSuccess": "{{name}} concluído com sucesso",
|
||||
"quickActionFailed": "{{name}} falhou",
|
||||
"quickActionError": "Falha ao executar {{name}}",
|
||||
"ports": {
|
||||
"title": "Portas em escuta",
|
||||
"protocol": "Protocolo",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Adicionar",
|
||||
"visibleWidgets": "Widgets Visíveis",
|
||||
"widgetsMovedToHostMetrics": "Os cartões são agora adicionados, organizados e redimensionados diretamente no separador Métricas do Host. Abra as Métricas do Host para este host e use Personalizar para escolher quais os cartões a mostrar.",
|
||||
"quickActionsLabel": "Ações Rápidas",
|
||||
"quickActionsToolbar": "As ações rápidas aparecem como botões na barra de ferramentas de Métricas do Host para execução de comandos com um clique.",
|
||||
"noQuickActions": "Ainda não existem ações rápidas.",
|
||||
"buttonLabel": "Texto do botão",
|
||||
"selectSnippetPlaceholder": "Selecionar snippet...",
|
||||
"addActionBtn": "Adicionar Ação",
|
||||
"hostNotFound": "Host não encontrado",
|
||||
"loadingHost": "A carregar o host..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Disco"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Cancelar",
|
||||
"confirmRunButton": "Executar",
|
||||
"confirmRunMessage": "Executar \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Ligado"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "funcţionare",
|
||||
"loginStats": "Statistici de conectare SSH",
|
||||
"noRecentLoginData": "Nu există date de conectare recente",
|
||||
"executingQuickAction": "Se execută {{name}}...",
|
||||
"quickActionSuccess": "{{name}} finalizat cu succes",
|
||||
"quickActionFailed": "{{name}} a eșuat",
|
||||
"quickActionError": "Executarea {{name}} a eșuat",
|
||||
"ports": {
|
||||
"title": "Porturi de ascultare",
|
||||
"protocol": "Protocol",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Adăuga",
|
||||
"visibleWidgets": "Widgeturi vizibile",
|
||||
"widgetsMovedToHostMetrics": "Cardurile sunt acum adăugate, aranjate și redimensionate direct în fila Indicatori gazdă. Deschideți Indicatori gazdă pentru această gazdă și utilizați Personalizare pentru a alege ce carduri sunt afișate.",
|
||||
"quickActionsLabel": "Acțiuni rapide",
|
||||
"quickActionsToolbar": "Acțiunile rapide apar sub formă de butoane în bara de instrumente Host Metrics pentru executarea comenzilor cu un singur clic.",
|
||||
"noQuickActions": "Nicio acțiune rapidă încă.",
|
||||
"buttonLabel": "Etichetă buton",
|
||||
"selectSnippetPlaceholder": "Selectați fragmentul...",
|
||||
"addActionBtn": "Adăugați o acțiune",
|
||||
"hostNotFound": "Gazdă negăsită",
|
||||
"loadingHost": "Se încarcă gazda..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Disc"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Anula",
|
||||
"confirmRunButton": "Aleargă",
|
||||
"confirmRunMessage": "Executați „{{name}}”?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Conectat"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "выполняется",
|
||||
"loginStats": "Статистика входов SSH",
|
||||
"noRecentLoginData": "Нет данных о недавних входах",
|
||||
"executingQuickAction": "Выполняется {{name}}...",
|
||||
"quickActionSuccess": "{{name}} успешно завершено",
|
||||
"quickActionFailed": "{{name}} не удалось",
|
||||
"quickActionError": "Не удалось выполнить {{name}}",
|
||||
"ports": {
|
||||
"title": "Прослушиваемые порты",
|
||||
"protocol": "Протокол",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Добавлять",
|
||||
"visibleWidgets": "Видимые виджеты",
|
||||
"widgetsMovedToHostMetrics": "Карточки теперь добавляются, упорядочиваются и изменяются в размере непосредственно на вкладке «Метрики хоста». Откройте метрики для этого хоста и используйте «Настроить», чтобы выбрать отображаемые карточки.",
|
||||
"quickActionsLabel": "Быстрые действия",
|
||||
"quickActionsToolbar": "Быстрые действия отображаются в виде кнопок на панели инструментов метрик хоста для выполнения команд в один клик.",
|
||||
"noQuickActions": "Пока нет быстрых действий.",
|
||||
"buttonLabel": "Надпись кнопки",
|
||||
"selectSnippetPlaceholder": "Выбрать сниппет...",
|
||||
"addActionBtn": "Добавить действие",
|
||||
"hostNotFound": "Хост не найден",
|
||||
"loadingHost": "Загрузка хоста..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Диск"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Отмена",
|
||||
"confirmRunButton": "Запустить",
|
||||
"confirmRunMessage": "Запустить \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Подключено"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "трчање",
|
||||
"loginStats": "Статистика SSH пријава",
|
||||
"noRecentLoginData": "Нема скорашњих података за пријављивање",
|
||||
"executingQuickAction": "Извршава се {{name}}...",
|
||||
"quickActionSuccess": "{{name}} успешно завршено",
|
||||
"quickActionFailed": "{{name}} није успело",
|
||||
"quickActionError": "Није успело извршавање {{name}}",
|
||||
"ports": {
|
||||
"title": "Портови за слушање",
|
||||
"protocol": "Протокол",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Додај",
|
||||
"visibleWidgets": "Видљиви виџети",
|
||||
"widgetsMovedToHostMetrics": "Картице се сада додају, распоређују и мењају им се величина директно на картици „Host Metrics“. Отворите „Host Metrics“ за овог хоста и користите „Customize“ да бисте изабрали које ће картице бити приказане.",
|
||||
"quickActionsLabel": "Брзе акције",
|
||||
"quickActionsToolbar": "Брзе акције се појављују као дугмад у траци са алаткама Host Metrics за извршавање команди једним кликом.",
|
||||
"noQuickActions": "Још нема брзих акција.",
|
||||
"buttonLabel": "Ознака дугмета",
|
||||
"selectSnippetPlaceholder": "Изаберите исечак...",
|
||||
"addActionBtn": "Додај акцију",
|
||||
"hostNotFound": "Хост није пронађен",
|
||||
"loadingHost": "Учитавање хоста..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Диск"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Откажи",
|
||||
"confirmRunButton": "Трчи",
|
||||
"confirmRunMessage": "Покрени „{{name}}“?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Повезано"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "spring",
|
||||
"loginStats": "SSH-inloggningsstatistik",
|
||||
"noRecentLoginData": "Inga aktuella inloggningsuppgifter",
|
||||
"executingQuickAction": "Kör {{name}}...",
|
||||
"quickActionSuccess": "{{name}} slutfört",
|
||||
"quickActionFailed": "{{name}} misslyckades",
|
||||
"quickActionError": "Misslyckades med att köra {{name}}",
|
||||
"ports": {
|
||||
"title": "Lyssningsportar",
|
||||
"protocol": "Protokoll",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Tillägga",
|
||||
"visibleWidgets": "Synliga widgetar",
|
||||
"widgetsMovedToHostMetrics": "Kort läggs nu till, ordnas och ändras i storlek direkt på fliken Värdstatistik. Öppna Värdstatistik för den här värden och använd Anpassa för att välja vilka kort som visas.",
|
||||
"quickActionsLabel": "Snabbåtgärder",
|
||||
"quickActionsToolbar": "Snabbåtgärder visas som knappar i verktygsfältet Host Metrics för att köra kommandon med ett klick.",
|
||||
"noQuickActions": "Inga snabba åtgärder ännu.",
|
||||
"buttonLabel": "Knappetikett",
|
||||
"selectSnippetPlaceholder": "Välj utdrag...",
|
||||
"addActionBtn": "Lägg till åtgärd",
|
||||
"hostNotFound": "Värden hittades inte",
|
||||
"loadingHost": "Laddar värd..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Disk"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Avboka",
|
||||
"confirmRunButton": "Sikt",
|
||||
"confirmRunMessage": "Kör \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Ansluten"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "วิ่ง",
|
||||
"loginStats": "สถิติการเข้าสู่ระบบ SSH",
|
||||
"noRecentLoginData": "ไม่มีข้อมูลการเข้าสู่ระบบล่าสุด",
|
||||
"executingQuickAction": "กำลังดำเนินการ {{name}}...",
|
||||
"quickActionSuccess": "{{name}} เสร็จสมบูรณ์เรียบร้อยแล้ว",
|
||||
"quickActionFailed": "{{name}} ล้มเหลว",
|
||||
"quickActionError": "ไม่สามารถดำเนินการ {{name}} ได้",
|
||||
"ports": {
|
||||
"title": "พอร์ตรับฟัง",
|
||||
"protocol": "โปรโตคอล",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "เพิ่ม",
|
||||
"visibleWidgets": "วิดเจ็ตที่มองเห็นได้",
|
||||
"widgetsMovedToHostMetrics": "ขณะนี้สามารถเพิ่ม จัดเรียง และปรับขนาดการ์ดได้โดยตรงในแท็บเมตริกโฮสต์ เปิดเมตริกโฮสต์สำหรับโฮสต์นี้แล้วใช้ ปรับแต่ง เพื่อเลือกการ์ดที่จะแสดง",
|
||||
"quickActionsLabel": "การดำเนินการด่วน",
|
||||
"quickActionsToolbar": "การดำเนินการด่วนจะปรากฏเป็นปุ่มในแถบเครื่องมือ Host Metrics เพื่อให้สามารถเรียกใช้คำสั่งได้ด้วยการคลิกเพียงครั้งเดียว",
|
||||
"noQuickActions": "ยังไม่มีการดำเนินการใดๆ อย่างเร่งด่วนในขณะนี้",
|
||||
"buttonLabel": "ป้ายกำกับปุ่ม",
|
||||
"selectSnippetPlaceholder": "เลือกส่วนย่อย...",
|
||||
"addActionBtn": "เพิ่มการดำเนินการ",
|
||||
"hostNotFound": "ไม่พบโฮสต์",
|
||||
"loadingHost": "กำลังโหลดโฮสต์..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "ดิสก์"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "ยกเลิก",
|
||||
"confirmRunButton": "วิ่ง",
|
||||
"confirmRunMessage": "เรียกใช้ \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "เชื่อมต่อแล้ว"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "çalışan",
|
||||
"loginStats": "SSH Giriş İstatistikleri",
|
||||
"noRecentLoginData": "Son giriş verisi yok",
|
||||
"executingQuickAction": "{{name}} çalıştırılıyor...",
|
||||
"quickActionSuccess": "{{name}} başarıyla tamamlandı",
|
||||
"quickActionFailed": "{{name}} başarısız oldu",
|
||||
"quickActionError": "{{name}} çalıştırılamadı",
|
||||
"ports": {
|
||||
"title": "Dinlenen Portlar",
|
||||
"protocol": "Protokol",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Eklemek",
|
||||
"visibleWidgets": "Görünür Widget'lar",
|
||||
"widgetsMovedToHostMetrics": "Kartlar artık doğrudan Sunucu Metrikleri sekmesinde eklenir, düzenlenir ve yeniden boyutlandırılır. Bu sunucu için Sunucu Metrikleri'ni açın ve hangi kartların gösterileceğini seçmek için Özelleştir'i kullanın.",
|
||||
"quickActionsLabel": "Hızlı Eylemler",
|
||||
"quickActionsToolbar": "Hızlı eylemler, tek tıklamayla komut çalıştırmak için Ana Bilgisayar Metrikleri araç çubuğunda düğme olarak görünür.",
|
||||
"noQuickActions": "Henüz hızlı eylem yok.",
|
||||
"buttonLabel": "Düğme etiketi",
|
||||
"selectSnippetPlaceholder": "Parçacık seç...",
|
||||
"addActionBtn": "Eylem Ekle",
|
||||
"hostNotFound": "Host bulunamadı",
|
||||
"loadingHost": "Host yükleniyor..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Disk"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "İptal",
|
||||
"confirmRunButton": "Çalıştır",
|
||||
"confirmRunMessage": "\"{{name}}\" çalıştırılsın mı?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Bağlandı"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "біг",
|
||||
"loginStats": "Статистика входу SSH",
|
||||
"noRecentLoginData": "Немає останніх даних для входу",
|
||||
"executingQuickAction": "Виконання {{name}}...",
|
||||
"quickActionSuccess": "{{name}} успішно завершено",
|
||||
"quickActionFailed": "{{name}} не вдалося",
|
||||
"quickActionError": "Не вдалося виконати {{name}}",
|
||||
"ports": {
|
||||
"title": "Порти прослуховування",
|
||||
"protocol": "Протокол",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Додати",
|
||||
"visibleWidgets": "Видимі віджети",
|
||||
"widgetsMovedToHostMetrics": "Тепер картки додаються, упорядковуються та змінюються за розміром безпосередньо на вкладці «Метрики хоста». Відкрийте «Метрики хоста» для цього хоста та скористайтеся функцією «Налаштувати», щоб вибрати, які картки відображатимуться.",
|
||||
"quickActionsLabel": "Швидкі дії",
|
||||
"quickActionsToolbar": "Швидкі дії відображаються як кнопки на панелі інструментів «Метрики хоста» для виконання команд одним клацанням миші.",
|
||||
"noQuickActions": "Поки що жодних швидких дій.",
|
||||
"buttonLabel": "Мітка кнопки",
|
||||
"selectSnippetPlaceholder": "Вибрати фрагмент...",
|
||||
"addActionBtn": "Додати дію",
|
||||
"hostNotFound": "Хост не знайдено",
|
||||
"loadingHost": "Завантаження хоста..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Диск"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Скасувати",
|
||||
"confirmRunButton": "Бігти",
|
||||
"confirmRunMessage": "Запустити \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Підключено"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "đang chạy",
|
||||
"loginStats": "Thống kê đăng nhập SSH",
|
||||
"noRecentLoginData": "Không có dữ liệu đăng nhập gần đây",
|
||||
"executingQuickAction": "Đang thực thi {{name}}...",
|
||||
"quickActionSuccess": "{{name}} đã hoàn tất thành công",
|
||||
"quickActionFailed": "{{name}} thất bại",
|
||||
"quickActionError": "Thực thi {{name}} thất bại",
|
||||
"ports": {
|
||||
"title": "Cổng đang lắng nghe",
|
||||
"protocol": "Giao thức",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "Thêm vào",
|
||||
"visibleWidgets": "Widget hiển thị",
|
||||
"widgetsMovedToHostMetrics": "Các thẻ hiện được thêm, sắp xếp và thay đổi kích thước trực tiếp trong tab Số liệu máy. Mở Số liệu máy cho máy này và dùng Tùy chỉnh để chọn thẻ hiển thị.",
|
||||
"quickActionsLabel": "Hành động nhanh",
|
||||
"quickActionsToolbar": "Hành động nhanh xuất hiện dưới dạng nút trên thanh công cụ Chỉ số máy để thực thi lệnh một lần nhấp.",
|
||||
"noQuickActions": "Chưa có hành động nhanh nào.",
|
||||
"buttonLabel": "Nhãn nút",
|
||||
"selectSnippetPlaceholder": "Chọn đoạn mã...",
|
||||
"addActionBtn": "Thêm hành động",
|
||||
"hostNotFound": "Không tìm thấy máy chủ",
|
||||
"loadingHost": "Đang tải máy chủ..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "Ổ đĩa"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "Hủy",
|
||||
"confirmRunButton": "Chạy",
|
||||
"confirmRunMessage": "Chạy \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "Đã kết nối"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "运行中",
|
||||
"loginStats": "SSH 登录统计",
|
||||
"noRecentLoginData": "无最近登录数据",
|
||||
"executingQuickAction": "正在执行 {{name}}...",
|
||||
"quickActionSuccess": "{{name}} 执行成功",
|
||||
"quickActionFailed": "{{name}} 执行失败",
|
||||
"quickActionError": "执行 {{name}} 失败",
|
||||
"ports": {
|
||||
"title": "监听端口",
|
||||
"protocol": "协议",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "添加",
|
||||
"visibleWidgets": "可见小部件",
|
||||
"widgetsMovedToHostMetrics": "卡片现在可在“主机指标”选项卡中直接添加、排列和调整大小。打开此主机的“主机指标”,使用“自定义”选择要显示的卡片。",
|
||||
"quickActionsLabel": "快速操作",
|
||||
"quickActionsToolbar": "快速操作以按钮形式显示在主机指标工具栏中,以便一键执行命令。",
|
||||
"noQuickActions": "尚无快速操作。",
|
||||
"buttonLabel": "按钮标签",
|
||||
"selectSnippetPlaceholder": "选择代码片段...",
|
||||
"addActionBtn": "添加操作",
|
||||
"hostNotFound": "未找到主机",
|
||||
"loadingHost": "正在加载主机..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "磁盘"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "取消",
|
||||
"confirmRunButton": "运行",
|
||||
"confirmRunMessage": "运行“{{name}}”?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "已连接"
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"processesRunning": "執行中",
|
||||
"loginStats": "SSH 登入統計資訊",
|
||||
"noRecentLoginData": "沒有最近的登入資料",
|
||||
"executingQuickAction": "正在執行 {{name}}...",
|
||||
"quickActionSuccess": "{{name}} 已成功完成",
|
||||
"quickActionFailed": "{{name}} 失敗",
|
||||
"quickActionError": "無法執行 {{name}}",
|
||||
"ports": {
|
||||
"title": "監聽中的連接埠",
|
||||
"protocol": "通訊協定",
|
||||
@@ -235,12 +231,6 @@
|
||||
"addExcludedMount": "添加",
|
||||
"visibleWidgets": "可見的小工具",
|
||||
"widgetsMovedToHostMetrics": "現在卡片可直接在主機指標分頁中新增、排列和調整大小。開啟此主機的主機指標,然後使用自訂功能選擇要顯示的卡片。",
|
||||
"quickActionsLabel": "快速動作",
|
||||
"quickActionsToolbar": "快速動作會以按鈕形式顯示在主機計量工具列上,可一鍵執行指令。",
|
||||
"noQuickActions": "尚無快速動作。",
|
||||
"buttonLabel": "按鈕標籤",
|
||||
"selectSnippetPlaceholder": "選取程式碼片段...",
|
||||
"addActionBtn": "新增動作",
|
||||
"hostNotFound": "找不到主機",
|
||||
"loadingHost": "正在載入主機..."
|
||||
},
|
||||
@@ -282,15 +272,6 @@
|
||||
"dashboardTab": {
|
||||
"disk": "磁碟"
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
"snippets": {
|
||||
"cancel": "取消",
|
||||
"confirmRunButton": "執行",
|
||||
"confirmRunMessage": "執行 \"{{name}}\"?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal": {
|
||||
"connected": "已連線"
|
||||
}
|
||||
|
||||
@@ -137,6 +137,10 @@
|
||||
{
|
||||
"id": "host-metrics.managers",
|
||||
"accepts": ["component"]
|
||||
},
|
||||
{
|
||||
"id": "host-metrics.toolbar",
|
||||
"accepts": ["component"]
|
||||
}
|
||||
],
|
||||
"uiPresets": {
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
SectionCard,
|
||||
SettingRow,
|
||||
FakeSwitch,
|
||||
Select2,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
@@ -12,33 +11,24 @@ import {
|
||||
useTranslation,
|
||||
type HostEditorSectionProps,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import {
|
||||
HardDrive,
|
||||
LayoutDashboard,
|
||||
Plus,
|
||||
Server,
|
||||
Trash2,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { HardDrive, LayoutDashboard, Plus, Server, Trash2 } from "lucide-react";
|
||||
|
||||
import { readHostMetricsSettings } from "../shared/stats-widgets.js";
|
||||
|
||||
const PLUGIN_ID = "host-metrics";
|
||||
|
||||
type PluginSettingsBag = Record<string, Record<string, unknown>>;
|
||||
type QuickActionRow = { name: string; snippetId: string };
|
||||
|
||||
/**
|
||||
* The Host Metrics section of the host editor. Its values are this plugin's
|
||||
* host settings, carried on form.pluginSettings and saved by the editor after
|
||||
* the host itself. Quick actions are a core host field.
|
||||
* the host itself.
|
||||
*/
|
||||
export function HostStatsTab({
|
||||
form,
|
||||
setField,
|
||||
updateForm,
|
||||
host,
|
||||
snippets,
|
||||
}: HostEditorSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
const [newMount, setNewMount] = useState("");
|
||||
@@ -49,8 +39,6 @@ export function HostStatsTab({
|
||||
];
|
||||
const settings = readHostMetricsSettings(bag);
|
||||
const { excludedMounts, monitoredMounts } = settings;
|
||||
const quickActions = (form.quickActions ?? []) as QuickActionRow[];
|
||||
const snippetOptions = (snippets ?? []) as { id: number; name: string }[];
|
||||
const api = usePluginApi();
|
||||
|
||||
const patch = (values: Record<string, unknown>) =>
|
||||
@@ -288,84 +276,6 @@ export function HostStatsTab({
|
||||
</p>
|
||||
</div>
|
||||
</SectionCard>
|
||||
<SectionCard
|
||||
title={t("hosts.quickActionsLabel")}
|
||||
icon={<Zap className="size-3.5" />}
|
||||
action={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-[10px] px-2 border-accent-brand/40 text-accent-brand"
|
||||
onClick={() =>
|
||||
setField("quickActions", [
|
||||
...quickActions,
|
||||
{ name: "", snippetId: "" },
|
||||
])
|
||||
}
|
||||
>
|
||||
<Plus className="size-3 mr-1" /> {t("hosts.addActionBtn")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-3 py-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("hosts.quickActionsToolbar")}
|
||||
</p>
|
||||
{quickActions.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-4 text-muted-foreground/40 gap-1.5">
|
||||
<Zap className="size-6" />
|
||||
<span className="text-xs">{t("hosts.noQuickActions")}</span>
|
||||
</div>
|
||||
)}
|
||||
{quickActions.map((a, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-2 p-2 bg-muted/20 border border-border group"
|
||||
>
|
||||
<Input
|
||||
className="h-7 text-xs flex-1"
|
||||
placeholder={t("hosts.buttonLabel")}
|
||||
value={a.name}
|
||||
onChange={(e) => {
|
||||
const updated = [...quickActions];
|
||||
updated[i] = { ...updated[i], name: e.target.value };
|
||||
setField("quickActions", updated);
|
||||
}}
|
||||
/>
|
||||
<Select2
|
||||
className="h-7 text-xs flex-1 border border-border bg-background px-2 outline-none focus:ring-1 focus:ring-ring"
|
||||
value={a.snippetId}
|
||||
onChange={(e) => {
|
||||
const updated = [...quickActions];
|
||||
updated[i] = {
|
||||
...updated[i],
|
||||
snippetId: e.target.value,
|
||||
};
|
||||
setField("quickActions", updated);
|
||||
}}
|
||||
>
|
||||
<option value="">{t("hosts.selectSnippetPlaceholder")}</option>
|
||||
{snippetOptions.map((s) => (
|
||||
<option key={s.id} value={String(s.id)}>
|
||||
{s.name}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
<button
|
||||
className="text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={() =>
|
||||
setField(
|
||||
"quickActions",
|
||||
quickActions.filter((_, idx) => idx !== i),
|
||||
)
|
||||
}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
Separator,
|
||||
Button,
|
||||
useConfirmation,
|
||||
ComponentSlot,
|
||||
TOTPDialog,
|
||||
useTabsSafe,
|
||||
ConnectionLogProvider,
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
CardGridCanvas,
|
||||
ColumnCountStepper,
|
||||
type GridCardCatalogEntry,
|
||||
SnippetVariablesDialog,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
import { readHostMetricsSettings } from "../shared/stats-widgets.js";
|
||||
import {
|
||||
@@ -23,33 +21,12 @@ import type { ServerMetrics } from "../shared/metrics.js";
|
||||
import React from "react";
|
||||
import { useHostMetricsApi } from "./host-metrics-api";
|
||||
|
||||
interface Snippet {
|
||||
id: number;
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
function errorText(error: unknown): string {
|
||||
if (error instanceof Error && error.message) return error.message;
|
||||
return typeof error === "string" ? error : "";
|
||||
}
|
||||
|
||||
// Mirrors the snippets plugin's own hasSnippetInputs: a pure check for
|
||||
// whether a snippet still has $INPUT_n placeholders once host variables are
|
||||
// substituted, so the quick-action dialog knows to ask before running.
|
||||
const SNIPPET_INPUT_PATTERN =
|
||||
/\$\{INPUT_(\d+)(?::([^}$]+))?\}|\$INPUT_(\d+)(?![a-zA-Z0-9_])/g;
|
||||
function hasSnippetInputs(content: string): boolean {
|
||||
SNIPPET_INPUT_PATTERN.lastIndex = 0;
|
||||
return SNIPPET_INPUT_PATTERN.test(content);
|
||||
}
|
||||
import {
|
||||
useTranslation,
|
||||
useSlotContributions,
|
||||
useConnectionRetry,
|
||||
useHost,
|
||||
useHostStatus,
|
||||
invokeAction,
|
||||
logActivity,
|
||||
usePluginUiPreferences,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
@@ -70,11 +47,6 @@ import { metricsChangeKey } from "./metrics-change-key";
|
||||
|
||||
const HISTORY_LEN = 30;
|
||||
|
||||
interface QuickAction {
|
||||
name: string;
|
||||
snippetId: number;
|
||||
}
|
||||
|
||||
type ConnectionLogPayload = Parameters<
|
||||
ReturnType<typeof useConnectionLog>["addLog"]
|
||||
>[0];
|
||||
@@ -87,7 +59,6 @@ interface HostConfig {
|
||||
name: string;
|
||||
ip: string;
|
||||
username: string;
|
||||
quickActions?: QuickAction[];
|
||||
statusCheckEnabled?: boolean;
|
||||
pluginSettings?: Record<string, Record<string, unknown>>;
|
||||
authType?: string;
|
||||
@@ -127,9 +98,6 @@ function HostMetricsInner({
|
||||
});
|
||||
const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig);
|
||||
const [isRefreshing, setIsRefreshing] = React.useState(false);
|
||||
const [executingActions, setExecutingActions] = React.useState<Set<number>>(
|
||||
new Set(),
|
||||
);
|
||||
const [totpRequired, setTotpRequired] = React.useState(false);
|
||||
const [totpSessionId, setTotpSessionId] = React.useState<string | null>(null);
|
||||
const [totpPrompt, setTotpPrompt] = React.useState<string>("");
|
||||
@@ -139,15 +107,9 @@ function HostMetricsInner({
|
||||
null,
|
||||
);
|
||||
const [editMode, setEditMode] = React.useState(false);
|
||||
const [runningAction, setRunningAction] = React.useState<{
|
||||
action: QuickAction;
|
||||
snippet: Snippet;
|
||||
} | null>(null);
|
||||
const { confirmWithToast } = useConfirmation();
|
||||
|
||||
const activityLoggedRef = React.useRef(false);
|
||||
const activityLoggingRef = React.useRef(false);
|
||||
const snippetsCacheRef = React.useRef(new Map<number, Snippet>());
|
||||
|
||||
const statsConfig = React.useMemo(() => {
|
||||
const settings = readHostMetricsSettings(
|
||||
@@ -532,102 +494,6 @@ function HostMetricsInner({
|
||||
}
|
||||
};
|
||||
|
||||
async function executeQuickAction(
|
||||
action: QuickAction,
|
||||
inputValues: Record<string, string>,
|
||||
) {
|
||||
if (!currentHostConfig) return;
|
||||
setExecutingActions((prev) => new Set(prev).add(action.snippetId));
|
||||
toast.loading(
|
||||
t("hostMetrics.executingQuickAction", { name: action.name }),
|
||||
{
|
||||
id: `quick-action-${action.snippetId}`,
|
||||
},
|
||||
);
|
||||
try {
|
||||
const result = (await invokeAction(
|
||||
"snippets.execute",
|
||||
action.snippetId,
|
||||
currentHostConfig.id,
|
||||
Object.keys(inputValues).length > 0 ? inputValues : undefined,
|
||||
)) as { success: boolean; output?: string; error?: string } | undefined;
|
||||
if (!result) {
|
||||
toast.error(t("hostMetrics.quickActionError", { name: action.name }), {
|
||||
id: `quick-action-${action.snippetId}`,
|
||||
description: t("hostMetrics.snippetsUnavailable"),
|
||||
duration: 5000,
|
||||
});
|
||||
} else if (result.success) {
|
||||
toast.success(
|
||||
t("hostMetrics.quickActionSuccess", { name: action.name }),
|
||||
{
|
||||
id: `quick-action-${action.snippetId}`,
|
||||
description: result.output?.substring(0, 200),
|
||||
duration: 5000,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
toast.error(t("hostMetrics.quickActionFailed", { name: action.name }), {
|
||||
id: `quick-action-${action.snippetId}`,
|
||||
description: result.error || result.output,
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(t("hostMetrics.quickActionError", { name: action.name }), {
|
||||
id: `quick-action-${action.snippetId}`,
|
||||
description: errorText(error),
|
||||
duration: 5000,
|
||||
});
|
||||
} finally {
|
||||
setExecutingActions((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(action.snippetId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function runQuickActionWithConfirm(
|
||||
action: QuickAction,
|
||||
inputValues: Record<string, string>,
|
||||
) {
|
||||
const shouldConfirm =
|
||||
localStorage.getItem("confirmSnippetExecution") === "true";
|
||||
const run = () => void executeQuickAction(action, inputValues);
|
||||
if (!shouldConfirm) {
|
||||
run();
|
||||
return;
|
||||
}
|
||||
confirmWithToast(
|
||||
t("newUi.sidebar.snippets.confirmRunMessage", { name: action.name }),
|
||||
run,
|
||||
t("newUi.sidebar.snippets.confirmRunButton"),
|
||||
t("newUi.sidebar.snippets.cancel"),
|
||||
{ confirmOnEnter: true, duration: 6000 },
|
||||
);
|
||||
}
|
||||
|
||||
async function runQuickAction(action: QuickAction) {
|
||||
if (!currentHostConfig) return;
|
||||
try {
|
||||
let snippet = snippetsCacheRef.current.get(action.snippetId);
|
||||
if (!snippet) {
|
||||
snippet =
|
||||
((await invokeAction("snippets.get", action.snippetId)) as
|
||||
Snippet | null | undefined) ?? undefined;
|
||||
if (snippet) snippetsCacheRef.current.set(snippet.id, snippet);
|
||||
}
|
||||
if (snippet && hasSnippetInputs(snippet.content)) {
|
||||
setRunningAction({ action, snippet });
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// fall through and run with no inputs if the snippet lookup fails
|
||||
}
|
||||
runQuickActionWithConfirm(action, {});
|
||||
}
|
||||
|
||||
const showCards =
|
||||
metricsEnabled && metricsRetry.status === "connected" && metrics;
|
||||
const showOffline =
|
||||
@@ -653,43 +519,13 @@ function HostMetricsInner({
|
||||
<h1 className="text-lg font-bold md:text-2xl">{title}</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-0">
|
||||
{currentHostConfig?.quickActions &&
|
||||
currentHostConfig.quickActions.length > 0 && (
|
||||
<>
|
||||
<div className="mr-3 flex flex-wrap gap-2">
|
||||
{currentHostConfig.quickActions.map(
|
||||
(action, index) => {
|
||||
const isExecuting = executingActions.has(
|
||||
action.snippetId,
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 text-xs font-semibold"
|
||||
disabled={isExecuting}
|
||||
onClick={() => void runQuickAction(action)}
|
||||
>
|
||||
{isExecuting ? (
|
||||
<>
|
||||
<RefreshCw className="mr-1 size-3 animate-spin" />
|
||||
{action.name}
|
||||
</>
|
||||
) : (
|
||||
action.name
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-3 h-8"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ComponentSlot
|
||||
slotId="host-metrics.toolbar"
|
||||
props={{
|
||||
hostId: currentHostConfig?.id,
|
||||
host: currentHostConfig,
|
||||
}}
|
||||
/>
|
||||
{editMode && (
|
||||
<>
|
||||
<ColumnCountStepper
|
||||
@@ -806,32 +642,6 @@ function HostMetricsInner({
|
||||
onCancel={handleTOTPCancel}
|
||||
backgroundColor="var(--bg-canvas)"
|
||||
/>
|
||||
|
||||
{runningAction && (
|
||||
<SnippetVariablesDialog
|
||||
snippet={
|
||||
runningAction.snippet as Parameters<
|
||||
typeof SnippetVariablesDialog
|
||||
>[0]["snippet"]
|
||||
}
|
||||
host={
|
||||
currentHostConfig
|
||||
? {
|
||||
ip: currentHostConfig.ip,
|
||||
username: currentHostConfig.username,
|
||||
port: currentHostConfig.port,
|
||||
name: currentHostConfig.name,
|
||||
}
|
||||
: null
|
||||
}
|
||||
onCancel={() => setRunningAction(null)}
|
||||
onConfirm={(_resolvedContent, inputValues) => {
|
||||
const action = runningAction.action;
|
||||
setRunningAction(null);
|
||||
runQuickActionWithConfirm(action, inputValues);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -140,4 +140,10 @@ export async function activate(app: TermixApp): Promise<void> {
|
||||
id: "host-metrics.managers",
|
||||
accepts: ["component"],
|
||||
});
|
||||
// Buttons before the toolbar's own controls, drawn with { hostId, host }.
|
||||
// The snippets plugin puts a host's quick actions here.
|
||||
app.declareActionSlot({
|
||||
id: "host-metrics.toolbar",
|
||||
accepts: ["component"],
|
||||
});
|
||||
}
|
||||
@@ -7,7 +7,6 @@ afterEach(cleanup);
|
||||
|
||||
function renderSection(pluginSettings: Record<string, unknown> = {}) {
|
||||
let form: Record<string, unknown> = {
|
||||
quickActions: [],
|
||||
pluginSettings: { "host-metrics": pluginSettings, other: { keep: 1 } },
|
||||
};
|
||||
const updateForm = vi.fn(
|
||||
@@ -21,7 +20,6 @@ function renderSection(pluginSettings: Record<string, unknown> = {}) {
|
||||
setField={vi.fn()}
|
||||
updateForm={updateForm as never}
|
||||
host={{ id: "7", name: "h", ip: "10.0.0.7", port: 22 }}
|
||||
snippets={[]}
|
||||
protocols={{ ssh: true }}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -195,5 +195,37 @@
|
||||
"shareFolderHint": "Shares every snippet in this folder. Snippets added later are not shared automatically.",
|
||||
"shareFolderSummary": "Shared {{count}} snippet(s) in this folder",
|
||||
"grantedBy": "Granted by",
|
||||
"expired": "Expired"
|
||||
"expired": "Expired",
|
||||
"variablesDialogTitle": "Run \"{{name}}\"",
|
||||
"variablesDialogDescription": "Fill in the values this snippet needs before running it.",
|
||||
"variablesPreviewLabel": "Preview",
|
||||
"variablesConfirmButton": "Run",
|
||||
"keybindings": {
|
||||
"runSnippet": "Run existing snippet",
|
||||
"snippetLabel": "Snippet",
|
||||
"selectSnippet": "Select a snippet",
|
||||
"snippetMissing": "snippet not found",
|
||||
"snippetRequired": "Select a snippet"
|
||||
},
|
||||
"paletteHintRun": "Run in the active terminal tab",
|
||||
"host": {
|
||||
"startupTitle": "Startup Snippet",
|
||||
"quickActionsTitle": "Quick Actions",
|
||||
"quickActionsDescription": "Quick actions appear as buttons in the Host Metrics toolbar for one-click command execution.",
|
||||
"noQuickActions": "No quick actions yet.",
|
||||
"buttonLabel": "Button label",
|
||||
"selectSnippet": "Select snippet...",
|
||||
"addQuickAction": "Add Action",
|
||||
"tabTitle": "Snippets",
|
||||
"startupDescription": "Runs in the terminal each time it connects to this host. Variables like $HOST are filled in; a snippet that asks for inputs is skipped.",
|
||||
"none": "None",
|
||||
"removeQuickAction": "Remove quick action"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Executing {{name}}...",
|
||||
"succeeded": "{{name}} completed successfully",
|
||||
"failed": "{{name}} failed",
|
||||
"error": "Failed to execute {{name}}"
|
||||
},
|
||||
"paletteHintPaste": "Paste in the active terminal tab"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Begin \"{{name}}\"",
|
||||
"variablesDialogDescription": "Vul die waardes in wat hierdie brokkie benodig voordat dit uitgevoer word.",
|
||||
"variablesPreviewLabel": "Voorskou",
|
||||
"variablesConfirmButton": "Hardloop",
|
||||
"keybindings": {
|
||||
"runSnippet": "Voer bestaande brokkie uit",
|
||||
"snippetLabel": "Brokkie",
|
||||
"selectSnippet": "Kies 'n uittreksel",
|
||||
"snippetMissing": "brokkie nie gevind nie",
|
||||
"snippetRequired": "Kies 'n uittreksel"
|
||||
},
|
||||
"paletteHintRun": "Begin in die aktiewe terminaal-oortjie",
|
||||
"host": {
|
||||
"startupTitle": "Opstart-brokkie",
|
||||
"quickActionsTitle": "Vinnige Aksies",
|
||||
"quickActionsDescription": "Vinnige aksies verskyn as knoppies in die Host Metrics-nutsbalk vir die uitvoering van opdragte met een klik.",
|
||||
"noQuickActions": "Geen vinnige aksies nog nie.",
|
||||
"buttonLabel": "Knoppie-etiket",
|
||||
"selectSnippet": "Kies uittreksel...",
|
||||
"addQuickAction": "Voeg Aksie by"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Voer tans {{name}} uit ...",
|
||||
"succeeded": "{{name}} suksesvol voltooi",
|
||||
"failed": "{{name}} het misluk",
|
||||
"error": "Kon nie {{name}} uitvoer nie"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "تشغيل \"{{name}}\"",
|
||||
"variablesDialogDescription": "أدخل القيم التي يحتاجها هذا المقتطف البرمجي قبل تشغيله.",
|
||||
"variablesPreviewLabel": "معاينة",
|
||||
"variablesConfirmButton": "يجري",
|
||||
"keybindings": {
|
||||
"runSnippet": "تشغيل المقتطف البرمجي الحالي",
|
||||
"snippetLabel": "مقتطف",
|
||||
"selectSnippet": "حدد جزءًا من التعليمات البرمجية",
|
||||
"snippetMissing": "لم يتم العثور على المقتطف",
|
||||
"snippetRequired": "حدد جزءًا من التعليمات البرمجية"
|
||||
},
|
||||
"paletteHintRun": "قم بتشغيله في علامة تبويب الطرفية النشطة",
|
||||
"host": {
|
||||
"startupTitle": "مقتطف بدء التشغيل",
|
||||
"quickActionsTitle": "إجراءات سريعة",
|
||||
"quickActionsDescription": "تظهر الإجراءات السريعة كأزرار في شريط أدوات مقاييس المضيف لتنفيذ الأوامر بنقرة واحدة.",
|
||||
"noQuickActions": "لا توجد إجراءات سريعة بعد.",
|
||||
"buttonLabel": "تسمية الزر",
|
||||
"selectSnippet": "اختر مقتطفًا...",
|
||||
"addQuickAction": "إضافة إجراء"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "جارٍ تنفيذ {{name}}...",
|
||||
"succeeded": "تم إكمال {{name}} بنجاح",
|
||||
"failed": "فشل {{name}}",
|
||||
"error": "فشل تنفيذ {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Изпълни „{{name}}“",
|
||||
"variablesDialogDescription": "Попълнете стойностите, необходими на този фрагмент, преди да го изпълните.",
|
||||
"variablesPreviewLabel": "Преглед",
|
||||
"variablesConfirmButton": "Бягане",
|
||||
"keybindings": {
|
||||
"runSnippet": "Изпълнете съществуващия фрагмент",
|
||||
"snippetLabel": "Фрагмент",
|
||||
"selectSnippet": "Изберете фрагмент",
|
||||
"snippetMissing": "фрагмент не е намерен",
|
||||
"snippetRequired": "Изберете фрагмент"
|
||||
},
|
||||
"paletteHintRun": "Изпълняване в активния раздел на терминала",
|
||||
"host": {
|
||||
"startupTitle": "Стартов фрагмент",
|
||||
"quickActionsTitle": "Бързи действия",
|
||||
"quickActionsDescription": "Бързите действия се показват като бутони в лентата с инструменти Host Metrics за изпълнение на команди с едно щракване.",
|
||||
"noQuickActions": "Все още няма бързи действия.",
|
||||
"buttonLabel": "Етикет на бутона",
|
||||
"selectSnippet": "Изберете фрагмент...",
|
||||
"addQuickAction": "Добавяне на действие"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Изпълнява се {{name}}...",
|
||||
"succeeded": "{{name}} завършено успешно",
|
||||
"failed": "{{name}} неуспешно",
|
||||
"error": "Неуспешно изпълнение на {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "দৌড়াও \"{{name}}\"",
|
||||
"variablesDialogDescription": "এই কোড স্নিপেটটি চালানোর আগে প্রয়োজনীয় মানগুলি পূরণ করুন।",
|
||||
"variablesPreviewLabel": "প্রিভিউ",
|
||||
"variablesConfirmButton": "দৌড়",
|
||||
"keybindings": {
|
||||
"runSnippet": "বিদ্যমান কোড চালান",
|
||||
"snippetLabel": "স্নিপেট",
|
||||
"selectSnippet": "একটি স্নিপেট নির্বাচন করুন",
|
||||
"snippetMissing": "স্নিপেটটি খুঁজে পাওয়া যায়নি",
|
||||
"snippetRequired": "একটি স্নিপেট নির্বাচন করুন"
|
||||
},
|
||||
"paletteHintRun": "সক্রিয় টার্মিনাল ট্যাবে চালান",
|
||||
"host": {
|
||||
"startupTitle": "স্টার্টআপ স্নিপেট",
|
||||
"quickActionsTitle": "দ্রুত পদক্ষেপ",
|
||||
"quickActionsDescription": "এক ক্লিকে কমান্ড কার্যকর করার জন্য কুইক অ্যাকশনগুলো হোস্ট মেট্রিক্স টুলবারে বাটন হিসেবে প্রদর্শিত হয়।",
|
||||
"noQuickActions": "এখনো কোনো দ্রুত পদক্ষেপ নেওয়া হয়নি।",
|
||||
"buttonLabel": "বোতাম লেবেল",
|
||||
"selectSnippet": "স্নিপেট নির্বাচন করুন...",
|
||||
"addQuickAction": "অ্যাকশন যোগ করুন"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "{{name}} কার্যকর করা হচ্ছে ...",
|
||||
"succeeded": "{{name}} সফলভাবে সম্পন্ন হয়েছে",
|
||||
"failed": "{{name}} ব্যর্থ হয়েছে",
|
||||
"error": "{{name}} কার্যকর করতে ব্যর্থ হয়েছে"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Executa \"{{name}}\"",
|
||||
"variablesDialogDescription": "Empleneu els valors que necessita aquest fragment abans d'executar-lo.",
|
||||
"variablesPreviewLabel": "Vista prèvia",
|
||||
"variablesConfirmButton": "Corre",
|
||||
"keybindings": {
|
||||
"runSnippet": "Executa el fragment existent",
|
||||
"snippetLabel": "Fragment",
|
||||
"selectSnippet": "Selecciona un fragment",
|
||||
"snippetMissing": "fragment no trobat",
|
||||
"snippetRequired": "Selecciona un fragment"
|
||||
},
|
||||
"paletteHintRun": "Executar a la pestanya activa del terminal",
|
||||
"host": {
|
||||
"startupTitle": "Fragment d'inici",
|
||||
"quickActionsTitle": "Accions ràpides",
|
||||
"quickActionsDescription": "Les accions ràpides apareixen com a botons a la barra d'eines de mètriques de l'amfitrió per executar ordres amb un sol clic.",
|
||||
"noQuickActions": "Encara no hi ha accions ràpides.",
|
||||
"buttonLabel": "Etiqueta del botó",
|
||||
"selectSnippet": "Selecciona un fragment...",
|
||||
"addQuickAction": "Afegeix una acció"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Executant {{name}}...",
|
||||
"succeeded": "{{name}} s'ha completat correctament",
|
||||
"failed": "{{name}} ha fallat",
|
||||
"error": "No s'ha pogut executar {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Spustit „{{name}}“",
|
||||
"variablesDialogDescription": "Před spuštěním úryvku kódu vyplňte jeho potřebné hodnoty.",
|
||||
"variablesPreviewLabel": "Náhled",
|
||||
"variablesConfirmButton": "Běh",
|
||||
"keybindings": {
|
||||
"runSnippet": "Spustit existující úryvek kódu",
|
||||
"snippetLabel": "Úryvek",
|
||||
"selectSnippet": "Vyberte úryvek",
|
||||
"snippetMissing": "úryvek nenalezen",
|
||||
"snippetRequired": "Vyberte úryvek"
|
||||
},
|
||||
"paletteHintRun": "Spustit v aktivní kartě terminálu",
|
||||
"host": {
|
||||
"startupTitle": "Úryvek pro spuštění",
|
||||
"quickActionsTitle": "Rychlé akce",
|
||||
"quickActionsDescription": "Rychlé akce se zobrazují jako tlačítka v panelu nástrojů Metriky hostitele pro spuštění příkazů jedním kliknutím.",
|
||||
"noQuickActions": "Zatím žádné rychlé akce.",
|
||||
"buttonLabel": "Štítek tlačítka",
|
||||
"selectSnippet": "Vybrat úryvek...",
|
||||
"addQuickAction": "Přidat akci"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Spouštění {{name}}...",
|
||||
"succeeded": "{{name}} úspěšně dokončeno",
|
||||
"failed": "{{name}} selhalo",
|
||||
"error": "Nepodařilo se spustit {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Kør \"{{name}}\"",
|
||||
"variablesDialogDescription": "Udfyld de værdier, som dette kodestykke skal bruge, før det køres.",
|
||||
"variablesPreviewLabel": "Forhåndsvisning",
|
||||
"variablesConfirmButton": "Løbe",
|
||||
"keybindings": {
|
||||
"runSnippet": "Kør eksisterende kodestykke",
|
||||
"snippetLabel": "Uddrag",
|
||||
"selectSnippet": "Vælg et uddrag",
|
||||
"snippetMissing": "uddrag ikke fundet",
|
||||
"snippetRequired": "Vælg et uddrag"
|
||||
},
|
||||
"paletteHintRun": "Kør i den aktive terminalfane",
|
||||
"host": {
|
||||
"startupTitle": "Opstartskodestykke",
|
||||
"quickActionsTitle": "Hurtige handlinger",
|
||||
"quickActionsDescription": "Hurtige handlinger vises som knapper i værktøjslinjen Host Metrics til udførelse af kommandoer med et enkelt klik.",
|
||||
"noQuickActions": "Ingen hurtige handlinger endnu.",
|
||||
"buttonLabel": "Knapmærke",
|
||||
"selectSnippet": "Vælg uddrag...",
|
||||
"addQuickAction": "Tilføj handling"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Udfører {{name}}...",
|
||||
"succeeded": "{{name}} gennemført",
|
||||
"failed": "{{name}} mislykkedes",
|
||||
"error": "Kunne ikke udføre {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Run \"{{name}}\"",
|
||||
"variablesDialogDescription": "Füllen Sie die für diesen Codeabschnitt erforderlichen Werte aus, bevor Sie ihn ausführen.",
|
||||
"variablesPreviewLabel": "Vorschau",
|
||||
"variablesConfirmButton": "Laufen",
|
||||
"keybindings": {
|
||||
"runSnippet": "Vorhandenen Codeausschnitt ausführen",
|
||||
"snippetLabel": "Ausschnitt",
|
||||
"selectSnippet": "Wählen Sie einen Ausschnitt aus",
|
||||
"snippetMissing": "Ausschnitt nicht gefunden",
|
||||
"snippetRequired": "Wählen Sie einen Ausschnitt aus"
|
||||
},
|
||||
"paletteHintRun": "Im aktiven Terminal-Tab ausführen",
|
||||
"host": {
|
||||
"startupTitle": "Startup-Snippet",
|
||||
"quickActionsTitle": "Schnellaktionen",
|
||||
"quickActionsDescription": "Schnellaktionen erscheinen als Schaltflächen in der Host-Metriken-Symbolleiste zur Ausführung von Befehlen mit einem Klick.",
|
||||
"noQuickActions": "Noch keine Schnellaktionen.",
|
||||
"buttonLabel": "Schaltflächenbeschriftung",
|
||||
"selectSnippet": "Snippet auswählen...",
|
||||
"addQuickAction": "Aktion hinzufügen"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "{{name}} wird ausgeführt...",
|
||||
"succeeded": "{{name}} erfolgreich abgeschlossen",
|
||||
"failed": "{{name}} fehlgeschlagen",
|
||||
"error": "Fehler beim Ausführen von {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Εκτέλεση \"{{name}}\"",
|
||||
"variablesDialogDescription": "Συμπληρώστε τις τιμές που χρειάζεται αυτό το τμήμα κώδικα πριν το εκτελέσετε.",
|
||||
"variablesPreviewLabel": "Πρεμιέρα",
|
||||
"variablesConfirmButton": "Τρέξιμο",
|
||||
"keybindings": {
|
||||
"runSnippet": "Εκτέλεση υπάρχοντος αποσπάσματος",
|
||||
"snippetLabel": "Απόσπασμα",
|
||||
"selectSnippet": "Επιλέξτε ένα απόσπασμα",
|
||||
"snippetMissing": "το απόσπασμα δεν βρέθηκε",
|
||||
"snippetRequired": "Επιλέξτε ένα απόσπασμα"
|
||||
},
|
||||
"paletteHintRun": "Εκτέλεση στην ενεργή καρτέλα τερματικού",
|
||||
"host": {
|
||||
"startupTitle": "Απόσπασμα εκκίνησης",
|
||||
"quickActionsTitle": "Γρήγορες ενέργειες",
|
||||
"quickActionsDescription": "Οι γρήγορες ενέργειες εμφανίζονται ως κουμπιά στη γραμμή εργαλείων Μετρήσεων κεντρικού υπολογιστή για εκτέλεση εντολών με ένα κλικ.",
|
||||
"noQuickActions": "Δεν υπάρχουν ακόμη γρήγορες ενέργειες.",
|
||||
"buttonLabel": "Ετικέτα κουμπιού",
|
||||
"selectSnippet": "Επιλογή αποσπάσματος...",
|
||||
"addQuickAction": "Προσθήκη ενέργειας"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Εκτέλεση {{name}}...",
|
||||
"succeeded": "{{name}} ολοκληρώθηκε με επιτυχία",
|
||||
"failed": "{{name}} απέτυχε",
|
||||
"error": "Αποτυχία εκτέλεσης {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Ejecutar \"{{name}}\"",
|
||||
"variablesDialogDescription": "Rellena los valores que necesita este fragmento de código antes de ejecutarlo.",
|
||||
"variablesPreviewLabel": "Avance",
|
||||
"variablesConfirmButton": "Correr",
|
||||
"keybindings": {
|
||||
"runSnippet": "Ejecutar el fragmento existente",
|
||||
"snippetLabel": "Retazo",
|
||||
"selectSnippet": "Seleccione un fragmento",
|
||||
"snippetMissing": "Fragmento no encontrado",
|
||||
"snippetRequired": "Seleccione un fragmento"
|
||||
},
|
||||
"paletteHintRun": "Ejecutar en la pestaña de terminal activa",
|
||||
"host": {
|
||||
"startupTitle": "Fragmento de inicio",
|
||||
"quickActionsTitle": "Acciones rápidas",
|
||||
"quickActionsDescription": "Las acciones rápidas aparecen como botones en la barra de herramientas de Métricas del host para la ejecución de comandos con un solo clic.",
|
||||
"noQuickActions": "Aún no hay acciones rápidas.",
|
||||
"buttonLabel": "Etiqueta del botón",
|
||||
"selectSnippet": "Seleccionar fragmento...",
|
||||
"addQuickAction": "Añadir acción"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Ejecutando {{name}}...",
|
||||
"succeeded": "{{name}} completado correctamente",
|
||||
"failed": "{{name}} falló",
|
||||
"error": "Error al ejecutar {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Suorita \"{{name}}\"",
|
||||
"variablesDialogDescription": "Täytä tämän koodinpätkän tarvitsemat arvot ennen sen suorittamista.",
|
||||
"variablesPreviewLabel": "Esikatselu",
|
||||
"variablesConfirmButton": "Juokse",
|
||||
"keybindings": {
|
||||
"runSnippet": "Suorita olemassa oleva koodinpätkä",
|
||||
"snippetLabel": "Katkelma",
|
||||
"selectSnippet": "Valitse katkelma",
|
||||
"snippetMissing": "katkelmaa ei löytynyt",
|
||||
"snippetRequired": "Valitse katkelma"
|
||||
},
|
||||
"paletteHintRun": "Suorita aktiivisessa päätevälilehdessä",
|
||||
"host": {
|
||||
"startupTitle": "Käynnistyskoodinpätkä",
|
||||
"quickActionsTitle": "Pikatoiminnot",
|
||||
"quickActionsDescription": "Pikatoiminnot näkyvät painikkeina isäntämetriikkatyökalurivillä komentojen suorittamista varten yhdellä napsautuksella.",
|
||||
"noQuickActions": "Ei vielä nopeita toimia.",
|
||||
"buttonLabel": "Painikkeen nimi",
|
||||
"selectSnippet": "Valitse katkelma...",
|
||||
"addQuickAction": "Lisää toiminto"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Suoritetaan {{name}}...",
|
||||
"succeeded": "{{name}} suoritettu onnistuneesti",
|
||||
"failed": "{{name}} epäonnistui",
|
||||
"error": "Suoritus epäonnistui {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Exécutez \"{{name}}\"",
|
||||
"variablesDialogDescription": "Renseignez les valeurs nécessaires à cet extrait de code avant de l'exécuter.",
|
||||
"variablesPreviewLabel": "Aperçu",
|
||||
"variablesConfirmButton": "Courir",
|
||||
"keybindings": {
|
||||
"runSnippet": "Exécuter l'extrait de code existant",
|
||||
"snippetLabel": "Fragment",
|
||||
"selectSnippet": "Sélectionnez un extrait",
|
||||
"snippetMissing": "extrait introuvable",
|
||||
"snippetRequired": "Sélectionnez un extrait"
|
||||
},
|
||||
"paletteHintRun": "Exécuter dans l'onglet du terminal actif",
|
||||
"host": {
|
||||
"startupTitle": "Extrait de démarrage",
|
||||
"quickActionsTitle": "Actions rapides",
|
||||
"quickActionsDescription": "Les actions rapides apparaissent sous forme de boutons dans la barre d'outils Métriques de l'hôte pour une exécution en un clic.",
|
||||
"noQuickActions": "Aucune action rapide pour le moment.",
|
||||
"buttonLabel": "Étiquette du bouton",
|
||||
"selectSnippet": "Sélectionner un snippet...",
|
||||
"addQuickAction": "Ajouter une action"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Exécution de {{name}}...",
|
||||
"succeeded": "{{name}} terminé avec succès",
|
||||
"failed": "Échec de {{name}}",
|
||||
"error": "Échec de l'exécution de {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "הפעל \"{{name}}\"",
|
||||
"variablesDialogDescription": "מלא את הערכים הדרושים לקטע זה לפני הפעלתו.",
|
||||
"variablesPreviewLabel": "תצוגה מקדימה",
|
||||
"variablesConfirmButton": "לָרוּץ",
|
||||
"keybindings": {
|
||||
"runSnippet": "הפעלת קטע קוד קיים",
|
||||
"snippetLabel": "קטע",
|
||||
"selectSnippet": "בחר קטע",
|
||||
"snippetMissing": "קטע לא נמצא",
|
||||
"snippetRequired": "בחר קטע"
|
||||
},
|
||||
"paletteHintRun": "הפעל בלשונית הטרמינל הפעילה",
|
||||
"host": {
|
||||
"startupTitle": "קטע הפעלה",
|
||||
"quickActionsTitle": "פעולות מהירות",
|
||||
"quickActionsDescription": "פעולות מהירות מופיעות ככפתורים בסרגל הכלים של Host Metrics לביצוע פקודה בלחיצה אחת.",
|
||||
"noQuickActions": "אין עדיין פעולות מהירות.",
|
||||
"buttonLabel": "תווית כפתור",
|
||||
"selectSnippet": "בחר קטע...",
|
||||
"addQuickAction": "הוסף פעולה"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "מבצע {{name}}...",
|
||||
"succeeded": "{{name}} הושלם בהצלחה",
|
||||
"failed": "{{name}} נכשל",
|
||||
"error": "נכשל בביצוע {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "रन \"{{name}}\"",
|
||||
"variablesDialogDescription": "इस कोड स्निपेट को चलाने से पहले इसमें आवश्यक मान भरें।",
|
||||
"variablesPreviewLabel": "पूर्व दर्शन",
|
||||
"variablesConfirmButton": "दौड़ना",
|
||||
"keybindings": {
|
||||
"runSnippet": "मौजूदा स्निपेट चलाएँ",
|
||||
"snippetLabel": "टुकड़ा",
|
||||
"selectSnippet": "एक अंश का चयन करें",
|
||||
"snippetMissing": "अंश नहीं मिला",
|
||||
"snippetRequired": "एक अंश का चयन करें"
|
||||
},
|
||||
"paletteHintRun": "सक्रिय टर्मिनल टैब में चलाएँ",
|
||||
"host": {
|
||||
"startupTitle": "स्टार्टअप स्निपेट",
|
||||
"quickActionsTitle": "त्वरित कार्रवाइयाँ",
|
||||
"quickActionsDescription": "त्वरित कार्रवाइयाँ एक-क्लिक कमांड निष्पादन के लिए होस्ट मेट्रिक्स टूलबार में बटन के रूप में दिखाई देती हैं।",
|
||||
"noQuickActions": "अभी तक कोई त्वरित कार्रवाई नहीं।",
|
||||
"buttonLabel": "बटन लेबल",
|
||||
"selectSnippet": "स्निपेट चुनें...",
|
||||
"addQuickAction": "कार्रवाई जोड़ें"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "{{name}} निष्पादित हो रहा है...",
|
||||
"succeeded": "{{name}} सफलतापूर्वक पूर्ण हुआ",
|
||||
"failed": "{{name}} विफल रहा",
|
||||
"error": "{{name}} निष्पादित करने में विफल"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Futtassa a \"{{name}}\"",
|
||||
"variablesDialogDescription": "Töltse ki a kódrészlet futtatása előtt szükséges értékeket.",
|
||||
"variablesPreviewLabel": "Előnézet",
|
||||
"variablesConfirmButton": "Fut",
|
||||
"keybindings": {
|
||||
"runSnippet": "Meglévő kódrészlet futtatása",
|
||||
"snippetLabel": "Töredék",
|
||||
"selectSnippet": "Válasszon ki egy részletet",
|
||||
"snippetMissing": "részlet nem található",
|
||||
"snippetRequired": "Válasszon ki egy részletet"
|
||||
},
|
||||
"paletteHintRun": "Futtatás az aktív terminálfülön",
|
||||
"host": {
|
||||
"startupTitle": "Indítási kódrészlet",
|
||||
"quickActionsTitle": "Gyors műveletek",
|
||||
"quickActionsDescription": "A gyors műveletek gombokként jelennek meg a Host Metrics eszköztáron, így egyetlen kattintással végrehajthatók a parancsok.",
|
||||
"noQuickActions": "Még nincsenek gyors intézkedések.",
|
||||
"buttonLabel": "Gomb felirata",
|
||||
"selectSnippet": "Válasszon részletet...",
|
||||
"addQuickAction": "Művelet hozzáadása"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "{{name}} végrehajtása ...",
|
||||
"succeeded": "{{name}} sikeresen befejeződött",
|
||||
"failed": "{{name}} sikertelen",
|
||||
"error": "Nem sikerült végrehajtani a {{name}} parancsot."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Jalankan \"{{name}}\"",
|
||||
"variablesDialogDescription": "Isi nilai-nilai yang dibutuhkan cuplikan kode ini sebelum menjalankannya.",
|
||||
"variablesPreviewLabel": "Pratinjau",
|
||||
"variablesConfirmButton": "Berlari",
|
||||
"keybindings": {
|
||||
"runSnippet": "Jalankan cuplikan kode yang sudah ada.",
|
||||
"snippetLabel": "Potongan",
|
||||
"selectSnippet": "Pilih cuplikan",
|
||||
"snippetMissing": "cuplikan tidak ditemukan",
|
||||
"snippetRequired": "Pilih cuplikan"
|
||||
},
|
||||
"paletteHintRun": "Jalankan di tab terminal aktif",
|
||||
"host": {
|
||||
"startupTitle": "Cuplikan Startup",
|
||||
"quickActionsTitle": "Tindakan Cepat",
|
||||
"quickActionsDescription": "Tindakan cepat muncul sebagai tombol di bilah alat Metrik Host untuk eksekusi perintah sekali klik.",
|
||||
"noQuickActions": "Belum ada tindakan cepat.",
|
||||
"buttonLabel": "Label tombol",
|
||||
"selectSnippet": "Pilih cuplikan...",
|
||||
"addQuickAction": "Tambahkan Tindakan"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Menjalankan {{name}}...",
|
||||
"succeeded": "{{name}} berhasil diselesaikan",
|
||||
"failed": "{{name}} gagal",
|
||||
"error": "Gagal mengeksekusi {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Esegui \"{{name}}\"",
|
||||
"variablesDialogDescription": "Inserisci i valori richiesti da questo frammento di codice prima di eseguirlo.",
|
||||
"variablesPreviewLabel": "Anteprima",
|
||||
"variablesConfirmButton": "Correre",
|
||||
"keybindings": {
|
||||
"runSnippet": "Esegui il frammento di codice esistente",
|
||||
"snippetLabel": "Frammento",
|
||||
"selectSnippet": "Seleziona un frammento",
|
||||
"snippetMissing": "frammento non trovato",
|
||||
"snippetRequired": "Seleziona un frammento"
|
||||
},
|
||||
"paletteHintRun": "Esegui nella scheda del terminale attiva",
|
||||
"host": {
|
||||
"startupTitle": "Frammento di avvio",
|
||||
"quickActionsTitle": "Azioni Rapide",
|
||||
"quickActionsDescription": "Le azioni rapide appaiono come pulsanti nella barra delle metriche host per l'esecuzione di comandi con un clic.",
|
||||
"noQuickActions": "Nessuna azione rapida ancora.",
|
||||
"buttonLabel": "Etichetta pulsante",
|
||||
"selectSnippet": "Seleziona snippet...",
|
||||
"addQuickAction": "Aggiungi Azione"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Esecuzione di {{name}}...",
|
||||
"succeeded": "{{name}} completato con successo",
|
||||
"failed": "{{name}} non riuscito",
|
||||
"error": "Impossibile eseguire {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "「{{name}} を実行」",
|
||||
"variablesDialogDescription": "このコードスニペットを実行する前に、必要な値を入力してください。",
|
||||
"variablesPreviewLabel": "プレビュー",
|
||||
"variablesConfirmButton": "走る",
|
||||
"keybindings": {
|
||||
"runSnippet": "既存のスニペットを実行する",
|
||||
"snippetLabel": "抜粋",
|
||||
"selectSnippet": "抜粋を選択してください",
|
||||
"snippetMissing": "スニペットが見つかりません",
|
||||
"snippetRequired": "抜粋を選択してください"
|
||||
},
|
||||
"paletteHintRun": "アクティブなターミナルタブで実行します",
|
||||
"host": {
|
||||
"startupTitle": "起動時スニペット",
|
||||
"quickActionsTitle": "クイックアクション",
|
||||
"quickActionsDescription": "クイックアクションはホストメトリクスツールバーにボタンとして表示され、ワンクリックでコマンドを実行できます。",
|
||||
"noQuickActions": "クイックアクションはまだありません",
|
||||
"buttonLabel": "ボタンラベル",
|
||||
"selectSnippet": "スニペットを選択...",
|
||||
"addQuickAction": "アクションを追加"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "{{name}} を実行中...",
|
||||
"succeeded": "{{name}} が正常に完了しました",
|
||||
"failed": "{{name}} が失敗しました",
|
||||
"error": "{{name}} の実行に失敗しました"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "\"{{name}} \" 실행",
|
||||
"variablesDialogDescription": "이 코드 조각을 실행하기 전에 필요한 값을 입력하세요.",
|
||||
"variablesPreviewLabel": "시사",
|
||||
"variablesConfirmButton": "달리다",
|
||||
"keybindings": {
|
||||
"runSnippet": "기존 코드 조각을 실행하세요",
|
||||
"snippetLabel": "단편",
|
||||
"selectSnippet": "코드 조각을 선택하세요",
|
||||
"snippetMissing": "스니펫을 찾을 수 없습니다.",
|
||||
"snippetRequired": "코드 조각을 선택하세요"
|
||||
},
|
||||
"paletteHintRun": "현재 활성화된 터미널 탭에서 실행하세요",
|
||||
"host": {
|
||||
"startupTitle": "시작 스니펫",
|
||||
"quickActionsTitle": "빠른 작업",
|
||||
"quickActionsDescription": "빠른 작업은 한 번의 클릭으로 명령을 실행할 수 있도록 호스트 메트릭 도구 모음에 버튼으로 표시됩니다.",
|
||||
"noQuickActions": "아직 빠른 작업이 없습니다.",
|
||||
"buttonLabel": "버튼 레이블",
|
||||
"selectSnippet": "스니펫 선택...",
|
||||
"addQuickAction": "작업 추가"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "{{name}} 실행 중...",
|
||||
"succeeded": "{{name}}이(가) 성공적으로 완료되었습니다.",
|
||||
"failed": "{{name}} 실패",
|
||||
"error": "{{name}} 실행에 실패했습니다."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Uitvoeren \"{{name}}\"",
|
||||
"variablesDialogDescription": "Vul de waarden in die dit codefragment nodig heeft voordat je het uitvoert.",
|
||||
"variablesPreviewLabel": "Voorbeeld",
|
||||
"variablesConfirmButton": "Loop",
|
||||
"keybindings": {
|
||||
"runSnippet": "Voer het bestaande codefragment uit.",
|
||||
"snippetLabel": "Fragment",
|
||||
"selectSnippet": "Selecteer een fragment",
|
||||
"snippetMissing": "fragment niet gevonden",
|
||||
"snippetRequired": "Selecteer een fragment"
|
||||
},
|
||||
"paletteHintRun": "Voer dit uit in het actieve terminaltabblad.",
|
||||
"host": {
|
||||
"startupTitle": "Opstartfragment",
|
||||
"quickActionsTitle": "Snelle acties",
|
||||
"quickActionsDescription": "Snelle acties verschijnen als knoppen in de Host Metrics-werkbalk, zodat u opdrachten met één klik kunt uitvoeren.",
|
||||
"noQuickActions": "Nog geen snelle acties ondernomen.",
|
||||
"buttonLabel": "Knoplabel",
|
||||
"selectSnippet": "Selecteer fragment...",
|
||||
"addQuickAction": "Actie toevoegen"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Het uitvoeren van {{name}}...",
|
||||
"succeeded": "{{name}} succesvol voltooid",
|
||||
"failed": "{{name}} mislukt",
|
||||
"error": "Het uitvoeren van {{name}} is mislukt."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Kjør «{{name}}»",
|
||||
"variablesDialogDescription": "Fyll inn verdiene dette kodestykket trenger før du kjører det.",
|
||||
"variablesPreviewLabel": "Forhåndsvisning",
|
||||
"variablesConfirmButton": "Løp",
|
||||
"keybindings": {
|
||||
"runSnippet": "Kjør eksisterende kodebit",
|
||||
"snippetLabel": "Utdrag",
|
||||
"selectSnippet": "Velg et utdrag",
|
||||
"snippetMissing": "utdraget ikke funnet",
|
||||
"snippetRequired": "Velg et utdrag"
|
||||
},
|
||||
"paletteHintRun": "Kjør i den aktive terminalfanen",
|
||||
"host": {
|
||||
"startupTitle": "Oppstartsutdrag",
|
||||
"quickActionsTitle": "Hurtighandlinger",
|
||||
"quickActionsDescription": "Hurtighandlinger vises som knapper i verktøylinjen for vertsmålinger for utførelse av kommandoer med ett klikk.",
|
||||
"noQuickActions": "Ingen hurtighandlinger ennå.",
|
||||
"buttonLabel": "Knappetikett",
|
||||
"selectSnippet": "Velg utdrag...",
|
||||
"addQuickAction": "Legg til handling"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Utfører {{name}}...",
|
||||
"succeeded": "{{name}} fullført",
|
||||
"failed": "{{name}} mislyktes",
|
||||
"error": "Kunne ikke utføre {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Uruchom „{{name}}”",
|
||||
"variablesDialogDescription": "Przed uruchomieniem snippetu wprowadź wymagane wartości.",
|
||||
"variablesPreviewLabel": "Podgląd",
|
||||
"variablesConfirmButton": "Uruchom",
|
||||
"keybindings": {
|
||||
"runSnippet": "Uruchom istniejący snippet",
|
||||
"snippetLabel": "Snippet",
|
||||
"selectSnippet": "Wybierz snippet",
|
||||
"snippetMissing": "nie znaleziono snippetu",
|
||||
"snippetRequired": "Wybierz snippet"
|
||||
},
|
||||
"paletteHintRun": "Uruchom w aktywnej karcie terminala",
|
||||
"host": {
|
||||
"startupTitle": "Snippet startowy",
|
||||
"quickActionsTitle": "Szybkie akcje",
|
||||
"quickActionsDescription": "Szybkie akcje pojawiają się jako przyciski na pasku narzędzi Metryki hosta, umożliwiając wykonanie polecenia jednym kliknięciem.",
|
||||
"noQuickActions": "Brak szybkich akcji.",
|
||||
"buttonLabel": "Etykieta przycisku",
|
||||
"selectSnippet": "Wybierz snippet...",
|
||||
"addQuickAction": "Dodaj akcję"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Wykonywanie {{name}}...",
|
||||
"succeeded": "{{name}} ukończono pomyślnie",
|
||||
"failed": "{{name}} nie powiodło się",
|
||||
"error": "Nie udało się wykonać {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Executar \"{{name}}\"",
|
||||
"variablesDialogDescription": "Preencha os valores necessários para este trecho de código antes de executá-lo.",
|
||||
"variablesPreviewLabel": "Pré-visualização",
|
||||
"variablesConfirmButton": "Correr",
|
||||
"keybindings": {
|
||||
"runSnippet": "Executar trecho de código existente",
|
||||
"snippetLabel": "Trecho",
|
||||
"selectSnippet": "Selecione um trecho",
|
||||
"snippetMissing": "trecho não encontrado",
|
||||
"snippetRequired": "Selecione um trecho"
|
||||
},
|
||||
"paletteHintRun": "Execute na aba do terminal ativo",
|
||||
"host": {
|
||||
"startupTitle": "Trecho de startup",
|
||||
"quickActionsTitle": "Ações rápidas",
|
||||
"quickActionsDescription": "As ações rápidas aparecem como botões na barra de ferramentas do Host Metrics para execução de comandos com um único clique.",
|
||||
"noQuickActions": "Nenhuma ação imediata ainda.",
|
||||
"buttonLabel": "Rótulo do botão",
|
||||
"selectSnippet": "Selecione o trecho...",
|
||||
"addQuickAction": "Adicionar ação"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Executando {{name}}...",
|
||||
"succeeded": "{{name}} concluído com sucesso",
|
||||
"failed": "{{name}} falhou",
|
||||
"error": "Falha ao executar {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Executar \"{{name}}\"",
|
||||
"variablesDialogDescription": "Preencha os valores necessários para este excerto de código antes de o executar.",
|
||||
"variablesPreviewLabel": "Pré-visualização",
|
||||
"variablesConfirmButton": "Correr",
|
||||
"keybindings": {
|
||||
"runSnippet": "Executar excerto de código existente",
|
||||
"snippetLabel": "Trecho",
|
||||
"selectSnippet": "Selecione um excerto",
|
||||
"snippetMissing": "trecho não encontrado",
|
||||
"snippetRequired": "Selecione um excerto"
|
||||
},
|
||||
"paletteHintRun": "Execute no separador do terminal ativo",
|
||||
"host": {
|
||||
"startupTitle": "Fragmento de inicialização",
|
||||
"quickActionsTitle": "Ações Rápidas",
|
||||
"quickActionsDescription": "As ações rápidas aparecem como botões na barra de ferramentas de Métricas do Host para execução de comandos com um clique.",
|
||||
"noQuickActions": "Ainda não existem ações rápidas.",
|
||||
"buttonLabel": "Texto do botão",
|
||||
"selectSnippet": "Selecionar snippet...",
|
||||
"addQuickAction": "Adicionar Ação"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "A executar {{name}}...",
|
||||
"succeeded": "{{name}} concluído com sucesso",
|
||||
"failed": "{{name}} falhou",
|
||||
"error": "Falha ao executar {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Executați „{{name}}”",
|
||||
"variablesDialogDescription": "Completați valorile necesare acestui fragment de cod înainte de a-l rula.",
|
||||
"variablesPreviewLabel": "Previzualizare",
|
||||
"variablesConfirmButton": "Aleargă",
|
||||
"keybindings": {
|
||||
"runSnippet": "Execută fragmentul existent",
|
||||
"snippetLabel": "Fragment",
|
||||
"selectSnippet": "Selectați un fragment",
|
||||
"snippetMissing": "fragmentul nu a fost găsit",
|
||||
"snippetRequired": "Selectați un fragment"
|
||||
},
|
||||
"paletteHintRun": "Execută în fila activă a terminalului",
|
||||
"host": {
|
||||
"startupTitle": "Fragment de pornire",
|
||||
"quickActionsTitle": "Acțiuni rapide",
|
||||
"quickActionsDescription": "Acțiunile rapide apar sub formă de butoane în bara de instrumente Host Metrics pentru executarea comenzilor cu un singur clic.",
|
||||
"noQuickActions": "Nicio acțiune rapidă încă.",
|
||||
"buttonLabel": "Etichetă buton",
|
||||
"selectSnippet": "Selectați fragmentul...",
|
||||
"addQuickAction": "Adăugați o acțiune"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Se execută {{name}}...",
|
||||
"succeeded": "{{name}} finalizat cu succes",
|
||||
"failed": "{{name}} a eșuat",
|
||||
"error": "Executarea {{name}} a eșuat"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Запуск \"{{name}}\"",
|
||||
"variablesDialogDescription": "Перед запуском заполните необходимые для этого фрагмента значения.",
|
||||
"variablesPreviewLabel": "Предварительный просмотр",
|
||||
"variablesConfirmButton": "Бегать",
|
||||
"keybindings": {
|
||||
"runSnippet": "Запустить существующий фрагмент",
|
||||
"snippetLabel": "Фрагмент",
|
||||
"selectSnippet": "Выберите фрагмент",
|
||||
"snippetMissing": "фрагмент не найден",
|
||||
"snippetRequired": "Выберите фрагмент"
|
||||
},
|
||||
"paletteHintRun": "Запустите в активной вкладке терминала.",
|
||||
"host": {
|
||||
"startupTitle": "Фрагмент запуска",
|
||||
"quickActionsTitle": "Быстрые действия",
|
||||
"quickActionsDescription": "Быстрые действия отображаются в виде кнопок на панели инструментов метрик хоста для выполнения команд в один клик.",
|
||||
"noQuickActions": "Пока нет быстрых действий.",
|
||||
"buttonLabel": "Надпись кнопки",
|
||||
"selectSnippet": "Выбрать сниппет...",
|
||||
"addQuickAction": "Добавить действие"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Выполняется {{name}}...",
|
||||
"succeeded": "{{name}} успешно завершено",
|
||||
"failed": "{{name}} не удалось",
|
||||
"error": "Не удалось выполнить {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Покрени „{{name}}“",
|
||||
"variablesDialogDescription": "Унесите вредности које су потребне овом фрагменту пре него што га покренете.",
|
||||
"variablesPreviewLabel": "Преглед",
|
||||
"variablesConfirmButton": "Трчи",
|
||||
"keybindings": {
|
||||
"runSnippet": "Покрени постојећи исечак кода",
|
||||
"snippetLabel": "Исечак",
|
||||
"selectSnippet": "Изаберите исечак",
|
||||
"snippetMissing": "фрагмент није пронађен",
|
||||
"snippetRequired": "Изаберите исечак"
|
||||
},
|
||||
"paletteHintRun": "Покрени у активној картици терминала",
|
||||
"host": {
|
||||
"startupTitle": "Исечак за покретање",
|
||||
"quickActionsTitle": "Брзе акције",
|
||||
"quickActionsDescription": "Брзе акције се појављују као дугмад у траци са алаткама Host Metrics за извршавање команди једним кликом.",
|
||||
"noQuickActions": "Још нема брзих акција.",
|
||||
"buttonLabel": "Ознака дугмета",
|
||||
"selectSnippet": "Изаберите исечак...",
|
||||
"addQuickAction": "Додај акцију"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Извршава се {{name}}...",
|
||||
"succeeded": "{{name}} успешно завршено",
|
||||
"failed": "{{name}} није успело",
|
||||
"error": "Није успело извршавање {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Kör \"{{name}}\"",
|
||||
"variablesDialogDescription": "Fyll i de värden som detta kodavsnitt behöver innan du kör det.",
|
||||
"variablesPreviewLabel": "Förhandsvisning",
|
||||
"variablesConfirmButton": "Sikt",
|
||||
"keybindings": {
|
||||
"runSnippet": "Kör befintligt kodavsnitt",
|
||||
"snippetLabel": "Utdrag",
|
||||
"selectSnippet": "Välj ett utdrag",
|
||||
"snippetMissing": "utdraget hittades inte",
|
||||
"snippetRequired": "Välj ett utdrag"
|
||||
},
|
||||
"paletteHintRun": "Kör i den aktiva terminalfliken",
|
||||
"host": {
|
||||
"startupTitle": "Startkodavsnitt",
|
||||
"quickActionsTitle": "Snabbåtgärder",
|
||||
"quickActionsDescription": "Snabbåtgärder visas som knappar i verktygsfältet Host Metrics för att köra kommandon med ett klick.",
|
||||
"noQuickActions": "Inga snabba åtgärder ännu.",
|
||||
"buttonLabel": "Knappetikett",
|
||||
"selectSnippet": "Välj utdrag...",
|
||||
"addQuickAction": "Lägg till åtgärd"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Kör {{name}}...",
|
||||
"succeeded": "{{name}} slutfört",
|
||||
"failed": "{{name}} misslyckades",
|
||||
"error": "Misslyckades med att köra {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "วิ่ง \"{{name}}\"",
|
||||
"variablesDialogDescription": "กรอกค่าที่จำเป็นสำหรับโค้ดส่วนนี้ก่อนที่จะเรียกใช้งาน",
|
||||
"variablesPreviewLabel": "ตัวอย่าง",
|
||||
"variablesConfirmButton": "วิ่ง",
|
||||
"keybindings": {
|
||||
"runSnippet": "เรียกใช้โค้ดตัวอย่างที่มีอยู่",
|
||||
"snippetLabel": "ส่วนหนึ่ง",
|
||||
"selectSnippet": "เลือกส่วนย่อย",
|
||||
"snippetMissing": "ไม่พบส่วนย่อย",
|
||||
"snippetRequired": "เลือกส่วนย่อย"
|
||||
},
|
||||
"paletteHintRun": "เรียกใช้ในแท็บเทอร์มินัลที่ใช้งานอยู่",
|
||||
"host": {
|
||||
"startupTitle": "ตัวอย่างโค้ดสำหรับเริ่มต้น",
|
||||
"quickActionsTitle": "การดำเนินการด่วน",
|
||||
"quickActionsDescription": "การดำเนินการด่วนจะปรากฏเป็นปุ่มในแถบเครื่องมือ Host Metrics เพื่อให้สามารถเรียกใช้คำสั่งได้ด้วยการคลิกเพียงครั้งเดียว",
|
||||
"noQuickActions": "ยังไม่มีการดำเนินการใดๆ อย่างเร่งด่วนในขณะนี้",
|
||||
"buttonLabel": "ป้ายกำกับปุ่ม",
|
||||
"selectSnippet": "เลือกส่วนย่อย...",
|
||||
"addQuickAction": "เพิ่มการดำเนินการ"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "กำลังดำเนินการ {{name}}...",
|
||||
"succeeded": "{{name}} เสร็จสมบูรณ์เรียบร้อยแล้ว",
|
||||
"failed": "{{name}} ล้มเหลว",
|
||||
"error": "ไม่สามารถดำเนินการ {{name}} ได้"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Çalıştır \"{{name}}\"",
|
||||
"variablesDialogDescription": "Bu kod parçasını çalıştırmadan önce gerekli değerleri girin.",
|
||||
"variablesPreviewLabel": "Önizleme",
|
||||
"variablesConfirmButton": "Koşmak",
|
||||
"keybindings": {
|
||||
"runSnippet": "Mevcut kod parçasını çalıştır",
|
||||
"snippetLabel": "Alıntı",
|
||||
"selectSnippet": "Bir parça seçin",
|
||||
"snippetMissing": "Kod parçası bulunamadı",
|
||||
"snippetRequired": "Bir parça seçin"
|
||||
},
|
||||
"paletteHintRun": "Etkin terminal sekmesinde çalıştırın.",
|
||||
"host": {
|
||||
"startupTitle": "Başlangıç Betiği",
|
||||
"quickActionsTitle": "Hızlı Eylemler",
|
||||
"quickActionsDescription": "Hızlı eylemler, tek tıklamayla komut çalıştırmak için Ana Bilgisayar Metrikleri araç çubuğunda düğme olarak görünür.",
|
||||
"noQuickActions": "Henüz hızlı eylem yok.",
|
||||
"buttonLabel": "Düğme etiketi",
|
||||
"selectSnippet": "Parçacık seç...",
|
||||
"addQuickAction": "Eylem Ekle"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "{{name}} çalıştırılıyor...",
|
||||
"succeeded": "{{name}} başarıyla tamamlandı",
|
||||
"failed": "{{name}} başarısız oldu",
|
||||
"error": "{{name}} çalıştırılamadı"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Виконати \"{{name}}\"",
|
||||
"variablesDialogDescription": "Заповніть значення, необхідні цьому фрагменту, перш ніж запускати його.",
|
||||
"variablesPreviewLabel": "Попередній перегляд",
|
||||
"variablesConfirmButton": "Бігти",
|
||||
"keybindings": {
|
||||
"runSnippet": "Запустити існуючий фрагмент коду",
|
||||
"snippetLabel": "Фрагмент",
|
||||
"selectSnippet": "Виберіть фрагмент",
|
||||
"snippetMissing": "фрагмент не знайдено",
|
||||
"snippetRequired": "Виберіть фрагмент"
|
||||
},
|
||||
"paletteHintRun": "Запустити в активній вкладці терміналу",
|
||||
"host": {
|
||||
"startupTitle": "Фрагмент запуску",
|
||||
"quickActionsTitle": "Швидкі дії",
|
||||
"quickActionsDescription": "Швидкі дії відображаються як кнопки на панелі інструментів «Метрики хоста» для виконання команд одним клацанням миші.",
|
||||
"noQuickActions": "Поки що жодних швидких дій.",
|
||||
"buttonLabel": "Мітка кнопки",
|
||||
"selectSnippet": "Вибрати фрагмент...",
|
||||
"addQuickAction": "Додати дію"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Виконання {{name}}...",
|
||||
"succeeded": "{{name}} успішно завершено",
|
||||
"failed": "{{name}} не вдалося",
|
||||
"error": "Не вдалося виконати {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "Chạy \"{{name}}\"",
|
||||
"variablesDialogDescription": "Hãy điền đầy đủ các giá trị cần thiết cho đoạn mã này trước khi chạy.",
|
||||
"variablesPreviewLabel": "Xem trước",
|
||||
"variablesConfirmButton": "Chạy",
|
||||
"keybindings": {
|
||||
"runSnippet": "Chạy đoạn mã hiện có",
|
||||
"snippetLabel": "Đoạn trích",
|
||||
"selectSnippet": "Chọn một đoạn trích",
|
||||
"snippetMissing": "Không tìm thấy đoạn mã.",
|
||||
"snippetRequired": "Chọn một đoạn trích"
|
||||
},
|
||||
"paletteHintRun": "Chạy trong tab cửa sổ terminal đang hoạt động",
|
||||
"host": {
|
||||
"startupTitle": "Đoạn mã khởi động",
|
||||
"quickActionsTitle": "Hành động nhanh",
|
||||
"quickActionsDescription": "Hành động nhanh xuất hiện dưới dạng nút trên thanh công cụ Chỉ số máy để thực thi lệnh một lần nhấp.",
|
||||
"noQuickActions": "Chưa có hành động nhanh nào.",
|
||||
"buttonLabel": "Nhãn nút",
|
||||
"selectSnippet": "Chọn đoạn mã...",
|
||||
"addQuickAction": "Thêm hành động"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "Đang thực thi {{name}}...",
|
||||
"succeeded": "{{name}} đã hoàn tất thành công",
|
||||
"failed": "{{name}} thất bại",
|
||||
"error": "Thực thi {{name}} thất bại"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "运行“{{name}}”",
|
||||
"variablesDialogDescription": "运行此代码片段之前,请填写所需的值。",
|
||||
"variablesPreviewLabel": "预览",
|
||||
"variablesConfirmButton": "运行",
|
||||
"keybindings": {
|
||||
"runSnippet": "运行现有代码片段",
|
||||
"snippetLabel": "片段",
|
||||
"selectSnippet": "选择代码片段",
|
||||
"snippetMissing": "未找到代码片段",
|
||||
"snippetRequired": "选择代码片段"
|
||||
},
|
||||
"paletteHintRun": "在活动终端标签页中运行",
|
||||
"host": {
|
||||
"startupTitle": "启动代码片段",
|
||||
"quickActionsTitle": "快速操作",
|
||||
"quickActionsDescription": "快速操作以按钮形式显示在主机指标工具栏中,以便一键执行命令。",
|
||||
"noQuickActions": "尚无快速操作。",
|
||||
"buttonLabel": "按钮标签",
|
||||
"selectSnippet": "选择代码片段...",
|
||||
"addQuickAction": "添加操作"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "正在执行 {{name}}...",
|
||||
"succeeded": "{{name}} 执行成功",
|
||||
"failed": "{{name}} 执行失败",
|
||||
"error": "执行 {{name}} 失败"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"variablesDialogTitle": "運行“{{name}}",
|
||||
"variablesDialogDescription": "在運行此程式碼片段之前,請填寫所需的值。",
|
||||
"variablesPreviewLabel": "預覽",
|
||||
"variablesConfirmButton": "跑步",
|
||||
"keybindings": {
|
||||
"runSnippet": "運行現有程式碼片段",
|
||||
"snippetLabel": "片段",
|
||||
"selectSnippet": "選擇一段文字",
|
||||
"snippetMissing": "未找到程式碼片段",
|
||||
"snippetRequired": "選擇一段文字"
|
||||
},
|
||||
"paletteHintRun": "在活動終端選項卡中運行",
|
||||
"host": {
|
||||
"startupTitle": "啟動程式碼片段",
|
||||
"quickActionsTitle": "快速動作",
|
||||
"quickActionsDescription": "快速動作會以按鈕形式顯示在主機計量工具列上,可一鍵執行指令。",
|
||||
"noQuickActions": "尚無快速動作。",
|
||||
"buttonLabel": "按鈕標籤",
|
||||
"selectSnippet": "選取程式碼片段...",
|
||||
"addQuickAction": "新增動作"
|
||||
},
|
||||
"quickActions": {
|
||||
"running": "正在執行 {{name}}...",
|
||||
"succeeded": "{{name}} 已成功完成",
|
||||
"failed": "{{name}} 失敗",
|
||||
"error": "無法執行 {{name}}"
|
||||
}
|
||||
}
|
||||
@@ -95,8 +95,51 @@
|
||||
"descriptionKey": "settings.confirmExecution.description",
|
||||
"default": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"host": {
|
||||
"editorGroup": "ssh",
|
||||
"editorOrder": 15,
|
||||
"fields": [
|
||||
{
|
||||
"key": "startupSnippetId",
|
||||
"type": "number",
|
||||
"labelKey": "host.startupTitle",
|
||||
"descriptionKey": "host.startupDescription",
|
||||
"hidden": true,
|
||||
"min": 1,
|
||||
"shareRead": "view"
|
||||
},
|
||||
{
|
||||
"key": "quickActions",
|
||||
"type": "json",
|
||||
"labelKey": "host.quickActionsTitle",
|
||||
"descriptionKey": "host.quickActionsDescription",
|
||||
"hidden": true,
|
||||
"default": [],
|
||||
"shareRead": "view"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"syncEntities": ["snippetFolders", "snippets"]
|
||||
"syncEntities": ["snippetFolders", "snippets"],
|
||||
"keybindingActions": [
|
||||
{
|
||||
"id": "runSnippet",
|
||||
"params": {
|
||||
"snippetId": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+$",
|
||||
"maxLength": 20,
|
||||
"syncEntity": "snippets"
|
||||
},
|
||||
"appendEnter": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"http": {
|
||||
"adminImpersonation": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,58 +4,8 @@ export interface SnippetExecutionResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface SnippetHostVars {
|
||||
ip?: string;
|
||||
username?: string;
|
||||
port?: number | string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const INPUT_PATTERN =
|
||||
/\$\{INPUT_(\d+)(?::([^}$]+))?\}|\$INPUT_(\d+)(?![a-zA-Z0-9_])/g;
|
||||
|
||||
function replaceVar(content: string, name: string, value?: string): string {
|
||||
if (value === undefined) return content;
|
||||
const pattern = new RegExp(`\\$\\{?${name}\\}?`, "g");
|
||||
return content.replace(pattern, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors src/ui/lib/snippet-variables.ts resolveSnippetContent. Frontend and
|
||||
* backend are separate builds, so this is kept as a small standalone copy
|
||||
* rather than a shared package for one pure function.
|
||||
*/
|
||||
export function resolveSnippetCommand(
|
||||
content: string,
|
||||
host: SnippetHostVars | null,
|
||||
inputValues: Record<string, string> = {},
|
||||
): string {
|
||||
let resolved = content;
|
||||
|
||||
resolved = replaceVar(resolved, "HOST", host?.ip);
|
||||
resolved = replaceVar(resolved, "USER", host?.username);
|
||||
resolved = replaceVar(
|
||||
resolved,
|
||||
"PORT",
|
||||
host?.port !== undefined ? String(host.port) : undefined,
|
||||
);
|
||||
resolved = replaceVar(resolved, "NAME", host?.name);
|
||||
|
||||
resolved = resolved.replace(
|
||||
INPUT_PATTERN,
|
||||
(
|
||||
fullMatch,
|
||||
braceDigits: string | undefined,
|
||||
_label,
|
||||
plainDigits: string | undefined,
|
||||
) => {
|
||||
const key = `INPUT_${braceDigits ?? plainDigits}`;
|
||||
return key in inputValues ? inputValues[key] : fullMatch;
|
||||
},
|
||||
);
|
||||
|
||||
return resolved;
|
||||
}
|
||||
export { resolveSnippetContent as resolveSnippetCommand } from "../shared/variables.js";
|
||||
export type { SnippetHostContext as SnippetHostVars } from "../shared/variables.js";
|
||||
|
||||
export function getSnippetExecutionTimeoutMs(
|
||||
value = process.env.SNIPPET_EXECUTION_TIMEOUT_SECONDS,
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
PLUGIN_ID,
|
||||
readQuickActions,
|
||||
readStartupSnippetId,
|
||||
} from "../shared/host-settings.js";
|
||||
|
||||
/**
|
||||
* Registered as "snippets.hostImportNormalizer": a 2.8 or early 2.9 export
|
||||
* carried quick actions as a host field and the startup snippet inside
|
||||
* terminalConfig. An export that already carries this plugin's
|
||||
* pluginSettings has had them written by core.
|
||||
*/
|
||||
export function hostImportNormalizer(
|
||||
raw: Record<string, unknown>,
|
||||
): Record<string, unknown> | null {
|
||||
const carried = (raw.pluginSettings as Record<string, unknown> | undefined)?.[
|
||||
PLUGIN_ID
|
||||
];
|
||||
if (
|
||||
carried &&
|
||||
typeof carried === "object" &&
|
||||
Object.keys(carried).length > 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const values: Record<string, unknown> = {};
|
||||
const quickActions = readQuickActions(raw.quickActions);
|
||||
if (quickActions.length > 0) values.quickActions = quickActions;
|
||||
|
||||
let terminalConfig = raw.terminalConfig;
|
||||
if (typeof terminalConfig === "string") {
|
||||
try {
|
||||
terminalConfig = JSON.parse(terminalConfig);
|
||||
} catch {
|
||||
terminalConfig = null;
|
||||
}
|
||||
}
|
||||
const startup = readStartupSnippetId(
|
||||
(terminalConfig as { startupSnippetId?: unknown } | null)?.startupSnippetId,
|
||||
);
|
||||
if (startup !== null) values.startupSnippetId = startup;
|
||||
|
||||
return Object.keys(values).length > 0 ? values : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered as "snippets.hostPayloadLegacy": quickActions and
|
||||
* terminalConfig.startupSnippetId in their 2.8 shape, for clients that
|
||||
* still read them (Termix-Mobile). Remove once the mobile app reads
|
||||
* pluginSettings.snippets.
|
||||
*/
|
||||
export function hostPayloadLegacy(
|
||||
values: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = {
|
||||
quickActions: readQuickActions(values.quickActions),
|
||||
};
|
||||
const startup = readStartupSnippetId(values.startupSnippetId);
|
||||
if (startup !== null) out.terminalConfig = { startupSnippetId: startup };
|
||||
return out;
|
||||
}
|
||||
|
||||
/** What hostSettingsSync needs from the repository. */
|
||||
export interface SnippetSyncLookup {
|
||||
findSyncIdById: (id: number) => Promise<string | null>;
|
||||
findIdBySyncId: (syncId: string) => Promise<number | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered as "snippets.hostSettingsSync": both host settings hold local
|
||||
* snippet ids, so remote sync carries the snippets' sync ids instead, which
|
||||
* are the same on both sides.
|
||||
*/
|
||||
export function createHostSettingsSync(lookup: SnippetSyncLookup) {
|
||||
const toSyncId = async (id: number) => lookup.findSyncIdById(id);
|
||||
const toId = async (value: unknown) =>
|
||||
typeof value === "string" && value ? lookup.findIdBySyncId(value) : null;
|
||||
|
||||
return {
|
||||
exportValue: async (key: string, value: unknown) => {
|
||||
if (key === "startupSnippetId") {
|
||||
const id = readStartupSnippetId(value);
|
||||
return id === null ? null : await toSyncId(id);
|
||||
}
|
||||
if (key === "quickActions") {
|
||||
const actions = readQuickActions(value);
|
||||
const out = [];
|
||||
for (const action of actions) {
|
||||
const syncId = await toSyncId(action.snippetId);
|
||||
if (syncId) out.push({ name: action.name, snippetId: syncId });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
importValue: async (key: string, value: unknown) => {
|
||||
if (key === "startupSnippetId") return await toId(value);
|
||||
if (key === "quickActions") {
|
||||
if (!Array.isArray(value)) return [];
|
||||
const out = [];
|
||||
for (const entry of value) {
|
||||
const row = (entry ?? {}) as { name?: unknown; snippetId?: unknown };
|
||||
const id = await toId(row.snippetId);
|
||||
if (id !== null) {
|
||||
out.push({
|
||||
name: typeof row.name === "string" ? row.name : "",
|
||||
snippetId: id,
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,15 @@ import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||
import { snippetAccess, snippetFolders, snippets } from "./tables.js";
|
||||
import { createSnippetRepository } from "./repository.js";
|
||||
import { registerSnippetRoutes } from "./routes.js";
|
||||
import { resolveSnippetCommand } from "./execution.js";
|
||||
import {
|
||||
extractSnippetInputs,
|
||||
resolveSnippetContent,
|
||||
} from "../shared/variables.js";
|
||||
import {
|
||||
createHostSettingsSync,
|
||||
hostImportNormalizer,
|
||||
hostPayloadLegacy,
|
||||
} from "./host-import.js";
|
||||
|
||||
/** What other plugins get from ctx.services.get("snippets.access", {userId}). */
|
||||
export interface SnippetsService {
|
||||
@@ -74,8 +82,24 @@ export async function activate(ctx: PluginContext) {
|
||||
type: "snippets",
|
||||
table: snippetsTable,
|
||||
order: 60,
|
||||
// Host quick actions and keybindings in core point at snippets by this.
|
||||
answersTo: ["commandSnippet"],
|
||||
});
|
||||
|
||||
// Host settings that name a snippet by its local id, and the 2.8 host
|
||||
// fields they came from. Keybindings name one through the runSnippet
|
||||
// declaration in the manifest, which core translates over sync.
|
||||
ctx.registry.provide("snippets.hostImportNormalizer", hostImportNormalizer);
|
||||
ctx.registry.provide("snippets.hostPayloadLegacy", hostPayloadLegacy);
|
||||
ctx.registry.provide(
|
||||
"snippets.hostSettingsSync",
|
||||
createHostSettingsSync(repo),
|
||||
);
|
||||
|
||||
// $HOST and $INPUT_n handling for commands other plugins run (fleets). A
|
||||
// pure function, so it goes through the registry rather than a service
|
||||
// gated on snippets.view.
|
||||
ctx.registry.provide("snippets.variables", {
|
||||
resolve: resolveSnippetContent,
|
||||
extractInputs: extractSnippetInputs,
|
||||
});
|
||||
|
||||
// The user row survives a password-reset data wipe, so it never triggers
|
||||
@@ -133,7 +157,7 @@ export async function activate(ctx: PluginContext) {
|
||||
if (!userId) return null;
|
||||
const snippet = await findAccessible(userId, id);
|
||||
if (!snippet) return null;
|
||||
return resolveSnippetCommand(snippet.content, vars, inputValues);
|
||||
return resolveSnippetContent(snippet.content, vars, inputValues);
|
||||
},
|
||||
create: async (input) => {
|
||||
const userId = ctx.currentActor();
|
||||
|
||||
@@ -882,6 +882,27 @@ export function createSnippetRepository(
|
||||
return rows[0] ?? null;
|
||||
},
|
||||
|
||||
/** A snippet's sync id, for a host setting that names it over sync. */
|
||||
async findSyncIdById(snippetId: number): Promise<string | null> {
|
||||
const drizzle = await client();
|
||||
const rows = await drizzle
|
||||
.select({ syncId: snippets.syncId })
|
||||
.from(snippets)
|
||||
.where(eq(snippets.id, snippetId))
|
||||
.limit(1);
|
||||
return (rows[0]?.syncId as string | null | undefined) ?? null;
|
||||
},
|
||||
|
||||
async findIdBySyncId(syncId: string): Promise<number | null> {
|
||||
const drizzle = await client();
|
||||
const rows = await drizzle
|
||||
.select({ id: snippets.id })
|
||||
.from(snippets)
|
||||
.where(eq(snippets.syncId, syncId))
|
||||
.limit(1);
|
||||
return (rows[0]?.id as number | undefined) ?? null;
|
||||
},
|
||||
|
||||
/** A user's role ids, for the sharing filters above. */
|
||||
async listUserRoleIds(userId: string): Promise<number[]> {
|
||||
const drizzle = await client();
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import { Plus, Play, Trash2, Zap } from "lucide-react";
|
||||
import {
|
||||
useTranslation,
|
||||
type HostEditorSectionProps,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { Button, Input, SectionCard, Select2 } from "@termix/plugin-sdk/ui";
|
||||
import { PLUGIN_ID, readStartupSnippetId } from "../shared/host-settings.js";
|
||||
import { useSnippetOptions } from "./use-snippet-options";
|
||||
|
||||
type PluginSettingsBag = Record<string, Record<string, unknown>>;
|
||||
|
||||
/** A quick action row while it is being edited: the snippet may be unset. */
|
||||
interface QuickActionRow {
|
||||
name: string;
|
||||
snippetId: number | null;
|
||||
}
|
||||
|
||||
function readRows(value: unknown): QuickActionRow[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.map((entry) => {
|
||||
const row = (entry ?? {}) as { name?: unknown; snippetId?: unknown };
|
||||
const id = Number(row.snippetId);
|
||||
return {
|
||||
name: typeof row.name === "string" ? row.name : "",
|
||||
snippetId: Number.isInteger(id) && id > 0 ? id : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The host editor's Snippets tab: the snippet a terminal runs when it
|
||||
* connects, and the quick action buttons Host Metrics shows for the host.
|
||||
* Both are this plugin's host settings, saved with the host.
|
||||
*/
|
||||
export function HostSnippetsSection({
|
||||
form,
|
||||
updateForm,
|
||||
adminTargetUserId,
|
||||
}: HostEditorSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
const { options } = useSnippetOptions(adminTargetUserId);
|
||||
const own =
|
||||
(form.pluginSettings as PluginSettingsBag | undefined)?.[PLUGIN_ID] ?? {};
|
||||
const startupSnippetId = readStartupSnippetId(own.startupSnippetId);
|
||||
const quickActions = readRows(own.quickActions);
|
||||
|
||||
const write = (values: Record<string, unknown>) =>
|
||||
updateForm((current) => {
|
||||
const all =
|
||||
(current.pluginSettings as PluginSettingsBag | undefined) ?? {};
|
||||
return {
|
||||
...current,
|
||||
pluginSettings: {
|
||||
...all,
|
||||
[PLUGIN_ID]: { ...(all[PLUGIN_ID] ?? {}), ...values },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const setQuickActions = (rows: QuickActionRow[]) =>
|
||||
write({ quickActions: rows });
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionCard
|
||||
title={t("host.startupTitle")}
|
||||
icon={<Play className="size-3.5" />}
|
||||
>
|
||||
<div className="flex flex-col gap-1.5 py-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("host.startupDescription")}
|
||||
</p>
|
||||
<Select2
|
||||
aria-label={t("host.startupTitle")}
|
||||
value={startupSnippetId ?? ""}
|
||||
onChange={(e) =>
|
||||
write({
|
||||
startupSnippetId: e.target.value
|
||||
? Number(e.target.value)
|
||||
: null,
|
||||
})
|
||||
}
|
||||
className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value="">{t("host.none")}</option>
|
||||
{options.map((snippet) => (
|
||||
<option key={snippet.id} value={snippet.id}>
|
||||
{snippet.name}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
</div>
|
||||
</SectionCard>
|
||||
<SectionCard
|
||||
title={t("host.quickActionsTitle")}
|
||||
icon={<Zap className="size-3.5" />}
|
||||
action={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-[10px] px-2 border-accent-brand/40 text-accent-brand"
|
||||
onClick={() =>
|
||||
setQuickActions([...quickActions, { name: "", snippetId: null }])
|
||||
}
|
||||
>
|
||||
<Plus className="size-3 mr-1" /> {t("host.addQuickAction")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-3 py-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("host.quickActionsDescription")}
|
||||
</p>
|
||||
{quickActions.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-4 text-muted-foreground/40 gap-1.5">
|
||||
<Zap className="size-6" />
|
||||
<span className="text-xs">{t("host.noQuickActions")}</span>
|
||||
</div>
|
||||
)}
|
||||
{quickActions.map((row, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-2 p-2 bg-muted/20 border border-border group"
|
||||
>
|
||||
<Input
|
||||
className="h-7 text-xs flex-1"
|
||||
placeholder={t("host.buttonLabel")}
|
||||
value={row.name}
|
||||
onChange={(e) => {
|
||||
const next = [...quickActions];
|
||||
next[index] = { ...row, name: e.target.value };
|
||||
setQuickActions(next);
|
||||
}}
|
||||
/>
|
||||
<Select2
|
||||
aria-label={t("host.selectSnippet")}
|
||||
className="h-7 text-xs flex-1 border border-border bg-background px-2 outline-none focus:ring-1 focus:ring-ring"
|
||||
value={row.snippetId ?? ""}
|
||||
onChange={(e) => {
|
||||
const next = [...quickActions];
|
||||
next[index] = {
|
||||
...row,
|
||||
snippetId: e.target.value ? Number(e.target.value) : null,
|
||||
};
|
||||
setQuickActions(next);
|
||||
}}
|
||||
>
|
||||
<option value="">{t("host.selectSnippet")}</option>
|
||||
{options.map((snippet) => (
|
||||
<option key={snippet.id} value={snippet.id}>
|
||||
{snippet.name}
|
||||
</option>
|
||||
))}
|
||||
</Select2>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("host.removeQuickAction")}
|
||||
className="text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={() =>
|
||||
setQuickActions(quickActions.filter((_, i) => i !== index))
|
||||
}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SectionCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
usePermission,
|
||||
usePluginApi,
|
||||
useTranslation,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { Button, Separator } from "@termix/plugin-sdk/ui";
|
||||
import {
|
||||
readQuickActions,
|
||||
snippetHostSettings,
|
||||
type QuickAction,
|
||||
} from "../shared/host-settings.js";
|
||||
import { hasSnippetInputs } from "../shared/variables.js";
|
||||
import { askForInputs, shouldConfirmExecution } from "./prompt-store";
|
||||
import { createSnippetsApi } from "./snippets-api";
|
||||
import { errorMessage } from "./types";
|
||||
|
||||
interface ToolbarHost {
|
||||
id?: number | string;
|
||||
name?: string;
|
||||
ip?: string;
|
||||
username?: string;
|
||||
port?: number;
|
||||
pluginSettings?: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The host's quick actions as buttons in the Host Metrics toolbar (the
|
||||
* host-metrics.toolbar slot). Each runs its snippet on the host over a fresh
|
||||
* SSH connection and reports the result in a toast.
|
||||
*/
|
||||
export function QuickActionButtons(props: Record<string, unknown>) {
|
||||
const host = props.host as ToolbarHost | undefined;
|
||||
const { t } = useTranslation();
|
||||
const api = usePluginApi();
|
||||
const client = useMemo(() => createSnippetsApi(api), [api]);
|
||||
const canView = usePermission("view");
|
||||
const [running, setRunning] = useState<Set<number>>(new Set());
|
||||
const actions = readQuickActions(snippetHostSettings(host).quickActions);
|
||||
const hostId = Number(host?.id);
|
||||
|
||||
if (!canView || actions.length === 0 || !Number.isInteger(hostId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const setBusy = (snippetId: number, busy: boolean) =>
|
||||
setRunning((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (busy) next.add(snippetId);
|
||||
else next.delete(snippetId);
|
||||
return next;
|
||||
});
|
||||
|
||||
async function execute(
|
||||
action: QuickAction,
|
||||
inputValues: Record<string, string>,
|
||||
) {
|
||||
const toastId = `quick-action-${hostId}-${action.snippetId}`;
|
||||
setBusy(action.snippetId, true);
|
||||
toast.loading(t("quickActions.running", { name: action.name }), {
|
||||
id: toastId,
|
||||
});
|
||||
try {
|
||||
const result = await client.execute(
|
||||
action.snippetId,
|
||||
hostId,
|
||||
Object.keys(inputValues).length > 0 ? inputValues : undefined,
|
||||
);
|
||||
if (result.success) {
|
||||
toast.success(t("quickActions.succeeded", { name: action.name }), {
|
||||
id: toastId,
|
||||
description: result.output?.substring(0, 200),
|
||||
duration: 5000,
|
||||
});
|
||||
} else {
|
||||
toast.error(t("quickActions.failed", { name: action.name }), {
|
||||
id: toastId,
|
||||
description: result.error || result.output,
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(t("quickActions.error", { name: action.name }), {
|
||||
id: toastId,
|
||||
description: errorMessage(error, t("executionFailed")),
|
||||
duration: 5000,
|
||||
});
|
||||
} finally {
|
||||
setBusy(action.snippetId, false);
|
||||
}
|
||||
}
|
||||
|
||||
async function run(action: QuickAction) {
|
||||
let inputValues: Record<string, string> = {};
|
||||
const snippet = await client.get(action.snippetId).catch(() => null);
|
||||
if (snippet && hasSnippetInputs(snippet.content)) {
|
||||
const values = await askForInputs(
|
||||
{ name: action.name || snippet.name, content: snippet.content },
|
||||
host
|
||||
? {
|
||||
ip: host.ip,
|
||||
username: host.username,
|
||||
port: host.port,
|
||||
name: host.name,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
if (!values) return;
|
||||
inputValues = values;
|
||||
}
|
||||
const go = () => void execute(action, inputValues);
|
||||
if (!shouldConfirmExecution()) {
|
||||
go();
|
||||
return;
|
||||
}
|
||||
toast(t("confirmRunMessage", { name: action.name }), {
|
||||
action: { label: t("confirmRunButton"), onClick: go },
|
||||
duration: 6000,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mr-3 flex flex-wrap gap-2">
|
||||
{actions.map((action, index) => {
|
||||
const busy = running.has(action.snippetId);
|
||||
return (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 text-xs font-semibold"
|
||||
disabled={busy}
|
||||
onClick={() => void run(action)}
|
||||
>
|
||||
{busy && <RefreshCw className="mr-1 size-3 animate-spin" />}
|
||||
{action.name}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Separator orientation="vertical" className="mx-3 h-8" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSettings } from "@termix/plugin-sdk/frontend";
|
||||
import { SnippetVariablesDialog } from "./SnippetVariablesDialog";
|
||||
import { readSnippetSettings } from "./settings";
|
||||
import { setConfirmExecution, usePendingPrompt } from "./prompt-store";
|
||||
|
||||
/**
|
||||
* Always mounted at the shell's root (the shell.overlay slot), so a snippet
|
||||
* started from the command palette, a keybinding or a host button can ask
|
||||
* for its inputs after whatever started it has closed.
|
||||
*/
|
||||
export function SnippetOverlay() {
|
||||
const settings = useSettings("user");
|
||||
const prompt = usePendingPrompt();
|
||||
const confirmExecution = readSnippetSettings(
|
||||
settings.values,
|
||||
).confirmExecution;
|
||||
|
||||
useEffect(() => {
|
||||
setConfirmExecution(confirmExecution);
|
||||
}, [confirmExecution]);
|
||||
|
||||
if (!prompt) return null;
|
||||
return (
|
||||
<SnippetVariablesDialog
|
||||
snippet={prompt.snippet}
|
||||
host={prompt.host}
|
||||
onCancel={() => prompt.settle(null)}
|
||||
onConfirm={(_resolved, values) => prompt.settle(values)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
import {
|
||||
extractSnippetInputs,
|
||||
resolveSnippetContent,
|
||||
type SnippetHostContext,
|
||||
} from "../shared/variables.js";
|
||||
|
||||
/** Enough of a snippet to fill in its inputs. */
|
||||
export interface VariablesDialogSnippet {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shown before running a snippet that contains $INPUT_n placeholders:
|
||||
* collects a value per placeholder and previews the fully resolved command
|
||||
* (host vars and inputs) before handing the result back to the caller.
|
||||
*/
|
||||
export function SnippetVariablesDialog({
|
||||
snippet,
|
||||
host,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: {
|
||||
snippet: VariablesDialogSnippet;
|
||||
host: SnippetHostContext | null;
|
||||
onCancel: () => void;
|
||||
onConfirm: (
|
||||
resolvedContent: string,
|
||||
inputValues: Record<string, string>,
|
||||
) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const inputs = useMemo(
|
||||
() => extractSnippetInputs(snippet.content),
|
||||
[snippet.content],
|
||||
);
|
||||
const [values, setValues] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
setValues({});
|
||||
}, [snippet]);
|
||||
|
||||
const preview = resolveSnippetContent(snippet.content, host, values);
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(v) => !v && onCancel()}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-bold">
|
||||
{t("variablesDialogTitle", { name: snippet.name })}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs text-muted-foreground">
|
||||
{t("variablesDialogDescription")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 mt-1">
|
||||
{inputs.map((input) => (
|
||||
<div key={input.key} className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-semibold">{input.label}</label>
|
||||
<Input
|
||||
autoFocus={inputs[0]?.key === input.key}
|
||||
value={values[input.key] ?? ""}
|
||||
onChange={(e) =>
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
[input.key]: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-semibold text-muted-foreground">
|
||||
{t("variablesPreviewLabel")}
|
||||
</label>
|
||||
<span className="text-xs text-muted-foreground font-mono px-2.5 py-2 border border-border bg-muted/20 min-w-0 break-all whitespace-pre-wrap">
|
||||
{preview}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 mt-2">
|
||||
<Button variant="ghost" onClick={onCancel}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand"
|
||||
onClick={() => onConfirm(preview, values)}
|
||||
>
|
||||
{t("variablesConfirmButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,53 @@
|
||||
import { Play } from "lucide-react";
|
||||
import type { PanelProps, TermixApp } from "@termix/plugin-sdk/frontend";
|
||||
import { Clipboard, Play } from "lucide-react";
|
||||
import type {
|
||||
PaletteItem,
|
||||
PanelProps,
|
||||
PluginHostRecord,
|
||||
TermixApp,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import type { Snippet } from "./types";
|
||||
import { SnippetsPanel } from "./SnippetsPanel";
|
||||
import { createSnippetsApi } from "./snippets-api";
|
||||
import { resolveSnippetContent, hasSnippetInputs } from "./snippet-variables";
|
||||
import {
|
||||
AdminUserSnippets,
|
||||
adminOptions,
|
||||
mapSnippets,
|
||||
} from "./AdminUserSnippets";
|
||||
extractSnippetInputs,
|
||||
resolveSnippetContent,
|
||||
type SnippetHostContext,
|
||||
} from "../shared/variables.js";
|
||||
import {
|
||||
readStartupSnippetId,
|
||||
snippetHostSettings,
|
||||
} from "../shared/host-settings.js";
|
||||
import { AdminUserSnippets } from "./AdminUserSnippets";
|
||||
import { SnippetOverlay } from "./SnippetOverlay";
|
||||
import { HostSnippetsSection } from "./HostSnippetsSection";
|
||||
import { QuickActionButtons } from "./QuickActionButtons";
|
||||
import { resetPrompts } from "./prompt-store";
|
||||
import { fetchSnippet, runInSession } from "./run-flows";
|
||||
import {
|
||||
RUN_SNIPPET_ACTION,
|
||||
RunSnippetEditor,
|
||||
RunSnippetSummary,
|
||||
runSnippetBinding,
|
||||
validateRunSnippet,
|
||||
} from "./keybinding";
|
||||
|
||||
function hostContext(host: unknown): SnippetHostContext | null {
|
||||
if (!host || typeof host !== "object") return null;
|
||||
const record = host as Record<string, unknown>;
|
||||
return {
|
||||
ip: typeof record.ip === "string" ? record.ip : undefined,
|
||||
username: typeof record.username === "string" ? record.username : undefined,
|
||||
port:
|
||||
typeof record.port === "number" || typeof record.port === "string"
|
||||
? record.port
|
||||
: undefined,
|
||||
name: typeof record.name === "string" ? record.name : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function activate(app: TermixApp): void {
|
||||
app.onDispose(resetPrompts);
|
||||
|
||||
app.registerRailItem({
|
||||
id: "snippets",
|
||||
icon: Play,
|
||||
@@ -33,6 +71,73 @@ export function activate(app: TermixApp): void {
|
||||
},
|
||||
});
|
||||
|
||||
// Every snippet in the command palette, run in the terminal the user is
|
||||
// working in.
|
||||
app.registerPaletteGroup({
|
||||
id: "snippets",
|
||||
titleKey: "nav.snippets",
|
||||
order: 10,
|
||||
load: async (): Promise<PaletteItem[]> => {
|
||||
if (!(await app.hasPermission("view"))) return [];
|
||||
const snippets: Snippet[] = await createSnippetsApi(app.api)
|
||||
.list()
|
||||
.catch(() => []);
|
||||
return snippets.map((snippet) => ({
|
||||
id: String(snippet.id),
|
||||
title: snippet.name,
|
||||
description: snippet.content,
|
||||
icon: snippet.isNote ? Clipboard : Play,
|
||||
keywords: [snippet.description ?? ""],
|
||||
needsTarget: true,
|
||||
hint: app.t(snippet.isNote ? "paletteHintPaste" : "paletteHintRun"),
|
||||
run: ({ targetTab }) => {
|
||||
if (!targetTab) return;
|
||||
void runInSession(
|
||||
app,
|
||||
snippet,
|
||||
{ sessionId: targetTab.id },
|
||||
hostContext(targetTab.host),
|
||||
{ announce: true },
|
||||
);
|
||||
},
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
// Keeps the input dialog and the confirm setting alive at the shell root.
|
||||
app.registerSlotContribution("shell.overlay", {
|
||||
actionId: "snippets.overlay",
|
||||
titleKey: "nav.snippets",
|
||||
kind: "component",
|
||||
component: SnippetOverlay as never,
|
||||
});
|
||||
|
||||
app.registerKeybindingAction({
|
||||
id: RUN_SNIPPET_ACTION,
|
||||
titleKey: "keybindings.runSnippet",
|
||||
editor: RunSnippetEditor,
|
||||
summary: RunSnippetSummary,
|
||||
validate: validateRunSnippet,
|
||||
run: (action, context) => runSnippetBinding(app, action, context),
|
||||
});
|
||||
|
||||
app.registerHostEditorSection({
|
||||
id: "snippets",
|
||||
group: "ssh",
|
||||
titleKey: "host.tabTitle",
|
||||
icon: Play,
|
||||
order: 15,
|
||||
component: HostSnippetsSection,
|
||||
});
|
||||
|
||||
// Host Metrics shows a host's quick actions as toolbar buttons.
|
||||
app.registerSlotContribution("host-metrics.toolbar", {
|
||||
actionId: "snippets.quickActions",
|
||||
titleKey: "host.quickActionsTitle",
|
||||
kind: "component",
|
||||
component: QuickActionButtons,
|
||||
});
|
||||
|
||||
app.registerSlotContribution("onboarding.features", {
|
||||
actionId: "snippets.feature",
|
||||
titleKey: "onboarding.feature_snippets",
|
||||
@@ -49,71 +154,17 @@ export function activate(app: TermixApp): void {
|
||||
order: 10,
|
||||
});
|
||||
|
||||
// Core's snippet pickers (the host editor's startup snippet, keybindings,
|
||||
// the command palette) read the list through this. An admin editing
|
||||
// another user's host passes that user's id.
|
||||
app.registerAction("snippet.list", (async (options?: {
|
||||
targetUserId?: string;
|
||||
}) => {
|
||||
const response = options?.targetUserId
|
||||
? await app.api.get("/", adminOptions(options.targetUserId))
|
||||
: await app.api.get("/");
|
||||
const raw = response.data as unknown;
|
||||
const list = Array.isArray(raw)
|
||||
? raw
|
||||
: ((raw as { snippets?: unknown[] })?.snippets ?? []);
|
||||
return (list as Record<string, unknown>[]).map((row) => ({
|
||||
...mapSnippets([row])[0],
|
||||
isNote: row.isNote === true,
|
||||
}));
|
||||
}) as never);
|
||||
|
||||
// Resolves a snippet by id for a terminal (startup snippet, custom
|
||||
// keybindings, the command palette), so core never imports snippet code.
|
||||
const resolveForTerminal = async (
|
||||
snippetId: number,
|
||||
host: {
|
||||
ip?: string;
|
||||
username?: string;
|
||||
port?: number;
|
||||
name?: string;
|
||||
} | null,
|
||||
inputValues?: Record<string, string>,
|
||||
// The command a terminal types when it connects to a host: the host's
|
||||
// startup snippet with its variables filled in, or null.
|
||||
app.registerAction("snippets.startupCommand", (async (
|
||||
host: PluginHostRecord | null,
|
||||
vars?: SnippetHostContext,
|
||||
) => {
|
||||
const api = createSnippetsApi(app.api);
|
||||
const snippet = await api.get(snippetId).catch(() => null);
|
||||
const id = readStartupSnippetId(snippetHostSettings(host).startupSnippetId);
|
||||
if (id === null) return null;
|
||||
const snippet = await fetchSnippet(app, id);
|
||||
if (!snippet) return null;
|
||||
if (
|
||||
hasSnippetInputs(snippet.content) &&
|
||||
(!inputValues || Object.keys(inputValues).length === 0)
|
||||
) {
|
||||
return {
|
||||
needsInputs: true,
|
||||
content: snippet.content,
|
||||
isNote: snippet.isNote,
|
||||
};
|
||||
}
|
||||
return {
|
||||
needsInputs: false,
|
||||
content: resolveSnippetContent(snippet.content, host, inputValues ?? {}),
|
||||
isNote: snippet.isNote,
|
||||
};
|
||||
};
|
||||
app.registerAction("snippet.resolveForTerminal", resolveForTerminal as never);
|
||||
app.registerAction(
|
||||
"snippets.resolveForTerminal",
|
||||
resolveForTerminal as never,
|
||||
);
|
||||
|
||||
// For other plugins' quick actions (host metrics): the snippet's content,
|
||||
// so the caller can ask for $INPUT_n values, and a run on a host.
|
||||
app.registerAction("snippets.get", (async (snippetId: number) => {
|
||||
const snippet = await createSnippetsApi(app.api)
|
||||
.get(snippetId)
|
||||
.catch(() => null);
|
||||
return snippet
|
||||
? { id: snippet.id, name: snippet.name, content: snippet.content }
|
||||
: null;
|
||||
return resolveSnippetContent(snippet.content, vars ?? hostContext(host));
|
||||
}) as never);
|
||||
|
||||
// For pickers in other plugins (the AI assistant's @-mentions): id and name.
|
||||
@@ -124,14 +175,7 @@ export function activate(app: TermixApp): void {
|
||||
return snippets.map((snippet) => ({ id: snippet.id, name: snippet.name }));
|
||||
}) as never);
|
||||
|
||||
app.registerAction("snippets.execute", (async (
|
||||
snippetId: number,
|
||||
hostId: number,
|
||||
inputValues?: Record<string, string>,
|
||||
) =>
|
||||
createSnippetsApi(app.api).execute(
|
||||
snippetId,
|
||||
hostId,
|
||||
inputValues,
|
||||
)) as never);
|
||||
// Variable handling for commands other plugins run (fleets' run box).
|
||||
app.registerAction("snippets.extractInputs", ((content: string) =>
|
||||
extractSnippetInputs(typeof content === "string" ? content : "")) as never);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
useTranslation,
|
||||
type KeybindingAction,
|
||||
type KeybindingActionEditorProps,
|
||||
type KeybindingRunContext,
|
||||
type TermixApp,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { fetchSnippet, runInSession } from "./run-flows";
|
||||
import { useSnippetOptions } from "./use-snippet-options";
|
||||
|
||||
/** Stored as action.type; kept from 2.8 so saved bindings keep working. */
|
||||
export const RUN_SNIPPET_ACTION = "runSnippet";
|
||||
|
||||
function snippetIdOf(action: KeybindingAction): string {
|
||||
return typeof action.snippetId === "string" ? action.snippetId : "";
|
||||
}
|
||||
|
||||
/** Picks the snippet a "Run snippet" binding runs. */
|
||||
export function RunSnippetEditor({
|
||||
action,
|
||||
onChange,
|
||||
}: KeybindingActionEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const { options } = useSnippetOptions();
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-semibold">
|
||||
{t("keybindings.snippetLabel")}
|
||||
</label>
|
||||
<select
|
||||
aria-label={t("keybindings.snippetLabel")}
|
||||
value={snippetIdOf(action)}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
type: RUN_SNIPPET_ACTION,
|
||||
snippetId: e.target.value || undefined,
|
||||
appendEnter: true,
|
||||
})
|
||||
}
|
||||
className="h-8 border border-border bg-background px-2 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value="">{t("keybindings.selectSnippet")}</option>
|
||||
{options.map((snippet) => (
|
||||
<option key={snippet.id} value={String(snippet.id)}>
|
||||
{snippet.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** The bound snippet's name in the binding list, or a warning once it is gone. */
|
||||
export function RunSnippetSummary({ action }: KeybindingActionEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const { options, loaded } = useSnippetOptions();
|
||||
if (!loaded) return null;
|
||||
const snippet = options.find((s) => String(s.id) === snippetIdOf(action));
|
||||
if (snippet) return <span>({snippet.name})</span>;
|
||||
return (
|
||||
<span className="text-destructive">
|
||||
({t("keybindings.snippetMissing")})
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function validateRunSnippet(action: KeybindingAction): string | null {
|
||||
return /^\d+$/.test(snippetIdOf(action))
|
||||
? null
|
||||
: "keybindings.snippetRequired";
|
||||
}
|
||||
|
||||
/** Runs a bound snippet in the terminal the key was pressed in. */
|
||||
export function runSnippetBinding(
|
||||
app: Pick<TermixApp, "api" | "t">,
|
||||
action: KeybindingAction,
|
||||
context: KeybindingRunContext,
|
||||
): void {
|
||||
void fetchSnippet(app, Number(snippetIdOf(action))).then((snippet) => {
|
||||
if (!snippet) return;
|
||||
void runInSession(
|
||||
app,
|
||||
{ ...snippet, isNote: false },
|
||||
{ sessionId: context.sessionId, send: context.send },
|
||||
context.host ?? null,
|
||||
{ pressEnter: action.appendEnter !== false },
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useSyncExternalStore } from "react";
|
||||
import type { SnippetHostContext } from "../shared/variables.js";
|
||||
|
||||
export interface PendingPrompt {
|
||||
snippet: { name: string; content: string };
|
||||
host: SnippetHostContext | null;
|
||||
settle: (values: Record<string, string> | null) => void;
|
||||
}
|
||||
|
||||
let pending: PendingPrompt | null = null;
|
||||
let confirmExecution = false;
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function notify(): void {
|
||||
for (const listener of [...listeners]) listener();
|
||||
}
|
||||
|
||||
function subscribe(listener: () => void): () => void {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks for a snippet's $INPUT_n values through the dialog the overlay draws.
|
||||
* Resolves null when the user cancels. A second request cancels the first.
|
||||
*/
|
||||
export function askForInputs(
|
||||
snippet: { name: string; content: string },
|
||||
host: SnippetHostContext | null,
|
||||
): Promise<Record<string, string> | null> {
|
||||
pending?.settle(null);
|
||||
return new Promise((resolve) => {
|
||||
const prompt: PendingPrompt = {
|
||||
snippet,
|
||||
host,
|
||||
settle: (values) => {
|
||||
if (pending === prompt) {
|
||||
pending = null;
|
||||
notify();
|
||||
}
|
||||
resolve(values);
|
||||
},
|
||||
};
|
||||
pending = prompt;
|
||||
notify();
|
||||
});
|
||||
}
|
||||
|
||||
export function usePendingPrompt(): PendingPrompt | null {
|
||||
return useSyncExternalStore(
|
||||
subscribe,
|
||||
() => pending,
|
||||
() => pending,
|
||||
);
|
||||
}
|
||||
|
||||
/** Kept current by the overlay from the user's confirmExecution setting. */
|
||||
export function setConfirmExecution(value: boolean): void {
|
||||
confirmExecution = value;
|
||||
}
|
||||
|
||||
export function shouldConfirmExecution(): boolean {
|
||||
return confirmExecution;
|
||||
}
|
||||
|
||||
/** Cancels anything waiting, for deactivate. */
|
||||
export function resetPrompts(): void {
|
||||
pending?.settle(null);
|
||||
pending = null;
|
||||
confirmExecution = false;
|
||||
notify();
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { toast } from "sonner";
|
||||
import { invokeAction, type TermixApp } from "@termix/plugin-sdk/frontend";
|
||||
import {
|
||||
hasSnippetInputs,
|
||||
resolveSnippetContent,
|
||||
type SnippetHostContext,
|
||||
} from "../shared/variables.js";
|
||||
import { askForInputs, shouldConfirmExecution } from "./prompt-store";
|
||||
import { createSnippetsApi } from "./snippets-api";
|
||||
|
||||
export interface RunnableSnippet {
|
||||
id: number;
|
||||
name: string;
|
||||
content: string;
|
||||
isNote?: boolean;
|
||||
}
|
||||
|
||||
/** Where a run sends its text: a session by id, or a raw writer. */
|
||||
export interface RunTarget {
|
||||
sessionId?: string;
|
||||
send?: (data: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The snippet with its variables filled in, asking for $INPUT_n values when
|
||||
* it has any. Null when the user cancels.
|
||||
*/
|
||||
export async function resolveForRun(
|
||||
snippet: { name: string; content: string },
|
||||
host: SnippetHostContext | null,
|
||||
): Promise<string | null> {
|
||||
if (!hasSnippetInputs(snippet.content)) {
|
||||
return resolveSnippetContent(snippet.content, host);
|
||||
}
|
||||
const values = await askForInputs(snippet, host);
|
||||
if (!values) return null;
|
||||
return resolveSnippetContent(snippet.content, host, values);
|
||||
}
|
||||
|
||||
/** Runs `execute` now, or after the user confirms when they asked to. */
|
||||
export function confirmThenRun(
|
||||
app: Pick<TermixApp, "t">,
|
||||
name: string,
|
||||
execute: () => void,
|
||||
): void {
|
||||
if (!shouldConfirmExecution()) {
|
||||
execute();
|
||||
return;
|
||||
}
|
||||
toast(app.t("confirmRunMessage", { name }), {
|
||||
action: { label: app.t("confirmRunButton"), onClick: execute },
|
||||
duration: 6000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Types a snippet into a terminal session: resolves its variables, asks for
|
||||
* inputs and confirmation as the user's settings say, then sends it. A note
|
||||
* is pasted without running.
|
||||
*/
|
||||
export async function runInSession(
|
||||
app: Pick<TermixApp, "t">,
|
||||
snippet: RunnableSnippet,
|
||||
target: RunTarget,
|
||||
host: SnippetHostContext | null,
|
||||
options: { pressEnter?: boolean; announce?: boolean } = {},
|
||||
): Promise<void> {
|
||||
const content = await resolveForRun(snippet, host);
|
||||
if (content === null) return;
|
||||
const run = !snippet.isNote && options.pressEnter !== false;
|
||||
const send = async () => {
|
||||
let sent = true;
|
||||
if (target.send) {
|
||||
target.send(content + (run ? "\r" : ""));
|
||||
} else if (target.sessionId) {
|
||||
sent =
|
||||
(await invokeAction(
|
||||
"terminal.sendToSession",
|
||||
target.sessionId,
|
||||
content,
|
||||
{
|
||||
run,
|
||||
},
|
||||
)) === true;
|
||||
} else {
|
||||
sent = false;
|
||||
}
|
||||
if (!options.announce) return;
|
||||
if (!sent) {
|
||||
toast.error(app.t("noTerminalTabsOpen"));
|
||||
return;
|
||||
}
|
||||
toast.success(
|
||||
app.t(snippet.isNote ? "pasteSuccess" : "runSuccess", {
|
||||
name: snippet.name,
|
||||
count: 1,
|
||||
}),
|
||||
);
|
||||
};
|
||||
if (snippet.isNote) void send();
|
||||
else confirmThenRun(app, snippet.name, () => void send());
|
||||
}
|
||||
|
||||
/** One snippet by id, or null when it is gone or unreadable. */
|
||||
export async function fetchSnippet(
|
||||
app: Pick<TermixApp, "api">,
|
||||
id: number,
|
||||
): Promise<RunnableSnippet | null> {
|
||||
if (!Number.isInteger(id) || id <= 0) return null;
|
||||
const snippet = await createSnippetsApi(app.api)
|
||||
.get(id)
|
||||
.catch(() => null);
|
||||
return snippet
|
||||
? {
|
||||
id: snippet.id,
|
||||
name: snippet.name,
|
||||
content: snippet.content,
|
||||
isNote: snippet.isNote,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePluginApi } from "@termix/plugin-sdk/frontend";
|
||||
import { adminOptions } from "./AdminUserSnippets";
|
||||
|
||||
export interface SnippetOption {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The user's snippets as picker options: their own, or the target user's
|
||||
* while an admin edits that user's host. `loaded` turns true once the list
|
||||
* came back, so a picker can tell "no snippets" from "not loaded yet".
|
||||
*/
|
||||
export function useSnippetOptions(targetUserId?: string): {
|
||||
options: SnippetOption[];
|
||||
loaded: boolean;
|
||||
} {
|
||||
const api = usePluginApi();
|
||||
const [state, setState] = useState<{
|
||||
options: SnippetOption[];
|
||||
loaded: boolean;
|
||||
}>({ options: [], loaded: false });
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const request = targetUserId
|
||||
? api.get("/", adminOptions(targetUserId))
|
||||
: api.get("/");
|
||||
request
|
||||
.then((response) => {
|
||||
const raw = response.data as unknown;
|
||||
const list = Array.isArray(raw)
|
||||
? raw
|
||||
: ((raw as { snippets?: unknown[] })?.snippets ?? []);
|
||||
const options = (list as Array<{ id?: unknown; name?: unknown }>)
|
||||
.filter((row) => typeof row.id === "number")
|
||||
.map((row) => ({
|
||||
id: row.id as number,
|
||||
name: typeof row.name === "string" ? row.name : `#${row.id}`,
|
||||
}));
|
||||
if (!cancelled) setState({ options, loaded: true });
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setState({ options: [], loaded: true });
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [api, targetUserId]);
|
||||
|
||||
return state;
|
||||
}
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
useTranslation,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { toast } from "sonner";
|
||||
import { SnippetVariablesDialog } from "@termix/plugin-sdk/ui";
|
||||
import {
|
||||
hasSnippetInputs,
|
||||
resolveSnippetContent,
|
||||
type SnippetHostContext,
|
||||
} from "./snippet-variables";
|
||||
} from "../shared/variables.js";
|
||||
import { SnippetVariablesDialog } from "./SnippetVariablesDialog";
|
||||
import { createSnippetsApi } from "./snippets-api";
|
||||
import {
|
||||
ExecutionResultsDialog,
|
||||
@@ -259,7 +259,7 @@ export function useSnippetRunner(confirmExecution = false) {
|
||||
|
||||
const variablesDialog = runningSnippet ? (
|
||||
<SnippetVariablesDialog
|
||||
snippet={runningSnippet.snippet as never}
|
||||
snippet={runningSnippet.snippet}
|
||||
host={runningSnippet.host}
|
||||
onCancel={() => setRunningSnippet(null)}
|
||||
onConfirm={runningSnippet.onConfirm}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* The snippets plugin's host settings: the startup snippet a terminal runs
|
||||
* when it connects, and the quick action buttons Host Metrics shows.
|
||||
*/
|
||||
|
||||
export const PLUGIN_ID = "snippets";
|
||||
|
||||
export interface QuickAction {
|
||||
name: string;
|
||||
snippetId: number;
|
||||
}
|
||||
|
||||
function snippetId(value: unknown): number | null {
|
||||
const id = typeof value === "string" ? Number(value) : value;
|
||||
return typeof id === "number" && Number.isInteger(id) && id > 0 ? id : null;
|
||||
}
|
||||
|
||||
/** Quick actions in their stored shape, dropping any without a snippet. */
|
||||
export function readQuickActions(value: unknown): QuickAction[] {
|
||||
let list = value;
|
||||
if (typeof list === "string") {
|
||||
try {
|
||||
list = JSON.parse(list);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(list)) return [];
|
||||
return list.flatMap((entry): QuickAction[] => {
|
||||
if (!entry || typeof entry !== "object") return [];
|
||||
const id = snippetId((entry as { snippetId?: unknown }).snippetId);
|
||||
if (id === null) return [];
|
||||
const name = (entry as { name?: unknown }).name;
|
||||
return [{ name: typeof name === "string" ? name : "", snippetId: id }];
|
||||
});
|
||||
}
|
||||
|
||||
export function readStartupSnippetId(value: unknown): number | null {
|
||||
return snippetId(value);
|
||||
}
|
||||
|
||||
/** This plugin's slice of a host record's pluginSettings. */
|
||||
export function snippetHostSettings(host: unknown): Record<string, unknown> {
|
||||
const all = (host as { pluginSettings?: Record<string, unknown> } | null)
|
||||
?.pluginSettings;
|
||||
const own = all?.[PLUGIN_ID];
|
||||
return own && typeof own === "object" ? (own as Record<string, unknown>) : {};
|
||||
}
|
||||
+6
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* $HOST, $USER, $PORT, $NAME and $INPUT_n in a command. The one copy: the
|
||||
* frontend, the backend and other plugins (fleets, through the
|
||||
* "snippets.variables" registry entry and action) all use this.
|
||||
*/
|
||||
|
||||
export interface SnippetInput {
|
||||
key: string;
|
||||
label: string;
|
||||
@@ -98,6 +98,28 @@ describe("snippets activate", () => {
|
||||
expect(after).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("registers its host settings hooks and the variables helper", async () => {
|
||||
server = await startServer();
|
||||
const registry = server.mock.ctx.registry;
|
||||
for (const key of [
|
||||
"snippets.hostImportNormalizer",
|
||||
"snippets.hostPayloadLegacy",
|
||||
"snippets.hostSettingsSync",
|
||||
]) {
|
||||
expect(registry.consume(key), key).toBeDefined();
|
||||
}
|
||||
const variables = registry.consume<{
|
||||
resolve: (c: string, h: { ip?: string } | null) => string;
|
||||
extractInputs: (c: string) => Array<{ key: string }>;
|
||||
}>("snippets.variables");
|
||||
expect(variables?.resolve("ping $HOST", { ip: "10.0.0.1" })).toBe(
|
||||
"ping 10.0.0.1",
|
||||
);
|
||||
expect(variables?.extractInputs("echo $INPUT_1")).toEqual([
|
||||
{ key: "INPUT_1", label: "Input 1" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("fails closed without db:own", async () => {
|
||||
db = await createTestDb(pluginDir);
|
||||
const mock = createMockCtx({
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createHostSettingsSync,
|
||||
hostImportNormalizer,
|
||||
hostPayloadLegacy,
|
||||
} from "../../src/backend/host-import.js";
|
||||
|
||||
describe("hostImportNormalizer", () => {
|
||||
it("reads a 2.8 export's quick actions and startup snippet", () => {
|
||||
expect(
|
||||
hostImportNormalizer({
|
||||
quickActions: [
|
||||
{ name: "Restart", snippetId: "3" },
|
||||
{ name: "Broken", snippetId: "" },
|
||||
],
|
||||
terminalConfig: JSON.stringify({ startupSnippetId: 5, fontSize: 12 }),
|
||||
}),
|
||||
).toEqual({
|
||||
quickActions: [{ name: "Restart", snippetId: 3 }],
|
||||
startupSnippetId: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves an export that carries this plugin's settings to core", () => {
|
||||
expect(
|
||||
hostImportNormalizer({
|
||||
pluginSettings: { snippets: { startupSnippetId: 1 } },
|
||||
terminalConfig: { startupSnippetId: 5 },
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("writes nothing for a host without either", () => {
|
||||
expect(hostImportNormalizer({ name: "web" })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hostPayloadLegacy", () => {
|
||||
it("puts the 2.8 fields back for older clients", () => {
|
||||
expect(
|
||||
hostPayloadLegacy({
|
||||
quickActions: [{ name: "Up", snippetId: 2 }],
|
||||
startupSnippetId: 7,
|
||||
}),
|
||||
).toEqual({
|
||||
quickActions: [{ name: "Up", snippetId: 2 }],
|
||||
terminalConfig: { startupSnippetId: 7 },
|
||||
});
|
||||
expect(hostPayloadLegacy({})).toEqual({ quickActions: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("hostSettingsSync", () => {
|
||||
const sync = createHostSettingsSync({
|
||||
findSyncIdById: async (id) => (id === 2 ? "sync-2" : null),
|
||||
findIdBySyncId: async (syncId) => (syncId === "sync-2" ? 9 : null),
|
||||
});
|
||||
|
||||
it("sends sync ids and drops what does not map", async () => {
|
||||
expect(await sync.exportValue("startupSnippetId", 2)).toBe("sync-2");
|
||||
expect(await sync.exportValue("startupSnippetId", 4)).toBeNull();
|
||||
expect(
|
||||
await sync.exportValue("quickActions", [
|
||||
{ name: "A", snippetId: 2 },
|
||||
{ name: "B", snippetId: 4 },
|
||||
]),
|
||||
).toEqual([{ name: "A", snippetId: "sync-2" }]);
|
||||
});
|
||||
|
||||
it("maps sync ids back to this side's rows", async () => {
|
||||
expect(await sync.importValue("startupSnippetId", "sync-2")).toBe(9);
|
||||
expect(await sync.importValue("startupSnippetId", "missing")).toBeNull();
|
||||
expect(
|
||||
await sync.importValue("quickActions", [
|
||||
{ name: "A", snippetId: "sync-2" },
|
||||
]),
|
||||
).toEqual([{ name: "A", snippetId: 9 }]);
|
||||
});
|
||||
});
|
||||
Loaded 100 of 262 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user