Skip to content

Commit

Permalink
WD-7494 - Make action logs table responsive (canonical#1661)
Browse files Browse the repository at this point in the history
* Make action logs table responsive.
  • Loading branch information
huwshimi authored Nov 28, 2023
1 parent 5a2000f commit bdbf382
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 141 deletions.
6 changes: 6 additions & 0 deletions src/components/Status/Status.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ describe("Status", () => {
expect(status).toHaveClass("is-middle");
});

it("can be inline", () => {
render(<Status status="middle" inline />);
const status = screen.getByText("middle");
expect(status).toHaveClass("status-icon--inline");
});

it("can display a count", () => {
render(<Status status="middle" count={23} />);
expect(screen.getByText(/(23)/)).toBeInTheDocument();
Expand Down
3 changes: 3 additions & 0 deletions src/components/Status/Status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,6 +15,7 @@ const Status = ({
status = "unknown",
children,
count,
inline,
useIcon = true,
actionsLogs = false,
className = null,
Expand All @@ -27,6 +29,7 @@ const Status = ({
<span
className={classNames(className, {
"status-icon": useIcon,
"status-icon--inline": inline,
[`is-${status.toLowerCase()}`]: useIcon && status,
})}
>
Expand Down
76 changes: 26 additions & 50 deletions src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,25 +144,14 @@ describe("Action Logs", () => {
it("requests the action logs data on load", async () => {
renderComponent(<ActionLogs />, { 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");
Expand All @@ -175,25 +164,14 @@ describe("Action Logs", () => {
it("fails gracefully if app does not exist in model data", async () => {
renderComponent(<ActionLogs />, { 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");
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -283,15 +250,7 @@ describe("Action Logs", () => {
renderComponent(<ActionLogs />, { 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.
Expand Down Expand Up @@ -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(<ActionLogs />, { 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(<ActionLogs />, { path, url, state });
const showOutputBtns = await screen.findAllByTestId("show-output");
Expand Down
152 changes: 80 additions & 72 deletions src/pages/EntityDetails/Model/Logs/ActionLogs/ActionLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
CodeSnippet,
CodeSnippetBlockAppearance,
ContextualMenu,
Icon,
Modal,
ModularTable,
Tooltip,
Expand Down Expand Up @@ -39,7 +40,6 @@ type TableRows = TableRow[];
type RowCells = {
application: ReactNode;
completed?: ReactNode;
controls?: ReactNode;
id: string;
message: ReactNode;
sortData: {
Expand All @@ -49,11 +49,10 @@ type RowCells = {
status: string;
};
status: ReactNode;
taskId: string;
};

type TableRow = RowCells & {
subRows: (RowCells & { controls: ReactNode })[];
subRows: RowCells[];
};

type ApplicationData = {
Expand Down Expand Up @@ -182,7 +181,6 @@ const generateApplicationRow = (
},
status: <Status status={actionData.status} useIcon actionsLogs />,
subRows: [],
taskId: "",
};
};

Expand Down Expand Up @@ -277,15 +275,18 @@ export default function ActionLogs() {
);
delete actionFullDetails?.output?.["return-code"];
if (!actionFullDetails) return;
const stdout = (actionFullDetails.log || []).map((m, i) => (
<span className="action-logs__stdout" key={i}>
{m.message}
</span>
));
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) => (
<span className="action-logs__stdout" key={i}>
{m.message}
</span>
))
: [];
const stderr = hasSterr ? actionFullDetails.message : "";
const completedDate = new Date(actionData.completed);
const name = actionData.action.receiver.replace(
/unit-(.+)-(\d+)/,
Expand All @@ -298,17 +299,72 @@ export default function ActionLogs() {
<span>{name}</span>
</>
),
id: "",
status: <Status status={actionData.status} useIcon actionsLogs />,
taskId: actionData.action.tag.split("-")[1],
message: (
<>
{outputType !== Output.STDERR ? <span>{stdout}</span> : null}
{outputType !== Output.STDOUT ? (
<span className="action-logs__stderr">{stderr}</span>
id: actionData.action.tag.split("-")[1],
status: (
<div className="u-flex u-flex--gap-small">
<div className="u-flex-shrink u-truncate">
<Status status={actionData.status} useIcon actionsLogs inline />
</div>
{Object.keys(actionFullDetails?.output ?? {}).length > 0 ? (
<div>
<Button
onClick={() => setModalDetails(actionFullDetails.output)}
data-testid="show-output"
dense
hasIcon
>
<Icon name="code" />
</Button>
</div>
) : null}
</>
</div>
),
message:
hasStdout || hasSterr ? (
<div className="u-flex">
<div>
{outputType !== Output.STDERR ? <span>{stdout}</span> : null}
{outputType !== Output.STDOUT ? (
<span className="action-logs__stderr">{stderr}</span>
) : null}
</div>
<div>
<ContextualMenu
hasToggleIcon
toggleProps={{
dense: true,
appearance: "base",
"aria-label": Label.OUTPUT,
}}
links={[
{
children: Output.ALL,
onClick: () =>
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,
},
]}
/>
</div>
</div>
) : null,
// Sometimes the log gets returned with a date of "0001-01-01T00:00:00Z".
completed:
completedDate.getFullYear() === 1 ? (
Expand All @@ -321,45 +377,6 @@ export default function ActionLogs() {
{formatFriendlyDateToNow(actionData.completed)}
</Tooltip>
),
controls: (
<div className="entity-details__action-buttons">
<ContextualMenu
hasToggleIcon
toggleProps={{ dense: true }}
toggleLabel={
outputType === Output.ALL ? Label.OUTPUT : outputType
}
links={[
{
children: Output.ALL,
onClick: () =>
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 && (
<Button
onClick={() => setModalDetails(actionFullDetails.output)}
data-testid="show-output"
dense
>
Result
</Button>
)}
</div>
),
sortData: {
application: name,
completed: completedDate.getTime(),
Expand Down Expand Up @@ -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,
},
],
[]
);
Expand Down
Loading

0 comments on commit bdbf382

Please sign in to comment.