Starting guac clients

This commit is contained in:
Sean Edmunds
2025-12-23 14:55:47 -06:00
parent 063a936cf7
commit 64605a341c
23 changed files with 853 additions and 480 deletions

View File

@@ -60,7 +60,7 @@ const ApplicationSettings = () => {
const [applicationList, setApplicationList] = useState<Application[]>([]);
const { isMobile, reorder, theme } = useCommonHooks();
const { mutateAsync } = useMutationAPI();
const [searchValue, setSearchValue] = useState('');
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [dragging, setDragging] = useState(false);
const [isOpen, setIsOpen] = useState(false);
@@ -98,9 +98,9 @@ const ApplicationSettings = () => {
applicationData
.filter(
(a: Application) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
a.url.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
a.baseUrl.toLowerCase().includes(debouncedSearch.toLowerCase()),
a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
a.url.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
a.baseUrl.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
)
.sort((a: Application, b: Application) => (a.index > b.index ? 1 : -1)),
);

View File

@@ -50,7 +50,7 @@ const BookmarkSettings = () => {
const [groupList, setGroupList] = useState<BookmarkGroup[]>([]);
const { isMobile, reorder, theme } = useCommonHooks();
const { mutateAsync } = useMutationAPI();
const [searchValue, setSearchValue] = useState('');
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedGroupSearch] = useDebouncedValue(searchValue, 500);
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [isLoading, setIsLoading] = useState(false);
@@ -93,8 +93,12 @@ const BookmarkSettings = () => {
bookmarks
.filter(
(a: Bookmark) =>
a.name.toLowerCase().includes(debouncedGroupSearch.toLowerCase()) ||
a.url.toLowerCase().includes(debouncedGroupSearch.toLowerCase()),
a.name
.toLowerCase()
.includes((debouncedGroupSearch as string).toLowerCase()) ||
a.url
.toLowerCase()
.includes((debouncedGroupSearch as string).toLowerCase()),
)
.map((a: Bookmark) =>
groupData.find(
@@ -105,7 +109,9 @@ const BookmarkSettings = () => {
),
];
const foundGroups = groupData.filter((a: BookmarkGroup) =>
a.categoryName.toLowerCase().includes(debouncedGroupSearch.toLowerCase()),
a.categoryName
.toLowerCase()
.includes((debouncedGroupSearch as string).toLowerCase()),
);
const groupList = [...new Set([...foundBookmarkGroups, ...foundGroups])].sort(

View File

@@ -41,7 +41,7 @@ function DebouncedGroupSettingsItem({
}: {
bookmarkGroup: BookmarkGroup;
bookmarks: Bookmark[];
searchTerm: string;
searchTerm: string | number;
}) {
const { isMobile, handleDelete, reorder, theme } = useCommonHooks();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
@@ -262,8 +262,12 @@ function DebouncedGroupSettingsItem({
>
<Popover.Target>
<Box pos={'relative'}>
{(bookmark.name.toLowerCase().includes(searchTerm) ||
bookmark.url.toLowerCase().includes(searchTerm)) &&
{(bookmark.name
.toLowerCase()
.includes(searchTerm as string) ||
bookmark.url
.toLowerCase()
.includes(searchTerm as string)) &&
searchTerm && (
<ActionIcon
variant='subtle'

View File

@@ -0,0 +1,122 @@
import { ActionIcon, Card, Group, Stack, Text, TextInput, Tooltip } from '@mantine/core';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { DeleteItemModal } from 'components';
import { useCommonHooks, useMutationAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { FiTrash } from 'react-icons/fi';
import { cardClasses, textClasses, textInputClasses, toolTipClasses } from 'styles';
import type { GuacamoleClient } from 'types';
function DebouncedGuacamoleClient({
guacamoleClient,
setIsOpen,
}: {
guacamoleClient: GuacamoleClient;
setIsOpen: (isOpen: boolean) => void;
}) {
const { handleDelete, isMobile, theme } = useCommonHooks();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const { mutateAsync } = useMutationAPI();
const [client, setClient] = useState<GuacamoleClient>(guacamoleClient);
const [update, setUpdate] = useState<any>({});
const [debounced] = useDebouncedValue(update, 1000);
const handleClose = () => {
setIsOpen(false);
closeModal();
};
useEffect(() => {
if (Object.keys(debounced).length) {
mutateAsync({ method: 'PATCH', endpoint: 'api/guacamoleClients', body: debounced });
}
}, [debounced]);
useEffect(() => {
if (update !== client && Object.keys(update).length) {
setClient(update);
}
}, [update]);
return (
<Card
p={isMobile ? 'xl' : undefined}
className={cardClasses.settingsItem}
classNames={cardClasses}
>
<DeleteItemModal
modalOpened={modalOpened}
handleClose={handleClose}
handleDelete={() => handleDelete(client, 'api/guacamoleClients')}
title={'Delete Guacamole Client'}
body={
<Stack gap={0} w={'100%'} h={'150px'} align='center'>
<Text>Are you sure you want to delete this WoL Client?</Text>
<Text
className={textClasses.deleteItem}
classNames={textClasses}
>{`"${client.clientName}"`}</Text>
<Text c='red'>This action cannot be undone!</Text>
</Stack>
}
/>
<Tooltip
className={toolTipClasses.default}
classNames={toolTipClasses}
label={'Delete'}
>
<ActionIcon
pos={'absolute'}
right={'1rem'}
color='red'
onClick={() => {
openModal();
}}
>
<FiTrash color='black' />
</ActionIcon>
</Tooltip>
<Group w={'100%'} justify='center'>
<TextInput
label={'Client Name'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={client.clientName}
onChange={(event) => {
setUpdate({
...client,
clientName: event.target.value,
});
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
<TextInput
label={'Client Identifier'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={client.clientIdentifier}
onChange={(event) => {
setUpdate({
...client,
clientIdentifier: event.target.value,
});
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
</Group>
</Card>
);
}
export default DebouncedGuacamoleClient;

View File

@@ -0,0 +1,23 @@
import { Card, Stack, Text } from '@mantine/core';
import { FaLaptop } from 'react-icons/fa';
import { cardClasses } from 'styles';
import type { GuacamoleClient } from 'types';
function GuacamoleClientSettingsItem({ client }: { client: GuacamoleClient }) {
return (
<Card
className={cardClasses.settingsItemHover}
classNames={cardClasses}
h={200}
w={'100%'}
>
<Stack align='center' gap={0}>
<FaLaptop fontSize={100} />
<Text fz={'1.5rem'}>{client.clientName}</Text>
<Text fz={'1.25rem'}>{client.clientIdentifier}</Text>
</Stack>
</Card>
);
}
export default GuacamoleClientSettingsItem;

View File

@@ -0,0 +1,211 @@
import {
Button,
Grid,
GridCol,
Group,
LoadingOverlay,
Popover,
Stack,
TextInput,
} from '@mantine/core';
import { isNotEmpty, useForm } from '@mantine/form';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { AddItemModal, NoItemsFound, SettingsHeader } from 'components';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { popoverClasses, textInputClasses } from 'styles';
import type { GuacamoleClient } from 'types';
import DebouncedGuacamoleClient from './DebouncedGuacamoleClient';
import GuacamoleClientSettingsItem from './GuacamoleClientSettingsItem';
const GuacamoleClients = () => {
const defaultGuacamoleClient: GuacamoleClient = {
id: undefined,
clientName: '',
clientIdentifier: '',
enabled: 1,
};
const form = useForm({
initialValues: defaultGuacamoleClient,
validate: {
clientName: isNotEmpty('Client Name is required'),
clientIdentifier: isNotEmpty('Client Identifier is required'),
},
});
const endpoint = 'api/guacamoleClients';
const {
data: guacamoleClientsData,
isFetched: guacamoleClientsIsFetched,
isFetching: guacamoleClientsIsFetching,
} = useQueryAPI({
endpoint: endpoint,
});
const [guacamoleClients, setGuacamoleClients] = useState<GuacamoleClient[]>([]);
const { mutateAsync } = useMutationAPI();
const { isMobile, theme } = useCommonHooks();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const handleClose = () => {
closeModal();
form.reset();
};
useEffect(() => {
if (guacamoleClientsData && guacamoleClientsIsFetched) {
setGuacamoleClients(
guacamoleClientsData.filter((a: GuacamoleClient) =>
a.clientName.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
),
);
}
if (debouncedSearch === searchValue) {
setIsLoading(false);
}
}, [guacamoleClientsData, guacamoleClientsIsFetched, debouncedSearch]);
return (
<Stack>
<AddItemModal
modalOpened={modalOpened}
handleClose={handleClose}
title={'Add a new Guacamole Client'}
body={
<form
onSubmit={form.onSubmit((values) => {
mutateAsync({
method: 'POST',
endpoint: endpoint,
body: { ...values, enabled: 1 },
});
handleClose();
})}
>
<Stack gap={0} w={'100%'} align='center'>
<Group w={'100%'} justify='center'>
<TextInput
key={form.key('clientName')}
{...form.getInputProps('clientName')}
label={'Client Name'}
placeholder='eg. Workstation'
className={textInputClasses.settingsItem}
classNames={textInputClasses}
withAsterisk
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
<TextInput
key={form.key('clientIdentifier')}
{...form.getInputProps('clientIdentifier')}
label={'clientIdentifier'}
placeholder='eg. MzIAYwBteXNxbA'
className={textInputClasses.settingsItem}
classNames={textInputClasses}
withAsterisk
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
</Group>
<Group mt={'1rem'}>
<Button type='submit' color={'green'}>
Save
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={handleClose}
>
Cancel
</Button>
</Group>
</Stack>
</form>
}
/>
<SettingsHeader
setDragging={undefined}
openModal={openModal}
title={'Guacamole Clients'}
tooltip={{
title: 'Guacamole clients can be configured here.',
highlightList: ['Clients', 'homepage', 'Guacamole'],
description: 'Clients can be connected to from the homepage.',
}}
searchValue={searchValue}
setSearchValue={setSearchValue}
searchPlaceholder={'Search Clients...'}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
<Stack
mt={isMobile ? '10rem' : '6rem'}
mb={isMobile ? '6rem' : '1rem'}
w={'100%'}
align='center'
>
<LoadingOverlay
visible={guacamoleClientsIsFetching}
zIndex={180}
overlayProps={{ blur: 5 }}
loaderProps={{
color: 'var(--mantine-color-primary-0)',
type: 'bars',
size: 'xl',
}}
/>
<Grid columns={18} w={'75%'}>
{guacamoleClients.length > 0 ? (
guacamoleClients?.map((client: GuacamoleClient) => (
<GridCol w={'100%'} span={isMobile ? 18 : 6} key={client.id}>
<Popover
withArrow
withOverlay
trapFocus
closeOnClickOutside={!isOpen}
classNames={popoverClasses}
middlewares={{ shift: { padding: 20 } }}
position='bottom'
offset={isMobile ? -100 : 0}
>
<Popover.Target>
<Group justify='center'>
<GuacamoleClientSettingsItem client={client} />
</Group>
</Popover.Target>
<Popover.Dropdown w={isMobile ? '90%' : undefined}>
<DebouncedGuacamoleClient
guacamoleClient={client}
setIsOpen={setIsOpen}
/>
</Popover.Dropdown>
</Popover>
</GridCol>
))
) : (
<GridCol
w={'100%'}
span={18}
style={{ display: 'flex', justifyContent: 'center' }}
>
<NoItemsFound />
</GridCol>
)}
</Grid>
</Stack>
</Stack>
);
};
export default GuacamoleClients;

View File

@@ -38,7 +38,7 @@ const MessageSettings = () => {
const [messages, setMessages] = useState<Message[]>([]);
const [message, setMessage] = useState({});
const [debounced] = useDebouncedValue(message, 1000);
const [searchValue, setSearchValue] = useState('');
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [isLoading, setIsLoading] = useState(false);
@@ -52,7 +52,7 @@ const MessageSettings = () => {
if (messageData && messageDataIsFetched) {
setMessages(
messageData.filter((m: Message) =>
m.message.toLowerCase().includes(debouncedSearch.toLowerCase()),
m.message.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
),
);
}

View File

@@ -17,7 +17,7 @@ const SearchHistory = () => {
endpoint: searchEndpoint,
});
const [searchHistory, setSearchHistory] = useState<SearchHistoryItem[]>([]);
const [searchValue, setSearchValue] = useState('');
const [searchValue, setSearchValue] = useState<string | number>('');
const [searchHistoryItem, setSearchHistoryItem] = useState<SearchHistoryItem>();
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [isLoading, setIsLoading] = useState(false);

View File

@@ -63,7 +63,7 @@ const SearchProviderSettings = () => {
const { isMobile, theme, iconFilter } = useCommonHooks();
const { mutateAsync } = useMutationAPI();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [searchValue, setSearchValue] = useState('');
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
@@ -79,8 +79,8 @@ const SearchProviderSettings = () => {
providerData
.filter(
(a: Provider) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
a.url.toLowerCase().includes(debouncedSearch.toLowerCase()),
a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
a.url.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
)
.sort((a: Provider, b: Provider) => (a.prefix > b.prefix ? 1 : -1)),
);

View File

@@ -34,7 +34,7 @@ export function ThemePlaylists() {
const [selectedPlaylist, setSelectedPlaylist] =
useState<ThemePlaylist>(defaultPlaylist);
const { currentThemePlaylist, theme, isMobile } = useCommonHooks();
const [searchValue, setSearchValue] = useState('');
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [isLoading, setIsLoading] = useState(false);
@@ -62,7 +62,7 @@ export function ThemePlaylists() {
setThemePlaylists(
themePlaylistData.filter(
(a: ThemePlaylist) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
a.name.includes('Time of Day'),
),
);

View File

@@ -65,7 +65,7 @@ export function Themes() {
textShadowsEnabled: 0,
appShadowsEnabled: 0,
});
const [searchValue, setSearchValue] = useState('');
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [isLoading, setIsLoading] = useState(false);
const [isNew, setIsNew] = useState(false);
@@ -103,7 +103,7 @@ export function Themes() {
if (allThemeData && allThemeDataIsFetched) {
setAllThemes(
allThemeData.filter((a: Theme) =>
a.name.toLowerCase().includes(debouncedSearch.toLowerCase()),
a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
),
);
}

View File

@@ -46,7 +46,7 @@ const WolClientSettings = () => {
const { mutateAsync } = useMutationAPI();
const { isMobile, theme } = useCommonHooks();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [searchValue, setSearchValue] = useState('');
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
@@ -60,7 +60,7 @@ const WolClientSettings = () => {
if (wolClientsData && wolClientsIsFetched) {
setWolClients(
wolClientsData.filter((a: WolClient) =>
a.hostname.toLowerCase().includes(debouncedSearch.toLowerCase()),
a.hostname.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
),
);
}