Search bar fixes and performance updates

This commit is contained in:
Sean Edmunds
2026-01-08 12:53:50 -06:00
parent 38285943cd
commit 93ea28f968
20 changed files with 376 additions and 543 deletions

View File

@@ -1,6 +1,5 @@
import { ActionIcon, Divider, Group, ScrollArea, Stack, Tabs, Text } from '@mantine/core';
import { useCommonHooks, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { FaPaintBrush, FaRegListAlt } from 'react-icons/fa';
import {
FiArrowLeftCircle,
@@ -27,19 +26,14 @@ import WolClients from './components/WoL/WolClients';
export function Settings() {
const endpoint = 'api/personalData';
const { data: personalData, isFetched: personalDataIsFetched } = useQueryAPI({
const { data: personalData } = useQueryAPI({
endpoint: endpoint,
});
const [personal, setPersonal] = useState<PersonalData>();
const personal: PersonalData = personalData?.[0];
const { isMobile, theme } = useCommonHooks();
const navigate = useNavigate();
const { tabValue } = useParams();
useEffect(() => {
if (personalData && personalDataIsFetched) {
setPersonal(personalData[0]);
}
}, [personalData, personalDataIsFetched]);
return (
<Stack h={'100%'} gap={0}>
<Group gap={0} style={{ position: 'relative', paddingBottom: '1rem' }}>

View File

@@ -38,6 +38,8 @@ const ApplicationSettings = () => {
index: 0,
enabled: 1,
};
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const form = useForm({
initialValues: defaultApplication,
validate: {
@@ -49,19 +51,21 @@ const ApplicationSettings = () => {
});
const appsEndpoint = 'api/applications';
const appsOrderEndpoint = 'api/applicationOrder';
const {
data: applicationData,
isFetched: applicationDataIsFetched,
isFetching: applicationDataIsFetching,
} = useQueryAPI({
const { data: applicationData, isFetching: applicationDataIsFetching } = useQueryAPI({
endpoint: appsEndpoint,
});
const [applications, setApplications] = useState<Application[]>([]);
const applications: Application[] =
applicationData
.filter(
(a: Application) =>
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)) || [];
const [applicationList, setApplicationList] = useState<Application[]>([]);
const { isMobile, reorder, theme } = useCommonHooks();
const { mutateAsync } = useMutationAPI();
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [dragging, setDragging] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
@@ -93,22 +97,10 @@ const ApplicationSettings = () => {
form.reset();
};
useEffect(() => {
if (applicationData && applicationDataIsFetched) {
setApplications(
applicationData
.filter(
(a: Application) =>
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)),
);
}
if (debouncedSearch === searchValue) {
setIsLoading(false);
}
}, [applicationData, applicationDataIsFetched, debouncedSearch]);
}, [debouncedSearch]);
useEffect(() => {
if (applicationList.length > 0 && applicationList !== applications) {

View File

@@ -33,14 +33,10 @@ const BookmarkSettings = () => {
},
});
const bookmarksEndpoint = 'api/bookmarks';
const {
data: bookmarkData,
isFetched: bookmarkDataIsFetched,
isFetching: bookmarkDataIsFetching,
} = useQueryAPI({
const { data: bookmarkData, isFetching: bookmarkDataIsFetching } = useQueryAPI({
endpoint: bookmarksEndpoint,
});
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
const bookmarks = bookmarkData || [];
const categoriesEndpoint = 'api/bookmarkCategories';
const { data: groupData, isFetched: groupsIsFetched } = useQueryAPI({
endpoint: categoriesEndpoint,
@@ -54,7 +50,27 @@ const BookmarkSettings = () => {
const [debouncedGroupSearch] = useDebouncedValue(searchValue, 500);
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [isLoading, setIsLoading] = useState(false);
const foundBookmarkGroups = [
...new Set(
bookmarks
.filter(
(a: Bookmark) =>
a.name
.toLowerCase()
.includes((debouncedGroupSearch as string).toLowerCase()) ||
a.url.toLowerCase().includes((debouncedGroupSearch as string).toLowerCase()),
)
.map((a: Bookmark) =>
groupData.find(
(b: BookmarkGroup) =>
b.categoryName.toLowerCase() === a.category.toLowerCase(),
),
),
),
];
const foundGroups = groupData.filter((a: BookmarkGroup) =>
a.categoryName.toLowerCase().includes((debouncedGroupSearch as string).toLowerCase()),
);
const handleClose = () => {
closeModal();
form.reset();
@@ -80,40 +96,8 @@ const BookmarkSettings = () => {
);
}
useEffect(() => {
if (bookmarkData && bookmarkDataIsFetched) {
setBookmarks(bookmarkData);
}
}, [bookmarkDataIsFetched, bookmarkData]);
useEffect(() => {
if (groupData && groupsIsFetched) {
const foundBookmarkGroups = [
...new Set(
bookmarks
.filter(
(a: Bookmark) =>
a.name
.toLowerCase()
.includes((debouncedGroupSearch as string).toLowerCase()) ||
a.url
.toLowerCase()
.includes((debouncedGroupSearch as string).toLowerCase()),
)
.map((a: Bookmark) =>
groupData.find(
(b: BookmarkGroup) =>
b.categoryName.toLowerCase() === a.category.toLowerCase(),
),
),
),
];
const foundGroups = groupData.filter((a: BookmarkGroup) =>
a.categoryName
.toLowerCase()
.includes((debouncedGroupSearch as string).toLowerCase()),
);
const groupList = [...new Set([...foundBookmarkGroups, ...foundGroups])].sort(
(a: BookmarkGroup, b: BookmarkGroup) =>
a.categoryIndex > b.categoryIndex ? 1 : -1,

View File

@@ -16,11 +16,15 @@ function DebouncedBookmarkItem({
const { handleDelete, isMobile, theme } = useCommonHooks();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const { mutateAsync } = useMutationAPI();
const { data: groupData, isFetched: groupsIsFetched } = useQueryAPI({
const { data: groupData } = useQueryAPI({
endpoint: 'api/bookmarkCategories',
});
const [bookmark, setBookmark] = useState<Bookmark>(bookmarkItem);
const [groups, setGroups] = useState<BookmarkGroup[]>([]);
const groups: BookmarkGroup[] =
groupData.sort(
(a: BookmarkGroup, b: BookmarkGroup) =>
a.categoryName.toLocaleLowerCase() > b.categoryName.toLocaleLowerCase(),
) || [];
const [update, setUpdate] = useState<any>({});
const [debounced] = useDebouncedValue(update, 1000);
const handleClose = () => {
@@ -39,17 +43,6 @@ function DebouncedBookmarkItem({
}
}, [update]);
useEffect(() => {
if (groupData) {
setGroups(
groupData.sort(
(a: BookmarkGroup, b: BookmarkGroup) =>
a.categoryName.toLocaleLowerCase() > b.categoryName.toLocaleLowerCase(),
),
);
}
}, [groupData, groupsIsFetched]);
return (
<Card
p={isMobile ? 'xl' : undefined}

View File

@@ -15,7 +15,7 @@ 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 { useState } from 'react';
import { FaTerminal } from 'react-icons/fa';
import { FaComputer, FaDesktop } from 'react-icons/fa6';
import { popoverClasses, selectClasses, textInputClasses } from 'styles';
@@ -39,20 +39,24 @@ const GuacamoleClients = () => {
},
});
const types: string[] = ['SSH', 'RDP', 'VNC'];
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const endpoint = 'api/guacamoleClients';
const {
data: guacamoleClientsData,
isFetched: guacamoleClientsIsFetched,
isFetching: guacamoleClientsIsFetching,
} = useQueryAPI({
endpoint: endpoint,
});
const [guacamoleClients, setGuacamoleClients] = useState<GuacamoleClient[]>([]);
const { data: guacamoleClientsData, isFetching: guacamoleClientsIsFetching } =
useQueryAPI({
endpoint: endpoint,
});
const guacamoleClients =
guacamoleClientsData.filter(
(a: GuacamoleClient) =>
a.clientName.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
a.clientIdentifier
.toLowerCase()
.includes((debouncedSearch as string).toLowerCase()),
) || [];
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);
@@ -61,25 +65,6 @@ const GuacamoleClients = () => {
form.reset();
};
useEffect(() => {
if (guacamoleClientsData && guacamoleClientsIsFetched) {
setGuacamoleClients(
guacamoleClientsData.filter(
(a: GuacamoleClient) =>
a.clientName
.toLowerCase()
.includes((debouncedSearch as string).toLowerCase()) ||
a.clientIdentifier
.toLowerCase()
.includes((debouncedSearch as string).toLowerCase()),
),
);
}
if (debouncedSearch === searchValue) {
setIsLoading(false);
}
}, [guacamoleClientsData, guacamoleClientsIsFetched, debouncedSearch]);
return (
<Stack>
<AddItemModal

View File

@@ -18,11 +18,7 @@ import DebouncedMessage from './DebouncedMessage';
const MessageSettings = () => {
const endpoint = 'api/messages';
const {
data: messageData,
isFetched: messageDataIsFetched,
isFetching: messageDataIsFetching,
} = useQueryAPI({
const { data: messageData, isFetching: messageDataIsFetching } = useQueryAPI({
endpoint: endpoint,
});
const form = useForm({
@@ -33,13 +29,16 @@ const MessageSettings = () => {
message: isNotEmpty('Message is required'),
},
});
const { isMobile, theme } = useCommonHooks();
const { mutateAsync } = useMutationAPI();
const [messages, setMessages] = useState<Message[]>([]);
const [message, setMessage] = useState({});
const [debounced] = useDebouncedValue(message, 1000);
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const { isMobile, theme } = useCommonHooks();
const { mutateAsync } = useMutationAPI();
const messages =
messageData.filter((m: Message) =>
m.message.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
) || [];
const [message, setMessage] = useState({});
const [debounced] = useDebouncedValue(message, 1000);
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [isLoading, setIsLoading] = useState(false);
@@ -48,19 +47,6 @@ const MessageSettings = () => {
form.reset();
};
useEffect(() => {
if (messageData && messageDataIsFetched) {
setMessages(
messageData.filter((m: Message) =>
m.message.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
),
);
}
if (debouncedSearch === searchValue) {
setIsLoading(false);
}
}, [messageData, messageDataIsFetched, debouncedSearch]);
useEffect(() => {
if (Object.keys(debounced).length) {
mutateAsync({ method: 'PATCH', endpoint: endpoint, body: debounced });

View File

@@ -9,14 +9,10 @@ import type { SearchHistoryItem } from 'types';
const SearchHistory = () => {
const searchEndpoint = 'api/searchHistory';
const {
data: historyData,
isFetched: historyDataIsFetched,
isFetching: historyDataIsFetching,
} = useQueryAPI({
const { data: historyData, isFetching: historyDataIsFetching } = useQueryAPI({
endpoint: searchEndpoint,
});
const [searchHistory, setSearchHistory] = useState<SearchHistoryItem[]>([]);
const searchHistory: SearchHistoryItem[] = historyData || [];
const [searchValue, setSearchValue] = useState<string | number>('');
const [searchHistoryItem, setSearchHistoryItem] = useState<SearchHistoryItem>();
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
@@ -36,12 +32,6 @@ const SearchHistory = () => {
},
]);
useEffect(() => {
if (historyData && historyDataIsFetched) {
setSearchHistory(historyData);
}
}, [historyData, historyDataIsFetched]);
useEffect(() => {
setGlobalFilter(debouncedSearch);
if (debouncedSearch === globalFilter) {

View File

@@ -52,19 +52,23 @@ const SearchProviderSettings = () => {
},
});
const endpoint = 'api/providers';
const {
data: providerData,
isFetched: providerDataIsFetched,
isFetching: providerDataIsFetching,
} = useQueryAPI({
const { data: providerData, isFetching: providerDataIsFetching } = useQueryAPI({
endpoint: endpoint,
});
const [providers, setProviders] = useState<Provider[]>([]);
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const providers =
providerData
.filter(
(a: Provider) =>
a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
a.url.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
a.prefix.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
)
.sort((a: Provider, b: Provider) => (a.prefix > b.prefix ? 1 : -1)) ?? [];
const { isMobile, theme, iconFilter } = useCommonHooks();
const { mutateAsync } = useMutationAPI();
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);
@@ -74,22 +78,10 @@ const SearchProviderSettings = () => {
};
useEffect(() => {
if (providerData && providerDataIsFetched) {
setProviders(
providerData
.filter(
(a: Provider) =>
a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
a.url.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
a.prefix.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
)
.sort((a: Provider, b: Provider) => (a.prefix > b.prefix ? 1 : -1)),
);
}
if (debouncedSearch === searchValue) {
setIsLoading(false);
}
}, [providerData, providerDataIsFetched, debouncedSearch]);
}, [debouncedSearch]);
return (
<Stack>

View File

@@ -15,14 +15,18 @@ export function ThemePlaylists() {
endpoint: themesEndpoint,
});
const themePlaylistsEndpoint = 'api/themePlaylists';
const {
data: themePlaylistData,
isFetched: themePlaylistDataIsFetched,
isFetching: themePlaylistDataIsFetching,
} = useQueryAPI({
endpoint: themePlaylistsEndpoint,
});
const [themePlaylists, setThemePlaylists] = useState<ThemePlaylist[]>([]);
const { data: themePlaylistData, isFetching: themePlaylistDataIsFetching } =
useQueryAPI({
endpoint: themePlaylistsEndpoint,
});
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const themePlaylists: ThemePlaylist[] =
themePlaylistData?.filter(
(a: ThemePlaylist) =>
a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
a.name.includes('Time of Day'),
) || [];
const defaultPlaylist = {
id: undefined,
default: 0,
@@ -34,8 +38,6 @@ export function ThemePlaylists() {
const [selectedPlaylist, setSelectedPlaylist] =
useState<ThemePlaylist>(defaultPlaylist);
const { currentThemePlaylist, theme, isMobile } = useCommonHooks();
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [isLoading, setIsLoading] = useState(false);
const handleOpen = (playlist: ThemePlaylist) => {
@@ -58,19 +60,10 @@ export function ThemePlaylists() {
};
useEffect(() => {
if (themePlaylistData && themePlaylistDataIsFetched) {
setThemePlaylists(
themePlaylistData.filter(
(a: ThemePlaylist) =>
a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
a.name.includes('Time of Day'),
),
);
}
if (debouncedSearch === searchValue) {
setIsLoading(false);
}
}, [themePlaylistData, themePlaylistDataIsFetched, debouncedSearch]);
}, [debouncedSearch]);
return (
<Stack>

View File

@@ -25,14 +25,14 @@ import { ThemeModal } from './ThemeModal';
export function Themes() {
const backgroundsEndpoint = 'api/backgrounds';
const {
data: allThemeData,
isFetched: allThemeDataIsFetched,
isFetching: allThemeDataIsFetching,
} = useQueryAPI({
const { data: allThemeData, isFetching: allThemeDataIsFetching } = useQueryAPI({
endpoint: 'api/themes',
});
const [allThemes, setAllThemes] = useState<Theme[]>([]);
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const allThemes: Theme[] = allThemeData?.filter((a: Theme) =>
a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
);
const {
data: backgroundData,
isFetched: backgroundDataIsFetched,
@@ -43,7 +43,7 @@ export function Themes() {
const { data: themePlaylistData } = useQueryAPI({
endpoint: 'api/themePlaylists',
});
const [themePlaylists, setThemePlaylists] = useState<ThemePlaylist[]>([]);
const themePlaylists: ThemePlaylist[] = themePlaylistData || [];
const [themeModalOpened, { open: openThemeModal, close: closeThemeModal }] =
useDisclosure(false);
const [
@@ -51,7 +51,18 @@ export function Themes() {
{ open: openBackgroundModal, close: closeBackgroundModal },
] = useDisclosure(false);
const [backgroundList, setBackgroundList] = useState<string[]>(['None']);
const [inUseBackgrounds, setInUseBackgrounds] = useState<string[]>([]);
const inUseBackgrounds: string[] = [
...new Set(
allThemes
?.filter((t) => {
if (t.backgroundImage !== 'none') {
return t.backgroundImage;
}
return undefined;
})
.map((t) => t.backgroundImage),
),
] as string[];
const [selectedTheme, setSelectedTheme] = useState<Theme>({
id: undefined,
name: 'default',
@@ -65,8 +76,6 @@ export function Themes() {
textShadowsEnabled: 0,
appShadowsEnabled: 0,
});
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const [isLoading, setIsLoading] = useState(false);
const [isNew, setIsNew] = useState(false);
const navigate = useNavigate();
@@ -94,40 +103,10 @@ export function Themes() {
}, [backgroundData, backgroundDataIsFetched]);
useEffect(() => {
if (themePlaylistData) {
setThemePlaylists(themePlaylistData);
}
}, [themePlaylistData]);
useEffect(() => {
if (allThemeData && allThemeDataIsFetched) {
setAllThemes(
allThemeData.filter((a: Theme) =>
a.name.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
),
);
}
if (debouncedSearch === searchValue) {
setIsLoading(false);
}
}, [allThemeData, allThemeDataIsFetched, debouncedSearch]);
useEffect(() => {
if (allThemes) {
setInUseBackgrounds([
...new Set(
allThemes
.filter((t) => {
if (t.backgroundImage !== 'none') {
return t.backgroundImage;
}
return undefined;
})
.map((t) => t.backgroundImage),
),
]);
}
}, [allThemes]);
}, [debouncedSearch]);
return (
<Stack>
@@ -195,7 +174,7 @@ export function Themes() {
}}
pos={'absolute'}
/>
{allThemes.length > 0 ? (
{allThemes?.length > 0 ? (
allThemes.map((cardTheme: Theme) => (
<Group key={cardTheme.name} pos={'relative'}>
{cardTheme.name === currentTheme && (

View File

@@ -35,19 +35,20 @@ const WolClientSettings = () => {
},
});
const endpoint = 'api/wolClients';
const {
data: wolClientsData,
isFetched: wolClientsIsFetched,
isFetching: wolClientsIsFetching,
} = useQueryAPI({
const { data: wolClientsData, isFetching: wolClientsIsFetching } = useQueryAPI({
endpoint: endpoint,
});
const [wolClients, setWolClients] = useState<WolClient[]>([]);
const [searchValue, setSearchValue] = useState<string | number>('');
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
const wolClients: WolClient[] =
wolClientsData.filter(
(a: WolClient) =>
a.hostname.toLowerCase().includes((debouncedSearch as string).toLowerCase()) ||
a.macAddress.toLowerCase().includes((debouncedSearch as string).toLowerCase()),
) || [];
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);
@@ -57,23 +58,10 @@ const WolClientSettings = () => {
};
useEffect(() => {
if (wolClientsData && wolClientsIsFetched) {
setWolClients(
wolClientsData.filter(
(a: WolClient) =>
a.hostname
.toLowerCase()
.includes((debouncedSearch as string).toLowerCase()) ||
a.macAddress
.toLowerCase()
.includes((debouncedSearch as string).toLowerCase()),
),
);
}
if (debouncedSearch === searchValue) {
setIsLoading(false);
}
}, [wolClientsData, wolClientsIsFetched, debouncedSearch]);
}, [debouncedSearch]);
return (
<Stack>