mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55:24 +00:00
v26.10.1 (#1599)
* feat: AI translations at release, 7 new languages and regional fallback * fix: proper plural forms for count strings and full translations * fix: plural forms for selected, hidden and active counts * fix: synced plugin updates failing on desktop and server-only plugins shown as stopped * fix: restore rounded corners on app icons and favicon * feat: desktop app auto updates with automatic, ask first or never modes * feat: community and custom plugin registries * feat: plugin guide for AI tools in the SDK with a lint check * fix: keep the terminal WebSocket open past nginx's 60s default (#1588) * chore: update changelog * feat: show plugin download counts only and stop CI builds inflating them * feat: hide installed plugins in the browse tab by default * chore: pin bundled plugins to latest releases * chore: update translations * fix: focusing a saved password field and saving no longer erases it * fix: tell desktop users when their 2.8 data needs 2.9 first * fix: point windows update info at the x64 installer * fix: keep desktop client tunnel sockets alive behind proxies * chore: pin bundled plugins to latest releases * fix: cover client tunnel keepalive in the c2s runtime tests * fix: typing after opening a host no longer goes to the host search * fix: accept tab and = separators in SSH config import (#1598) * chore: update changelog * chore: drop plugin versions from core changelog * chore: update translations * chore: lint, format, and bump version to 26.10.1 --------- Co-authored-by: Darnel Hunter <68697450+innotelinc@users.noreply.github.com> Co-authored-by: Iain <Iain1970@protonmail.com>
This commit is contained in:
174 files changed
+94334
-39995
No files matched your search
@@ -0,0 +1,162 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { ExternalLink, MonitorDown } from "lucide-react";
|
||||
import { Button } from "@/components/button";
|
||||
import { SectionCard, SettingRow } from "@/components/section-card";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/select";
|
||||
import {
|
||||
canConfigureUpdates,
|
||||
desktopUpdates,
|
||||
useDesktopUpdate,
|
||||
type DesktopUpdateMode,
|
||||
type DesktopUpdateState,
|
||||
} from "@/lib/desktop-updates";
|
||||
|
||||
const MODES: DesktopUpdateMode[] = ["automatic", "prompt", "never"];
|
||||
|
||||
function StatusText({ state }: { state: DesktopUpdateState }) {
|
||||
const { t } = useTranslation();
|
||||
const version = state.version;
|
||||
switch (state.status) {
|
||||
case "checking":
|
||||
return <>{t("desktopUpdates.statusChecking")}</>;
|
||||
case "available":
|
||||
return <>{t("desktopUpdates.statusAvailable", { version })}</>;
|
||||
case "downloading":
|
||||
return (
|
||||
<>
|
||||
{t("desktopUpdates.statusDownloading", {
|
||||
version,
|
||||
progress: state.progress ?? 0,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
case "downloaded":
|
||||
return <>{t("desktopUpdates.statusReady", { version })}</>;
|
||||
case "up-to-date":
|
||||
return <>{t("desktopUpdates.statusUpToDate")}</>;
|
||||
case "error":
|
||||
return <>{t("desktopUpdates.statusError")}</>;
|
||||
default:
|
||||
return <>{t("desktopUpdates.statusIdle")}</>;
|
||||
}
|
||||
}
|
||||
|
||||
/** Desktop only: how this install updates itself. */
|
||||
export function DesktopUpdatesCard() {
|
||||
const { t } = useTranslation();
|
||||
const state = useDesktopUpdate();
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
if (!canConfigureUpdates(state)) return null;
|
||||
const bridge = desktopUpdates();
|
||||
|
||||
const run = async (action: () => Promise<unknown> | undefined) => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await action();
|
||||
} catch {
|
||||
toast.error(t("settings.updates.saveFailed"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const modeLabel = (mode: DesktopUpdateMode) =>
|
||||
mode === "automatic"
|
||||
? t("desktopUpdates.modeAutomatic")
|
||||
: mode === "prompt"
|
||||
? t("desktopUpdates.modePrompt")
|
||||
: t("desktopUpdates.modeNever");
|
||||
|
||||
return (
|
||||
<SectionCard
|
||||
title={t("desktopUpdates.title")}
|
||||
icon={<MonitorDown className="size-3.5" />}
|
||||
rowId="updates-desktop"
|
||||
>
|
||||
<SettingRow
|
||||
label={t("desktopUpdates.mode")}
|
||||
description={
|
||||
state.kind === "manual"
|
||||
? t("desktopUpdates.manualModeHint")
|
||||
: t("desktopUpdates.modeHint")
|
||||
}
|
||||
>
|
||||
<Select
|
||||
disabled={busy}
|
||||
value={state.mode}
|
||||
onValueChange={(next) =>
|
||||
void run(() => bridge?.setMode(next as DesktopUpdateMode))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-40 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{MODES.map((mode) => (
|
||||
<SelectItem key={mode} value={mode}>
|
||||
{modeLabel(mode)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("desktopUpdates.status", { version: state.currentVersion })}
|
||||
description={<StatusText state={state} />}
|
||||
>
|
||||
{state.status === "downloaded" ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8"
|
||||
onClick={() => void bridge?.install()}
|
||||
>
|
||||
{t("desktopUpdates.restart")}
|
||||
</Button>
|
||||
) : state.status === "available" && state.kind === "auto" ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8"
|
||||
disabled={busy}
|
||||
onClick={() => void run(() => bridge?.download())}
|
||||
>
|
||||
{t("desktopUpdates.download")}
|
||||
</Button>
|
||||
) : state.status === "available" && state.releaseUrl ? (
|
||||
<Button size="sm" variant="outline" className="h-8 gap-1.5" asChild>
|
||||
<a
|
||||
href={state.releaseUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t("desktopUpdates.download")}
|
||||
<ExternalLink className="size-3" />
|
||||
</a>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8"
|
||||
disabled={
|
||||
busy ||
|
||||
state.status === "checking" ||
|
||||
state.status === "downloading"
|
||||
}
|
||||
onClick={() => void run(() => bridge?.check())}
|
||||
>
|
||||
{t("desktopUpdates.checkNow")}
|
||||
</Button>
|
||||
)}
|
||||
</SettingRow>
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
@@ -387,7 +387,7 @@ export function SettingsTab({
|
||||
)}
|
||||
</SettingsEmbedContext.Provider>
|
||||
{page.id === "keybindings" && <KeybindingsSettings />}
|
||||
{page.id === "updates" && isAdmin && <UpdatesSettings />}
|
||||
{page.id === "updates" && <UpdatesSettings isAdmin={isAdmin} />}
|
||||
{pluginFeature && (
|
||||
<>
|
||||
{pluginFeature.user && (
|
||||
|
||||
@@ -22,6 +22,7 @@ import { isElectron } from "@/lib/electron";
|
||||
import { isPrerelease } from "@/plugins/plugin-model";
|
||||
import { OPEN_PLUGINS_EVENT } from "@/plugins/open-plugins";
|
||||
import { docsUrl } from "@/lib/docs";
|
||||
import { DesktopUpdatesCard } from "./DesktopUpdatesCard";
|
||||
|
||||
const DOCKER_BETA_IMAGE = "ghcr.io/termix-ssh/termix:beta";
|
||||
|
||||
@@ -61,7 +62,7 @@ function ReleaseRow({
|
||||
);
|
||||
}
|
||||
|
||||
export function UpdatesSettings() {
|
||||
function InstanceUpdates() {
|
||||
const { t } = useTranslation();
|
||||
const [channel, setChannel] = useState<UpdateChannelState | null>(null);
|
||||
const [releases, setReleases] = useState<{
|
||||
@@ -317,3 +318,12 @@ export function UpdatesSettings() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function UpdatesSettings({ isAdmin }: { isAdmin: boolean }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<DesktopUpdatesCard />
|
||||
{isAdmin && <InstanceUpdates />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
UI_FONTS,
|
||||
} from "@/lib/theme";
|
||||
import { changeAppLanguage, normalizeLanguageCode } from "@/i18n/i18n";
|
||||
import { LANGUAGES } from "@/i18n/languages";
|
||||
import { getUserPreferences, saveUserPreferences } from "@/api/open-tabs-api";
|
||||
import type { FontSizeId, ThemeId, UiFontId } from "@/types/ui-types";
|
||||
|
||||
@@ -48,44 +49,6 @@ const THEME_PALETTES: Record<
|
||||
gruvbox: { bg: "#282828", panel: "#3c3836", text: "#ebdbb2" },
|
||||
};
|
||||
|
||||
export const LANGUAGES = [
|
||||
{ code: "en", label: "English" },
|
||||
{ code: "af", label: "Afrikaans" },
|
||||
{ code: "ar", label: "العربية" },
|
||||
{ code: "bn", label: "বাংলা" },
|
||||
{ code: "bg", label: "Български" },
|
||||
{ code: "ca", label: "Català" },
|
||||
{ code: "zh-CN", label: "中文 (简体)" },
|
||||
{ code: "zh-TW", label: "中文 (繁體)" },
|
||||
{ code: "cs", label: "Čeština" },
|
||||
{ code: "da", label: "Dansk" },
|
||||
{ code: "nl", label: "Nederlands" },
|
||||
{ code: "fi", label: "Suomi" },
|
||||
{ code: "fr", label: "Français" },
|
||||
{ code: "de", label: "Deutsch" },
|
||||
{ code: "el", label: "Ελληνικά" },
|
||||
{ code: "he", label: "עברית" },
|
||||
{ code: "hi", label: "हिन्दी" },
|
||||
{ code: "hu", label: "Magyar" },
|
||||
{ code: "id", label: "Indonesia" },
|
||||
{ code: "it", label: "Italiano" },
|
||||
{ code: "ja", label: "日本語" },
|
||||
{ code: "ko", label: "한국어" },
|
||||
{ code: "no", label: "Norsk" },
|
||||
{ code: "pl", label: "Polski" },
|
||||
{ code: "pt-PT", label: "Português (PT)" },
|
||||
{ code: "pt-BR", label: "Português (BR)" },
|
||||
{ code: "ro", label: "Română" },
|
||||
{ code: "ru", label: "Русский" },
|
||||
{ code: "sr", label: "Српски" },
|
||||
{ code: "es-ES", label: "Español" },
|
||||
{ code: "sv-SE", label: "Svenska" },
|
||||
{ code: "th", label: "ไทย" },
|
||||
{ code: "tr", label: "Türkçe" },
|
||||
{ code: "uk", label: "Українська" },
|
||||
{ code: "vi", label: "Tiếng Việt" },
|
||||
];
|
||||
|
||||
export const THEME_LABEL_KEYS: Record<ThemeId, string> = {
|
||||
system: "newUi.sidebar.userProfile.themeSystem",
|
||||
light: "newUi.sidebar.userProfile.themeLight",
|
||||
|
||||
@@ -38,6 +38,8 @@ export interface SettingsPage {
|
||||
icon: LucideIcon;
|
||||
/** Only for admins. */
|
||||
admin?: boolean;
|
||||
/** Also shown to non-admins in the desktop app. */
|
||||
desktopToo?: boolean;
|
||||
/** Left out of the desktop app. */
|
||||
browserOnly?: boolean;
|
||||
/** The profile section this page shows. */
|
||||
@@ -135,6 +137,7 @@ export const CORE_SETTINGS_PAGES: SettingsPage[] = [
|
||||
labelKey: "settings.updates.title",
|
||||
icon: FlaskConical,
|
||||
admin: true,
|
||||
desktopToo: true,
|
||||
}),
|
||||
core({
|
||||
id: "admin-users",
|
||||
@@ -225,7 +228,9 @@ export function visibleSettingsPages(
|
||||
{ isAdmin, isElectron }: { isAdmin: boolean; isElectron: boolean },
|
||||
): SettingsPage[] {
|
||||
return pages.filter(
|
||||
(page) => (!page.admin || isAdmin) && (!page.browserOnly || !isElectron),
|
||||
(page) =>
|
||||
(!page.admin || isAdmin || (page.desktopToo && isElectron)) &&
|
||||
(!page.browserOnly || !isElectron),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export const SETTINGS_SEARCH_INDEX: Record<string, string[]> = {
|
||||
"admin.apiKeyUser",
|
||||
"admin.apiKeysCliHint",
|
||||
"admin.apiKeysCliLink",
|
||||
"admin.apiKeysCount",
|
||||
"admin.apiKeysCount_other",
|
||||
"admin.apiKeysDocsLink",
|
||||
"admin.copiedToClipboard",
|
||||
"admin.createKey",
|
||||
@@ -162,7 +162,7 @@ export const SETTINGS_SEARCH_INDEX: Record<string, string[]> = {
|
||||
"admin.roleDescription",
|
||||
"admin.roleDisplayName",
|
||||
"admin.roleName",
|
||||
"admin.rolePermissions.count",
|
||||
"admin.rolePermissions.count_other",
|
||||
"admin.rolePermissions.editAction",
|
||||
"admin.rolePermissions.loadError",
|
||||
"admin.rolePermissions.pluginDisabled",
|
||||
@@ -170,7 +170,7 @@ export const SETTINGS_SEARCH_INDEX: Record<string, string[]> = {
|
||||
"admin.rolePermissions.saveError",
|
||||
"admin.rolePermissions.saved",
|
||||
"admin.rolePermissions.saving",
|
||||
"admin.rolesCount",
|
||||
"admin.rolesCount_other",
|
||||
"admin.sectionRoles",
|
||||
"admin.systemBadge",
|
||||
"common.cancel",
|
||||
@@ -187,7 +187,7 @@ export const SETTINGS_SEARCH_INDEX: Record<string, string[]> = {
|
||||
"admin.sectionSessions",
|
||||
"admin.sessionActive",
|
||||
"admin.sessionExpires",
|
||||
"admin.sessionsActive",
|
||||
"admin.sessionsActive_other",
|
||||
"admin.youBadge",
|
||||
],
|
||||
"admin-ssl": [
|
||||
@@ -282,7 +282,7 @@ export const SETTINGS_SEARCH_INDEX: Record<string, string[]> = {
|
||||
"admin.userAdminAccess",
|
||||
"admin.userAdminAccessDesc",
|
||||
"admin.userRoles",
|
||||
"admin.usersCount",
|
||||
"admin.usersCount_other",
|
||||
"common.cancel",
|
||||
"common.loading",
|
||||
"common.next",
|
||||
@@ -294,7 +294,7 @@ export const SETTINGS_SEARCH_INDEX: Record<string, string[]> = {
|
||||
"admin.revokeKey",
|
||||
"admin.revokeKeyConfirm",
|
||||
"newUi.sidebar.userProfile.apiKeyActive",
|
||||
"newUi.sidebar.userProfile.apiKeyCount",
|
||||
"newUi.sidebar.userProfile.apiKeyCount_other",
|
||||
"newUi.sidebar.userProfile.apiKeyCreateFailed",
|
||||
"newUi.sidebar.userProfile.apiKeyCreated",
|
||||
"newUi.sidebar.userProfile.apiKeyCreatedWarning",
|
||||
@@ -434,6 +434,24 @@ export const SETTINGS_SEARCH_INDEX: Record<string, string[]> = {
|
||||
"newUi.sidebar.userProfile.updatePassword",
|
||||
],
|
||||
"updates": [
|
||||
"desktopUpdates.checkNow",
|
||||
"desktopUpdates.download",
|
||||
"desktopUpdates.manualModeHint",
|
||||
"desktopUpdates.mode",
|
||||
"desktopUpdates.modeAutomatic",
|
||||
"desktopUpdates.modeHint",
|
||||
"desktopUpdates.modeNever",
|
||||
"desktopUpdates.modePrompt",
|
||||
"desktopUpdates.restart",
|
||||
"desktopUpdates.status",
|
||||
"desktopUpdates.statusAvailable",
|
||||
"desktopUpdates.statusChecking",
|
||||
"desktopUpdates.statusDownloading",
|
||||
"desktopUpdates.statusError",
|
||||
"desktopUpdates.statusIdle",
|
||||
"desktopUpdates.statusReady",
|
||||
"desktopUpdates.statusUpToDate",
|
||||
"desktopUpdates.title",
|
||||
"settings.updates.allPluginsBeta",
|
||||
"settings.updates.allPluginsStable",
|
||||
"settings.updates.backToStable",
|
||||
|
||||
Reference in new issue
Block a user