Skip to content

Commit

Permalink
Merge pull request #1444 from flanksource/1436-playbook-actions-resul…
Browse files Browse the repository at this point in the history
…ts-improvements

fix: decode logs in results object and remove overflow of caused by text
  • Loading branch information
moshloop authored Oct 18, 2023
2 parents ad5bedb + d6e1b27 commit d47b62f
Show file tree
Hide file tree
Showing 15 changed files with 204 additions and 87 deletions.
18 changes: 14 additions & 4 deletions src/api/services/playbooks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { PlaybookRunWithActions } from "../../components/Playbooks/Runs/PlaybookRunsActions";
import { PlaybookRunAction } from "../../components/Playbooks/Runs/PlaybookRunsSidePanel";
import { PlaybookRunWithActions } from "../../components/Playbooks/Runs/Actions/PlaybookRunsActions";
import {
PlaybookRun,
PlaybookRunAction
} from "../../components/Playbooks/Runs/PlaybookRunTypes";
import { SubmitPlaybookRunFormValues } from "../../components/Playbooks/Runs/SubmitPlaybookRunForm";
import {
NewPlaybookSpec,
Expand Down Expand Up @@ -70,7 +73,14 @@ export async function getPlaybookToRunForResource(

export async function getPlaybookRun(id: string) {
const res = await IncidentCommander.get<PlaybookRunWithActions[] | null>(
`/playbook_runs?id=eq.${id}&select=*,created_by(${AVATAR_INFO}),playbooks(id,name),component:components(id,name,icon),actions:playbook_run_actions(*)`
`/playbook_runs?id=eq.${id}&select=*,created_by(${AVATAR_INFO}),playbooks(id,name),component:components(id,name,icon),actions:playbook_run_actions(id,name,status,start_time,end_time)`
);
return res.data?.[0] ?? undefined;
}

export async function getPlaybookRunActionById(id: string) {
const res = await IncidentCommander.get<PlaybookRunAction[] | null>(
`/playbook_run_actions?id=eq.${id}&select=*`
);
return res.data?.[0] ?? undefined;
}
Expand All @@ -94,7 +104,7 @@ export async function getPlaybookRuns({
const pagingParams = `&limit=${pageSize}&offset=${pageIndex * pageSize}`;

const res = await resolve(
ConfigDB.get<PlaybookRunAction[] | null>(
ConfigDB.get<PlaybookRun[] | null>(
`/playbook_runs?select=*,playbooks(id,name),component:components(id,name,icon),check:checks(id,name,icon),config:config_items(id,name,type,config_class)&order=created_at.desc${componentParamString}&${configParamString}${pagingParams}}`,
{
headers: {
Expand Down
22 changes: 22 additions & 0 deletions src/components/Playbooks/Runs/Actions/PlaybookRunActionFetch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useQuery } from "@tanstack/react-query";
import { getPlaybookRunActionById } from "../../../../api/services/playbooks";
import TableSkeletonLoader from "../../../SkeletonLoader/TableSkeletonLoader";
import PlaybooksRunActionsResults from "./PlaybooksActionsResults";

type Props = {
playbookRunActionId: string;
};

export default function PlaybookRunActionFetch({ playbookRunActionId }: Props) {
const { data: action, isLoading } = useQuery({
queryKey: ["playbookRunAction", playbookRunActionId],
queryFn: () => getPlaybookRunActionById(playbookRunActionId),
enabled: !!playbookRunActionId
});

if (isLoading || !action) {
return <TableSkeletonLoader />;
}

return <PlaybooksRunActionsResults action={action} />;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { relativeDateTime } from "../../../utils/date";
import { PlaybookRunAction } from "./PlaybookRunsSidePanel";
import PlaybookRunsStatus from "./PlaybookRunsStatus";
import { relativeDateTime } from "../../../../utils/date";
import { PlaybookRunAction } from "../PlaybookRunTypes";
import PlaybookRunsStatus from "../PlaybookRunsStatus";

type PlaybookRunsActionItemProps = {
action: PlaybookRunAction;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { ReactNode, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import { relativeDateTime } from "../../../utils/date";
import { Avatar } from "../../Avatar";
import { Icon } from "../../Icon";
import { relativeDateTime } from "../../../../utils/date";
import { Avatar } from "../../../Avatar";
import { Icon } from "../../../Icon";
import { PlaybookRun, PlaybookRunAction } from "../PlaybookRunTypes";
import PlaybookRunsStatus from "./../PlaybookRunsStatus";
import PlaybookRunActionFetch from "./PlaybookRunActionFetch";
import PlaybookRunsActionItem from "./PlaybookRunsActionItem";
import { PlaybookRunAction } from "./PlaybookRunsSidePanel";
import PlaybookRunsStatus from "./PlaybookRunsStatus";
import PlaybooksRunActionsResults from "./PlaybooksActionsResults";

export type PlaybookRunWithActions = PlaybookRunAction & {
export type PlaybookRunWithActions = PlaybookRun & {
actions: PlaybookRunAction[];
};

Expand Down Expand Up @@ -97,10 +97,10 @@ export default function PlaybookRunsActions({ data }: PlaybookRunActionsProps) {
))}
</div>
</div>
<div className="flex flex-col flex-1 h-full font-mono px-4 py-2 text-white bg-gray-700">
<div className="flex flex-col flex-1 h-full font-mono px-4 py-2 text-white bg-gray-700 overflow-hidden">
{selectedAction && (
<div className="flex flex-col flex-1 w-full overflow-auto gap-2 whitespace-pre-wrap ">
<PlaybooksRunActionsResults action={selectedAction} />
<div className="flex flex-col flex-1 w-full overflow-x-hidden overflow-y-auto gap-2 whitespace-pre-wrap break-all">
<PlaybookRunActionFetch playbookRunActionId={selectedAction.id} />
</div>
)}
</div>
Expand Down
40 changes: 40 additions & 0 deletions src/components/Playbooks/Runs/Actions/PlaybooksActionsResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Convert from "ansi-to-html";
import { PlaybookRunAction } from "../PlaybookRunTypes";

const convert = new Convert();

type Props = {
action: Pick<PlaybookRunAction, "result" | "error">;
className?: string;
};

export default function PlaybooksRunActionsResults({
action,
className = "whitespace-pre-wrap break-all"
}: Props) {
const { result, error } = action;

if (!result && !error) {
return <>No result</>;
}

if (action.error) {
return <pre className={className}>{action.error}</pre>;
}

if (result?.stdout) {
return <pre className={className}>{result.stdout}</pre>;
}

if (result?.logs) {
const html = convert.toHtml(result.logs);

return (
<pre className={className} dangerouslySetInnerHTML={{ __html: html }} />
);
}

const json = JSON.stringify(result, null, 2);

return <pre className={className}>{json}</pre>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { render, screen } from "@testing-library/react";
import PlaybooksRunActionsResults from "../PlaybooksActionsResults";

describe("PlaybooksRunActionsResults", () => {
it("renders 'No result' when result is falsy", () => {
render(<PlaybooksRunActionsResults action={{}} />);
expect(screen.getByText("No result")).toBeInTheDocument();
});

it("renders stdout when result has stdout", () => {
const action = { result: { stdout: "Hello, world!" } };
render(<PlaybooksRunActionsResults action={action} />);
expect(screen.getByText("Hello, world!")).toBeInTheDocument();
});

it("renders logs when result has logs", () => {
const action = { result: { logs: "Hello, world!" } };
render(<PlaybooksRunActionsResults action={action} />);
expect(screen.getByText("Hello, world!")).toBeInTheDocument();
});

it("renders JSON when result has neither stdout nor logs", () => {
const action = { result: { foo: "bar" } };
render(<PlaybooksRunActionsResults action={action} />);
expect(
screen.getByText('{ "foo": "bar" }', {
exact: false
})
).toBeInTheDocument();
});

it("applies the className prop to the pre element", () => {
const action = { result: { stdout: "Hello, world!" } };
render(
<PlaybooksRunActionsResults action={action} className="text-red-500" />
);
expect(screen.getByText("Hello, world!")).toHaveClass("text-red-500");
});

it("renders error when action has error", () => {
const action = { error: "Something went wrong" };
render(<PlaybooksRunActionsResults action={action} />);
expect(
screen.getByText("Something went wrong", {
exact: false
})
).toBeInTheDocument();
});
});
50 changes: 50 additions & 0 deletions src/components/Playbooks/Runs/PlaybookRunTypes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ConfigItem } from "../../../api/services/configs";
import { User } from "../../../api/services/users";
import { Topology } from "../../../context/TopologyPageContext";
import { HealthCheck } from "../../../types/healthChecks";
import { PlaybookSpec } from "../Settings/PlaybookSpecsTable";

export type PlaybookRunStatus =
| "scheduled"
| "running"
| "cancelled"
| "completed"
| "failed"
| "pending";

export type PlaybookRunAction = {
id: string;
name: string;
status: PlaybookRunStatus;
playbook_run_id: string;
start_time: string;
scheduled_time?: string;
end_time?: string;
result?: {
stdout?: string;
logs?: string;
[key: string]: unknown;
};
error?: string;
};

export type PlaybookRun = {
id: string;
playbook_id?: string;
status: PlaybookRunStatus;
start_time: string;
scheduled_time?: string;
end_time?: string;
created_at?: string;
created_by?: User;
check_id?: string;
config_id?: string;
component_id?: string;
parameters?: Record<string, unknown>;
agent_id?: string;
/* relationships */
playbooks?: PlaybookSpec;
component?: Pick<Topology, "id" | "name" | "icon">;
check?: Pick<HealthCheck, "id" | "name" | "icon">;
config?: Pick<ConfigItem, "id" | "name" | "type" | "config_class">;
};
8 changes: 4 additions & 4 deletions src/components/Playbooks/Runs/PlaybookRunsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { Avatar } from "../../Avatar";
import { DateCell } from "../../ConfigViewer/columns";
import { DataTable, PaginationOptions } from "../../DataTable";
import { Icon } from "../../Icon";
import { PlaybookRunAction, PlaybookRunStatus } from "./PlaybookRunsSidePanel";
import { PlaybookRun, PlaybookRunStatus } from "./PlaybookRunTypes";
import PlaybookRunsStatus from "./PlaybookRunsStatus";

const playbookRunsTableColumns: ColumnDef<PlaybookRunAction>[] = [
const playbookRunsTableColumns: ColumnDef<PlaybookRun>[] = [
{
header: "Name",
accessorKey: "name",
Expand Down Expand Up @@ -86,7 +86,7 @@ const playbookRunsTableColumns: ColumnDef<PlaybookRunAction>[] = [
];

type Props = {
data: PlaybookRunAction[];
data: PlaybookRun[];
isLoading?: boolean;
pagination?: PaginationOptions;
} & Omit<React.HTMLProps<HTMLDivElement>, "data">;
Expand All @@ -100,7 +100,7 @@ export default function PlaybookRunsTable({
const navigate = useNavigate();

const onRowClick = useCallback(
(row: PlaybookRunAction) => {
(row: PlaybookRun) => {
navigate(`/playbooks/runs/${row.id}`);
},
[navigate]
Expand Down
45 changes: 3 additions & 42 deletions src/components/Playbooks/Runs/PlaybookRunsSidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import { useAtom } from "jotai";
import { useEffect, useMemo, useState } from "react";
import { AiOutlineTeam } from "react-icons/ai";
import { useNavigate } from "react-router-dom";
import { ConfigItem } from "../../../api/services/configs";
import { getPlaybookRuns } from "../../../api/services/playbooks";
import { Topology } from "../../../context/TopologyPageContext";
import { HealthCheck } from "../../../types/healthChecks";
import { relativeDateTime } from "../../../utils/date";
import PillBadge from "../../Badge/PillBadge";
import CollapsiblePanel from "../../CollapsiblePanel";
Expand All @@ -16,8 +13,7 @@ import { InfiniteTable } from "../../InfiniteTable/InfiniteTable";
import TextSkeletonLoader from "../../SkeletonLoader/TextSkeletonLoader";
import { refreshButtonClickedTrigger } from "../../SlidingSideBar";
import Title from "../../Title/title";
import { PlaybookSpec } from "../Settings/PlaybookSpecsTable";
import { User } from "../../../api/services/users";
import { PlaybookRun } from "./PlaybookRunTypes";

type TopologySidePanelProps = {
panelType: "topology";
Expand All @@ -34,48 +30,13 @@ type Props = {
onCollapsedStateChange?: (isClosed: boolean) => void;
};

export type PlaybookRunStatus =
| "scheduled"
| "running"
| "cancelled"
| "completed"
| "failed"
| "pending";

export type PlaybookRunAction = {
id: string;
name?: string;
status: PlaybookRunStatus;
playbook_run_id?: string;
start_time: string;
end_time?: string;
result?: {
stdout?: string;
[key: string]: unknown;
};
error?: string;
playbooks?: PlaybookSpec;
playbook_id?: string;
created_at?: string;
created_by?: User;
check_id?: string;
config_id?: string;
component_id?: string;
parameters?: Record<string, unknown>;
agent_id?: string;
component?: Pick<Topology, "id" | "name" | "icon">;
check?: Pick<HealthCheck, "id" | "name" | "icon">;
config?: Pick<ConfigItem, "id" | "name" | "type" | "config_class">;
};

const runsColumns: ColumnDef<PlaybookRunAction, any>[] = [
const runsColumns: ColumnDef<PlaybookRun, any>[] = [
{
header: "Name",
id: "name",
accessorKey: "name",
size: 60,
cell: ({ row }) => {
const name = row.original.name ?? row.original.playbooks?.name;
const name = row.original.playbooks?.name;
return <span>{name}</span>;
}
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/Playbooks/Runs/PlaybookRunsStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BsFillExclamationCircleFill } from "react-icons/bs";
import { FaCheckCircle, FaClock, FaSpinner } from "react-icons/fa";
import { PlaybookRunStatus } from "./PlaybookRunsSidePanel";
import { PlaybookRunStatus } from "./PlaybookRunTypes";

const statusIconMap: Record<PlaybookRunStatus, { icon: React.ReactNode }> = {
completed: {
Expand Down
21 changes: 0 additions & 21 deletions src/components/Playbooks/Runs/PlaybooksActionsResults.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/pages/playbooks/PlaybookRunsDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
BreadcrumbRoot
} from "../../components/BreadcrumbNav";
import { SearchLayout } from "../../components/Layout";
import PlaybookRunsActions from "../../components/Playbooks/Runs/PlaybookRunsActions";
import PlaybookRunsActions from "../../components/Playbooks/Runs/Actions/PlaybookRunsActions";
import CardsSkeletonLoader from "../../components/SkeletonLoader/CardsSkeletonLoader";
import { Head } from "../../components/Head/Head";
import { playbookRunsPageTabs } from "../../components/Playbooks/Runs/PlaybookRunsPageTabs";
Expand Down
Loading

0 comments on commit d47b62f

Please sign in to comment.