mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
312 lines
8.7 KiB
TypeScript
312 lines
8.7 KiB
TypeScript
import React, { useCallback, useMemo, useState } from "react";
|
|
import { InjectedRouter } from "react-router";
|
|
|
|
import PATHS from "router/paths";
|
|
import { ISoftwareFleetMaintainedAppsResponse } from "services/entities/software";
|
|
import { getNextLocationPath } from "utilities/helpers";
|
|
import {
|
|
FleetMaintainedAppPlatform,
|
|
ICombinedFMA,
|
|
IFleetMaintainedApp,
|
|
} from "interfaces/software";
|
|
|
|
import TableContainer from "components/TableContainer";
|
|
import TableCount from "components/TableContainer/TableCount";
|
|
import { ITableQueryData } from "components/TableContainer/TableContainer";
|
|
import EmptyTable from "components/EmptyTable";
|
|
import CustomLink from "components/CustomLink";
|
|
import {
|
|
FmaStatusFilter,
|
|
FmaPlatformFilter,
|
|
FmaPlatformValue,
|
|
FmaStatusValue,
|
|
} from "./FmaFilters/FmaFilters";
|
|
|
|
import { generateTableConfig } from "./FleetMaintainedAppsTableConfig";
|
|
|
|
const baseClass = "fleet-maintained-apps-table";
|
|
|
|
const EmptyFleetAppsTable = () => (
|
|
<EmptyTable
|
|
graphicName="empty-search-question"
|
|
header="No items match the current search criteria"
|
|
info={
|
|
<>
|
|
Can't find app?{" "}
|
|
<CustomLink
|
|
newTab
|
|
url="https://fleetdm.com/feature-request"
|
|
text="File an issue on GitHub"
|
|
/>
|
|
</>
|
|
}
|
|
/>
|
|
);
|
|
|
|
/** Used to convert FleetMaintainedApp API response which has separate entries
|
|
* for Windows FMA and macOS FMA into table friendly format that combines
|
|
* entries for the same app for different platforms */
|
|
const combineAppsByPlatform = (
|
|
fmaList: IFleetMaintainedApp[]
|
|
): ICombinedFMA[] => {
|
|
const combinedApps: { [name: string]: ICombinedFMA } = {};
|
|
|
|
fmaList.forEach((app: IFleetMaintainedApp) => {
|
|
const { name, platform, ...rest } = app;
|
|
|
|
if (!combinedApps[name]) {
|
|
combinedApps[name] = { name, macos: null, windows: null };
|
|
}
|
|
|
|
if (platform === "darwin") {
|
|
combinedApps[name].macos = {
|
|
platform: platform as FleetMaintainedAppPlatform,
|
|
...rest,
|
|
};
|
|
} else if (platform === "windows") {
|
|
combinedApps[name].windows = {
|
|
platform: platform as FleetMaintainedAppPlatform,
|
|
...rest,
|
|
};
|
|
}
|
|
});
|
|
|
|
return Object.values(combinedApps);
|
|
};
|
|
|
|
interface IFleetMaintainedAppsTableProps {
|
|
teamId: number;
|
|
isLoading: boolean;
|
|
query: string;
|
|
perPage: number;
|
|
orderDirection: "asc" | "desc";
|
|
orderKey: string;
|
|
currentPage: number;
|
|
router: InjectedRouter;
|
|
data?: ISoftwareFleetMaintainedAppsResponse;
|
|
}
|
|
|
|
interface IRowProps {
|
|
original: IFleetMaintainedApp;
|
|
}
|
|
|
|
const FleetMaintainedAppsTable = ({
|
|
teamId,
|
|
isLoading,
|
|
data,
|
|
router,
|
|
query,
|
|
perPage,
|
|
orderDirection,
|
|
orderKey,
|
|
currentPage,
|
|
}: IFleetMaintainedAppsTableProps) => {
|
|
const [status, setStatus] = useState<FmaStatusValue>("all");
|
|
const [platform, setPlatform] = useState<FmaPlatformValue>("all");
|
|
|
|
const determineQueryParamChange = useCallback(
|
|
(newTableQuery: ITableQueryData) => {
|
|
const changedEntry = Object.entries(newTableQuery).find(([key, val]) => {
|
|
switch (key) {
|
|
case "searchQuery":
|
|
return val !== query;
|
|
case "sortDirection":
|
|
return val !== orderDirection;
|
|
case "sortHeader":
|
|
return val !== orderKey;
|
|
case "pageIndex":
|
|
return val !== currentPage;
|
|
default:
|
|
return false;
|
|
}
|
|
});
|
|
return changedEntry?.[0] ?? "";
|
|
},
|
|
[currentPage, orderDirection, orderKey, query]
|
|
);
|
|
|
|
const generateNewQueryParams = useCallback(
|
|
(
|
|
newTableQuery: ITableQueryData,
|
|
changedParam: string,
|
|
nextPlatform: FmaPlatformValue,
|
|
nextStatus: FmaStatusValue
|
|
) => {
|
|
const newQueryParam: Record<string, string | number | undefined> = {
|
|
query: newTableQuery.searchQuery,
|
|
team_id: teamId,
|
|
order_direction: newTableQuery.sortDirection,
|
|
order_key: newTableQuery.sortHeader,
|
|
page: changedParam === "pageIndex" ? newTableQuery.pageIndex : 0,
|
|
platform: nextPlatform === "all" ? undefined : nextPlatform,
|
|
status: nextStatus === "all" ? undefined : nextStatus,
|
|
};
|
|
|
|
return newQueryParam;
|
|
},
|
|
[teamId]
|
|
);
|
|
|
|
// NOTE: this is called once on initial render and every time the query changes
|
|
const onQueryChange = useCallback(
|
|
(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: PATHS.SOFTWARE_ADD_FLEET_MAINTAINED,
|
|
routeTemplate: "",
|
|
queryParams: generateNewQueryParams(
|
|
newTableQuery,
|
|
changedParam,
|
|
platform,
|
|
status
|
|
),
|
|
});
|
|
|
|
router.replace(newRoute);
|
|
},
|
|
[
|
|
determineQueryParamChange,
|
|
generateNewQueryParams,
|
|
router,
|
|
platform,
|
|
status,
|
|
]
|
|
);
|
|
|
|
const tableHeadersConfig = useMemo(() => {
|
|
if (!data) return [];
|
|
return generateTableConfig(router, teamId);
|
|
}, [data, router, teamId]);
|
|
|
|
// Note: Serverside filtering will be buggy with pagination if > 20 apps
|
|
// API will need to be refactored to combine macOS/windows apps
|
|
// for correct pagination, sort, and counts when we go over 20 apps
|
|
const combinedAppsByPlatform =
|
|
(data && combineAppsByPlatform(data.fleet_maintained_apps ?? [])) ?? [];
|
|
|
|
const filteredApps = combinedAppsByPlatform.filter((app) => {
|
|
const macAvailable = !!app.macos && !app.macos.software_title_id;
|
|
const winAvailable = !!app.windows && !app.windows.software_title_id;
|
|
|
|
// platform filter
|
|
if (platform === "macos" && !app.macos) return false;
|
|
if (platform === "windows" && !app.windows) return false;
|
|
|
|
// status filter
|
|
if (status === "all") {
|
|
return true;
|
|
}
|
|
|
|
if (status === "available") {
|
|
if (platform === "macos") return macAvailable;
|
|
if (platform === "windows") return winAvailable;
|
|
return macAvailable || winAvailable;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
const renderCount = () => {
|
|
if (!filteredApps) return null;
|
|
|
|
return <TableCount name="items" count={filteredApps.length} />;
|
|
};
|
|
|
|
const handleFmaStatusDropdownChange = (newStatus: FmaStatusValue) => {
|
|
setStatus(newStatus);
|
|
|
|
const newRoute = getNextLocationPath({
|
|
pathPrefix: PATHS.SOFTWARE_ADD_FLEET_MAINTAINED,
|
|
routeTemplate: "",
|
|
queryParams: generateNewQueryParams(
|
|
{
|
|
searchQuery: query,
|
|
sortDirection: orderDirection,
|
|
sortHeader: orderKey,
|
|
pageIndex: currentPage,
|
|
pageSize: perPage,
|
|
},
|
|
"status",
|
|
platform,
|
|
newStatus
|
|
),
|
|
});
|
|
|
|
router.replace(newRoute);
|
|
};
|
|
|
|
const handleFmaPlatformDropdownChange = (newPlatform: FmaPlatformValue) => {
|
|
setPlatform(newPlatform);
|
|
|
|
const newRoute = getNextLocationPath({
|
|
pathPrefix: PATHS.SOFTWARE_ADD_FLEET_MAINTAINED,
|
|
routeTemplate: "",
|
|
queryParams: generateNewQueryParams(
|
|
{
|
|
searchQuery: query,
|
|
sortDirection: orderDirection,
|
|
sortHeader: orderKey,
|
|
pageIndex: currentPage,
|
|
pageSize: perPage,
|
|
},
|
|
"platform",
|
|
newPlatform,
|
|
status
|
|
),
|
|
});
|
|
|
|
router.replace(newRoute);
|
|
};
|
|
|
|
const renderCustomControls = () => (
|
|
<div className={`${baseClass}__filters`}>
|
|
<FmaStatusFilter
|
|
value={status}
|
|
onChange={handleFmaStatusDropdownChange}
|
|
className={`${baseClass}__status-filter`}
|
|
/>
|
|
<FmaPlatformFilter
|
|
value={platform}
|
|
onChange={handleFmaPlatformDropdownChange}
|
|
className={`${baseClass}__platform-filter`}
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<TableContainer<IRowProps>
|
|
className={baseClass}
|
|
columnConfigs={tableHeadersConfig}
|
|
data={filteredApps}
|
|
isLoading={isLoading}
|
|
resultsTitle="items"
|
|
emptyComponent={EmptyFleetAppsTable}
|
|
defaultSortHeader={orderKey}
|
|
defaultSortDirection={orderDirection}
|
|
pageIndex={currentPage}
|
|
defaultSearchQuery={query}
|
|
manualSortBy
|
|
pageSize={perPage}
|
|
showMarkAllPages={false}
|
|
isAllPagesSelected={false}
|
|
disableNextPage={!data?.meta.has_next_results}
|
|
searchable
|
|
inputPlaceHolder="Search by name"
|
|
onQueryChange={onQueryChange}
|
|
renderCount={renderCount}
|
|
customControl={renderCustomControls}
|
|
stackControls
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default FleetMaintainedAppsTable;
|