From b88145f5e11be5fc91770f33ded4a9ff4e9dbba1 Mon Sep 17 00:00:00 2001 From: Mubashir Shariq Date: Sat, 21 Sep 2024 18:55:47 +0530 Subject: [PATCH] progress 1 --- keep-ui/app/runbooks/page.tsx | 9 + keep-ui/app/runbooks/runbook-table.tsx | 177 ++++++++++++++++++ .../components/navbar/NoiseReductionLinks.tsx | 9 + keep/api/models/db/runbook.py | 92 +++++++++ .../github_provider/github_provider.py | 47 ++++- .../gitlab_provider/gitlab_provider.py | 44 +++++ 6 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 keep-ui/app/runbooks/page.tsx create mode 100644 keep-ui/app/runbooks/runbook-table.tsx create mode 100644 keep/api/models/db/runbook.py 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 ( +
+ + + + data={runbookData} + columns={columns} + rowCount={runbookData.length} + offset={offset} + limit={limit} + onPaginationChange={handlePaginationChange} + onRowClick={(row) => { + console.log("Runbook clicked:", row); + }} + /> + + {/* Modal for Settings */} + +

Runbook Settings

+ +
+ + setRepositoryName(e.target.value)} + placeholder="Enter repository name" + style={{ width: '100%', padding: '8px', marginBottom: '10px' }} + /> +
+ +
+ + setPathToMdFiles(e.target.value)} + placeholder="Enter path to markdown files" + style={{ width: '100%', padding: '8px', marginBottom: '10px' }} + /> +
+ +
+ + +
+
+
+ ); +} + +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)