Skip to content

Commit

Permalink
feat: add playbook runs page
Browse files Browse the repository at this point in the history
Closes #1354

fix: improve playbook run as suggested in the PR

feat: add duration and date columns to the runs list
  • Loading branch information
mainawycliffe committed Oct 3, 2023
1 parent 3400e95 commit 53da9c3
Show file tree
Hide file tree
Showing 12 changed files with 670 additions and 34 deletions.
58 changes: 41 additions & 17 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ import { ConnectionsPage } from "./pages/Settings/ConnectionsPage";
import { EventQueueStatusPage } from "./pages/Settings/EventQueueStatus";
import { FeatureFlagsPage } from "./pages/Settings/FeatureFlagsPage";
import { LogBackendsPage } from "./pages/Settings/LogBackendsPage";
import { PlaybookSettingsPage } from "./pages/Settings/PlaybookSettingsPage";
import { PlaybooksListPage } from "./pages/playbooks/PlaybooksList";
import { UsersPage } from "./pages/UsersPage";
import { ConfigDetailsInsightsPage } from "./pages/config/ConfigDetailsInsightsPage";
import { ConfigInsightsPage } from "./pages/config/ConfigInsightsList";
import { HealthPage } from "./pages/health";
import PlaybookRunsPage from "./pages/playbooks/PlaybookRuns";
import PlaybookRunsDetailsPage from "./pages/playbooks/PlaybookRunsDetails";
import { features } from "./services/permissions/features";
import { stringSortHelper } from "./utils/common";

Expand Down Expand Up @@ -108,6 +110,13 @@ const navigation: NavigationItems = [
icon: LogsIcon,
featureName: features.logs,
resourceName: tables.database
},
{
name: "Playbooks",
href: "/playbooks",
icon: FaTasks,
featureName: features.playbooks,
resourceName: tables.database
}
];

Expand Down Expand Up @@ -188,13 +197,6 @@ const settingsNav: SettingsNavigationItems = {
icon: FaTasks,
featureName: features["settings.event_queue_status"],
resourceName: tables.database
},
{
name: "Playbooks",
href: "/settings/playbooks",
icon: FaTasks,
featureName: features["settings.playbooks"],
resourceName: tables.database
}
].sort((v1, v2) => stringSortHelper(v1.name, v2.name))
};
Expand Down Expand Up @@ -258,6 +260,37 @@ export function IncidentManagerRoutes({ sidebar }: { sidebar: ReactNode }) {
/>
</Route>

<Route path="playbooks" element={sidebar}>
<Route
index
element={withAccessCheck(
<PlaybooksListPage />,
tables.database,
"read"
)}
/>

<Route path="runs">
<Route
index
element={withAccessCheck(
<PlaybookRunsPage />,
tables.database,
"read"
)}
/>

<Route
path=":id"
element={withAccessCheck(
<PlaybookRunsDetailsPage />,
tables.database,
"read"
)}
/>
</Route>
</Route>

<Route path="settings" element={sidebar}>
<Route
path="connections"
Expand Down Expand Up @@ -313,15 +346,6 @@ export function IncidentManagerRoutes({ sidebar }: { sidebar: ReactNode }) {
)}
/>

<Route
path="playbooks"
element={withAccessCheck(
<PlaybookSettingsPage />,
tables.database,
"read"
)}
/>

{settingsNav.submenu
.filter((v) => (v as SchemaResourceType).table)
.map((x) => {
Expand Down
16 changes: 16 additions & 0 deletions src/api/services/playbooks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { PlaybookRunWithActions } from "../../components/Playbooks/Runs/PlaybookRunsActions";
import { PlaybookRun } from "../../components/Playbooks/Runs/PlaybookRunsList";
import {
NewPlaybookSpec,
PlaybookSpec,
Expand All @@ -20,6 +22,20 @@ export async function getPlaybookSpec(id: string) {
return res.data ?? undefined;
}

export async function getPlaybookRuns() {
const res = await IncidentCommander.get<PlaybookRun[] | null>(
`/playbook_runs?select=*,created_by(${AVATAR_INFO}),playbooks(id,name),component:components(id,name,icon)&order=created_at.desc`
);
return res.data ?? [];
}

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(*)`
);
return res.data?.[0] ?? undefined;
}

export async function createPlaybookSpec(spec: NewPlaybookSpec) {
const res = await IncidentCommander.post<PlaybookSpec>("/playbooks", spec);
return res.data;
Expand Down
44 changes: 44 additions & 0 deletions src/components/Playbooks/Runs/PlaybookRunsActionItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { formatLongDate, relativeDateTime } from "../../../utils/date";
import { PlaybookRunAction } from "./PlaybookRunsActions";
import PlaybookRunsStatus from "./PlaybookRunsStatus";

type PlaybookRunsActionItemProps = {
action: PlaybookRunAction;
onClick?: () => void;
isSelected?: boolean;
};

export default function PlaybookRunsActionItem({
action,
onClick = () => {},
isSelected = false
}: PlaybookRunsActionItemProps) {
return (
<div
role="button"
onClick={onClick}
key={action.id}
className="flex flex-row items-center justify-between px-4 py-2 bg-white border border-gray-200 hover:bg-gray-200 rounded cursor-pointer"
>
<div className="flex flex-col">
<div className="text-sm font-medium text-gray-600">{action.name}</div>
<div className={`text-xs flex flex-row gap-1 items-center`}>
<PlaybookRunsStatus status={action.status} />
</div>
</div>
<div className="flex flex-col">
<div className="text-sm font-medium text-gray-600">
{" "}
{!isSelected
? relativeDateTime(action.start_time!, action.end_time)
: relativeDateTime(action.start_time!)}
</div>
{isSelected && (
<div className="text-sm font-medium text-gray-500">
{relativeDateTime(action.end_time!)}
</div>
)}
</div>
</div>
);
}
86 changes: 86 additions & 0 deletions src/components/Playbooks/Runs/PlaybookRunsActions.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { StoryObj } from "@storybook/react";
import PlaybookRunsActions, {
PlaybookRunWithActions
} from "./PlaybookRunsActions";

export default {
title: "PlaybookRunsActions",
component: PlaybookRunsActions
};

const mockPlaybookRun: PlaybookRunWithActions = {
id: "1",
playbook_id: "1",
status: "completed",
created_at: "2021-08-19T07:00:00.000Z",
start_time: "2021-08-19T07:00:00.000Z",
end_time: "2021-08-19T07:00:00.000Z",
created_by: {
id: "1",
name: "John Doe",
email: ""
},
component_id: "1",
parameters: {},
component: {
id: "1",
name: "Topology 1",
icon: "TopologyIcon"
},
actions: [
{
id: "1",
name: "Action 1",
status: "completed",
end_time: "2021-08-19T07:00:00.000Z",
start_time: "2021-08-19T07:00:00.000Z",
playbook_run_id: "1",
result: `You can also use variant modifiers to target media queries like responsive breakpoints, dark mode, prefers-reduced-motion, and more. For example, use md:font-serif to apply the font-serif utility at only medium screen sizes and above.
You can also use variant modifiers to target media queries like responsive breakpoints, dark mode, prefers-reduced-motion, and more. For example, use md:font-serif to apply the font-serif utility at only medium screen sizes and above.
You can also use variant modifiers to target media queries like responsive breakpoints, dark mode, prefers-reduced-motion, and more. For example, use md:font-serif to apply the font-serif utility at only medium screen sizes and above.
You can also use variant modifiers to target media queries like responsive breakpoints, dark mode, prefers-reduced-motion, and more. For example, use md:font-serif to apply the font-serif utility at only medium screen sizes and above.`,
error: ""
},
{
id: "2",
name: "Action 2",
status: "completed",
end_time: "2021-08-19T07:00:00.000Z",
start_time: "2021-08-19T07:00:00.000Z",
playbook_run_id: "1",
result: `You can also use variant modifiers to target media queries like responsive breakpoints, dark mode, prefers-reduced-motion, and more. For example, use md:font-serif to apply the font-serif utility at only medium screen sizes and above.
You can also use variant modifiers to target media queries like responsive breakpoints, dark mode, prefers-reduced-motion, and more. For example, use md:font-serif to apply the font-serif utility at only medium screen sizes and above.`,
error: ""
},
{
id: "3",
name: "Action 3",
status: "pending",
end_time: undefined,
error: "",
result: "",
start_time: "2021-08-19T07:00:00.000Z",
playbook_run_id: "1"
},
{
id: "4",
name: "Action 4",
status: "running",
end_time: undefined,
error: "",
result: "",
start_time: "2021-08-19T07:00:00.000Z",
playbook_run_id: "1"
}
]
};

type Story = StoryObj<typeof PlaybookRunsActions>;

export const Default: Story = {
render: () => <PlaybookRunsActions data={mockPlaybookRun} />
};
138 changes: 138 additions & 0 deletions src/components/Playbooks/Runs/PlaybookRunsActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { ReactNode, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import { relativeDateTime } from "../../../utils/date";
import { DescriptionCard } from "../../DescriptionCard";
import { Icon } from "../../Icon";
import PlaybookRunsActionItem from "./PlaybookRunsActionItem";
import { PlaybookRun, PlaybookRunStatus } from "./PlaybookRunsList";
import PlaybookRunsStatus from "./PlaybookRunsStatus";
import { Avatar } from "../../Avatar";

export type PlaybookRunAction = {
id: string;
name: string;
status: PlaybookRunStatus;
playbook_run_id: string;
start_time?: string;
end_time?: string;
result?: {
stdout?: string;
};
error?: string;
};

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

type PlaybookRunActionsProps = {
data: PlaybookRunWithActions;
};

export default function PlaybookRunsActions({ data }: PlaybookRunActionsProps) {
const [selectedAction, setSelectedAction] = useState<PlaybookRunAction>();

const headerContent = useMemo(
() =>
new Map<string, ReactNode>([
[
"Triggered By",
data.created_by ? <Avatar user={data.created_by} /> : null
],
[
"Playbook",
<Link
className="text-blue-500 hover:underline"
to={`/playbooks/${data.playbook_id}`}
>
{data.playbooks?.name}
</Link>
],
[
"Component",
<Link
className="text-blue-500 hover:underline"
to={`/topologies/${data.component_id}`}
>
<Icon name={data.component?.icon} className="mr-1 h-5 w-5" />
{data.component?.name}
</Link>
],
[
"Status",
<div className="flex flex-row gap-2 items-center">
<PlaybookRunsStatus status={data.status} />
</div>
],
["Start Time", relativeDateTime(data.start_time)],
["Duration", relativeDateTime(data.start_time, data.end_time)]
]),
[data]
);

return (
<div className="flex flex-col flex-1 gap-4">
<div className="flex flex-row gap-2 px-4 py-2">
<DescriptionCard
className="flex flex-wrap gap-2"
title="Playbook"
contentClassName="font-semibold text-gray-600"
columns={5}
labelStyle="top"
items={Array.from(headerContent.entries()).map(([label, value]) => ({
label,
value
}))}
/>
</div>
<div className="flex flex-row h-full">
<div className="flex flex-col w-[30rem] h-full px-2 border-r border-gray-200">
<div className="flex flex-row items-center justify-between px-4 py-2 mb-2 border-b border-gray-100">
<div className="font-semibold text-gray-600">Actions</div>
</div>
<div className="flex flex-col flex-1 overflow-y-auto gap-2">
{data.actions.map((action) => (
<PlaybookRunsActionItem
isSelected={selectedAction?.id === action.id}
key={action.id}
action={action}
onClick={() => setSelectedAction(action)}
/>
))}
</div>
</div>
<div className="flex flex-col flex-1 h-full font-mono px-4 py-2 text-white bg-gray-700">
{selectedAction && (
<>
<div className="flex flex-col gap-2 py-6">
<div className="flex flex-row gap-4 items-center">
<PlaybookRunsStatus
status={selectedAction.status}
hideStatusLabel
className="flex-shrink-0"
/>
<div className="font-semibold text-xl">
{selectedAction?.name}
</div>
</div>
<div className="text-sm">
Duration:{" "}
{relativeDateTime(
selectedAction.start_time!,
selectedAction.end_time
)}
</div>
</div>

<div className="flex flex-col gap-2 whitespace-pre-wrap ">
{selectedAction.result?.stdout
? selectedAction.result.stdout
: selectedAction.error || "No result"}
</div>
</>
)}
</div>
</div>
</div>
);
}
Loading

0 comments on commit 53da9c3

Please sign in to comment.