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) => {