From bdbf382637e0be5121b8bc8b124d764be2330622 Mon Sep 17 00:00:00 2001 From: huwshimi Date: Wed, 29 Nov 2023 09:53:25 +1100 Subject: [PATCH] WD-7494 - Make action logs table responsive (#1661) * Make action logs table responsive. --- src/components/Status/Status.test.tsx | 6 + src/components/Status/Status.tsx | 3 + .../Model/Logs/ActionLogs/ActionLogs.test.tsx | 76 +++------ .../Model/Logs/ActionLogs/ActionLogs.tsx | 152 +++++++++--------- .../Model/Logs/ActionLogs/_action-logs.scss | 38 +++-- src/scss/_utils.scss | 14 +- src/scss/custom/_status_icons.scss | 4 + 7 files changed, 152 insertions(+), 141 deletions(-) diff --git a/src/components/Status/Status.test.tsx b/src/components/Status/Status.test.tsx index 5e046c54d..a81eb5a89 100644 --- a/src/components/Status/Status.test.tsx +++ b/src/components/Status/Status.test.tsx @@ -25,6 +25,12 @@ describe("Status", () => { expect(status).toHaveClass("is-middle"); }); + it("can be inline", () => { + render(); + const status = screen.getByText("middle"); + expect(status).toHaveClass("status-icon--inline"); + }); + it("can display a count", () => { render(); expect(screen.getByText(/(23)/)).toBeInTheDocument(); diff --git a/src/components/Status/Status.tsx b/src/components/Status/Status.tsx index 457b138f5..e4dc52792 100644 --- a/src/components/Status/Status.tsx +++ b/src/components/Status/Status.tsx @@ -5,6 +5,7 @@ import type { PropsWithChildren } from "react"; type Props = { status?: string; count?: number | null; + inline?: boolean; useIcon?: boolean; actionsLogs?: boolean; className?: string | null; @@ -14,6 +15,7 @@ const Status = ({ status = "unknown", children, count, + inline, useIcon = true, actionsLogs = false, className = null, @@ -27,6 +29,7 @@ const Status = ({ diff --git a/src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.test.tsx b/src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.test.tsx index a682bd5f2..16ab6aa1f 100644 --- a/src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.test.tsx +++ b/src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.test.tsx @@ -144,25 +144,14 @@ describe("Action Logs", () => { it("requests the action logs data on load", async () => { renderComponent(, { path, url, state }); const expected = [ - ["easyrsa", "1/list-disks", "completed", "", "", "", ""], - [ - "└easyrsa/0", - "", - "completed", - "2", - "log message 1", - "over 1 year ago", - "Output", - "Result", - ], + ["easyrsa", "1/list-disks", "completed", "", ""], + ["└easyrsa/0", "2", "completed", "log message 1", "over 1 year ago"], [ "└easyrsa/1", - "", - "completed", "3", + "completed", "log message 1log message 2error message", "over 1 year ago", - "Output", ], ]; const rows = await screen.findAllByRole("row"); @@ -175,25 +164,14 @@ describe("Action Logs", () => { it("fails gracefully if app does not exist in model data", async () => { renderComponent(, { path, url, state }); const expected = [ - ["easyrsa", "1/list-disks", "completed", "", "", "", ""], - [ - "└easyrsa/0", - "", - "completed", - "2", - "log message 1", - "over 1 year ago", - "Output", - "Result", - ], + ["easyrsa", "1/list-disks", "completed", "", ""], + ["└easyrsa/0", "2", "completed", "log message 1", "over 1 year ago"], [ "└easyrsa/1", - "", - "completed", "3", + "completed", "log message 1log message 2error message", "over 1 year ago", - "Output", ], ]; const rows = await screen.findAllByRole("row"); @@ -236,23 +214,12 @@ describe("Action Logs", () => { const expected = [ [ "└easyrsa/1", - "", - "completed", "3", + "completed", "log message 1log message 2error message", "1 day ago", - "Output", - ], - [ - "└easyrsa/0", - "", - "completed", - "2", - "log message 1", - "over 1 year ago", - "Output", - "Result", ], + ["└easyrsa/0", "2", "completed", "log message 1", "over 1 year ago"], ]; const tableBody = await screen.findAllByRole("rowgroup"); const rows = await within(tableBody[1]).findAllByRole("row"); @@ -283,15 +250,7 @@ describe("Action Logs", () => { renderComponent(, { path, url, state }); const expected = [ ["easyrsa", "1/list-disks", "completed", "", "", "", ""], - [ - "└easyrsa/0", - "", - "completed", - "2", - "log message 1", - "Unknown", - "Output", - ], + ["└easyrsa/0", "2", "completed", "log message 1", "Unknown"], ]; const rows = await screen.findAllByRole("row"); // Start at row 1 because row 0 is the header row. @@ -324,6 +283,23 @@ describe("Action Logs", () => { expect(within(rows[2]).getByText("error message")).toBeInTheDocument(); }); + it("does not display a toggle when there is neither STOUT or STDERR", async () => { + const mockActionResults = actionResultsFactory.build({ + results: [ + actionResultFactory.build({ + log: undefined, + status: "completed", + }), + ], + }); + jest.spyOn(juju, "queryActionsList").mockResolvedValue(mockActionResults); + renderComponent(, { path, url, state }); + const rows = await screen.findAllByRole("row"); + expect( + within(rows[2]).queryByRole("button", { name: Label.OUTPUT }) + ).not.toBeInTheDocument(); + }); + it("only shows the action result button when there is a result", async () => { renderComponent(, { path, url, state }); const showOutputBtns = await screen.findAllByTestId("show-output"); diff --git a/src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.tsx b/src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.tsx index 6eb709ff4..614e71693 100644 --- a/src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.tsx +++ b/src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.tsx @@ -7,6 +7,7 @@ import { CodeSnippet, CodeSnippetBlockAppearance, ContextualMenu, + Icon, Modal, ModularTable, Tooltip, @@ -39,7 +40,6 @@ type TableRows = TableRow[]; type RowCells = { application: ReactNode; completed?: ReactNode; - controls?: ReactNode; id: string; message: ReactNode; sortData: { @@ -49,11 +49,10 @@ type RowCells = { status: string; }; status: ReactNode; - taskId: string; }; type TableRow = RowCells & { - subRows: (RowCells & { controls: ReactNode })[]; + subRows: RowCells[]; }; type ApplicationData = { @@ -182,7 +181,6 @@ const generateApplicationRow = ( }, status: , subRows: [], - taskId: "", }; }; @@ -277,15 +275,18 @@ export default function ActionLogs() { ); delete actionFullDetails?.output?.["return-code"]; if (!actionFullDetails) return; - const stdout = (actionFullDetails.log || []).map((m, i) => ( - - {m.message} - - )); - const stderr = - actionFullDetails.status === "failed" - ? actionFullDetails.message - : ""; + const hasStdout = + actionFullDetails.log && actionFullDetails.log.length > 0; + const hasSterr = + actionFullDetails.status === "failed" && !!actionFullDetails.message; + const stdout = hasStdout + ? actionFullDetails.log.map((m, i) => ( + + {m.message} + + )) + : []; + const stderr = hasSterr ? actionFullDetails.message : ""; const completedDate = new Date(actionData.completed); const name = actionData.action.receiver.replace( /unit-(.+)-(\d+)/, @@ -298,17 +299,72 @@ export default function ActionLogs() { {name} ), - id: "", - status: , - taskId: actionData.action.tag.split("-")[1], - message: ( - <> - {outputType !== Output.STDERR ? {stdout} : null} - {outputType !== Output.STDOUT ? ( - {stderr} + id: actionData.action.tag.split("-")[1], + status: ( +
+
+ +
+ {Object.keys(actionFullDetails?.output ?? {}).length > 0 ? ( +
+ +
) : null} - +
), + message: + hasStdout || hasSterr ? ( +
+
+ {outputType !== Output.STDERR ? {stdout} : null} + {outputType !== Output.STDOUT ? ( + {stderr} + ) : null} +
+
+ + handleOutputSelect(actionData.action.tag, Output.ALL), + }, + { + children: Output.STDOUT, + onClick: () => + handleOutputSelect( + actionData.action.tag, + Output.STDOUT + ), + disabled: !stdout.length, + }, + { + children: Output.STDERR, + onClick: () => + handleOutputSelect( + actionData.action.tag, + Output.STDERR + ), + disabled: !stderr.length, + }, + ]} + /> +
+
+ ) : null, // Sometimes the log gets returned with a date of "0001-01-01T00:00:00Z". completed: completedDate.getFullYear() === 1 ? ( @@ -321,45 +377,6 @@ export default function ActionLogs() { {formatFriendlyDateToNow(actionData.completed)} ), - controls: ( -
- - handleOutputSelect(actionData.action.tag, Output.ALL), - }, - { - children: Output.STDOUT, - onClick: () => - handleOutputSelect(actionData.action.tag, Output.STDOUT), - disabled: !stdout.length, - }, - { - children: Output.STDERR, - onClick: () => - handleOutputSelect(actionData.action.tag, Output.STDERR), - disabled: !stderr.length, - }, - ]} - /> - {Object.keys(actionFullDetails?.output ?? {}).length > 0 && ( - - )} -
- ), sortData: { application: name, completed: completedDate.getTime(), @@ -393,30 +410,21 @@ export default function ActionLogs() { sortType: "basic", }, { - Header: "status", + Header: "result", accessor: "status", sortType: tableSort.bind(null, "status"), }, - { - Header: "task id", - accessor: "taskId", - sortType: "basic", - }, { Header: "action message", accessor: "message", sortType: tableSort.bind(null, "message"), }, { - Header: "completion time", + Header: "completed", accessor: "completed", sortType: tableSort.bind(null, "completed"), sortInverted: true, }, - { - accessor: "controls", - disableSortBy: true, - }, ], [] ); diff --git a/src/pages/EntityDetails/Model/Logs/ActionLogs/_action-logs.scss b/src/pages/EntityDetails/Model/Logs/ActionLogs/_action-logs.scss index aa46c393a..79bca5694 100644 --- a/src/pages/EntityDetails/Model/Logs/ActionLogs/_action-logs.scss +++ b/src/pages/EntityDetails/Model/Logs/ActionLogs/_action-logs.scss @@ -1,21 +1,33 @@ @import "../../../../../scss/settings"; @import "vanilla-framework/scss/vanilla"; +@import "../../../../../scss/breakpoints"; @mixin action-logs { .entity-details__action-logs { - table { - // Operation ID column - th:nth-child(2) { + td, + th { + // Completion time. + &:nth-child(5) { width: 15%; } - // log messages column - th:nth-child(5), - // Completion time column. - th:nth-child(6), - // control buttons column - th:nth-child(7) { - width: 20%; + @include mobile-and-medium { + // Completion time. + &:nth-child(5) { + display: none; + } + } + + @include mobile { + // Status. + &:nth-child(3) { + width: 25%; + } + + // Log messages. + &:nth-child(4) { + display: none; + } } } } @@ -27,12 +39,6 @@ .action-logs__stderr { color: $color-negative; } - - .entity-details__action-buttons { - & > * { - margin-right: $sp-small !important; - } - } } @include action-logs; diff --git a/src/scss/_utils.scss b/src/scss/_utils.scss index b2ff32363..bb0fef425 100644 --- a/src/scss/_utils.scss +++ b/src/scss/_utils.scss @@ -35,13 +35,21 @@ gap: $sp-medium; } + &--gap-small { + gap: $sp-small; + } + &--block { flex: 1 auto; } -} -.u-flex-grow { - flex-grow: 1; + &-grow { + flex-grow: 1; + } + + &-shrink { + flex-shrink: 1; + } } // Extend has-icon to work outside of buttons diff --git a/src/scss/custom/_status_icons.scss b/src/scss/custom/_status_icons.scss index 4d0a6275a..9c25e5f73 100644 --- a/src/scss/custom/_status_icons.scss +++ b/src/scss/custom/_status_icons.scss @@ -11,6 +11,10 @@ top: -6px; } + &--inline { + display: inline; + } + &.is-spinner::before { content: none; }