diff --git a/app/src/components/admin/ControllerPage.js b/app/src/components/admin/ControllerPage.js index 01a18f51..9f52b8c1 100644 --- a/app/src/components/admin/ControllerPage.js +++ b/app/src/components/admin/ControllerPage.js @@ -1,6 +1,23 @@ import React from 'react'; -import {Checkbox, Container, Dropdown, Form, Icon, Input} from "semantic-ui-react"; -import {Button, Confirm, Header, Loader, Modal, Segment, Table} from "../Theme"; +import {IconInfoCircle, IconPlayerEject} from '@tabler/icons-react'; +import { + Button, + Checkbox, + Confirm, + Group, + Header, + Icon, + IconButton, + Loading, + Message, + Modal, + Panel, + Select, + Status, + Table, + TextInput, + toast, +} from "../ui"; import {APIButton, BluetoothToggle, DesktopToggle, DirectorySearch, HandPointMessage, HotspotToggle, InfoMessage, ThrottleToggle, Toggle, VncToggle,} from "../Common"; import {useDockerized, useMediaDirectory} from "../../hooks/customHooks"; import {Media} from "../../contexts/contexts"; @@ -29,18 +46,18 @@ import { updateHotspotSettings, } from "../../api/controller"; import QRCode from "react-qr-code"; -import {toast} from "../ui"; import {RestartButton, ShutdownButton} from "./Settings"; -import Message from "semantic-ui-react/dist/commonjs/collections/Message"; import {CONTROLLER_URI} from "../Vars"; -// Service status color mapping -const statusColors = { - running: 'green', - stopped: 'red', - failed: 'red', - unknown: 'yellow', +// Service status kind mapping. Status only knows complete/active/pending/failed, +// so "stopped" and "unknown" both land on pending -- the Start/Stop button and +// service name still tell them apart, this dot only carries running vs. failed. +const statusKinds = { + running: 'complete', + stopped: 'pending', + failed: 'failed', + unknown: 'pending', }; // Sort order within a group: problems first so they stay visible. @@ -69,11 +86,11 @@ export const groupServices = (services) => { const linesOptions = [ - {key: 100, text: '100 lines', value: 100}, - {key: 250, text: '250 lines', value: 250}, - {key: 500, text: '500 lines', value: 500}, - {key: 1000, text: '1000 lines', value: 1000}, - {key: 5000, text: '5000 lines', value: 5000}, + {value: '100', label: '100 lines'}, + {value: '250', label: '250 lines'}, + {value: '500', label: '500 lines'}, + {value: '1000', label: '1000 lines'}, + {value: '5000', label: '5000 lines'}, ]; // Custom hook for service row state and handlers @@ -157,9 +174,10 @@ function useServiceRow(service, onAction) { fetchLogs(linesCount, true); // Show loading on initial open }; - const handleLinesChange = (e, {value}) => { - setLinesCount(value); - fetchLogs(value); + const handleLinesChange = (value) => { + const lines = parseInt(value, 10); + setLinesCount(lines); + fetchLogs(lines); }; const handleDownloadLogs = () => { @@ -175,7 +193,7 @@ function useServiceRow(service, onAction) { }; const isRunning = service.status === 'running'; - const statusColor = statusColors[service.status] || 'grey'; + const statusKind = statusKinds[service.status] || 'pending'; // Build view URL if service is viewable let viewUrl = null; @@ -191,7 +209,7 @@ function useServiceRow(service, onAction) { return { loading, logsOpen, setLogsOpen, logs, logsLoading, linesCount, countdown, logsRef, handleAction, fetchLogs, handleViewLogs, handleLinesChange, handleDownloadLogs, - isRunning, statusColor, viewUrl + isRunning, statusKind, viewUrl }; } @@ -205,7 +223,6 @@ function ServiceLogsModal({ logsRef, linesCount, countdown, - handleViewLogs, handleLinesChange, handleDownloadLogs, fetchLogs @@ -214,24 +231,22 @@ function ServiceLogsModal({ setLogsOpen(false)} - onOpen={handleViewLogs} - trigger={ - + @@ -1009,35 +1023,29 @@ function DiskSection() { if (dockerized) { return ( - -
- - Disk Management -
+ +
Disk Management

Disk management is not available in Docker environments.

-
+ ); } if (loading) { - return + return
Disk Management
- -
; + + ; } return ( - -
- - Disk Management -
+ +
Disk Management
- {error &&

Some disk information unavailable: {error}

} + {error && Some disk information unavailable: {error}}
Disks
{disks.length > 0 ? ( - +
Name @@ -1067,7 +1075,7 @@ function DiskSection() { {isMounted ? ( {disk.mountpoint} ) : ( - - + - )} @@ -1078,28 +1086,30 @@ function DiskSection() { label={persistent ? 'Enabled' : 'Disabled'} /> ) : ( - - + - )} {isMounted && isSystemMount ? ( - System + System ) : isMounted ? ( ) : ( )} @@ -1113,9 +1123,10 @@ function DiskSection() {
SMART Health
{!smartAvailable ? ( -

SMART monitoring not available (pySMART not installed or not supported)

+

SMART monitoring not available (pySMART not installed or not + supported)

) : smartDrives.length > 0 ? ( -
+
Device @@ -1129,10 +1140,9 @@ function DiskSection() { {drive.device} - + + ● + {' '} {drive.health || drive.assessment || 'Unknown'} {drive.smart_limited ? ' (limited)' : ''} @@ -1143,11 +1153,13 @@ function DiskSection() { ))} @@ -1172,12 +1184,19 @@ function DiskSection() { + title='Unmount Disk' + destructive + confirmLabel='Unmount' + onCancel={() => { + setUnmountConfirmOpen(false); + setUnmountTarget(null); + }} + onConfirm={handleUnmountConfirm} + > + {unmountTarget === PRIMARY_MOUNT ? ( +

Are you sure you want to unmount {unmountTarget}?

- This is the primary WROLPi drive. The WROLPi API service will be stopped before unmounting, and your media will be unavailable until a drive is mounted at {PRIMARY_MOUNT} and @@ -1185,19 +1204,12 @@ function DiskSection() {

) : `Are you sure you want to unmount ${unmountTarget}?`} - onCancel={() => { - setUnmountConfirmOpen(false); - setUnmountTarget(null); - }} - onConfirm={handleUnmountConfirm} - confirmButton='Unmount' - /> +
- - + ); } @@ -1271,35 +1283,29 @@ function SambaSection() { }; if (loading) { - return ; + return ; } if (!status || !status.available) { if (dockerized) return null; return ( - -
- - Network Shares (Samba) -
- Samba is not available on this system. -
+ +
Network Shares (Samba)
+ Samba is not available on this system. +
); } const shares = status.shares || []; return ( - -
- - Network Shares (Samba) -
+ +
Network Shares (Samba)

Configure directories to share over the local network. Start the smbd service to activate sharing.

{shares.length > 0 && ( -
+
Name @@ -1317,10 +1323,11 @@ function SambaSection() { {share.read_only ? 'Yes' : 'No'} {share.comment} - - { setAddOpen(false); setShareAll(true); }} size='small'> + { + setAddOpen(false); + setShareAll(true); + }} size='small'> Add Samba Share -
- - - setNewName(value)} + setNewName(e.target.value)} + /> +
+ setShareAll(e.currentTarget.checked)} + /> +
+ {!shareAll && ( +
+ + - - - setShareAll(checked)} - /> - - {!shareAll && ( - - - - - )} - - setNewReadOnly(checked)} - /> - - - - setNewComment(value)} - /> - - +
+ )} +
+ setNewReadOnly(e.currentTarget.checked)} + /> +
+ setNewComment(e.target.value)} + />
- +
- + ); } @@ -1493,99 +1503,93 @@ function HotspotSettingsForm() { }; if (loading) { - return -
- - Hotspot Settings -
- -
; + return +
Hotspot Settings
+ +
; } // Keep the saved device selectable even if it is not currently present // (e.g. a USB WiFi dongle is unplugged). const deviceOptions = [...new Set([...devices, form.device].filter(Boolean))] - .map(device => ({key: device, value: device, text: device})); + .map(device => ({value: device, label: device})); // Keep the saved protocol selectable even if the device does not report it. const protocolOptions = [...new Set([...protocols, form.protocol].filter(Boolean))] - .map(protocol => ({key: protocol, value: protocol, text: hotspotProtocolLabels[protocol] || protocol})); + .map(protocol => ({value: protocol, label: hotspotProtocolLabels[protocol] || protocol})); // Special string which allows a mobile device to connect to a specific Wi-Fi. // The WPA QR format requires backslash-escaping of \ ; , " in the SSID and password. const escapeWifi = (s) => s.replace(/([\\;,"])/g, '\\$1'); const qrCodeValue = `WIFI:S:${escapeWifi(form.ssid)};T:WPA;P:${escapeWifi(form.password)};;`; - return -
- - Hotspot Settings -
-
- - setForm({...form, ssid: value})} - /> - setForm({...form, password: value})} - /> - setForm({...form, device: value})} - /> - setForm({...form, protocol: value})} - /> - + return +
Hotspot Settings
+ + setForm({...form, ssid: e.target.value})} + /> + setForm({...form, password: e.target.value})} + /> + setForm({...form, protocol: value})} + /> + +
- setQrOpen(false)} - onOpen={() => setQrOpen(true)} - open={qrOpen} - trigger={
)} -
+ ); } export function ControllerPage() { return ( - +
- +
); } diff --git a/app/src/components/admin/ControllerPage.test.js b/app/src/components/admin/ControllerPage.test.js index 9a1f789a..62ddad71 100644 --- a/app/src/components/admin/ControllerPage.test.js +++ b/app/src/components/admin/ControllerPage.test.js @@ -1,7 +1,9 @@ import React from 'react'; -import {fireEvent, render, screen, waitFor} from '@testing-library/react'; +import {fireEvent, render, screen, waitFor, within} from '@testing-library/react'; import {BrowserRouter} from 'react-router'; +import {MantineProvider} from '@mantine/core'; import {ThemeContext, SettingsContext} from '../../contexts/contexts'; +import {cssVariablesResolver, mantineTheme} from '../../themes/mantine'; import {ControllerPage} from './ControllerPage'; // Mock the controller API @@ -134,11 +136,13 @@ const defaultSettings = { const renderControllerPage = () => { return render( - - - - - + + + + + + + ); }; @@ -279,9 +283,11 @@ describe('DiskSection primary unmount', () => { expect(screen.getByText(/API service will/)).toBeInTheDocument(); }); - // Semantic's Confirm renders confirmButton='Unmount' as the last match. - const buttons = screen.getAllByText('Unmount'); - fireEvent.click(buttons[buttons.length - 1]); + // The Confirm modal is portaled to the end of , so it renders + // *before* the page content in DOM order -- scope the query to the + // dialog rather than assuming which match comes last. + const dialog = screen.getByRole('dialog'); + fireEvent.click(within(dialog).getByText('Unmount')); await waitFor(() => { expect(controllerApi.unmountDisk).toHaveBeenCalledWith('/media/wrolpi'); diff --git a/app/src/semantic-baseline.json b/app/src/semantic-baseline.json index 8f52ca63..d6769cfd 100644 --- a/app/src/semantic-baseline.json +++ b/app/src/semantic-baseline.json @@ -12,6 +12,5 @@ "src/components/Upload.js", "src/components/VideoPlayer.js", "src/components/Zim.js", - "src/components/admin/ControllerPage.js", "src/components/admin/Downloads.js" ]