mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
Fix unreleased bugs in software installers UI (#19119)
This commit is contained in:
parent
54cca7b28a
commit
c6d0a39930
8 changed files with 127 additions and 120 deletions
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
padding-top: $pad-xlarge;
|
||||
.textarea {
|
||||
margin-top: $pad-medium;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue