UI code cleanup and tests for self service feature (#19487)
various code cleanup tasks for the self service UI. Also adds some tests for self service. - [x] Added/updated tests - [x] Manual QA for all new/changed functionality
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { IDeviceUser } from "interfaces/host";
|
||||
import { IDeviceSoftware } from "interfaces/software";
|
||||
import { IGetDeviceSoftwareResponse } from "services/entities/device_user";
|
||||
|
||||
const DEFAULT_DEVICE_USER_MOCK: IDeviceUser = {
|
||||
email: "test@test.com",
|
||||
@@ -11,4 +13,43 @@ const createMockDeviceUser = (
|
||||
return { ...DEFAULT_DEVICE_USER_MOCK, ...overrides };
|
||||
};
|
||||
|
||||
const DEFAULT_DEVICE_SOFTWARE_MOCK: IDeviceSoftware = {
|
||||
id: 1,
|
||||
name: "mock software 1.app",
|
||||
self_service: false,
|
||||
source: "apps",
|
||||
bundle_identifier: "com.app.mock",
|
||||
status: null,
|
||||
last_install: null,
|
||||
installed_versions: null,
|
||||
package: {
|
||||
name: "mock software 1",
|
||||
version: "1.0.0",
|
||||
},
|
||||
};
|
||||
|
||||
export const createMockDeviceSoftware = (
|
||||
overrides?: Partial<IDeviceSoftware>
|
||||
) => {
|
||||
return { ...DEFAULT_DEVICE_SOFTWARE_MOCK, ...overrides };
|
||||
};
|
||||
|
||||
const DEFAULT_DEVICE_SOFTWARE_RESPONSE_MOCK = {
|
||||
software: [createMockDeviceSoftware()],
|
||||
count: 0,
|
||||
meta: {
|
||||
has_next_results: false,
|
||||
has_previous_results: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const createMockDeviceSoftwareResponse = (
|
||||
overrides?: Partial<IGetDeviceSoftwareResponse>
|
||||
) => {
|
||||
return {
|
||||
...DEFAULT_DEVICE_SOFTWARE_RESPONSE_MOCK,
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
export default createMockDeviceUser;
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
.software-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid $ui-fleet-black-10;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&__install-icon {
|
||||
|
||||
@@ -23,10 +23,14 @@ interface ITooltipWrapper {
|
||||
tipContent: React.ReactNode;
|
||||
/** If set to `true`, will not show the tooltip. This can be used to dynamically
|
||||
* disable the tooltip from the parent component.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
disableTooltip?: boolean;
|
||||
/** If set to `true`, will show the arrow on the tooltip.
|
||||
* This can be used to dynamically hide the arrow from the parent component.
|
||||
* @default false
|
||||
*/
|
||||
showArrow?: boolean;
|
||||
}
|
||||
|
||||
const baseClass = "component__tooltip-wrapper";
|
||||
@@ -44,8 +48,10 @@ const TooltipWrapper = ({
|
||||
tooltipClass,
|
||||
clickable = true,
|
||||
disableTooltip = false,
|
||||
showArrow = false,
|
||||
}: ITooltipWrapper) => {
|
||||
const wrapperClassNames = classnames(baseClass, className, {
|
||||
"show-arrow": showArrow,
|
||||
// [`${baseClass}__${wrapperCustomClass}`]: !!wrapperCustomClass,
|
||||
});
|
||||
|
||||
@@ -71,7 +77,7 @@ const TooltipWrapper = ({
|
||||
id={tipId}
|
||||
delayShow={isDelayed ? 500 : undefined}
|
||||
delayHide={isDelayed ? 500 : undefined}
|
||||
noArrow
|
||||
noArrow={!showArrow}
|
||||
place={position}
|
||||
opacity={1}
|
||||
disableStyleInjection
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
.component__tooltip-wrapper {
|
||||
|
||||
&.show-arrow {
|
||||
@include tooltip5-arrow-styles;
|
||||
}
|
||||
|
||||
display: inline-flex;
|
||||
|
||||
&__element {
|
||||
|
||||
@@ -232,10 +232,12 @@ export interface IHostSoftware {
|
||||
installed_versions: ISoftwareInstallVersion[] | null;
|
||||
}
|
||||
|
||||
export interface IDeviceSoftware extends IHostSoftware {
|
||||
package_available_for_install: never;
|
||||
export type IDeviceSoftware = Omit<
|
||||
IHostSoftware,
|
||||
"package_available_for_install"
|
||||
> & {
|
||||
package: {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
+3
-2
@@ -35,8 +35,7 @@ import AdvancedOptionsModal from "../AdvancedOptionsModal";
|
||||
const baseClass = "software-package-card";
|
||||
|
||||
/** TODO: pull this hook and SoftwareName component out. We could use this other places */
|
||||
|
||||
function useTruncatedElement(ref: any) {
|
||||
function useTruncatedElement<T extends HTMLElement>(ref: React.RefObject<T>) {
|
||||
const [isTruncated, setIsTruncated] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -64,6 +63,7 @@ const SoftwareName = ({ name }: ISoftwareNameProps) => {
|
||||
position="top"
|
||||
underline={false}
|
||||
disableTooltip={!isTruncated}
|
||||
showArrow
|
||||
>
|
||||
<div ref={titleRef} className={`${baseClass}__title`}>
|
||||
{name}
|
||||
@@ -125,6 +125,7 @@ const PackageStatusCount = ({
|
||||
position="top"
|
||||
tipContent={displayData.tooltip}
|
||||
underline={false}
|
||||
showArrow
|
||||
>
|
||||
<div className={`${baseClass}__status-title`}>
|
||||
<Icon name={displayData.iconName} />
|
||||
|
||||
+2
-1
@@ -24,7 +24,8 @@
|
||||
&__title {
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
@include ellipse-text(290px);
|
||||
@include ellipse-text;
|
||||
max-width: 290px;
|
||||
}
|
||||
|
||||
&__details {
|
||||
|
||||
@@ -353,9 +353,6 @@ const SoftwareTable = ({
|
||||
pageSize={perPage}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
disablePagination={
|
||||
!data?.meta.has_next_results && !data?.meta.has_previous_results
|
||||
}
|
||||
disableNextPage={!data?.meta.has_next_results}
|
||||
searchable={searchable}
|
||||
inputPlaceHolder="Search by name or vulnerabilities (CVEs)"
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import React from "react";
|
||||
import { screen } from "@testing-library/react";
|
||||
|
||||
import { createCustomRenderer, createMockRouter } from "test/test-utils";
|
||||
import mockServer from "test/mock-server";
|
||||
import { customDeviceSoftwareHandler } from "test/handlers/device-handler";
|
||||
import { createMockDeviceSoftware } from "__mocks__/deviceUserMock";
|
||||
|
||||
import SelfService from "./SelfService";
|
||||
|
||||
describe("SelfService", () => {
|
||||
it("should render the self service items correctly", async () => {
|
||||
mockServer.use(
|
||||
customDeviceSoftwareHandler({
|
||||
software: [
|
||||
createMockDeviceSoftware({ name: "test1" }),
|
||||
createMockDeviceSoftware({ name: "test2" }),
|
||||
createMockDeviceSoftware({ name: "test3" }),
|
||||
],
|
||||
count: 3,
|
||||
})
|
||||
);
|
||||
|
||||
const render = createCustomRenderer({ withBackendMock: true });
|
||||
|
||||
render(
|
||||
<SelfService
|
||||
contactUrl={"http://example.com"}
|
||||
deviceToken={"123-456"}
|
||||
isSoftwareEnabled
|
||||
pathname={"/test"}
|
||||
queryParams={{
|
||||
page: 1,
|
||||
query: "",
|
||||
order_key: "name",
|
||||
order_direction: "asc",
|
||||
per_page: 10,
|
||||
}}
|
||||
router={createMockRouter()}
|
||||
/>
|
||||
);
|
||||
|
||||
// waiting for the device software data to render
|
||||
await screen.findByText("test1");
|
||||
|
||||
expect(true).toBe(true);
|
||||
expect(screen.getByText("test1")).toBeInTheDocument();
|
||||
expect(screen.getByText("test2")).toBeInTheDocument();
|
||||
expect(screen.getByText("test3")).toBeInTheDocument();
|
||||
expect(screen.getByText("3 items")).toBeInTheDocument();
|
||||
screen.debug();
|
||||
});
|
||||
|
||||
it("should render the contact link text if contact url is provided", () => {
|
||||
mockServer.use(customDeviceSoftwareHandler());
|
||||
|
||||
const render = createCustomRenderer({ withBackendMock: true });
|
||||
|
||||
const expectedUrl = "http://example.com";
|
||||
|
||||
render(
|
||||
<SelfService
|
||||
contactUrl={expectedUrl}
|
||||
deviceToken={"123-456"}
|
||||
isSoftwareEnabled
|
||||
pathname={"/test"}
|
||||
queryParams={{
|
||||
page: 1,
|
||||
query: "test",
|
||||
order_key: "name",
|
||||
order_direction: "asc",
|
||||
per_page: 10,
|
||||
}}
|
||||
router={createMockRouter()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("reach out to IT")).toBeInTheDocument();
|
||||
expect(screen.getByText("reach out to IT").getAttribute("href")).toBe(
|
||||
expectedUrl
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -32,6 +32,15 @@ const DEFAULT_SELF_SERVICE_QUERY_PARAMS = {
|
||||
self_service: true,
|
||||
} as const;
|
||||
|
||||
interface ISoftwareSelfServiceProps {
|
||||
contactUrl: string;
|
||||
deviceToken: string;
|
||||
isSoftwareEnabled?: boolean;
|
||||
pathname: string;
|
||||
queryParams: ReturnType<typeof parseHostSoftwareQueryParams>;
|
||||
router: InjectedRouter;
|
||||
}
|
||||
|
||||
const SoftwareSelfService = ({
|
||||
contactUrl,
|
||||
deviceToken,
|
||||
@@ -39,15 +48,7 @@ const SoftwareSelfService = ({
|
||||
pathname,
|
||||
queryParams,
|
||||
router,
|
||||
}: {
|
||||
contactUrl: string; // TODO: confirm this has been added to the device API response
|
||||
deviceToken: string;
|
||||
isSoftwareEnabled?: boolean;
|
||||
pathname: string;
|
||||
queryParams: ReturnType<typeof parseHostSoftwareQueryParams>;
|
||||
router: InjectedRouter;
|
||||
}) => {
|
||||
// TOOD: loading state for fetching?
|
||||
}: ISoftwareSelfServiceProps) => {
|
||||
const { data, isLoading, isError, refetch } = useQuery<
|
||||
IGetDeviceSoftwareResponse,
|
||||
AxiosError,
|
||||
@@ -121,7 +122,8 @@ const SoftwareSelfService = ({
|
||||
</div>
|
||||
<div className={`${baseClass}__items`}>
|
||||
{data.software.map((s) => {
|
||||
const key = `${s.id}${s.last_install?.install_uuid}`; // concatenating install_uuid so item updates with fresh data on refetch
|
||||
// concatenating install_uuid so item updates with fresh data on refetch
|
||||
const key = `${s.id}${s.last_install?.install_uuid}`;
|
||||
return (
|
||||
<SelfServiceItem
|
||||
key={key}
|
||||
|
||||
@@ -9,6 +9,11 @@ export const baseUrl = (path: string) => {
|
||||
return `/api/latest/fleet${path}`;
|
||||
};
|
||||
|
||||
// These are the default handlers that are used when testing the frontend. They
|
||||
// are used to mock the responses from the Fleet API when running tests.
|
||||
// These can be overridden in individual tests using the .use() method on the
|
||||
// mock server within the desired test.
|
||||
// More info on .use() here: https://mswjs.io/docs/api/setup-worker/use/
|
||||
const handlers = [
|
||||
defaultDeviceHandler,
|
||||
defaultDeviceMappingHandler,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { rest } from "msw";
|
||||
|
||||
import createMockDeviceUser from "__mocks__/deviceUserMock";
|
||||
import createMockDeviceUser, {
|
||||
createMockDeviceSoftwareResponse,
|
||||
} from "__mocks__/deviceUserMock";
|
||||
import createMockHost from "__mocks__/hostMock";
|
||||
import createMockLicense from "__mocks__/licenseMock";
|
||||
import createMockMacAdmins from "__mocks__/macAdminsMock";
|
||||
import { baseUrl } from "test/test-utils";
|
||||
import { IDeviceUserResponse } from "interfaces/host";
|
||||
import { IGetDeviceSoftwareResponse } from "services/entities/device_user";
|
||||
|
||||
export const defaultDeviceHandler = rest.get(
|
||||
baseUrl("/device/:token"),
|
||||
@@ -64,3 +67,10 @@ export const defaultMacAdminsHandler = rest.get(
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const customDeviceSoftwareHandler = (
|
||||
overrides?: Partial<IGetDeviceSoftwareResponse>
|
||||
) =>
|
||||
rest.get(baseUrl("/device/:token/software"), (req, res, context) => {
|
||||
return res(context.json(createMockDeviceSoftwareResponse(overrides)));
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { render, RenderOptions, RenderResult } from "@testing-library/react";
|
||||
import type { UserEvent } from "@testing-library/user-event/dist/types/setup/setup";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
@@ -151,3 +152,22 @@ export const renderWithSetup = (component: JSX.Element) => {
|
||||
...render(component),
|
||||
};
|
||||
};
|
||||
|
||||
const DEFAULT_MOCK_ROUTER: InjectedRouter = {
|
||||
push: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
goBack: jest.fn(),
|
||||
goForward: jest.fn(),
|
||||
go: jest.fn(),
|
||||
setRouteLeaveHook: jest.fn(),
|
||||
isActive: jest.fn(),
|
||||
createHref: jest.fn(),
|
||||
createPath: jest.fn(),
|
||||
};
|
||||
|
||||
export const createMockRouter = (overrides?: Partial<InjectedRouter>) => {
|
||||
return {
|
||||
...DEFAULT_MOCK_ROUTER,
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import software from "interfaces/software";
|
||||
|
||||
const API_VERSION = "latest";
|
||||
|
||||
export default {
|
||||
|
||||
Reference in New Issue
Block a user