diff --git a/foo b/foo deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ui/.betterer.results b/ui/.betterer.results index 572d24cc13..7eb68d235e 100644 --- a/ui/.betterer.results +++ b/ui/.betterer.results @@ -7,7 +7,7 @@ exports[`stricter compilation`] = { [162, 4, 36, "Object is possibly \'null\'.", "1039669632"] ], "src/app/App.tsx:3872899616": [ - [21, 7, 25, "Could not find a declaration file for module \'@maas-ui/maas-ui-shared\'. \'/home/caleb/Projects/maas-ui/shared/dist/index.js\' implicitly has an \'any\' type.\\n Try \`npm install @types/maas-ui__maas-ui-shared\` if it exists or add a new declaration (.d.ts) file containing \`declare module \'@maas-ui/maas-ui-shared\';\`", "1778274862"], + [21, 7, 25, "Could not find a declaration file for module \'@maas-ui/maas-ui-shared\'. \'/Users/kit/src/canonical/local/maas-ui/shared/dist/index.js\' implicitly has an \'any\' type.\\n Try \`npm install @types/maas-ui__maas-ui-shared\` if it exists or add a new declaration (.d.ts) file containing \`declare module \'@maas-ui/maas-ui-shared\';\`", "1778274862"], [188, 17, 17, "Object is possibly \'null\'.", "2133029343"], [193, 7, 7, "Variable \'content\' is used before being assigned.", "3716929964"] ], @@ -44,7 +44,7 @@ exports[`stricter compilation`] = { [75, 4, 5, "Argument of type \'boolean | undefined\' is not assignable to parameter of type \'boolean\'.\\n Type \'undefined\' is not assignable to type \'boolean\'.", "195688512"] ], "src/app/base/components/LegacyLink/LegacyLink.tsx:2706551295": [ - [4, 52, 25, "Could not find a declaration file for module \'@maas-ui/maas-ui-shared\'. \'/home/caleb/Projects/maas-ui/shared/dist/index.js\' implicitly has an \'any\' type.\\n Try \`npm install @types/maas-ui__maas-ui-shared\` if it exists or add a new declaration (.d.ts) file containing \`declare module \'@maas-ui/maas-ui-shared\';\`", "1778274862"] + [4, 52, 25, "Could not find a declaration file for module \'@maas-ui/maas-ui-shared\'. \'/Users/kit/src/canonical/local/maas-ui/shared/dist/index.js\' implicitly has an \'any\' type.\\n Try \`npm install @types/maas-ui__maas-ui-shared\` if it exists or add a new declaration (.d.ts) file containing \`declare module \'@maas-ui/maas-ui-shared\';\`", "1778274862"] ], "src/app/base/components/NotificationGroup/Notification/Notification.tsx:122297593": [ [26, 26, 12, "Argument of type \'Notification | null\' is not assignable to parameter of type \'Notification\'.\\n Type \'null\' is not assignable to type \'Notification\'.\\n Type \'null\' is not assignable to type \'Model\'.", "148512008"], @@ -86,7 +86,7 @@ exports[`stricter compilation`] = { [214, 7, 11, "Property \'placeholder\' is missing in type \'{ disabledTags: { id: number; name: string; }[]; initialSelected: { id: number; name: string; }[]; tags: { id: number; name: string; }[]; }\' but required in type \'Props\'.", "3766634306"] ], "src/app/base/components/TagSelector/TagSelector.tsx:2755544058": [ - [1, 18, 51, "Could not find a declaration file for module \'@canonical/react-components/dist/components/Field\'. \'/home/caleb/Projects/maas-ui/node_modules/@canonical/react-components/dist/components/Field/index.js\' implicitly has an \'any\' type.\\n Try \`npm install @types/canonical__react-components\` if it exists or add a new declaration (.d.ts) file containing \`declare module \'@canonical/react-components/dist/components/Field\';\`", "1535046059"], + [1, 18, 51, "Could not find a declaration file for module \'@canonical/react-components/dist/components/Field\'. \'/Users/kit/src/canonical/local/maas-ui/node_modules/@canonical/react-components/dist/components/Field/index.js\' implicitly has an \'any\' type.\\n Try \`npm install @types/canonical__react-components\` if it exists or add a new declaration (.d.ts) file containing \`declare module \'@canonical/react-components/dist/components/Field\';\`", "1535046059"], [37, 2, 12, "Binding element \'allowNewTags\' implicitly has an \'any\' type.", "3979358209"], [38, 2, 6, "Binding element \'filter\' implicitly has an \'any\' type.", "1355726373"], [39, 2, 12, "Binding element \'selectedTags\' implicitly has an \'any\' type.", "2698915821"], @@ -298,7 +298,7 @@ exports[`stricter compilation`] = { [50, 6, 8, "Type \'(values: MarkBrokenFormValues) => void\' is not assignable to type \'(...args: unknown[]) => void\'.\\n Types of parameters \'values\' and \'args\' are incompatible.\\n Type \'unknown\' is not assignable to type \'MarkBrokenFormValues\'.", "1301647696"], [53, 27, 10, "Property \'markBroken\' does not exist on type \'{ fetch: ActionCreatorWithPreparedPayload<[params?: any], { params: any; }, string, never, { model: any; method: string; }>; create: ActionCreatorWithPreparedPayload<[params?: any], { params: any; }, string, never, { ...; }>; update: ActionCreatorWithPreparedPayload<...>; delete: ActionCreatorWithPreparedPayload<......\'.", "3004692879"] ], - "src/app/machines/components/ActionFormWrapper/TestForm/TestForm.test.tsx:3198696153": [ + "src/app/machines/components/ActionFormWrapper/TestForm/TestForm.test.tsx:3774525721": [ [95, 6, 66, "Cannot invoke an object which is possibly \'undefined\'.", "4166470712"], [99, 10, 15, "Argument of type \'{ enableSSH: boolean; scripts: Scripts[]; scriptInputs: { \\"internet-connectivity\\": string; }; }\' is not assignable to parameter of type \'FormEvent<{}>\'.\\n Object literal may only specify known properties, and \'enableSSH\' does not exist in type \'FormEvent<{}>\'.", "2907345984"], [216, 6, 66, "Cannot invoke an object which is possibly \'undefined\'.", "4166470712"], @@ -346,23 +346,23 @@ exports[`stricter compilation`] = { [40, 28, 3, "Property \'get\' does not exist on type \'{ fetch: ActionCreatorWithPreparedPayload<[params?: any], { params: any; }, string, never, { model: any; method: string; }>; create: ActionCreatorWithPreparedPayload<[params?: any], { params: any; }, string, never, { ...; }>; update: ActionCreatorWithPreparedPayload<...>; delete: ActionCreatorWithPreparedPayload<......\'.", "193411891"], [105, 42, 10, "Property \'checkPower\' does not exist on type \'{ fetch: ActionCreatorWithPreparedPayload<[params?: any], { params: any; }, string, never, { model: any; method: string; }>; create: ActionCreatorWithPreparedPayload<[params?: any], { params: any; }, string, never, { ...; }>; update: ActionCreatorWithPreparedPayload<...>; delete: ActionCreatorWithPreparedPayload<......\'.", "953482588"] ], - "src/app/machines/views/MachineDetails/MachineSummary/MachineSummary.tsx:564355202": [ + "src/app/machines/views/MachineDetails/MachineSummary/MachineSummary.tsx:2439231865": [ [42, 28, 3, "Property \'get\' does not exist on type \'{ fetch: ActionCreatorWithPreparedPayload<[params?: any], { params: any; }, string, never, { model: any; method: string; }>; create: ActionCreatorWithPreparedPayload<[params?: any], { params: any; }, string, never, { ...; }>; update: ActionCreatorWithPreparedPayload<...>; delete: ActionCreatorWithPreparedPayload<......\'.", "193411891"] ], "src/app/machines/views/MachineDetails/MachineSummary/NumaCard/NumaCard.test.tsx:2502469861": [ [33, 27, 10, "Property \'numa_nodes\' does not exist on type \'Machine\'.\\n Property \'numa_nodes\' does not exist on type \'BaseMachine\'.", "3857696382"] ], - "src/app/machines/views/MachineDetails/MachineSummary/OverviewCard/CpuCard/CpuCard.tsx:461133352": [ - [17, 18, 36, "Element implicitly has an \'any\' type because expression of type \'string\' can\'t be used to index type \'MachineDetails\'.\\n No index signature with a parameter of type \'string\' was found on type \'MachineDetails\'.", "830072625"], - [146, 18, 7, "Type \'string | null\' is not assignable to type \'string | undefined\'.\\n Type \'null\' is not assignable to type \'string | undefined\'.", "1236122734"] - ], - "src/app/machines/views/MachineDetails/MachineSummary/OverviewCard/MemoryCard/MemoryCard.tsx:2120332270": [ - [16, 18, 36, "Element implicitly has an \'any\' type because expression of type \'string\' can\'t be used to index type \'MachineDetails\'.\\n No index signature with a parameter of type \'string\' was found on type \'MachineDetails\'.", "830072625"], - [119, 16, 7, "Type \'string | null\' is not assignable to type \'string | undefined\'.\\n Type \'null\' is not assignable to type \'string | undefined\'.", "1236122734"] - ], - "src/app/machines/views/MachineDetails/MachineSummary/OverviewCard/StorageCard/StorageCard.tsx:3582856219": [ - [17, 18, 36, "Element implicitly has an \'any\' type because expression of type \'string\' can\'t be used to index type \'MachineDetails\'.\\n No index signature with a parameter of type \'string\' was found on type \'MachineDetails\'.", "830072625"], - [127, 16, 7, "Type \'string | null\' is not assignable to type \'string | undefined\'.\\n Type \'null\' is not assignable to type \'string | undefined\'.", "1236122734"] + "src/app/machines/views/MachineDetails/MachineSummary/TestResults/TestResults.tsx:4182062532": [ + [18, 18, 36, "Element implicitly has an \'any\' type because expression of type \'string\' can\'t be used to index type \'MachineDetails\'.\\n No index signature with a parameter of type \'string\' was found on type \'MachineDetails\'.", "830072625"], + [37, 9, 36, "Element implicitly has an \'any\' type because expression of type \'string\' can\'t be used to index type \'MachineDetails\'.\\n No index signature with a parameter of type \'string\' was found on type \'MachineDetails\'.", "830072625"], + [52, 15, 36, "Element implicitly has an \'any\' type because expression of type \'string\' can\'t be used to index type \'MachineDetails\'.\\n No index signature with a parameter of type \'string\' was found on type \'MachineDetails\'.", "830072625"], + [57, 9, 36, "Element implicitly has an \'any\' type because expression of type \'string\' can\'t be used to index type \'MachineDetails\'.\\n No index signature with a parameter of type \'string\' was found on type \'MachineDetails\'.", "830072625"], + [58, 10, 36, "Element implicitly has an \'any\' type because expression of type \'string\' can\'t be used to index type \'MachineDetails\'.\\n No index signature with a parameter of type \'string\' was found on type \'MachineDetails\'.", "830072625"], + [74, 15, 36, "Element implicitly has an \'any\' type because expression of type \'string\' can\'t be used to index type \'MachineDetails\'.\\n No index signature with a parameter of type \'string\' was found on type \'MachineDetails\'.", "830072625"], + [75, 16, 36, "Element implicitly has an \'any\' type because expression of type \'string\' can\'t be used to index type \'MachineDetails\'.\\n No index signature with a parameter of type \'string\' was found on type \'MachineDetails\'.", "830072625"], + [80, 9, 36, "Element implicitly has an \'any\' type because expression of type \'string\' can\'t be used to index type \'MachineDetails\'.\\n No index signature with a parameter of type \'string\' was found on type \'MachineDetails\'.", "830072625"], + [95, 15, 36, "Element implicitly has an \'any\' type because expression of type \'string\' can\'t be used to index type \'MachineDetails\'.\\n No index signature with a parameter of type \'string\' was found on type \'MachineDetails\'.", "830072625"], + [120, 14, 7, "Type \'string | null\' is not assignable to type \'string | undefined\'.\\n Type \'null\' is not assignable to type \'string | undefined\'.", "1236122734"] ], "src/app/machines/views/MachineList/MachineListHeader/MachineListHeader.tsx:539216384": [ [104, 10, 17, "Type \'(action: MachineAction, deselect?: boolean | undefined) => void\' is not assignable to type \'SetSelectedAction\'.\\n Types of parameters \'action\' and \'action\' are incompatible.\\n Type \'SelectedAction | null\' is not assignable to type \'MachineAction\'.\\n Type \'null\' is not assignable to type \'MachineAction\'.", "167402512"], diff --git a/ui/src/app/base/enum.ts b/ui/src/app/base/enum.ts index 46b8629496..ccbb7792e2 100644 --- a/ui/src/app/base/enum.ts +++ b/ui/src/app/base/enum.ts @@ -68,7 +68,7 @@ export const scriptStatus = { export enum HardwareType { Node = 0, - Cpu = 1, + CPU = 1, Memory = 2, Storage = 3, Network = 4, diff --git a/ui/src/app/machines/components/ActionFormWrapper/TestForm/TestForm.test.tsx b/ui/src/app/machines/components/ActionFormWrapper/TestForm/TestForm.test.tsx index 35f0a408ce..50ab36e3bb 100644 --- a/ui/src/app/machines/components/ActionFormWrapper/TestForm/TestForm.test.tsx +++ b/ui/src/app/machines/components/ActionFormWrapper/TestForm/TestForm.test.tsx @@ -162,7 +162,7 @@ describe("TestForm", () => { state.scripts.items = [ networkScript, scriptsFactory({ - hardware_type: HardwareType.Cpu, + hardware_type: HardwareType.CPU, type: ScriptType.Testing, }), scriptsFactory({ diff --git a/ui/src/app/machines/views/MachineDetails/MachineSummary/MachineSummary.tsx b/ui/src/app/machines/views/MachineDetails/MachineSummary/MachineSummary.tsx index 94bd6e8cbd..e025a7ac52 100644 --- a/ui/src/app/machines/views/MachineDetails/MachineSummary/MachineSummary.tsx +++ b/ui/src/app/machines/views/MachineDetails/MachineSummary/MachineSummary.tsx @@ -52,7 +52,7 @@ const MachineSummary = ({ setSelectedAction }: Props): JSX.Element => { - + ); }; diff --git a/ui/src/app/machines/views/MachineDetails/MachineSummary/NetworkCard/NetworkCard.test.tsx b/ui/src/app/machines/views/MachineDetails/MachineSummary/NetworkCard/NetworkCard.test.tsx index e354de452f..129cf21baf 100644 --- a/ui/src/app/machines/views/MachineDetails/MachineSummary/NetworkCard/NetworkCard.test.tsx +++ b/ui/src/app/machines/views/MachineDetails/MachineSummary/NetworkCard/NetworkCard.test.tsx @@ -11,6 +11,7 @@ import { machineState as machineStateFactory, rootState as rootStateFactory, vlanState as vlanStateFactory, + testStatus as testStatusFactory, } from "testing/factories"; import NetworkCard from "./NetworkCard"; import type { RootState } from "app/store/root/types"; @@ -39,7 +40,9 @@ describe("NetworkCard", () => { } + component={() => ( + + )} /> @@ -79,7 +82,9 @@ describe("NetworkCard", () => { } + component={() => ( + + )} /> @@ -144,7 +149,9 @@ describe("NetworkCard", () => { } + component={() => ( + + )} /> @@ -155,4 +162,132 @@ describe("NetworkCard", () => { expect(wrapper.find("Table").at(2).find("tbody TableRow").length).toBe(2); expect(wrapper.find("Table").at(3).find("tbody TableRow").length).toBe(1); }); + + describe("test results", () => { + it("renders a link with a count of passed tests", () => { + const machine = machineDetailsFactory({ + system_id: "abc123", + }); + + machine.network_test_status = testStatusFactory({ + passed: 2, + }); + state.machine.items = [machine]; + + const store = mockStore(state); + const wrapper = mount( + + + + + + ); + + expect( + wrapper.find("[data-test='tests']").childAt(0).find("Button").text() + ).toEqual("2"); + }); + + it("renders a link with a count of pending and running tests", () => { + const machine = machineDetailsFactory({ + system_id: "abc123", + }); + + machine.network_test_status = testStatusFactory({ + running: 1, + pending: 2, + }); + state.machine.items = [machine]; + + const store = mockStore(state); + const wrapper = mount( + + + + + + ); + + expect( + wrapper.find("[data-test='tests']").childAt(0).find("Button").text() + ).toEqual("3"); + }); + + it("renders a link with a count of failed tests", () => { + const machine = machineDetailsFactory({ + system_id: "abc123", + }); + machine.network_test_status = testStatusFactory({ + failed: 5, + }); + state.machine.items = [machine]; + + const store = mockStore(state); + const wrapper = mount( + + + + + + ); + + expect( + wrapper.find("[data-test='tests']").childAt(0).find("Button").text() + ).toEqual("5"); + }); + + it("renders a results link", () => { + const machine = machineDetailsFactory({ + system_id: "abc123", + }); + machine.network_test_status = testStatusFactory({ + failed: 5, + }); + state.machine.items = [machine]; + + const store = mockStore(state); + const wrapper = mount( + + + + + + ); + + expect( + wrapper.find("[data-test='tests']").childAt(1).find("Button").text() + ).toContain("View results"); + }); + + it("renders a test storage link if no tests run", () => { + const machine = machineDetailsFactory({ + system_id: "abc123", + }); + machine.network_test_status = testStatusFactory(); + state.machine.items = [machine]; + + const store = mockStore(state); + const wrapper = mount( + + + + + + ); + + expect( + wrapper.find("[data-test='tests']").childAt(0).find("Button").text() + ).toContain("Test network"); + }); + }); }); diff --git a/ui/src/app/machines/views/MachineDetails/MachineSummary/NetworkCard/NetworkCard.tsx b/ui/src/app/machines/views/MachineDetails/MachineSummary/NetworkCard/NetworkCard.tsx index 5b45695106..2ba4bf06a6 100644 --- a/ui/src/app/machines/views/MachineDetails/MachineSummary/NetworkCard/NetworkCard.tsx +++ b/ui/src/app/machines/views/MachineDetails/MachineSummary/NetworkCard/NetworkCard.tsx @@ -3,14 +3,17 @@ import { useDispatch, useSelector } from "react-redux"; import { Link } from "react-router-dom"; import React, { Fragment, useEffect } from "react"; +import NetworkCardTable from "./NetworkCardTable"; +import { SetSelectedAction } from "../MachineSummary"; +import TestResults from "../TestResults"; +import { HardwareType } from "app/base/enum"; import { actions as fabricActions } from "app/store/fabric"; -import { actions as vlanActions } from "app/store/vlan"; import fabricSelectors from "app/store/fabric/selectors"; import machineSelectors from "app/store/machine/selectors"; -import vlanSelectors from "app/store/vlan/selectors"; import type { Machine, NetworkInterface } from "app/store/machine/types"; +import { actions as vlanActions } from "app/store/vlan"; +import vlanSelectors from "app/store/vlan/selectors"; import type { RootState } from "app/store/root/types"; -import NetworkCardTable from "./NetworkCardTable"; type InterfaceGroup = { firmwareVersion: string; @@ -21,6 +24,7 @@ type InterfaceGroup = { type Props = { id: Machine["system_id"]; + setSelectedAction: SetSelectedAction; }; /** @@ -92,7 +96,7 @@ const groupInterfaces = (interfaces: NetworkInterface[]): InterfaceGroup[] => { return sortedGroups; }; -const NetworkCard = ({ id }: Props): JSX.Element => { +const NetworkCard = ({ id, setSelectedAction }: Props): JSX.Element => { const dispatch = useDispatch(); const machine = useSelector((state: RootState) => machineSelectors.getById(state, id) @@ -144,6 +148,12 @@ const NetworkCard = ({ id }: Props): JSX.Element => { Information about tagged traffic can be seen in the{" "} Network tab. + + ); } else { diff --git a/ui/src/app/machines/views/MachineDetails/MachineSummary/OverviewCard/CpuCard/CpuCard.tsx b/ui/src/app/machines/views/MachineDetails/MachineSummary/OverviewCard/CpuCard/CpuCard.tsx index 47725b10bd..30bfa641ed 100644 --- a/ui/src/app/machines/views/MachineDetails/MachineSummary/OverviewCard/CpuCard/CpuCard.tsx +++ b/ui/src/app/machines/views/MachineDetails/MachineSummary/OverviewCard/CpuCard/CpuCard.tsx @@ -1,26 +1,16 @@ import pluralize from "pluralize"; import React from "react"; -import { Link } from "react-router-dom"; - -import { Button, Icon, ICONS, Tooltip } from "@canonical/react-components"; import type { SetSelectedAction } from "../../MachineSummary"; -import { useSendAnalytics } from "app/base/hooks"; import type { MachineDetails } from "app/store/machine/types"; import { HardwareType } from "app/base/enum"; +import TestResults from "../../TestResults"; type Props = { machine: MachineDetails; setSelectedAction: SetSelectedAction; }; -const hasTestsRun = (machine: MachineDetails, scriptType: string) => { - const testObj = machine[`${scriptType}_test_status`]; - return ( - testObj.passed + testObj.pending + testObj.running + testObj.failed > 0 - ); -}; - // Get the subtext for the CPU card. Only nodes commissioned after // MAAS 2.4 will have the CPU speed. const getCPUSubtext = (machine: MachineDetails) => { @@ -39,146 +29,27 @@ const getCPUSubtext = (machine: MachineDetails) => { return text; }; -const CpuCard = ({ machine, setSelectedAction }: Props): JSX.Element => { - const sendAnalytics = useSendAnalytics(); - - const testsTabUrl = `/machine/${machine.system_id}/tests`; - - return ( - <> -
-
- CPU - {machine.architecture} -
-

- {getCPUSubtext(machine)} -

-

- {machine.metadata.cpu_model || "Unknown model"} -

+const CpuCard = ({ machine, setSelectedAction }: Props): JSX.Element => ( + <> +
+
+ CPU + {machine.architecture}
- -
-
    - {machine.cpu_test_status.passed ? ( -
  • - -
  • - ) : null} - - {machine.cpu_test_status.pending + machine.cpu_test_status.running > - 0 ? ( -
  • - -
  • - ) : null} - - {machine.cpu_test_status.failed > 0 ? ( -
  • - -
  • - ) : null} - - {hasTestsRun(machine, "cpu") ? ( -
  • - -
  • - ) : ( -
  • - - - - - -
  • - )} -
-
- - ); -}; +

+ {getCPUSubtext(machine)} +

+

+ {machine.metadata.cpu_model || "Unknown model"} +

+
+ + + +); export default CpuCard; diff --git a/ui/src/app/machines/views/MachineDetails/MachineSummary/OverviewCard/MemoryCard/MemoryCard.tsx b/ui/src/app/machines/views/MachineDetails/MachineSummary/OverviewCard/MemoryCard/MemoryCard.tsx index b753775bba..99886a17f1 100644 --- a/ui/src/app/machines/views/MachineDetails/MachineSummary/OverviewCard/MemoryCard/MemoryCard.tsx +++ b/ui/src/app/machines/views/MachineDetails/MachineSummary/OverviewCard/MemoryCard/MemoryCard.tsx @@ -1,156 +1,28 @@ import React from "react"; -import { Link } from "react-router-dom"; - -import { Button, Icon, ICONS, Tooltip } from "@canonical/react-components"; import type { SetSelectedAction } from "../../MachineSummary"; -import { useSendAnalytics } from "app/base/hooks"; -import type { MachineDetails } from "app/store/machine/types"; +import TestResults from "../../TestResults"; import { HardwareType } from "app/base/enum"; +import type { MachineDetails } from "app/store/machine/types"; type Props = { machine: MachineDetails; setSelectedAction: SetSelectedAction; }; -const hasTestsRun = (machine: MachineDetails, scriptType: string) => { - const testObj = machine[`${scriptType}_test_status`]; - return ( - testObj.passed + testObj.pending + testObj.running + testObj.failed > 0 - ); -}; - -const MemoryCard = ({ machine, setSelectedAction }: Props): JSX.Element => { - const sendAnalytics = useSendAnalytics(); - - const testsTabUrl = `/machine/${machine.system_id}/tests`; - - return ( - <> -
- Memory -

{machine.memory ? machine.memory + " GiB" : "Unknown"}

-
- -
-
    - {machine.memory_test_status.passed > 0 ? ( -
  • - -
  • - ) : null} - - {machine.memory_test_status.pending + - machine.memory_test_status.running > - 0 ? ( -
  • - -
  • - ) : null} - - {machine.memory_test_status.failed > 0 ? ( -
  • - -
  • - ) : null} - - {hasTestsRun(machine, "memory") ? ( -
  • - -
  • - ) : ( -
  • - - - -
  • - )} -
-
- - ); -}; +const MemoryCard = ({ machine, setSelectedAction }: Props): JSX.Element => ( + <> +
+ Memory +

{machine.memory ? machine.memory + " GiB" : "Unknown"}

+
+ + + +); export default MemoryCard; diff --git a/ui/src/app/machines/views/MachineDetails/MachineSummary/OverviewCard/StorageCard/StorageCard.tsx b/ui/src/app/machines/views/MachineDetails/MachineSummary/OverviewCard/StorageCard/StorageCard.tsx index d0bf956dc3..8e29354f3f 100644 --- a/ui/src/app/machines/views/MachineDetails/MachineSummary/OverviewCard/StorageCard/StorageCard.tsx +++ b/ui/src/app/machines/views/MachineDetails/MachineSummary/OverviewCard/StorageCard/StorageCard.tsx @@ -1,164 +1,36 @@ import pluralize from "pluralize"; import React from "react"; -import { Link } from "react-router-dom"; - -import { Button, Icon, ICONS, Tooltip } from "@canonical/react-components"; import type { SetSelectedAction } from "../../MachineSummary"; -import { useSendAnalytics } from "app/base/hooks"; -import type { MachineDetails } from "app/store/machine/types"; +import TestResults from "../../TestResults"; import { HardwareType } from "app/base/enum"; +import type { MachineDetails } from "app/store/machine/types"; type Props = { machine: MachineDetails; setSelectedAction: SetSelectedAction; }; -const hasTestsRun = (machine: MachineDetails, scriptType: string) => { - const testObj = machine[`${scriptType}_test_status`]; - return ( - testObj.passed + testObj.pending + testObj.running + testObj.failed > 0 - ); -}; - -const StorageCard = ({ machine, setSelectedAction }: Props): JSX.Element => { - const sendAnalytics = useSendAnalytics(); - - const testsTabUrl = `/machine/${machine.system_id}/tests`; - - return ( - <> -
- Storage -

- {machine.storage ? `${machine.storage}GB` : "Unknown"} - {machine.storage && machine.physical_disk_count ? ( - -  over {pluralize("disk", machine.physical_disk_count, true)} - - ) : null} -

-
- -
-
    - {machine.storage_test_status.passed ? ( -
  • - -
  • - ) : null} - - {machine.storage_test_status.pending + - machine.storage_test_status.running > - 0 ? ( -
  • - -
  • - ) : null} - - {machine.storage_test_status.failed > 0 ? ( -
  • - -
  • - ) : null} - - {hasTestsRun(machine, "storage") ? ( -
  • - -
  • - ) : ( -
  • - - - -
  • - )} -
-
- - ); -}; +const StorageCard = ({ machine, setSelectedAction }: Props): JSX.Element => ( + <> +
+ Storage +

+ {machine.storage ? `${machine.storage}GB` : "Unknown"} + {machine.storage && machine.physical_disk_count ? ( + +  over {pluralize("disk", machine.physical_disk_count, true)} + + ) : null} +

+
+ + + +); export default StorageCard; diff --git a/ui/src/app/machines/views/MachineDetails/MachineSummary/TestResults/TestResults.test.tsx b/ui/src/app/machines/views/MachineDetails/MachineSummary/TestResults/TestResults.test.tsx new file mode 100644 index 0000000000..5d5453a12f --- /dev/null +++ b/ui/src/app/machines/views/MachineDetails/MachineSummary/TestResults/TestResults.test.tsx @@ -0,0 +1,160 @@ +import { mount } from "enzyme"; +import { Provider } from "react-redux"; +import { MemoryRouter } from "react-router-dom"; +import configureStore from "redux-mock-store"; +import React from "react"; + +import { + machineDetails as machineDetailsFactory, + machineState as machineStateFactory, + rootState as rootStateFactory, + testStatus as testStatusFactory, +} from "testing/factories"; +import TestResults from "./TestResults"; +import type { RootState } from "app/store/root/types"; +import { HardwareType } from "app/base/enum"; + +const mockStore = configureStore(); + +describe("TestResults", () => { + let state: RootState; + beforeEach(() => { + state = rootStateFactory({ + machine: machineStateFactory(), + }); + }); + + it("renders a link with a count of passed cpu tests", () => { + const machine = machineDetailsFactory(); + machine.cpu_test_status = testStatusFactory({ + passed: 2, + }); + state.machine.items = [machine]; + + const store = mockStore(state); + const wrapper = mount( + + + + + + ); + + expect( + wrapper.find("[data-test='tests']").childAt(0).find("Button").text() + ).toEqual("2"); + }); + + it("renders a link with a count of pending and running memory tests", () => { + const machine = machineDetailsFactory(); + machine.memory_test_status = testStatusFactory({ + running: 1, + pending: 2, + }); + state.machine.items = [machine]; + + const store = mockStore(state); + const wrapper = mount( + + + + + + ); + + expect( + wrapper.find("[data-test='tests']").childAt(0).find("Button").text() + ).toEqual("3"); + }); + + it("renders a link with a count of failed storage tests", () => { + const machine = machineDetailsFactory(); + machine.storage_test_status = testStatusFactory({ + failed: 5, + }); + state.machine.items = [machine]; + + const store = mockStore(state); + const wrapper = mount( + + + + + + ); + + expect( + wrapper.find("[data-test='tests']").childAt(0).find("Button").text() + ).toEqual("5"); + }); + + it("renders a results link", () => { + const machine = machineDetailsFactory(); + machine.cpu_test_status = testStatusFactory({ + failed: 5, + }); + state.machine.items = [machine]; + + const store = mockStore(state); + const wrapper = mount( + + + + + + ); + + expect( + wrapper.find("[data-test='tests']").childAt(1).find("Button").text() + ).toContain("View results"); + }); + + it("renders a test network link if no tests run", () => { + const machine = machineDetailsFactory(); + machine.network_test_status = testStatusFactory(); + state.machine.items = [machine]; + + const store = mockStore(state); + const wrapper = mount( + + + + + + ); + + expect( + wrapper.find("[data-test='tests']").childAt(0).find("Button").text() + ).toContain("Test network"); + }); +}); diff --git a/ui/src/app/machines/views/MachineDetails/MachineSummary/TestResults/TestResults.tsx b/ui/src/app/machines/views/MachineDetails/MachineSummary/TestResults/TestResults.tsx new file mode 100644 index 0000000000..fd0cba8424 --- /dev/null +++ b/ui/src/app/machines/views/MachineDetails/MachineSummary/TestResults/TestResults.tsx @@ -0,0 +1,158 @@ +import React from "react"; +import { Link } from "react-router-dom"; + +import { Button, Icon, ICONS, Tooltip } from "@canonical/react-components"; + +import type { SetSelectedAction } from ".."; +import { capitaliseFirst } from "app/utils"; +import { useSendAnalytics } from "app/base/hooks"; +import type { MachineDetails } from "app/store/machine/types"; +import { HardwareType } from "app/base/enum"; + +type Props = { + machine: MachineDetails; + hardwareType: HardwareType; + setSelectedAction: SetSelectedAction; +}; + +const hasTestsRun = (machine: MachineDetails, scriptType: string) => { + const testObj = machine[`${scriptType}_test_status`]; + return ( + testObj.passed + testObj.pending + testObj.running + testObj.failed > 0 + ); +}; + +const TestResults = ({ + machine, + hardwareType, + setSelectedAction, +}: Props): JSX.Element => { + const sendAnalytics = useSendAnalytics(); + + const testsTabUrl = `/machine/${machine.system_id}/tests`; + const scriptType = HardwareType[hardwareType]?.toLowerCase(); + + return ( +
+
    + {machine[`${scriptType}_test_status`].passed ? ( +
  • + +
  • + ) : null} + + {machine[`${scriptType}_test_status`].pending + + machine[`${scriptType}_test_status`].running > + 0 ? ( +
  • + +
  • + ) : null} + + {machine[`${scriptType}_test_status`].failed > 0 ? ( +
  • + +
  • + ) : null} + + {hasTestsRun(machine, scriptType) ? ( +
  • + +
  • + ) : ( +
  • + + + +
  • + )} +
+
+ ); +}; + +export default TestResults; diff --git a/ui/src/app/machines/views/MachineDetails/MachineSummary/TestResults/index.ts b/ui/src/app/machines/views/MachineDetails/MachineSummary/TestResults/index.ts new file mode 100644 index 0000000000..cac5cc0dea --- /dev/null +++ b/ui/src/app/machines/views/MachineDetails/MachineSummary/TestResults/index.ts @@ -0,0 +1 @@ +export { default } from "./TestResults";