Starting guac clients
This commit is contained in:
717
package-lock.json
generated
717
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -10,36 +10,36 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gfazioli/mantine-text-animate": "^2.3.4",
|
||||
"@gfazioli/mantine-text-animate": "^2.3.9",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@mantine/core": "^8.3.7",
|
||||
"@mantine/dates": "^8.3.7",
|
||||
"@mantine/form": "^8.3.7",
|
||||
"@mantine/hooks": "^8.3.7",
|
||||
"@mantine/notifications": "^8.3.7",
|
||||
"@tanstack/react-query": "^5.90.9",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"@tanstack/react-query-persist-client": "^5.90.11",
|
||||
"@mantine/core": "^8.3.10",
|
||||
"@mantine/dates": "^8.3.10",
|
||||
"@mantine/form": "^8.3.10",
|
||||
"@mantine/hooks": "^8.3.10",
|
||||
"@mantine/notifications": "^8.3.10",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@tanstack/react-query-devtools": "^5.91.1",
|
||||
"@tanstack/react-query-persist-client": "^5.90.14",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"jotai": "^2.15.1",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"jotai": "^2.16.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.9.6",
|
||||
"recharts": "^3.4.1"
|
||||
"react-router-dom": "^7.11.0",
|
||||
"recharts": "^3.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"@vitest/coverage-v8": "^4.0.9",
|
||||
"@vitest/ui": "^4.0.9",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitest/coverage-v8": "^4.0.16",
|
||||
"@vitest/ui": "^4.0.16",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.2",
|
||||
"vite": "^7.3.0",
|
||||
"vite-plugin-dts": "~4.5.4",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.0.9"
|
||||
"vite-tsconfig-paths": "^6.0.3",
|
||||
"vitest": "^4.0.16"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { ActionIcon, TextInput } from '@mantine/core';
|
||||
import { TextInput, type TextInputProps } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FiX } from 'react-icons/fi';
|
||||
import { textInputClasses } from 'styles';
|
||||
|
||||
export function DebouncedInput({
|
||||
value: initialValue,
|
||||
@@ -12,7 +10,7 @@ export function DebouncedInput({
|
||||
value: string | number;
|
||||
onChange: (value: string | number) => void;
|
||||
debounce?: number;
|
||||
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'>) {
|
||||
} & Omit<TextInputProps, 'onChange'>) {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -25,43 +23,14 @@ export function DebouncedInput({
|
||||
}, debounce);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [value, debounce, onChange]);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
{...props}
|
||||
value={value}
|
||||
size='sm'
|
||||
p={0}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
classNames={textInputClasses}
|
||||
styles={{
|
||||
input: {
|
||||
borderRadius: 0,
|
||||
borderStyle: 'none',
|
||||
color: 'var(--mantine-color-primary-0)',
|
||||
'&::placeholder': {
|
||||
color: 'red !important',
|
||||
},
|
||||
},
|
||||
wrapper: {
|
||||
'&::placeholder': {
|
||||
color: 'red !important',
|
||||
},
|
||||
},
|
||||
}}
|
||||
rightSection={
|
||||
value && (
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
setValue('');
|
||||
}}
|
||||
>
|
||||
<FiX />
|
||||
</ActionIcon>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Stack,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { useCommonHooks } from 'hooks';
|
||||
import { useState } from 'react';
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
textClasses,
|
||||
textInputClasses,
|
||||
} from 'styles';
|
||||
import { DebouncedInput } from './DebouncedInput';
|
||||
|
||||
export function SettingsHeader({
|
||||
setDragging,
|
||||
@@ -40,8 +40,8 @@ export function SettingsHeader({
|
||||
highlightList: string[];
|
||||
description: string;
|
||||
};
|
||||
searchValue: string | undefined;
|
||||
setSearchValue: (searchValue: string) => void;
|
||||
searchValue: string | number;
|
||||
setSearchValue: (searchValue: string | number) => void;
|
||||
searchPlaceholder: string | undefined;
|
||||
isLoading: boolean;
|
||||
setIsLoading: (isLoading: boolean) => void;
|
||||
@@ -120,7 +120,7 @@ export function SettingsHeader({
|
||||
)}
|
||||
</Group>
|
||||
{searchValue !== undefined && (
|
||||
<TextInput
|
||||
<DebouncedInput
|
||||
disabled={disabled}
|
||||
w={isMobile ? undefined : '35%'}
|
||||
pos={isMobile ? undefined : 'absolute'}
|
||||
@@ -129,9 +129,9 @@ export function SettingsHeader({
|
||||
autoFocus={!isMobile}
|
||||
placeholder={searchPlaceholder ?? 'Search...'}
|
||||
value={searchValue}
|
||||
onInput={(event: any) => {
|
||||
setIsLoading(true);
|
||||
setSearchValue(event.target.value);
|
||||
onInput={() => setIsLoading(true)}
|
||||
onChange={(value: any) => {
|
||||
setSearchValue(value);
|
||||
}}
|
||||
style={{
|
||||
zIndex: 100,
|
||||
|
||||
@@ -69,7 +69,7 @@ export function useCommonHooks() {
|
||||
key: 'providerIcon',
|
||||
defaultValue: 'FaGoogle',
|
||||
});
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [searchValue, setSearchValue] = useState<string | number>('');
|
||||
const [providerLabel, setProviderLabel] = useState(defaultProviderLabel);
|
||||
const [providerURL, setProviderURL] = useState(defaultProviderURL);
|
||||
const [providerPrefix, setProviderPrefix] = useState(defaultProviderPrefix);
|
||||
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
Bookmark,
|
||||
CurrentWeather,
|
||||
ExtendedForecast,
|
||||
GuacamoleClient,
|
||||
Provider,
|
||||
SearchHistoryItem,
|
||||
} from 'types';
|
||||
@@ -48,9 +49,14 @@ export default function Search({
|
||||
endpoint: 'api/providers',
|
||||
});
|
||||
const [providers, setProviders] = useState<Provider[]>([]);
|
||||
const [guacamoleClients, setGuacamoleClients] = useState<GuacamoleClient[]>([]);
|
||||
const { data: applicationData, isFetching: isApplicationDataFetching } = useQueryAPI({
|
||||
endpoint: 'api/enabledApplications',
|
||||
});
|
||||
const { data: guacamoleClientData, isFetched: guacamoleClientDataIsFetched } =
|
||||
useQueryAPI({
|
||||
endpoint: 'api/guacamoleClients',
|
||||
});
|
||||
const { data: bookmarkData, isFetched: bookmarksIsFetched } = useQueryAPI({
|
||||
endpoint: 'api/bookmarks',
|
||||
});
|
||||
@@ -130,10 +136,6 @@ export default function Search({
|
||||
item.target.value.substr(1, item.target.value.indexOf(' ') - 1) ??
|
||||
item.target.value.substr(1);
|
||||
subtext = item.target.value.substr(2 + option.length);
|
||||
} else if (item.target.value.startsWith('!')) {
|
||||
if (item.target.value.startsWith('!w')) {
|
||||
subtext = item.target.value.substr(2);
|
||||
}
|
||||
} else {
|
||||
subtext = item.target.value;
|
||||
}
|
||||
@@ -162,6 +164,13 @@ export default function Search({
|
||||
? url
|
||||
: `http://${url}`;
|
||||
open(validUrl, '_self');
|
||||
} else if (providerLabel.startsWith('Connect to')) {
|
||||
const id = guacamoleClients.find(
|
||||
(client: GuacamoleClient) =>
|
||||
client.clientName.substring(1).toLowerCase() === item.target.value.substr(1),
|
||||
)?.id;
|
||||
|
||||
open(`https://sean.bet/guacamole/#/${id}`, '_self');
|
||||
} else if (urlPattern.test(item.target.value)) {
|
||||
if (/^(https?:\/\/).*/i.test(item.target.value))
|
||||
globalThis.location = item.target.value;
|
||||
@@ -250,7 +259,6 @@ export default function Search({
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onInput = (item: any) => {
|
||||
openDropdown();
|
||||
setSearchValue(item.currentTarget.value);
|
||||
@@ -288,6 +296,17 @@ export default function Search({
|
||||
} else {
|
||||
setProviderLabel('Invalid function');
|
||||
}
|
||||
} else if (item.target.value.startsWith('>')) {
|
||||
if (
|
||||
guacamoleClients?.find(
|
||||
(client: GuacamoleClient) =>
|
||||
client.clientName.substring(1).toLowerCase() === item.target.value.substr(1),
|
||||
)
|
||||
) {
|
||||
setProviderLabel(`Connect to ${item.target.value.substr(1)}`);
|
||||
} else {
|
||||
setProviderLabel('Invalid connection');
|
||||
}
|
||||
} else {
|
||||
setProviderLabel(defaultProviderLabel);
|
||||
setProviderURL(defaultProviderURL);
|
||||
@@ -311,6 +330,21 @@ export default function Search({
|
||||
};
|
||||
});
|
||||
}
|
||||
if (splittedSearch[0].startsWith('>')) {
|
||||
return guacamoleClientData
|
||||
.filter((guacamoleClient: GuacamoleClient) =>
|
||||
guacamoleClient.clientName
|
||||
.toLowerCase()
|
||||
.includes(splittedSearch[0].substring(1).toLowerCase()),
|
||||
)
|
||||
.map((guacamoleClient: GuacamoleClient) => {
|
||||
return {
|
||||
label: guacamoleClient.clientName,
|
||||
value: guacamoleClient.clientIdentifier,
|
||||
...guacamoleClient,
|
||||
};
|
||||
});
|
||||
}
|
||||
const historyOptions = options
|
||||
.filter((option: any) => {
|
||||
return splittedSearch.every((searchWord) =>
|
||||
@@ -350,7 +384,9 @@ export default function Search({
|
||||
: (providers.find((p: Provider) => p.prefix === item.option.prefix)?.icon ??
|
||||
'')
|
||||
];
|
||||
return searchValue.startsWith('!') ? (
|
||||
if ((searchValue as string).startsWith('>')) {
|
||||
}
|
||||
return (searchValue as string).startsWith('!') ? (
|
||||
<Group
|
||||
pos={'relative'}
|
||||
m={0}
|
||||
@@ -380,6 +416,26 @@ export default function Search({
|
||||
{item.option.name}
|
||||
</Text>
|
||||
</Group>
|
||||
) : (searchValue as string).startsWith('>') ? (
|
||||
<Group
|
||||
pos={'relative'}
|
||||
m={0}
|
||||
p={0}
|
||||
gap={0}
|
||||
justify='center'
|
||||
w={'100%'}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Text pos={'absolute'} left={'1rem'} c={'var(--mantine-color-accent-0)'}>
|
||||
Connect to
|
||||
</Text>
|
||||
<Text pos={'absolute'} right={'1rem'} c={'var(--mantine-color-accent-0)'}>
|
||||
{item.option.name || item.option.clientName || 'Invalid connection'}
|
||||
</Text>
|
||||
</Group>
|
||||
) : item.option.value === 'load' ? (
|
||||
<Group
|
||||
pos={'relative'}
|
||||
@@ -419,6 +475,12 @@ export default function Search({
|
||||
}
|
||||
}, [providerData, providerDataIsFetched]);
|
||||
|
||||
useEffect(() => {
|
||||
if (guacamoleClientData && guacamoleClientDataIsFetched) {
|
||||
setGuacamoleClients(guacamoleClientData);
|
||||
}
|
||||
}, [guacamoleClientData, guacamoleClientDataIsFetched]);
|
||||
|
||||
useEffect(() => {
|
||||
if (applicationData && !isApplicationDataFetching) {
|
||||
setApplications(applicationData);
|
||||
@@ -429,7 +491,7 @@ export default function Search({
|
||||
if (applications) {
|
||||
setFilteredApplications(
|
||||
applications.filter((app: Application) =>
|
||||
app.name.toLowerCase().includes(searchValue.toLowerCase()),
|
||||
app.name.toLowerCase().includes((searchValue as string).toLowerCase()),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -441,7 +503,7 @@ export default function Search({
|
||||
bookmarkData.filter(
|
||||
(bookmark: Bookmark) =>
|
||||
bookmark.enabled === 1 &&
|
||||
bookmark.name.toLowerCase().includes(searchValue.toLowerCase()),
|
||||
bookmark.name.toLowerCase().includes((searchValue as string).toLowerCase()),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -467,7 +529,7 @@ export default function Search({
|
||||
}, [listIndex, dropdownOpened]);
|
||||
|
||||
useEffect(() => {
|
||||
const splittedSearch = searchValue.toLowerCase().trim().split(' ');
|
||||
const splittedSearch = (searchValue as string).toLowerCase().trim().split(' ');
|
||||
const filtered = searchHistory.filter((item: SearchHistoryItem) => {
|
||||
const option = `${item.prefix} ${item.searchTerm}`;
|
||||
const words = option.toLowerCase().trim().split(' ');
|
||||
@@ -514,8 +576,8 @@ export default function Search({
|
||||
...item,
|
||||
}))}
|
||||
filter={optionsFilter}
|
||||
value={searchValue}
|
||||
searchValue={searchValue}
|
||||
value={searchValue as string}
|
||||
searchValue={searchValue as string}
|
||||
placeholder={placeholderValue}
|
||||
nothingFoundMessage={
|
||||
<Text c={'var(--mantine-color-primary-0)'}>Nothing found...</Text>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function RecommendedApplications({
|
||||
dropdownOpened,
|
||||
filteredApplications,
|
||||
}: {
|
||||
searchValue: string;
|
||||
searchValue: string | number;
|
||||
dropdownOpened: boolean;
|
||||
filteredApplications: Application[];
|
||||
}) {
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function RecommendedBookmarks({
|
||||
dropdownOpened,
|
||||
}: {
|
||||
bookmarks: Bookmark[];
|
||||
searchValue: string;
|
||||
searchValue: string | number;
|
||||
dropdownOpened: boolean;
|
||||
}) {
|
||||
return (
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { actionIconClasses, scrollAreaClasses, tabClasses, textClasses } from 'styles';
|
||||
import Applications from './components/Applications/Applications';
|
||||
import Bookmarks from './components/Bookmarks/Bookmarks';
|
||||
import GuacamoleClients from './components/GuacamoleClients/GuacamoleClients';
|
||||
import Messages from './components/Messages/Messages';
|
||||
import Personalization from './components/Personalization/Personalization';
|
||||
import SearchHistory from './components/Search/SearchHistory';
|
||||
@@ -162,6 +163,9 @@ export function Settings() {
|
||||
<Tabs.Tab px={isMobile ? '.9rem' : undefined} value='wolClients'>
|
||||
{isMobile ? <FiMonitor /> : 'WoL Clients'}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab px={isMobile ? '.9rem' : undefined} value='guacamoleClients'>
|
||||
{isMobile ? <FiMonitor /> : 'Guacamole Clients'}
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value='personalization'>
|
||||
<ScrollArea
|
||||
@@ -235,6 +239,14 @@ export function Settings() {
|
||||
<WolClients />
|
||||
</ScrollArea>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value='guacamoleClients'>
|
||||
<ScrollArea
|
||||
className={scrollAreaClasses.default}
|
||||
classNames={scrollAreaClasses}
|
||||
>
|
||||
<GuacamoleClients />
|
||||
</ScrollArea>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
6
src/types/guacamoleClient.ts
Normal file
6
src/types/guacamoleClient.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface GuacamoleClient {
|
||||
id: number | undefined;
|
||||
clientName: string;
|
||||
clientIdentifier: string;
|
||||
enabled: number;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export type { AppCategory, Application } from './application';
|
||||
export type { Bookmark } from './bookmark';
|
||||
export type { BookmarkGroup } from './bookmarkGroup';
|
||||
export type { GuacamoleClient } from './guacamoleClient';
|
||||
export type { Location } from './location';
|
||||
export type { Message } from './message';
|
||||
export type { PersonalData } from './personalData';
|
||||
|
||||
Reference in New Issue
Block a user