From 4fdea91a7a08cf9fde5bd6b2d68f7d712e36c4be Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:16:11 -0500 Subject: [PATCH] Fleet UI: Add Software > FMA table - Add platform and status filter (#37805) --- changes/37804-filtering-fmas | 1 + .../FleetMaintainedAppsTable.tsx | 128 ++++++++++++++++-- .../FmaFilters/FmaFilters.tsx | 95 +++++++++++++ .../FmaFilters/_styles.scss | 13 ++ .../FleetMaintainedAppsTable/_styles.scss | 5 + 5 files changed, 234 insertions(+), 8 deletions(-) create mode 100644 changes/37804-filtering-fmas create mode 100644 frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FmaFilters/FmaFilters.tsx create mode 100644 frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FmaFilters/_styles.scss diff --git a/changes/37804-filtering-fmas b/changes/37804-filtering-fmas new file mode 100644 index 0000000000..25e1d2fd35 --- /dev/null +++ b/changes/37804-filtering-fmas @@ -0,0 +1 @@ +* Fleet UI: Add filtering by platform and add status to the Software > Add Fleet-maintained apps table \ No newline at end of file diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTable.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTable.tsx index e74ad8a4f2..d750d5463f 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTable.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { InjectedRouter } from "react-router"; import PATHS from "router/paths"; @@ -15,6 +15,12 @@ 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"; @@ -95,6 +101,9 @@ const FleetMaintainedAppsTable = ({ orderKey, currentPage, }: IFleetMaintainedAppsTableProps) => { + const [status, setStatus] = useState("all"); + const [platform, setPlatform] = useState("all"); + const determineQueryParamChange = useCallback( (newTableQuery: ITableQueryData) => { const changedEntry = Object.entries(newTableQuery).find(([key, val]) => { @@ -117,13 +126,20 @@ const FleetMaintainedAppsTable = ({ ); const generateNewQueryParams = useCallback( - (newTableQuery: ITableQueryData, changedParam: string) => { + ( + newTableQuery: ITableQueryData, + changedParam: string, + nextPlatform: FmaPlatformValue, + nextStatus: FmaStatusValue + ) => { const newQueryParam: Record = { 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; @@ -147,12 +163,23 @@ const FleetMaintainedAppsTable = ({ const newRoute = getNextLocationPath({ pathPrefix: PATHS.SOFTWARE_ADD_FLEET_MAINTAINED, routeTemplate: "", - queryParams: generateNewQueryParams(newTableQuery, changedParam), + queryParams: generateNewQueryParams( + newTableQuery, + changedParam, + platform, + status + ), }); router.replace(newRoute); }, - [determineQueryParamChange, generateNewQueryParams, router] + [ + determineQueryParamChange, + generateNewQueryParams, + router, + platform, + status, + ] ); const tableHeadersConfig = useMemo(() => { @@ -166,17 +193,100 @@ const FleetMaintainedAppsTable = ({ const combinedAppsByPlatform = (data && combineAppsByPlatform(data.fleet_maintained_apps ?? [])) ?? []; - const renderCount = () => { - if (!combinedAppsByPlatform) return null; + const filteredApps = combinedAppsByPlatform.filter((app) => { + const macAvailable = !!app.macos && !app.macos.software_title_id; + const winAvailable = !!app.windows && !app.windows.software_title_id; - return ; + // 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 ; }; + 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 = () => ( +
+ + +
+ ); + return ( className={baseClass} columnConfigs={tableHeadersConfig} - data={combinedAppsByPlatform} + data={filteredApps} isLoading={isLoading} resultsTitle="items" emptyComponent={EmptyFleetAppsTable} @@ -193,6 +303,8 @@ const FleetMaintainedAppsTable = ({ inputPlaceHolder="Search by name" onQueryChange={onQueryChange} renderCount={renderCount} + customControl={renderCustomControls} + stackControls /> ); }; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FmaFilters/FmaFilters.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FmaFilters/FmaFilters.tsx new file mode 100644 index 0000000000..fdb42cd96b --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FmaFilters/FmaFilters.tsx @@ -0,0 +1,95 @@ +import React, { useMemo } from "react"; +import { SingleValue } from "react-select-5"; + +import Slider from "components/forms/fields/Slider/Slider"; +import DropdownWrapper, { + CustomOptionType, +} from "components/forms/fields/DropdownWrapper/DropdownWrapper"; + +const statusBaseClass = "fma-status-select"; +const platformBaseClass = "fma-platform-select"; + +export type FmaPlatformValue = "all" | "macos" | "windows"; +export type FmaStatusValue = "all" | "available"; + +interface IFmaPlatformFilterProps { + value: FmaPlatformValue; + onChange: (value: FmaPlatformValue) => void; + className?: string; +} + +export const FmaPlatformFilter = ({ + value, + onChange, + className, +}: IFmaPlatformFilterProps) => { + const options = useMemo(() => { + return [ + { + value: "all", + label: "All platforms", + isDisabled: false, + }, + { + value: "macos", + label: "macOS", + isDisabled: false, + }, + { + value: "windows", + label: "Windows", + isDisabled: false, + }, + ]; + }, []); + + const handleChange = (newValue: SingleValue) => { + if (!newValue) return; + onChange(newValue.value as FmaPlatformValue); + }; + + return ( +
+ +
+ ); +}; + +interface IFmaStatusFilterProps { + value: FmaStatusValue; + onChange: (value: FmaStatusValue) => void; + className?: string; +} + +export const FmaStatusFilter = ({ + value, + onChange, + className, +}: IFmaStatusFilterProps) => { + const handleChange = () => { + onChange(value === "all" ? ("available" as FmaStatusValue) : "all"); + }; + + const enabled = value === "available"; + + return ( +
+ +
+ ); +}; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FmaFilters/_styles.scss b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FmaFilters/_styles.scss new file mode 100644 index 0000000000..4a400d8720 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FmaFilters/_styles.scss @@ -0,0 +1,13 @@ +.fma-platform-select { + width: 170px; +} + +.fma-status-select { + width: max-content; + display: flex; + align-items: center; + + .form-field { + align-items: end; + } +} diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/_styles.scss b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/_styles.scss index 1fcd45549b..d10251e938 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/_styles.scss @@ -6,4 +6,9 @@ justify-content: center; } } + + &__filters { + display: flex; + gap: $gap-table-elements; + } }