mirror of https://github.com/jetkvm/kvm.git
Compare commits
6 Commits
ff29be998c
...
917a28fe4a
Author | SHA1 | Date |
---|---|---|
|
917a28fe4a | |
|
488276f3a8 | |
|
7267347261 | |
|
393bc122d4 | |
|
6d13e1be12 | |
|
002c2f4937 |
|
@ -3,6 +3,7 @@ package confparser
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -372,6 +373,10 @@ func (f *FieldConfig) validateField() error {
|
||||||
if _, err := idna.Lookup.ToASCII(val); err != nil {
|
if _, err := idna.Lookup.ToASCII(val); err != nil {
|
||||||
return fmt.Errorf("field `%s` is not a valid hostname: %s", f.Name, val)
|
return fmt.Errorf("field `%s` is not a valid hostname: %s", f.Name, val)
|
||||||
}
|
}
|
||||||
|
case "proxy":
|
||||||
|
if url, err := url.Parse(val); err != nil || (url.Scheme != "http" && url.Scheme != "https") || url.Host == "" {
|
||||||
|
return fmt.Errorf("field `%s` is not a valid HTTP proxy URL: %s", f.Name, val)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType)
|
return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package network
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/guregu/null/v6"
|
"github.com/guregu/null/v6"
|
||||||
|
@ -32,8 +34,9 @@ type IPv6StaticConfig struct {
|
||||||
DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"`
|
DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"`
|
||||||
}
|
}
|
||||||
type NetworkConfig struct {
|
type NetworkConfig struct {
|
||||||
Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"`
|
Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"`
|
||||||
Domain null.String `json:"domain,omitempty" validate_type:"hostname"`
|
HTTPProxy null.String `json:"http_proxy,omitempty" validate_type:"proxy"`
|
||||||
|
Domain null.String `json:"domain,omitempty" validate_type:"hostname"`
|
||||||
|
|
||||||
IPv4Mode null.String `json:"ipv4_mode,omitempty" one_of:"dhcp,static,disabled" default:"dhcp"`
|
IPv4Mode null.String `json:"ipv4_mode,omitempty" one_of:"dhcp,static,disabled" default:"dhcp"`
|
||||||
IPv4Static *IPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"`
|
IPv4Static *IPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"`
|
||||||
|
@ -69,6 +72,18 @@ func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
|
||||||
|
|
||||||
return listenOptions
|
return listenOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *NetworkConfig) GetTransportProxyFunc() func(*http.Request) (*url.URL, error) {
|
||||||
|
return func(*http.Request) (*url.URL, error) {
|
||||||
|
if s.HTTPProxy.String == "" {
|
||||||
|
return nil, nil
|
||||||
|
} else {
|
||||||
|
proxyUrl, _ := url.Parse(s.HTTPProxy.String)
|
||||||
|
return proxyUrl, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) GetHostname() string {
|
func (s *NetworkInterfaceState) GetHostname() string {
|
||||||
hostname := ToValidHostname(s.config.Hostname.String)
|
hostname := ToValidHostname(s.config.Hostname.String)
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -57,6 +58,7 @@ func (t *TimeSync) queryMultipleHttp(urls []string, timeout time.Duration) (now
|
||||||
ctx,
|
ctx,
|
||||||
url,
|
url,
|
||||||
timeout,
|
timeout,
|
||||||
|
t.networkConfig.GetTransportProxyFunc(),
|
||||||
)
|
)
|
||||||
duration := time.Since(startTime)
|
duration := time.Since(startTime)
|
||||||
|
|
||||||
|
@ -122,10 +124,16 @@ func queryHttpTime(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
url string,
|
url string,
|
||||||
timeout time.Duration,
|
timeout time.Duration,
|
||||||
|
proxyFunc func(*http.Request) (*url.URL, error),
|
||||||
) (now *time.Time, response *http.Response, err error) {
|
) (now *time.Time, response *http.Response, err error) {
|
||||||
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
|
transport.Proxy = proxyFunc
|
||||||
|
|
||||||
client := http.Client{
|
client := http.Client{
|
||||||
Timeout: timeout,
|
Transport: transport,
|
||||||
|
Timeout: timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
|
|
@ -30,8 +30,8 @@ var defaultGadgetConfig = map[string]gadgetConfigItem{
|
||||||
attrs: gadgetAttributes{
|
attrs: gadgetAttributes{
|
||||||
"bcdUSB": "0x0200", // USB 2.0
|
"bcdUSB": "0x0200", // USB 2.0
|
||||||
"idVendor": "0x1d6b", // The Linux Foundation
|
"idVendor": "0x1d6b", // The Linux Foundation
|
||||||
"idProduct": "0104", // Multifunction Composite Gadget
|
"idProduct": "0x0104", // Multifunction Composite Gadget
|
||||||
"bcdDevice": "0100",
|
"bcdDevice": "0x0100", // USB2
|
||||||
},
|
},
|
||||||
configAttrs: gadgetAttributes{
|
configAttrs: gadgetAttributes{
|
||||||
"MaxPower": "250", // in unit of 2mA
|
"MaxPower": "250", // in unit of 2mA
|
||||||
|
|
19
jsonrpc.go
19
jsonrpc.go
|
@ -681,10 +681,11 @@ func rpcResetConfig() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
type DCPowerState struct {
|
type DCPowerState struct {
|
||||||
IsOn bool `json:"isOn"`
|
IsOn bool `json:"isOn"`
|
||||||
Voltage float64 `json:"voltage"`
|
Voltage float64 `json:"voltage"`
|
||||||
Current float64 `json:"current"`
|
Current float64 `json:"current"`
|
||||||
Power float64 `json:"power"`
|
Power float64 `json:"power"`
|
||||||
|
RestoreState int `json:"restoreState"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetDCPowerState() (DCPowerState, error) {
|
func rpcGetDCPowerState() (DCPowerState, error) {
|
||||||
|
@ -700,6 +701,15 @@ func rpcSetDCPowerState(enabled bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rpcSetDCRestoreState(state int) error {
|
||||||
|
logger.Info().Int("state", state).Msg("Setting DC restore state")
|
||||||
|
err := setDCRestoreState(state)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set DC restore state: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func rpcGetActiveExtension() (string, error) {
|
func rpcGetActiveExtension() (string, error) {
|
||||||
return config.ActiveExtension, nil
|
return config.ActiveExtension, nil
|
||||||
}
|
}
|
||||||
|
@ -1088,6 +1098,7 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"getBacklightSettings": {Func: rpcGetBacklightSettings},
|
"getBacklightSettings": {Func: rpcGetBacklightSettings},
|
||||||
"getDCPowerState": {Func: rpcGetDCPowerState},
|
"getDCPowerState": {Func: rpcGetDCPowerState},
|
||||||
"setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}},
|
"setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}},
|
||||||
|
"setDCRestoreState": {Func: rpcSetDCRestoreState, Params: []string{"state"}},
|
||||||
"getActiveExtension": {Func: rpcGetActiveExtension},
|
"getActiveExtension": {Func: rpcGetActiveExtension},
|
||||||
"setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}},
|
"setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}},
|
||||||
"getATXState": {Func: rpcGetATXState},
|
"getATXState": {Func: rpcGetATXState},
|
||||||
|
|
10
ota.go
10
ota.go
|
@ -89,7 +89,14 @@ func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease
|
||||||
return nil, fmt.Errorf("error creating request: %w", err)
|
return nil, fmt.Errorf("error creating request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
|
transport.Proxy = config.NetworkConfig.GetTransportProxyFunc()
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error sending request: %w", err)
|
return nil, fmt.Errorf("error sending request: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -135,6 +142,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress
|
||||||
client := http.Client{
|
client := http.Client{
|
||||||
Timeout: 10 * time.Minute,
|
Timeout: 10 * time.Minute,
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
|
Proxy: config.NetworkConfig.GetTransportProxyFunc(),
|
||||||
TLSHandshakeTimeout: 30 * time.Second,
|
TLSHandshakeTimeout: 30 * time.Second,
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
RootCAs: rootcerts.ServerCertPool(),
|
RootCAs: rootcerts.ServerCertPool(),
|
||||||
|
|
39
serial.go
39
serial.go
|
@ -142,6 +142,7 @@ var dcState DCPowerState
|
||||||
func runDCControl() {
|
func runDCControl() {
|
||||||
scopedLogger := serialLogger.With().Str("service", "dc_control").Logger()
|
scopedLogger := serialLogger.With().Str("service", "dc_control").Logger()
|
||||||
reader := bufio.NewReader(port)
|
reader := bufio.NewReader(port)
|
||||||
|
hasRestoreFeature := false
|
||||||
for {
|
for {
|
||||||
line, err := reader.ReadString('\n')
|
line, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -151,7 +152,13 @@ func runDCControl() {
|
||||||
|
|
||||||
// Split the line by semicolon
|
// Split the line by semicolon
|
||||||
parts := strings.Split(strings.TrimSpace(line), ";")
|
parts := strings.Split(strings.TrimSpace(line), ";")
|
||||||
if len(parts) != 4 {
|
if len(parts) == 5 {
|
||||||
|
scopedLogger.Debug().Str("line", line).Msg("Detected DC extension with restore feature")
|
||||||
|
hasRestoreFeature = true
|
||||||
|
} else if len(parts) == 4 {
|
||||||
|
scopedLogger.Debug().Str("line", line).Msg("Detected DC extension without restore feature")
|
||||||
|
hasRestoreFeature = false
|
||||||
|
} else {
|
||||||
scopedLogger.Warn().Str("line", line).Msg("Invalid line")
|
scopedLogger.Warn().Str("line", line).Msg("Invalid line")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -163,6 +170,17 @@ func runDCControl() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dcState.IsOn = powerState == 1
|
dcState.IsOn = powerState == 1
|
||||||
|
if hasRestoreFeature {
|
||||||
|
restoreState, err := strconv.Atoi(parts[4])
|
||||||
|
if err != nil {
|
||||||
|
scopedLogger.Warn().Err(err).Msg("Invalid restore state")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dcState.RestoreState = restoreState
|
||||||
|
} else {
|
||||||
|
// -1 means not supported
|
||||||
|
dcState.RestoreState = -1
|
||||||
|
}
|
||||||
milliVolts, err := strconv.ParseFloat(parts[1], 64)
|
milliVolts, err := strconv.ParseFloat(parts[1], 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("Invalid voltage")
|
scopedLogger.Warn().Err(err).Msg("Invalid voltage")
|
||||||
|
@ -210,6 +228,25 @@ func setDCPowerState(on bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setDCRestoreState(state int) error {
|
||||||
|
_, err := port.Write([]byte("\n"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
command := "RESTORE_MODE_OFF\n"
|
||||||
|
switch state {
|
||||||
|
case 1:
|
||||||
|
command = "RESTORE_MODE_ON\n"
|
||||||
|
case 2:
|
||||||
|
command = "RESTORE_MODE_LAST_STATE\n"
|
||||||
|
}
|
||||||
|
_, err = port.Write([]byte(command))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var defaultMode = &serial.Mode{
|
var defaultMode = &serial.Mode{
|
||||||
BaudRate: 115200,
|
BaudRate: 115200,
|
||||||
DataBits: 8,
|
DataBits: 8,
|
||||||
|
|
|
@ -262,23 +262,6 @@ export default function Actionbar({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* {useSettingsStore().actionBarCtrlAltDel && (
|
|
||||||
<div className="hidden lg:block">
|
|
||||||
<Button
|
|
||||||
size="XS"
|
|
||||||
theme="light"
|
|
||||||
text="Ctrl + Alt + Del"
|
|
||||||
LeadingIcon={FaLock}
|
|
||||||
onClick={() => {
|
|
||||||
sendKeyboardEvent(
|
|
||||||
[keys["Delete"]],
|
|
||||||
[modifiers["ControlLeft"], modifiers["AltLeft"]],
|
|
||||||
);
|
|
||||||
setTimeout(resetKeyboardState, 100);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
|
|
|
@ -8,12 +8,14 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import FieldLabel from "@components/FieldLabel";
|
import FieldLabel from "@components/FieldLabel";
|
||||||
import LoadingSpinner from "@components/LoadingSpinner";
|
import LoadingSpinner from "@components/LoadingSpinner";
|
||||||
|
import {SelectMenuBasic} from "@components/SelectMenuBasic";
|
||||||
|
|
||||||
interface DCPowerState {
|
interface DCPowerState {
|
||||||
isOn: boolean;
|
isOn: boolean;
|
||||||
voltage: number;
|
voltage: number;
|
||||||
current: number;
|
current: number;
|
||||||
power: number;
|
power: number;
|
||||||
|
restoreState: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DCPowerControl() {
|
export function DCPowerControl() {
|
||||||
|
@ -43,6 +45,20 @@ export function DCPowerControl() {
|
||||||
getDCPowerState(); // Refresh state after change
|
getDCPowerState(); // Refresh state after change
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const handleRestoreChange = (state: number) => {
|
||||||
|
// const state = powerState?.restoreState === 0 ? 1 : powerState?.restoreState === 1 ? 2 : 0;
|
||||||
|
send("setDCRestoreState", { state }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to set DC power state: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getDCPowerState(); // Refresh state after change
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getDCPowerState();
|
getDCPowerState();
|
||||||
|
@ -63,7 +79,7 @@ export function DCPowerControl() {
|
||||||
<LoadingSpinner className="h-6 w-6 text-blue-500 dark:text-blue-400" />
|
<LoadingSpinner className="h-6 w-6 text-blue-500 dark:text-blue-400" />
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card className="h-[160px] animate-fadeIn opacity-0">
|
<Card className="animate-fadeIn opacity-0">
|
||||||
<div className="space-y-4 p-3">
|
<div className="space-y-4 p-3">
|
||||||
{/* Power Controls */}
|
{/* Power Controls */}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
@ -84,6 +100,21 @@ export function DCPowerControl() {
|
||||||
onClick={() => handlePowerToggle(false)}
|
onClick={() => handlePowerToggle(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{powerState.restoreState > -1 ? (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
label="Restore Power Loss"
|
||||||
|
value={powerState.restoreState}
|
||||||
|
onChange={e => handleRestoreChange(parseInt(e.target.value))}
|
||||||
|
options={[
|
||||||
|
{ value: '0', label: "Power OFF" },
|
||||||
|
{ value: '1', label: "Power ON" },
|
||||||
|
{ value: '2', label: "Last State" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<hr className="border-slate-700/30 dark:border-slate-600/30" />
|
<hr className="border-slate-700/30 dark:border-slate-600/30" />
|
||||||
|
|
||||||
{/* Status Display */}
|
{/* Status Display */}
|
||||||
|
|
|
@ -308,9 +308,6 @@ interface SettingsState {
|
||||||
keyboardLayout: string;
|
keyboardLayout: string;
|
||||||
setKeyboardLayout: (layout: string) => void;
|
setKeyboardLayout: (layout: string) => void;
|
||||||
|
|
||||||
actionBarCtrlAltDel: boolean;
|
|
||||||
setActionBarCtrlAltDel: (enabled: boolean) => void;
|
|
||||||
|
|
||||||
keyboardLedSync: KeyboardLedSync;
|
keyboardLedSync: KeyboardLedSync;
|
||||||
setKeyboardLedSync: (sync: KeyboardLedSync) => void;
|
setKeyboardLedSync: (sync: KeyboardLedSync) => void;
|
||||||
|
|
||||||
|
@ -359,9 +356,6 @@ export const useSettingsStore = create(
|
||||||
keyboardLayout: "en-US",
|
keyboardLayout: "en-US",
|
||||||
setKeyboardLayout: layout => set({ keyboardLayout: layout }),
|
setKeyboardLayout: layout => set({ keyboardLayout: layout }),
|
||||||
|
|
||||||
actionBarCtrlAltDel: false,
|
|
||||||
setActionBarCtrlAltDel: enabled => set({ actionBarCtrlAltDel: enabled }),
|
|
||||||
|
|
||||||
keyboardLedSync: "auto",
|
keyboardLedSync: "auto",
|
||||||
setKeyboardLedSync: sync => set({ keyboardLedSync: sync }),
|
setKeyboardLedSync: sync => set({ keyboardLedSync: sync }),
|
||||||
|
|
||||||
|
@ -753,6 +747,7 @@ export type TimeSyncMode =
|
||||||
export interface NetworkSettings {
|
export interface NetworkSettings {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
|
http_proxy: string;
|
||||||
ipv4_mode: IPv4Mode;
|
ipv4_mode: IPv4Mode;
|
||||||
ipv6_mode: IPv6Mode;
|
ipv6_mode: IPv6Mode;
|
||||||
lldp_mode: LLDPMode;
|
lldp_mode: LLDPMode;
|
||||||
|
|
|
@ -42,6 +42,7 @@ import SettingsHardwareRoute from "./routes/devices.$id.settings.hardware";
|
||||||
import SettingsVideoRoute from "./routes/devices.$id.settings.video";
|
import SettingsVideoRoute from "./routes/devices.$id.settings.video";
|
||||||
import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance";
|
import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance";
|
||||||
import * as SettingsGeneralIndexRoute from "./routes/devices.$id.settings.general._index";
|
import * as SettingsGeneralIndexRoute from "./routes/devices.$id.settings.general._index";
|
||||||
|
import SettingsGeneralRebootRoute from "./routes/devices.$id.settings.general.reboot";
|
||||||
import SettingsGeneralUpdateRoute from "./routes/devices.$id.settings.general.update";
|
import SettingsGeneralUpdateRoute from "./routes/devices.$id.settings.general.update";
|
||||||
import SettingsNetworkRoute from "./routes/devices.$id.settings.network";
|
import SettingsNetworkRoute from "./routes/devices.$id.settings.network";
|
||||||
import SecurityAccessLocalAuthRoute from "./routes/devices.$id.settings.access.local-auth";
|
import SecurityAccessLocalAuthRoute from "./routes/devices.$id.settings.access.local-auth";
|
||||||
|
@ -140,6 +141,10 @@ if (isOnDevice) {
|
||||||
index: true,
|
index: true,
|
||||||
element: <SettingsGeneralIndexRoute.default />,
|
element: <SettingsGeneralIndexRoute.default />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "reboot",
|
||||||
|
element: <SettingsGeneralRebootRoute />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "update",
|
path: "update",
|
||||||
element: <SettingsGeneralUpdateRoute />,
|
element: <SettingsGeneralUpdateRoute />,
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
|
|
||||||
import { Checkbox } from "@/components/Checkbox";
|
|
||||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
|
||||||
import { useSettingsStore } from "@/hooks/stores";
|
|
||||||
|
|
||||||
import { SettingsItem } from "./devices.$id.settings";
|
|
||||||
|
|
||||||
export default function SettingsCtrlAltDelRoute() {
|
|
||||||
const enableCtrlAltDel = useSettingsStore(state => state.actionBarCtrlAltDel);
|
|
||||||
const setEnableCtrlAltDel = useSettingsStore(state => state.setActionBarCtrlAltDel);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<SettingsPageHeader
|
|
||||||
title="Action Bar"
|
|
||||||
description="Customize the action bar of your JetKVM interface"
|
|
||||||
/>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<SettingsItem title="Enable Ctrl-Alt-Del" description="Enable the Ctrl-Alt-Del key on the virtual keyboard">
|
|
||||||
<Checkbox
|
|
||||||
checked={enableCtrlAltDel}
|
|
||||||
onChange={e => setEnableCtrlAltDel(e.target.checked)}
|
|
||||||
/>
|
|
||||||
</SettingsItem>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -92,6 +92,21 @@ export default function SettingsGeneralRoute() {
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 flex items-center justify-between gap-x-2">
|
||||||
|
<SettingsItem
|
||||||
|
title="Reboot Device"
|
||||||
|
description="Power cycle the JetKVM"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
text="Reboot Device"
|
||||||
|
onClick={() => navigateTo("./reboot")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
|
||||||
|
export default function SettingsGeneralRebootRoute() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [send] = useJsonRpc();
|
||||||
|
|
||||||
|
const onConfirmUpdate = useCallback(() => {
|
||||||
|
// This is where we send the RPC to the golang binary
|
||||||
|
send("reboot", {force: true});
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
|
{
|
||||||
|
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||||
|
}
|
||||||
|
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Dialog({
|
||||||
|
onClose,
|
||||||
|
onConfirmUpdate,
|
||||||
|
}: {
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirmUpdate: () => void;
|
||||||
|
}) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-auto relative mx-auto text-left">
|
||||||
|
<div>
|
||||||
|
<ConfirmationBox
|
||||||
|
onYes={onConfirmUpdate}
|
||||||
|
onNo={onClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConfirmationBox({
|
||||||
|
onYes,
|
||||||
|
onNo,
|
||||||
|
}: {
|
||||||
|
onYes: () => void;
|
||||||
|
onNo: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
Reboot JetKVM
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||||
|
Do you want to proceed with rebooting the system?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-4 flex gap-x-2">
|
||||||
|
<Button size="SM" theme="light" text="Yes" onClick={onYes} />
|
||||||
|
<Button size="SM" theme="blank" text="No" onClick={onNo} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -116,15 +116,6 @@ export default function SettingsHardwareRoute() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
{/* <SettingsItem
|
|
||||||
title="Enable Ctrl+Alt+Del Action Bar"
|
|
||||||
description="Enable or disable the action bar action for sending a Ctrl+Alt+Del to the host"
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={actionBarConfig.ctrlAltDel}
|
|
||||||
onChange={onActionBarItemChange("ctrlAltDel")}
|
|
||||||
/>
|
|
||||||
</SettingsItem> */}
|
|
||||||
{settings.backlightSettings.max_brightness != 0 && (
|
{settings.backlightSettings.max_brightness != 0 && (
|
||||||
<>
|
<>
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
|
|
|
@ -34,6 +34,7 @@ dayjs.extend(relativeTime);
|
||||||
|
|
||||||
const defaultNetworkSettings: NetworkSettings = {
|
const defaultNetworkSettings: NetworkSettings = {
|
||||||
hostname: "",
|
hostname: "",
|
||||||
|
http_proxy: "",
|
||||||
domain: "",
|
domain: "",
|
||||||
ipv4_mode: "unknown",
|
ipv4_mode: "unknown",
|
||||||
ipv6_mode: "unknown",
|
ipv6_mode: "unknown",
|
||||||
|
@ -185,6 +186,10 @@ export default function SettingsNetworkRoute() {
|
||||||
setNetworkSettings({ ...networkSettings, hostname: value });
|
setNetworkSettings({ ...networkSettings, hostname: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleProxyChange = (value: string) => {
|
||||||
|
setNetworkSettings({ ...networkSettings, http_proxy: value });
|
||||||
|
};
|
||||||
|
|
||||||
const handleDomainChange = (value: string) => {
|
const handleDomainChange = (value: string) => {
|
||||||
setNetworkSettings({ ...networkSettings, domain: value });
|
setNetworkSettings({ ...networkSettings, domain: value });
|
||||||
};
|
};
|
||||||
|
@ -253,6 +258,26 @@ export default function SettingsNetworkRoute() {
|
||||||
</div>
|
</div>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsItem
|
||||||
|
title="HTTP Proxy"
|
||||||
|
description="Proxy server for outgoing HTTP(S) requests from the device. Blank for none."
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<div>
|
||||||
|
<InputField
|
||||||
|
size="SM"
|
||||||
|
type="text"
|
||||||
|
placeholder="http://proxy.example.com:8080/"
|
||||||
|
defaultValue={networkSettings.http_proxy}
|
||||||
|
onChange={e => {
|
||||||
|
handleProxyChange(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
Loading…
Reference in New Issue