From e75ef8bc1cc7183afcde00c24601511c874d80cc Mon Sep 17 00:00:00 2001 From: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com> Date: Fri, 7 Mar 2025 12:28:07 -0600 Subject: [PATCH] Use server-side sort for host certificates (#26898) --- frontend/interfaces/certificates.ts | 7 ++++ frontend/interfaces/list_options.ts | 13 +++++++ .../details/DeviceUserPage/DeviceUserPage.tsx | 25 ++++++++++--- .../HostDetailsPage/HostDetailsPage.tsx | 34 ++++++++++++------ .../cards/Certificates/Certificates.tsx | 15 ++++++-- .../CertificatesTable/CertificatesTable.tsx | 35 +++++++++++++------ frontend/services/entities/device_user.ts | 26 ++++++++------ frontend/services/entities/hosts.ts | 27 ++++++++------ 8 files changed, 134 insertions(+), 48 deletions(-) create mode 100644 frontend/interfaces/list_options.ts diff --git a/frontend/interfaces/certificates.ts b/frontend/interfaces/certificates.ts index 55e76fbe21..1c7db728ab 100644 --- a/frontend/interfaces/certificates.ts +++ b/frontend/interfaces/certificates.ts @@ -1,3 +1,5 @@ +import { IListSort } from "./list_options"; + export interface IHostCertificate { id: number; not_valid_after: string; @@ -22,3 +24,8 @@ export interface IHostCertificate { common_name: string; }; } + +export const CERTIFICATES_DEFAULT_SORT: IListSort = { + order_key: "common_name", + order_direction: "asc", +} as const; diff --git a/frontend/interfaces/list_options.ts b/frontend/interfaces/list_options.ts new file mode 100644 index 0000000000..acdee7801c --- /dev/null +++ b/frontend/interfaces/list_options.ts @@ -0,0 +1,13 @@ +/** + * Represents query params used as list options by the Fleet API + */ +export interface IListOptions { + page: number; + per_page: number; + order_key: string; + order_direction: string; +} + +export type IListSort = Pick; + +export type IListPagination = Pick; diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx index 97e42db4d9..ec12c1f927 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx @@ -6,7 +6,9 @@ import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import { pick, findIndex } from "lodash"; import { NotificationContext } from "context/notification"; + import deviceUserAPI, { + IGetDeviceCertsRequestParams, IGetDeviceCertificatesResponse, } from "services/entities/device_user"; import diskEncryptionAPI from "services/entities/disk_encryption"; @@ -16,10 +18,14 @@ import { IDeviceUserResponse, IHostDevice, } from "interfaces/host"; +import { IListSort } from "interfaces/list_options"; import { IHostPolicy } from "interfaces/policy"; import { IDeviceGlobalConfig } from "interfaces/config"; import { IHostSoftware } from "interfaces/software"; -import { IHostCertificate } from "interfaces/certificates"; +import { + IHostCertificate, + CERTIFICATES_DEFAULT_SORT, +} from "interfaces/certificates"; import { isAppleDevice } from "interfaces/platform"; import DeviceUserError from "components/DeviceUserError"; @@ -137,6 +143,9 @@ const DeviceUserPage = ({ const [certificatePage, setCertificatePage] = useState( DEFAULT_CERTIFICATES_PAGE ); + const [sortCerts, setSortCerts] = useState({ + ...CERTIFICATES_DEFAULT_SORT, + }); const { data: deviceMapping, refetch: refetchDeviceMapping } = useQuery( ["deviceMapping", deviceAuthToken], @@ -174,24 +183,27 @@ const DeviceUserPage = ({ IGetDeviceCertificatesResponse, Error, IGetDeviceCertificatesResponse, - Array<{ scope: string; token: string; page: number; perPage: number }> + Array >( [ { scope: "device-certificates", token: deviceAuthToken, page: certificatePage, - perPage: DEFAULT_CERTIFICATES_PAGE_SIZE, + per_page: DEFAULT_CERTIFICATES_PAGE_SIZE, + order_key: sortCerts.order_key, + order_direction: sortCerts.order_direction, }, ], - ({ queryKey: [{ token, page, perPage }] }) => - deviceUserAPI.getDeviceCertificates(token, page, perPage), + ({ queryKey }) => deviceUserAPI.getDeviceCertificates(queryKey[0]), { ...DEFAULT_USE_QUERY_OPTIONS, // FIXME: is it worth disabling for unsupported platforms? we'd have to workaround the a // catch-22 where we need to know the platform to know if it's supported but we also need to // be able to include the cert refetch in the hosts query hook. enabled: !!deviceUserAPI, + keepPreviousData: true, + staleTime: 15000, } ); @@ -486,12 +498,15 @@ const DeviceUserPage = ({ isError={isErrorDeviceCertificates} page={certificatePage} pageSize={DEFAULT_CERTIFICATES_PAGE_SIZE} + sortHeader={sortCerts.order_key} + sortDirection={sortCerts.order_direction} hostPlatform={host.platform} onSelectCertificate={onSelectCertificate} onNextPage={() => setCertificatePage(certificatePage + 1)} onPreviousPage={() => setCertificatePage(certificatePage - 1) } + onSortChange={setSortCerts} /> )} diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 0213d44397..e0eb85ae55 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -15,7 +15,10 @@ import activitiesAPI, { IHostPastActivitiesResponse, IHostUpcomingActivitiesResponse, } from "services/entities/activities"; -import hostAPI, { IGetHostCertificatesResponse } from "services/entities/hosts"; +import hostAPI, { + IGetHostCertificatesResponse, + IGetHostCertsRequestParams, +} from "services/entities/hosts"; import teamAPI, { ILoadTeamsResponse } from "services/entities/teams"; import { @@ -27,12 +30,16 @@ import { IPackStats, } from "interfaces/host"; import { ILabel } from "interfaces/label"; +import { IListSort } from "interfaces/list_options"; import { IHostPolicy } from "interfaces/policy"; import { IQueryStats } from "interfaces/query_stats"; import { IHostSoftware } from "interfaces/software"; import { ITeam } from "interfaces/team"; import { IHostUpcomingActivity } from "interfaces/activity"; -import { IHostCertificate } from "interfaces/certificates"; +import { + IHostCertificate, + CERTIFICATES_DEFAULT_SORT, +} from "interfaces/certificates"; import { normalizeEmptyValues, wrapFleetHelper } from "utilities/helpers"; import permissions from "utilities/permissions"; @@ -225,6 +232,9 @@ const HostDetailsPage = ({ const [certificatePage, setCertificatePage] = useState( DEFAULT_CERTIFICATES_PAGE ); + const [sortCerts, setSortCerts] = useState({ + ...CERTIFICATES_DEFAULT_SORT, + }); const { data: teams } = useQuery( "teams", @@ -283,31 +293,33 @@ const HostDetailsPage = ({ const { data: hostCertificates, - isLoading: isLoadingHostCertificates, isError: isErrorHostCertificates, refetch: refetchHostCertificates, } = useQuery< IGetHostCertificatesResponse, Error, IGetHostCertificatesResponse, - Array<{ scope: string; hostId: number; page: number; perPage: number }> + Array >( [ { scope: "host-certificates", - hostId: hostIdFromURL, + host_id: hostIdFromURL, page: certificatePage, - perPage: DEFAULT_CERTIFICATES_PAGE_SIZE, + per_page: DEFAULT_CERTIFICATES_PAGE_SIZE, + order_key: sortCerts.order_key, + order_direction: sortCerts.order_direction, }, ], - ({ queryKey: [{ hostId, page, perPage }] }) => - hostAPI.getHostCertificates(hostId, page, perPage), + ({ queryKey }) => hostAPI.getHostCertificates(queryKey[0]), { ...DEFAULT_USE_QUERY_OPTIONS, // FIXME: is it worth disabling for unsupported platforms? we'd have to workaround the a // catch-22 where we need to know the platform to know if it's supported but we also need to // be able to include the cert refetch in the hosts query hook. enabled: !!hostIdFromURL, + keepPreviousData: true, + staleTime: 15000, } ); @@ -786,8 +798,7 @@ const HostDetailsPage = ({ !host || isLoadingHost || pastActivitiesIsLoading || - upcomingActivitiesIsLoading || - isLoadingHostCertificates + upcomingActivitiesIsLoading ) { return ; } @@ -980,6 +991,9 @@ const HostDetailsPage = ({ onPreviousPage={() => setCertificatePage(certificatePage - 1) } + sortDirection={sortCerts.order_direction} + sortHeader={sortCerts.order_key} + onSortChange={setSortCerts} /> )} diff --git a/frontend/pages/hosts/details/cards/Certificates/Certificates.tsx b/frontend/pages/hosts/details/cards/Certificates/Certificates.tsx index 56433cc922..d733fbb052 100644 --- a/frontend/pages/hosts/details/cards/Certificates/Certificates.tsx +++ b/frontend/pages/hosts/details/cards/Certificates/Certificates.tsx @@ -1,9 +1,11 @@ import React from "react"; -import { IHostCertificate } from "interfaces/certificates"; -import { HostPlatform } from "interfaces/platform"; import { IGetHostCertificatesResponse } from "services/entities/hosts"; +import { IHostCertificate } from "interfaces/certificates"; +import { IListSort } from "interfaces/list_options"; +import { HostPlatform } from "interfaces/platform"; + import Card from "components/Card"; import DataError from "components/DataError"; @@ -16,11 +18,14 @@ interface ICertificatesProps { hostPlatform: HostPlatform; page: number; pageSize: number; + sortHeader: string; + sortDirection: string; isError: boolean; isMyDevicePage?: boolean; onSelectCertificate: (certificate: IHostCertificate) => void; onNextPage: () => void; onPreviousPage: () => void; + onSortChange: ({ order_key, order_direction }: IListSort) => void; } const CertificatesCard = ({ @@ -29,10 +34,13 @@ const CertificatesCard = ({ isError, page, pageSize, + sortHeader, + sortDirection, isMyDevicePage = false, onSelectCertificate, onNextPage, onPreviousPage, + onSortChange, }: ICertificatesProps) => { const renderContent = () => { if (isError) return ; @@ -43,6 +51,9 @@ const CertificatesCard = ({ showHelpText={!isMyDevicePage && hostPlatform === "darwin"} page={page} pageSize={pageSize} + sortDirection={sortDirection} + sortHeader={sortHeader} + onSortChange={onSortChange} onSelectCertificate={onSelectCertificate} onNextPage={onNextPage} onPreviousPage={onPreviousPage} diff --git a/frontend/pages/hosts/details/cards/Certificates/CertificatesTable/CertificatesTable.tsx b/frontend/pages/hosts/details/cards/Certificates/CertificatesTable/CertificatesTable.tsx index 9ae3fc00fa..9bf45bb9fb 100644 --- a/frontend/pages/hosts/details/cards/Certificates/CertificatesTable/CertificatesTable.tsx +++ b/frontend/pages/hosts/details/cards/Certificates/CertificatesTable/CertificatesTable.tsx @@ -2,6 +2,7 @@ import React, { useCallback } from "react"; import { IHostCertificate } from "interfaces/certificates"; import { IGetHostCertificatesResponse } from "services/entities/hosts"; +import { IListSort } from "interfaces/list_options"; import TableContainer from "components/TableContainer"; import CustomLink from "components/CustomLink"; @@ -17,9 +18,12 @@ interface ICertificatesTableProps { showHelpText: boolean; page: number; pageSize: number; + sortHeader: string; + sortDirection: string; onSelectCertificate: (certificate: IHostCertificate) => void; onNextPage: () => void; onPreviousPage: () => void; + onSortChange: ({ order_key, order_direction }: IListSort) => void; } const CertificatesTable = ({ @@ -27,9 +31,12 @@ const CertificatesTable = ({ showHelpText, page, pageSize, + sortDirection, + sortHeader, onSelectCertificate, onNextPage, onPreviousPage, + onSortChange, }: ICertificatesTableProps) => { const tableConfig = generateTableConfig(); @@ -38,18 +45,23 @@ const CertificatesTable = ({ }; const onQueryChange = useCallback( - async (newTableQuery: ITableQueryData) => { - console.log(newTableQuery); - - if (page === newTableQuery.pageIndex) return; - - if (newTableQuery.pageIndex > page) { - onNextPage(); - } else { - onPreviousPage(); + (newQuery: ITableQueryData) => { + switch (true) { + case newQuery.pageIndex > page: + return onNextPage(); + case newQuery.pageIndex < page: + return onPreviousPage(); + case newQuery.sortHeader !== sortHeader || + newQuery.sortDirection !== sortDirection: + return onSortChange({ + order_key: newQuery.sortHeader, + order_direction: newQuery.sortDirection || "asc", + }); + default: + return undefined; // noop } }, - [onNextPage, onPreviousPage, page] + [onNextPage, onPreviousPage, onSortChange, page, sortDirection, sortHeader] ); const helpText = showHelpText ? ( @@ -81,8 +93,11 @@ const CertificatesTable = ({ )} pageSize={pageSize} defaultPageIndex={page} + defaultSortHeader={sortHeader} + defaultSortDirection={sortDirection} onQueryChange={onQueryChange} disableNextPage={data?.meta.has_next_results === false} + manualSortBy /> ); }; diff --git a/frontend/services/entities/device_user.ts b/frontend/services/entities/device_user.ts index 72b7a93ac0..364d2c73f4 100644 --- a/frontend/services/entities/device_user.ts +++ b/frontend/services/entities/device_user.ts @@ -1,10 +1,10 @@ import { IDeviceUserResponse } from "interfaces/host"; +import { IListOptions } from "interfaces/list_options"; import { IDeviceSoftware } from "interfaces/software"; import { IHostCertificate } from "interfaces/certificates"; import sendRequest from "services"; import endpoints from "utilities/endpoints"; import { buildQueryStringFromParams } from "utilities/url"; -import { createMockGetHostCertificatesResponse } from "__mocks__/certificatesMock"; import { IHostSoftwareQueryParams } from "./hosts"; @@ -38,6 +38,10 @@ export interface IGetDeviceCertificatesResponse { }; } +export interface IGetDeviceCertsRequestParams extends IListOptions { + token: string; +} + export default { loadHostDetails: ({ token, @@ -86,17 +90,19 @@ export default { return sendRequest("POST", path); }, - getDeviceCertificates: ( - deviceToken: string, - page = 0, - perPage = 10 - ): Promise => { + getDeviceCertificates: ({ + token, + page, + per_page, + order_key, + order_direction, + }: IGetDeviceCertsRequestParams): Promise => { const { DEVICE_CERTIFICATES } = endpoints; - const path = `${DEVICE_CERTIFICATES( - deviceToken - )}?${buildQueryStringFromParams({ + const path = `${DEVICE_CERTIFICATES(token)}?${buildQueryStringFromParams({ page, - per_page: perPage, + per_page, + order_key, + order_direction, })}`; return sendRequest("GET", path); diff --git a/frontend/services/entities/hosts.ts b/frontend/services/entities/hosts.ts index 6d21f7500e..76e3fd0bba 100644 --- a/frontend/services/entities/hosts.ts +++ b/frontend/services/entities/hosts.ts @@ -24,10 +24,7 @@ import { import { IMunkiIssuesAggregate } from "interfaces/macadmins"; import { PlatformValueOptions, PolicyResponse } from "utilities/constants"; import { IHostCertificate } from "interfaces/certificates"; -import { - createMockGetHostCertificatesResponse, - createMockHostCertificate, -} from "__mocks__/certificatesMock"; +import { IListOptions } from "interfaces/list_options"; export interface ISortOption { key: string; @@ -176,6 +173,10 @@ export interface IHostSoftwareQueryKey extends IHostSoftwareQueryParams { softwareUpdatedAt?: string; } +export interface IGetHostCertsRequestParams extends IListOptions { + host_id: number; +} + export interface IGetHostCertificatesResponse { certificates: IHostCertificate[]; meta: { @@ -601,15 +602,19 @@ export default { ); }, - getHostCertificates: ( - hostId: number, - page = 0, - perPage = 10 - ): Promise => { + getHostCertificates: ({ + host_id, + page, + per_page, + order_key, + order_direction, + }: IGetHostCertsRequestParams): Promise => { const { HOST_CERTIFICATES } = endpoints; - const path = `${HOST_CERTIFICATES(hostId)}?${buildQueryStringFromParams({ + const path = `${HOST_CERTIFICATES(host_id)}?${buildQueryStringFromParams({ page, - per_page: perPage, + per_page, + order_key, + order_direction, })}`; return sendRequest("GET", path);