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:
Gabriel Hernandez
2024-06-11 12:56:50 +01:00
committed by GitHub
parent 30553cecc3
commit de0562a686
14 changed files with 195 additions and 26 deletions
+41
View File
@@ -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 {
+5 -3
View File
@@ -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;
};
}
};
@@ -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} />
@@ -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}
+5
View File
@@ -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,
+11 -1
View File
@@ -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)));
});
+20
View File
@@ -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,
};
};
-2
View File
@@ -1,5 +1,3 @@
import software from "interfaces/software";
const API_VERSION = "latest";
export default {