From d4fc3d1c07d65c430eeee368b2a84d6bc33bf81d Mon Sep 17 00:00:00 2001 From: Roland Date: Thu, 3 Oct 2024 13:35:06 -0600 Subject: [PATCH] Moving Configs to SettingsPage. --- app/src/api.js | 25 +- app/src/components/admin/AdminRoute.js | 3 - app/src/components/admin/Configs.js | 39 +- app/src/components/admin/Settings.js | 682 ++++++++++++------------- app/src/contexts/contexts.js | 2 + app/src/hooks/customHooks.js | 23 +- 6 files changed, 375 insertions(+), 399 deletions(-) diff --git a/app/src/api.js b/app/src/api.js index 6295be58..9e693b36 100644 --- a/app/src/api.js +++ b/app/src/api.js @@ -271,7 +271,30 @@ export async function getStatus() { export async function getSettings() { const response = await apiGet(`${API_URI}/settings`); if (response.ok) { - return await response.json(); + const content = await response.json(); + return { + archive_destination: content.archive_destination, + download_manager_disabled: content.download_manager_disabled, + download_manager_stopped: content.download_manager_stopped, + download_on_startup: content.download_on_startup, + download_timeout: content.download_timeout, + hotspot_device: content.hotspot_device, + hotspot_on_startup: content.hotspot_on_startup, + hotspot_password: content.hotspot_password, + hotspot_ssid: content.hotspot_ssid, + hotspot_status: content.hotspot_status, + ignore_outdated_zims: content.ignore_outdated_zims, + ignored_directories: content.ignored_directories, + log_level: content.log_level, + map_destination: content.map_destination, + nav_color: content.nav_color, + media_directory: content.media_directory, + throttle_on_startup: content.throttle_on_startup, + throttle_status: content.throttle_status, + videos_destination: content.videos_destination, + wrol_mode: content.wrol_mode, + zims_destination: content.zims_destination, + } } else { const message = await getErrorMessage(response, 'Could not get settings.'); toast({ diff --git a/app/src/components/admin/AdminRoute.js b/app/src/components/admin/AdminRoute.js index b9e5b9b4..f0e6f50d 100644 --- a/app/src/components/admin/AdminRoute.js +++ b/app/src/components/admin/AdminRoute.js @@ -8,7 +8,6 @@ import {Header, Loader, Segment} from "../Theme"; import {DownloadsPage} from "./Downloads"; import {SettingsPage} from "./Settings"; import {StatusPage} from "./Status"; -import {ConfigsPage} from "./Configs"; class WROLMode extends React.Component { @@ -72,7 +71,6 @@ export default function AdminRoute() { {text: 'Downloads', to: '/admin', key: 'admin', end: true}, {text: 'Settings', to: '/admin/settings', key: 'settings'}, {text: 'Status', to: '/admin/status', key: 'status'}, - {text: 'Configs', to: '/admin/configs', key: 'configs'}, {text: 'WROL Mode', to: '/admin/wrol', key: 'wrol'}, ]; @@ -82,7 +80,6 @@ export default function AdminRoute() { }/> }/> }/> - }/> }/> diff --git a/app/src/components/admin/Configs.js b/app/src/components/admin/Configs.js index 5bb47dc4..58e5c9e6 100644 --- a/app/src/components/admin/Configs.js +++ b/app/src/components/admin/Configs.js @@ -66,7 +66,7 @@ function ConfigTableRow({config, importConfig, saveConfig, fetchConfigs}) { } -function ConfigsTable({configs, loading, importConfig, saveConfig, fetchConfigs}) { +export function ConfigsTable({configs, loading, importConfig, saveConfig, fetchConfigs}) { let body = @@ -109,40 +109,3 @@ function ConfigsTable({configs, loading, importConfig, saveConfig, fetchConfigs} } - -export function ConfigsPage() { - const {configs, loading, importConfig, saveConfig, fetchConfigs} = useConfigs(); - - React.useEffect(() => { - fetchConfigs(); - }, []); - - const refreshButton = ; - const refreshPopupButton = ; - - return - - - -
Configs
-
- {refreshPopupButton} -
-
- - -
-} \ No newline at end of file diff --git a/app/src/components/admin/Settings.js b/app/src/components/admin/Settings.js index 676eb6e4..6bb8094b 100644 --- a/app/src/components/admin/Settings.js +++ b/app/src/components/admin/Settings.js @@ -1,5 +1,5 @@ import React from "react"; -import {getSettings, postRestart, postShutdown, saveSettings} from "../../api"; +import {postRestart, postShutdown} from "../../api"; import { Button, Divider, @@ -12,6 +12,7 @@ import { Modal, ModalContent, ModalHeader, + Popup, Segment } from "../Theme"; import {ButtonGroup, Container, Dimmer, Dropdown, GridColumn, GridRow, Icon, Input} from "semantic-ui-react"; @@ -26,10 +27,11 @@ import { WROLModeMessage } from "../Common"; import QRCode from "react-qr-code"; -import {useDockerized} from "../../hooks/customHooks"; +import {useConfigs, useDockerized} from "../../hooks/customHooks"; import {toast} from "react-semantic-toasts-2"; import Grid from "semantic-ui-react/dist/commonjs/collections/Grid"; import {SettingsContext} from "../../contexts/contexts"; +import {ConfigsTable} from "./Configs"; export function ShutdownButton() { const dockerized = useDockerized(); @@ -168,389 +170,373 @@ function toApiLogLevel(logLevel) { }[logLevel] } -export class SettingsPage extends React.Component { +export function SettingsPage() { + const [disabled, setDisabled] = React.useState(false); + const [editSpecialDirectories, setEditSpecialDirectories] = React.useState(false); + const [qrCodeValue, setQrCodeValue] = React.useState(''); + const [qrOpen, setQrOpen] = React.useState(false); + const [ready, setReady] = React.useState(false); + const [pendingSave, setPendingSave] = React.useState(false); - constructor(props) { - super(props); - this.state = { - disabled: false, - hotspot_encryption: 'WPA', - edit_special_directories: false, - pending: false, - qrCodeValue: '', - qrOpen: false, - ready: false, + // Get current settings from the API. + const {settings, saveSettings, fetchSettings} = React.useContext(SettingsContext); + // Used to track changes to the settings form between saves/loads. + const [state, setState] = React.useState({}); - archive_destination: null, - download_on_startup: null, - download_timeout: null, - hotspot_device: null, - hotspot_password: null, - hotspot_ssid: null, - hotspot_status: null, - ignore_outdated_zims: null, - log_level: null, - map_destination: null, - navColor: null, - throttle_on_startup: null, - throttle_status: null, - videos_destination: null, - zims_destination: null, - } + const {configs, loading, importConfig, saveConfig, fetchConfigs} = useConfigs(); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleHotspotChange = this.handleHotspotChange.bind(this); - } + React.useEffect(() => { + fetchConfigs(); + }, []); - async componentDidMount() { + const localFetchSettings = async () => { + console.debug('fetching settings...'); try { - const settings = await getSettings(); - this.setState({ - ready: true, - archive_destination: settings.archive_destination, - disabled: settings.wrol_mode, - download_on_startup: settings.download_on_startup, - download_timeout: settings.download_timeout || '', - hotspot_device: settings.hotspot_device, - hotspot_password: settings.hotspot_password, - hotspot_ssid: settings.hotspot_ssid, - hotspot_status: settings.hotspot_status, - ignore_outdated_zims: settings.ignore_outdated_zims, - log_level: fromApiLogLevel(settings.log_level), - map_destination: settings.map_destination, - navColor: settings.nav_color || 'violet', - throttle_on_startup: settings.throttle_on_startup, - throttle_status: settings.throttle_status, - videos_destination: settings.videos_destination, - zims_destination: settings.zims_destination, - }, this.handleHotspotChange); + await fetchSettings(); + console.debug('successfully fetched settings'); } catch (e) { - console.error(e); - this.setState({ready: undefined}); - } - } - - async handleSubmit(callback) { - this.setState({disabled: true, pending: true}); - let settings = { - archive_destination: this.state.archive_destination, - download_on_startup: this.state.download_on_startup, - download_timeout: this.state.download_timeout ? parseInt(this.state.download_timeout) : 0, - hotspot_device: this.state.hotspot_device, - hotspot_password: this.state.hotspot_password, - hotspot_ssid: this.state.hotspot_ssid, - ignore_outdated_zims: this.state.ignore_outdated_zims, - log_level: toApiLogLevel(this.state.log_level), - map_destination: this.state.map_destination, - nav_color: this.state.navColor, - throttle_on_startup: this.state.throttle_on_startup, - videos_destination: this.state.videos_destination, - zims_destination: this.state.zims_destination, - } - try { - const response = await saveSettings(settings); - if (response.status !== 204) { - throw Error('Failed to save settings'); - } - } catch (e) { - toast({ - type: 'error', - title: 'Failed', - description: 'Failed to save settings.', - time: 5000, - }); + console.error('failed to fetch settings!'); throw e; - } finally { - this.setState({disabled: false, pending: false}); - if (callback) { - callback(); - } } } - handleInputChange = async (e, name, value) => { + const localSaveSettings = async (newSettings) => { + setDisabled(true); + setPendingSave(true); + newSettings.download_timeout = parseInt(newSettings.download_timeout); + newSettings.log_level = toApiLogLevel(newSettings.log_level); + try { + await saveSettings(newSettings); + await localFetchSettings(); + } finally { + setDisabled(false); + setPendingSave(false); + } + } + + const handleHotspotChange = async () => { + let {hotspot_ssid, hotspot_encryption, hotspot_password} = state; + // Special string which allows a mobile device to connect to a specific Wi-Fi. + setQrCodeValue(`WIFI:S:${hotspot_ssid};T:${hotspot_encryption};P:${hotspot_password};;`); + } + + React.useEffect(() => { + console.debug('settings changed, replacing state...'); + setReady(settings ? true : undefined); + setState({ + // Only these settings can be changed on the SettingsPage. + archive_destination: settings.archive_destination, + download_on_startup: settings.download_on_startup, + download_timeout: settings.download_timeout, + hotspot_device: settings.hotspot_device, + hotspot_on_startup: settings.hotspot_on_startup, + hotspot_password: settings.hotspot_password, + hotspot_ssid: settings.hotspot_ssid, + ignore_outdated_zims: settings.ignore_outdated_zims, + log_level: fromApiLogLevel(settings.log_level), + map_destination: settings.map_destination, + nav_color: settings.nav_color, + media_directory: settings.media_directory, + throttle_on_startup: settings.throttle_on_startup, + videos_destination: settings.videos_destination, + zims_destination: settings.zims_destination, + }); + }, [JSON.stringify(settings)]); + + React.useEffect(() => { + handleHotspotChange(); + }, [state.hotspot_ssid, state.hotspot_password, state.hotspot_device]); + + const handleInputChange = async (e, name, value) => { if (e) { e.preventDefault() } - this.setState({[name]: value}); + setState({...state, [name]: value}); } - handleHotspotChange = async () => { - let {hotspot_ssid, hotspot_encryption, hotspot_password} = this.state; - // Special string which allows a mobile device to connect to a specific Wi-Fi. - let qrCodeValue = `WIFI:S:${hotspot_ssid};T:${hotspot_encryption};P:${hotspot_password};;`; - this.setState({qrCodeValue}); - } - - handleTimeoutChange = async (e, name, value) => { + const handleTimeoutChange = async (e, name, value) => { if (e) { e.preventDefault() } // Restrict timeout to numbers. value = value.replace(/[^\d]/, ''); - this.setState({[name]: value}); + setState({...state, [name]: value}); } - handleQrOpen = async (e) => { + const handleQrOpen = async (e) => { e.preventDefault(); - this.setState({qrOpen: true}); + setQrOpen(true); } - render() { - const controlSegment = -
Control WROLPi
- - -
; + const qrButton = ; - if (this.state.ready === false) { - return <> - - {controlSegment} - - } else if (this.state.ready === undefined) { - return Unable to fetch settings - } + const navColorOptions = Object.keys(semanticUIColorMap).map(i => { + return {key: i, value: i, text: i.charAt(0).toUpperCase() + i.slice(1)} + }); - const { - archive_destination, - disabled, - download_on_startup, - download_timeout, - edit_special_directories, - hotspot_device, - hotspot_password, - hotspot_ssid, - ignore_outdated_zims, - log_level, - map_destination, - navColor, - pending, - qrCodeValue, - throttle_on_startup, - videos_destination, - zims_destination, - } = this.state; + const mediaDirectoryLabel = `${settings.media_directory}/`; - const qrButton = ; + let body; + if (ready === true) { + // Settings have been fetched, display form. + body = <> +

Any changes will be written to {settings.media_directory}/config/wrolpi.yaml.

- const navColorOptions = Object.keys(semanticUIColorMap).map(i => { - return {key: i, value: i, text: i.charAt(0).toUpperCase() + i.slice(1)} - }); +
+
+ handleInputChange(null, 'download_on_startup', checked)} + /> +
- return {({settings, fetchSettings}) => { - const mediaDirectoryLabel = `${settings.media_directory}/`; - return - +
+ handleInputChange(null, 'throttle_on_startup', checked)} + /> +
- - - - +
+ handleInputChange(null, 'ignore_outdated_zims', checked)} + /> +
- -
Settings
-

Any changes will be written to {settings.media_directory}/config/wrolpi.yaml.

- - -
- this.handleInputChange(null, 'download_on_startup', checked)} - /> -
- -
- this.handleInputChange(null, 'throttle_on_startup', checked)} - /> -
- -
- this.handleInputChange(null, 'ignore_outdated_zims', checked)} - /> -
- - - - Download Timeout - - } - value={download_timeout} - disabled={disabled || download_timeout === null} - onChange={(e, d) => this.handleTimeoutChange(e, 'download_timeout', d.value)} + } + value={state.download_timeout} + disabled={disabled || state.download_timeout === null} + onChange={(e, i) => handleTimeoutChange(e, 'download_timeout', i.value)} + /> + + + + setState({...state, hotspot_ssid: i.value})} + /> + setState({...state, hotspot_password: i.value})} + /> + handleInputChange(e, 'hotspot_device', i.value)} + /> + + + setQrOpen(false)} + onOpen={handleQrOpen} + open={qrOpen} + trigger={qrButton} + > + + Scan this code to join the hotspot + + +
+ +
+
+
+ +
+ + +
+ setState({...state, log_level: parseInt(e.target.value)})} + style={{marginBottom: '1em'}} + /> + + + + + + + + +
+ + + + setState({...state, navColor: value})} + value={state.navColor} + /> + + + + +
Special Directories
+

WROLPi will save files to these directories (any video files will be saved to + the videos by default, etc.).

+ + + + + + + handleInputChange(e, 'archive_destination', d.value)} + /> + + + + + + handleInputChange(e, 'videos_destination', d.value)} + /> + + + + + + + + handleInputChange(e, 'map_destination', d.value)} + /> + + + + + + handleInputChange(e, 'zims_destination', d.value)} + /> + + + + + + setEditSpecialDirectories(checked)} /> - + + + - - this.setState({hotspot_ssid: d.value}, this.handleHotspotChange)} - /> - this.setState({hotspot_password: d.value}, this.handleHotspotChange)} - /> - this.handleInputChange(e, 'hotspot_device', d.value)} - /> - + - this.setState({qrOpen: false})} - onOpen={this.handleQrOpen} - open={this.state.qrOpen} - trigger={qrButton} - > - - Scan this code to join the hotspot - - -
- -
-
-
+ localSaveSettings(state)} + obeyWROLMode={true} + disabled={disabled} + >Save -
+ + + - -
- this.setState({'log_level': parseInt(e.target.value)})} - style={{marginBottom: '1em'}} - /> - - - - - - - - -
- - - - this.setState({navColor: value})} - value={navColor} - /> - - - - -
Special Directories
-

WROLPi will save files to these directories (any video files will be saved to - the videos by default, etc.).

- - - - - - - this.handleInputChange(e, 'archive_destination', d.value)} - /> - - - - - - this.handleInputChange(e, 'videos_destination', d.value)} - /> - - - - - - - - this.handleInputChange(e, 'map_destination', d.value)} - /> - - - - - - this.handleInputChange(e, 'zims_destination', d.value)} - /> - - - - - - this.handleInputChange(null, 'edit_special_directories', checked)} - /> - - - - - - - this.handleSubmit(fetchSettings)} - obeyWROLMode={true} - disabled={disabled} - >Save - - - - - - -
- - {controlSegment} -
; - } - }
+ + + } else if (ready === false) { + body = + } else { + body = Unable to fetch settings } + + const refreshButton = ; + const refreshPopupButton = ; + + const controlSegment = +
Control WROLPi
+ + + + + +
; + + const configsSegment = + + + +
Configs
+
+ {refreshPopupButton} +
+
+ +

Configs control a WROLPi, they are the source of truth. Any changes to a config will be applied to the + database when import (typically at startup).

+ + +
; + + return + + + {controlSegment} + + +
Settings
+ {body} +
+ + {configsSegment} +
; } + diff --git a/app/src/contexts/contexts.js b/app/src/contexts/contexts.js index 70ca26b1..9b4afe8b 100644 --- a/app/src/contexts/contexts.js +++ b/app/src/contexts/contexts.js @@ -11,6 +11,8 @@ export const StatusContext = React.createContext({ export const SettingsContext = React.createContext({ settings: {}, fetchSettings: null, + saveSettings: null, + pending: false, }); // `useQuery` diff --git a/app/src/hooks/customHooks.js b/app/src/hooks/customHooks.js index 8b8959c0..6945b48a 100644 --- a/app/src/hooks/customHooks.js +++ b/app/src/hooks/customHooks.js @@ -19,6 +19,7 @@ import { getVideosStatistics, postImportConfig, postSaveConfig, + saveSettings, searchArchives, searchChannels, searchDirectories, @@ -787,8 +788,10 @@ export const useSearchDirectories = (value) => { export const useSettings = () => { const [settings, setSettings] = useState({}); + const [pending, setPending] = useState(false); const fetchSettings = async () => { + setPending(true); try { setSettings(await getSettings()) } catch (e) { @@ -799,6 +802,8 @@ export const useSettings = () => { description: 'Could not get directories', time: 5000, }); + } finally { + setPending(false); } } @@ -806,7 +811,15 @@ export const useSettings = () => { fetchSettings(); }, []); - return {settings, fetchSettings}; + return {settings, pending, fetchSettings, saveSettings}; +} + +export const useSettingsInterval = () => { + const {settings, pending, fetchSettings, saveSettings} = useSettings(); + + useRecurringTimeout(fetchSettings, 1000 * 10); + + return {settings, pending, fetchSettings, saveSettings}; } export const useMediaDirectory = () => { @@ -815,14 +828,6 @@ export const useMediaDirectory = () => { return settings['media_directory']; } -export const useSettingsInterval = () => { - const {settings, fetchSettings} = useSettings(); - - useRecurringTimeout(fetchSettings, 1000 * 10); - - return {settings, fetchSettings}; -} - export const useStatus = () => { const [status, setStatus] = useState({});