diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx index d45403fc60..9cf8e41e24 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx @@ -70,13 +70,15 @@ const generateTableHeaders = ( id.toString() )}?${teamQueryParam}`; + const hasPackage = Boolean(software_package) && !!teamId; // teamId is required for package installation + return ( ); }, diff --git a/frontend/pages/SoftwarePage/components/SoftwareInstallDetails/_styles.scss b/frontend/pages/SoftwarePage/components/SoftwareInstallDetails/_styles.scss index 390ae3a59e..5e8df96d67 100644 --- a/frontend/pages/SoftwarePage/components/SoftwareInstallDetails/_styles.scss +++ b/frontend/pages/SoftwarePage/components/SoftwareInstallDetails/_styles.scss @@ -12,6 +12,7 @@ padding-top: $pad-xlarge; .textarea { margin-top: $pad-medium; + overflow-wrap: break-word; } } } diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx index f1dd56657d..c86d059d4d 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx +++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx @@ -44,6 +44,7 @@ import ManualEnrollMdmModal from "./ManualEnrollMdmModal"; import OSSettingsModal from "../OSSettingsModal"; import ResetKeyModal from "./ResetKeyModal"; import BootstrapPackageModal from "../HostDetailsPage/modals/BootstrapPackageModal"; +import { parseHostSoftwareQueryParams } from "../cards/Software/Software"; const baseClass = "device-user"; @@ -69,7 +70,7 @@ const DeviceUserPage = ({ params: { device_auth_token }, }: IDeviceUserPageProps): JSX.Element => { const deviceAuthToken = device_auth_token; - const queryParams = location.query; + const { renderFlash } = useContext(NotificationContext); const [isPremiumTier, setIsPremiumTier] = useState(false); @@ -410,7 +411,7 @@ const DeviceUserPage = ({ isFleetdHost={!!host.orbit_version} router={router} pathname={location.pathname} - queryParams={queryParams} + queryParams={parseHostSoftwareQueryParams(location.query)} isMyDevicePage teamId={host.team_id || 0} /> diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 62eb51cde4..bd9c4a93aa 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -3,7 +3,6 @@ import { Params, InjectedRouter } from "react-router/lib/Router"; import { useQuery } from "react-query"; import { useErrorHandler } from "react-error-boundary"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; -import { RouteProps } from "react-router"; import { pick } from "lodash"; import PATHS from "router/paths"; @@ -93,6 +92,7 @@ import { } from "../helpers"; import WipeModal from "./modals/WipeModal"; import SoftwareDetailsModal from "../cards/Software/SoftwareDetailsModal"; +import { parseHostSoftwareQueryParams } from "../cards/Software/Software"; const baseClass = "host-details"; @@ -133,7 +133,6 @@ const HostDetailsPage = ({ params: { host_id }, }: IHostDetailsProps): JSX.Element => { const hostIdFromURL = parseInt(host_id, 10); - const queryParams = location.query; const { config, @@ -387,8 +386,12 @@ const HostDetailsPage = ({ activeTab: activeActivityTab, }, ], - ({ queryKey: [{ pageIndex: page, perPage }] }) => { - return activitiesAPI.getHostPastActivities(hostIdFromURL, page, perPage); + ({ queryKey: [{ pageIndex, perPage }] }) => { + return activitiesAPI.getHostPastActivities( + hostIdFromURL, + pageIndex, + perPage + ); }, { keepPreviousData: true, @@ -421,10 +424,10 @@ const HostDetailsPage = ({ activeTab: activeActivityTab, }, ], - ({ queryKey: [{ pageIndex: page, perPage }] }) => { + ({ queryKey: [{ pageIndex, perPage }] }) => { return activitiesAPI.getHostUpcomingActivities( hostIdFromURL, - page, + pageIndex, perPage ); }, @@ -852,7 +855,7 @@ const HostDetailsPage = ({ isFleetdHost={!!host.orbit_version} isSoftwareEnabled={featuresConfig?.enable_software_inventory} router={router} - queryParams={queryParams} + queryParams={parseHostSoftwareQueryParams(location.query)} pathname={location.pathname} onShowSoftwareDetails={setSelectedSoftwareDetails} teamId={host.team_id || 0} diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx index f8e7a3c4f7..caccee69b5 100644 --- a/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx +++ b/frontend/pages/hosts/details/cards/Software/HostSoftwareTable/HostSoftwareTable.tsx @@ -26,6 +26,16 @@ interface IHostSoftwareTableProps { pagePath: string; } +const SoftwareCount = (count: number) => { + return ( +
+ + {count === 1 ? `${count} software item` : `${count} software items`} + +
+ ); +}; + const HostSoftwareTable = ({ tableConfig, data, @@ -96,29 +106,18 @@ const HostSoftwareTable = ({ [determineQueryParamChange, pagePath, generateNewQueryParams, router] ); - const getItemsCountText = () => { - const count = data?.count; - if (!data?.software?.length || !count) return ""; + const memoizedSoftwareCount = useCallback(() => { + return SoftwareCount(data.count || data.software.length || 0); + }, [data.count, data.software.length]); - return count === 1 ? `${count} software item` : `${count} software items`; - }; - - const renderSoftwareCount = () => { - const itemText = getItemsCountText(); - - if (!itemText) return null; - - return ( -
- {itemText} -
- ); - }; + const memoizedEmptyComponent = useCallback(() => { + return ; + }, [searchQuery]); return (
( - - )} + emptyComponent={memoizedEmptyComponent} showMarkAllPages={false} isAllPagesSelected={false} searchable diff --git a/frontend/pages/hosts/details/cards/Software/Software.tsx b/frontend/pages/hosts/details/cards/Software/Software.tsx index 9a9476deee..1b92a1b6fa 100644 --- a/frontend/pages/hosts/details/cards/Software/Software.tsx +++ b/frontend/pages/hosts/details/cards/Software/Software.tsx @@ -6,10 +6,10 @@ import { trimEnd, upperFirst } from "lodash"; import hostAPI, { IGetHostSoftwareResponse, - IHostSoftwareQueryParams, + IHostSoftwareQueryKey, } from "services/entities/hosts"; import deviceAPI, { - IDeviceSoftwareQueryParams, + IDeviceSoftwareQueryKey, IGetDeviceSoftwareResponse, } from "services/entities/device_user"; import { getErrorReason } from "interfaces/errors"; @@ -18,9 +18,9 @@ import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants"; import { NotificationContext } from "context/notification"; import { AppContext } from "context/app"; -import Card from "components/Card"; -import Spinner from "components/Spinner"; +import Card from "components/Card/Card"; import DataError from "components/DataError"; +import Spinner from "components/Spinner"; import { generateSoftwareTableHeaders as generateHostSoftwareTableConfig } from "./HostSoftwareTableConfig"; import { generateSoftwareTableHeaders as generateDeviceSoftwareTableConfig } from "./DeviceSoftwareTableConfig"; @@ -37,12 +37,7 @@ interface ISoftwareCardProps { id: number | string; isFleetdHost: boolean; router: InjectedRouter; - queryParams?: { - page?: string; - query?: string; - order_key?: string; - order_direction?: "asc" | "desc"; - }; + queryParams: ReturnType; pathname: string; /** Team id for the host */ teamId: number; @@ -57,6 +52,29 @@ const DEFAULT_SORT_HEADER = "name"; const DEFAULT_PAGE = 0; const DEFAULT_PAGE_SIZE = 20; +export const parseHostSoftwareQueryParams = (queryParams: { + page?: string; + query?: string; + order_key?: string; + order_direction?: "asc" | "desc"; +}) => { + const searchQuery = queryParams?.query ?? DEFAULT_SEARCH_QUERY; + const sortHeader = queryParams?.order_key ?? DEFAULT_SORT_HEADER; + const sortDirection = queryParams?.order_direction ?? DEFAULT_SORT_DIRECTION; + const page = queryParams?.page + ? parseInt(queryParams.page, 10) + : DEFAULT_PAGE; + const pageSize = DEFAULT_PAGE_SIZE; + + return { + page, + query: searchQuery, + order_key: sortHeader, + order_direction: sortDirection, + per_page: pageSize, + }; +}; + const SoftwareCard = ({ id, isFleetdHost, @@ -80,14 +98,6 @@ const SoftwareCard = ({ number | null >(null); - const searchQuery = queryParams?.query ?? DEFAULT_SEARCH_QUERY; - const sortHeader = queryParams?.order_key ?? DEFAULT_SORT_HEADER; - const sortDirection = queryParams?.order_direction ?? DEFAULT_SORT_DIRECTION; - const page = queryParams?.page - ? parseInt(queryParams.page, 10) - : DEFAULT_PAGE; - const pageSize = DEFAULT_PAGE_SIZE; - const { data: hostSoftwareRes, isLoading: hostSoftwareLoading, @@ -98,24 +108,23 @@ const SoftwareCard = ({ IGetHostSoftwareResponse, AxiosError, IGetHostSoftwareResponse, - [string, IHostSoftwareQueryParams] + IHostSoftwareQueryKey[] >( [ - "host-software", { - page, - per_page: pageSize, - query: searchQuery, - order_key: sortHeader, - order_direction: sortDirection, + scope: "host_software", + id: id as number, + ...queryParams, }, ], ({ queryKey }) => { - return hostAPI.getHostSoftware(id as number, queryKey[1]); + return hostAPI.getHostSoftware(queryKey[0]); }, { ...DEFAULT_USE_QUERY_OPTIONS, enabled: isSoftwareEnabled && !isMyDevicePage, + keepPreviousData: true, + staleTime: 7000, } ); @@ -129,22 +138,21 @@ const SoftwareCard = ({ IGetDeviceSoftwareResponse, AxiosError, IGetDeviceSoftwareResponse, - [string, IDeviceSoftwareQueryParams] + IDeviceSoftwareQueryKey[] >( [ - "device-software", { - page, - per_page: pageSize, - query: searchQuery, - order_key: sortHeader, - order_direction: sortDirection, + scope: "device_software", + id: id as string, + ...queryParams, }, ], - ({ queryKey }) => deviceAPI.getDeviceSoftware(id as string, queryKey[1]), + ({ queryKey }) => deviceAPI.getDeviceSoftware(queryKey[0]), { ...DEFAULT_USE_QUERY_OPTIONS, enabled: isSoftwareEnabled && isMyDevicePage, + keepPreviousData: true, + staleTime: 7000, } ); @@ -222,43 +230,13 @@ const SoftwareCard = ({ isFleetdHost, ]); - const renderSoftwareTable = () => { - if (hostSoftwareLoading || deviceSoftwareLoading) { - return ; - } + const isLoading = isMyDevicePage + ? deviceSoftwareLoading + : hostSoftwareLoading; - if (hostSoftwareError || deviceSoftwareError) { - return ; - } + const isError = isMyDevicePage ? deviceSoftwareError : hostSoftwareError; - const props = { - router, - tableConfig, - sortHeader, - sortDirection, - searchQuery, - page, - pagePath: pathname, - }; - - if (!isMyDevicePage) { - return hostSoftwareRes ? ( - - ) : null; - } - - return deviceSoftwareRes ? ( - - ) : null; - }; + const data = isMyDevicePage ? deviceSoftwareRes : hostSoftwareRes; return (

Software

- {renderSoftwareTable()} + {isLoading ? ( + + ) : ( + <> + {(isError || !data) && } + {!isError && data && ( + + )} + + )}
); }; + export default React.memo(SoftwareCard); diff --git a/frontend/services/entities/device_user.ts b/frontend/services/entities/device_user.ts index 77f92106e6..d0de571946 100644 --- a/frontend/services/entities/device_user.ts +++ b/frontend/services/entities/device_user.ts @@ -3,16 +3,14 @@ import { IHostSoftware } from "interfaces/software"; import sendRequest from "services"; import endpoints from "utilities/endpoints"; import { buildQueryStringFromParams } from "utilities/url"; +import { IHostSoftwareQueryParams } from "./hosts"; export type ILoadHostDetailsExtension = "device_mapping" | "macadmins"; -export type IDeviceSoftwareQueryParams = { - page: number; - per_page: number; - query: string; - order_key: string; - order_direction: "asc" | "desc"; -}; +export interface IDeviceSoftwareQueryKey extends IHostSoftwareQueryParams { + scope: "device_software"; + id: string; +} export interface IGetDeviceSoftwareResponse { software: IHostSoftware[]; @@ -47,14 +45,12 @@ export default { }, getDeviceSoftware: ( - deviceAuthToken: string, - params: IDeviceSoftwareQueryParams + params: IDeviceSoftwareQueryKey ): Promise => { const { DEVICE_SOFTWARE } = endpoints; - const queryString = buildQueryStringFromParams(params as any); // TODO: fix with generics - return sendRequest( - "GET", - `${DEVICE_SOFTWARE(deviceAuthToken)}?${queryString}` - ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id, scope, ...rest } = params; + const queryString = buildQueryStringFromParams(rest); + return sendRequest("GET", `${DEVICE_SOFTWARE(id)}?${queryString}`); }, }; diff --git a/frontend/services/entities/hosts.ts b/frontend/services/entities/hosts.ts index b1eeab100b..c5c8be0f44 100644 --- a/frontend/services/entities/hosts.ts +++ b/frontend/services/entities/hosts.ts @@ -3,6 +3,7 @@ import sendRequest from "services"; import endpoints from "utilities/endpoints"; import { IHost, HostStatus } from "interfaces/host"; import { + QueryParams, buildQueryStringFromParams, getLabelParam, reconcileMutuallyExclusiveHostParams, @@ -158,7 +159,7 @@ export interface IGetHostSoftwareResponse { }; } -export interface IHostSoftwareQueryParams { +export interface IHostSoftwareQueryParams extends QueryParams { page: number; per_page: number; query: string; @@ -166,6 +167,11 @@ export interface IHostSoftwareQueryParams { order_direction: "asc" | "desc"; } +export interface IHostSoftwareQueryKey extends IHostSoftwareQueryParams { + scope: "host_software"; + id: number; +} + export type ILoadHostDetailsExtension = "device_mapping" | "macadmins"; const LABEL_PREFIX = "labels/"; @@ -568,13 +574,14 @@ export default { }, getHostSoftware: ( - hostId: number, - params: IHostSoftwareQueryParams + params: IHostSoftwareQueryKey ): Promise => { const { HOST_SOFTWARE } = endpoints; - const queryString = buildQueryStringFromParams(params as any); // TODO: fix with generics + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id, scope, ...rest } = params; + const queryString = buildQueryStringFromParams(rest); - return sendRequest("GET", `${HOST_SOFTWARE(hostId)}?${queryString}`); + return sendRequest("GET", `${HOST_SOFTWARE(id)}?${queryString}`); }, installHostSoftwarePackage: (hostId: number, softwareId: number) => {