From b88145f5e11be5fc91770f33ded4a9ff4e9dbba1 Mon Sep 17 00:00:00 2001 From: Mubashir Shariq Date: Sat, 21 Sep 2024 18:55:47 +0530 Subject: [PATCH 01/10] 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) From d2e90fdfff184bf9e5ad3149b9406ff2beecbab0 Mon Sep 17 00:00:00 2001 From: Mubashirshariq Date: Sun, 29 Sep 2024 12:06:30 +0530 Subject: [PATCH 02/10] made some changes --- keep-ui/app/runbooks/runbook-table.tsx | 45 ++++++++++++------- .../github_provider/github_provider.py | 44 ------------------ 2 files changed, 29 insertions(+), 60 deletions(-) diff --git a/keep-ui/app/runbooks/runbook-table.tsx b/keep-ui/app/runbooks/runbook-table.tsx index aa3ad1933..8e6ec4877 100644 --- a/keep-ui/app/runbooks/runbook-table.tsx +++ b/keep-ui/app/runbooks/runbook-table.tsx @@ -1,19 +1,14 @@ "use client"; import React, { useState } from "react"; -import Modal from "react-modal"; // Add this import for react-modal +import Modal from "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"; +import { GenericTable } from "@/components/table/GenericTable"; +import { useSession } from "next-auth/react"; const customStyles = { @@ -95,6 +90,8 @@ function RunbookIncidentTable() { const [repositoryName, setRepositoryName] = useState(''); const [pathToMdFiles, setPathToMdFiles] = useState(''); + const { data: session } = useSession(); + const handlePaginationChange = (newLimit: number, newOffset: number) => { setLimit(newLimit); setOffset(newOffset); @@ -110,12 +107,28 @@ function RunbookIncidentTable() { 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(); + const handleQuerySettings = async ({ repositoryName, pathToMdFiles }) => { + try { + const [owner, repo] = repositoryName.split("/"); + const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${pathToMdFiles}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to query settings'); + } + + const data = await response.json(); + console.log('Settings queried successfully:', data); + + } catch (error) { + console.error('Error while querying settings:', error); + alert('An error occurred while querying settings'); + } }; return ( @@ -149,7 +162,7 @@ function RunbookIncidentTable() { type="text" value={repositoryName} onChange={(e) => setRepositoryName(e.target.value)} - placeholder="Enter repository name" + placeholder="Enter repository name (e.g., owner/repo)" style={{ width: '100%', padding: '8px', marginBottom: '10px' }} /> @@ -167,7 +180,7 @@ function RunbookIncidentTable() {
    - +
    diff --git a/keep/providers/github_provider/github_provider.py b/keep/providers/github_provider/github_provider.py index 7e9bdfaa1..7b495189e 100644 --- a/keep/providers/github_provider/github_provider.py +++ b/keep/providers/github_provider/github_provider.py @@ -25,21 +25,6 @@ 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): """ @@ -72,33 +57,6 @@ 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): """ @@ -153,8 +111,6 @@ def _query( "test", ProviderConfig(authentication={ "access_token": os.environ.get("GITHUB_PAT"), - "repository": os.environ.get("GITHUB_REPOSITORY"), - "md_path": os.environ.get("MARKDOWN_PATH"), } ), ) From 2e9bc5d22c7e05caf0bd934511357e79368dc7ce Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Tue, 1 Oct 2024 19:06:58 +0530 Subject: [PATCH 03/10] fix: access token issue on frontend. but need to move the fetching logic to backend. --- keep-ui/app/runbooks/runbook-table.tsx | 379 ++++++++++++++----------- keep-ui/utils/apiUrl.ts | 13 + keep-ui/utils/fetcher.ts | 9 +- keep-ui/utils/hooks/useRunbook.ts | 157 ++++++++++ 4 files changed, 391 insertions(+), 167 deletions(-) create mode 100644 keep-ui/utils/hooks/useRunbook.ts diff --git a/keep-ui/app/runbooks/runbook-table.tsx b/keep-ui/app/runbooks/runbook-table.tsx index 8e6ec4877..7cc095bee 100644 --- a/keep-ui/app/runbooks/runbook-table.tsx +++ b/keep-ui/app/runbooks/runbook-table.tsx @@ -1,190 +1,239 @@ "use client"; -import React, { useState } from "react"; -import Modal from "react-modal"; -import { - Button, - Badge, -} from "@tremor/react"; +import React, { useEffect, useMemo, useState } from "react"; +import Modal from "react-modal"; +import { Button, Badge } from "@tremor/react"; import { DisplayColumnDef } from "@tanstack/react-table"; -import { GenericTable } from "@/components/table/GenericTable"; +import { GenericTable } from "@/components/table/GenericTable"; import { useSession } from "next-auth/react"; - +import { useProviders } from "utils/hooks/useProviders"; +import { ProvidersResponse, Provider } from "app/providers/providers"; +import { set } from "date-fns"; +import { useRunBookTriggers } from "utils/hooks/useRunbook"; +import { useForm, get } from "react-hook-form"; const customStyles = { - content: { - top: '50%', - left: '50%', - right: 'auto', - bottom: 'auto', - marginRight: '-50%', - transform: 'translate(-50%, -50%)', - width: '400px', - }, + content: { + top: "50%", + left: "50%", + right: "auto", + bottom: "auto", + marginRight: "-50%", + transform: "translate(-50%, -50%)", + width: "400px", + }, }; interface Incident { - id: number; - name: string; + id: number; + name: string; } interface Runbook { - id: number; - title: string; - incidents: Incident[]; + 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" }, - ], - }, + { + 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} - - ))} -
    - ), - }, + { + 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 { data: session } = useSession(); - - const handlePaginationChange = (newLimit: number, newOffset: number) => { - setLimit(newLimit); - setOffset(newOffset); - }; - - // Open modal handler - const openModal = () => { - setIsModalOpen(true); - }; - - // Close modal handler - const closeModal = () => { - setIsModalOpen(false); - }; - - const handleQuerySettings = async ({ repositoryName, pathToMdFiles }) => { - try { - const [owner, repo] = repositoryName.split("/"); - const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${pathToMdFiles}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${session?.accessToken}`, - }, - }); - - if (!response.ok) { - throw new Error('Failed to query settings'); - } - - const data = await response.json(); - console.log('Settings queried successfully:', data); - - } catch (error) { - console.error('Error while querying settings:', error); - alert('An error occurred while querying settings'); - } - }; - - return ( -
    - - - - data={runbookData} - columns={columns} - rowCount={runbookData.length} - offset={offset} - limit={limit} - onPaginationChange={handlePaginationChange} - onRowClick={(row) => { - console.log("Runbook clicked:", row); - }} +function SettingsPage() { + const [isModalOpen, setIsModalOpen] = useState(false); + const { register, handleSubmit, reset, getValues, setValue } = useForm(); + const [userName, setUserName] = useState(""); + const { + runBookInstalledProviders, + reposData, + handleSubmit: submitHandler, + } = useRunBookTriggers(getValues()); + + const openModal = () => { + reset(); // Reset form when opening modal + setIsModalOpen(true); + }; + + const closeModal = () => { + setIsModalOpen(false); + }; + + const onSubmit = (data: any) => { + submitHandler(data); // Call the submit handler with form data + // closeModal(); // Close modal after submit + }; + + return ( +
    + + +

    Runbook Settings

    +
    +
    + + +
    +
    + {/* It change according to the provider. for github we neeed user name and for gitlab we need userId */} + + { + setUserName(e.target.value); + setValue("userName", e.target.value); + }} /> - - {/* Modal for Settings */} - +
    + + +
    +
    + + +
    +
    + + +
    +
    + - -
    -
    -
    - ); + Cancel + + +
    + + +
    + ); +} + +function RunbookIncidentTable() { + const [offset, setOffset] = useState(0); + const [limit, setLimit] = useState(10); + + // Modal state management + + const { data: session } = useSession(); + + const handlePaginationChange = (newLimit: number, newOffset: number) => { + setLimit(newLimit); + setOffset(newOffset); + }; + + // Open modal handler + + return ( +
    + + + data={runbookData} + columns={columns} + rowCount={runbookData.length} + offset={offset} + limit={limit} + onPaginationChange={handlePaginationChange} + onRowClick={(row) => { + console.log("Runbook clicked:", row); + }} + /> + + {/* Modal for Settings */} +
    + ); } export default RunbookIncidentTable; diff --git a/keep-ui/utils/apiUrl.ts b/keep-ui/utils/apiUrl.ts index 512f7dba9..8d773c901 100644 --- a/keep-ui/utils/apiUrl.ts +++ b/keep-ui/utils/apiUrl.ts @@ -1,3 +1,5 @@ +import { Provider } from "app/providers/providers"; + export function getApiURL(): string { // https://github.com/vercel/next.js/issues/5354#issuecomment-520305040 // https://stackoverflow.com/questions/49411796/how-do-i-detect-whether-i-am-on-server-on-client-in-next-js @@ -38,3 +40,14 @@ export function getApiURL(): string { return process.env.API_URL!.replace("keep-api", serviceName); } } + +export function getRunBookUrl(provider: Provider): string { + switch (provider?.type) { + case "github": + return "https://api.github.com"; + case "gitlab": + return "https://gitlab.com/api/v4"; + default: + return ""; + } +} diff --git a/keep-ui/utils/fetcher.ts b/keep-ui/utils/fetcher.ts index 1b1446b1b..24a59867e 100644 --- a/keep-ui/utils/fetcher.ts +++ b/keep-ui/utils/fetcher.ts @@ -1,12 +1,17 @@ import { KeepApiError } from '../app/error'; +export interface OverideHeaders { + headers: Record; +} + export const fetcher = async ( url: string, accessToken: string | undefined, - requestInit: RequestInit = {} + requestInit: RequestInit = {}, + overideHeaders?: OverideHeaders ) => { const response = await fetch(url, { - headers: { + headers: overideHeaders?.headers ?? { Authorization: `Bearer ${accessToken}`, }, ...requestInit, diff --git a/keep-ui/utils/hooks/useRunbook.ts b/keep-ui/utils/hooks/useRunbook.ts new file mode 100644 index 000000000..aa6543cea --- /dev/null +++ b/keep-ui/utils/hooks/useRunbook.ts @@ -0,0 +1,157 @@ +import useSWR, { SWRConfiguration } from "swr"; +import { getRunBookUrl } from "utils/apiUrl"; +import { fetcher, OverideHeaders } from "utils/fetcher"; +import { useProviders } from "./useProviders"; +import { ProvidersResponse, Provider } from "app/providers/providers"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { debounce } from "lodash"; + +export const useRunBookTriggers = (values: any) => { + const providersData = useProviders(); + const [error, setError] = useState(""); + const [synced, setSynced] = useState(false); + const [fileData, setFileData] = useState([]); + const [reposData, setRepoData] = useState([]); + const { pathToMdFile, repoName, userName, providerId, domain } = values || {}; + const { installed_providers, providers } = (providersData?.data || + {}) as ProvidersResponse; + const runBookInstalledProviders = + installed_providers?.filter((provider) => + ["github", "gitlab"].includes(provider.type) + ) || []; + const provider = runBookInstalledProviders?.find( + (provider) => provider.id === providerId + ); + + const apiUrl = getRunBookUrl(provider!); + + function getAccessToken(provider: Provider) { + switch (provider?.type) { + case "github": + return provider?.details?.authentication?.access_token; + case "gitlab": + return ( + provider?.details?.authentication?.personal_access_token || + provider?.details?.authentication?.access_token + ); + default: + return ""; + } + } + + const accessToken = getAccessToken(provider!); + + function constructRepoUrl() { + switch (provider?.type) { + case "github": + return `${apiUrl}/users/${userName}/repos`; + case "gitlab": + return `${apiUrl}/users/${userName}/projects`; + default: + return ""; + } + } + + useEffect(() => { + const getUserRepos = async () => { + console.log("entering this", userName, apiUrl); + if (!userName) { + return setRepoData([]); + } + const url = constructRepoUrl(); + if (!url) { + return setRepoData([]); + } + try { + //need to move it backend. + const data = await fetcher(url, accessToken); + setRepoData(data); + } catch (err) { + console.log("error occurred while fetching data"); + setRepoData([]); + } + }; + + const debounceUserReposRequest = debounce(getUserRepos, 400); + debounceUserReposRequest(); + + // Cleanup to cancel the debounce on unmount or before the next effect run + return () => { + debounceUserReposRequest.cancel(); + }; + }, [userName, apiUrl, provider?.id]); + + function constructUrl( + userName: string, + repoName: string, + pathToMdFile: string + ) { + switch (provider?.type) { + case "github": + return `${apiUrl}/repos/${userName}/${repoName}/contents/${pathToMdFile}`; + case "gitlab": + const repoId = reposData?.find( + (repo: any) => repo.name === repoName + )?.id; + return `${apiUrl}/projects/${repoId}/repository/files/${pathToMdFile}?ref=main`; + default: + return ""; + } + } + + function constructHeaders() { + switch (provider?.type) { + case "gitlab": + return { + headers: { + "PRIVATE-TOKEN": accessToken, + }, + } as OverideHeaders; + default: + return {} as OverideHeaders; + } + } + + const handleSubmit = async (data: any) => { + const { pathToMdFile, repoName, userName } = data; + console.log("entering this", data); + if (!pathToMdFile) { + return { loading: false, data: [], error: "User name is required" }; + } + + const url = constructUrl(userName, repoName, pathToMdFile); + if (!url) { + return setError("Url not found"); + } + + const headers = constructHeaders(); + + try { + //need to move to backend. + const response = await fetcher(url, accessToken, {}, headers); + + if (!response) { + return setError("Something went wrong. try agian after some time"); + } + + setFileData(response); + setSynced(false); + //send it to backend and store the details in db. to DO + } catch (err) { + return setError("Something went wrong. try agian after some time"); + } finally { + setSynced(true); + } + }; + + const HandlePreview = () => {}; + + return { + runBookInstalledProviders, + providersData, + reposData, + handleSubmit, + fileData, + HandlePreview, + }; +}; From 8d1c4433be501c84867a87381462f71ee166cdaa Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Tue, 1 Oct 2024 19:08:51 +0530 Subject: [PATCH 04/10] chore:remvoed unwanted logs --- keep-ui/utils/hooks/useRunbook.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/keep-ui/utils/hooks/useRunbook.ts b/keep-ui/utils/hooks/useRunbook.ts index aa6543cea..fcf75bfb6 100644 --- a/keep-ui/utils/hooks/useRunbook.ts +++ b/keep-ui/utils/hooks/useRunbook.ts @@ -54,7 +54,6 @@ export const useRunBookTriggers = (values: any) => { useEffect(() => { const getUserRepos = async () => { - console.log("entering this", userName, apiUrl); if (!userName) { return setRepoData([]); } @@ -114,7 +113,6 @@ export const useRunBookTriggers = (values: any) => { const handleSubmit = async (data: any) => { const { pathToMdFile, repoName, userName } = data; - console.log("entering this", data); if (!pathToMdFile) { return { loading: false, data: [], error: "User name is required" }; } From 546a0bd21c3d01f76a761459fb51e4f2bc052c8a Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Wed, 2 Oct 2024 17:52:36 +0530 Subject: [PATCH 05/10] refactor: moved the entire runbook fetching logic to backend and added the new tag --- .../app/providers/filter-context/constants.ts | 1 + keep-ui/app/providers/provider-tile.tsx | 4 +- keep-ui/app/providers/providers.tsx | 2 +- keep-ui/app/runbooks/runbook-table.tsx | 137 +++++++++------ keep-ui/utils/apiUrl.ts | 12 -- keep-ui/utils/fetcher.ts | 8 +- keep-ui/utils/hooks/useRunbook.ts | 117 +++---------- keep/api/models/db/runbook.py | 6 +- keep/api/models/provider.py | 2 +- keep/api/routes/providers.py | 69 +++++++- keep/providers/base/base_provider.py | 11 +- .../github_provider/github_provider.py | 143 +++++++++++++++- .../gitlab_provider/gitlab_provider.py | 161 +++++++++++++++--- keep/providers/providers_factory.py | 7 +- 14 files changed, 483 insertions(+), 197 deletions(-) diff --git a/keep-ui/app/providers/filter-context/constants.ts b/keep-ui/app/providers/filter-context/constants.ts index 4034af03f..99b072577 100644 --- a/keep-ui/app/providers/filter-context/constants.ts +++ b/keep-ui/app/providers/filter-context/constants.ts @@ -7,6 +7,7 @@ export const PROVIDER_LABELS: Record = { ticketing: 'Ticketing', data: 'Data', queue: 'Queue', + runbook: 'Runbook', } export const PROVIDER_LABELS_KEYS = Object.keys(PROVIDER_LABELS); \ No newline at end of file diff --git a/keep-ui/app/providers/provider-tile.tsx b/keep-ui/app/providers/provider-tile.tsx index 0e09b6e0a..f5a5b127e 100644 --- a/keep-ui/app/providers/provider-tile.tsx +++ b/keep-ui/app/providers/provider-tile.tsx @@ -19,7 +19,7 @@ import { import "./provider-tile.css"; import moment from "moment"; import ImageWithFallback from "@/components/ImageWithFallback"; -import { FaCode } from "react-icons/fa"; +import { FaCode, FaMarkdown } from "react-icons/fa"; interface Props { provider: Provider; @@ -101,6 +101,8 @@ function getIconForTag(tag: TProviderLabels) { return QueueListIcon; case "topology": return MapIcon; + case "runbook": + return FaMarkdown default: return ChatBubbleBottomCenterIcon; } diff --git a/keep-ui/app/providers/providers.tsx b/keep-ui/app/providers/providers.tsx index ed060ead2..c6af9d314 100644 --- a/keep-ui/app/providers/providers.tsx +++ b/keep-ui/app/providers/providers.tsx @@ -51,7 +51,7 @@ interface AlertDistritbuionData { number: number; } -export type TProviderLabels = 'alert' | 'topology' | 'messaging' | 'ticketing' | 'data' | 'queue'; +export type TProviderLabels = 'alert' | 'topology' | 'messaging' | 'ticketing' | 'data' | 'queue' | 'runbook'; export interface Provider { // key value pair of auth method name and auth method config diff --git a/keep-ui/app/runbooks/runbook-table.tsx b/keep-ui/app/runbooks/runbook-table.tsx index 7cc095bee..dda6f3965 100644 --- a/keep-ui/app/runbooks/runbook-table.tsx +++ b/keep-ui/app/runbooks/runbook-table.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useMemo, useState } from "react"; import Modal from "react-modal"; import { Button, Badge } from "@tremor/react"; -import { DisplayColumnDef } from "@tanstack/react-table"; +import { createColumnHelper, DisplayColumnDef } from "@tanstack/react-table"; import { GenericTable } from "@/components/table/GenericTable"; import { useSession } from "next-auth/react"; import { useProviders } from "utils/hooks/useProviders"; @@ -11,6 +11,8 @@ import { ProvidersResponse, Provider } from "app/providers/providers"; import { set } from "date-fns"; import { useRunBookTriggers } from "utils/hooks/useRunbook"; import { useForm, get } from "react-hook-form"; +import Markdown from "react-markdown"; +import remarkGfm from "remark-gfm"; const customStyles = { content: { @@ -35,7 +37,7 @@ interface Runbook { incidents: Incident[]; } -const runbookData: Runbook[] = [ +const runbookData = [ { id: 1, title: "Database Recovery", @@ -57,46 +59,88 @@ const runbookData: Runbook[] = [ { id: 302, name: "Scheduled Maintenance" }, ], }, -]; +] as Runbook[]; -const columns: DisplayColumnDef[] = [ - { - accessorKey: "title", +const columnHelper = createColumnHelper(); + +const columns = [ + columnHelper.display({ + id: "title", header: "Runbook Title", - cell: (info) => info.getValue(), - }, - { - accessorKey: "incidents", - header: "Incidents", - cell: (info) => ( -
    - {info.getValue().map((incident: Incident) => ( - - {incident.name} - - ))} -
    - ), - }, -]; + cell: ({ row }) => { + return
    {row.original.title}
    ; + }, + }), + columnHelper.display({ + id: "incidents", + header: "Incdients", + cell: ({ row }) => { + return ( +
    + {row.original.incidents?.map((incident: Incident) => ( + + {incident.name} + + ))} +
    + ); + }, + }), +] as DisplayColumnDef[]; + +// function PreviewContent({type, data}:{type:string, data:string}){ +// const [isModalOpen, setIsModalOpen] = useState(open); + +// // const closeModal = () => { +// // setIsModalOpen(false); +// // }; +// const decodeBase64 = (encodedContent:string) => { +// // Use atob to decode the Base64 string and then handle UTF-8 encoding +// return encodedContent ? decodeURIComponent(atob(encodedContent)) : ''; +// }; +// return
    +// +// {decodeBase64(data)} +// +//
    +// } + +// TO DO: Need to work on styling function SettingsPage() { const [isModalOpen, setIsModalOpen] = useState(false); const { register, handleSubmit, reset, getValues, setValue } = useForm(); - const [userName, setUserName] = useState(""); + const [refresh, setRefresh] = useState(0); + const [openPreview, setOpenPreview] = useState(false); const { runBookInstalledProviders, reposData, handleSubmit: submitHandler, - } = useRunBookTriggers(getValues()); + provider, + fileData, + } = useRunBookTriggers(getValues(), refresh); + + useEffect(() => { + setValue( + "repoName", + reposData?.legnth ? provider?.details?.authentication.repository : "" + ); + setOpenPreview(false); + }, [reposData]); const openModal = () => { reset(); // Reset form when opening modal setIsModalOpen(true); + setOpenPreview(false); }; - const closeModal = () => { + const closeModal = (openPreview?: boolean) => { setIsModalOpen(false); + if (openPreview) { + setOpenPreview(true); + } else { + setOpenPreview(false); + } }; const onSubmit = (data: any) => { @@ -109,7 +153,7 @@ function SettingsPage() { closeModal()} style={customStyles} contentLabel="Settings Modal" > @@ -121,8 +165,8 @@ function SettingsPage() { {...register("providerId")} style={{ width: "100%", padding: "8px", marginBottom: "10px" }} onChange={(e) => { - setValue("userName", ""); - setUserName(""); + setValue("providerId", e.target.value); + setRefresh((prev) => prev + 1); }} > + {/* {fileData?.content && openPreview &&} */} ); } @@ -207,15 +248,11 @@ function RunbookIncidentTable() { // Modal state management - const { data: session } = useSession(); - const handlePaginationChange = (newLimit: number, newOffset: number) => { setLimit(newLimit); setOffset(newOffset); }; - // Open modal handler - return (
    @@ -230,8 +267,6 @@ function RunbookIncidentTable() { console.log("Runbook clicked:", row); }} /> - - {/* Modal for Settings */}
    ); } diff --git a/keep-ui/utils/apiUrl.ts b/keep-ui/utils/apiUrl.ts index 8d773c901..52006d2ee 100644 --- a/keep-ui/utils/apiUrl.ts +++ b/keep-ui/utils/apiUrl.ts @@ -1,4 +1,3 @@ -import { Provider } from "app/providers/providers"; export function getApiURL(): string { // https://github.com/vercel/next.js/issues/5354#issuecomment-520305040 @@ -40,14 +39,3 @@ export function getApiURL(): string { return process.env.API_URL!.replace("keep-api", serviceName); } } - -export function getRunBookUrl(provider: Provider): string { - switch (provider?.type) { - case "github": - return "https://api.github.com"; - case "gitlab": - return "https://gitlab.com/api/v4"; - default: - return ""; - } -} diff --git a/keep-ui/utils/fetcher.ts b/keep-ui/utils/fetcher.ts index 24a59867e..c8c7fd899 100644 --- a/keep-ui/utils/fetcher.ts +++ b/keep-ui/utils/fetcher.ts @@ -1,17 +1,11 @@ import { KeepApiError } from '../app/error'; - -export interface OverideHeaders { - headers: Record; -} - export const fetcher = async ( url: string, accessToken: string | undefined, requestInit: RequestInit = {}, - overideHeaders?: OverideHeaders ) => { const response = await fetch(url, { - headers: overideHeaders?.headers ?? { + headers: { Authorization: `Bearer ${accessToken}`, }, ...requestInit, diff --git a/keep-ui/utils/hooks/useRunbook.ts b/keep-ui/utils/hooks/useRunbook.ts index fcf75bfb6..fabc1dc25 100644 --- a/keep-ui/utils/hooks/useRunbook.ts +++ b/keep-ui/utils/hooks/useRunbook.ts @@ -1,18 +1,20 @@ import useSWR, { SWRConfiguration } from "swr"; -import { getRunBookUrl } from "utils/apiUrl"; -import { fetcher, OverideHeaders } from "utils/fetcher"; +import { getApiURL } from "utils/apiUrl"; +import { fetcher } from "utils/fetcher"; import { useProviders } from "./useProviders"; import { ProvidersResponse, Provider } from "app/providers/providers"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { debounce } from "lodash"; +import { useSession } from "next-auth/react"; -export const useRunBookTriggers = (values: any) => { +export const useRunBookTriggers = (values: any, refresh: number) => { const providersData = useProviders(); const [error, setError] = useState(""); const [synced, setSynced] = useState(false); const [fileData, setFileData] = useState([]); const [reposData, setRepoData] = useState([]); const { pathToMdFile, repoName, userName, providerId, domain } = values || {}; + const { data: session } = useSession(); const { installed_providers, providers } = (providersData?.data || {}) as ProvidersResponse; const runBookInstalledProviders = @@ -23,47 +25,15 @@ export const useRunBookTriggers = (values: any) => { (provider) => provider.id === providerId ); - const apiUrl = getRunBookUrl(provider!); - - function getAccessToken(provider: Provider) { - switch (provider?.type) { - case "github": - return provider?.details?.authentication?.access_token; - case "gitlab": - return ( - provider?.details?.authentication?.personal_access_token || - provider?.details?.authentication?.access_token - ); - default: - return ""; - } - } - - const accessToken = getAccessToken(provider!); - - function constructRepoUrl() { - switch (provider?.type) { - case "github": - return `${apiUrl}/users/${userName}/repos`; - case "gitlab": - return `${apiUrl}/users/${userName}/projects`; - default: - return ""; - } - } + const baseApiurl = getApiURL(); useEffect(() => { const getUserRepos = async () => { - if (!userName) { - return setRepoData([]); - } - const url = constructRepoUrl(); - if (!url) { - return setRepoData([]); - } try { - //need to move it backend. - const data = await fetcher(url, accessToken); + const data = await fetcher( + `${baseApiurl}/providers/${provider?.type}/${provider?.id}/repositories`, + session?.accessToken + ); setRepoData(data); } catch (err) { console.log("error occurred while fetching data"); @@ -78,63 +48,31 @@ export const useRunBookTriggers = (values: any) => { return () => { debounceUserReposRequest.cancel(); }; - }, [userName, apiUrl, provider?.id]); - - function constructUrl( - userName: string, - repoName: string, - pathToMdFile: string - ) { - switch (provider?.type) { - case "github": - return `${apiUrl}/repos/${userName}/${repoName}/contents/${pathToMdFile}`; - case "gitlab": - const repoId = reposData?.find( - (repo: any) => repo.name === repoName - )?.id; - return `${apiUrl}/projects/${repoId}/repository/files/${pathToMdFile}?ref=main`; - default: - return ""; - } - } - - function constructHeaders() { - switch (provider?.type) { - case "gitlab": - return { - headers: { - "PRIVATE-TOKEN": accessToken, - }, - } as OverideHeaders; - default: - return {} as OverideHeaders; - } - } + }, [refresh]); const handleSubmit = async (data: any) => { - const { pathToMdFile, repoName, userName } = data; - if (!pathToMdFile) { - return { loading: false, data: [], error: "User name is required" }; - } - - const url = constructUrl(userName, repoName, pathToMdFile); - if (!url) { - return setError("Url not found"); - } - - const headers = constructHeaders(); - + const { pathToMdFile, repoName } = data; try { - //need to move to backend. - const response = await fetcher(url, accessToken, {}, headers); + const params = new URLSearchParams(); + if (pathToMdFile) { + params.append("md_path", pathToMdFile); + } + if (repoName) { + params.append("repo", repoName); + } + //TO DO backend runbook records needs to be created. + const response = await fetcher( + `${baseApiurl}/providers/${provider?.type}/${ + provider?.id + }/runbook?${params.toString()}`, + session?.accessToken + ); if (!response) { return setError("Something went wrong. try agian after some time"); } - setFileData(response); setSynced(false); - //send it to backend and store the details in db. to DO } catch (err) { return setError("Something went wrong. try agian after some time"); } finally { @@ -151,5 +89,6 @@ export const useRunBookTriggers = (values: any) => { handleSubmit, fileData, HandlePreview, + provider, }; }; diff --git a/keep/api/models/db/runbook.py b/keep/api/models/db/runbook.py index 5aba84e24..2549140c1 100644 --- a/keep/api/models/db/runbook.py +++ b/keep/api/models/db/runbook.py @@ -12,14 +12,16 @@ 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() - + repo_id: str = Field(nullable=False) # Github repo id + relative_path: str = Field(nullable=False) # Relative path to the .md file 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 ) - + provider_type: str + provider_id: str | None created_at: datetime = Field(default_factory=datetime.utcnow) class Config: diff --git a/keep/api/models/provider.py b/keep/api/models/provider.py index b800a2890..f316873ab 100644 --- a/keep/api/models/provider.py +++ b/keep/api/models/provider.py @@ -40,7 +40,7 @@ class Provider(BaseModel): last_pull_time: datetime | None = None docs: str | None = None tags: list[ - Literal["alert", "ticketing", "messaging", "data", "queue", "topology"] + Literal["alert", "ticketing", "messaging", "data", "queue", "topology", "runbook"] ] = [] alertsDistribution: dict[str, int] | None = None alertExample: dict | None = None diff --git a/keep/api/routes/providers.py b/keep/api/routes/providers.py index e55e92e24..c3fd52497 100644 --- a/keep/api/routes/providers.py +++ b/keep/api/routes/providers.py @@ -5,7 +5,7 @@ import uuid from typing import Callable, Optional -from fastapi import APIRouter, Body, Depends, HTTPException, Request +from fastapi import APIRouter, Body, Depends, HTTPException, Request, Query from fastapi.responses import JSONResponse from sqlmodel import Session, select from starlette.datastructures import UploadFile @@ -733,3 +733,70 @@ def get_webhook_settings( ), webhookMarkdown=webhookMarkdown, ) + +@router.get("/{provider_type}/{provider_id}/repositories") +def get_repositories( + provider_type: str, + provider_id: str, + authenticated_entity: AuthenticatedEntity = Depends( + IdentityManagerFactory.get_auth_verifier(["read:providers"]) + ), + session: Session = Depends(get_session), +): + tenant_id = authenticated_entity.tenant_id + logger.info("Getting respositories", extra={"provider_type": provider_type, "provider_id": provider_id}) + tenant_id = authenticated_entity.tenant_id + logger.info( + "Getting provider alerts", + extra={ + "tenant_id": tenant_id, + "provider_type": provider_type, + "provider_id": provider_id, + }, + ) + context_manager = ContextManager(tenant_id=tenant_id) + secret_manager = SecretManagerFactory.get_secret_manager(context_manager) + provider_config = secret_manager.read_secret( + f"{tenant_id}_{provider_type}_{provider_id}", is_json=True + ) + provider = ProvidersFactory.get_provider( + context_manager, provider_id, provider_type, provider_config + ) + + return provider.pull_repositories() + + +@router.get("/{provider_type}/{provider_id}/runbook") +def get_repositories( + provider_type: str, + provider_id: str, + authenticated_entity: AuthenticatedEntity = Depends( + IdentityManagerFactory.get_auth_verifier(["read:providers"]) + ), + session: Session = Depends(get_session), + repo: str = Query(None), + branch: str = Query(None), + md_path: str = Query(None), +): + tenant_id = authenticated_entity.tenant_id + logger.info("Getting runbook", extra={"provider_type": provider_type, "provider_id": provider_id}) + tenant_id = authenticated_entity.tenant_id + logger.info( + "Getting provider alerts", + extra={ + "tenant_id": tenant_id, + "provider_type": provider_type, + "provider_id": provider_id, + }, + ) + context_manager = ContextManager(tenant_id=tenant_id) + secret_manager = SecretManagerFactory.get_secret_manager(context_manager) + provider_config = secret_manager.read_secret( + f"{tenant_id}_{provider_type}_{provider_id}", is_json=True + ) + provider = ProvidersFactory.get_provider( + context_manager, provider_id, provider_type, provider_config + ) + + return provider.pull_runbook(repo=repo, branch=branch, md_path=md_path) + diff --git a/keep/providers/base/base_provider.py b/keep/providers/base/base_provider.py index ba968837b..09721b973 100644 --- a/keep/providers/base/base_provider.py +++ b/keep/providers/base/base_provider.py @@ -37,7 +37,7 @@ class BaseProvider(metaclass=abc.ABCMeta): PROVIDER_METHODS: list[ProviderMethod] = [] FINGERPRINT_FIELDS: list[str] = [] PROVIDER_TAGS: list[ - Literal["alert", "ticketing", "messaging", "data", "queue", "topology"] + Literal["alert", "ticketing", "messaging", "data", "queue", "topology", "runbook"] ] = [] def __init__( @@ -663,3 +663,12 @@ def simulate_alert(cls) -> dict: class BaseTopologyProvider(BaseProvider): def pull_topology(self) -> list[TopologyServiceInDto]: raise NotImplementedError("get_topology() method not implemented") + + +class BaseRunBookProvider(BaseProvider): + def pull_runbook(self, repo=None, branch=None, md_path=None): + raise NotImplementedError("get_runbook() method not implemented") + + def pull_repositories(self): + raise NotImplementedError("get_repositories() method not implemented") + diff --git a/keep/providers/github_provider/github_provider.py b/keep/providers/github_provider/github_provider.py index 7b495189e..3445b502b 100644 --- a/keep/providers/github_provider/github_provider.py +++ b/keep/providers/github_provider/github_provider.py @@ -5,10 +5,10 @@ import dataclasses import pydantic -from github import Github +from github import Github, GithubException from keep.contextmanager.contextmanager import ContextManager -from keep.providers.base.base_provider import BaseProvider +from keep.providers.base.base_provider import BaseProvider, BaseRunBookProvider from keep.providers.models.provider_config import ProviderConfig @@ -26,7 +26,22 @@ class GithubProviderAuthConfig: } ) -class GithubProvider(BaseProvider): + repository: str = dataclasses.field( + metadata={ + "description": "GitHub Repository Name", + "sensitive": False, + }, + default=None, + ) + md_path: str = dataclasses.field( + metadata={ + "description": "Path to .md files in the repository", + "sensitive": False, + }, + default=None, + ) + +class GithubProvider(BaseRunBookProvider): """ Enrich alerts with data from GitHub. """ @@ -58,6 +73,108 @@ def validate_config(self): **self.config.authentication ) + + def _format_repo(self, repo:dict): + """ + Format the repository data. + """ + if repo is not None: + return { + "id": repo.id, + "name": repo.name, + "full_name": repo.full_name, + "url": repo.html_url, + "description": repo.description, + "private": repo.private, + "option_value": repo.name, + "display_name": repo.full_name, + "default_branch": repo.default_branch, + } + + return {} + + def _format_repos(self, repos:list[dict]): + """ + Format the repository data into a list of dictionaries. + """ + formatted_repos = [] + for repo in repos: + formatted_repos.append( + self._format_repo(repo) + ) + + return formatted_repos + + def pull_repositories(self, project_id=None): + """ + Get user repositories. + """ + if self.authentication_config.access_token: + client = Github(self.authentication_config.access_token) + else: + client = Github() + user = client.get_user() + repos = user.get_repos() + if project_id: + repo = client.get_repo(project_id) + return self._format_repo(repo) + + repos_list = self._format_repos(repos) + return repos_list + + def _format_runbook(self, runbook, repo): + """ + Format the runbook data into a dictionary. + """ + if runbook is None: + return {} + + return { + "file_name": runbook.name, + "file_path": runbook.path, + "file_size": runbook.size, + "file_type": runbook.type, + "repo_id": repo.get("id"), + "repo_name": repo.get("name"), + "repo_display_name": repo.get("display_name"), + "provider_type": "github", + "provider_id": self.config.authentication.get("provider_id"), + "link": f"https://api.github.com/{repo.get('full_name')}/blob/{repo.get('default_branch')}/{runbook.path}", + "content": runbook.content, + "encoding": runbook.encoding, + } + + + + def pull_runbook(self, repo=None, branch=None, md_path=None): + """Retrieve markdown files from the GitHub repository using the GitHub client.""" + + repo_name = repo if repo else self.authentication_config.repository + branch = branch if branch else "main" + md_path = md_path if md_path else self.authentication_config.md_path + + + if repo_name and branch and md_path: + # Initialize the GitHub client + client = self.__generate_client() + + try: + # Get the repository + user = client.get_user() + username = user.login + repo = client.get_repo(f"{username}/{repo_name}") + if repo is None: + raise Exception(f"Repository {repo_name} not found") + + runbook = repo.get_contents(md_path, branch) + response = self._format_runbook(runbook, self._format_repo(repo)) + return response + + except GithubException as e: + raise Exception(f"Failed to retrieve runbook: {e}") + + raise Exception(f"Failed to get runbook: repository, branch, md_path, or access_token not set") + class GithubStarsProvider(GithubProvider): """ GithubStarsProvider is a class that provides a way to read stars from a GitHub repository. @@ -106,11 +223,13 @@ def _query( tenant_id="singletenant", workflow_id="test", ) + github_pat = os.environ.get("GITHUB_PAT") + github_stars_provider = GithubStarsProvider( context_manager, "test", ProviderConfig(authentication={ - "access_token": os.environ.get("GITHUB_PAT"), + "access_token": github_pat, } ), ) @@ -119,3 +238,19 @@ def _query( repository="keephq/keep", previous_stars_count=910 ) print(result) + + + # Initalize the provider and provider config + config = ProviderConfig( + description="GitHub Provider", + authentication={ + "access_token": github_pat, + "repository": os.environ.get("GITHUB_REPOSITORY"), + "md_path": os.environ.get("MARKDOWN_PATH"), + }, + ) + provider = GithubProvider(context_manager, provider_id="github", config=config) + result = provider.pull_runbook() + result = provider.pull_repositories() + + print(result) diff --git a/keep/providers/gitlab_provider/gitlab_provider.py b/keep/providers/gitlab_provider/gitlab_provider.py index dc6b8656a..8bba50f46 100644 --- a/keep/providers/gitlab_provider/gitlab_provider.py +++ b/keep/providers/gitlab_provider/gitlab_provider.py @@ -10,7 +10,7 @@ from requests import HTTPError from keep.contextmanager.contextmanager import ContextManager -from keep.providers.base.base_provider import BaseProvider +from keep.providers.base.base_provider import BaseProvider, BaseRunBookProvider from keep.providers.models.provider_config import ProviderConfig, ProviderScope @@ -37,7 +37,7 @@ class GitlabProviderAuthConfig: ) repository: str = dataclasses.field( metadata={ - "description": "GitHub Repository", + "description": "GitHub Repository Id", "sensitive": False, }, default=None, @@ -51,7 +51,7 @@ class GitlabProviderAuthConfig: ) -class GitlabProvider(BaseProvider): +class GitlabProvider(BaseRunBookProvider): """Enrich alerts with GitLab tickets.""" PROVIDER_SCOPES = [ @@ -62,7 +62,7 @@ class GitlabProvider(BaseProvider): alias="GitLab PAT with api scope", ), ] - PROVIDER_TAGS = ["ticketing"] + PROVIDER_TAGS = ["ticketing", "runbook"] PROVIDER_DISPLAY_NAME = "GitLab" def __init__( @@ -156,33 +156,138 @@ 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, + # def query_for_repositories(self, **kwargs: dict): + # """Retrieve repositories from the Gitlab""" + # project_id = kwargs.get("project_id") + # user_name = self.authentication_config.username + # auth=self.__get_auth_header() + # url = f"{slef.gitlab_host}/api/v4/projects/{project_id}" if project_id else f"{self.gitlab_host}/api/v4/projects" + # resp = requests.get( + # url, headers=self.__get_auth_header() + # auth=auth + # ) + # if response.status_code != 200: + # raise Exception(f"Repository Query Failed: {response.content}") + # return response.json() + + def get_gitlab_user_id(self): + """ + Retrieve the user ID from the access token in GitLab. + """ + url = f"{self.gitlab_host}/api/v4/user" + headers = self.__get_auth_header() + response = requests.get(url, headers=headers) + + if response.status_code == 200: + user_data = response.json() + print(user_data) + return user_data['id'] # The user ID + else: + raise Exception(f"Failed to retrieve user info: {response.status_code}, {response.text}") + + def _format_repos(self, repos, project_id=None): + """ + Format the repository data into a list of dictionaries. + """ + if project_id is not None: + if repos is not None: + return { + "id": repos.get("id"), + "name": repos.get("name"), + "full_name": repos.get("full_name"), + "url": repos.get("web_url"), + "description": repos.get("description"), + "private": repos.get("visibility"), + "option_value": repos.get("id"), + "display_name": repos.get("path_with_namespace"), + "default_branch": repos.get("default_branch"), + } + return {} + + formatted_repos = [] + for repo in repos: + formatted_repos.append( + { + "id": repo.get("id"), + "name": repo.get("name"), + "full_name": repo.get("full_name"), + "url": repo.get("web_url"), + "description": repo.get("description"), + "private": repo.get("visibility"), + "option_value": repo.get("id"), + "display_name": repo.get("path_with_namespace"), + "default_branch": repo.get("default_branch"), + } + ) + + return formatted_repos + + def pull_repositories(self, project_id=None): + """Get user repositories.""" + if self.authentication_config.personal_access_token: + user_id = self.get_gitlab_user_id() + url = f"{self.gitlab_host}/api/v4/projects/{project_id}" if project_id else f"{self.gitlab_host}/api/v4/users/{user_id}/projects" + resp = requests.get( + url, + headers=self.__get_auth_header() + ) + try: + resp.raise_for_status() + except HTTPError as e: + raise Exception(f"Failed to query repositories: {e}") + + repos = resp.json() + return self._format_repos(repos, project_id) + + raise Exception(f"Failed to get repositories: personal_access_token not set") + + def _format_runbook(self, runbook, repo): + """ + Format the runbook data into a dictionary. + """ + if runbook is None: + return {} + + return { + "file_name": runbook.get("file_name"), + "file_path": runbook.get("file_path"), + "file_size": runbook.get("size"), + "file_type": runbook.get("type"), + "repo_id": repo.get("id"), + "repo_name": repo.get("name"), + "repo_display_name": repo.get("display_name"), + "provider_type": "gitlab", + "config": self.config.authentication.get("provider_id"), + "link": f"{self.gitlab_host}/api/v4/projects/{repo.get('id')}/repository/files/{runbook.get('file_path')}/raw", + "content": runbook.get("content"), + "encoding": runbook.get("encoding"), + } + + def pull_runbook(self, repo=None, branch=None, md_path=None): + """Retrieve markdown files from the GitLab repository.""" + repo = repo if repo else self.authentication_config.repository + branch = branch if branch else "main" + md_path = md_path if md_path else self.authentication_config.md_path + + repo_meta = self.pull_repositories(project_id=repo) + + if repo_meta and branch and md_path: + repo_id = repo_meta.get("id") + resp = requests.get( + f"{self.gitlab_host}/api/v4/projects/{repo_id}/repository/files/{md_path}?ref={branch}", + headers=self.__get_auth_header() ) - 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}") + try: + resp.raise_for_status() + except HTTPError as e: + raise Exception(f"Failed to get runbook: {e}") - return response.json() + return self._format_runbook(resp.json(), repo_meta) + + raise Exception(f"Failed to get runbook: repository or md_path not set") def _notify(self, id: str, title: str, description: str = "", labels: str = "", issue_type: str = "issue", @@ -235,3 +340,7 @@ def _notify(self, id: str, title: str, description: str = "", labels: str = "", summary="Test Alert", description="Test Alert Description", ) + + result = provider.pull_runbook() + result = provider.pull_repositories() + print(result) diff --git a/keep/providers/providers_factory.py b/keep/providers/providers_factory.py index 0b3385625..3daf72d64 100644 --- a/keep/providers/providers_factory.py +++ b/keep/providers/providers_factory.py @@ -22,7 +22,7 @@ from keep.api.models.alert import DeduplicationRuleDto from keep.api.models.provider import Provider from keep.contextmanager.contextmanager import ContextManager -from keep.providers.base.base_provider import BaseProvider, BaseTopologyProvider +from keep.providers.base.base_provider import BaseProvider, BaseTopologyProvider, BaseRunBookProvider from keep.providers.models.provider_config import ProviderConfig from keep.providers.models.provider_method import ProviderMethodDTO, ProviderMethodParam from keep.secretmanager.secretmanagerfactory import SecretManagerFactory @@ -313,7 +313,12 @@ def get_all_providers() -> list[Provider]: docs = provider_class.__doc__ can_fetch_topology = issubclass(provider_class, BaseTopologyProvider) + can_fetch_runbook = issubclass(provider_class, BaseRunBookProvider) + provider_tags = set(provider_class.PROVIDER_TAGS) + + if can_fetch_runbook: + provider_tags.add("runbook") if can_fetch_topology: provider_tags.add("topology") if can_query and "data" not in provider_tags: From 9c99c29b05484acb873052fb2e97686269912320 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Wed, 2 Oct 2024 17:56:31 +0530 Subject: [PATCH 06/10] chore: clean up the code --- keep/providers/gitlab_provider/gitlab_provider.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/keep/providers/gitlab_provider/gitlab_provider.py b/keep/providers/gitlab_provider/gitlab_provider.py index 8bba50f46..2feec6216 100644 --- a/keep/providers/gitlab_provider/gitlab_provider.py +++ b/keep/providers/gitlab_provider/gitlab_provider.py @@ -157,21 +157,6 @@ def __build_params_from_kwargs(self, kwargs: dict): params[param] = kwargs[param] return params - - # def query_for_repositories(self, **kwargs: dict): - # """Retrieve repositories from the Gitlab""" - # project_id = kwargs.get("project_id") - # user_name = self.authentication_config.username - # auth=self.__get_auth_header() - # url = f"{slef.gitlab_host}/api/v4/projects/{project_id}" if project_id else f"{self.gitlab_host}/api/v4/projects" - # resp = requests.get( - # url, headers=self.__get_auth_header() - # auth=auth - # ) - # if response.status_code != 200: - # raise Exception(f"Repository Query Failed: {response.content}") - # return response.json() - def get_gitlab_user_id(self): """ Retrieve the user ID from the access token in GitLab. From a8712623b59169d045216dbf0081f3fb7e1ce8c8 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Wed, 2 Oct 2024 18:17:49 +0530 Subject: [PATCH 07/10] chore: resolved the lint issues --- keep/api/models/db/runbook.py | 37 +++++++++---------- .../github_provider/github_provider.py | 4 +- .../gitlab_provider/gitlab_provider.py | 8 ++-- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/keep/api/models/db/runbook.py b/keep/api/models/db/runbook.py index 2549140c1..b0f577210 100644 --- a/keep/api/models/db/runbook.py +++ b/keep/api/models/db/runbook.py @@ -2,11 +2,25 @@ from typing import List, Optional from uuid import UUID, uuid4 -from pydantic import BaseModel -from sqlalchemy import DateTime, ForeignKey, Column, TEXT, JSON +from sqlalchemy import ForeignKey, Column, TEXT, JSON from sqlmodel import Field, Relationship, SQLModel from keep.api.models.db.tenant import Tenant +# 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, + ) + ) + + # Runbook Model class Runbook(SQLModel, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True) @@ -21,28 +35,13 @@ class Runbook(SQLModel, table=True): back_populates="runbooks", link_model=RunbookToIncident ) provider_type: str - provider_id: str | None + provider_id: Optional[str] = None 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) @@ -56,7 +55,7 @@ class Incident(SQLModel, table=True): generated_summary: Optional[str] = Field(sa_column=Column(TEXT), nullable=True) assignee: Optional[str] = None - severity: int = Field(default=IncidentSeverity.CRITICAL.order) + # severity: int = Field(default=IncidentSeverity.CRITICAL.order) creation_time: datetime = Field(default_factory=datetime.utcnow) diff --git a/keep/providers/github_provider/github_provider.py b/keep/providers/github_provider/github_provider.py index 3445b502b..f89db02a1 100644 --- a/keep/providers/github_provider/github_provider.py +++ b/keep/providers/github_provider/github_provider.py @@ -8,7 +8,7 @@ from github import Github, GithubException from keep.contextmanager.contextmanager import ContextManager -from keep.providers.base.base_provider import BaseProvider, BaseRunBookProvider +from keep.providers.base.base_provider import BaseRunBookProvider from keep.providers.models.provider_config import ProviderConfig @@ -173,7 +173,7 @@ def pull_runbook(self, repo=None, branch=None, md_path=None): except GithubException as e: raise Exception(f"Failed to retrieve runbook: {e}") - raise Exception(f"Failed to get runbook: repository, branch, md_path, or access_token not set") + raise Exception("Failed to get runbook: repository, branch, md_path, or access_token not set") class GithubStarsProvider(GithubProvider): """ diff --git a/keep/providers/gitlab_provider/gitlab_provider.py b/keep/providers/gitlab_provider/gitlab_provider.py index 2feec6216..480f5dbff 100644 --- a/keep/providers/gitlab_provider/gitlab_provider.py +++ b/keep/providers/gitlab_provider/gitlab_provider.py @@ -1,5 +1,5 @@ """ -GitlabProvider is a class that implements the BaseProvider interface for GitLab updates. +GitlabProvider is a class that implements the BaseRunBookProvider interface for GitLab updates. """ import dataclasses @@ -10,7 +10,7 @@ from requests import HTTPError from keep.contextmanager.contextmanager import ContextManager -from keep.providers.base.base_provider import BaseProvider, BaseRunBookProvider +from keep.providers.base.base_provider import BaseRunBookProvider from keep.providers.models.provider_config import ProviderConfig, ProviderScope @@ -226,7 +226,7 @@ def pull_repositories(self, project_id=None): repos = resp.json() return self._format_repos(repos, project_id) - raise Exception(f"Failed to get repositories: personal_access_token not set") + raise Exception("Failed to get repositories: personal_access_token not set") def _format_runbook(self, runbook, repo): """ @@ -272,7 +272,7 @@ def pull_runbook(self, repo=None, branch=None, md_path=None): return self._format_runbook(resp.json(), repo_meta) - raise Exception(f"Failed to get runbook: repository or md_path not set") + raise Exception("Failed to get runbook: repository or md_path not set") def _notify(self, id: str, title: str, description: str = "", labels: str = "", issue_type: str = "issue", From 25cd6e847bb3f5efe7a354ff5bc00dabcac9dac8 Mon Sep 17 00:00:00 2001 From: Rajesh Jonnalagadda Date: Wed, 2 Oct 2024 19:58:22 +0530 Subject: [PATCH 08/10] fix: minor drop down issue --- keep-ui/app/runbooks/runbook-table.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/keep-ui/app/runbooks/runbook-table.tsx b/keep-ui/app/runbooks/runbook-table.tsx index dda6f3965..b3fbc9cb4 100644 --- a/keep-ui/app/runbooks/runbook-table.tsx +++ b/keep-ui/app/runbooks/runbook-table.tsx @@ -148,6 +148,7 @@ function SettingsPage() { // closeModal(); // Close modal after submit }; + console.log("content", fileData); return (
    @@ -167,9 +168,11 @@ function SettingsPage() { onChange={(e) => { setValue("providerId", e.target.value); setRefresh((prev) => prev + 1); + setValue('repoName', ''); }} + defaultValue={provider?.details?.authentication.provider_id ?? ""} > - {runBookInstalledProviders.map((provider) => ( @@ -184,8 +187,9 @@ function SettingsPage() { { - setValue("providerId", e.target.value); - setRefresh((prev) => prev + 1); - setValue('repoName', ''); - }} - defaultValue={provider?.details?.authentication.provider_id ?? ""} - > - - {runBookInstalledProviders.map((provider) => ( - - ))} - +
    +
    + {/* */} + +
    +
    + +
    +
    + +
    +
    + +
    -
    - - -
    -
    - - + Preview + + +
    */} +
    + +
    -
    - - + + + {/* {fileData?.content && openPreview &&} */} +
    + ); +} + +function RunbookIncidentTable() { + const [offset, setOffset] = useState(0); + const [limit, setLimit] = useState(10); + + // Modal state management + + const handlePaginationChange = (newLimit: number, newOffset: number) => { + setLimit(newLimit); + setOffset(newOffset); + }; + + return ( +
    +
    + Runbook + +
    + + + data={runbookData} + columns={columns} + rowCount={runbookData.length} + offset={offset} + limit={limit} + onPaginationChange={handlePaginationChange} + onRowClick={(row) => { + console.log("Runbook clicked:", row); + }} + /> + +
    + ); +} + +export default RunbookIncidentTable; + + setOpenPreview(false); + }; + + const closeModal = (openPreview?: boolean) => { + setIsModalOpen(false); + if (openPreview) { + setOpenPreview(true); + } else { + setOpenPreview(false); + } + }; + + const onSubmit = (data: any) => { + submitHandler(data); // Call the submit handler with form data + // closeModal(); // Close modal after submit + }; + + const handleProviderChange = (value: string) => { + setValue("repoName", ""); + setValue("providerId", value); + setRefresh((prev) => prev + 1); + }; + + return ( +
    + + closeModal()} + style={customStyles} + contentLabel="Settings Modal" + > +

    Runbook Settings

    +
    +
    +
    + {/* */} + +
    +
    + +
    +
    + +
    +
    + +
    {/*
    // } -// TO DO: Need to work on styling -function SettingsPage() { - const [isModalOpen, setIsModalOpen] = useState(false); - const { register, handleSubmit, reset, getValues, setValue, watch } = - useForm(); - const [refresh, setRefresh] = useState(0); - const [openPreview, setOpenPreview] = useState(false); - - const { - runBookInstalledProviders, - reposData, - handleSubmit: submitHandler, - provider, - fileData, - } = useRunBookTriggers(getValues(), refresh); - - const selectedProviderId = watch( - "providerId", - provider?.details?.authentication?.provider_id ?? "" - ); - const selectedRepo = watch( - "repoName", - provider?.details?.authentication?.repository ?? "" - ); - - useEffect(() => { - setValue( - "repoName", - reposData?.legnth ? provider?.details?.authentication.repository : "" - ); - setOpenPreview(false); - }, [reposData]); - - const openModal = () => { - reset(); // Reset form when opening modal - setIsModalOpen(true);"use client"; - -import React, { useEffect, useMemo, useState } from "react"; -import Modal from "react-modal"; -import { - Button, - Badge, - Select, - SelectItem, - TextInput, - Title, - Card, -} from "@tremor/react"; -import { createColumnHelper, DisplayColumnDef } from "@tanstack/react-table"; -import { GenericTable } from "@/components/table/GenericTable"; -import { useRunBookTriggers } from "utils/hooks/useRunbook"; -import { useForm, get } from "react-hook-form"; -import Markdown from "react-markdown"; -import remarkGfm from "remark-gfm"; - -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 = [ - { - 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" }, - ], - }, -] as Runbook[]; - -const columnHelper = createColumnHelper(); - -const columns = [ - columnHelper.display({ - id: "title", - header: "Runbook Title", - cell: ({ row }) => { - return
    {row.original.title}
    ; - }, - }), - columnHelper.display({ - id: "incidents", - header: "Incdients", - cell: ({ row }) => { - return ( -
    - {row.original.incidents?.map((incident: Incident) => ( - - {incident.name} - - ))} -
    - ); - }, - }), -] as DisplayColumnDef[]; - -// function PreviewContent({type, data}:{type:string, data:string}){ -// const [isModalOpen, setIsModalOpen] = useState(open); - -// // const closeModal = () => { -// // setIsModalOpen(false); -// // }; -// const decodeBase64 = (encodedContent:string) => { -// // Use atob to decode the Base64 string and then handle UTF-8 encoding -// return encodedContent ? decodeURIComponent(atob(encodedContent)) : ''; -// }; -// return
    - -// -// {decodeBase64(data)} -// -//
    -// } - // TO DO: Need to work on styling function SettingsPage() { const [isModalOpen, setIsModalOpen] = useState(false); @@ -422,163 +275,11 @@ function RunbookIncidentTable() { return (
    -
    - Runbook - -
    - - - data={runbookData} - columns={columns} - rowCount={runbookData.length} - offset={offset} - limit={limit} - onPaginationChange={handlePaginationChange} - onRowClick={(row) => { - console.log("Runbook clicked:", row); - }} - /> - -
    - ); -} - -export default RunbookIncidentTable; - - setOpenPreview(false); - }; - - const closeModal = (openPreview?: boolean) => { - setIsModalOpen(false); - if (openPreview) { - setOpenPreview(true); - } else { - setOpenPreview(false); - } - }; - - const onSubmit = (data: any) => { - submitHandler(data); // Call the submit handler with form data - // closeModal(); // Close modal after submit - }; - - const handleProviderChange = (value: string) => { - setValue("repoName", ""); - setValue("providerId", value); - setRefresh((prev) => prev + 1); - }; - - return ( -
    - - closeModal()} - style={customStyles} - contentLabel="Settings Modal" - > -

    Runbook Settings

    - -
    -
    - {/* */} - -
    -
    - -
    -
    - -
    -
    - -
    -
    - {/*
    - - -
    */} -
    - - -
    - -
    - {/* {fileData?.content && openPreview &&} */} -
    - ); -} - -function RunbookIncidentTable() { - const [offset, setOffset] = useState(0); - const [limit, setLimit] = useState(10); - - // Modal state management - - const handlePaginationChange = (newLimit: number, newOffset: number) => { - setLimit(newLimit); - setOffset(newOffset); - }; - - return ( -
    -
    +
    Runbook
    - + data={runbookData} columns={columns}