fleet/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibraryTable/HostSoftwareLibraryTable.tsx
2025-06-23 11:09:20 -04:00

217 lines
6.7 KiB
TypeScript

import React, { useCallback } from "react";
import { InjectedRouter } from "react-router";
import { SingleValue } from "react-select-5";
import { IGetHostSoftwareResponse } from "services/entities/hosts";
import { IGetDeviceSoftwareResponse } from "services/entities/device_user";
import { getNextLocationPath } from "utilities/helpers";
import { convertParamsToSnakeCase, QueryParams } from "utilities/url";
import { SUPPORT_LINK } from "utilities/constants";
import { HostPlatform, isAndroid } from "interfaces/platform";
import DropdownWrapper from "components/forms/fields/DropdownWrapper";
import TableContainer from "components/TableContainer";
import { ITableQueryData } from "components/TableContainer/TableContainer";
import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper";
import EmptySoftwareTable from "pages/SoftwarePage/components/tables/EmptySoftwareTable";
import TableCount from "components/TableContainer/TableCount";
import EmptyTable from "components/EmptyTable";
import CustomLink from "components/CustomLink";
import { DROPDOWN_OPTIONS, IHostSWLibraryDropdownFilterVal } from "../helpers";
const DEFAULT_PAGE_SIZE = 20;
const baseClass = "host-sw-library-table";
interface IHostSoftwareLibraryTableProps {
tableConfig: any; // TODO: type
data?: IGetHostSoftwareResponse | IGetDeviceSoftwareResponse;
platform: HostPlatform;
isLoading: boolean;
router: InjectedRouter;
sortHeader: string;
sortDirection: "asc" | "desc";
searchQuery: string;
page: number;
pagePath: string;
selfService: boolean;
}
const HostSoftwareLibraryTable = ({
tableConfig,
data,
platform,
isLoading,
router,
sortHeader,
sortDirection,
searchQuery,
selfService,
page,
pagePath,
}: IHostSoftwareLibraryTableProps) => {
const determineQueryParamChange = useCallback(
(newTableQuery: ITableQueryData) => {
const changedEntry = Object.entries(newTableQuery).find(([key, val]) => {
switch (key) {
case "searchQuery":
return val !== searchQuery;
case "sortDirection":
return val !== sortDirection;
case "sortHeader":
return val !== sortHeader;
case "pageIndex":
return val !== page;
default:
return false;
}
});
return changedEntry?.[0] ?? "";
},
[page, searchQuery, sortDirection, sortHeader]
);
const generateNewQueryParams = useCallback(
(newTableQuery: ITableQueryData, changedParam: string) => {
const newQueryParam: QueryParams = {
query: newTableQuery.searchQuery,
order_direction: newTableQuery.sortDirection,
order_key: newTableQuery.sortHeader,
page: changedParam === "pageIndex" ? newTableQuery.pageIndex : 0,
};
return newQueryParam;
},
[]
);
// TODO: Look into useDebounceCallback with dependencies
const onQueryChange = useCallback(
async (newTableQuery: ITableQueryData) => {
// we want to determine which query param has changed in order to
// reset the page index to 0 if any other param has changed.
const changedParam = determineQueryParamChange(newTableQuery);
// if nothing has changed, don't update the route. this can happen when
// this handler is called on the inital render. Can also happen when
// the filter dropdown is changed. That is handled on the onChange handler
// for the dropdown.
if (changedParam === "") return;
const newRoute = getNextLocationPath({
pathPrefix: pagePath,
routeTemplate: "",
queryParams: generateNewQueryParams(newTableQuery, changedParam),
});
router.replace(newRoute);
},
[determineQueryParamChange, pagePath, generateNewQueryParams, router]
);
const handleCustomFilterDropdownChange = (
value: IHostSWLibraryDropdownFilterVal
) => {
const queryParams: QueryParams = {
query: searchQuery,
orderDirection: sortDirection,
orderKey: sortHeader,
page: 0, // resets page index
selfService: value === "selfService",
};
router.replace(
getNextLocationPath({
pathPrefix: pagePath,
routeTemplate: "",
queryParams: convertParamsToSnakeCase(queryParams),
})
);
};
const count = data?.count || data?.software?.length || 0;
const isSoftwareNotDetected = count === 0 && searchQuery === "";
const memoizedSoftwareCount = useCallback(() => {
if (isSoftwareNotDetected) {
return null;
}
return <TableCount name="items" count={count} />;
}, [count, isSoftwareNotDetected]);
const memoizedEmptyComponent = useCallback(() => {
return <EmptySoftwareTable noSearchQuery={searchQuery === ""} />;
}, [searchQuery]);
// Determines if a user should be able to filter or search in the table
const hasData = data && data.software.length > 0;
const hasQuery = searchQuery !== "";
const showFilterHeaders = hasData || hasQuery;
if (isAndroid(platform)) {
return (
<EmptyTable
header="Installers are not supported for this host"
info={
<>
Interested in installing software on Android hosts?{" "}
<CustomLink url={SUPPORT_LINK} text="Let us know" newTab />
</>
}
/>
);
}
const renderCustomControls = () => {
return (
<div className={`${baseClass}__filter-controls`}>
<DropdownWrapper
name="host-library-filter"
value={selfService ? "selfService" : "available"}
className={`${baseClass}__host-library-filter`}
options={DROPDOWN_OPTIONS}
onChange={(newValue: SingleValue<CustomOptionType>) =>
newValue &&
handleCustomFilterDropdownChange(
newValue.value as IHostSWLibraryDropdownFilterVal
)
}
variant="table-filter"
/>
</div>
);
};
return (
<div className={baseClass}>
<TableContainer
renderCount={memoizedSoftwareCount}
columnConfigs={tableConfig}
data={data?.software || []}
isLoading={isLoading}
defaultSortHeader={sortHeader}
defaultSortDirection={sortDirection}
defaultSearchQuery={searchQuery}
pageIndex={page}
disableNextPage={data?.meta.has_next_results === false}
pageSize={DEFAULT_PAGE_SIZE}
inputPlaceHolder="Search by name"
onQueryChange={onQueryChange}
customControl={showFilterHeaders ? renderCustomControls : undefined}
emptyComponent={memoizedEmptyComponent}
showMarkAllPages={false}
isAllPagesSelected={false}
searchable={showFilterHeaders}
manualSortBy
/>
</div>
);
};
export default HostSoftwareLibraryTable;