Fix unreleased bugs in software installers UI (#19119)

This commit is contained in:
Sarah Gillespie 2024-05-17 12:30:55 -05:00 committed by GitHub
parent 54cca7b28a
commit c6d0a39930
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 127 additions and 120 deletions

View file

@ -70,13 +70,15 @@ const generateTableHeaders = (
id.toString()
)}?${teamQueryParam}`;
const hasPackage = Boolean(software_package) && !!teamId; // teamId is required for package installation
return (
<SoftwareNameCell
name={name}
source={source}
path={softwareTitleDetailsPath}
router={router}
hasPackage={Boolean(software_package)}
hasPackage={hasPackage}
/>
);
},

View file

@ -12,6 +12,7 @@
padding-top: $pad-xlarge;
.textarea {
margin-top: $pad-medium;
overflow-wrap: break-word;
}
}
}

View file

@ -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}
/>

View file

@ -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}

View file

@ -26,6 +26,16 @@ interface IHostSoftwareTableProps {
pagePath: string;
}
const SoftwareCount = (count: number) => {
return (
<div className={`${baseClass}__count`}>
<span>
{count === 1 ? `${count} software item` : `${count} software items`}
</span>
</div>
);
};
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 (
<div className={`${baseClass}__count`}>
<span>{itemText}</span>
</div>
);
};
const memoizedEmptyComponent = useCallback(() => {
return <EmptySoftwareTable isSearching={searchQuery !== ""} />;
}, [searchQuery]);
return (
<div className={baseClass}>
<TableContainer
renderCount={renderSoftwareCount}
renderCount={memoizedSoftwareCount}
resultsTitle="software items"
columnConfigs={tableConfig}
data={data.software}
@ -130,9 +129,7 @@ const HostSoftwareTable = ({
pageSize={DEFAULT_PAGE_SIZE}
inputPlaceHolder="Search by name"
onQueryChange={onQueryChange}
emptyComponent={() => (
<EmptySoftwareTable isSearching={searchQuery !== ""} />
)}
emptyComponent={memoizedEmptyComponent}
showMarkAllPages={false}
isAllPagesSelected={false}
searchable

View file

@ -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<typeof parseHostSoftwareQueryParams>;
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 <Spinner />;
}
const isLoading = isMyDevicePage
? deviceSoftwareLoading
: hostSoftwareLoading;
if (hostSoftwareError || deviceSoftwareError) {
return <DataError />;
}
const isError = isMyDevicePage ? deviceSoftwareError : hostSoftwareError;
const props = {
router,
tableConfig,
sortHeader,
sortDirection,
searchQuery,
page,
pagePath: pathname,
};
if (!isMyDevicePage) {
return hostSoftwareRes ? (
<HostSoftwareTable
isLoading={hostSoftwareLoading}
data={hostSoftwareRes}
{...props}
/>
) : null;
}
return deviceSoftwareRes ? (
<HostSoftwareTable
isLoading={deviceSoftwareLoading}
data={deviceSoftwareRes}
{...props}
/>
) : null;
};
const data = isMyDevicePage ? deviceSoftwareRes : hostSoftwareRes;
return (
<Card
@ -268,8 +246,30 @@ const SoftwareCard = ({
className={baseClass}
>
<p className="card__header">Software</p>
{renderSoftwareTable()}
{isLoading ? (
<Spinner />
) : (
<>
{(isError || !data) && <DataError />}
{!isError && data && (
<HostSoftwareTable
isLoading={
isMyDevicePage ? deviceSoftwareFetching : hostSoftwareFetching
}
data={data}
router={router}
tableConfig={tableConfig}
sortHeader={queryParams.order_key}
sortDirection={queryParams.order_direction}
searchQuery={queryParams.query}
page={queryParams.page}
pagePath={pathname}
/>
)}
</>
)}
</Card>
);
};
export default React.memo(SoftwareCard);

View file

@ -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<IGetDeviceSoftwareResponse> => {
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}`);
},
};

View file

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