mirror of https://github.com/jetkvm/kvm.git
Compare commits
5 Commits
f752636cd3
...
bc6c1097d2
Author | SHA1 | Date |
---|---|---|
|
bc6c1097d2 | |
|
4b91c758fa | |
|
222a8470a5 | |
|
860327bfcd | |
|
709855dc3d |
|
@ -0,0 +1,17 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /ui
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
|
@ -19,13 +19,13 @@ jobs:
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: v21.1.0
|
node-version: "22"
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: "**/package-lock.json"
|
cache-dependency-path: "**/package-lock.json"
|
||||||
- name: Set up Golang
|
- name: Set up Golang
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.24.0"
|
go-version: "1.24.3"
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
run: |
|
run: |
|
||||||
make frontend
|
make frontend
|
||||||
|
|
|
@ -26,7 +26,7 @@ jobs:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.24.x
|
go-version: 1.24.3
|
||||||
- name: Create empty resource directory
|
- name: Create empty resource directory
|
||||||
run: |
|
run: |
|
||||||
mkdir -p static && touch static/.gitkeep
|
mkdir -p static && touch static/.gitkeep
|
||||||
|
|
|
@ -106,7 +106,7 @@ jobs:
|
||||||
- name: Set up Golang
|
- name: Set up Golang
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.24.0"
|
go-version: "1.24.3"
|
||||||
- name: Golang Test Report
|
- name: Golang Test Report
|
||||||
uses: becheran/go-testreport@v0.3.2
|
uses: becheran/go-testreport@v0.3.2
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -266,8 +266,13 @@ func rpcSetDevChannelState(enabled bool) error {
|
||||||
func rpcGetUpdateStatus() (*UpdateStatus, error) {
|
func rpcGetUpdateStatus() (*UpdateStatus, error) {
|
||||||
includePreRelease := config.IncludePreRelease
|
includePreRelease := config.IncludePreRelease
|
||||||
updateStatus, err := GetUpdateStatus(context.Background(), GetDeviceID(), includePreRelease)
|
updateStatus, err := GetUpdateStatus(context.Background(), GetDeviceID(), includePreRelease)
|
||||||
|
// to ensure backwards compatibility,
|
||||||
|
// if there's an error, we won't return an error, but we will set the error field
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error checking for updates: %w", err)
|
if updateStatus == nil {
|
||||||
|
return nil, fmt.Errorf("error checking for updates: %w", err)
|
||||||
|
}
|
||||||
|
updateStatus.Error = err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
return updateStatus, nil
|
return updateStatus, nil
|
||||||
|
|
40
ota.go
40
ota.go
|
@ -41,6 +41,9 @@ type UpdateStatus struct {
|
||||||
Remote *UpdateMetadata `json:"remote"`
|
Remote *UpdateMetadata `json:"remote"`
|
||||||
SystemUpdateAvailable bool `json:"systemUpdateAvailable"`
|
SystemUpdateAvailable bool `json:"systemUpdateAvailable"`
|
||||||
AppUpdateAvailable bool `json:"appUpdateAvailable"`
|
AppUpdateAvailable bool `json:"appUpdateAvailable"`
|
||||||
|
|
||||||
|
// for backwards compatibility
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const UpdateMetadataUrl = "https://api.jetkvm.com/releases"
|
const UpdateMetadataUrl = "https://api.jetkvm.com/releases"
|
||||||
|
@ -489,52 +492,47 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUpdateStatus(ctx context.Context, deviceId string, includePreRelease bool) (*UpdateStatus, error) {
|
func GetUpdateStatus(ctx context.Context, deviceId string, includePreRelease bool) (*UpdateStatus, error) {
|
||||||
|
updateStatus := &UpdateStatus{}
|
||||||
|
|
||||||
// Get local versions
|
// Get local versions
|
||||||
systemVersionLocal, appVersionLocal, err := GetLocalVersion()
|
systemVersionLocal, appVersionLocal, err := GetLocalVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error getting local version: %w", err)
|
return updateStatus, fmt.Errorf("error getting local version: %w", err)
|
||||||
|
}
|
||||||
|
updateStatus.Local = &LocalMetadata{
|
||||||
|
AppVersion: appVersionLocal.String(),
|
||||||
|
SystemVersion: systemVersionLocal.String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get remote metadata
|
// Get remote metadata
|
||||||
remoteMetadata, err := fetchUpdateMetadata(ctx, deviceId, includePreRelease)
|
remoteMetadata, err := fetchUpdateMetadata(ctx, deviceId, includePreRelease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error checking for updates: %w", err)
|
return updateStatus, fmt.Errorf("error checking for updates: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
// Build local UpdateMetadata
|
|
||||||
localMetadata := &LocalMetadata{
|
|
||||||
AppVersion: appVersionLocal.String(),
|
|
||||||
SystemVersion: systemVersionLocal.String(),
|
|
||||||
}
|
}
|
||||||
|
updateStatus.Remote = remoteMetadata
|
||||||
|
|
||||||
|
// Get remote versions
|
||||||
systemVersionRemote, err := semver.NewVersion(remoteMetadata.SystemVersion)
|
systemVersionRemote, err := semver.NewVersion(remoteMetadata.SystemVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error parsing remote system version: %w", err)
|
return updateStatus, fmt.Errorf("error parsing remote system version: %w", err)
|
||||||
}
|
}
|
||||||
appVersionRemote, err := semver.NewVersion(remoteMetadata.AppVersion)
|
appVersionRemote, err := semver.NewVersion(remoteMetadata.AppVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error parsing remote app version: %w, %s", err, remoteMetadata.AppVersion)
|
return updateStatus, fmt.Errorf("error parsing remote app version: %w, %s", err, remoteMetadata.AppVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
systemUpdateAvailable := systemVersionRemote.GreaterThan(systemVersionLocal)
|
updateStatus.SystemUpdateAvailable = systemVersionRemote.GreaterThan(systemVersionLocal)
|
||||||
appUpdateAvailable := appVersionRemote.GreaterThan(appVersionLocal)
|
updateStatus.AppUpdateAvailable = appVersionRemote.GreaterThan(appVersionLocal)
|
||||||
|
|
||||||
// Handle pre-release updates
|
// Handle pre-release updates
|
||||||
isRemoteSystemPreRelease := systemVersionRemote.Prerelease() != ""
|
isRemoteSystemPreRelease := systemVersionRemote.Prerelease() != ""
|
||||||
isRemoteAppPreRelease := appVersionRemote.Prerelease() != ""
|
isRemoteAppPreRelease := appVersionRemote.Prerelease() != ""
|
||||||
|
|
||||||
if isRemoteSystemPreRelease && !includePreRelease {
|
if isRemoteSystemPreRelease && !includePreRelease {
|
||||||
systemUpdateAvailable = false
|
updateStatus.SystemUpdateAvailable = false
|
||||||
}
|
}
|
||||||
if isRemoteAppPreRelease && !includePreRelease {
|
if isRemoteAppPreRelease && !includePreRelease {
|
||||||
appUpdateAvailable = false
|
updateStatus.AppUpdateAvailable = false
|
||||||
}
|
|
||||||
|
|
||||||
updateStatus := &UpdateStatus{
|
|
||||||
Local: localMetadata,
|
|
||||||
Remote: remoteMetadata,
|
|
||||||
SystemUpdateAvailable: systemUpdateAvailable,
|
|
||||||
AppUpdateAvailable: appUpdateAvailable,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return updateStatus, nil
|
return updateStatus, nil
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
"arrowParens": "avoid",
|
"arrowParens": "avoid",
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"plugins": ["prettier-plugin-tailwindcss"],
|
"plugins": ["prettier-plugin-tailwindcss"],
|
||||||
"tailwindFunctions": ["clsx"],
|
"tailwindFunctions": ["clsx", "cx"],
|
||||||
"printWidth": 90
|
"printWidth": 90,
|
||||||
|
"tailwindStylesheet": "./src/index.css"
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ const AutoHeight = ({ children, ...props }: { children: React.ReactNode }) => {
|
||||||
{...props}
|
{...props}
|
||||||
height={height}
|
height={height}
|
||||||
duration={300}
|
duration={300}
|
||||||
contentClassName="h-fit"
|
contentClassName="h-fit p-px"
|
||||||
contentRef={contentDiv}
|
contentRef={contentDiv}
|
||||||
disableDisplayNone
|
disableDisplayNone
|
||||||
>
|
>
|
||||||
|
|
|
@ -12,7 +12,7 @@ const sizes = {
|
||||||
|
|
||||||
const checkboxVariants = cva({
|
const checkboxVariants = cva({
|
||||||
base: cx(
|
base: cx(
|
||||||
"block rounded",
|
"form-checkbox block rounded",
|
||||||
|
|
||||||
// Colors
|
// Colors
|
||||||
"border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800 checked:accent-blue-700 checked:dark:accent-blue-500 transition-colors",
|
"border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800 checked:accent-blue-700 checked:dark:accent-blue-500 transition-colors",
|
||||||
|
|
|
@ -0,0 +1,212 @@
|
||||||
|
import { LuRefreshCcw } from "react-icons/lu";
|
||||||
|
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { GridCard } from "@/components/Card";
|
||||||
|
import { LifeTimeLabel } from "@/routes/devices.$id.settings.network";
|
||||||
|
import { NetworkState } from "@/hooks/stores";
|
||||||
|
|
||||||
|
export default function DhcpLeaseCard({
|
||||||
|
networkState,
|
||||||
|
setShowRenewLeaseConfirm,
|
||||||
|
}: {
|
||||||
|
networkState: NetworkState;
|
||||||
|
setShowRenewLeaseConfirm: (show: boolean) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<GridCard>
|
||||||
|
<div className="animate-fadeIn p-4 opacity-0 animation-duration-500 text-black dark:text-white">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
|
DHCP Lease Information
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex gap-x-6 gap-y-2">
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
{networkState?.dhcp_lease?.ip && (
|
||||||
|
<div className="flex justify-between border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
IP Address
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.ip}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.netmask && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Subnet Mask
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.netmask}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.dns && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
DNS Servers
|
||||||
|
</span>
|
||||||
|
<span className="text-right text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.dns.map(dns => <div key={dns}>{dns}</div>)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.broadcast && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Broadcast
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.broadcast}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.domain && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Domain
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.domain}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.ntp_servers &&
|
||||||
|
networkState?.dhcp_lease?.ntp_servers.length > 0 && (
|
||||||
|
<div className="flex justify-between gap-x-8 border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<div className="w-full grow text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
NTP Servers
|
||||||
|
</div>
|
||||||
|
<div className="shrink text-right text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.ntp_servers.map(server => (
|
||||||
|
<div key={server}>{server}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.hostname && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Hostname
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.hostname}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
{networkState?.dhcp_lease?.routers &&
|
||||||
|
networkState?.dhcp_lease?.routers.length > 0 && (
|
||||||
|
<div className="flex justify-between pt-2">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Gateway
|
||||||
|
</span>
|
||||||
|
<span className="text-right text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.routers.map(router => (
|
||||||
|
<div key={router}>{router}</div>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.server_id && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
DHCP Server
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.server_id}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.lease_expiry && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Lease Expires
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
<LifeTimeLabel
|
||||||
|
lifetime={`${networkState?.dhcp_lease?.lease_expiry}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.mtu && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">MTU</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.mtu}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.ttl && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">TTL</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.ttl}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.bootp_next_server && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Boot Next Server
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.bootp_next_server}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.bootp_server_name && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Boot Server Name
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.bootp_server_name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{networkState?.dhcp_lease?.bootp_file && (
|
||||||
|
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Boot File
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.dhcp_lease?.bootp_file}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
className="text-red-500"
|
||||||
|
text="Renew DHCP Lease"
|
||||||
|
LeadingIcon={LuRefreshCcw}
|
||||||
|
onClick={() => setShowRenewLeaseConfirm(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { NetworkState } from "../hooks/stores";
|
||||||
|
import { LifeTimeLabel } from "../routes/devices.$id.settings.network";
|
||||||
|
|
||||||
|
import { GridCard } from "./Card";
|
||||||
|
|
||||||
|
export default function Ipv6NetworkCard({
|
||||||
|
networkState,
|
||||||
|
}: {
|
||||||
|
networkState: NetworkState;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<GridCard>
|
||||||
|
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
|
IPv6 Information
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-x-6 gap-y-2">
|
||||||
|
{networkState?.dhcp_lease?.ip && (
|
||||||
|
<div className="flex flex-col justify-between">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Link-local
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{networkState?.ipv6_link_local}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 pt-2">
|
||||||
|
{networkState?.ipv6_addresses && networkState?.ipv6_addresses.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="text-sm font-semibold">IPv6 Addresses</h4>
|
||||||
|
{networkState.ipv6_addresses.map(
|
||||||
|
addr => (
|
||||||
|
<div
|
||||||
|
key={addr.address}
|
||||||
|
className="rounded-md rounded-l-none border border-slate-500/10 border-l-blue-700/50 bg-white p-4 pl-4 backdrop-blur-sm dark:bg-transparent"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
||||||
|
<div className="col-span-2 flex flex-col justify-between">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Address
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">{addr.address}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{addr.valid_lifetime && (
|
||||||
|
<div className="flex flex-col justify-between">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Valid Lifetime
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{addr.valid_lifetime === "" ? (
|
||||||
|
<span className="text-slate-400 dark:text-slate-600">
|
||||||
|
N/A
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<LifeTimeLabel lifetime={`${addr.valid_lifetime}`} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{addr.preferred_lifetime && (
|
||||||
|
<div className="flex flex-col justify-between">
|
||||||
|
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
Preferred Lifetime
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{addr.preferred_lifetime === "" ? (
|
||||||
|
<span className="text-slate-400 dark:text-slate-600">
|
||||||
|
N/A
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
);
|
||||||
|
}
|
|
@ -20,7 +20,9 @@ const Modal = React.memo(function Modal({
|
||||||
transition
|
transition
|
||||||
className="fixed inset-0 bg-gray-500/75 transition-opacity data-closed:opacity-0 data-enter:duration-500 data-leave:duration-200 data-enter:ease-out data-leave:ease-in dark:bg-slate-900/90"
|
className="fixed inset-0 bg-gray-500/75 transition-opacity data-closed:opacity-0 data-enter:duration-500 data-leave:duration-200 data-enter:ease-out data-leave:ease-in dark:bg-slate-900/90"
|
||||||
/>
|
/>
|
||||||
<div className="fixed inset-0 z-20 w-screen overflow-y-auto">
|
<div className="fixed inset-0 z-20 w-screen overflow-y-auto" style={{
|
||||||
|
scrollbarGutter: 'stable'
|
||||||
|
}}>
|
||||||
{/* TODO: This doesn't work well with other-sessions */}
|
{/* TODO: This doesn't work well with other-sessions */}
|
||||||
<div className="flex min-h-full items-end justify-center p-4 text-center md:items-baseline md:p-4">
|
<div className="flex min-h-full items-end justify-center p-4 text-center md:items-baseline md:p-4">
|
||||||
<DialogPanel
|
<DialogPanel
|
||||||
|
|
|
@ -107,7 +107,7 @@ export function ATXPowerControl() {
|
||||||
<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-[120px] animate-fadeIn">
|
<Card className="h-[120px] animate-fadeIn opacity-0">
|
||||||
<div className="space-y-4 p-3">
|
<div className="space-y-4 p-3">
|
||||||
{/* Control Buttons */}
|
{/* Control Buttons */}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|
|
@ -63,7 +63,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">
|
<Card className="h-[160px] 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">
|
||||||
|
|
|
@ -58,7 +58,7 @@ export function SerialConsole() {
|
||||||
description="Configure your serial console settings"
|
description="Configure your serial console settings"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card className="animate-fadeIn">
|
<Card className="animate-fadeIn opacity-0">
|
||||||
<div className="space-y-4 p-3">
|
<div className="space-y-4 p-3">
|
||||||
{/* Open Console Button */}
|
{/* Open Console Button */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|
|
@ -92,7 +92,7 @@ export default function ExtensionPopover() {
|
||||||
{renderActiveExtension()}
|
{renderActiveExtension()}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex animate-fadeIn items-center justify-end space-x-2"
|
className="flex animate-fadeIn opacity-0 items-center justify-end space-x-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
|
@ -113,7 +113,7 @@ export default function ExtensionPopover() {
|
||||||
title="Extensions"
|
title="Extensions"
|
||||||
description="Load and manage your extensions"
|
description="Load and manage your extensions"
|
||||||
/>
|
/>
|
||||||
<Card className="animate-fadeIn">
|
<Card className="animate-fadeIn opacity-0" >
|
||||||
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
|
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
|
||||||
{AVAILABLE_EXTENSIONS.map(extension => (
|
{AVAILABLE_EXTENSIONS.map(extension => (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -214,7 +214,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="animate-fadeIn space-y-2"
|
className="animate-fadeIn opacity-0 space-y-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.1s",
|
animationDelay: "0.1s",
|
||||||
|
@ -289,7 +289,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
|
||||||
|
|
||||||
{!remoteVirtualMediaState && (
|
{!remoteVirtualMediaState && (
|
||||||
<div
|
<div
|
||||||
className="flex animate-fadeIn items-center justify-end space-x-2"
|
className="flex animate-fadeIn opacity-0 items-center justify-end space-x-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
|
|
|
@ -83,7 +83,7 @@ export default function PasteModal() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="animate-fadeIn space-y-2"
|
className="animate-fadeIn opacity-0 space-y-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.1s",
|
animationDelay: "0.1s",
|
||||||
|
@ -137,7 +137,7 @@ export default function PasteModal() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex animate-fadeIn items-center justify-end gap-x-2"
|
className="flex animate-fadeIn opacity-0 items-center justify-end gap-x-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default function AddDeviceForm({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div
|
<div
|
||||||
className="animate-fadeIn space-y-4"
|
className="animate-fadeIn opacity-0 space-y-4"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.5s",
|
animationDuration: "0.5s",
|
||||||
animationFillMode: "forwards",
|
animationFillMode: "forwards",
|
||||||
|
@ -73,7 +73,7 @@ export default function AddDeviceForm({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex animate-fadeIn items-center justify-end space-x-2"
|
className="flex animate-fadeIn opacity-0 items-center justify-end space-x-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default function DeviceList({
|
||||||
}: DeviceListProps) {
|
}: DeviceListProps) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Card className="animate-fadeIn">
|
<Card className="animate-fadeIn opacity-0">
|
||||||
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
|
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
|
||||||
{storedDevices.map((device, index) => (
|
{storedDevices.map((device, index) => (
|
||||||
<div key={index} className="flex items-center justify-between gap-x-2 p-3">
|
<div key={index} className="flex items-center justify-between gap-x-2 p-3">
|
||||||
|
@ -63,7 +63,7 @@ export default function DeviceList({
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<div
|
<div
|
||||||
className="flex animate-fadeIn items-center justify-end space-x-2"
|
className="flex animate-fadeIn opacity-0 items-center justify-end space-x-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
|
|
|
@ -13,7 +13,7 @@ export default function EmptyStateCard({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="select-none space-y-4">
|
<div className="select-none space-y-4">
|
||||||
<Card className="animate-fadeIn">
|
<Card className="animate-fadeIn opacity-0">
|
||||||
<div className="flex items-center justify-center py-8 text-center">
|
<div className="flex items-center justify-center py-8 text-center">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
@ -35,7 +35,7 @@ export default function EmptyStateCard({
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<div
|
<div
|
||||||
className="flex animate-fadeIn items-center justify-end space-x-2"
|
className="flex animate-fadeIn opacity-0 items-center justify-end space-x-2"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@import 'tailwindcss';
|
@import "tailwindcss";
|
||||||
|
|
||||||
@config "../tailwind.config.js";
|
@config "../tailwind.config.js";
|
||||||
|
|
||||||
|
@ -10,10 +10,10 @@
|
||||||
@custom-variant dark (&:where(.dark, .dark *));
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: 'Circular', sans-serif;
|
--font-sans: "Circular", sans-serif;
|
||||||
--font-display: 'Circular', sans-serif;
|
--font-display: "Circular", sans-serif;
|
||||||
--font-serif: 'Circular', serif;
|
--font-serif: "Circular", serif;
|
||||||
--font-mono: 'Source Code Pro Variable', monospace;
|
--font-mono: "Source Code Pro Variable", monospace;
|
||||||
|
|
||||||
--grid-layout: auto 1fr auto;
|
--grid-layout: auto 1fr auto;
|
||||||
--grid-headerBody: auto 1fr;
|
--grid-headerBody: auto 1fr;
|
||||||
|
@ -122,13 +122,22 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* If we don't ignore this, Prettier will add a space between the value and the `ms`. Rendering the utility invalid. */
|
||||||
|
/* prettier-ignore */
|
||||||
@utility max-width-* {
|
@utility max-width-* {
|
||||||
max-width: --modifier(--container- *, [length], [ *]);
|
max-width: --modifier(--container-*, [length], [*]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure there is not a `ms` and ms -> `...)ms` */
|
/* If we don't ignore this, Prettier will add a space between the value and the `ms`. Rendering the utility invalid. */
|
||||||
|
/* prettier-ignore */
|
||||||
@utility animation-delay-* {
|
@utility animation-delay-* {
|
||||||
animation-delay: --value(integer) ms;
|
animation-delay: --value(integer)ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If we don't ignore this, Prettier will add a space between the value and the `ms`. Rendering the utility invalid. */
|
||||||
|
/* prettier-ignore */
|
||||||
|
@utility animation-duration-* {
|
||||||
|
animation-duration: --value(integer)ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
LuCheck,
|
LuCheck,
|
||||||
LuUpload,
|
LuUpload,
|
||||||
} from "react-icons/lu";
|
} from "react-icons/lu";
|
||||||
import { PlusCircleIcon , ExclamationTriangleIcon } from "@heroicons/react/20/solid";
|
import { PlusCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid";
|
||||||
import { TrashIcon } from "@heroicons/react/16/solid";
|
import { TrashIcon } from "@heroicons/react/16/solid";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
@ -38,7 +38,6 @@ import {
|
||||||
useRTCStore,
|
useRTCStore,
|
||||||
} from "../hooks/stores";
|
} from "../hooks/stores";
|
||||||
|
|
||||||
|
|
||||||
export default function MountRoute() {
|
export default function MountRoute() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
{
|
{
|
||||||
|
@ -283,8 +282,8 @@ function ModeSelectionView({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full space-y-4">
|
<div className="w-full space-y-4">
|
||||||
<div className="animate-fadeIn space-y-0">
|
<div className="animate-fadeIn space-y-0 opacity-0">
|
||||||
<h2 className="text-lg font-bold leading-tight dark:text-white">
|
<h2 className="text-lg leading-tight font-bold dark:text-white">
|
||||||
Virtual Media Source
|
Virtual Media Source
|
||||||
</h2>
|
</h2>
|
||||||
<div className="text-sm leading-snug text-slate-600 dark:text-slate-400">
|
<div className="text-sm leading-snug text-slate-600 dark:text-slate-400">
|
||||||
|
@ -320,7 +319,7 @@ function ModeSelectionView({
|
||||||
].map(({ label, description, value: mode, icon: Icon, tag, disabled }, index) => (
|
].map(({ label, description, value: mode, icon: Icon, tag, disabled }, index) => (
|
||||||
<div
|
<div
|
||||||
key={label}
|
key={label}
|
||||||
className={cx("animate-fadeIn")}
|
className="animate-fadeIn opacity-0"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: `${25 * (index * 5)}ms`,
|
animationDelay: `${25 * (index * 5)}ms`,
|
||||||
|
@ -337,7 +336,7 @@ function ModeSelectionView({
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="relative z-50 flex select-none flex-col items-start p-4"
|
className="relative z-50 flex flex-col items-start p-4 select-none"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
disabled ? null : setSelectedMode(mode as "browser" | "url" | "device")
|
disabled ? null : setSelectedMode(mode as "browser" | "url" | "device")
|
||||||
}
|
}
|
||||||
|
@ -365,7 +364,7 @@ function ModeSelectionView({
|
||||||
value={mode}
|
value={mode}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
checked={selectedMode === mode}
|
checked={selectedMode === mode}
|
||||||
className="absolute right-4 top-4 h-4 w-4 text-blue-700"
|
className="absolute top-4 right-4 form-radio h-4 w-4 rounded-full text-blue-700"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -373,7 +372,7 @@ function ModeSelectionView({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex animate-fadeIn justify-end"
|
className="flex animate-fadeIn justify-end opacity-0"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
|
@ -437,19 +436,19 @@ function BrowserFileView({
|
||||||
className="block cursor-pointer select-none"
|
className="block cursor-pointer select-none"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="group animate-fadeIn"
|
className="group animate-fadeIn opacity-0"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card className="outline-dashed transition-all duration-300 hover:bg-blue-50/50">
|
<Card className="transition-all duration-300 outline-dashed">
|
||||||
<div className="w-full px-4 py-12">
|
<div className="w-full px-4 py-12">
|
||||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||||
{selectedFile ? (
|
{selectedFile ? (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<LuHardDrive className="mx-auto h-6 w-6 text-blue-700" />
|
<LuHardDrive className="mx-auto h-6 w-6 text-blue-700" />
|
||||||
<h3 className="text-sm font-semibold leading-none">
|
<h3 className="text-sm leading-none font-semibold">
|
||||||
{formatters.truncateMiddle(selectedFile.name, 40)}
|
{formatters.truncateMiddle(selectedFile.name, 40)}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs leading-none text-slate-700">
|
<p className="text-xs leading-none text-slate-700">
|
||||||
|
@ -460,7 +459,7 @@ function BrowserFileView({
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700" />
|
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700" />
|
||||||
<h3 className="text-sm font-semibold leading-none">
|
<h3 className="text-sm leading-none font-semibold">
|
||||||
Click to select a file
|
Click to select a file
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs leading-none text-slate-700">
|
<p className="text-xs leading-none text-slate-700">
|
||||||
|
@ -483,7 +482,7 @@ function BrowserFileView({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex w-full animate-fadeIn items-end justify-between"
|
className="flex w-full animate-fadeIn items-end justify-between opacity-0"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.1s",
|
animationDelay: "0.1s",
|
||||||
|
@ -578,7 +577,7 @@ function UrlView({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="animate-fadeIn"
|
className="animate-fadeIn opacity-0"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
}}
|
}}
|
||||||
|
@ -593,7 +592,7 @@ function UrlView({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="flex w-full animate-fadeIn items-end justify-between"
|
className="flex w-full animate-fadeIn items-end justify-between opacity-0"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.1s",
|
animationDelay: "0.1s",
|
||||||
|
@ -619,7 +618,7 @@ function UrlView({
|
||||||
|
|
||||||
<hr className="border-slate-800/30 dark:border-slate-300/20" />
|
<hr className="border-slate-800/30 dark:border-slate-300/20" />
|
||||||
<div
|
<div
|
||||||
className="animate-fadeIn"
|
className="animate-fadeIn opacity-0"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.2s",
|
animationDelay: "0.2s",
|
||||||
|
@ -628,13 +627,13 @@ function UrlView({
|
||||||
<h2 className="mb-2 text-sm font-semibold text-black dark:text-white">
|
<h2 className="mb-2 text-sm font-semibold text-black dark:text-white">
|
||||||
Popular images
|
Popular images
|
||||||
</h2>
|
</h2>
|
||||||
<Card className="divide-y-slate-800/30 w-full divide-y dark:divide-slate-300/20">
|
<Card className="w-full divide-y divide-slate-800/20 dark:divide-slate-300/20">
|
||||||
{popularImages.map((image, index) => (
|
{popularImages.map((image, index) => (
|
||||||
<div key={index} className="flex items-center justify-between gap-x-4 p-3.5">
|
<div key={index} className="flex items-center justify-between gap-x-4 p-3.5">
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<img src={image.icon} alt={`${image.name} Icon`} className="w-6" />
|
<img src={image.icon} alt={`${image.name} Icon`} className="w-6" />
|
||||||
<div className="flex flex-col gap-y-1">
|
<div className="flex flex-col gap-y-1">
|
||||||
<h3 className="text-sm font-semibold leading-none dark:text-white">
|
<h3 className="text-sm leading-none font-semibold dark:text-white">
|
||||||
{formatters.truncateMiddle(image.name, 40)}
|
{formatters.truncateMiddle(image.name, 40)}
|
||||||
</h3>
|
</h3>
|
||||||
{image.description && (
|
{image.description && (
|
||||||
|
@ -797,7 +796,7 @@ function DeviceFileView({
|
||||||
description="Select an image to mount from the JetKVM storage"
|
description="Select an image to mount from the JetKVM storage"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="w-full animate-fadeIn"
|
className="w-full animate-fadeIn opacity-0"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.1s",
|
animationDelay: "0.1s",
|
||||||
|
@ -809,7 +808,7 @@ function DeviceFileView({
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700 dark:text-blue-500" />
|
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||||
No images available
|
No images available
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||||
|
@ -827,7 +826,7 @@ function DeviceFileView({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y-slate-800/30 w-full divide-y dark:divide-slate-300/20">
|
<div className="w-full divide-y divide-slate-800/20 dark:divide-slate-300/20">
|
||||||
{currentFiles.map((file, index) => (
|
{currentFiles.map((file, index) => (
|
||||||
<PreUploadedImageItem
|
<PreUploadedImageItem
|
||||||
key={index}
|
key={index}
|
||||||
|
@ -886,7 +885,7 @@ function DeviceFileView({
|
||||||
|
|
||||||
{onStorageFiles.length > 0 ? (
|
{onStorageFiles.length > 0 ? (
|
||||||
<div
|
<div
|
||||||
className="flex animate-fadeIn items-end justify-between"
|
className="flex animate-fadeIn items-end justify-between opacity-0"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.15s",
|
animationDelay: "0.15s",
|
||||||
|
@ -914,7 +913,7 @@ function DeviceFileView({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="flex animate-fadeIn items-end justify-end"
|
className="flex animate-fadeIn items-end justify-end opacity-0"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.15s",
|
animationDelay: "0.15s",
|
||||||
|
@ -927,7 +926,7 @@ function DeviceFileView({
|
||||||
)}
|
)}
|
||||||
<hr className="border-slate-800/20 dark:border-slate-300/20" />
|
<hr className="border-slate-800/20 dark:border-slate-300/20" />
|
||||||
<div
|
<div
|
||||||
className="animate-fadeIn space-y-2"
|
className="animate-fadeIn space-y-2 opacity-0"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.20s",
|
animationDelay: "0.20s",
|
||||||
|
@ -959,7 +958,7 @@ function DeviceFileView({
|
||||||
|
|
||||||
{onStorageFiles.length > 0 && (
|
{onStorageFiles.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className="w-full animate-fadeIn"
|
className="w-full animate-fadeIn opacity-0"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.25s",
|
animationDelay: "0.25s",
|
||||||
|
@ -1251,7 +1250,7 @@ function UploadFileView({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="animate-fadeIn space-y-2"
|
className="animate-fadeIn space-y-2 opacity-0"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
}}
|
}}
|
||||||
|
@ -1267,7 +1266,7 @@ function UploadFileView({
|
||||||
<div className="group">
|
<div className="group">
|
||||||
<Card
|
<Card
|
||||||
className={cx("transition-all duration-300", {
|
className={cx("transition-all duration-300", {
|
||||||
"cursor-pointer hover:bg-blue-900/50 dark:hover:bg-blue-900/50":
|
"cursor-pointer hover:bg-blue-50/50 dark:hover:bg-blue-900/50":
|
||||||
uploadState === "idle",
|
uploadState === "idle",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
@ -1282,7 +1281,7 @@ function UploadFileView({
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||||
{incompleteFileName
|
{incompleteFileName
|
||||||
? `Click to select "${incompleteFileName.replace(".incomplete", "")}"`
|
? `Click to select "${incompleteFileName.replace(".incomplete", "")}"`
|
||||||
: "Click to select a file"}
|
: "Click to select a file"}
|
||||||
|
@ -1336,7 +1335,7 @@ function UploadFileView({
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
|
<h3 className="text-sm leading-none font-semibold text-black dark:text-white">
|
||||||
Upload successful
|
Upload successful
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
|
||||||
|
@ -1365,7 +1364,7 @@ function UploadFileView({
|
||||||
{/* Display upload error if present */}
|
{/* Display upload error if present */}
|
||||||
{uploadError && (
|
{uploadError && (
|
||||||
<div
|
<div
|
||||||
className="mt-2 animate-fadeIn truncate text-sm text-red-600 dark:text-red-400"
|
className="mt-2 animate-fadeIn truncate text-sm text-red-600 dark:text-red-400 opacity-0"
|
||||||
style={{ animationDuration: "0.7s" }}
|
style={{ animationDuration: "0.7s" }}
|
||||||
>
|
>
|
||||||
Error: {uploadError}
|
Error: {uploadError}
|
||||||
|
@ -1373,7 +1372,7 @@ function UploadFileView({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex w-full animate-fadeIn items-end"
|
className="flex w-full animate-fadeIn items-end opacity-0"
|
||||||
style={{
|
style={{
|
||||||
animationDuration: "0.7s",
|
animationDuration: "0.7s",
|
||||||
animationDelay: "0.1s",
|
animationDelay: "0.1s",
|
||||||
|
@ -1422,7 +1421,7 @@ function ErrorView({
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center space-x-2 text-red-600">
|
<div className="flex items-center space-x-2 text-red-600">
|
||||||
<ExclamationTriangleIcon className="h-6 w-6" />
|
<ExclamationTriangleIcon className="h-6 w-6" />
|
||||||
<h2 className="text-lg font-bold leading-tight">Mount Error</h2>
|
<h2 className="text-lg leading-tight font-bold">Mount Error</h2>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm leading-snug text-slate-600">
|
<p className="text-sm leading-snug text-slate-600">
|
||||||
An error occurred while attempting to mount the media. Please try again.
|
An error occurred while attempting to mount the media. Please try again.
|
||||||
|
@ -1481,8 +1480,8 @@ function PreUploadedImageItem({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<div className="select-none space-y-0.5">
|
<div className="space-y-0.5 select-none">
|
||||||
<div className="text-sm font-semibold leading-none dark:text-white">
|
<div className="text-sm leading-none font-semibold dark:text-white">
|
||||||
{formatters.truncateMiddle(name, 45)}
|
{formatters.truncateMiddle(name, 45)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center text-sm">
|
<div className="flex items-center text-sm">
|
||||||
|
@ -1494,7 +1493,7 @@ function PreUploadedImageItem({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex select-none items-center gap-x-3">
|
<div className="relative flex items-center gap-x-3 select-none">
|
||||||
<div
|
<div
|
||||||
className={cx("opacity-0 transition-opacity duration-200", {
|
className={cx("opacity-0 transition-opacity duration-200", {
|
||||||
"w-auto opacity-100": isHovering,
|
"w-auto opacity-100": isHovering,
|
||||||
|
@ -1518,7 +1517,7 @@ function PreUploadedImageItem({
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onChange={onSelect}
|
onChange={onSelect}
|
||||||
name={name}
|
name={name}
|
||||||
className="h-3 w-3 border-slate-800/30 bg-white text-blue-700 focus:ring-blue-500 disabled:opacity-30 dark:border-slate-300/20 dark:bg-slate-800"
|
className="form-radio h-3 w-3 border-slate-800/30 bg-white text-blue-700 focus:ring-blue-500 disabled:opacity-30 dark:border-slate-300/20 dark:bg-slate-800"
|
||||||
onClick={e => e.stopPropagation()} // Prevent double-firing of onSelect
|
onClick={e => e.stopPropagation()} // Prevent double-firing of onSelect
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -1540,7 +1539,7 @@ function PreUploadedImageItem({
|
||||||
function ViewHeader({ title, description }: { title: string; description: string }) {
|
function ViewHeader({ title, description }: { title: string; description: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-0">
|
<div className="space-y-0">
|
||||||
<h2 className="text-lg font-bold leading-tight text-black dark:text-white">
|
<h2 className="text-lg leading-tight font-bold text-black dark:text-white">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="text-sm leading-snug text-slate-600 dark:text-slate-400">
|
<div className="text-sm leading-snug text-slate-600 dark:text-slate-400">
|
||||||
|
@ -1558,7 +1557,7 @@ function UsbModeSelector({
|
||||||
setUsbMode: (mode: RemoteVirtualMediaState["mode"]) => void;
|
setUsbMode: (mode: RemoteVirtualMediaState["mode"]) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex select-none flex-col items-start space-y-1">
|
<div className="flex flex-col items-start space-y-1 select-none">
|
||||||
<label className="text-sm font-semibold text-black dark:text-white">Mount as</label>
|
<label className="text-sm font-semibold text-black dark:text-white">Mount as</label>
|
||||||
<div className="flex space-x-4">
|
<div className="flex space-x-4">
|
||||||
<label htmlFor="cdrom" className="flex items-center">
|
<label htmlFor="cdrom" className="flex items-center">
|
||||||
|
@ -1568,7 +1567,7 @@ function UsbModeSelector({
|
||||||
name="mountType"
|
name="mountType"
|
||||||
onChange={() => setUsbMode("CDROM")}
|
onChange={() => setUsbMode("CDROM")}
|
||||||
checked={usbMode === "CDROM"}
|
checked={usbMode === "CDROM"}
|
||||||
className="h-3 w-3 border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
|
className="form-radio h-3 w-3 rounded-full border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
|
||||||
/>
|
/>
|
||||||
<span className="ml-2 text-sm font-medium text-slate-900 dark:text-white">
|
<span className="ml-2 text-sm font-medium text-slate-900 dark:text-white">
|
||||||
CD/DVD
|
CD/DVD
|
||||||
|
@ -1581,13 +1580,11 @@ function UsbModeSelector({
|
||||||
name="mountType"
|
name="mountType"
|
||||||
checked={usbMode === "Disk"}
|
checked={usbMode === "Disk"}
|
||||||
onChange={() => setUsbMode("Disk")}
|
onChange={() => setUsbMode("Disk")}
|
||||||
className="h-3 w-3 border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
|
className="form-radio h-3 w-3 rounded-full border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
|
||||||
/>
|
/>
|
||||||
<div className="ml-2 flex flex-col gap-y-0">
|
<span className="ml-2 text-sm font-medium text-slate-900 dark:text-white">
|
||||||
<span className="text-sm font-medium leading-none text-slate-900 opacity-50 dark:text-white">
|
Disk
|
||||||
Disk
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -43,9 +43,10 @@ export default function SettingsGeneralUpdateRoute() {
|
||||||
|
|
||||||
export interface SystemVersionInfo {
|
export interface SystemVersionInfo {
|
||||||
local: { appVersion: string; systemVersion: string };
|
local: { appVersion: string; systemVersion: string };
|
||||||
remote: { appVersion: string; systemVersion: string };
|
remote?: { appVersion: string; systemVersion: string };
|
||||||
systemUpdateAvailable: boolean;
|
systemUpdateAvailable: boolean;
|
||||||
appUpdateAvailable: boolean;
|
appUpdateAvailable: boolean;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dialog({
|
export function Dialog({
|
||||||
|
@ -142,13 +143,19 @@ function LoadingState({
|
||||||
return new Promise<SystemVersionInfo>((resolve, reject) => {
|
return new Promise<SystemVersionInfo>((resolve, reject) => {
|
||||||
send("getUpdateStatus", {}, async resp => {
|
send("getUpdateStatus", {}, async resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error("Failed to check for updates");
|
notifications.error(`Failed to check for updates: ${resp.error}`);
|
||||||
reject(new Error("Failed to check for updates"));
|
reject(new Error("Failed to check for updates"));
|
||||||
} else {
|
} else {
|
||||||
const result = resp.result as SystemVersionInfo;
|
const result = resp.result as SystemVersionInfo;
|
||||||
setAppVersion(result.local.appVersion);
|
setAppVersion(result.local.appVersion);
|
||||||
setSystemVersion(result.local.systemVersion);
|
setSystemVersion(result.local.systemVersion);
|
||||||
resolve(result);
|
|
||||||
|
if (result.error) {
|
||||||
|
notifications.error(`Failed to check for updates: ${result.error}`);
|
||||||
|
reject(new Error("Failed to check for updates"));
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -235,9 +242,9 @@ function UpdatingDeviceState({
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`For ${type}:\n` +
|
`For ${type}:\n` +
|
||||||
` Download Progress: ${downloadProgress}% (${otaState[`${type}DownloadProgress`]})\n` +
|
` Download Progress: ${downloadProgress}% (${otaState[`${type}DownloadProgress`]})\n` +
|
||||||
` Update Progress: ${updateProgress}% (${otaState[`${type}UpdateProgress`]})\n` +
|
` Update Progress: ${updateProgress}% (${otaState[`${type}UpdateProgress`]})\n` +
|
||||||
` Verification Progress: ${verificationProgress}% (${otaState[`${type}VerificationProgress`]})`,
|
` Verification Progress: ${verificationProgress}% (${otaState[`${type}VerificationProgress`]})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (type === "app") {
|
if (type === "app") {
|
||||||
|
@ -442,13 +449,14 @@ function UpdateAvailableState({
|
||||||
{versionInfo?.systemUpdateAvailable ? (
|
{versionInfo?.systemUpdateAvailable ? (
|
||||||
<>
|
<>
|
||||||
<span className="font-semibold">System:</span>{" "}
|
<span className="font-semibold">System:</span>{" "}
|
||||||
{versionInfo?.remote.systemVersion}
|
{versionInfo?.remote?.systemVersion}
|
||||||
<br />
|
<br />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{versionInfo?.appUpdateAvailable ? (
|
{versionInfo?.appUpdateAvailable ? (
|
||||||
<>
|
<>
|
||||||
<span className="font-semibold">App:</span> {versionInfo?.remote.appVersion}
|
<span className="font-semibold">App:</span>{" "}
|
||||||
|
{versionInfo?.remote?.appVersion}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
import { LuEthernetPort } from "react-icons/lu";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IPv4Mode,
|
IPv4Mode,
|
||||||
|
@ -16,13 +16,18 @@ import {
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import { GridCard } from "@components/Card";
|
import { GridCard } from "@components/Card";
|
||||||
import InputField from "@components/InputField";
|
import InputField, { InputFieldWithLabel } from "@components/InputField";
|
||||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||||
import Fieldset from "@/components/Fieldset";
|
import Fieldset from "@/components/Fieldset";
|
||||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
|
|
||||||
|
import Ipv6NetworkCard from "../components/Ipv6NetworkCard";
|
||||||
|
import EmptyCard from "../components/EmptyCard";
|
||||||
|
import AutoHeight from "../components/AutoHeight";
|
||||||
|
import DhcpLeaseCard from "../components/DhcpLeaseCard";
|
||||||
|
|
||||||
import { SettingsItem } from "./devices.$id.settings";
|
import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
@ -56,15 +61,11 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<strong>{dayjs(lifetime).format("YYYY-MM-DD HH:mm")}</strong>
|
<span className="text-sm font-medium">{remaining && <> {remaining}</>}</span>
|
||||||
{remaining && (
|
<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||||
<>
|
{" "}
|
||||||
{" "}
|
({dayjs(lifetime).format("YYYY-MM-DD HH:mm")})
|
||||||
<span className="text-xs text-slate-700 dark:text-slate-300">
|
</span>
|
||||||
({remaining})
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -114,6 +115,14 @@ export default function SettingsNetworkRoute() {
|
||||||
});
|
});
|
||||||
}, [send]);
|
}, [send]);
|
||||||
|
|
||||||
|
const getNetworkState = useCallback(() => {
|
||||||
|
send("getNetworkState", {}, resp => {
|
||||||
|
if ("error" in resp) return;
|
||||||
|
console.log(resp.result);
|
||||||
|
setNetworkState(resp.result as NetworkState);
|
||||||
|
});
|
||||||
|
}, [send, setNetworkState]);
|
||||||
|
|
||||||
const setNetworkSettingsRemote = useCallback(
|
const setNetworkSettingsRemote = useCallback(
|
||||||
(settings: NetworkSettings) => {
|
(settings: NetworkSettings) => {
|
||||||
setNetworkSettingsLoaded(false);
|
setNetworkSettingsLoaded(false);
|
||||||
|
@ -129,21 +138,14 @@ export default function SettingsNetworkRoute() {
|
||||||
// We need to update the firstNetworkSettings ref to the new settings so we can use it to determine if the settings have changed
|
// We need to update the firstNetworkSettings ref to the new settings so we can use it to determine if the settings have changed
|
||||||
firstNetworkSettings.current = resp.result as NetworkSettings;
|
firstNetworkSettings.current = resp.result as NetworkSettings;
|
||||||
setNetworkSettings(resp.result as NetworkSettings);
|
setNetworkSettings(resp.result as NetworkSettings);
|
||||||
|
getNetworkState();
|
||||||
setNetworkSettingsLoaded(true);
|
setNetworkSettingsLoaded(true);
|
||||||
notifications.success("Network settings saved");
|
notifications.success("Network settings saved");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[send],
|
[getNetworkState, send],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getNetworkState = useCallback(() => {
|
|
||||||
send("getNetworkState", {}, resp => {
|
|
||||||
if ("error" in resp) return;
|
|
||||||
console.log(resp.result);
|
|
||||||
setNetworkState(resp.result as NetworkState);
|
|
||||||
});
|
|
||||||
}, [send, setNetworkState]);
|
|
||||||
|
|
||||||
const handleRenewLease = useCallback(() => {
|
const handleRenewLease = useCallback(() => {
|
||||||
send("renewDHCPLease", {}, resp => {
|
send("renewDHCPLease", {}, resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
|
@ -171,10 +173,6 @@ export default function SettingsNetworkRoute() {
|
||||||
setNetworkSettings({ ...networkSettings, lldp_mode: value as LLDPMode });
|
setNetworkSettings({ ...networkSettings, lldp_mode: value as LLDPMode });
|
||||||
};
|
};
|
||||||
|
|
||||||
// const handleLldpTxTlvsChange = (value: string[]) => {
|
|
||||||
// setNetworkSettings({ ...networkSettings, lldp_tx_tlvs: value });
|
|
||||||
// };
|
|
||||||
|
|
||||||
const handleMdnsModeChange = (value: mDNSMode | string) => {
|
const handleMdnsModeChange = (value: mDNSMode | string) => {
|
||||||
setNetworkSettings({ ...networkSettings, mdns_mode: value as mDNSMode });
|
setNetworkSettings({ ...networkSettings, mdns_mode: value as mDNSMode });
|
||||||
};
|
};
|
||||||
|
@ -258,7 +256,7 @@ export default function SettingsNetworkRoute() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-1">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="Domain"
|
title="Domain"
|
||||||
description="Network domain suffix for the device"
|
description="Network domain suffix for the device"
|
||||||
|
@ -277,19 +275,17 @@ export default function SettingsNetworkRoute() {
|
||||||
</div>
|
</div>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
{selectedDomainOption === "custom" && (
|
{selectedDomainOption === "custom" && (
|
||||||
<div className="flex items-center justify-between gap-x-2">
|
<div className="mt-2 w-1/3 border-l border-slate-800/10 pl-4 dark:border-slate-300/20">
|
||||||
<InputField
|
<InputFieldWithLabel
|
||||||
size="SM"
|
size="SM"
|
||||||
type="text"
|
type="text"
|
||||||
|
label="Custom Domain"
|
||||||
placeholder="home"
|
placeholder="home"
|
||||||
value={customDomain}
|
value={customDomain}
|
||||||
onChange={e => setCustomDomain(e.target.value)}
|
onChange={e => {
|
||||||
/>
|
setCustomDomain(e.target.value);
|
||||||
<Button
|
handleCustomDomainChange(e.target.value);
|
||||||
size="SM"
|
}}
|
||||||
theme="primary"
|
|
||||||
text="Save Domain"
|
|
||||||
onClick={() => handleCustomDomainChange(customDomain)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -359,209 +355,35 @@ export default function SettingsNetworkRoute() {
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
{networkState?.dhcp_lease && (
|
<AutoHeight>
|
||||||
<GridCard>
|
{!networkSettingsLoaded ? (
|
||||||
<div className="p-4">
|
<GridCard>
|
||||||
<div className="space-y-4">
|
<div className="p-4">
|
||||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
<div className="space-y-4">
|
||||||
DHCP Lease
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
</h3>
|
DHCP Lease Information
|
||||||
|
</h3>
|
||||||
<div className="flex gap-x-6 gap-y-2">
|
<div className="animate-pulse space-y-3">
|
||||||
<div className="flex-1 space-y-2">
|
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
{networkState?.dhcp_lease?.ip && (
|
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
<div className="flex justify-between border-slate-800/10 pt-2 dark:border-slate-300/20">
|
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
IP Address
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{networkState?.dhcp_lease?.ip}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.netmask && (
|
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
Subnet Mask
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{networkState?.dhcp_lease?.netmask}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.dns && (
|
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
DNS Servers
|
|
||||||
</span>
|
|
||||||
<span className="text-right text-sm font-medium">
|
|
||||||
{networkState?.dhcp_lease?.dns.map(dns => (
|
|
||||||
<div key={dns}>{dns}</div>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.broadcast && (
|
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
Broadcast
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{networkState?.dhcp_lease?.broadcast}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.domain && (
|
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
Domain
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{networkState?.dhcp_lease?.domain}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.ntp_servers &&
|
|
||||||
networkState?.dhcp_lease?.ntp_servers.length > 0 && (
|
|
||||||
<div className="flex justify-between gap-x-8 border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
|
||||||
<div className="w-full grow text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
NTP Servers
|
|
||||||
</div>
|
|
||||||
<div className="shrink text-right text-sm font-medium">
|
|
||||||
{networkState?.dhcp_lease?.ntp_servers.map(server => (
|
|
||||||
<div key={server}>{server}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.hostname && (
|
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
Hostname
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{networkState?.dhcp_lease?.hostname}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 space-y-2">
|
|
||||||
{networkState?.dhcp_lease?.routers &&
|
|
||||||
networkState?.dhcp_lease?.routers.length > 0 && (
|
|
||||||
<div className="flex justify-between pt-2">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
Gateway
|
|
||||||
</span>
|
|
||||||
<span className="text-right text-sm font-medium">
|
|
||||||
{networkState?.dhcp_lease?.routers.map(router => (
|
|
||||||
<div key={router}>{router}</div>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.server_id && (
|
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
DHCP Server
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{networkState?.dhcp_lease?.server_id}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.lease_expiry && (
|
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
Lease Expires
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
<LifeTimeLabel
|
|
||||||
lifetime={`${networkState?.dhcp_lease?.lease_expiry}`}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.mtu && (
|
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
MTU
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{networkState?.dhcp_lease?.mtu}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.ttl && (
|
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
TTL
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{networkState?.dhcp_lease?.ttl}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.bootp_next_server && (
|
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
Boot Next Server
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{networkState?.dhcp_lease?.bootp_next_server}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.bootp_server_name && (
|
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
Boot Server Name
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{networkState?.dhcp_lease?.bootp_server_name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{networkState?.dhcp_lease?.bootp_file && (
|
|
||||||
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
Boot File
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{networkState?.dhcp_lease?.bootp_file}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
size="SM"
|
|
||||||
theme="light"
|
|
||||||
className="text-red-500"
|
|
||||||
text="Renew DHCP Lease"
|
|
||||||
LeadingIcon={ArrowPathIcon}
|
|
||||||
onClick={() => setShowRenewLeaseConfirm(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</GridCard>
|
||||||
</GridCard>
|
) : networkState?.dhcp_lease && networkState.dhcp_lease.ip ? (
|
||||||
)}
|
<DhcpLeaseCard
|
||||||
|
networkState={networkState}
|
||||||
|
setShowRenewLeaseConfirm={setShowRenewLeaseConfirm}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<EmptyCard
|
||||||
|
IconElm={LuEthernetPort}
|
||||||
|
headline="DHCP Information"
|
||||||
|
description="No DHCP lease information available"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AutoHeight>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsItem title="IPv6 Mode" description="Configure the IPv6 mode">
|
<SettingsItem title="IPv6 Mode" description="Configure the IPv6 mode">
|
||||||
|
@ -579,93 +401,32 @@ export default function SettingsNetworkRoute() {
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
{networkState?.ipv6_addresses && (
|
<AutoHeight>
|
||||||
<GridCard>
|
{!networkSettingsLoaded ? (
|
||||||
<div className="p-4">
|
<GridCard>
|
||||||
<div className="space-y-4">
|
<div className="p-4">
|
||||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
<div className="space-y-4">
|
||||||
IPv6 Information
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
</h3>
|
IPv6 Information
|
||||||
|
</h3>
|
||||||
<div className="grid grid-cols-2 gap-x-6 gap-y-2">
|
<div className="animate-pulse space-y-3">
|
||||||
{networkState?.dhcp_lease?.ip && (
|
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
<div className="flex flex-col justify-between">
|
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
|
||||||
Link-local
|
</div>
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{networkState?.ipv6_link_local}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3 pt-2">
|
|
||||||
{networkState?.ipv6_addresses &&
|
|
||||||
networkState?.ipv6_addresses.length > 0 && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h4 className="text-sm font-semibold">IPv6 Addresses</h4>
|
|
||||||
{networkState.ipv6_addresses.map(addr => (
|
|
||||||
<div
|
|
||||||
key={addr.address}
|
|
||||||
className="rounded-md rounded-l-none border border-slate-500/10 border-l-blue-700/50 bg-slate-100/40 p-4 pl-4 dark:border-blue-500 dark:bg-slate-900"
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
|
||||||
<div className="col-span-2 flex flex-col justify-between">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
Address
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{addr.address}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{addr.valid_lifetime && (
|
|
||||||
<div className="flex flex-col justify-between">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
Valid Lifetime
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{addr.valid_lifetime === "" ? (
|
|
||||||
<span className="text-slate-400 dark:text-slate-600">
|
|
||||||
N/A
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<LifeTimeLabel
|
|
||||||
lifetime={`${addr.valid_lifetime}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{addr.preferred_lifetime && (
|
|
||||||
<div className="flex flex-col justify-between">
|
|
||||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
Preferred Lifetime
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{addr.preferred_lifetime === "" ? (
|
|
||||||
<span className="text-slate-400 dark:text-slate-600">
|
|
||||||
N/A
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<LifeTimeLabel
|
|
||||||
lifetime={`${addr.preferred_lifetime}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</GridCard>
|
||||||
</GridCard>
|
) : networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0 ? (
|
||||||
)}
|
<Ipv6NetworkCard networkState={networkState} />
|
||||||
|
) : (
|
||||||
|
<EmptyCard
|
||||||
|
IconElm={LuEthernetPort}
|
||||||
|
headline="IPv6 Information"
|
||||||
|
description="No IPv6 addresses configured"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AutoHeight>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden space-y-4">
|
<div className="hidden space-y-4">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
|
|
|
@ -703,12 +703,17 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
send("getUpdateStatus", {}, async resp => {
|
send("getUpdateStatus", {}, async resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error("Failed to get device version");
|
notifications.error(`Failed to get device version: ${resp.error}`);
|
||||||
} else {
|
return
|
||||||
const result = resp.result as SystemVersionInfo;
|
|
||||||
setAppVersion(result.local.appVersion);
|
|
||||||
setSystemVersion(result.local.systemVersion);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = resp.result as SystemVersionInfo;
|
||||||
|
if (result.error) {
|
||||||
|
notifications.error(`Failed to get device version: ${result.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAppVersion(result.local.appVersion);
|
||||||
|
setSystemVersion(result.local.systemVersion);
|
||||||
});
|
});
|
||||||
}, [appVersion, send, setAppVersion, setSystemVersion]);
|
}, [appVersion, send, setAppVersion, setSystemVersion]);
|
||||||
|
|
||||||
|
|
|
@ -116,7 +116,7 @@ export default function WelcomeLocalModeRoute() {
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setSelectedMode(mode as "password" | "noPassword");
|
setSelectedMode(mode as "password" | "noPassword");
|
||||||
}}
|
}}
|
||||||
className="absolute top-2 right-2 h-4 w-4 text-blue-600"
|
className="form-radio absolute top-2 right-2 h-4 w-4 text-blue-600"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</GridCard>
|
</GridCard>
|
||||||
|
|
Loading…
Reference in New Issue