mirror of https://github.com/jetkvm/kvm.git
extract i18n strings
This commit is contained in:
parent
161c272099
commit
75ea98d396
|
|
@ -24,5 +24,6 @@
|
|||
],
|
||||
"url": "./internal/ota/testdata/ota.schema.json"
|
||||
}
|
||||
]
|
||||
],
|
||||
"typescript.tsdk": "./ui/node_modules/typescript/lib"
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
|
||||
import { Button } from "@components/Button";
|
||||
import { GridCard } from "@components/Card";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import { cx } from "@/cva.config";
|
||||
|
||||
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
|
||||
|
||||
import { Button } from "./Button";
|
||||
import { GridCard } from "./Card";
|
||||
import LoadingSpinner from "./LoadingSpinner";
|
||||
|
||||
export default function UpdateInProgressStatusCard() {
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
||||
|
|
@ -22,7 +21,7 @@ export default function UpdateInProgressStatusCard() {
|
|||
<div className="text-sm leading-none">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<span className={cx("transition")}>
|
||||
Please don{"'"}t turn off your device...
|
||||
Please don't turn off your device…
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
|
||||
export interface UpdatePart {
|
||||
pending: boolean;
|
||||
status: string;
|
||||
progress: number;
|
||||
complete: boolean;
|
||||
}
|
||||
|
||||
export default function UpdatingStatusCard({
|
||||
label,
|
||||
part,
|
||||
}: {
|
||||
label: string;
|
||||
part: UpdatePart;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-semibold text-black dark:text-white">{label}</p>
|
||||
{part.progress < 100 ? (
|
||||
<LoadingSpinner className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||
) : (
|
||||
<CheckCircleIcon className="h-4 w-4 text-blue-700 dark:text-blue-500" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300 dark:bg-slate-600"
|
||||
role="progressbar"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={Math.round(part.progress)}
|
||||
aria-label={`${label} progress`}
|
||||
>
|
||||
<div
|
||||
className="h-2.5 rounded-full bg-blue-700 transition-all duration-500 ease-linear dark:bg-blue-500"
|
||||
style={{ width: `${part.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
||||
<span>{part.status}</span>
|
||||
{part.progress < 100 ? <span>{`${Math.round(part.progress)}%`}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useLoaderData, useNavigate, type LoaderFunction } from "react-router";
|
||||
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
|
||||
import { GridCard } from "@components/Card";
|
||||
import { Button, LinkButton } from "@components/Button";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
|
|
@ -17,7 +17,6 @@ import api from "@/api";
|
|||
import notifications from "@/notifications";
|
||||
import { DEVICE_API } from "@/ui.config";
|
||||
import { isOnDevice } from "@/main";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
import { LocalDevice } from "./devices.$id";
|
||||
import { CloudState } from "./adopt";
|
||||
|
|
@ -93,7 +92,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
send("deregisterDevice", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
m.access_failed_deregister({ error: resp.error.data || m.unknown_error() }),
|
||||
`Failed to deregister device: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -108,14 +107,14 @@ export default function SettingsAccessIndexRoute() {
|
|||
const onCloudAdoptClick = useCallback(
|
||||
(cloudApiUrl: string, cloudAppUrl: string) => {
|
||||
if (!deviceId) {
|
||||
notifications.error(m.access_no_device_id());
|
||||
notifications.error("No device ID found");
|
||||
return;
|
||||
}
|
||||
|
||||
send("setCloudUrl", { apiUrl: cloudApiUrl, appUrl: cloudAppUrl }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
m.access_failed_update_cloud_url({ error: resp.error.data || m.unknown_error() }),
|
||||
`Failed to update cloud URL: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -161,12 +160,12 @@ export default function SettingsAccessIndexRoute() {
|
|||
send("setTLSState", { state }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
m.access_failed_update_tls({ error: resp.error.data || m.unknown_error() }),
|
||||
`Failed to update TLS state: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.success(m.access_tls_updated());
|
||||
notifications.success("TLS state updated successfully");
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
|
|
@ -207,22 +206,22 @@ export default function SettingsAccessIndexRoute() {
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title={m.access_title()}
|
||||
description={m.access_description()}
|
||||
title="Access"
|
||||
description="Manage the Access Control of the device"
|
||||
/>
|
||||
|
||||
{loaderData?.authMode && (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<SettingsSectionHeader
|
||||
title={m.access_local_title()}
|
||||
description={m.access_local_description()}
|
||||
title="Local"
|
||||
description="Manage the mode of local access to the device"
|
||||
/>
|
||||
<>
|
||||
<SettingsItem
|
||||
title={m.access_https_mode_title()}
|
||||
title="HTTPS Mode"
|
||||
badge="Experimental"
|
||||
description={m.access_https_description()}
|
||||
description="Configure secure HTTPS access to your device"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
|
|
@ -230,9 +229,9 @@ export default function SettingsAccessIndexRoute() {
|
|||
onChange={e => handleTlsModeChange(e.target.value)}
|
||||
disabled={tlsMode === "unknown"}
|
||||
options={[
|
||||
{ value: "disabled", label: m.access_tls_disabled() },
|
||||
{ value: "self-signed", label: m.access_tls_self_signed() },
|
||||
{ value: "custom", label: m.access_tls_custom() },
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
{ value: "self-signed", label: "Self-signed" },
|
||||
{ value: "custom", label: "Custom" },
|
||||
]}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
|
@ -240,11 +239,11 @@ export default function SettingsAccessIndexRoute() {
|
|||
{tlsMode === "custom" && (
|
||||
<NestedSettingsGroup className="mt-4">
|
||||
<SettingsItem
|
||||
title={m.access_tls_certificate_title()}
|
||||
description={m.access_tls_certificate_description()}
|
||||
title="TLS Certificate"
|
||||
description="Paste your TLS certificate below. For certificate chains, include the entire chain (leaf, intermediate, and root certificates)."
|
||||
/>
|
||||
<TextAreaWithLabel
|
||||
label={m.access_certificate_label()}
|
||||
label="Certificate"
|
||||
rows={3}
|
||||
placeholder={
|
||||
"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
|
|
@ -253,8 +252,8 @@ export default function SettingsAccessIndexRoute() {
|
|||
onChange={e => handleTlsCertChange(e.target.value)}
|
||||
/>
|
||||
<TextAreaWithLabel
|
||||
label={m.access_private_key_label()}
|
||||
description={m.access_private_key_description()}
|
||||
label="Private Key"
|
||||
description="For security reasons, it will not be displayed after saving."
|
||||
rows={3}
|
||||
placeholder={
|
||||
"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
|
||||
|
|
@ -266,7 +265,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text={m.access_update_tls_settings()}
|
||||
text="Update TLS Settings"
|
||||
onClick={handleCustomTlsUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -274,14 +273,14 @@ export default function SettingsAccessIndexRoute() {
|
|||
)}
|
||||
|
||||
<SettingsItem
|
||||
title={m.access_authentication_mode_title()}
|
||||
description={loaderData.authMode === "password" ? m.access_auth_mode_password() : m.access_auth_mode_no_password()}
|
||||
title="Authentication Mode"
|
||||
description={loaderData.authMode === "password" ? "Current Mode: Password Protected" : "Current Mode: No password"}
|
||||
>
|
||||
{loaderData.authMode === "password" ? (
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={m.access_disable_protection()}
|
||||
text="Disable Protection"
|
||||
onClick={() => {
|
||||
navigateTo("./local-auth", { state: { init: "deletePassword" } });
|
||||
}}
|
||||
|
|
@ -290,7 +289,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={m.access_enable_password()}
|
||||
text="Enable Password"
|
||||
onClick={() => {
|
||||
navigateTo("./local-auth", { state: { init: "createPassword" } });
|
||||
}}
|
||||
|
|
@ -301,13 +300,13 @@ export default function SettingsAccessIndexRoute() {
|
|||
|
||||
{loaderData.authMode === "password" && (
|
||||
<SettingsItem
|
||||
title={m.access_change_password_title()}
|
||||
description={m.access_change_password_description()}
|
||||
title="Change Password"
|
||||
description="Set a password to protect your device"
|
||||
>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={m.access_change_password_button()}
|
||||
text="Change Password"
|
||||
onClick={() => {
|
||||
navigateTo("./local-auth", { state: { init: "updatePassword" } });
|
||||
}}
|
||||
|
|
@ -322,23 +321,23 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="space-y-4">
|
||||
<SettingsSectionHeader
|
||||
title="Remote"
|
||||
description={m.access_remote_description()}
|
||||
description="Manage the mode of Remote access to the device"
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
{!isAdopted && (
|
||||
<>
|
||||
<SettingsItem
|
||||
title={m.access_cloud_provider_title()}
|
||||
description={m.access_cloud_provider_description()}
|
||||
title="Cloud Provider"
|
||||
description="Select the cloud provider for your device"
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={selectedProvider}
|
||||
onChange={e => handleProviderChange(e.target.value)}
|
||||
options={[
|
||||
{ value: "jetkvm", label: m.access_provider_jetkvm() },
|
||||
{ value: "custom", label: m.access_provider_custom() },
|
||||
{ value: "jetkvm", label: "JetKVM" },
|
||||
{ value: "custom", label: "Custom" },
|
||||
]}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
|
@ -348,7 +347,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="flex items-end gap-x-2">
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
label={m.access_cloud_api_url_label()}
|
||||
label="API URL"
|
||||
value={cloudApiUrl}
|
||||
onChange={e => setCloudApiUrl(e.target.value)}
|
||||
placeholder="https://api.example.com"
|
||||
|
|
@ -357,7 +356,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="flex items-end gap-x-2">
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
label={m.access_cloud_app_url_label()}
|
||||
label="App URL"
|
||||
value={cloudAppUrl}
|
||||
onChange={e => setCloudAppUrl(e.target.value)}
|
||||
placeholder="https://app.example.com"
|
||||
|
|
@ -376,26 +375,27 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
{m.access_cloud_security_title()}
|
||||
Cloud Security
|
||||
</h3>
|
||||
<div>
|
||||
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
|
||||
<li>{m.access_security_encryption()}</li>
|
||||
<li>{m.access_security_zero_trust()}</li>
|
||||
<li>{m.access_security_oidc()}</li>
|
||||
<li>{m.access_security_streams()}</li>
|
||||
<li>End-to-end encryption using WebRTC (DTLS and SRTP)</li>
|
||||
<li>OIDC (OpenID Connect) authentication</li>
|
||||
<li>All cloud components are open-source and available on GitHub.</li>
|
||||
<li>All streams encrypted in transit</li>
|
||||
<li>Zero Trust security model</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-slate-700 dark:text-slate-300">
|
||||
{m.access_security_open_source()}{" "}
|
||||
All cloud components are open-source and available on GitHub.{" "}
|
||||
<a
|
||||
href="https://github.com/jetkvm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400"
|
||||
>
|
||||
{m.access_github_link()}
|
||||
GitHub repository
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
|
|
@ -407,7 +407,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
to="https://jetkvm.com/docs/networking/remote-access"
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={m.access_learn_security()}
|
||||
text="Learn more"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -421,32 +421,32 @@ export default function SettingsAccessIndexRoute() {
|
|||
onClick={() => onCloudAdoptClick(cloudApiUrl, cloudAppUrl)}
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text={m.access_adopt_kvm()}
|
||||
text="Adopt KVM to Cloud"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
{m.access_adopted_message()}
|
||||
Your device is adopted to the Cloud. You can deregister it at any time.
|
||||
</p>
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={m.access_deregister()}
|
||||
text="De-register from Cloud"
|
||||
className="text-red-600"
|
||||
onClick={() => {
|
||||
if (deviceId) {
|
||||
if (
|
||||
window.confirm(
|
||||
m.access_confirm_deregister(),
|
||||
"Are you sure you want to de-register this device?",
|
||||
)
|
||||
) {
|
||||
deregisterDevice();
|
||||
}
|
||||
} else {
|
||||
notifications.error(m.access_no_device_id());
|
||||
notifications.error("No device ID available");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
@ -460,4 +460,4 @@ export default function SettingsAccessIndexRoute() {
|
|||
);
|
||||
}
|
||||
|
||||
SettingsAccessIndexRoute.loader = loader;
|
||||
SettingsAccessIndexRoute.loader = loader;
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { useSettingsStore } from "@hooks/stores";
|
||||
import { JsonRpcError, JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { SystemVersionInfo } from "@hooks/useVersion";
|
||||
|
||||
import { Button } from "@components/Button";
|
||||
import Checkbox, { CheckboxWithLabel } from "@components/Checkbox";
|
||||
import { ConfirmDialog } from "@components/ConfirmDialog";
|
||||
|
|
@ -15,10 +16,8 @@ import { InputFieldWithLabel } from "@components/InputField";
|
|||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { isOnDevice } from "@/main";
|
||||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages.js";
|
||||
import { sleep } from "@/utils";
|
||||
import { checkUpdateComponents, UpdateComponents } from "@/utils/jsonrpc";
|
||||
import { SystemVersionInfo } from "@hooks/useVersion";
|
||||
|
||||
import { FeatureFlag } from "../components/FeatureFlag";
|
||||
|
||||
|
|
@ -79,8 +78,11 @@ export default function SettingsAdvancedRoute() {
|
|||
(enabled: boolean) => {
|
||||
send("setUsbEmulationState", { enabled: enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
const errorMsg = resp.error.data || "Unknown error";
|
||||
notifications.error(
|
||||
`Failed to ${enabled ? "enable" : "disable"} USB emulation: ${resp.error.data || "Unknown error"}`,
|
||||
enabled
|
||||
? `Failed to enable USB emulation: ${errorMsg}`
|
||||
: `Failed to disable USB emulation: ${errorMsg}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -95,7 +97,7 @@ export default function SettingsAdvancedRoute() {
|
|||
send("resetConfig", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to reset configuration: ${resp.error.data || "Unknown error"}`,
|
||||
`Failed to reset configuration: ${resp.error.data || "Unknown error"}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -107,7 +109,7 @@ export default function SettingsAdvancedRoute() {
|
|||
send("setSSHKeyState", { sshKey }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to update SSH key: ${resp.error.data || "Unknown error"}`,
|
||||
`Failed to update SSH key: ${resp.error.data || "Unknown error"}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -120,7 +122,7 @@ export default function SettingsAdvancedRoute() {
|
|||
send("setDevModeState", { enabled: developerMode }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set dev mode: ${resp.error.data || "Unknown error"}`,
|
||||
`Failed to set dev mode: ${resp.error.data || "Unknown error"}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -135,7 +137,7 @@ export default function SettingsAdvancedRoute() {
|
|||
send("setDevChannelState", { enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set dev channel state: ${resp.error.data || "Unknown error"}`,
|
||||
`Failed to set dev channel state: ${resp.error.data || "Unknown error"}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -149,20 +151,19 @@ export default function SettingsAdvancedRoute() {
|
|||
(enabled: boolean) => {
|
||||
send("setLocalLoopbackOnly", { enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
const errorMsg = resp.error.data || "Unknown error";
|
||||
notifications.error(
|
||||
`Failed to ${enabled ? "enable" : "disable"} loopback-only mode: ${resp.error.data || "Unknown error"}`,
|
||||
enabled
|
||||
? `Failed to enable loopback-only mode: ${errorMsg}`
|
||||
: `Failed to disable loopback-only mode: ${errorMsg}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
setLocalLoopbackOnly(enabled);
|
||||
if (enabled) {
|
||||
notifications.success(
|
||||
"Loopback-only mode enabled. Restart your device to apply.",
|
||||
);
|
||||
notifications.success("Loopback-only mode enabled. Restart your device to apply.");
|
||||
} else {
|
||||
notifications.success(
|
||||
"Loopback-only mode disabled. Restart your device to apply.",
|
||||
);
|
||||
notifications.success("Loopback-only mode disabled. Restart your device to apply.");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -188,10 +189,9 @@ export default function SettingsAdvancedRoute() {
|
|||
}, [applyLoopbackOnlyMode, setShowLoopbackWarning]);
|
||||
|
||||
const handleVersionUpdateError = useCallback((error?: JsonRpcError | string) => {
|
||||
const errorMessage = typeof error === "string" ? error : (error?.data ?? error?.message ?? "Unknown error");
|
||||
notifications.error(
|
||||
m.advanced_error_version_update({
|
||||
error: typeof error === "string" ? error : (error?.data ?? error?.message ?? m.unknown_error())
|
||||
}),
|
||||
`Failed to initiate version update: ${errorMessage}`,
|
||||
{ duration: 1000 * 15 } // 15 seconds
|
||||
);
|
||||
setCustomVersionUpdateLoading(false);
|
||||
|
|
@ -289,17 +289,17 @@ export default function SettingsAdvancedRoute() {
|
|||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
{m.advanced_developer_mode_enabled_title()}
|
||||
Developer Mode Enabled
|
||||
</h3>
|
||||
<div>
|
||||
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
|
||||
<li>{m.advanced_developer_mode_warning_security()}</li>
|
||||
<li>{m.advanced_developer_mode_warning_risks()}</li>
|
||||
<li>Security is weakened while active</li>
|
||||
<li>Only use if you understand the risks</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-slate-700 dark:text-slate-300">
|
||||
{m.advanced_developer_mode_warning_advanced()}
|
||||
For advanced users only. Not for production use.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -308,24 +308,24 @@ export default function SettingsAdvancedRoute() {
|
|||
{isOnDevice && (
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title={m.advanced_ssh_access_title()}
|
||||
description={m.advanced_ssh_access_description()}
|
||||
title="SSH Access"
|
||||
description="Add your SSH public key to enable secure remote access to the device"
|
||||
/>
|
||||
<TextAreaWithLabel
|
||||
label={m.advanced_ssh_public_key_label()}
|
||||
label="SSH Public Key"
|
||||
value={sshKey || ""}
|
||||
rows={3}
|
||||
onChange={e => setSSHKey(e.target.value)}
|
||||
placeholder={m.advanced_ssh_public_key_placeholder()}
|
||||
placeholder="Enter your SSH public key"
|
||||
/>
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
{m.advanced_ssh_default_user()}<strong>root</strong>.
|
||||
The default SSH user is<strong>root</strong>.
|
||||
</p>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text={m.advanced_update_ssh_key_button()}
|
||||
text="Update SSH Key"
|
||||
onClick={handleUpdateSSHKey}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -335,16 +335,16 @@ export default function SettingsAdvancedRoute() {
|
|||
<FeatureFlag minAppVersion="0.4.10" name="version-update">
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title={m.advanced_version_update_title()}
|
||||
description={m.advanced_version_update_description()}
|
||||
title="Update to Specific Version"
|
||||
description="Install a specific version from GitHub releases"
|
||||
/>
|
||||
|
||||
<SelectMenuBasic
|
||||
label={m.advanced_version_update_target_label()}
|
||||
label="What to update"
|
||||
options={[
|
||||
{ value: "app", label: m.advanced_version_update_target_app() },
|
||||
{ value: "system", label: m.advanced_version_update_target_system() },
|
||||
{ value: "both", label: m.advanced_version_update_target_both() },
|
||||
{ value: "app", label: "App only" },
|
||||
{ value: "system", label: "System only" },
|
||||
{ value: "both", label: "Both App and System" },
|
||||
]}
|
||||
value={updateTarget}
|
||||
onChange={e => setUpdateTarget(e.target.value)}
|
||||
|
|
@ -352,7 +352,7 @@ export default function SettingsAdvancedRoute() {
|
|||
|
||||
{(updateTarget === "app" || updateTarget === "both") && (
|
||||
<InputFieldWithLabel
|
||||
label={m.advanced_version_update_app_label()}
|
||||
label="App Version"
|
||||
placeholder="0.4.9"
|
||||
value={appVersion}
|
||||
onChange={e => setAppVersion(e.target.value)}
|
||||
|
|
@ -361,7 +361,7 @@ export default function SettingsAdvancedRoute() {
|
|||
|
||||
{(updateTarget === "system" || updateTarget === "both") && (
|
||||
<InputFieldWithLabel
|
||||
label={m.advanced_version_update_system_label()}
|
||||
label="System Version"
|
||||
placeholder="0.4.9"
|
||||
value={systemVersion}
|
||||
onChange={e => setSystemVersion(e.target.value)}
|
||||
|
|
@ -369,21 +369,21 @@ export default function SettingsAdvancedRoute() {
|
|||
)}
|
||||
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
{m.advanced_version_update_helper()}{" "}
|
||||
Find available versions on the{" "}
|
||||
<a
|
||||
href="https://github.com/jetkvm/kvm/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-blue-700 hover:underline dark:text-blue-500"
|
||||
>
|
||||
{m.advanced_version_update_github_link()}
|
||||
JetKVM releases page
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<CheckboxWithLabel
|
||||
label={m.advanced_version_update_reset_config_label()}
|
||||
description={m.advanced_version_update_reset_config_description()}
|
||||
label="Reset configuration"
|
||||
description="Reset configuration after the update"
|
||||
checked={resetConfig}
|
||||
onChange={e => setResetConfig(e.target.checked)}
|
||||
/>
|
||||
|
|
@ -400,7 +400,7 @@ export default function SettingsAdvancedRoute() {
|
|||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text={m.advanced_version_update_button()}
|
||||
text="Update to Version"
|
||||
disabled={
|
||||
(updateTarget === "app" && !appVersion) ||
|
||||
(updateTarget === "system" && !systemVersion) ||
|
||||
|
|
@ -417,8 +417,8 @@ export default function SettingsAdvancedRoute() {
|
|||
) : null}
|
||||
|
||||
<SettingsItem
|
||||
title={m.advanced_loopback_only_title()}
|
||||
description={m.advanced_loopback_only_description()}
|
||||
title="Loopback-Only Mode"
|
||||
description="Restrict web interface access to localhost only (127.0.0.1)"
|
||||
>
|
||||
<Checkbox
|
||||
checked={localLoopbackOnly}
|
||||
|
|
@ -464,8 +464,10 @@ export default function SettingsAdvancedRoute() {
|
|||
size="SM"
|
||||
theme="light"
|
||||
text="Reset Config"
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
handleResetConfig();
|
||||
// Add 2s delay between resetting the configuration and calling reload() to prevent reload from interrupting the RPC call to reset things.
|
||||
await sleep(2000);
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
|
|
@ -499,4 +501,4 @@ export default function SettingsAdvancedRoute() {
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useNavigate } from "react-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useJsonRpc } from "@hooks/useJsonRpc";
|
||||
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { Button } from "@components/Button";
|
||||
import { useFailsafeModeStore } from "@/hooks/stores";
|
||||
import { sleep } from "@/utils";
|
||||
|
|
@ -29,29 +29,25 @@ export default function SettingsGeneralRebootRoute() {
|
|||
|
||||
const onConfirmUpdate = useCallback(async () => {
|
||||
setIsRebooting(true);
|
||||
// This is where we send the RPC to the golang binary
|
||||
send("reboot", { force: true });
|
||||
send("reboot", { force: true });
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, REBOOT_REDIRECT_DELAY_MS));
|
||||
setFailsafeMode(false, "");
|
||||
navigateTo("/");
|
||||
}, [navigateTo, send, setFailsafeMode]);
|
||||
|
||||
{
|
||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||
}
|
||||
return <Dialog isRebooting={isRebooting} onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||
return <Dialog isRebooting={isRebooting} onClose={onClose} onConfirmUpdate={onConfirmUpdate} />;
|
||||
}
|
||||
|
||||
export function Dialog({
|
||||
isRebooting,
|
||||
onClose,
|
||||
onConfirmUpdate,
|
||||
}: {
|
||||
}: Readonly<{
|
||||
isRebooting: boolean;
|
||||
onClose: () => void;
|
||||
onConfirmUpdate: () => void;
|
||||
}) {
|
||||
}>) {
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto relative mx-auto text-left">
|
||||
|
|
@ -97,4 +93,4 @@ function ConfirmationBox({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,14 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router";
|
||||
|
||||
import { useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { UpdateState, useUpdateStore } from "@hooks/stores";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { useVersion } from "@hooks/useVersion";
|
||||
import UpdatingStatusCard, { type UpdatePart } from "@components/UpdatingStatusCard";
|
||||
|
||||
import { Button } from "@components/Button";
|
||||
import Card from "@components/Card";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import UpdatingStatusCard, { type UpdatePart } from "@components/UpdatingStatusCard";
|
||||
import { m } from "@localizations/messages.js";
|
||||
import { sleep } from "@/utils";
|
||||
import { checkUpdateComponents, SystemVersionInfo, UpdateComponents, updateParams } from "@/utils/jsonrpc";
|
||||
|
||||
|
|
@ -236,10 +235,10 @@ function LoadingState({
|
|||
<div className="space-y-4">
|
||||
<div className="space-y-0">
|
||||
<p className="text-base font-semibold text-black dark:text-white">
|
||||
Checking for updates...
|
||||
Checking for updates…
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
We{"'"}re ensuring your device has the latest features and improvements.
|
||||
We're ensuring your device has the latest features and improvements.
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300">
|
||||
|
|
@ -302,17 +301,18 @@ function UpdatingDeviceState({
|
|||
const verifiedAt = otaState[`${type}VerifiedAt`];
|
||||
const updatedAt = otaState[`${type}UpdatedAt`];
|
||||
|
||||
const update_type = () => (type === "system" ? "System" : "App");
|
||||
|
||||
if (!otaState.metadataFetchedAt) {
|
||||
return "Fetching update information...";
|
||||
return "Fetching update information…";
|
||||
} else if (!downloadFinishedAt) {
|
||||
return `Downloading ${type} update...`;
|
||||
return `Downloading ${update_type()} update…`;
|
||||
} else if (!verifiedAt) {
|
||||
return `Verifying ${type} update...`;
|
||||
return `Verifying ${update_type()} update…`;
|
||||
} else if (!updatedAt) {
|
||||
return `Installing ${type} update...`;
|
||||
return `Installing ${update_type()} update…`;
|
||||
} else {
|
||||
return `Awaiting reboot`;
|
||||
return "Awaiting reboot";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -352,7 +352,6 @@ function UpdatingDeviceState({
|
|||
};
|
||||
}, [otaState]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="w-full max-w-sm space-y-4">
|
||||
|
|
@ -361,7 +360,7 @@ function UpdatingDeviceState({
|
|||
Updating your device
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
Please don{"'"}t turn off your device. This process may take a few minutes.
|
||||
Please don't turn off your device. This process may take a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
<Card className="space-y-4 p-4">
|
||||
|
|
@ -370,7 +369,7 @@ function UpdatingDeviceState({
|
|||
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
||||
<span className="font-medium text-black dark:text-white">
|
||||
Rebooting to complete the update...
|
||||
Rebooting to complete the update…
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -449,27 +448,26 @@ function UpdateAvailableState({
|
|||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="text-left">
|
||||
<p className="text-base font-semibold text-black dark:text-white">
|
||||
Update Available
|
||||
Update available
|
||||
</p>
|
||||
<p className="mb-2 text-sm text-slate-600 dark:text-slate-300">
|
||||
A new update is available to enhance system performance and improve
|
||||
compatibility. We recommend updating to ensure everything runs smoothly.
|
||||
A new update is available to enhance system performance and improve compatibility. We recommend updating to ensure everything runs smoothly.
|
||||
</p>
|
||||
<p className="mb-4 text-sm text-slate-600 dark:text-slate-300">
|
||||
{versionInfo?.systemUpdateAvailable ? (
|
||||
<>
|
||||
<span className="font-semibold">Linux System Update</span>: {versionInfo?.local?.systemVersion} <span className="text-slate-600 dark:text-slate-300">→</span> {versionInfo?.remote?.systemVersion}
|
||||
<span className="font-semibold">System</span>: {versionInfo?.local?.systemVersion} <span className="text-slate-600 dark:text-slate-300">→</span> {versionInfo?.remote?.systemVersion}
|
||||
<br />
|
||||
</>
|
||||
) : null}
|
||||
{versionInfo?.appUpdateAvailable ? (
|
||||
<>
|
||||
<span className="font-semibold">App Update</span>: {versionInfo?.local?.appVersion} <span className="text-slate-600 dark:text-slate-300">→</span> {versionInfo?.remote?.appVersion}
|
||||
<span className="font-semibold">App</span>: {versionInfo?.local?.appVersion} <span className="text-slate-600 dark:text-slate-300">→</span> {versionInfo?.remote?.appVersion}
|
||||
</>
|
||||
) : null}
|
||||
{versionInfo?.willDisableAutoUpdate ? (
|
||||
<p className="mb-4 text-sm text-red-600 dark:text-red-400">
|
||||
You{"'"}re about to manually change your device version. Auto-update will be disabled after the update is completed to prevent accidental updates.
|
||||
You're about to manually change your device version. Auto-update will be disabled after the update is completed to prevent accidental updates.
|
||||
</p>
|
||||
) : null}
|
||||
</p>
|
||||
|
|
@ -490,8 +488,7 @@ function UpdateCompletedState({ onClose }: { onClose: () => void }) {
|
|||
Update Completed Successfully
|
||||
</p>
|
||||
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
|
||||
Your device has been successfully updated to the latest version. Enjoy the new
|
||||
features and improvements!
|
||||
Your device has been successfully updated to the latest version. Enjoy the new features and improvements!
|
||||
</p>
|
||||
<div className="flex items-center justify-start">
|
||||
<Button size="SM" theme="primary" text="Back" onClick={onClose} />
|
||||
|
|
@ -529,4 +526,4 @@ function UpdateErrorState({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,502 +1,237 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useSettingsStore } from "@hooks/stores";
|
||||
import { JsonRpcError, JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { SystemVersionInfo } from "@hooks/useVersion";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BacklightSettings, useSettingsStore } from "@hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
|
||||
import { Button } from "@components/Button";
|
||||
import Checkbox, { CheckboxWithLabel } from "@components/Checkbox";
|
||||
import { ConfirmDialog } from "@components/ConfirmDialog";
|
||||
import { GridCard } from "@components/Card";
|
||||
import { Checkbox } from "@components/Checkbox";
|
||||
import { FeatureFlag } from "@components/FeatureFlag";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { SettingsItem } from "@components/SettingsItem";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
|
||||
import { NestedSettingsGroup } from "@components/NestedSettingsGroup";
|
||||
import { TextAreaWithLabel } from "@components/TextArea";
|
||||
import { InputFieldWithLabel } from "@components/InputField";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { isOnDevice } from "@/main";
|
||||
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
|
||||
import { UsbInfoSetting } from "@components/UsbInfoSetting";
|
||||
import notifications from "@/notifications";
|
||||
import { sleep } from "@/utils";
|
||||
import { checkUpdateComponents, UpdateComponents } from "@/utils/jsonrpc";
|
||||
|
||||
|
||||
import { FeatureFlag } from "../components/FeatureFlag";
|
||||
|
||||
export default function SettingsAdvancedRoute() {
|
||||
export default function SettingsHardwareRoute() {
|
||||
const { send } = useJsonRpc();
|
||||
const { navigateTo } = useDeviceUiNavigation();
|
||||
|
||||
const [sshKey, setSSHKey] = useState<string>("");
|
||||
const { setDeveloperMode } = useSettingsStore();
|
||||
const [devChannel, setDevChannel] = useState(false);
|
||||
const [usbEmulationEnabled, setUsbEmulationEnabled] = useState(false);
|
||||
const [showLoopbackWarning, setShowLoopbackWarning] = useState(false);
|
||||
const [localLoopbackOnly, setLocalLoopbackOnly] = useState(false);
|
||||
const [updateTarget, setUpdateTarget] = useState<string>("app");
|
||||
const [appVersion, setAppVersion] = useState<string>("");
|
||||
const [systemVersion, setSystemVersion] = useState<string>("");
|
||||
const [resetConfig, setResetConfig] = useState(false);
|
||||
const [versionChangeAcknowledged, setVersionChangeAcknowledged] = useState(false);
|
||||
const [customVersionUpdateLoading, setCustomVersionUpdateLoading] = useState(false);
|
||||
const settings = useSettingsStore();
|
||||
const { displayRotation, setDisplayRotation } = useSettingsStore();
|
||||
const [powerSavingEnabled, setPowerSavingEnabled] = useState(false);
|
||||
|
||||
const handleDisplayRotationChange = (rotation: string) => {
|
||||
setDisplayRotation(rotation);
|
||||
handleDisplayRotationSave();
|
||||
};
|
||||
|
||||
const handleDisplayRotationSave = () => {
|
||||
send("setDisplayRotation", { params: { rotation: displayRotation } }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set display orientation: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("Display orientation updated successfully");
|
||||
});
|
||||
};
|
||||
|
||||
const { backlightSettings, setBacklightSettings } = useSettingsStore();
|
||||
|
||||
const handleBacklightSettingsChange = (settings: BacklightSettings) => {
|
||||
// If the user has set the display to dim after it turns off, set the dim_after
|
||||
// value to never.
|
||||
if (settings.dim_after > settings.off_after && settings.off_after != 0) {
|
||||
settings.dim_after = 0;
|
||||
}
|
||||
|
||||
setBacklightSettings(settings);
|
||||
handleBacklightSettingsSave(settings);
|
||||
};
|
||||
|
||||
const handleBacklightSettingsSave = (backlightSettings: BacklightSettings) => {
|
||||
send("setBacklightSettings", { params: backlightSettings }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set backlight settings: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("Backlight settings updated successfully");
|
||||
});
|
||||
};
|
||||
|
||||
const handleBacklightMaxBrightnessChange = (max_brightness: number) => {
|
||||
const settings = { ...backlightSettings, max_brightness };
|
||||
handleBacklightSettingsChange(settings);
|
||||
};
|
||||
|
||||
const handleBacklightDimAfterChange = (dim_after: number) => {
|
||||
const settings = { ...backlightSettings, dim_after };
|
||||
handleBacklightSettingsChange(settings);
|
||||
};
|
||||
|
||||
const handleBacklightOffAfterChange = (off_after: number) => {
|
||||
const settings = { ...backlightSettings, off_after };
|
||||
handleBacklightSettingsChange(settings);
|
||||
};
|
||||
|
||||
const handlePowerSavingChange = (enabled: boolean) => {
|
||||
setPowerSavingEnabled(enabled);
|
||||
const duration = enabled ? 90 : -1;
|
||||
send("setVideoSleepMode", { duration }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(`Failed to set power saving mode: ${resp.error.data || "Unknown error"}`);
|
||||
setPowerSavingEnabled(!enabled); // Attempt to revert on error
|
||||
return;
|
||||
}
|
||||
notifications.success(enabled ? 'Power saving mode enabled' : 'Power saving mode disabled');
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
send("getDevModeState", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
const result = resp.result as { enabled: boolean };
|
||||
setDeveloperMode(result.enabled);
|
||||
});
|
||||
|
||||
send("getSSHKeyState", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
setSSHKey(resp.result as string);
|
||||
});
|
||||
|
||||
send("getUsbEmulationState", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
setUsbEmulationEnabled(resp.result as boolean);
|
||||
});
|
||||
|
||||
send("getDevChannelState", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
setDevChannel(resp.result as boolean);
|
||||
});
|
||||
|
||||
send("getLocalLoopbackOnly", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
setLocalLoopbackOnly(resp.result as boolean);
|
||||
});
|
||||
}, [send, setDeveloperMode]);
|
||||
|
||||
const getUsbEmulationState = useCallback(() => {
|
||||
send("getUsbEmulationState", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
setUsbEmulationEnabled(resp.result as boolean);
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const handleUsbEmulationToggle = useCallback(
|
||||
(enabled: boolean) => {
|
||||
send("setUsbEmulationState", { enabled: enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to ${enabled ? "enable" : "disable"} USB emulation: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setUsbEmulationEnabled(enabled);
|
||||
getUsbEmulationState();
|
||||
});
|
||||
},
|
||||
[getUsbEmulationState, send],
|
||||
);
|
||||
|
||||
const handleResetConfig = useCallback(() => {
|
||||
send("resetConfig", {}, (resp: JsonRpcResponse) => {
|
||||
send("getBacklightSettings", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to reset configuration: ${resp.error.data || "Unknown error"}`,
|
||||
return notifications.error(
|
||||
`Failed to get backlight settings: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
const result = resp.result as BacklightSettings;
|
||||
setBacklightSettings(result);
|
||||
});
|
||||
}, [send, setBacklightSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
send("getVideoSleepMode", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
console.error("Failed to get power saving mode:", resp.error);
|
||||
return;
|
||||
}
|
||||
notifications.success("Configuration reset to default successfully");
|
||||
const result = resp.result as { enabled: boolean; duration: number };
|
||||
setPowerSavingEnabled(result.duration >= 0);
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
const handleUpdateSSHKey = useCallback(() => {
|
||||
send("setSSHKeyState", { sshKey }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to update SSH key: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("SSH key updated successfully");
|
||||
});
|
||||
}, [send, sshKey]);
|
||||
|
||||
const handleDevModeChange = useCallback(
|
||||
(developerMode: boolean) => {
|
||||
send("setDevModeState", { enabled: developerMode }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set dev mode: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setDeveloperMode(developerMode);
|
||||
});
|
||||
},
|
||||
[send, setDeveloperMode],
|
||||
);
|
||||
|
||||
const handleDevChannelChange = useCallback(
|
||||
(enabled: boolean) => {
|
||||
send("setDevChannelState", { enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set dev channel state: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setDevChannel(enabled);
|
||||
});
|
||||
},
|
||||
[send, setDevChannel],
|
||||
);
|
||||
|
||||
const applyLoopbackOnlyMode = useCallback(
|
||||
(enabled: boolean) => {
|
||||
send("setLocalLoopbackOnly", { enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to ${enabled ? "enable" : "disable"} loopback-only mode: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setLocalLoopbackOnly(enabled);
|
||||
if (enabled) {
|
||||
notifications.success(
|
||||
"Loopback-only mode enabled. Restart your device to apply.",
|
||||
);
|
||||
} else {
|
||||
notifications.success(
|
||||
"Loopback-only mode disabled. Restart your device to apply.",
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
[send, setLocalLoopbackOnly],
|
||||
);
|
||||
|
||||
const handleLoopbackOnlyModeChange = useCallback(
|
||||
(enabled: boolean) => {
|
||||
// If trying to enable loopback-only mode, show warning first
|
||||
if (enabled) {
|
||||
setShowLoopbackWarning(true);
|
||||
} else {
|
||||
// If disabling, just proceed
|
||||
applyLoopbackOnlyMode(false);
|
||||
}
|
||||
},
|
||||
[applyLoopbackOnlyMode, setShowLoopbackWarning],
|
||||
);
|
||||
|
||||
const confirmLoopbackModeEnable = useCallback(() => {
|
||||
applyLoopbackOnlyMode(true);
|
||||
setShowLoopbackWarning(false);
|
||||
}, [applyLoopbackOnlyMode, setShowLoopbackWarning]);
|
||||
|
||||
const handleVersionUpdateError = useCallback((error?: JsonRpcError | string) => {
|
||||
notifications.error(
|
||||
m.advanced_error_version_update({
|
||||
error: typeof error === "string" ? error : (error?.data ?? error?.message ?? m.unknown_error())
|
||||
}),
|
||||
{ duration: 1000 * 15 } // 15 seconds
|
||||
);
|
||||
setCustomVersionUpdateLoading(false);
|
||||
}, []);
|
||||
|
||||
const handleCustomVersionUpdate = useCallback(async () => {
|
||||
const components: UpdateComponents = {};
|
||||
if (["app", "both"].includes(updateTarget) && appVersion) components.app = appVersion;
|
||||
if (["system", "both"].includes(updateTarget) && systemVersion) components.system = systemVersion;
|
||||
let versionInfo: SystemVersionInfo | undefined;
|
||||
|
||||
try {
|
||||
// we do not need to set it to false if check succeeds,
|
||||
// because it will be redirected to the update page later
|
||||
setCustomVersionUpdateLoading(true);
|
||||
versionInfo = await checkUpdateComponents({
|
||||
components,
|
||||
}, devChannel);
|
||||
} catch (error: unknown) {
|
||||
const jsonRpcError = error as JsonRpcError;
|
||||
handleVersionUpdateError(jsonRpcError);
|
||||
return;
|
||||
}
|
||||
|
||||
let hasUpdate = false;
|
||||
|
||||
const pageParams = new URLSearchParams();
|
||||
if (components.app && versionInfo?.remote?.appVersion && versionInfo?.appUpdateAvailable) {
|
||||
hasUpdate = true;
|
||||
pageParams.set("custom_app_version", versionInfo.remote?.appVersion);
|
||||
}
|
||||
if (components.system && versionInfo?.remote?.systemVersion && versionInfo?.systemUpdateAvailable) {
|
||||
hasUpdate = true;
|
||||
pageParams.set("custom_system_version", versionInfo.remote?.systemVersion);
|
||||
}
|
||||
pageParams.set("reset_config", resetConfig.toString());
|
||||
|
||||
if (!hasUpdate) {
|
||||
handleVersionUpdateError("No update available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to update page
|
||||
navigateTo(`/settings/general/update?${pageParams.toString()}`);
|
||||
}, [
|
||||
updateTarget, appVersion, systemVersion, devChannel,
|
||||
navigateTo, resetConfig, handleVersionUpdateError,
|
||||
setCustomVersionUpdateLoading
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Advanced"
|
||||
description="Access additional settings for troubleshooting and customization"
|
||||
title="Hardware"
|
||||
description={"Configure display settings and hardware options for your JetKVM device"}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Dev Channel Updates"
|
||||
description="Receive early updates from the development channel"
|
||||
title="Display Orientation"
|
||||
description="Set the orientation of the display"
|
||||
>
|
||||
<Checkbox
|
||||
checked={devChannel}
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.displayRotation.toString()}
|
||||
options={[
|
||||
{ value: "270", label: "Normal" },
|
||||
{ value: "90", label: "Inverted" },
|
||||
]}
|
||||
onChange={e => {
|
||||
handleDevChannelChange(e.target.checked);
|
||||
handleDisplayRotationChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
title="Developer Mode"
|
||||
description="Enable advanced features for developers"
|
||||
title="Display Brightness"
|
||||
description="Set the brightness of the display"
|
||||
>
|
||||
<Checkbox
|
||||
checked={settings.developerMode}
|
||||
onChange={e => handleDevModeChange(e.target.checked)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
{settings.developerMode ? (
|
||||
<NestedSettingsGroup>
|
||||
<GridCard>
|
||||
<div className="flex items-start gap-x-4 p-4 select-none">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="mt-1 h-8 w-8 shrink-0 text-amber-600 dark:text-amber-500"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
{m.advanced_developer_mode_enabled_title()}
|
||||
</h3>
|
||||
<div>
|
||||
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
|
||||
<li>{m.advanced_developer_mode_warning_security()}</li>
|
||||
<li>{m.advanced_developer_mode_warning_risks()}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-slate-700 dark:text-slate-300">
|
||||
{m.advanced_developer_mode_warning_advanced()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
|
||||
{isOnDevice && (
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title={m.advanced_ssh_access_title()}
|
||||
description={m.advanced_ssh_access_description()}
|
||||
/>
|
||||
<TextAreaWithLabel
|
||||
label={m.advanced_ssh_public_key_label()}
|
||||
value={sshKey || ""}
|
||||
rows={3}
|
||||
onChange={e => setSSHKey(e.target.value)}
|
||||
placeholder={m.advanced_ssh_public_key_placeholder()}
|
||||
/>
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
{m.advanced_ssh_default_user()}<strong>root</strong>.
|
||||
</p>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text={m.advanced_update_ssh_key_button()}
|
||||
onClick={handleUpdateSSHKey}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FeatureFlag minAppVersion="0.4.10" name="version-update">
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title={m.advanced_version_update_title()}
|
||||
description={m.advanced_version_update_description()}
|
||||
/>
|
||||
|
||||
<SelectMenuBasic
|
||||
label={m.advanced_version_update_target_label()}
|
||||
options={[
|
||||
{ value: "app", label: m.advanced_version_update_target_app() },
|
||||
{ value: "system", label: m.advanced_version_update_target_system() },
|
||||
{ value: "both", label: m.advanced_version_update_target_both() },
|
||||
]}
|
||||
value={updateTarget}
|
||||
onChange={e => setUpdateTarget(e.target.value)}
|
||||
/>
|
||||
|
||||
{(updateTarget === "app" || updateTarget === "both") && (
|
||||
<InputFieldWithLabel
|
||||
label={m.advanced_version_update_app_label()}
|
||||
placeholder="0.4.9"
|
||||
value={appVersion}
|
||||
onChange={e => setAppVersion(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(updateTarget === "system" || updateTarget === "both") && (
|
||||
<InputFieldWithLabel
|
||||
label={m.advanced_version_update_system_label()}
|
||||
placeholder="0.4.9"
|
||||
value={systemVersion}
|
||||
onChange={e => setSystemVersion(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
{m.advanced_version_update_helper()}{" "}
|
||||
<a
|
||||
href="https://github.com/jetkvm/kvm/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-blue-700 hover:underline dark:text-blue-500"
|
||||
>
|
||||
{m.advanced_version_update_github_link()}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<CheckboxWithLabel
|
||||
label={m.advanced_version_update_reset_config_label()}
|
||||
description={m.advanced_version_update_reset_config_description()}
|
||||
checked={resetConfig}
|
||||
onChange={e => setResetConfig(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<CheckboxWithLabel
|
||||
label="I understand version changes may break my device and require factory reset"
|
||||
checked={versionChangeAcknowledged}
|
||||
onChange={e => setVersionChangeAcknowledged(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text={m.advanced_version_update_button()}
|
||||
disabled={
|
||||
(updateTarget === "app" && !appVersion) ||
|
||||
(updateTarget === "system" && !systemVersion) ||
|
||||
(updateTarget === "both" && (!appVersion || !systemVersion)) ||
|
||||
!versionChangeAcknowledged ||
|
||||
customVersionUpdateLoading
|
||||
}
|
||||
loading={customVersionUpdateLoading}
|
||||
onClick={handleCustomVersionUpdate}
|
||||
/>
|
||||
</div>
|
||||
</FeatureFlag>
|
||||
</NestedSettingsGroup>
|
||||
) : null}
|
||||
|
||||
<SettingsItem
|
||||
title={m.advanced_loopback_only_title()}
|
||||
description={m.advanced_loopback_only_description()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={localLoopbackOnly}
|
||||
onChange={e => handleLoopbackOnlyModeChange(e.target.checked)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
|
||||
|
||||
<SettingsItem
|
||||
title="Troubleshooting Mode"
|
||||
description="Diagnostic tools and additional controls for troubleshooting and development purposes"
|
||||
>
|
||||
<Checkbox
|
||||
defaultChecked={settings.debugMode}
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={backlightSettings.max_brightness.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Off" },
|
||||
{ value: "10", label: "Low" },
|
||||
{ value: "35", label: "Medium" },
|
||||
{ value: "64", label: "High" },
|
||||
]}
|
||||
onChange={e => {
|
||||
settings.setDebugMode(e.target.checked);
|
||||
handleBacklightMaxBrightnessChange(Number.parseInt(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
{settings.debugMode && (
|
||||
{backlightSettings.max_brightness != 0 && (
|
||||
<NestedSettingsGroup>
|
||||
<SettingsItem
|
||||
title="USB Emulation"
|
||||
description="Control the USB emulation state"
|
||||
title="Dim Display After"
|
||||
description="Set how long to wait before dimming the display"
|
||||
>
|
||||
<Button
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={
|
||||
usbEmulationEnabled ? "Disable USB Emulation" : "Enable USB Emulation"
|
||||
}
|
||||
onClick={() => handleUsbEmulationToggle(!usbEmulationEnabled)}
|
||||
label=""
|
||||
value={backlightSettings.dim_after.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Never" },
|
||||
{ value: "60", label: "1 minute" },
|
||||
{ value: "300", label: "5 minutes" },
|
||||
{ value: "600", label: "10 minutes" },
|
||||
{ value: "1800", label: "30 minutes" },
|
||||
{ value: "3600", label: "1 hour" },
|
||||
]}
|
||||
onChange={e => {
|
||||
handleBacklightDimAfterChange(Number.parseInt(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="Reset Configuration"
|
||||
description="Reset configuration to default. This will log you out."
|
||||
title="Turn Off Display After"
|
||||
description="Period of inactivity before display automatically turns off"
|
||||
>
|
||||
<Button
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Reset Config"
|
||||
onClick={() => {
|
||||
handleResetConfig();
|
||||
window.location.reload();
|
||||
label=""
|
||||
value={backlightSettings.off_after.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Never" },
|
||||
{ value: "300", label: "5 minutes" },
|
||||
{ value: "600", label: "10 minutes" },
|
||||
{ value: "1800", label: "30 minutes" },
|
||||
{ value: "3600", label: "1 hour" },
|
||||
]}
|
||||
onChange={e => {
|
||||
handleBacklightOffAfterChange(Number.parseInt(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</NestedSettingsGroup>
|
||||
)}
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
The display will wake up when the connection state changes, or when touched.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={showLoopbackWarning}
|
||||
onClose={() => {
|
||||
setShowLoopbackWarning(false);
|
||||
}}
|
||||
title="Enable Loopback-Only Mode?"
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
WARNING: This will restrict web interface access to localhost (127.0.0.1)
|
||||
only.
|
||||
</p>
|
||||
<p>Before enabling this feature, make sure you have either:</p>
|
||||
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
|
||||
<li>SSH access configured and tested</li>
|
||||
<li>Cloud access enabled and working</li>
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
variant="warning"
|
||||
confirmText="I Understand, Enable Anyway"
|
||||
onConfirm={confirmLoopbackModeEnable}
|
||||
/>
|
||||
<FeatureFlag minAppVersion="0.4.9">
|
||||
<div className="space-y-4">
|
||||
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||
<SettingsSectionHeader
|
||||
title="Power Saving"
|
||||
description="Reduce power consumption when not in use"
|
||||
/>
|
||||
<SettingsItem
|
||||
badge="Experimental"
|
||||
title="HDMI Sleep Mode"
|
||||
description="Reduce power consumption when the HDMI port is not in use"
|
||||
>
|
||||
<Checkbox
|
||||
checked={powerSavingEnabled}
|
||||
onChange={(e) => handlePowerSavingChange(e.target.checked)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
</FeatureFlag>
|
||||
|
||||
<FeatureFlag minAppVersion="0.3.8">
|
||||
<UsbDeviceSetting />
|
||||
</FeatureFlag>
|
||||
|
||||
<FeatureFlag minAppVersion="0.3.8">
|
||||
<UsbInfoSetting />
|
||||
</FeatureFlag>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue