Compare commits

...

17 Commits

Author SHA1 Message Date
Scai 20614d7e33
Merge 57fbee1490 into a60e1a5e98 2025-05-20 15:17:36 -05:00
Siyuan Miao a60e1a5e98 chore: bump version to 0.4.0 2025-05-20 20:38:00 +02:00
Siyuan Miao 4e90883bf8 build: enable trimpath for both dev and prod releases 2025-05-20 20:28:40 +02:00
Adam Shiervani 8eaa86ae45
style(ui): update styling for access and network settings components (#487)
* style(ui): update styling for access and network settings components

* fix(ui): simplify conditional rendering in network settings component
2025-05-20 20:26:24 +02:00
Siyuan Miao 354941b54d build: add trimpath to go build command 2025-05-20 20:18:21 +02:00
Aveline 4b91c758fa
chore: upgrade golang to 1.24.3 and nodejs to 22.x (#483) 2025-05-20 19:08:10 +02:00
Adam Shiervani 222a8470a5
refactor: network settings UI (#486)
* feat(ui): update prettier configuration and quote styles

- Add cx to tailwind functions
- Set tailwind stylesheet path
- Convert single quotes to double quotes in CSS
- Add prettier ignore comments for animation utilities

* refactor(ui): extract network information into separate components

- Create DhcpLeaseCard component
- Create Ipv6NetworkCard component

* style(ui): refine component styling and layout

- Add padding to AutoHeight component
- Improve lifetime label display format
- Enhance network information card layouts

* style(ui): enhance checkbox and radio button styling

- Update Checkbox component to use form-checkbox class
- Refactor radio button classes for consistency across components

* style(ui): Add opacity for fade-in animations

* refactor(ui): enhance Modal and network settings components

- Add stable scrollbar gutter to Modal component
- Refactor custom domain input handling and layout adjustments
2025-05-20 15:22:08 +02:00
Scai 57fbee1490 chore: remove unnecessary actions 2025-01-08 00:51:44 +00:00
Scai 0e65c0a9a9 chore: add last go version to matrix 2025-01-08 00:41:33 +00:00
Scai 2dafb5c9c1 chore: update existing comment 2025-01-08 00:37:19 +00:00
Scai 566305549f chore: fix table comment 2025-01-08 00:34:49 +00:00
Scai 1505c37e4c chore: comment fix issues 2025-01-08 00:29:04 +00:00
Scai 564eee9b00 chore: change to artifact 2025-01-08 00:19:52 +00:00
Scai fab575dbe0 chore: permissions update on push 2025-01-08 00:17:38 +00:00
Scai 97958e7b86 chore: push workflow 2025-01-08 00:13:52 +00:00
Scai 2f7042df18 chore: increase permissions to content workflow 2025-01-07 22:58:23 +00:00
Scai 2cadda4e00 chore: create release workflow 2025-01-07 22:40:03 +00:00
26 changed files with 702 additions and 403 deletions

View File

@ -19,13 +19,13 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: v21.1.0
node-version: "22"
cache: "npm"
cache-dependency-path: "**/package-lock.json"
- name: Set up Golang
uses: actions/setup-go@v4
with:
go-version: "1.24.0"
go-version: "1.24.3"
- name: Build frontend
run: |
make frontend

View File

@ -26,7 +26,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: 1.24.x
go-version: 1.24.3
- name: Create empty resource directory
run: |
mkdir -p static && touch static/.gitkeep

126
.github/workflows/push.yaml vendored Normal file
View File

@ -0,0 +1,126 @@
name: Push
on:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
build:
name: Build
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04]
go: [1.21, 1.23.4]
node: [21]
goos: [linux]
goarch: [arm]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- name: Install Dependencies
working-directory: ui
run: npm ci
- name: Build UI
working-directory: ui
run: npm run build:device
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
- name: Install Go Dependencies
run: |
go mod download
- name: Build Binaries
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-s -w -X kvm.builtAppVersion=dev-${GIT_COMMIT:0:7}" -o bin/jetkvm_app cmd/main.go
chmod 755 bin/jetkvm_app
- name: Upload Debug Artifact
uses: actions/upload-artifact@v4
if: ${{ (github.ref == 'refs/heads/main' || github.event_name == 'pull_request') && matrix.go == '1.21' }}
with:
name: jetkvm_app_debug
path: bin/jetkvm_app
comment:
name: Comment
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Generate Links
id: linksa
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ARTIFACT_ID=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[0].id')
echo "ARTIFACT_URL=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID" >> $GITHUB_ENV
echo "LATEST_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Comment on PR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
TITLE="${{ github.event.pull_request.title }}"
PR_NUMBER=${{ github.event.pull_request.number }}
else
TITLE="main branch"
fi
COMMENT=$(cat << EOF
✅ **Build successfully for $TITLE!**
| Name | Link |
|------------------|----------------------------------------------------------------------|
| 🔗 Debug Binary | [Download](${{ env.ARTIFACT_URL }}) |
| 🔗 Latest commit | [${{ env.LATEST_COMMIT }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}) |
EOF
)
# Post Comment
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
# Look for an existing comment
COMMENT_ID=$(gh api repos/${{ github.repository }}/issues/$PR_NUMBER/comments \
--jq '.[] | select(.body | contains("✅ **Build successfully for")) | .id')
if [ -z "$COMMENT_ID" ]; then
# Create a new comment if none exists
gh pr comment $PR_NUMBER --body "$COMMENT"
else
# Update the existing comment
gh api repos/${{ github.repository }}/issues/comments/$COMMENT_ID \
--method PATCH \
-f body="$COMMENT"
fi
else
# Log the comment for main branch
echo "$COMMENT"
fi

91
.github/workflows/release.yaml vendored Normal file
View File

@ -0,0 +1,91 @@
name: Release
on:
push:
tags:
- "v*"
jobs:
release:
name: Release
runs-on: ubuntu-22.04
permissions:
contents: write
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 21
- name: Install Dependencies
working-directory: ui
run: npm ci
- name: Build UI
working-directory: ui
run: npm run build:device
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.21
- name: Build Release Binaries
env:
REF: ${{ github.ref }}
run: |
GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-s -w -X kvm.builtAppVersion=${REF:11}" -o bin/jetkvm_app cmd/main.go
chmod 755 bin/jetkvm_app
- name: Create checksum
env:
REF: ${{ github.ref }}
run: |
SUM=$(shasum -a 256 bin/jetkvm_app | cut -d ' ' -f 1)
echo -e "\n#### SHA256 Checksum\n\`\`\`\n$SUM bin/jetkvm_app\n\`\`\`\n" >> ./RELEASE_CHANGELOG
echo -e "$SUM bin/jetkvm_app\n" > checksums.txt
- name: Create Release Branch
env:
REF: ${{ github.ref }}
run: |
BRANCH=release/${REF:10}
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git checkout -b ${BRANCH}
git push -u origin ${BRANCH}
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
draft: true
prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'beta') || contains(github.ref, 'alpha') }}
body_path: ./RELEASE_CHANGELOG
- name: Upload JetKVM binary
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: bin/jetkvm_app
asset_name: jetkvm_app
asset_content_type: application/octet-stream
- name: Upload checksum
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./checksums.txt
asset_name: checksums.txt
asset_content_type: text/plain

View File

@ -106,7 +106,7 @@ jobs:
- name: Set up Golang
uses: actions/setup-go@v4
with:
go-version: "1.24.0"
go-version: "1.24.3"
- name: Golang Test Report
uses: becheran/go-testreport@v0.3.2
with:

View File

@ -2,8 +2,8 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
BUILDDATE ?= $(shell date -u +%FT%T%z)
BUILDTS ?= $(shell date -u +%s)
REVISION ?= $(shell git rev-parse HEAD)
VERSION_DEV := 0.4.0-dev$(shell date +%Y%m%d%H%M)
VERSION := 0.3.9
VERSION_DEV := 0.4.1-dev$(shell date +%Y%m%d%H%M)
VERSION := 0.4.0
PROMETHEUS_TAG := github.com/prometheus/common/version
KVM_PKG_NAME := github.com/jetkvm/kvm
@ -25,7 +25,10 @@ hash_resource:
build_dev: hash_resource
@echo "Building..."
$(GO_CMD) build -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" -o $(BIN_DIR)/jetkvm_app cmd/main.go
$(GO_CMD) build \
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \
-trimpath \
-o $(BIN_DIR)/jetkvm_app cmd/main.go
build_test2json:
$(GO_CMD) build -o $(BIN_DIR)/test2json cmd/test2json
@ -66,7 +69,10 @@ dev_release: frontend build_dev
build_release: frontend hash_resource
@echo "Building release..."
$(GO_CMD) build -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" -o bin/jetkvm_app cmd/main.go
$(GO_CMD) build \
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" \
-trimpath \
-o bin/jetkvm_app cmd/main.go
release:
@if rclone lsf r2://jetkvm-update/app/$(VERSION)/ | grep -q "jetkvm_app"; then \

View File

@ -6,6 +6,7 @@
"arrowParens": "avoid",
"singleQuote": false,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindFunctions": ["clsx"],
"printWidth": 90
"tailwindFunctions": ["clsx", "cx"],
"printWidth": 90,
"tailwindStylesheet": "./src/index.css"
}

View File

@ -22,7 +22,7 @@ const AutoHeight = ({ children, ...props }: { children: React.ReactNode }) => {
{...props}
height={height}
duration={300}
contentClassName="h-fit"
contentClassName="h-fit p-px"
contentRef={contentDiv}
disableDisplayNone
>

View File

@ -12,7 +12,7 @@ const sizes = {
const checkboxVariants = cva({
base: cx(
"block rounded",
"form-checkbox block rounded",
// 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",

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -20,7 +20,9 @@ const Modal = React.memo(function Modal({
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"
/>
<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 */}
<div className="flex min-h-full items-end justify-center p-4 text-center md:items-baseline md:p-4">
<DialogPanel

View File

@ -107,7 +107,7 @@ export function ATXPowerControl() {
<LoadingSpinner className="h-6 w-6 text-blue-500 dark:text-blue-400" />
</Card>
) : (
<Card className="h-[120px] animate-fadeIn">
<Card className="h-[120px] animate-fadeIn opacity-0">
<div className="space-y-4 p-3">
{/* Control Buttons */}
<div className="flex items-center space-x-2">

View File

@ -63,7 +63,7 @@ export function DCPowerControl() {
<LoadingSpinner className="h-6 w-6 text-blue-500 dark:text-blue-400" />
</Card>
) : (
<Card className="h-[160px] animate-fadeIn">
<Card className="h-[160px] animate-fadeIn opacity-0">
<div className="space-y-4 p-3">
{/* Power Controls */}
<div className="flex items-center space-x-2">

View File

@ -58,7 +58,7 @@ export function SerialConsole() {
description="Configure your serial console settings"
/>
<Card className="animate-fadeIn">
<Card className="animate-fadeIn opacity-0">
<div className="space-y-4 p-3">
{/* Open Console Button */}
<div className="flex items-center">

View File

@ -92,7 +92,7 @@ export default function ExtensionPopover() {
{renderActiveExtension()}
<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={{
animationDuration: "0.7s",
animationDelay: "0.2s",
@ -113,7 +113,7 @@ export default function ExtensionPopover() {
title="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">
{AVAILABLE_EXTENSIONS.map(extension => (
<div

View File

@ -214,7 +214,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
) : null}
<div
className="animate-fadeIn space-y-2"
className="animate-fadeIn opacity-0 space-y-2"
style={{
animationDuration: "0.7s",
animationDelay: "0.1s",
@ -289,7 +289,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
{!remoteVirtualMediaState && (
<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={{
animationDuration: "0.7s",
animationDelay: "0.2s",

View File

@ -83,7 +83,7 @@ export default function PasteModal() {
/>
<div
className="animate-fadeIn space-y-2"
className="animate-fadeIn opacity-0 space-y-2"
style={{
animationDuration: "0.7s",
animationDelay: "0.1s",
@ -137,7 +137,7 @@ export default function PasteModal() {
</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={{
animationDuration: "0.7s",
animationDelay: "0.2s",

View File

@ -26,7 +26,7 @@ export default function AddDeviceForm({
return (
<div className="space-y-4">
<div
className="animate-fadeIn space-y-4"
className="animate-fadeIn opacity-0 space-y-4"
style={{
animationDuration: "0.5s",
animationFillMode: "forwards",
@ -73,7 +73,7 @@ export default function AddDeviceForm({
/>
</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={{
animationDuration: "0.7s",
animationDelay: "0.2s",

View File

@ -28,7 +28,7 @@ export default function DeviceList({
}: DeviceListProps) {
return (
<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">
{storedDevices.map((device, index) => (
<div key={index} className="flex items-center justify-between gap-x-2 p-3">
@ -63,7 +63,7 @@ export default function DeviceList({
</div>
</Card>
<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={{
animationDuration: "0.7s",
animationDelay: "0.2s",

View File

@ -13,7 +13,7 @@ export default function EmptyStateCard({
}) {
return (
<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="space-y-3">
<div className="space-y-1">
@ -35,7 +35,7 @@ export default function EmptyStateCard({
</div>
</Card>
<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={{
animationDuration: "0.7s",
animationDelay: "0.2s",

View File

@ -1,4 +1,4 @@
@import 'tailwindcss';
@import "tailwindcss";
@config "../tailwind.config.js";
@ -10,10 +10,10 @@
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--font-sans: 'Circular', sans-serif;
--font-display: 'Circular', sans-serif;
--font-serif: 'Circular', serif;
--font-mono: 'Source Code Pro Variable', monospace;
--font-sans: "Circular", sans-serif;
--font-display: "Circular", sans-serif;
--font-serif: "Circular", serif;
--font-mono: "Source Code Pro Variable", monospace;
--grid-layout: auto 1fr auto;
--grid-headerBody: auto 1fr;
@ -122,15 +122,24 @@
}
}
/* 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-* {
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-* {
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 {
@apply scroll-smooth;
}

View File

@ -38,7 +38,6 @@ import {
useRTCStore,
} from "../hooks/stores";
export default function MountRoute() {
const navigate = useNavigate();
{
@ -283,8 +282,8 @@ function ModeSelectionView({
return (
<div className="w-full space-y-4">
<div className="animate-fadeIn space-y-0">
<h2 className="text-lg font-bold leading-tight dark:text-white">
<div className="animate-fadeIn space-y-0 opacity-0">
<h2 className="text-lg leading-tight font-bold dark:text-white">
Virtual Media Source
</h2>
<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) => (
<div
key={label}
className={cx("animate-fadeIn")}
className="animate-fadeIn opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: `${25 * (index * 5)}ms`,
@ -337,7 +336,7 @@ function ModeSelectionView({
)}
>
<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={() =>
disabled ? null : setSelectedMode(mode as "browser" | "url" | "device")
}
@ -365,7 +364,7 @@ function ModeSelectionView({
value={mode}
disabled={disabled}
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>
</Card>
@ -373,7 +372,7 @@ function ModeSelectionView({
))}
</div>
<div
className="flex animate-fadeIn justify-end"
className="flex animate-fadeIn justify-end opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.2s",
@ -437,19 +436,19 @@ function BrowserFileView({
className="block cursor-pointer select-none"
>
<div
className="group animate-fadeIn"
className="group animate-fadeIn opacity-0"
style={{
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="flex h-full flex-col items-center justify-center text-center">
{selectedFile ? (
<>
<div className="space-y-1">
<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)}
</h3>
<p className="text-xs leading-none text-slate-700">
@ -460,7 +459,7 @@ function BrowserFileView({
) : (
<div className="space-y-1">
<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
</h3>
<p className="text-xs leading-none text-slate-700">
@ -483,7 +482,7 @@ function BrowserFileView({
</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={{
animationDuration: "0.7s",
animationDelay: "0.1s",
@ -578,7 +577,7 @@ function UrlView({
/>
<div
className="animate-fadeIn"
className="animate-fadeIn opacity-0"
style={{
animationDuration: "0.7s",
}}
@ -593,7 +592,7 @@ function UrlView({
/>
</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={{
animationDuration: "0.7s",
animationDelay: "0.1s",
@ -619,7 +618,7 @@ function UrlView({
<hr className="border-slate-800/30 dark:border-slate-300/20" />
<div
className="animate-fadeIn"
className="animate-fadeIn opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.2s",
@ -628,13 +627,13 @@ function UrlView({
<h2 className="mb-2 text-sm font-semibold text-black dark:text-white">
Popular images
</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) => (
<div key={index} className="flex items-center justify-between gap-x-4 p-3.5">
<div className="flex items-center gap-x-4">
<img src={image.icon} alt={`${image.name} Icon`} className="w-6" />
<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)}
</h3>
{image.description && (
@ -797,7 +796,7 @@ function DeviceFileView({
description="Select an image to mount from the JetKVM storage"
/>
<div
className="w-full animate-fadeIn"
className="w-full animate-fadeIn opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.1s",
@ -809,7 +808,7 @@ function DeviceFileView({
<div className="space-y-3">
<div className="space-y-1">
<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
</h3>
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
@ -827,7 +826,7 @@ function DeviceFileView({
</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) => (
<PreUploadedImageItem
key={index}
@ -886,7 +885,7 @@ function DeviceFileView({
{onStorageFiles.length > 0 ? (
<div
className="flex animate-fadeIn items-end justify-between"
className="flex animate-fadeIn items-end justify-between opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.15s",
@ -914,7 +913,7 @@ function DeviceFileView({
</div>
) : (
<div
className="flex animate-fadeIn items-end justify-end"
className="flex animate-fadeIn items-end justify-end opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.15s",
@ -927,7 +926,7 @@ function DeviceFileView({
)}
<hr className="border-slate-800/20 dark:border-slate-300/20" />
<div
className="animate-fadeIn space-y-2"
className="animate-fadeIn space-y-2 opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.20s",
@ -959,7 +958,7 @@ function DeviceFileView({
{onStorageFiles.length > 0 && (
<div
className="w-full animate-fadeIn"
className="w-full animate-fadeIn opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.25s",
@ -1251,7 +1250,7 @@ function UploadFileView({
}
/>
<div
className="animate-fadeIn space-y-2"
className="animate-fadeIn space-y-2 opacity-0"
style={{
animationDuration: "0.7s",
}}
@ -1267,7 +1266,7 @@ function UploadFileView({
<div className="group">
<Card
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",
})}
>
@ -1282,7 +1281,7 @@ function UploadFileView({
</div>
</Card>
</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
? `Click to select "${incompleteFileName.replace(".incomplete", "")}"`
: "Click to select a file"}
@ -1336,7 +1335,7 @@ function UploadFileView({
</div>
</Card>
</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
</h3>
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
@ -1365,7 +1364,7 @@ function UploadFileView({
{/* Display upload error if present */}
{uploadError && (
<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" }}
>
Error: {uploadError}
@ -1373,7 +1372,7 @@ function UploadFileView({
)}
<div
className="flex w-full animate-fadeIn items-end"
className="flex w-full animate-fadeIn items-end opacity-0"
style={{
animationDuration: "0.7s",
animationDelay: "0.1s",
@ -1422,7 +1421,7 @@ function ErrorView({
<div className="space-y-2">
<div className="flex items-center space-x-2 text-red-600">
<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>
<p className="text-sm leading-snug text-slate-600">
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="select-none space-y-0.5">
<div className="text-sm font-semibold leading-none dark:text-white">
<div className="space-y-0.5 select-none">
<div className="text-sm leading-none font-semibold dark:text-white">
{formatters.truncateMiddle(name, 45)}
</div>
<div className="flex items-center text-sm">
@ -1494,7 +1493,7 @@ function PreUploadedImageItem({
</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
className={cx("opacity-0 transition-opacity duration-200", {
"w-auto opacity-100": isHovering,
@ -1518,7 +1517,7 @@ function PreUploadedImageItem({
checked={isSelected}
onChange={onSelect}
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
/>
) : (
@ -1540,7 +1539,7 @@ function PreUploadedImageItem({
function ViewHeader({ title, description }: { title: string; description: string }) {
return (
<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}
</h2>
<div className="text-sm leading-snug text-slate-600 dark:text-slate-400">
@ -1558,7 +1557,7 @@ function UsbModeSelector({
setUsbMode: (mode: RemoteVirtualMediaState["mode"]) => void;
}) {
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>
<div className="flex space-x-4">
<label htmlFor="cdrom" className="flex items-center">
@ -1568,7 +1567,7 @@ function UsbModeSelector({
name="mountType"
onChange={() => setUsbMode("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">
CD/DVD
@ -1581,13 +1580,11 @@ function UsbModeSelector({
name="mountType"
checked={usbMode === "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="text-sm font-medium leading-none text-slate-900 opacity-50 dark:text-white">
<span className="ml-2 text-sm font-medium text-slate-900 dark:text-white">
Disk
</span>
</div>
</label>
</div>
</div>

View File

@ -409,7 +409,7 @@ export default function SettingsAccessIndexRoute() {
.
</div>
</div>
<hr className="block w-full dark:border-slate-600" />
<hr className="block w-full border-slate-800/20 dark:border-slate-300/20" />
<div>
<LinkButton

View File

@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { ArrowPathIcon } from "@heroicons/react/24/outline";
import { LuEthernetPort } from "react-icons/lu";
import {
IPv4Mode,
@ -16,13 +16,18 @@ import {
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { Button } from "@components/Button";
import { GridCard } from "@components/Card";
import InputField from "@components/InputField";
import InputField, { InputFieldWithLabel } from "@components/InputField";
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
import { SettingsPageHeader } from "@/components/SettingsPageheader";
import Fieldset from "@/components/Fieldset";
import { ConfirmDialog } from "@/components/ConfirmDialog";
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";
dayjs.extend(relativeTime);
@ -56,16 +61,12 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
return (
<>
<strong>{dayjs(lifetime).format("YYYY-MM-DD HH:mm")}</strong>
{remaining && (
<>
{" "}
<span className="text-sm font-medium">{remaining && <> {remaining}</>}</span>
<span className="text-xs text-slate-700 dark:text-slate-300">
({remaining})
{" "}
({dayjs(lifetime).format("YYYY-MM-DD HH:mm")})
</span>
</>
)}
</>
);
}
@ -114,6 +115,14 @@ export default function SettingsNetworkRoute() {
});
}, [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(
(settings: NetworkSettings) => {
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
firstNetworkSettings.current = resp.result as NetworkSettings;
setNetworkSettings(resp.result as NetworkSettings);
getNetworkState();
setNetworkSettingsLoaded(true);
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(() => {
send("renewDHCPLease", {}, resp => {
if ("error" in resp) {
@ -171,10 +173,6 @@ export default function SettingsNetworkRoute() {
setNetworkSettings({ ...networkSettings, lldp_mode: value as LLDPMode });
};
// const handleLldpTxTlvsChange = (value: string[]) => {
// setNetworkSettings({ ...networkSettings, lldp_tx_tlvs: value });
// };
const handleMdnsModeChange = (value: mDNSMode | string) => {
setNetworkSettings({ ...networkSettings, mdns_mode: value as mDNSMode });
};
@ -258,7 +256,7 @@ export default function SettingsNetworkRoute() {
</div>
<div className="space-y-4">
<div className="space-y-4">
<div className="space-y-1">
<SettingsItem
title="Domain"
description="Network domain suffix for the device"
@ -277,19 +275,17 @@ export default function SettingsNetworkRoute() {
</div>
</SettingsItem>
{selectedDomainOption === "custom" && (
<div className="flex items-center justify-between gap-x-2">
<InputField
<div className="mt-2 w-1/3 border-l border-slate-800/10 pl-4 dark:border-slate-300/20">
<InputFieldWithLabel
size="SM"
type="text"
label="Custom Domain"
placeholder="home"
value={customDomain}
onChange={e => setCustomDomain(e.target.value)}
/>
<Button
size="SM"
theme="primary"
text="Save Domain"
onClick={() => handleCustomDomainChange(customDomain)}
onChange={e => {
setCustomDomain(e.target.value);
handleCustomDomainChange(e.target.value);
}}
/>
</div>
)}
@ -359,209 +355,35 @@ export default function SettingsNetworkRoute() {
])}
/>
</SettingsItem>
{networkState?.dhcp_lease && (
<AutoHeight>
{!networkSettingsLoaded && !networkState?.dhcp_lease ? (
<GridCard>
<div className="p-4">
<div className="space-y-4">
<h3 className="text-base font-bold text-slate-900 dark:text-white">
DHCP Lease
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={ArrowPathIcon}
onClick={() => setShowRenewLeaseConfirm(true)}
/>
<div className="animate-pulse space-y-3">
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
</div>
</div>
</div>
</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 className="space-y-4">
<SettingsItem title="IPv6 Mode" description="Configure the IPv6 mode">
@ -579,93 +401,33 @@ export default function SettingsNetworkRoute() {
])}
/>
</SettingsItem>
{networkState?.ipv6_addresses && (
<AutoHeight>
{!networkSettingsLoaded &&
!(networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0) ? (
<GridCard>
<div className="p-4">
<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-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 className="animate-pulse space-y-3">
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
</div>
</div>
</div>
</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 className="hidden space-y-4">
<SettingsItem

View File

@ -116,7 +116,7 @@ export default function WelcomeLocalModeRoute() {
onChange={() => {
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>
</GridCard>