diff --git a/keep-ui/app/runbooks/page.tsx b/keep-ui/app/runbooks/page.tsx
new file mode 100644
index 000000000..21853c030
--- /dev/null
+++ b/keep-ui/app/runbooks/page.tsx
@@ -0,0 +1,9 @@
+import RunbookIncidentTable from './runbook-table';
+
+export default function RunbookPage() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/keep-ui/app/runbooks/runbook-table.tsx b/keep-ui/app/runbooks/runbook-table.tsx
new file mode 100644
index 000000000..aa3ad1933
--- /dev/null
+++ b/keep-ui/app/runbooks/runbook-table.tsx
@@ -0,0 +1,177 @@
+"use client";
+
+import React, { useState } from "react";
+import Modal from "react-modal"; // Add this import for react-modal
+import {
+ Button,
+ Badge,
+ Table as TremorTable,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeaderCell,
+ TableRow,
+} from "@tremor/react";
+import { DisplayColumnDef } from "@tanstack/react-table";
+import { GenericTable } from "@/components/table/GenericTable";
+
+
+const customStyles = {
+ content: {
+ top: '50%',
+ left: '50%',
+ right: 'auto',
+ bottom: 'auto',
+ marginRight: '-50%',
+ transform: 'translate(-50%, -50%)',
+ width: '400px',
+ },
+};
+
+interface Incident {
+ id: number;
+ name: string;
+}
+
+interface Runbook {
+ id: number;
+ title: string;
+ incidents: Incident[];
+}
+
+const runbookData: Runbook[] = [
+ {
+ id: 1,
+ title: "Database Recovery",
+ incidents: [
+ { id: 101, name: "DB Outage on 2024-01-01" },
+ { id: 102, name: "DB Backup Failure" },
+ ],
+ },
+ {
+ id: 2,
+ title: "API Health Check",
+ incidents: [
+ { id: 201, name: "API Latency Issue" },
+ ],
+ },
+ {
+ id: 3,
+ title: "Server Restart Guide",
+ incidents: [
+ { id: 301, name: "Unexpected Server Crash" },
+ { id: 302, name: "Scheduled Maintenance" },
+ ],
+ },
+];
+
+const columns: DisplayColumnDef[] = [
+ {
+ accessorKey: 'title',
+ header: 'Runbook Title',
+ cell: info => info.getValue(),
+ },
+ {
+ accessorKey: 'incidents',
+ header: 'Incidents',
+ cell: info => (
+
+ {info.getValue().map((incident: Incident) => (
+
+ {incident.name}
+
+ ))}
+
+ ),
+ },
+];
+
+function RunbookIncidentTable() {
+ const [offset, setOffset] = useState(0);
+ const [limit, setLimit] = useState(10);
+
+ // Modal state management
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [repositoryName, setRepositoryName] = useState('');
+ const [pathToMdFiles, setPathToMdFiles] = useState('');
+
+ const handlePaginationChange = (newLimit: number, newOffset: number) => {
+ setLimit(newLimit);
+ setOffset(newOffset);
+ };
+
+ // Open modal handler
+ const openModal = () => {
+ setIsModalOpen(true);
+ };
+
+ // Close modal handler
+ const closeModal = () => {
+ setIsModalOpen(false);
+ };
+
+ // Handle save action from modal
+ const handleSave = () => {
+ // You can handle saving the data here (e.g., API call or updating state)
+ console.log('Repository:', repositoryName);
+ console.log('Path to MD Files:', pathToMdFiles);
+ closeModal();
+ };
+
+ return (
+
+ );
+}
+
+export default RunbookIncidentTable;
diff --git a/keep-ui/components/navbar/NoiseReductionLinks.tsx b/keep-ui/components/navbar/NoiseReductionLinks.tsx
index b7799d42b..06980dbe4 100644
--- a/keep-ui/components/navbar/NoiseReductionLinks.tsx
+++ b/keep-ui/components/navbar/NoiseReductionLinks.tsx
@@ -10,6 +10,7 @@ import classNames from "classnames";
import { AILink } from "./AILink";
import { TbTopologyRing } from "react-icons/tb";
import { FaVolumeMute } from "react-icons/fa";
+import { FaMarkdown } from "react-icons/fa";
import { useTopology } from "utils/hooks/useTopology";
type NoiseReductionLinksProps = { session: Session | null };
@@ -41,6 +42,14 @@ export const NoiseReductionLinks = ({ session }: NoiseReductionLinksProps) => {
+
+
+ Runbooks
+
+
Correlations
diff --git a/keep/api/models/db/runbook.py b/keep/api/models/db/runbook.py
new file mode 100644
index 000000000..5aba84e24
--- /dev/null
+++ b/keep/api/models/db/runbook.py
@@ -0,0 +1,92 @@
+from datetime import datetime
+from typing import List, Optional
+from uuid import UUID, uuid4
+
+from pydantic import BaseModel
+from sqlalchemy import DateTime, ForeignKey, Column, TEXT, JSON
+from sqlmodel import Field, Relationship, SQLModel
+from keep.api.models.db.tenant import Tenant
+
+# Runbook Model
+class Runbook(SQLModel, table=True):
+ id: UUID = Field(default_factory=uuid4, primary_key=True)
+ tenant_id: str = Field(foreign_key="tenant.id")
+ tenant: Tenant = Relationship()
+
+ title: str = Field(nullable=False) # Title of the runbook
+ link: str = Field(nullable=False) # Link to the .md file
+
+ incidents: List["Incident"] = Relationship(
+ back_populates="runbooks", link_model=RunbookToIncident
+ )
+
+ created_at: datetime = Field(default_factory=datetime.utcnow)
+
+ class Config:
+ arbitrary_types_allowed = True
+
+
+# Link Model between Runbook and Incident
+class RunbookToIncident(SQLModel, table=True):
+ tenant_id: str = Field(foreign_key="tenant.id")
+ runbook_id: UUID = Field(foreign_key="runbook.id", primary_key=True)
+ incident_id: UUID = Field(foreign_key="incident.id", primary_key=True)
+
+ incident_id: UUID = Field(
+ sa_column=Column(
+ UUID(binary=False),
+ ForeignKey("incident.id", ondelete="CASCADE"),
+ primary_key=True,
+ )
+ )
+
+
+# Incident Model
+class Incident(SQLModel, table=True):
+ id: UUID = Field(default_factory=uuid4, primary_key=True)
+ tenant_id: str = Field(foreign_key="tenant.id")
+ tenant: Tenant = Relationship()
+
+ user_generated_name: Optional[str] = None
+ ai_generated_name: Optional[str] = None
+
+ user_summary: Optional[str] = Field(sa_column=Column(TEXT), nullable=True)
+ generated_summary: Optional[str] = Field(sa_column=Column(TEXT), nullable=True)
+
+ assignee: Optional[str] = None
+ severity: int = Field(default=IncidentSeverity.CRITICAL.order)
+
+ creation_time: datetime = Field(default_factory=datetime.utcnow)
+
+ start_time: Optional[datetime] = None
+ end_time: Optional[datetime] = None
+ last_seen_time: Optional[datetime] = None
+
+ runbooks: List["Runbook"] = Relationship(
+ back_populates="incidents", link_model=RunbookToIncident
+ )
+
+ is_predicted: bool = Field(default=False)
+ is_confirmed: bool = Field(default=False)
+
+ alerts_count: int = Field(default=0)
+ affected_services: List = Field(sa_column=Column(JSON), default_factory=list)
+ sources: List = Field(sa_column=Column(JSON), default_factory=list)
+
+ rule_id: Optional[UUID] = Field(
+ sa_column=Column(
+ UUID(binary=False),
+ ForeignKey("rule.id", ondelete="CASCADE"),
+ nullable=True,
+ ),
+ )
+
+ rule_fingerprint: str = Field(default="", sa_column=Column(TEXT))
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ if "runbooks" not in kwargs:
+ self.runbooks = []
+
+ class Config:
+ arbitrary_types_allowed = True
diff --git a/keep/providers/github_provider/github_provider.py b/keep/providers/github_provider/github_provider.py
index 5dc3fe265..7e9bdfaa1 100644
--- a/keep/providers/github_provider/github_provider.py
+++ b/keep/providers/github_provider/github_provider.py
@@ -25,6 +25,20 @@ class GithubProviderAuthConfig:
"sensitive": True,
}
)
+ repository: str = dataclasses.field(
+ metadata={
+ "description": "GitHub Repository",
+ "sensitive": False,
+ },
+ default=None,
+ )
+ md_path: str = dataclasses.field(
+ metadata={
+ "description": "Path to .md files in the repository",
+ "sensitive": False,
+ },
+ default=None,
+ )
class GithubProvider(BaseProvider):
@@ -58,6 +72,32 @@ def validate_config(self):
self.authentication_config = GithubProviderAuthConfig(
**self.config.authentication
)
+ def query_runbook(self,query):
+ """Retrieve markdown files from the GitHub repository."""
+
+ if not query:
+ raise ValueError("Query is required")
+
+ auth=None
+ if self.authentication_config.repository and self.authentication_config.md_path:
+ auth = HTTPBasicAuth(
+ self.authentication_config.repository,
+ self.authentication_config.md_path,
+ )
+
+ resp = requests.get(
+ f"{self.authentication_config.url}/api/v1/query",
+ params={"query": query},
+ auth=(
+ auth
+ if self.authentication_config.repository and self.authentication_config.md_path
+ else None
+ )
+ )
+ if response.status_code != 200:
+ raise Exception(f"Runbook Query Failed: {response.content}")
+
+ return response.json()
class GithubStarsProvider(GithubProvider):
@@ -111,7 +151,12 @@ def _query(
github_stars_provider = GithubStarsProvider(
context_manager,
"test",
- ProviderConfig(authentication={"access_token": os.environ.get("GITHUB_PAT")}),
+ ProviderConfig(authentication={
+ "access_token": os.environ.get("GITHUB_PAT"),
+ "repository": os.environ.get("GITHUB_REPOSITORY"),
+ "md_path": os.environ.get("MARKDOWN_PATH"),
+ }
+ ),
)
result = github_stars_provider.query(
diff --git a/keep/providers/gitlab_provider/gitlab_provider.py b/keep/providers/gitlab_provider/gitlab_provider.py
index 90563a15e..dc6b8656a 100644
--- a/keep/providers/gitlab_provider/gitlab_provider.py
+++ b/keep/providers/gitlab_provider/gitlab_provider.py
@@ -35,6 +35,20 @@ class GitlabProviderAuthConfig:
"documentation_url": "https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html",
}
)
+ repository: str = dataclasses.field(
+ metadata={
+ "description": "GitHub Repository",
+ "sensitive": False,
+ },
+ default=None,
+ )
+ md_path: str = dataclasses.field(
+ metadata={
+ "description": "Path to .md files in the repository",
+ "sensitive": False,
+ },
+ default=None,
+ )
class GitlabProvider(BaseProvider):
@@ -142,6 +156,34 @@ def __build_params_from_kwargs(self, kwargs: dict):
else:
params[param] = kwargs[param]
return params
+
+ def query_runbook(self,query):
+ """Retrieve markdown files from the GitHub repository."""
+
+ if not query:
+ raise ValueError("Query is required")
+
+ auth=None
+ if self.authentication_config.repository and self.authentication_config.md_path:
+ auth = HTTPBasicAuth(
+ self.authentication_config.repository,
+ self.authentication_config.md_path,
+ )
+
+ resp = requests.get(
+ f"{self.authentication_config.url}/api/v1/query",
+ params={"query": query},
+ auth=(
+ auth
+ if self.authentication_config.repository and self.authentication_config.md_path
+ else None
+ )
+ )
+ if response.status_code != 200:
+ raise Exception(f"Runbook Query Failed: {response.content}")
+
+ return response.json()
+
def _notify(self, id: str, title: str, description: str = "", labels: str = "", issue_type: str = "issue",
**kwargs: dict):
@@ -180,6 +222,8 @@ def _notify(self, id: str, title: str, description: str = "", labels: str = "",
authentication={
"personal_access_token": gitlab_pat,
"host": gitlab_host,
+ "repository": os.environ.get("GITHUB_REPOSITORY"),
+ "md_path": os.environ.get("MARKDOWN_PATH")
},
)
provider = GitlabProvider(context_manager, provider_id="gitlab", config=config)