Updated WoL Settings

This commit is contained in:
Sean Edmunds
2025-03-21 10:33:50 -05:00
parent 27a9db82f0
commit 27a7dd2fb9
12 changed files with 637 additions and 135 deletions

View File

@@ -0,0 +1,74 @@
import { Icon } from '@iconify/react';
import { Box, Stack, Text } from '@mantine/core';
import { useCommonHooks } from 'hooks';
import { useState } from 'react';
import { boxClasses, textClasses } from 'styles';
function AppSettingsItem({ url, icon, name }: any) {
const [textDecoration, setTextDecoration] = useState<string>('none');
const [hover, setHover] = useState<boolean>(false);
const { isMobile } = useCommonHooks();
return (
<Box
onMouseOver={() => {
setTextDecoration('underline');
setHover(true);
}}
onMouseOut={() => {
setTextDecoration('none');
setHover(false);
}}
className={boxClasses.appItemBox}
>
{hover && (
<svg
style={{ position: 'absolute', borderRadius: '6px' }}
height='100%'
width='100%'
xmlns='http://www.w3.org/2000/svg'
>
<rect rx='16' ry='16' height='100%' width='100%' strokeLinejoin='round' />
</svg>
)}
<Stack
align={isMobile ? 'center' : undefined}
justify={isMobile ? 'center' : undefined}
style={{
height: '128px',
width: isMobile ? '100%' : undefined,
marginRight: isMobile ? '0' : '1em',
marginLeft: isMobile ? '0' : '2em',
paddingTop: isMobile ? '0' : '32px',
}}
>
<Icon icon={`mdi:${icon}`} height={64} width={64} />
</Stack>
<Stack
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
flex: 1,
overflow: 'hidden',
gap: 0,
}}
>
<Text fz={22} className={textClasses.appName} classNames={textClasses}>
{name}
</Text>
<Text
fz={16}
className={textClasses.appUrl}
classNames={textClasses}
style={{
textDecoration: textDecoration,
}}
>
{url}
</Text>
</Stack>
</Box>
);
}
export default AppSettingsItem;

View File

@@ -15,7 +15,6 @@ import {
} from '@mantine/core';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import AppItem from 'pages/homepage/components/Applications/AppItem';
import { useEffect, useState } from 'react';
import { FiHelpCircle, FiPlus } from 'react-icons/fi';
import {
@@ -25,6 +24,7 @@ import {
textClasses,
textInputClasses,
} from 'styles';
import AppSettingsItem from './AppSettingsItem';
import DebouncedApplication from './DebouncedApplication';
const ApplicationSettings = () => {
@@ -34,9 +34,6 @@ const ApplicationSettings = () => {
});
const [applicationData, setApplicationData] = useState<any>([]);
const { theme } = useCommonHooks();
const [application, setApplication] = useState<any>({});
const [update, setUpdate] = useState({});
const [debounced] = useDebouncedValue(update, 1000);
const [newApplication, setNewApplication] = useState<any>({
name: '',
url: '',
@@ -62,18 +59,6 @@ const ApplicationSettings = () => {
}
}, [apps, isAppsFetching, debouncedSearch]);
useEffect(() => {
if (Object.keys(debounced).length) {
mutateAsync({ method: 'PATCH', endpoint: endpoint, body: debounced });
}
}, [debounced]);
useEffect(() => {
if (update !== application && Object.keys(update).length > 0) {
setApplication(update);
}
}, [update]);
return (
<Stack w={'100%'}>
<Modal.Root
@@ -103,71 +88,79 @@ const ApplicationSettings = () => {
</Modal.Header>
<Modal.Body pb={'2rem'}>
<Stack gap={0} w={'100%'} align='center'>
<TextInput
label='Name'
autoFocus
w={'75%'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
onChange={(event) => {
setNewApplication({
...newApplication,
name: event.target.value,
});
}}
/>
<TextInput
label='URL'
autoFocus
w={'75%'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
onChange={(event) => {
setNewApplication({
...newApplication,
url: event.target.value,
});
}}
/>
<TextInput
label='Icon'
autoFocus
w={'75%'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
onChange={(event) => {
setNewApplication({
...newApplication,
icon: event.target.value,
});
}}
/>
<TextInput
label='Base URL'
autoFocus
w={'75%'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
onChange={(event) => {
setNewApplication({
...newApplication,
baseUrl: event.target.value,
});
}}
/>
<TextInput
label='Filter Name'
autoFocus
w={'75%'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
onChange={(event) => {
setNewApplication({
...newApplication,
filterName: event.target.value,
});
}}
/>
<Group w={'100%'} justify='center'>
<TextInput
label={'Name'}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={newApplication.name}
onChange={(event) => {
setNewApplication({
...newApplication,
name: event.target.value,
});
}}
/>
<TextInput
label={'Base URL'}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={newApplication.baseUrl}
onChange={(event) => {
setNewApplication({
...newApplication,
baseUrl: event.target.value,
});
}}
/>
</Group>
<Group w={'100%'} justify='center'>
<TextInput
label={'URL'}
className={textInputClasses.settingsItem}
w={'59%'}
pt={16}
classNames={textInputClasses}
defaultValue={newApplication.url}
onChange={(event) => {
setNewApplication({
...newApplication,
url: event.target.value,
});
}}
/>
</Group>
<Group w={'100%'} justify='center'>
<TextInput
label={'Icon'}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={newApplication.icon}
onChange={(event) => {
setNewApplication({
...newApplication,
icon: event.target.value,
});
}}
/>
<TextInput
label={'Filter'}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={newApplication.filterName}
onChange={(event) => {
setNewApplication({
...newApplication,
filterName: event.target.value,
});
}}
/>
</Group>
<Divider color='black' my='sm' />
<Group pos={'absolute'} bottom={8}>
<Button
@@ -262,20 +255,18 @@ const ApplicationSettings = () => {
<Stack w={'100%'} align='center'>
<Grid columns={18} w={'75%'}>
{applicationData?.map((application: any) => (
<GridCol span={3} key={application.id}>
<GridCol span={6} key={application.id}>
<Popover withArrow withOverlay withinPortal={false}>
<Popover.Target>
<Box
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
<AppItem
<AppSettingsItem
url={application.url}
icon={application.icon}
name={application.name}
linkDisabled
/>
</Box>
</Popover.Target>
@@ -283,7 +274,6 @@ const ApplicationSettings = () => {
<DebouncedApplication
application={application}
handleDelete={handleDelete}
setApplication={setUpdate}
/>
</Popover.Dropdown>
</Popover>

View File

@@ -9,14 +9,33 @@ import {
TextInput,
Tooltip,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { IconTrash } from '@tabler/icons-react';
import { useCommonHooks } from 'hooks';
import { useCommonHooks, useMutationAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { cardClasses, modalClasses, textInputClasses } from 'styles';
function DebouncedApplication({ application, handleDelete, setApplication }: any) {
function DebouncedApplication({ application, handleDelete }: any) {
const { theme } = useCommonHooks();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const { mutateAsync } = useMutationAPI();
const [app, setApp] = useState<any>(application);
const [update, setUpdate] = useState({});
const [debounced] = useDebouncedValue(update, 1000);
useEffect(() => {
if (Object.keys(debounced).length) {
mutateAsync({ method: 'PATCH', endpoint: 'api/apps', body: debounced });
}
}, [debounced]);
useEffect(() => {
if (update !== app && Object.keys(update).length) {
setApp(update);
}
}, [update]);
return (
<Card className={cardClasses.settingsItem} classNames={cardClasses}>
<Modal.Root
@@ -57,14 +76,14 @@ function DebouncedApplication({ application, handleDelete, setApplication }: any
backgroundColor: 'var(--color-text-acc)',
color: 'var(--color-background)',
}}
>{`"${application.name}"`}</Text>
>{`"${app.name}"`}</Text>
<Text c='red'>This action cannot be undone!</Text>
<Group pos={'absolute'} bottom={8}>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
handleDelete(application);
handleDelete(app);
closeModal();
}}
>
@@ -106,10 +125,10 @@ function DebouncedApplication({ application, handleDelete, setApplication }: any
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={application.name}
defaultValue={app.name}
onChange={(event) => {
setApplication({
...application,
setUpdate({
...app,
name: event.target.value,
});
}}
@@ -119,10 +138,10 @@ function DebouncedApplication({ application, handleDelete, setApplication }: any
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={application.baseUrl}
defaultValue={app.baseUrl}
onChange={(event) => {
setApplication({
...application,
setUpdate({
...app,
baseUrl: event.target.value,
});
}}
@@ -135,10 +154,10 @@ function DebouncedApplication({ application, handleDelete, setApplication }: any
w={'100%'}
pt={16}
classNames={textInputClasses}
defaultValue={application.url}
defaultValue={app.url}
onChange={(event) => {
setApplication({
...application,
setUpdate({
...app,
url: event.target.value,
});
}}
@@ -150,10 +169,10 @@ function DebouncedApplication({ application, handleDelete, setApplication }: any
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={application.icon}
defaultValue={app.icon}
onChange={(event) => {
setApplication({
...application,
setUpdate({
...app,
icon: event.target.value,
});
}}
@@ -163,10 +182,10 @@ function DebouncedApplication({ application, handleDelete, setApplication }: any
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={application.filterName}
defaultValue={app.filterName}
onChange={(event) => {
setApplication({
...application,
setUpdate({
...app,
filterName: event.target.value,
});
}}

View File

@@ -41,7 +41,7 @@ function DebouncedMessage({ label, message, setMessage, handleDelete }: any) {
<Modal.Overlay />
<Modal.Content>
<Modal.Header>
<Modal.Title>Add a new message</Modal.Title>
<Modal.Title>Delete message</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body pb={'2rem'}>

View File

@@ -26,12 +26,7 @@ import {
} from 'styles';
const MessageSettings = () => {
const endpoint = 'api/messages';
const {
data: messageData,
isFetching: isMessagesFetching,
isFetched: isMessagesFetched,
isLoading: isMessagesLoading,
} = useQueryAPI({
const { data: messageData, isFetched: isMessagesFetched } = useQueryAPI({
endpoint: 'api/messages',
});
const { theme } = useCommonHooks();

View File

@@ -0,0 +1,154 @@
import {
ActionIcon,
Button,
Card,
Group,
Modal,
Stack,
Text,
TextInput,
Tooltip,
} from '@mantine/core';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { IconTrash } from '@tabler/icons-react';
import { useCommonHooks, useMutationAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { cardClasses, modalClasses, textInputClasses } from 'styles';
function DebouncedWolClient({ wolClient, handleDelete }: any) {
const { theme } = useCommonHooks();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const { mutateAsync } = useMutationAPI();
const [client, setClient] = useState<any>(wolClient);
const [update, setUpdate] = useState({});
const [debounced] = useDebouncedValue(update, 1000);
useEffect(() => {
if (Object.keys(debounced).length) {
mutateAsync({ method: 'PATCH', endpoint: 'api/WolClients', body: debounced });
}
}, [debounced]);
useEffect(() => {
if (update !== client && Object.keys(update).length) {
setClient(update);
}
}, [update]);
return (
<Card className={cardClasses.settingsItem} classNames={cardClasses}>
<Modal.Root
size={'60%'}
className={modalClasses.helpModal}
classNames={modalClasses}
opened={modalOpened}
onClose={() => {
closeModal();
}}
style={{
'--color-text-pri': theme.primaryColor,
'--color-text-acc': theme.accentColor,
'--color-background': theme.backgroundColor,
'--color-border': theme.borderColor,
'--color-search-background': theme.searchColor,
'--image-background': `url(${theme.backgroundImage})`,
}}
closeOnEscape
closeOnClickOutside
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header>
<Modal.Title>Delete Client</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body pb={'2rem'}>
<Stack gap={0} w={'100%'} h={'150px'} align='center'>
<Text>Are you sure you want to delete this WoL Client?</Text>
<Text
style={{
fontWeight: 'bold',
fontSize: '20px',
padding: '8px',
margin: '8px',
borderRadius: '10px',
backgroundColor: 'var(--color-text-acc)',
color: 'var(--color-background)',
}}
>{`"${client.hostname}"`}</Text>
<Text c='red'>This action cannot be undone!</Text>
<Group pos={'absolute'} bottom={8}>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
handleDelete(client);
closeModal();
}}
>
Delete
</Button>
<Button
color={'grey'}
styles={{ label: { color: 'black' } }}
onClick={() => {
closeModal();
}}
>
Cancel
</Button>
</Group>
</Stack>
</Modal.Body>
</Modal.Content>
</Modal.Root>
<Tooltip
color={theme.primaryColor}
bd={`solid 1px ${theme.borderColor}`}
label={'Delete'}
>
<ActionIcon
pos={'absolute'}
right={16}
color='red'
onClick={() => {
openModal();
}}
>
<IconTrash />
</ActionIcon>
</Tooltip>
<Group w={'100%'} justify='center'>
<TextInput
label={'Hostname'}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={client.hostname}
onChange={(event) => {
setUpdate({
...client,
hostname: event.target.value,
});
}}
/>
<TextInput
label={'MAC Address'}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={client.macAddress}
onChange={(event) => {
setUpdate({
...client,
macAddress: event.target.value,
});
}}
/>
</Group>
</Card>
);
}
export default DebouncedWolClient;

View File

@@ -0,0 +1,22 @@
import { Card, Stack, Text } from '@mantine/core';
import { FaLaptop } from 'react-icons/fa';
import { cardClasses } from 'styles';
function WolClientSettingsItem({ client }: any) {
return (
<Card
className={cardClasses.settingsItemHover}
classNames={cardClasses}
h={200}
w={200}
>
<Stack align='center' gap={0}>
<FaLaptop fontSize={100} />
<Text fz={24}>{client.hostname}</Text>
<Text fz={20}>{client.macAddress}</Text>
</Stack>
</Card>
);
}
export default WolClientSettingsItem;

View File

@@ -1,8 +1,31 @@
import { Stack, Text } from '@mantine/core';
import { DataTable, WolClientColumns } from 'components';
import { useQueryAPI } from 'hooks';
import {
ActionIcon,
Box,
Button,
Divider,
Grid,
GridCol,
Group,
Highlight,
Modal,
Popover,
Stack,
Text,
TextInput,
} from '@mantine/core';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { textClasses } from 'styles';
import { FiHelpCircle, FiPlus } from 'react-icons/fi';
import {
actionIconClasses,
buttonClasses,
modalClasses,
textClasses,
textInputClasses,
} from 'styles';
import DebouncedWolClient from './DebouncedWolClient';
import WolClientSettingsItem from './WolClientSettingsItem';
const WolClientSettings = () => {
const endpoint = 'api/wolClients';
@@ -15,26 +38,201 @@ const WolClientSettings = () => {
endpoint: endpoint,
});
const [wolClients, setWolClients] = useState<any>([]);
const { mutateAsync } = useMutationAPI();
const { theme } = useCommonHooks();
const [newWolClient, setNewWolClient] = useState<any>({
name: '',
url: '',
icon: '',
baseUrl: '',
filterName: '',
});
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [searchValue, setSearchValue] = useState('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const handleDelete = (application: any) => {
mutateAsync({ method: 'DELETE', endpoint: endpoint, body: application });
};
useEffect(() => {
if (wolClientsData && wolClientsIsFetched) {
setWolClients(wolClientsData);
setWolClients(
wolClientsData.filter((a: any) =>
a.hostname.toLowerCase().includes(debouncedSearch.toLowerCase()),
),
);
}
}, [wolClientsData, wolClientsIsFetched]);
}, [wolClientsData, wolClientsIsFetched, debouncedSearch]);
return (
<Stack>
<Text className={textClasses.settingsSubTitle} classNames={textClasses}>
WoL Clients
</Text>
<DataTable
tableName='WoL Clients'
columns={WolClientColumns()}
tableData={wolClients}
isFetching={wolClientsIsFetching}
isLoading={wolClientsIsLoading}
endpoint={endpoint}
/>
<Modal.Root
size={'60%'}
className={modalClasses.helpModal}
classNames={modalClasses}
opened={modalOpened}
onClose={() => {
closeModal();
}}
style={{
'--color-text-pri': theme.primaryColor,
'--color-text-acc': theme.accentColor,
'--color-background': theme.backgroundColor,
'--color-border': theme.borderColor,
'--color-search-background': theme.searchColor,
'--image-background': `url(${theme.backgroundImage})`,
}}
closeOnEscape
closeOnClickOutside
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header>
<Modal.Title>Add a new application</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body pb={'2rem'}>
<Stack gap={0} w={'100%'} align='center'>
<Group w={'100%'} justify='center'>
<TextInput
label={'Hostname'}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={newWolClient.hostname}
onChange={(event) => {
setNewWolClient({
...newWolClient,
hostname: event.target.value,
});
}}
/>
<TextInput
label={'MAC Address'}
className={textInputClasses.settingsItem}
pt={16}
classNames={textInputClasses}
defaultValue={newWolClient.macAddress}
onChange={(event) => {
setNewWolClient({
...newWolClient,
macAddress: event.target.value,
});
}}
/>
</Group>
<Divider color='black' my='sm' />
<Group pos={'absolute'} bottom={8}>
<Button
color={'green'}
styles={{ label: { color: 'black' } }}
onClick={() => {
if (newWolClient) {
mutateAsync({
method: 'POST',
endpoint: endpoint,
body: newWolClient,
});
closeModal();
}
}}
>
Save
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
closeModal();
}}
>
Cancel
</Button>
</Group>
</Stack>
</Modal.Body>
</Modal.Content>
</Modal.Root>
<Group align='center'>
<Text className={textClasses.settingsSubTitle} classNames={textClasses}>
WoL Clients
</Text>
<Popover withArrow withOverlay withinPortal={false}>
<Popover.Target>
<ActionIcon
className={actionIconClasses.settingsTooltip}
classNames={actionIconClasses}
>
<FiHelpCircle />
</ActionIcon>
</Popover.Target>
<Popover.Dropdown
style={{
color: theme.primaryColor,
backgroundColor: theme.backgroundColor,
}}
>
<Stack>
<Text className={textClasses.settingsTooltip} classNames={textClasses}>
{'Wake on Lan clients can be configured here.'}
</Text>
<Highlight
ta={'center'}
highlight={['Clients', 'homepage']}
className={textClasses.settingsTooltip}
classNames={textClasses}
highlightStyles={{
backgroundImage:
'linear-gradient(45deg, var(--color-text-acc), var(--color-text-pri))',
fontWeight: 700,
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}
>
{'Clients can be powered on from the settings button the homepage.'}
</Highlight>
</Stack>
</Popover.Dropdown>
</Popover>
<Button
className={buttonClasses.tableButton}
classNames={buttonClasses}
onClick={openModal}
>
<FiPlus />
</Button>
<TextInput
className={textInputClasses.settingsItem}
classNames={textInputClasses}
autoFocus
placeholder='Search...'
value={searchValue}
onInput={(event: any) => {
setSearchValue(event.target.value);
}}
/>
</Group>
<Stack w={'100%'} align='center'>
<Grid columns={18} w={'75%'}>
{wolClients?.map((client: any) => (
<GridCol span={6} key={client.id}>
<Popover withArrow withOverlay withinPortal={false}>
<Popover.Target>
<Box
onClick={(e) => {
e.stopPropagation();
}}
>
<WolClientSettingsItem client={client} />
</Box>
</Popover.Target>
<Popover.Dropdown>
<DebouncedWolClient wolClient={client} handleDelete={handleDelete} />
</Popover.Dropdown>
</Popover>
</GridCol>
))}
</Grid>
</Stack>
</Stack>
);
};