diff --git a/frontend/src/pages/admin/activity/AdminActivity.tsx b/frontend/src/pages/admin/activity/AdminActivity.tsx index 1b980e8e..5db39b27 100644 --- a/frontend/src/pages/admin/activity/AdminActivity.tsx +++ b/frontend/src/pages/admin/activity/AdminActivity.tsx @@ -1,24 +1,20 @@ -import { useState } from 'react'; -import { z } from 'zod'; import getAdminActivity from '@/api/admin/getAdminActivity.ts'; import { getEmptyPaginationSet } from '@/api/axios.ts'; import AdminContentContainer from '@/elements/containers/AdminContentContainer.tsx'; import Table from '@/elements/Table.tsx'; import { queryKeys } from '@/lib/queryKeys.ts'; -import { activitySchema } from '@/lib/schemas/activity.ts'; import { adminActivityColumns } from '@/lib/tableColumns.ts'; import { useSearchablePaginatedTable } from '@/plugins/useSearchablePageableTable.ts'; import ActivityRow from './ActivityRow.tsx'; export default function AdminActivity() { - const [activities, setActivities] = useState>>(getEmptyPaginationSet()); - - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.activity.all(), fetcher: getAdminActivity, - setStoreData: setActivities, }); + const activities = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( diff --git a/frontend/src/pages/admin/backupConfigurations/AdminBackupConfigurations.tsx b/frontend/src/pages/admin/backupConfigurations/AdminBackupConfigurations.tsx index 6d8c4cdf..2ae5879e 100644 --- a/frontend/src/pages/admin/backupConfigurations/AdminBackupConfigurations.tsx +++ b/frontend/src/pages/admin/backupConfigurations/AdminBackupConfigurations.tsx @@ -1,30 +1,32 @@ import { faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Route, Routes, useNavigate } from 'react-router'; +import { z } from 'zod'; import getBackupConfigurations from '@/api/admin/backup-configurations/getBackupConfigurations.ts'; +import { getEmptyPaginationSet } from '@/api/axios.ts'; import Button from '@/elements/Button.tsx'; import { AdminCan } from '@/elements/Can.tsx'; import AdminContentContainer from '@/elements/containers/AdminContentContainer.tsx'; import Table from '@/elements/Table.tsx'; import { queryKeys } from '@/lib/queryKeys.ts'; +import { adminBackupConfigurationSchema } from '@/lib/schemas/admin/backupConfigurations.ts'; import { backupConfigurationTableColumns } from '@/lib/tableColumns.ts'; import BackupConfigurationCreateOrUpdate from '@/pages/admin/backupConfigurations/BackupConfigurationCreateOrUpdate.tsx'; import BackupConfigurationRow from '@/pages/admin/backupConfigurations/BackupConfigurationRow.tsx'; import BackupConfigurationView from '@/pages/admin/backupConfigurations/BackupConfigurationView.tsx'; import { useSearchablePaginatedTable } from '@/plugins/useSearchablePageableTable.ts'; import AdminPermissionGuard from '@/routers/guards/AdminPermissionGuard.tsx'; -import { useAdminStore } from '@/stores/admin.tsx'; function BackupConfigurationsContainer() { const navigate = useNavigate(); - const { backupConfigurations, setBackupConfigurations } = useAdminStore(); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.backupConfigurations.all(), fetcher: getBackupConfigurations, - setStoreData: setBackupConfigurations, }); + const backupConfigurations = data ?? getEmptyPaginationSet>(); + return ( ; }) { - const [backupConfigurationBackups, setBackupConfigurationBackups] = useState< - Pagination> - >(getEmptyPaginationSet()); - - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.backupConfigurations.backups(backupConfiguration.uuid), fetcher: (page, search) => getBackupConfigurationBackups(backupConfiguration.uuid, page, search), - setStoreData: setBackupConfigurationBackups, }); + const backupConfigurationBackups = data ?? getEmptyPaginationSet>(); + return ( diff --git a/frontend/src/pages/admin/backupConfigurations/locations/AdminBackupConfigurationLocations.tsx b/frontend/src/pages/admin/backupConfigurations/locations/AdminBackupConfigurationLocations.tsx index ecf4f2a9..85c4c8d7 100644 --- a/frontend/src/pages/admin/backupConfigurations/locations/AdminBackupConfigurationLocations.tsx +++ b/frontend/src/pages/admin/backupConfigurations/locations/AdminBackupConfigurationLocations.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import { z } from 'zod'; import getBackupConfigurationLocations from '@/api/admin/backup-configurations/locations/getBackupConfigurationLocations.ts'; import { getEmptyPaginationSet } from '@/api/axios.ts'; @@ -6,7 +5,6 @@ import AdminSubContentContainer from '@/elements/containers/AdminSubContentConta import Table from '@/elements/Table.tsx'; import { queryKeys } from '@/lib/queryKeys.ts'; import { adminBackupConfigurationSchema } from '@/lib/schemas/admin/backupConfigurations.ts'; -import { adminLocationSchema } from '@/lib/schemas/admin/locations.ts'; import { locationTableColumns } from '@/lib/tableColumns.ts'; import LocationRow from '@/pages/admin/locations/LocationRow.tsx'; import { useSearchablePaginatedTable } from '@/plugins/useSearchablePageableTable.ts'; @@ -16,16 +14,13 @@ export default function AdminBackupConfigurationLocations({ }: { backupConfiguration: z.infer; }) { - const [backupConfigurationLocations, setBackupConfigurationLocations] = useState< - Pagination> - >(getEmptyPaginationSet()); - - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.backupConfigurations.locations(backupConfiguration.uuid), fetcher: (page, search) => getBackupConfigurationLocations(backupConfiguration.uuid, page, search), - setStoreData: setBackupConfigurationLocations, }); + const backupConfigurationLocations = (data ?? getEmptyPaginationSet()) as NonNullable; + return (
; }) { - const [backupConfigurationNodes, setBackupConfigurationNodes] = useState>>( - getEmptyPaginationSet(), - ); - - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.backupConfigurations.nodes(backupConfiguration.uuid), fetcher: (page, search) => getBackupConfigurationNodes(backupConfiguration.uuid, page, search), - setStoreData: setBackupConfigurationNodes, }); + const backupConfigurationNodes = data ?? getEmptyPaginationSet>(); + return (
diff --git a/frontend/src/pages/admin/backupConfigurations/servers/AdminBackupConfigurationServers.tsx b/frontend/src/pages/admin/backupConfigurations/servers/AdminBackupConfigurationServers.tsx index 5cb789ad..c7dcefca 100644 --- a/frontend/src/pages/admin/backupConfigurations/servers/AdminBackupConfigurationServers.tsx +++ b/frontend/src/pages/admin/backupConfigurations/servers/AdminBackupConfigurationServers.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import { z } from 'zod'; import getBackupConfigurationServers from '@/api/admin/backup-configurations/servers/getBackupConfigurationServers.ts'; import { getEmptyPaginationSet } from '@/api/axios.ts'; @@ -16,16 +15,13 @@ export default function AdminBackupConfigurationServers({ }: { backupConfiguration: z.infer; }) { - const [backupConfigurationServers, setBackupConfigurationServers] = useState< - Pagination> - >(getEmptyPaginationSet()); - - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.backupConfigurations.servers(backupConfiguration.uuid), fetcher: (page, search) => getBackupConfigurationServers(backupConfiguration.uuid, page, search), - setStoreData: setBackupConfigurationServers, }); + const backupConfigurationServers = data ?? getEmptyPaginationSet>(); + return (
; + return ( ; }) { - const [databaseHostDatabases, setDatabaseHostDatabases] = useState< - Pagination> - >(getEmptyPaginationSet()); - - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.databaseHosts.databases(databaseHost.uuid), fetcher: (page, search) => getDatabaseHostDatabases(databaseHost.uuid, page, search), - setStoreData: setDatabaseHostDatabases, }); + const databaseHostDatabases = data ?? getEmptyPaginationSet>(); + return (
; + return ( ; + return ( ; }) { - const [eggRepositoryEggs, setEggRepositoryEggs] = useState( - getEmptyPaginationSet>(), - ); const [selectedEggs, setSelectedEggs] = useState( new ObjectSet, 'uuid'>('uuid'), ); @@ -57,12 +54,13 @@ export default function EggRepositoryEggs({ [], ); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.eggRepositories.eggs(contextEggRepository.uuid), fetcher: (page, search) => getEggRepositoryEggs(contextEggRepository.uuid, page, search), - setStoreData: setEggRepositoryEggs, }); + const eggRepositoryEggs = (data ?? getEmptyPaginationSet()) as NonNullable; + useKeyboardShortcuts({ shortcuts: [ { diff --git a/frontend/src/pages/admin/home/health/AdminOverviewHealth.tsx b/frontend/src/pages/admin/home/health/AdminOverviewHealth.tsx index 79b052ad..c7ae8a2c 100644 --- a/frontend/src/pages/admin/home/health/AdminOverviewHealth.tsx +++ b/frontend/src/pages/admin/home/health/AdminOverviewHealth.tsx @@ -10,7 +10,7 @@ import { Title } from '@mantine/core'; import { useEffect, useState } from 'react'; import getGeneralHealth from '@/api/admin/system/health/getGeneralHealth.ts'; import getNodesHealth from '@/api/admin/system/health/getNodesHealth.ts'; -import { httpErrorToHuman } from '@/api/axios.ts'; +import { getEmptyPaginationSet, httpErrorToHuman } from '@/api/axios.ts'; import Card from '@/elements/Card.tsx'; import Code from '@/elements/Code.tsx'; import Spinner from '@/elements/Spinner.tsx'; @@ -26,15 +26,15 @@ export default function AdminOverviewHealth() { const { addToast } = useToast(); const [general, setGeneral] = useState> | null>(null); - const [nodes, setNodes] = useState> | null>(null); - const { loading, setPage } = useSearchablePaginatedTable({ + const { data, loading, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.health.nodes(), fetcher: (page) => getNodesHealth(page), - setStoreData: setNodes, paginationKey: 'desyncNodes', }); + const nodes = (data ?? getEmptyPaginationSet()) as NonNullable; + useEffect(() => { getGeneralHealth() .then(setGeneral) diff --git a/frontend/src/pages/admin/home/updates/AdminOverviewUpdates.tsx b/frontend/src/pages/admin/home/updates/AdminOverviewUpdates.tsx index ce30b771..0a904595 100644 --- a/frontend/src/pages/admin/home/updates/AdminOverviewUpdates.tsx +++ b/frontend/src/pages/admin/home/updates/AdminOverviewUpdates.tsx @@ -13,7 +13,7 @@ import { z } from 'zod'; import getNodeUpdates from '@/api/admin/system/updates/getNodeUpdates.ts'; import getUpdateHistory from '@/api/admin/system/updates/getUpdateHistory.ts'; import recheckUpdates from '@/api/admin/system/updates/recheckUpdates.ts'; -import { httpErrorToHuman } from '@/api/axios.ts'; +import { getEmptyPaginationSet, httpErrorToHuman } from '@/api/axios.ts'; import Alert from '@/elements/Alert.tsx'; import Anchor from '@/elements/Anchor.tsx'; import Button from '@/elements/Button.tsx'; @@ -39,15 +39,13 @@ export default function AdminOverviewUpdates() { const { addToast } = useToast(); const { updateInformation, setUpdateInformation } = useAdminStore(); - const [nodes, setNodes] = useState> | null>(null); const [updateHistory, setUpdateHistory] = useState> | null>(null); const [selectedUpdateHistory, setSelectedUpdateHistory] = useState(null); const [recheckLoading, setRecheckLoading] = useState(false); - const { loading, setPage, refetch } = useSearchablePaginatedTable({ + const { data, loading, setPage, refetch } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.updates.nodes(), fetcher: (page) => getNodeUpdates(page), - setStoreData: setNodes, paginationKey: 'outdatedNodes', }); @@ -59,6 +57,8 @@ export default function AdminOverviewUpdates() { }); }, []); + const nodes = (data ?? getEmptyPaginationSet()) as NonNullable; + const extensionUpdates = useMemo( () => Object.entries(updateInformation?.extensions || {}).filter( diff --git a/frontend/src/pages/admin/locations/AdminLocations.tsx b/frontend/src/pages/admin/locations/AdminLocations.tsx index 985b9a12..2089201a 100644 --- a/frontend/src/pages/admin/locations/AdminLocations.tsx +++ b/frontend/src/pages/admin/locations/AdminLocations.tsx @@ -2,6 +2,7 @@ import { faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Route, Routes, useNavigate } from 'react-router'; import getLocations from '@/api/admin/locations/getLocations.ts'; +import { getEmptyPaginationSet } from '@/api/axios.ts'; import Button from '@/elements/Button.tsx'; import { AdminCan } from '@/elements/Can.tsx'; import AdminContentContainer from '@/elements/containers/AdminContentContainer.tsx'; @@ -10,21 +11,20 @@ import { queryKeys } from '@/lib/queryKeys.ts'; import { locationTableColumns } from '@/lib/tableColumns.ts'; import { useSearchablePaginatedTable } from '@/plugins/useSearchablePageableTable.ts'; import AdminPermissionGuard from '@/routers/guards/AdminPermissionGuard.tsx'; -import { useAdminStore } from '@/stores/admin.tsx'; import LocationCreateOrUpdate from './LocationCreateOrUpdate.tsx'; import LocationRow from './LocationRow.tsx'; import LocationView from './LocationView.tsx'; function LocationsContainer() { const navigate = useNavigate(); - const { locations, setLocations } = useAdminStore(); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.locations.all(), fetcher: getLocations, - setStoreData: setLocations, }); + const locations = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( }) { - const { locationDatabaseHosts, setLocationDatabaseHosts } = useAdminStore(); - const [openModal, setOpenModal] = useState<'create' | null>(null); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.locations.databaseHosts(location.uuid), fetcher: (page, search) => getLocationDatabaseHosts(location.uuid, page, search), - setStoreData: setLocationDatabaseHosts, }); + const locationDatabaseHosts = data ?? getEmptyPaginationSet>(); + return ( }) { - const [locationNodes, setLocationNodes] = useState>>( - getEmptyPaginationSet(), - ); - - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.locations.nodes(location.uuid), fetcher: (page, search) => getLocationNodes(location.uuid, page, search), - setStoreData: setLocationNodes, }); + const locationNodes = data ?? getEmptyPaginationSet>(); + return (
diff --git a/frontend/src/pages/admin/mounts/AdminMounts.tsx b/frontend/src/pages/admin/mounts/AdminMounts.tsx index b089a4fa..362810bc 100644 --- a/frontend/src/pages/admin/mounts/AdminMounts.tsx +++ b/frontend/src/pages/admin/mounts/AdminMounts.tsx @@ -2,6 +2,7 @@ import { faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Route, Routes, useNavigate } from 'react-router'; import getMounts from '@/api/admin/mounts/getMounts.ts'; +import { getEmptyPaginationSet } from '@/api/axios.ts'; import Button from '@/elements/Button.tsx'; import { AdminCan } from '@/elements/Can.tsx'; import AdminContentContainer from '@/elements/containers/AdminContentContainer.tsx'; @@ -11,20 +12,19 @@ import { mountTableColumns } from '@/lib/tableColumns.ts'; import MountView from '@/pages/admin/mounts/MountView.tsx'; import { useSearchablePaginatedTable } from '@/plugins/useSearchablePageableTable.ts'; import AdminPermissionGuard from '@/routers/guards/AdminPermissionGuard.tsx'; -import { useAdminStore } from '@/stores/admin.tsx'; import MountCreateOrUpdate from './MountCreateOrUpdate.tsx'; import MountRow from './MountRow.tsx'; function MountsContainer() { const navigate = useNavigate(); - const { mounts, setMounts } = useAdminStore(); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.mounts.all(), fetcher: getMounts, - setStoreData: setMounts, }); + const mounts = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( }) { - const [mountNestEggs, setMountNestEggs] = useState< - Pagination; nestEgg: z.infer }>> - >(getEmptyPaginationSet()); const [openModal, setOpenModal] = useState<'add' | null>(null); - const { loading, search, setSearch, setPage, refetch } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage, refetch } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.mounts.eggs(mount.uuid), fetcher: (page, search) => getMountNestEggs(mount.uuid, page, search), - setStoreData: setMountNestEggs, }); + const mountNestEggs = + data ?? + getEmptyPaginationSet< + AndCreated<{ nest: z.infer; nestEgg: z.infer }> + >(); + return ( }) { - const [mountNodes, setMountNodes] = useState }>>>( - getEmptyPaginationSet(), - ); const [openModal, setOpenModal] = useState<'add' | null>(null); - const { loading, search, setSearch, setPage, refetch } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage, refetch } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.mounts.nodes(mount.uuid), fetcher: (page, search) => getMountNodes(mount.uuid, page, search), - setStoreData: setMountNodes, }); + const mountNodes = data ?? getEmptyPaginationSet }>>(); + return ( }) { - const [mountServers, setMountServers] = useState< - Pagination }>> - >(getEmptyPaginationSet()); - - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.mounts.servers(mount.uuid), fetcher: (page, search) => getMountServers(mount.uuid, page, search), - setStoreData: setMountServers, }); + const mountServers = data ?? getEmptyPaginationSet }>>(); + return (
diff --git a/frontend/src/pages/admin/nests/AdminNests.tsx b/frontend/src/pages/admin/nests/AdminNests.tsx index d131b126..52ca573d 100644 --- a/frontend/src/pages/admin/nests/AdminNests.tsx +++ b/frontend/src/pages/admin/nests/AdminNests.tsx @@ -2,6 +2,7 @@ import { faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Route, Routes, useNavigate } from 'react-router'; import getNests from '@/api/admin/nests/getNests.ts'; +import { getEmptyPaginationSet } from '@/api/axios.ts'; import Button from '@/elements/Button.tsx'; import { AdminCan } from '@/elements/Can.tsx'; import AdminContentContainer from '@/elements/containers/AdminContentContainer.tsx'; @@ -10,21 +11,20 @@ import { queryKeys } from '@/lib/queryKeys.ts'; import { nestTableColumns } from '@/lib/tableColumns.ts'; import { useSearchablePaginatedTable } from '@/plugins/useSearchablePageableTable.ts'; import AdminPermissionGuard from '@/routers/guards/AdminPermissionGuard.tsx'; -import { useAdminStore } from '@/stores/admin.tsx'; import NestCreateOrUpdate from './NestCreateOrUpdate.tsx'; import NestRow from './NestRow.tsx'; import NestView from './NestView.tsx'; function NestsContainer() { const navigate = useNavigate(); - const { nests, setNests } = useAdminStore(); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.nests.all(), fetcher: getNests, - setStoreData: setNests, }); + const nests = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( }) { const navigate = useNavigate(); const { addToast } = useToast(); - const { eggs, setEggs, addEgg } = useAdminStore(); const selectedEggsPreviousRef = useRef[]>([]); const fileInputRef = useRef(null); const [selectedEggs, setSelectedEggs] = useState(new ObjectSet, 'uuid'>('uuid')); - const { loading, refetch, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, refetch, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.nests.eggs(contextNest.uuid), fetcher: (page, search) => getEggs(contextNest.uuid, page, search), - setStoreData: setEggs, }); + const eggs = (data ?? getEmptyPaginationSet()) as NonNullable; + const handleImport = async (file: File) => { const text = await file.text().then((t) => t.trim()); let data: object; @@ -61,7 +60,7 @@ function EggsContainer({ contextNest }: { contextNest: z.infer { - addEgg(data); + refetch(); addToast('Egg imported.', 'success'); }) .catch((msg) => { diff --git a/frontend/src/pages/admin/nests/eggs/mounts/AdminEggMounts.tsx b/frontend/src/pages/admin/nests/eggs/mounts/AdminEggMounts.tsx index 18ddd40f..d2b76392 100644 --- a/frontend/src/pages/admin/nests/eggs/mounts/AdminEggMounts.tsx +++ b/frontend/src/pages/admin/nests/eggs/mounts/AdminEggMounts.tsx @@ -3,6 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useState } from 'react'; import { z } from 'zod'; import getEggMounts from '@/api/admin/nests/eggs/mounts/getEggMounts.ts'; +import { getEmptyPaginationSet } from '@/api/axios.ts'; import Button from '@/elements/Button.tsx'; import { ContextMenuProvider } from '@/elements/ContextMenu.tsx'; import AdminSubContentContainer from '@/elements/containers/AdminSubContentContainer.tsx'; @@ -11,7 +12,6 @@ import { queryKeys } from '@/lib/queryKeys.ts'; import { adminEggSchema } from '@/lib/schemas/admin/eggs.ts'; import { adminNestSchema } from '@/lib/schemas/admin/nests.ts'; import { useSearchablePaginatedTable } from '@/plugins/useSearchablePageableTable.ts'; -import { useAdminStore } from '@/stores/admin.tsx'; import EggMountRow from './EggMountRow.tsx'; import EggMountAddModal from './modals/EggMountAddModal.tsx'; @@ -22,16 +22,15 @@ export default function AdminEggMounts({ contextNest: z.infer; contextEgg: z.infer; }) { - const { eggMounts, setEggMounts } = useAdminStore(); - const [openModal, setOpenModal] = useState<'add' | null>(null); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.eggs.mounts(contextEgg.uuid), fetcher: (page, search) => getEggMounts(contextNest.uuid, contextEgg.uuid, page, search), - setStoreData: setEggMounts, }); + const eggMounts = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( ; contextEgg: z.infer; }) { - const [eggServers, setEggServers] = useState>>(getEmptyPaginationSet()); - - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.eggs.servers(contextEgg.uuid), fetcher: (page, search) => getEggServers(contextNest.uuid, contextEgg.uuid, page, search), - setStoreData: setEggServers, }); + const eggServers = data ?? getEmptyPaginationSet>(); + return (
diff --git a/frontend/src/pages/admin/nodes/AdminNodes.tsx b/frontend/src/pages/admin/nodes/AdminNodes.tsx index dc771a88..29bbe52a 100644 --- a/frontend/src/pages/admin/nodes/AdminNodes.tsx +++ b/frontend/src/pages/admin/nodes/AdminNodes.tsx @@ -5,6 +5,7 @@ import { Route, Routes, useNavigate } from 'react-router'; import { z } from 'zod'; import getLocations from '@/api/admin/locations/getLocations.ts'; import getNodes from '@/api/admin/nodes/getNodes.ts'; +import { getEmptyPaginationSet } from '@/api/axios.ts'; import Button from '@/elements/Button.tsx'; import { AdminCan } from '@/elements/Can.tsx'; import AdminContentContainer from '@/elements/containers/AdminContentContainer.tsx'; @@ -17,7 +18,6 @@ import { nodeTableColumns } from '@/lib/tableColumns.ts'; import { useKeyboardShortcuts } from '@/plugins/useKeyboardShortcuts.ts'; import { useSearchablePaginatedTable } from '@/plugins/useSearchablePageableTable.ts'; import AdminPermissionGuard from '@/routers/guards/AdminPermissionGuard.tsx'; -import { useAdminStore } from '@/stores/admin.tsx'; import LocationCreateOrUpdateModal from './LocationCreateOrUpdateModal.tsx'; import NodeActionBar from './NodeActionBar.tsx'; import NodeCreateOrUpdate from './NodeCreateOrUpdate.tsx'; @@ -26,18 +26,18 @@ import NodeView from './NodeView.tsx'; function NodesContainer() { const navigate = useNavigate(); - const { nodes, setNodes } = useAdminStore(); const [showLocationModal, setShowLocationModal] = useState(false); const [checkingLocations, setCheckingLocations] = useState(true); const [selectedNodes, setSelectedNodes] = useState(new ObjectSet, 'uuid'>('uuid')); const selectedNodesPreviousRef = useRef[]>([]); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.nodes.all(), fetcher: getNodes, - setStoreData: setNodes, }); + const nodes = (data ?? getEmptyPaginationSet()) as NonNullable; + useEffect(() => { setSelectedNodes(new ObjectSet('uuid')); }, []); diff --git a/frontend/src/pages/admin/nodes/allocations/AdminNodeAllocations.tsx b/frontend/src/pages/admin/nodes/allocations/AdminNodeAllocations.tsx index d7ad11c4..2de7480b 100644 --- a/frontend/src/pages/admin/nodes/allocations/AdminNodeAllocations.tsx +++ b/frontend/src/pages/admin/nodes/allocations/AdminNodeAllocations.tsx @@ -4,6 +4,7 @@ import { Group } from '@mantine/core'; import { MouseEvent as ReactMouseEvent, Ref, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { z } from 'zod'; import getNodeAllocations from '@/api/admin/nodes/allocations/getNodeAllocations.ts'; +import { getEmptyPaginationSet } from '@/api/axios.ts'; import ActionIcon from '@/elements/ActionIcon.tsx'; import Button from '@/elements/Button.tsx'; import AdminSubContentContainer from '@/elements/containers/AdminSubContentContainer.tsx'; @@ -22,13 +23,24 @@ import NodeAllocationsCreateModal from './modals/NodeAllocationsCreateModal.tsx' import NodeAllocationRow from './NodeAllocationRow.tsx'; export default function AdminNodeAllocations({ node }: { node: z.infer }) { - const { nodeAllocations, setNodeAllocations, selectedNodeAllocations, setSelectedNodeAllocations } = useAdminStore(); + const { selectedNodeAllocations, setSelectedNodeAllocations } = useAdminStore(); const [openModal, setOpenModal] = useState<'create' | null>(null); const [ipFilter, setIpFilter] = useState(''); const [portFilter, setPortFilter] = useState(''); const selectedNodeAllocationsPreviousRef = useRef(selectedNodeAllocations.values()); + const { data, loading, search, setSearch, setPage, refetch } = useSearchablePaginatedTable({ + queryKey: queryKeys.admin.nodes.allocations(node.uuid), + fetcher: (page, generalSearch) => { + const finalSearch = buildSearch(generalSearch || '', ipFilter, portFilter); + return getNodeAllocations(node.uuid, page, finalSearch); + }, + deps: [ipFilter, portFilter, node.uuid], + }); + + const nodeAllocations = (data ?? getEmptyPaginationSet()) as NonNullable; + const uniqueIps = useMemo(() => { const ips = new Set(); nodeAllocations.data.forEach((alloc) => ips.add(alloc.ip)); @@ -53,16 +65,6 @@ export default function AdminNodeAllocations({ node }: { node: z.infer 0 ? parts.join(':') : undefined; }, []); - const { loading, search, setSearch, setPage, refetch } = useSearchablePaginatedTable({ - queryKey: queryKeys.admin.nodes.allocations(node.uuid), - fetcher: (page, generalSearch) => { - const finalSearch = buildSearch(generalSearch || '', ipFilter, portFilter); - return getNodeAllocations(node.uuid, page, finalSearch); - }, - setStoreData: setNodeAllocations, - deps: [ipFilter, portFilter, node.uuid], - }); - useEffect(() => { refetch(); }, [ipFilter, portFilter]); diff --git a/frontend/src/pages/admin/nodes/backups/AdminNodeBackups.tsx b/frontend/src/pages/admin/nodes/backups/AdminNodeBackups.tsx index b955e0d6..dd86e30d 100644 --- a/frontend/src/pages/admin/nodes/backups/AdminNodeBackups.tsx +++ b/frontend/src/pages/admin/nodes/backups/AdminNodeBackups.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { z } from 'zod'; import getNodeBackups from '@/api/admin/nodes/backups/getNodeBackups.ts'; +import { getEmptyPaginationSet } from '@/api/axios.ts'; import { ContextMenuProvider } from '@/elements/ContextMenu.tsx'; import AdminSubContentContainer from '@/elements/containers/AdminSubContentContainer.tsx'; import Switch from '@/elements/input/Switch.tsx'; @@ -8,21 +9,19 @@ import Table from '@/elements/Table.tsx'; import { queryKeys } from '@/lib/queryKeys.ts'; import { adminNodeSchema } from '@/lib/schemas/admin/nodes.ts'; import { useSearchablePaginatedTable } from '@/plugins/useSearchablePageableTable.ts'; -import { useAdminStore } from '@/stores/admin.tsx'; import NodeBackupRow from './NodeBackupRow.tsx'; export default function AdminNodeBackups({ node }: { node: z.infer }) { - const { nodeBackups, setNodeBackups } = useAdminStore(); - const [showDetachedNodeBackups, setShowDetachedNodeBackups] = useState(false); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.nodes.backups(node.uuid), fetcher: (page, search) => getNodeBackups(node.uuid, page, search, showDetachedNodeBackups), - setStoreData: setNodeBackups, deps: [showDetachedNodeBackups], }); + const nodeBackups = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( }) { - const { nodeMounts, setNodeMounts } = useAdminStore(); - const [openModal, setOpenModal] = useState<'add' | null>(null); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.nodes.mounts(node.uuid), fetcher: (page, search) => getNodeMounts(node.uuid, page, search), - setStoreData: setNodeMounts, }); + const nodeMounts = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( }) { const { t, tItem } = useTranslations(); const { addToast } = useToast(); - const [nodeServers, setNodeServers] = useState>>( - getEmptyPaginationSet(), - ); const [selectedServers, setSelectedServers] = useState( new ObjectSet, 'uuid'>('uuid'), ); @@ -37,12 +34,13 @@ export default function AdminNodeServers({ node }: { node: z.infer | null>(null); const [openModal, setOpenModal] = useState<'transfer' | null>(null); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.nodes.servers(node.uuid), fetcher: (page, search) => getNodeServers(node.uuid, page, search), - setStoreData: setNodeServers, }); + const nodeServers = (data ?? getEmptyPaginationSet()) as NonNullable; + const onSelectedStart = useCallback( (event: React.MouseEvent | MouseEvent) => { selectedServersPreviousRef.current = event.shiftKey ? selectedServers.values() : []; diff --git a/frontend/src/pages/admin/nodes/transfers/AdminNodeTransfers.tsx b/frontend/src/pages/admin/nodes/transfers/AdminNodeTransfers.tsx index d0030793..f187c82a 100644 --- a/frontend/src/pages/admin/nodes/transfers/AdminNodeTransfers.tsx +++ b/frontend/src/pages/admin/nodes/transfers/AdminNodeTransfers.tsx @@ -1,4 +1,4 @@ -import { Ref, useEffect, useState } from 'react'; +import { Ref, useEffect } from 'react'; import { z } from 'zod'; import getNodeTransferringServers from '@/api/admin/nodes/servers/getNodeTransferringServers.ts'; import { getEmptyPaginationSet } from '@/api/axios.ts'; @@ -6,27 +6,21 @@ import AdminSubContentContainer from '@/elements/containers/AdminSubContentConta import SelectionArea from '@/elements/SelectionArea.tsx'; import Table from '@/elements/Table.tsx'; import { queryKeys } from '@/lib/queryKeys.ts'; -import { adminNodeSchema, adminNodeTransferProgressSchema } from '@/lib/schemas/admin/nodes.ts'; +import { adminNodeSchema } from '@/lib/schemas/admin/nodes.ts'; import { adminServerSchema } from '@/lib/schemas/admin/servers.ts'; import { useSearchablePaginatedTable } from '@/plugins/useSearchablePageableTable.ts'; import ServerRow from './ServerRow.tsx'; export default function AdminNodeTransfers({ node }: { node: z.infer }) { - const [nodeTransferringServers, setNodeTransferringServers] = useState<{ - servers: Pagination>; - transfers: Record>; - }>({ - servers: getEmptyPaginationSet(), - transfers: {}, - }); - - const { loading, search, setSearch, setPage, refetch } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage, refetch } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.nodes.transfers(node.uuid), fetcher: (page, search) => getNodeTransferringServers(node.uuid, page, search), - setStoreData: setNodeTransferringServers, paginationKey: 'servers', }); + const servers = data?.servers ?? getEmptyPaginationSet>(); + const transfers = data?.transfers ?? {}; + useEffect(() => { const interval = setInterval(() => { refetch(); @@ -41,17 +35,17 @@ export default function AdminNodeTransfers({ node }: { node: z.infer - {nodeTransferringServers.servers.data.map((server) => ( + {servers.data.map((server) => ( {(innerRef: Ref) => ( } /> )} diff --git a/frontend/src/pages/admin/oAuthProviders/AdminOAuthProviders.tsx b/frontend/src/pages/admin/oAuthProviders/AdminOAuthProviders.tsx index 6dd9cf39..2028f09a 100644 --- a/frontend/src/pages/admin/oAuthProviders/AdminOAuthProviders.tsx +++ b/frontend/src/pages/admin/oAuthProviders/AdminOAuthProviders.tsx @@ -6,7 +6,7 @@ import { Route, Routes, useNavigate } from 'react-router'; import { z } from 'zod'; import createOAuthProvider from '@/api/admin/oauth-providers/createOAuthProvider.ts'; import getOAuthProviders from '@/api/admin/oauth-providers/getOAuthProviders.ts'; -import { httpErrorToHuman } from '@/api/axios.ts'; +import { getEmptyPaginationSet, httpErrorToHuman } from '@/api/axios.ts'; import Button from '@/elements/Button.tsx'; import { AdminCan } from '@/elements/Can.tsx'; import AdminContentContainer from '@/elements/containers/AdminContentContainer.tsx'; @@ -19,7 +19,6 @@ import { useImportDragAndDrop } from '@/plugins/useImportDragAndDrop.ts'; import { useSearchablePaginatedTable } from '@/plugins/useSearchablePageableTable.ts'; import { useToast } from '@/providers/ToastProvider.tsx'; import AdminPermissionGuard from '@/routers/guards/AdminPermissionGuard.tsx'; -import { useAdminStore } from '@/stores/admin.tsx'; import OAuthProviderCreateOrUpdate from './OAuthProviderCreateOrUpdate.tsx'; import OAuthProviderImportOverlay from './OAuthProviderImportOverlay.tsx'; import OAuthProviderRow from './OAuthProviderRow.tsx'; @@ -28,16 +27,16 @@ import OAuthProviderView from './OAuthProviderView.tsx'; function OAuthProvidersContainer() { const navigate = useNavigate(); const { addToast } = useToast(); - const { oauthProviders, addOAuthProvider, setOAuthProviders } = useAdminStore(); const fileInputRef = useRef(null); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage, refetch } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.oAuthProviders.all(), fetcher: getOAuthProviders, - setStoreData: setOAuthProviders, }); + const oauthProviders = (data ?? getEmptyPaginationSet()) as NonNullable; + const handleImport = async (file: File) => { const text = await file.text().then((t) => t.trim()); let data: object; @@ -57,8 +56,8 @@ function OAuthProvidersContainer() { clientId: 'example', clientSecret: 'example', }) - .then((data) => { - addOAuthProvider(data); + .then(() => { + refetch(); addToast('OAuth Provider imported.', 'success'); }) .catch((msg) => { diff --git a/frontend/src/pages/admin/oAuthProviders/users/AdminOAuthProviderUsers.tsx b/frontend/src/pages/admin/oAuthProviders/users/AdminOAuthProviderUsers.tsx index 2d48136e..f5e3ec2c 100644 --- a/frontend/src/pages/admin/oAuthProviders/users/AdminOAuthProviderUsers.tsx +++ b/frontend/src/pages/admin/oAuthProviders/users/AdminOAuthProviderUsers.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import { z } from 'zod'; import getOAuthProviderUsers from '@/api/admin/oauth-providers/users/getOAuthProviderUsers.ts'; import { getEmptyPaginationSet } from '@/api/axios.ts'; @@ -15,16 +14,13 @@ export default function AdminOAuthProviderUsers({ }: { oauthProvider: z.infer; }) { - const [oauthProviderUsers, setOAuthProviderUsers] = useState>>( - getEmptyPaginationSet(), - ); - - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.oAuthProviders.users(oauthProvider.uuid), fetcher: (page, search) => getOAuthProviderUsers(oauthProvider.uuid, page, search), - setStoreData: setOAuthProviderUsers, }); + const oauthProviderUsers = data ?? getEmptyPaginationSet>(); + return (
; + return ( }) { - const [roleUsers, setRoleUsers] = useState>>(getEmptyPaginationSet()); - - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.roles.users(role.uuid), fetcher: (page, search) => getRoleUsers(role.uuid, page, search), - setStoreData: setRoleUsers, }); + const roleUsers = data ?? getEmptyPaginationSet>(); + return (
diff --git a/frontend/src/pages/admin/servers/AdminServers.tsx b/frontend/src/pages/admin/servers/AdminServers.tsx index 6adb0f26..a163b5b9 100644 --- a/frontend/src/pages/admin/servers/AdminServers.tsx +++ b/frontend/src/pages/admin/servers/AdminServers.tsx @@ -2,6 +2,7 @@ import { faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Route, Routes, useNavigate } from 'react-router'; import getServers from '@/api/admin/servers/getServers.ts'; +import { getEmptyPaginationSet } from '@/api/axios.ts'; import Button from '@/elements/Button.tsx'; import { AdminCan } from '@/elements/Can.tsx'; import AdminContentContainer from '@/elements/containers/AdminContentContainer.tsx'; @@ -10,21 +11,20 @@ import { queryKeys } from '@/lib/queryKeys.ts'; import { serverTableColumns } from '@/lib/tableColumns.ts'; import { useSearchablePaginatedTable } from '@/plugins/useSearchablePageableTable.ts'; import AdminPermissionGuard from '@/routers/guards/AdminPermissionGuard.tsx'; -import { useAdminStore } from '@/stores/admin.tsx'; import ServerCreate from './ServerCreate.tsx'; import ServerRow from './ServerRow.tsx'; import ServerView from './ServerView.tsx'; function ServersContainer() { const navigate = useNavigate(); - const { servers, setServers } = useAdminStore(); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.servers.all(), fetcher: getServers, - setStoreData: setServers, }); + const servers = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( }) { - const { serverAllocations, setServerAllocations } = useAdminStore(); - const [openModal, setOpenModal] = useState<'add' | null>(null); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.servers.allocations(server.uuid), fetcher: (page, search) => getServerAllocations(server.uuid, page, search), - setStoreData: setServerAllocations, }); + const serverAllocations = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( }) { - const [serverBackups, setServerBackups] = useState>>( - getEmptyPaginationSet(), - ); const [showPartiallyDetachedServerBackups, setShowPartiallyDetachedServerBackups] = useState(false); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.servers.backups(server.uuid), fetcher: (page, search) => getServerBackups(server.uuid, page, search, showPartiallyDetachedServerBackups), - setStoreData: setServerBackups, deps: [showPartiallyDetachedServerBackups], }); + const serverBackups = data ?? getEmptyPaginationSet>(); + return ( }) { - const { serverMounts, setServerMounts } = useAdminStore(); - const [openModal, setOpenModal] = useState<'add' | null>(null); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.servers.mounts(server.uuid), fetcher: (page, search) => getServerMounts(server.uuid, page, search), - setStoreData: setServerMounts, }); + const serverMounts = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( ; + return ( }) { const { t } = useTranslations(); - const [userActivity, setUserActivity] = useState>>( - getEmptyPaginationSet(), - ); - - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.users.activity(user.uuid), fetcher: (page, search) => getUserActivity(user.uuid, page, search), - setStoreData: setUserActivity, }); + const userActivity = data ?? getEmptyPaginationSet>(); + return (
}) { - const { userOAuthLinks, setUserOAuthLinks } = useAdminStore(); - const [openModal, setOpenModal] = useState<'add' | null>(null); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.users.oauthLinks(user.uuid), fetcher: (page, search) => getUserOAuthLinks(user.uuid, page, search), - setStoreData: setUserOAuthLinks, }); + const userOAuthLinks = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( }) { const [showOwnedUserServers, setShowOwnedUserServers] = useState(false); - const [userServers, setUserServers] = useState>>( - getEmptyPaginationSet(), - ); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.admin.users.servers(user.uuid), fetcher: (page, search) => getUserServers(user.uuid, page, search, showOwnedUserServers), - setStoreData: setUserServers, deps: [showOwnedUserServers], }); + const userServers = data ?? getEmptyPaginationSet>(); + return ( >>(getEmptyPaginationSet()); - - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.user.activity.all(), fetcher: getUserActivity, - setStoreData: setActivities, }); + const activities = data ?? getEmptyPaginationSet>(); + return ( (null); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.user.apiKeys.all(), fetcher: getApiKeys, - setStoreData: setApiKeys, }); + const apiKeys = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( (null); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.user.commandSnippets.all(), fetcher: getCommandSnippets, - setStoreData: setCommandSnippets, }); + const commandSnippets = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( getServers(page, search, serverListShowOthers), - setStoreData: setServers, deps: [serverListShowOthers], }); + const servers = (data ?? getEmptyPaginationSet()) as NonNullable; + const handleServerSelectionChange = (server: z.infer, selected: boolean) => { setSelectedServers((prev) => { const newSet = new ObjectSet('uuid', prev.values()); diff --git a/frontend/src/pages/dashboard/home/ServerGroupItem.tsx b/frontend/src/pages/dashboard/home/ServerGroupItem.tsx index 856781ff..9ca2054b 100644 --- a/frontend/src/pages/dashboard/home/ServerGroupItem.tsx +++ b/frontend/src/pages/dashboard/home/ServerGroupItem.tsx @@ -11,8 +11,9 @@ import { } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Collapse, Menu, useComputedColorScheme } from '@mantine/core'; +import { useQueryClient } from '@tanstack/react-query'; import classNames from 'classnames'; -import { ComponentProps, memo, startTransition, useEffect, useState } from 'react'; +import { ComponentProps, memo, useEffect, useMemo, useState } from 'react'; import { z } from 'zod'; import { getEmptyPaginationSet, httpErrorToHuman } from '@/api/axios.ts'; import deleteServerGroup from '@/api/me/servers/groups/deleteServerGroup.ts'; @@ -66,23 +67,30 @@ export default function ServerGroupItem({ const { t, tItem } = useTranslations(); const { updateServerGroup: updateStateServerGroup, removeServerGroup } = useUserStore(); const { addToast } = useToast(); + const queryClient = useQueryClient(); const isDark = useComputedColorScheme('dark') === 'dark'; const [isExpanded, setIsExpanded] = useState( localStorage.getItem(`server-group-expanded-${serverGroup.uuid}`) !== 'false', ); - const [servers, setServers] = useState(getEmptyPaginationSet>()); + const [openModal, setOpenModal] = useState<'edit' | 'delete' | 'add-server' | null>(null); const { handleBulkPowerAction, bulkActionLoading: groupActionLoading } = useBulkPowerActions(); - const { loading, search, setSearch, setPage, refetch } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage, refetch } = useSearchablePaginatedTable({ queryKey: [...queryKeys.user.servers.all(), serverGroup.uuid], fetcher: (page, search) => getServerGroupServers(serverGroup.uuid, page, search), - setStoreData: setServers, modifyParams: false, }); + const servers = (data ?? getEmptyPaginationSet()) as NonNullable; + + const queryKey = useMemo( + () => [...queryKeys.user.servers.all(), serverGroup.uuid, servers.page, search], + [serverGroup.uuid, servers.page, search], + ); + useEffect(() => { localStorage.setItem(`server-group-expanded-${serverGroup.uuid}`, String(isExpanded)); }, [isExpanded, serverGroup.uuid]); @@ -109,6 +117,17 @@ export default function ServerGroupItem({ const serverCount = servers?.total ?? serverGroup.serverOrder.length; + const doOptimisticUpdate = (items: DndServer[], serverOrder: string[]) => { + queryClient.setQueryData(queryKey, (old) => { + if (!old) return old; + return { ...old, data: items }; + }); + + updateStateServerGroup(serverGroup.uuid, { + serverOrder, + }); + }; + return ( <> { + const startIndex = (servers.page - 1) * servers.perPage; + const serverOrder = insertItems( serverGroup.serverOrder, items.map((s) => s.uuid), - (servers.page - 1) * servers.perPage, + startIndex, ); - startTransition(() => { - setServers({ ...servers, data: items }); - }); + const previous = queryClient.getQueryData(queryKey); - await updateServerGroup(serverGroup.uuid, { serverOrder }).catch((err) => { - addToast(httpErrorToHuman(err), 'error'); + doOptimisticUpdate(items, serverOrder); + + try { + await updateServerGroup(serverGroup.uuid, { serverOrder }); + } catch (err) { + queryClient.setQueryData(queryKey, previous); updateStateServerGroup(serverGroup.uuid, { serverOrder: serverGroup.serverOrder, }); - setServers({ ...servers, data: servers.data }); - }); - }, - onError: (error) => { - console.error('Drag error:', error); + addToast(httpErrorToHuman(err), 'error'); + } }, }} - renderOverlay={(activeServer) => - activeServer ? ( + renderOverlay={(active) => + active ? (
- null} showSelection={false} /> +
) : null } @@ -302,16 +322,19 @@ export default function ServerGroupItem({ server={server} showSelection={false} onGroupRemove={() => { + const previous = queryClient.getQueryData(queryKey); + const serverOrder = serverGroup.serverOrder.filter( - (_, orderI) => (servers.page - 1) * servers.perPage + i !== orderI, + (_, idx) => (servers.page - 1) * servers.perPage + i !== idx, ); - updateStateServerGroup(serverGroup.uuid, { - serverOrder, - }); - setServers((prev) => ({ ...prev, data: prev.data.filter((_, dataI) => i !== dataI) })); - updateServerGroup(serverGroup.uuid, { serverOrder }).catch((msg) => { - addToast(httpErrorToHuman(msg), 'error'); + const newItems = items.filter((_, idx) => idx !== i); + + doOptimisticUpdate(newItems, serverOrder); + + updateServerGroup(serverGroup.uuid, { serverOrder }).catch((err) => { + queryClient.setQueryData(queryKey, previous); + addToast(httpErrorToHuman(err), 'error'); }); }} /> diff --git a/frontend/src/pages/dashboard/oauth-links/DashboardOAuthLinks.tsx b/frontend/src/pages/dashboard/oauth-links/DashboardOAuthLinks.tsx index 27deb07b..155ae518 100644 --- a/frontend/src/pages/dashboard/oauth-links/DashboardOAuthLinks.tsx +++ b/frontend/src/pages/dashboard/oauth-links/DashboardOAuthLinks.tsx @@ -3,6 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useEffect, useState } from 'react'; import { z } from 'zod'; import getOAuthProviders from '@/api/auth/getOAuthProviders.ts'; +import { getEmptyPaginationSet } from '@/api/axios.ts'; import getOAuthLinks from '@/api/me/oauth-links/getOAuthLinks.ts'; import Button from '@/elements/Button.tsx'; import ContextMenu, { ContextMenuProvider } from '@/elements/ContextMenu.tsx'; @@ -10,14 +11,13 @@ import AccountContentContainer from '@/elements/containers/AccountContentContain import Table from '@/elements/Table.tsx'; import { queryKeys } from '@/lib/queryKeys.ts'; import { oAuthProviderSchema } from '@/lib/schemas/generic.ts'; +import { userOAuthLinkSchema } from '@/lib/schemas/user/oAuth.ts'; import { useSearchablePaginatedTable } from '@/plugins/useSearchablePageableTable.ts'; import { useTranslations } from '@/providers/TranslationProvider.tsx'; -import { useUserStore } from '@/stores/user.ts'; import OAuthLinkRow from './OAuthLinkRow.tsx'; export default function DashboardOAuthLinks() { const { t } = useTranslations(); - const { oauthLinks, setOAuthLinks } = useUserStore(); const [oAuthProviders, setOAuthProviders] = useState[]>([]); useEffect(() => { @@ -26,12 +26,13 @@ export default function DashboardOAuthLinks() { }); }, []); - const { loading, setPage } = useSearchablePaginatedTable({ + const { data, loading, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.user.oauthLinks.all(), fetcher: getOAuthLinks, - setStoreData: setOAuthLinks, }); + const oauthLinks = data ?? getEmptyPaginationSet>(); + return ( (null); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.user.securityKeys.all(), fetcher: getSecurityKeys, - setStoreData: setSecurityKeys, }); + const securityKeys = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( ; + return ( (null); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.user.sshKeys.all(), fetcher: getSshKeys, - setStoreData: setSshKeys, }); + const sshKeys = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( >>( - getEmptyPaginationSet(), - ); const server = useServerStore((state) => state.server); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.server(server.uuid).activity.all(), fetcher: (page, search) => getServerActivity(server.uuid, page, search), - setStoreData: setActivities, }); + const activities = data ?? getEmptyPaginationSet>(); + return ( (null); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.server(server.uuid).backups.all(), fetcher: (page, search) => getBackups(server.uuid, page, search), - setStoreData: setBackups, }); + const backups = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( state.state); const socketInstance = useServerStore((state) => state.socketInstance); - const [activities, setActivities] = useState>>( - getEmptyPaginationSet(), - ); const [selectedCommand, setSelectedCommand] = useState(null); - const { loading, setPage } = useSearchablePaginatedTable({ + const { data, loading, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.server(server.uuid).activity.all(), fetcher: (page) => getServerActivity(server.uuid, page, 'server:console.command'), - setStoreData: setActivities, modifyParams: false, canRequest: useServerCan('activity.read'), deps: [server.uuid], }); + const activities = (data ?? getEmptyPaginationSet()) as NonNullable; + const handleRowClick = (activity: z.infer) => { const data = activity.data as { command?: string } | null; if (data?.command) { diff --git a/frontend/src/pages/server/databases/ServerDatabases.tsx b/frontend/src/pages/server/databases/ServerDatabases.tsx index 894d03c2..4577aa5c 100644 --- a/frontend/src/pages/server/databases/ServerDatabases.tsx +++ b/frontend/src/pages/server/databases/ServerDatabases.tsx @@ -1,6 +1,7 @@ import { faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useState } from 'react'; +import { getEmptyPaginationSet } from '@/api/axios.ts'; import getDatabases from '@/api/server/databases/getDatabases.ts'; import Button from '@/elements/Button.tsx'; import { ServerCan } from '@/elements/Can.tsx'; @@ -17,16 +18,17 @@ import DatabaseCreateModal from './modals/DatabaseCreateModal.tsx'; export default function ServerDatabases() { const { t } = useTranslations(); - const { server, databases, setDatabases } = useServerStore(); + const { server } = useServerStore(); const [openModal, setOpenModal] = useState<'create' | null>(null); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage } = useSearchablePaginatedTable({ queryKey: queryKeys.server(server.uuid).databases.all(), fetcher: (page, search) => getDatabases(server.uuid, page, search), - setStoreData: setDatabases, }); + const databases = (data ?? getEmptyPaginationSet()) as NonNullable; + return ( state.server); - const [mounts, setMounts] = useState>>(getEmptyPaginationSet()); - - const { loading } = useSearchablePaginatedTable({ + const { data, loading } = useSearchablePaginatedTable({ queryKey: queryKeys.server(server.uuid).mounts.all(), fetcher: () => getMounts(server.uuid), - setStoreData: setMounts, }); + const mounts = data ?? getEmptyPaginationSet>(); + return ( getAllocations(server.uuid, page, search), - setStoreData: setAllocations, }); + const allocations = (data ?? getEmptyPaginationSet()) as NonNullable; + const doAdd = () => { createAllocation(server.uuid) - .then((alloc) => { - addAllocation(alloc); + .then(() => { + refetch(); addToast(t('pages.server.network.toast.created', {}), 'success'); }) .catch((msg) => { diff --git a/frontend/src/pages/server/schedules/ServerSchedules.tsx b/frontend/src/pages/server/schedules/ServerSchedules.tsx index 8346760b..2224653f 100644 --- a/frontend/src/pages/server/schedules/ServerSchedules.tsx +++ b/frontend/src/pages/server/schedules/ServerSchedules.tsx @@ -2,7 +2,7 @@ import { faPlus, faUpload } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import jsYaml from 'js-yaml'; import { ChangeEvent, useRef, useState } from 'react'; -import { httpErrorToHuman } from '@/api/axios.ts'; +import { getEmptyPaginationSet, httpErrorToHuman } from '@/api/axios.ts'; import getSchedules from '@/api/server/schedules/getSchedules.ts'; import importSchedule from '@/api/server/schedules/importSchedule.ts'; import Button from '@/elements/Button.tsx'; @@ -24,18 +24,19 @@ import ScheduleRow from './ScheduleRow.tsx'; export default function ServerSchedules() { const { t } = useTranslations(); const { addToast } = useToast(); - const { server, schedules, setSchedules, addSchedule } = useServerStore(); + const { server } = useServerStore(); const [openModal, setOpenModal] = useState<'create' | null>(null); const fileInputRef = useRef(null); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage, refetch } = useSearchablePaginatedTable({ queryKey: queryKeys.server(server.uuid).schedules.all(), fetcher: (page, search) => getSchedules(server.uuid, page, search), - setStoreData: setSchedules, }); + const schedules = (data ?? getEmptyPaginationSet()) as NonNullable; + const handleImport = async (file: File) => { const text = await file.text().then((t) => t.trim()); let data: object; @@ -51,8 +52,8 @@ export default function ServerSchedules() { } importSchedule(server.uuid, data) - .then((data) => { - addSchedule(data); + .then(() => { + refetch(); addToast(t('pages.server.schedules.toast.imported', {}), 'success'); }) .catch((msg) => { diff --git a/frontend/src/pages/server/subusers/ServerSubusers.tsx b/frontend/src/pages/server/subusers/ServerSubusers.tsx index d925cafa..a1bac508 100644 --- a/frontend/src/pages/server/subusers/ServerSubusers.tsx +++ b/frontend/src/pages/server/subusers/ServerSubusers.tsx @@ -1,7 +1,7 @@ import { faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useEffect, useState } from 'react'; -import { httpErrorToHuman } from '@/api/axios.ts'; +import { getEmptyPaginationSet, httpErrorToHuman } from '@/api/axios.ts'; import getPermissions from '@/api/getPermissions.ts'; import createSubuser from '@/api/server/subusers/createSubuser.ts'; import getSubusers from '@/api/server/subusers/getSubusers.ts'; @@ -23,7 +23,7 @@ import SubuserRow from './SubuserRow.tsx'; export default function ServerSubusers() { const { t } = useTranslations(); const { addToast } = useToast(); - const { server, subusers, setSubusers, addSubuser } = useServerStore(); + const { server } = useServerStore(); const { settings, setAvailablePermissions } = useGlobalStore(); const [openModal, setOpenModal] = useState<'create' | null>(null); @@ -34,16 +34,17 @@ export default function ServerSubusers() { }); }, []); - const { loading, search, setSearch, setPage } = useSearchablePaginatedTable({ + const { data, loading, search, setSearch, setPage, refetch } = useSearchablePaginatedTable({ queryKey: queryKeys.server(server.uuid).subusers.all(), fetcher: (page, search) => getSubusers(server.uuid, page, search), - setStoreData: setSubusers, }); + const subusers = (data ?? getEmptyPaginationSet()) as NonNullable; + const doCreate = (email: string, permissions: string[], ignoredFiles: string[], captcha: string | null) => { createSubuser(server.uuid, { email, permissions, ignoredFiles, captcha }) - .then((subuser) => { - addSubuser(subuser); + .then(() => { + refetch(); addToast(t('pages.server.subusers.modal.createSubuser.toast.created', {}), 'success'); setOpenModal(null); }) diff --git a/frontend/src/plugins/useSearchablePageableTable.ts b/frontend/src/plugins/useSearchablePageableTable.ts index 99488ec8..79d729cd 100644 --- a/frontend/src/plugins/useSearchablePageableTable.ts +++ b/frontend/src/plugins/useSearchablePageableTable.ts @@ -1,6 +1,6 @@ import { keepPreviousData, useQuery } from '@tanstack/react-query'; import debounce from 'debounce'; -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'react-router'; import { httpErrorToHuman } from '@/api/axios.ts'; import { useToast } from '@/providers/ToastProvider.tsx'; @@ -8,7 +8,6 @@ import { useToast } from '@/providers/ToastProvider.tsx'; interface UseSearchablePaginatedTableOptions { queryKey: readonly unknown[]; fetcher: (page: number, search: string) => Promise; - setStoreData: (data: T) => void; paginationKey?: string; deps?: unknown[]; debounceMs?: number; @@ -28,7 +27,6 @@ function parseNumber(num: string | null): number | null { export function useSearchablePaginatedTable({ queryKey = [], fetcher, - setStoreData, paginationKey, deps = [], debounceMs = 150, @@ -49,10 +47,7 @@ export function useSearchablePaginatedTable({ } }, [modifyParams, page, search]); - const updateDebouncedSearch = useCallback( - debounce((s: string) => setDebouncedSearch(s), debounceMs), - [], - ); + const updateDebouncedSearch = useMemo(() => debounce((s: string) => setDebouncedSearch(s), debounceMs), [debounceMs]); useEffect(() => { if (!search) { @@ -63,7 +58,7 @@ export function useSearchablePaginatedTable({ } }, [search]); - const { data, isFetching, error, refetch } = useQuery({ + const { data, isLoading, error, refetch } = useQuery({ queryKey: [...queryKey, ...deps, { page, search: debouncedSearch }], queryFn: () => fetcher(page, debouncedSearch), placeholderData: keepPreviousData, @@ -101,16 +96,13 @@ export function useSearchablePaginatedTable({ setPage(1); } else if (page > totalPages && totalPages !== 0) { setPage(totalPages); - } else { - setStoreData(data); } - } else { - setStoreData(data); } }, [data]); return { - loading: isFetching, + data, + loading: isLoading, search, setSearch, page,