Skip to content

Commit

Permalink
feat: wip
Browse files Browse the repository at this point in the history
  • Loading branch information
shahargl committed Sep 30, 2024
1 parent 6c7ae5e commit e984c7b
Show file tree
Hide file tree
Showing 15 changed files with 826 additions and 368 deletions.
15 changes: 15 additions & 0 deletions examples/workflows/incident_example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
workflow:
id: aks-example
description: aks-example
triggers:
- type: incident
events:
- updated
- created

actions:
- name: just-echo
provider:
type: console
with:
alert_message: "Hey there! I am an incident!"
6 changes: 3 additions & 3 deletions keep-ui/app/alerts/alert-name.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ export default function AlertName({
}

return (
<div className="flex items-center justify-between">
<div className="truncate" title={alert.name}>
<div className="flex items-start justify-between">
<div className="truncate whitespace-pre-wrap" title={alert.name}>
{name}
</div>
<div>
<div className="flex-shrink-0">
{(url ?? generatorURL) && (
<a href={url || generatorURL} target="_blank">
<Icon
Expand Down
301 changes: 301 additions & 0 deletions keep-ui/app/incidents/[id]/incident-workflow-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
import React, { useEffect, useState } from "react";
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
Callout,
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
Button,
Badge,
} from "@tremor/react";
import {
ExclamationTriangleIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@radix-ui/react-icons";
import Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
import { IncidentDto } from "../models";
import IncidentPagination from "../incident-pagination";
import {
PaginatedWorkflowExecutionDto,
WorkflowExecution,
} from "app/workflows/builder/types";
import { useIncidentWorkflowExecutions } from "utils/hooks/useIncidents";
import { useRouter } from "next/navigation";
import {
getIcon,
getTriggerIcon,
extractTriggerValue,
extractTriggerDetails,
} from "app/workflows/[workflow_id]/workflow-execution-table";

interface Props {
incident: IncidentDto;
}

interface Pagination {
limit: number;
offset: number;
}

const columnHelper = createColumnHelper<WorkflowExecution>();

export default function IncidentWorkflowTable({ incident }: Props) {
const router = useRouter();
const [workflowsPagination, setWorkflowsPagination] = useState<Pagination>({
limit: 20,
offset: 0,
});
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());

const { data: workflows, isLoading } = useIncidentWorkflowExecutions(
incident.id,
workflowsPagination.limit,
workflowsPagination.offset
);

const [pagination, setTablePagination] = useState({
pageIndex: workflows ? Math.ceil(workflows.offset / workflows.limit) : 0,
pageSize: workflows ? workflows.limit : 20,
});

useEffect(() => {
if (workflows && workflows.limit != pagination.pageSize) {
setWorkflowsPagination({
limit: pagination.pageSize,
offset: 0,
});
}
const currentOffset = pagination.pageSize * pagination.pageIndex;
if (workflows && workflows.offset != currentOffset) {
setWorkflowsPagination({
limit: pagination.pageSize,
offset: currentOffset,
});
}
}, [pagination, workflows]);

const columns = [
columnHelper.accessor("workflow_name", {
header: "Name",
cell: (info) => info.getValue() || "Unnamed Workflow",
}),
columnHelper.accessor("status", {
header: "Status",
cell: (info) => getIcon(info.getValue()),
}),
columnHelper.accessor("started", {
header: "Start Time",
cell: (info) => new Date(info.getValue()).toLocaleString(),
}),
columnHelper.display({
id: "execution_time",
header: "Duration",
cell: ({ row }) => {
const customFormatter = (seconds: number | null) => {
if (seconds === undefined || seconds === null) {
return "";
}

const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;

if (hours > 0) {
return `${hours} hr ${minutes}m ${remainingSeconds}s`;
} else if (minutes > 0) {
return `${minutes}m ${remainingSeconds}s`;
} else {
return `${remainingSeconds.toFixed(2)}s`;
}
};

return (
<div>{customFormatter(row.original.execution_time ?? null)}</div>
);
},
}),
columnHelper.display({
id: "triggered_by",
header: "Trigger",
cell: ({ row }) => {
const triggered_by = row.original.triggered_by;
const valueToShow = extractTriggerValue(triggered_by);

return triggered_by ? (
<div className="flex items-center gap-2">
<Button
className="px-3 py-1 bg-orange-100 text-black rounded-xl border-2 border-orange-400 inline-flex items-center gap-2 font-bold hover:bg-orange-200"
variant="secondary"
tooltip={triggered_by ?? ""}
icon={getTriggerIcon(valueToShow)}
>
<div>{valueToShow}</div>
</Button>
</div>
) : null;
},
}),
columnHelper.display({
id: "triggered_by",
header: "Trigger Details",
cell: ({ row }) => {
const triggered_by = row.original.triggered_by;
const details = extractTriggerDetails(triggered_by);
return triggered_by ? (
<div className="flex items-center gap-2 flex-wrap">
{details.map((detail, index) => (
<Badge key={index} className="px-3 py-1" color="orange">
{detail}
</Badge>
))}
</div>
) : null;
},
}),
columnHelper.display({
id: "expand",
header: "",
cell: ({ row }) => (
<Button
variant="light"
onClick={(e) => {
e.stopPropagation();
toggleRowExpansion(row.id);
}}
>
{expandedRows.has(row.id) ? <ChevronUpIcon /> : <ChevronDownIcon />}
</Button>
),
}),
];

const table = useReactTable({
columns,
data: workflows?.items ?? [],
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
pageCount: workflows ? Math.ceil(workflows.count / workflows.limit) : -1,
state: {
pagination,
},
onPaginationChange: setTablePagination,
});

const toggleRowExpansion = (rowId: string) => {
setExpandedRows((prev) => {
const newSet = new Set(prev);
if (newSet.has(rowId)) {
newSet.delete(rowId);
} else {
newSet.add(rowId);
}
return newSet;
});
};

return (
<>
{!isLoading && (workflows?.items ?? []).length === 0 && (
<Callout
className="mt-4 w-full"
title="No Workflows"
icon={ExclamationTriangleIcon}
color="orange"
>
No workflows have been executed for this incident yet.
</Callout>
)}

<Table>
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHeaderCell key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHeaderCell>
))}
</TableRow>
))}
</TableHead>
{workflows && workflows.items.length > 0 && (
<TableBody>
{table.getRowModel().rows.map((row) => (
<React.Fragment key={row.id}>
<TableRow
className="hover:bg-slate-100 cursor-pointer"
onClick={() => toggleRowExpansion(row.id)}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
{expandedRows.has(row.id) && (
<TableRow>
<TableCell colSpan={columns.length}>
<div className="p-4 bg-gray-50 flex">
<div className="w-1/2 pr-2">
<h3 className="font-bold mb-2">Logs:</h3>
<pre className="whitespace-pre-wrap">
{Array.isArray(row.original.logs)
? row.original.logs
.map((log) => JSON.stringify(log))
.join("\n")
: String(row.original.logs)}
</pre>
</div>
<div className="w-1/2 pl-2">
<h3 className="font-bold mb-2">Results:</h3>
<pre className="whitespace-pre-wrap">
{JSON.stringify(row.original.results, null, 2)}
</pre>
</div>
</div>
</TableCell>
</TableRow>
)}
</React.Fragment>
))}
</TableBody>
)}
{(isLoading || (workflows?.items ?? []).length === 0) && (
<TableBody>
{Array(pagination.pageSize)
.fill("")
.map((_, index) => (
<TableRow key={`skeleton-${index}`}>
{columns.map((_, cellIndex) => (
<TableCell key={`cell-${cellIndex}`}>
<Skeleton />
</TableCell>
))}
</TableRow>
))}
</TableBody>
)}
</Table>

<div className="mt-4 mb-8">
<IncidentPagination table={table} isRefreshAllowed={true} />
</div>
</>
);
}
7 changes: 6 additions & 1 deletion keep-ui/app/incidents/[id]/incident.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import { CiBellOn, CiChat2, CiViewTimeline } from "react-icons/ci";
import { IoIosGitNetwork } from "react-icons/io";
import { EmptyStateCard } from "@/components/ui/EmptyStateCard";
import IncidentChat from "./incident-chat";

import { Workflows } from "components/icons";
import IncidentWorkflowTable from "./incident-workflow-table";
interface Props {
incidentId: string;
}
Expand Down Expand Up @@ -50,6 +51,7 @@ export default function IncidentView({ incidentId }: Props) {
<Tab icon={CiBellOn}>Alerts</Tab>
<Tab icon={CiViewTimeline}>Timeline</Tab>
<Tab icon={IoIosGitNetwork}>Topology</Tab>
<Tab icon={Workflows}>Workflows</Tab>
<Tab icon={CiChat2}>
Chat
<Badge
Expand All @@ -75,6 +77,9 @@ export default function IncidentView({ incidentId }: Props) {
onClick={() => router.push("/topology")}
/>
</TabPanel>
<TabPanel>
<IncidentWorkflowTable incident={incident} />
</TabPanel>
<TabPanel>
<IncidentChat incident={incident} />
</TabPanel>
Expand Down
Loading

0 comments on commit e984c7b

Please sign in to comment.