From 4463c01b89e1e33182213c48743bcbcd21047f67 Mon Sep 17 00:00:00 2001 From: Furkan Pehlivan <65170388+pehlicd@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:24:59 +0100 Subject: [PATCH 1/7] feat(providers): added retry mechanism to google chat provider to overcome rate limiting (#2572) Co-authored-by: Shahar Glazner --- .../google_chat_provider.py | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/keep/providers/google_chat_provider/google_chat_provider.py b/keep/providers/google_chat_provider/google_chat_provider.py index ba27d4624..7eef362e6 100644 --- a/keep/providers/google_chat_provider/google_chat_provider.py +++ b/keep/providers/google_chat_provider/google_chat_provider.py @@ -1,4 +1,7 @@ +import http import os +import time + import pydantic import dataclasses import requests @@ -65,18 +68,32 @@ def _notify(self, message="", **kwargs: dict): if not message: raise ProviderException("Message is required") + def __send_message(url, body, headers, retries=3): + for attempt in range(retries): + try: + resp = requests.post(url, json=body, headers=headers) + if resp.status_code == http.HTTPStatus.OK: + return resp + + self.logger.warning(f"Attempt {attempt + 1} failed with status code {resp.status_code}") + + except requests.exceptions.RequestException as e: + self.logger.error(f"Attempt {attempt + 1} failed: {e}") + + if attempt < retries - 1: + time.sleep(1) + + raise requests.exceptions.RequestException(f"Failed to notify message after {retries} attempts") + payload = { "text": message, } - requestHeaders = {"Content-Type": "application/json; charset=UTF-8"} - - response = requests.post(webhook_url, json=payload, headers=requestHeaders) + request_headers = {"Content-Type": "application/json; charset=UTF-8"} - if not response.ok: - raise ProviderException( - f"Failed to notify message to Google Chat: {response.text}" - ) + response = __send_message(webhook_url, body=payload, headers=request_headers) + if response.status_code != http.HTTPStatus.OK: + raise ProviderException(f"Failed to notify message to Google Chat: {response.text}") self.logger.debug("Alert message sent to Google Chat successfully") return "Alert message sent to Google Chat successfully" From ea748d01355b82905beb5d0ff51d9e67455f1924 Mon Sep 17 00:00:00 2001 From: Kirill Chernakov Date: Sun, 24 Nov 2024 13:32:01 +0400 Subject: [PATCH 2/7] fix: feed counter (#2587) --- keep-ui/app/(keep)/topology/api/index.ts | 29 +++++++++---------- .../app/(keep)/topology/model/useTopology.ts | 6 ++-- .../topology/model/useTopologyApplications.ts | 23 ++++++++------- keep-ui/utils/hooks/usePresets.ts | 8 +++-- keep-ui/utils/state.ts | 14 ++------- keep/api/core/db.py | 10 +++++-- keep/api/routes/preset.py | 4 +-- keep/api/tasks/process_event_task.py | 10 +++---- 8 files changed, 51 insertions(+), 53 deletions(-) diff --git a/keep-ui/app/(keep)/topology/api/index.ts b/keep-ui/app/(keep)/topology/api/index.ts index 2d4e3b7f5..5736af7b9 100644 --- a/keep-ui/app/(keep)/topology/api/index.ts +++ b/keep-ui/app/(keep)/topology/api/index.ts @@ -2,19 +2,16 @@ import { fetcher } from "@/utils/fetcher"; import { Session } from "next-auth"; import { TopologyApplication, TopologyService } from "../model/models"; -export function buildTopologyUrl( - apiUrl: string, - { - providerIds, - services, - environment, - }: { - providerIds?: string[]; - services?: string[]; - environment?: string; - } -) { - const baseUrl = `${apiUrl}/topology`; +export function buildTopologyUrl({ + providerIds, + services, + environment, +}: { + providerIds?: string[]; + services?: string[]; + environment?: string; +}) { + const baseUrl = `/topology`; const params = new URLSearchParams(); @@ -57,6 +54,8 @@ export function getTopology( if (!session) { return null; } - const url = buildTopologyUrl(apiUrl, { providerIds, services, environment }); - return fetcher(url, session.accessToken) as Promise; + const url = buildTopologyUrl({ providerIds, services, environment }); + return fetcher(apiUrl + url, session.accessToken) as Promise< + TopologyService[] + >; } diff --git a/keep-ui/app/(keep)/topology/model/useTopology.ts b/keep-ui/app/(keep)/topology/model/useTopology.ts index 24f616f21..b38ebff03 100644 --- a/keep-ui/app/(keep)/topology/model/useTopology.ts +++ b/keep-ui/app/(keep)/topology/model/useTopology.ts @@ -7,7 +7,7 @@ import { buildTopologyUrl } from "@/app/(keep)/topology/api"; import { useTopologyPollingContext } from "@/app/(keep)/topology/model/TopologyPollingContext"; import { useApiUrl } from "utils/hooks/useConfig"; -export const useTopologyBaseKey = () => `${useApiUrl()}/topology`; +export const TOPOLOGY_URL = `/topology`; type UseTopologyOptions = { providerIds?: string[]; @@ -37,11 +37,11 @@ export const useTopology = ( const url = !session ? null - : buildTopologyUrl(apiUrl!, { providerIds, services, environment }); + : buildTopologyUrl({ providerIds, services, environment }); const { data, error, mutate } = useSWR( url, - (url: string) => fetcher(url, session!.accessToken), + (url: string) => fetcher(apiUrl! + url, session!.accessToken), { fallbackData, ...options, diff --git a/keep-ui/app/(keep)/topology/model/useTopologyApplications.ts b/keep-ui/app/(keep)/topology/model/useTopologyApplications.ts index eb749d254..bfc211c65 100644 --- a/keep-ui/app/(keep)/topology/model/useTopologyApplications.ts +++ b/keep-ui/app/(keep)/topology/model/useTopologyApplications.ts @@ -4,14 +4,17 @@ import useSWR, { SWRConfiguration } from "swr"; import { fetcher } from "@/utils/fetcher"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { useCallback, useMemo } from "react"; -import { useTopologyBaseKey, useTopology } from "./useTopology"; +import { useTopology } from "./useTopology"; import { useRevalidateMultiple } from "@/utils/state"; +import { TOPOLOGY_URL } from "./useTopology"; type UseTopologyApplicationsOptions = { initialData?: TopologyApplication[]; options?: SWRConfiguration; }; +export const TOPOLOGY_APPLICATIONS_URL = `/topology/applications`; + export function useTopologyApplications( { initialData, options }: UseTopologyApplicationsOptions = { options: { @@ -21,13 +24,11 @@ export function useTopologyApplications( ) { const apiUrl = useApiUrl(); const { data: session } = useSession(); - const topologyBaseKey = useTopologyBaseKey(); const revalidateMultiple = useRevalidateMultiple(); const { topologyData, mutate: mutateTopology } = useTopology(); - const topologyApplicationsKey = `${apiUrl}/topology/applications`; const { data, error, isLoading, mutate } = useSWR( - !session ? null : topologyApplicationsKey, - (url: string) => fetcher(url, session!.accessToken), + !session ? null : TOPOLOGY_APPLICATIONS_URL, + (url: string) => fetcher(apiUrl + url, session!.accessToken), { fallbackData: initialData, ...options, @@ -48,7 +49,7 @@ export function useTopologyApplications( }); if (response.ok) { console.log("mutating on success"); - revalidateMultiple([topologyBaseKey, topologyApplicationsKey]); + revalidateMultiple([TOPOLOGY_URL, TOPOLOGY_APPLICATIONS_URL]); } else { // Rollback optimistic update on error throw new Error("Failed to add application", { @@ -58,7 +59,7 @@ export function useTopologyApplications( const json = await response.json(); return json as TopologyApplication; }, - [revalidateMultiple, session?.accessToken, topologyApplicationsKey] + [apiUrl, revalidateMultiple, session?.accessToken] ); const updateApplication = useCallback( @@ -98,7 +99,7 @@ export function useTopologyApplications( } ); if (response.ok) { - revalidateMultiple([topologyBaseKey, topologyApplicationsKey]); + revalidateMultiple([TOPOLOGY_URL, TOPOLOGY_APPLICATIONS_URL]); } else { // Rollback optimistic update on error mutate(applications, false); @@ -110,12 +111,12 @@ export function useTopologyApplications( return response; }, [ + apiUrl, applications, mutate, mutateTopology, revalidateMultiple, session?.accessToken, - topologyApplicationsKey, topologyData, ] ); @@ -152,7 +153,7 @@ export function useTopologyApplications( } ); if (response.ok) { - revalidateMultiple([topologyBaseKey, topologyApplicationsKey]); + revalidateMultiple([TOPOLOGY_URL, TOPOLOGY_APPLICATIONS_URL]); } else { // Rollback optimistic update on error mutate(applications, false); @@ -164,12 +165,12 @@ export function useTopologyApplications( return response; }, [ + apiUrl, applications, mutate, mutateTopology, revalidateMultiple, session?.accessToken, - topologyApplicationsKey, topologyData, ] ); diff --git a/keep-ui/utils/hooks/usePresets.ts b/keep-ui/utils/hooks/usePresets.ts index 6cc10873d..a3c72ec5e 100644 --- a/keep-ui/utils/hooks/usePresets.ts +++ b/keep-ui/utils/hooks/usePresets.ts @@ -9,6 +9,7 @@ import { useConfig } from "./useConfig"; import useSWRSubscription from "swr/subscription"; import { useWebsocket } from "./usePusher"; import { useSearchParams } from "next/navigation"; +import { useRevalidateMultiple } from "../state"; export const usePresets = (type?: string, useFilters?: boolean) => { const { data: session } = useSession(); @@ -31,6 +32,7 @@ export const usePresets = (type?: string, useFilters?: boolean) => { const presetsOrderRef = useRef(presetsOrderFromLS); const staticPresetsOrderRef = useRef(staticPresetsOrderFromLS); const { bind, unbind } = useWebsocket(); + const revalidateMultiple = useRevalidateMultiple(); useEffect(() => { presetsOrderRef.current = presetsOrderFromLS; @@ -88,6 +90,8 @@ export const usePresets = (type?: string, useFilters?: boolean) => { (_, { next }) => { const newPresets = (newPresets: Preset[]) => { updateLocalPresets(newPresets); + // update the presets aggregated endpoint for the sidebar + revalidateMultiple(["/preset"]); next(null, { presets: newPresets, isAsyncLoading: false, @@ -110,11 +114,11 @@ export const usePresets = (type?: string, useFilters?: boolean) => { return useSWR( () => session - ? `${apiUrl}/preset${ + ? `/preset${ useFilters && filters && isDashBoard ? `?${filters}` : "" }` : null, - (url) => fetcher(url, session?.accessToken), + (url) => fetcher(apiUrl + url, session?.accessToken), { ...options, onSuccess: (data) => { diff --git a/keep-ui/utils/state.ts b/keep-ui/utils/state.ts index 517ad49f9..ded186d6a 100644 --- a/keep-ui/utils/state.ts +++ b/keep-ui/utils/state.ts @@ -1,17 +1,9 @@ import { useSWRConfig } from "swr"; -type MutateArgs = [string, (data: any) => any]; - -export const mutateLocalMultiple = (args: MutateArgs[]) => { - const { cache } = useSWRConfig(); - args.forEach(([key, mutateFunction]) => { - const currentData = cache.get(key as string); - cache.set(key as string, mutateFunction(currentData)); - }); -}; - export const useRevalidateMultiple = () => { const { mutate } = useSWRConfig(); return (keys: string[]) => - mutate((key) => typeof key === "string" && keys.includes(key)); + mutate( + (key) => typeof key === "string" && keys.some((k) => key.startsWith(k)) + ); }; diff --git a/keep/api/core/db.py b/keep/api/core/db.py index f374117ee..9be0d25f7 100644 --- a/keep/api/core/db.py +++ b/keep/api/core/db.py @@ -40,6 +40,7 @@ from sqlalchemy.sql import exists, expression from sqlmodel import Session, SQLModel, col, or_, select, text +from keep.api.consts import STATIC_PRESETS from keep.api.core.db_utils import create_db_engine, get_json_extract_field # This import is required to create the tables @@ -2615,7 +2616,7 @@ def get_presets( return presets -def get_preset_by_name(tenant_id: str, preset_name: str) -> Preset: +def get_db_preset_by_name(tenant_id: str, preset_name: str) -> Preset | None: with Session(engine) as session: preset = session.exec( select(Preset) @@ -2624,8 +2625,7 @@ def get_preset_by_name(tenant_id: str, preset_name: str) -> Preset: ).first() return preset - -def get_all_presets(tenant_id: str) -> List[Preset]: +def get_db_presets(tenant_id: str) -> List[Preset]: with Session(engine) as session: presets = ( session.exec(select(Preset).where(Preset.tenant_id == tenant_id)) @@ -2634,6 +2634,10 @@ def get_all_presets(tenant_id: str) -> List[Preset]: ) return presets +def get_all_presets_dtos(tenant_id: str) -> List[PresetDto]: + presets = get_db_presets(tenant_id) + static_presets_dtos = list(STATIC_PRESETS.values()) + return [PresetDto(**preset.to_dict()) for preset in presets] + static_presets_dtos def get_dashboards(tenant_id: str, email=None) -> List[Dict[str, Any]]: with Session(engine) as session: diff --git a/keep/api/routes/preset.py b/keep/api/routes/preset.py index 1f3f9b22a..bd331806c 100644 --- a/keep/api/routes/preset.py +++ b/keep/api/routes/preset.py @@ -15,7 +15,7 @@ from sqlmodel import Session, select from keep.api.consts import PROVIDER_PULL_INTERVAL_DAYS, STATIC_PRESETS -from keep.api.core.db import get_preset_by_name as get_preset_by_name_db +from keep.api.core.db import get_db_preset_by_name from keep.api.core.db import get_presets as get_presets_db from keep.api.core.db import ( get_session, @@ -448,7 +448,7 @@ def get_preset_alerts( if preset_name in STATIC_PRESETS: preset = STATIC_PRESETS[preset_name] else: - preset = get_preset_by_name_db(tenant_id, preset_name) + preset = get_db_preset_by_name(tenant_id, preset_name) # if preset does not exist if not preset: raise HTTPException(404, "Preset not found") diff --git a/keep/api/tasks/process_event_task.py b/keep/api/tasks/process_event_task.py index a4bec5e8c..3809ea872 100644 --- a/keep/api/tasks/process_event_task.py +++ b/keep/api/tasks/process_event_task.py @@ -20,7 +20,7 @@ from keep.api.core.db import ( bulk_upsert_alert_fields, get_alerts_by_fingerprint, - get_all_presets, + get_all_presets_dtos, get_enrichment_with_session, get_session_sync, ) @@ -28,7 +28,6 @@ from keep.api.core.elastic import ElasticClient from keep.api.models.alert import AlertDto, AlertStatus, IncidentDto from keep.api.models.db.alert import Alert, AlertActionType, AlertAudit, AlertRaw -from keep.api.models.db.preset import PresetDto from keep.api.utils.enrichment_helpers import ( calculated_start_firing_time, convert_db_alerts_to_dto_alerts, @@ -443,12 +442,11 @@ def __handle_formatted_events( return try: - presets = get_all_presets(tenant_id) + presets = get_all_presets_dtos(tenant_id) rules_engine = RulesEngine(tenant_id=tenant_id) presets_do_update = [] - for preset in presets: + for preset_dto in presets: # filter the alerts based on the search query - preset_dto = PresetDto(**preset.to_dict()) filtered_alerts = rules_engine.filter_alerts( enriched_formatted_events, preset_dto.cel_query ) @@ -458,7 +456,7 @@ def __handle_formatted_events( presets_do_update.append(preset_dto) preset_dto.alerts_count = len(filtered_alerts) # update noisy - if preset.is_noisy: + if preset_dto.is_noisy: firing_filtered_alerts = list( filter( lambda alert: alert.status == AlertStatus.FIRING.value, From 2c04548af2a3a25107abd5860a8d04cf3ee1b790 Mon Sep 17 00:00:00 2001 From: Kirill Chernakov Date: Sun, 24 Nov 2024 14:17:56 +0400 Subject: [PATCH 3/7] refactor: migrate remaining API routes to App router (#2571) --- .../incidents/[id]/chat/page.client.tsx | 14 +++++++ keep-ui/app/api/aws-marketplace/route.ts | 21 ++++++++++ keep-ui/app/api/copilotkit/route.ts | 41 +++++++++++++++++++ keep-ui/next-env.d.ts | 1 - keep-ui/package-lock.json | 8 ++-- keep-ui/package.json | 2 +- keep-ui/pages/_error.jsx | 17 -------- keep-ui/pages/api/aws-marketplace.tsx | 23 ----------- keep-ui/pages/api/copilotkit.ts | 28 ------------- keep-ui/shared/lib/server/getConfig.ts | 1 + .../shared/ui/EmptyState/EmptyStateCard.tsx | 36 ++++++++++++++++ keep-ui/shared/ui/EmptyState/index.ts | 1 + keep-ui/shared/ui/index.ts | 1 + keep-ui/types/internal-config.ts | 1 + 14 files changed, 121 insertions(+), 74 deletions(-) create mode 100644 keep-ui/app/api/aws-marketplace/route.ts create mode 100644 keep-ui/app/api/copilotkit/route.ts delete mode 100644 keep-ui/pages/_error.jsx delete mode 100644 keep-ui/pages/api/aws-marketplace.tsx delete mode 100644 keep-ui/pages/api/copilotkit.ts create mode 100644 keep-ui/shared/ui/EmptyState/EmptyStateCard.tsx create mode 100644 keep-ui/shared/ui/EmptyState/index.ts diff --git a/keep-ui/app/(keep)/incidents/[id]/chat/page.client.tsx b/keep-ui/app/(keep)/incidents/[id]/chat/page.client.tsx index b0e2447b2..e718b90a8 100644 --- a/keep-ui/app/(keep)/incidents/[id]/chat/page.client.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/chat/page.client.tsx @@ -2,13 +2,27 @@ import { IncidentChat } from "./incident-chat"; import { IncidentDto } from "@/entities/incidents/model"; +import { EmptyStateCard } from "@/shared/ui"; +import { useConfig } from "@/utils/hooks/useConfig"; import { CopilotKit } from "@copilotkit/react-core"; +import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; export function IncidentChatClientPage({ incident, }: { incident: IncidentDto; }) { + const { data: config } = useConfig(); + if (config && !config.OPEN_AI_API_KEY_SET) { + return ( + + ); + } + return ( diff --git a/keep-ui/app/api/aws-marketplace/route.ts b/keep-ui/app/api/aws-marketplace/route.ts new file mode 100644 index 000000000..f8c4bb07c --- /dev/null +++ b/keep-ui/app/api/aws-marketplace/route.ts @@ -0,0 +1,21 @@ +import { NextRequest } from "next/server"; +import { redirect } from "next/navigation"; + +export async function POST(request: NextRequest) { + try { + // In App Router, we need to parse the request body manually + const body = await request.json(); + + const token = body["x-amzn-marketplace-token"]; + const offerType = body["x-amzn-marketplace-offer-type"]; + + // Base64 encode the token + const base64EncodedToken = encodeURIComponent(btoa(token)); + + // In App Router, we use the redirect function for redirects + return redirect(`/signin?amt=${base64EncodedToken}`); + } catch (error) { + console.error("Error processing request:", error); + return new Response("Bad Request", { status: 400 }); + } +} diff --git a/keep-ui/app/api/copilotkit/route.ts b/keep-ui/app/api/copilotkit/route.ts new file mode 100644 index 000000000..e8cde87f1 --- /dev/null +++ b/keep-ui/app/api/copilotkit/route.ts @@ -0,0 +1,41 @@ +import { + CopilotRuntime, + OpenAIAdapter, + copilotRuntimeNextJSAppRouterEndpoint, +} from "@copilotkit/runtime"; +import OpenAI, { OpenAIError } from "openai"; +import { NextRequest } from "next/server"; + +function initializeCopilotRuntime() { + try { + const openai = new OpenAI({ + organization: process.env.OPEN_AI_ORGANIZATION_ID, + apiKey: process.env.OPEN_AI_API_KEY, + }); + const serviceAdapter = new OpenAIAdapter({ openai }); + const runtime = new CopilotRuntime(); + return { runtime, serviceAdapter }; + } catch (error) { + if (error instanceof OpenAIError) { + console.log("Error connecting to OpenAI", error); + } else { + console.error("Error initializing Copilot Runtime", error); + } + return null; + } +} + +const runtimeOptions = initializeCopilotRuntime(); + +export const POST = async (req: NextRequest) => { + if (!runtimeOptions) { + return new Response("Error initializing Copilot Runtime", { status: 500 }); + } + const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({ + runtime: runtimeOptions.runtime, + serviceAdapter: runtimeOptions.serviceAdapter, + endpoint: "/api/copilotkit", + }); + + return handleRequest(req); +}; diff --git a/keep-ui/next-env.d.ts b/keep-ui/next-env.d.ts index 725dd6f24..40c3d6809 100644 --- a/keep-ui/next-env.d.ts +++ b/keep-ui/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/keep-ui/package-lock.json b/keep-ui/package-lock.json index e58b5e758..551791941 100644 --- a/keep-ui/package-lock.json +++ b/keep-ui/package-lock.json @@ -53,7 +53,7 @@ "moment": "^2.29.4", "next": "^14.2.13", "next-auth": "^5.0.0-beta.25", - "openai": "^4.72.0", + "openai": "^4.73.0", "postcss": "^8.4.31", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", @@ -16064,9 +16064,9 @@ } }, "node_modules/openai": { - "version": "4.72.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.72.0.tgz", - "integrity": "sha512-hFqG9BWCs7L7ifrhJXw7mJXmUBr7d9N6If3J9563o0jfwVA4wFANFDDaOIWFdgDdwgCXg5emf0Q+LoLCGszQYA==", + "version": "4.73.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.73.0.tgz", + "integrity": "sha512-NZstV77w3CEol9KQTRBRQ15+Sw6nxVTicAULSjYO4wn9E5gw72Mtp3fAVaBFXyyVPws4241YmFG6ya4L8v03tA==", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", diff --git a/keep-ui/package.json b/keep-ui/package.json index d06a62b64..42a3e8395 100644 --- a/keep-ui/package.json +++ b/keep-ui/package.json @@ -54,7 +54,7 @@ "moment": "^2.29.4", "next": "^14.2.13", "next-auth": "^5.0.0-beta.25", - "openai": "^4.72.0", + "openai": "^4.73.0", "postcss": "^8.4.31", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", diff --git a/keep-ui/pages/_error.jsx b/keep-ui/pages/_error.jsx deleted file mode 100644 index 46a61d690..000000000 --- a/keep-ui/pages/_error.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as Sentry from "@sentry/nextjs"; -import Error from "next/error"; - -const CustomErrorComponent = (props) => { - return ; -}; - -CustomErrorComponent.getInitialProps = async (contextData) => { - // In case this is running in a serverless function, await this in order to give Sentry - // time to send the error before the lambda exits - await Sentry.captureUnderscoreErrorException(contextData); - - // This will contain the status code of the response - return Error.getInitialProps(contextData); -}; - -export default CustomErrorComponent; diff --git a/keep-ui/pages/api/aws-marketplace.tsx b/keep-ui/pages/api/aws-marketplace.tsx deleted file mode 100644 index 8d5b8d203..000000000 --- a/keep-ui/pages/api/aws-marketplace.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - if (req.method === "POST") { - const { - "x-amzn-marketplace-token": token, - "x-amzn-marketplace-offer-type": offerType, - } = req.body; - - const base64EncodedToken = encodeURIComponent(btoa(token)); - - // Redirect to the sign-in page or wherever you want - // amt is amazon-marketplace-token - res.writeHead(302, { Location: `/signin?amt=${base64EncodedToken}` }); - res.end(); - } else { - // Handle any non-POST requests - res.status(405).send("Method Not Allowed"); - } -} diff --git a/keep-ui/pages/api/copilotkit.ts b/keep-ui/pages/api/copilotkit.ts deleted file mode 100644 index 2af1161ab..000000000 --- a/keep-ui/pages/api/copilotkit.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import { - CopilotRuntime, - OpenAIAdapter, - copilotRuntimeNextJSPagesRouterEndpoint, -} from "@copilotkit/runtime"; -import OpenAI from "openai"; - -const openai = new OpenAI({ - organization: process.env.OPEN_AI_ORGANIZATION_ID, - apiKey: process.env.OPEN_AI_API_KEY, -}); - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const serviceAdapter = new OpenAIAdapter({ openai }); - const runtime = new CopilotRuntime(); - - const handleRequest = copilotRuntimeNextJSPagesRouterEndpoint({ - endpoint: "/api/copilotkit", - runtime, - serviceAdapter, - }); - - return await handleRequest(req, res); -} diff --git a/keep-ui/shared/lib/server/getConfig.ts b/keep-ui/shared/lib/server/getConfig.ts index 7a823ed15..b10cbbe70 100644 --- a/keep-ui/shared/lib/server/getConfig.ts +++ b/keep-ui/shared/lib/server/getConfig.ts @@ -54,5 +54,6 @@ export function getConfig() { POSTHOG_HOST: process.env.POSTHOG_HOST, SENTRY_DISABLED: process.env.SENTRY_DISABLED, READ_ONLY: process.env.KEEP_READ_ONLY === "true", + OPEN_AI_API_KEY_SET: !!process.env.OPEN_AI_API_KEY, }; } diff --git a/keep-ui/shared/ui/EmptyState/EmptyStateCard.tsx b/keep-ui/shared/ui/EmptyState/EmptyStateCard.tsx new file mode 100644 index 000000000..c683e3e0e --- /dev/null +++ b/keep-ui/shared/ui/EmptyState/EmptyStateCard.tsx @@ -0,0 +1,36 @@ +import { Card } from "@tremor/react"; +import { CircleStackIcon } from "@heroicons/react/24/outline"; +import clsx from "clsx"; + +export function EmptyStateCard({ + title, + icon, + description, + className, + children, +}: { + icon?: React.ElementType; + title: string; + description: string; + className?: string; + children?: React.ReactNode; +}) { + const Icon = icon || CircleStackIcon; + return ( + +
+ +

+ {title} +

+

+ {description} +

+ {children} +
+
+ ); +} diff --git a/keep-ui/shared/ui/EmptyState/index.ts b/keep-ui/shared/ui/EmptyState/index.ts new file mode 100644 index 000000000..a31d06c86 --- /dev/null +++ b/keep-ui/shared/ui/EmptyState/index.ts @@ -0,0 +1 @@ +export { EmptyStateCard } from "./EmptyStateCard"; diff --git a/keep-ui/shared/ui/index.ts b/keep-ui/shared/ui/index.ts index 4cfab5810..307db1bbb 100644 --- a/keep-ui/shared/ui/index.ts +++ b/keep-ui/shared/ui/index.ts @@ -2,3 +2,4 @@ export { TablePagination } from "./TablePagination"; export { TabLinkNavigation, TabNavigationLink } from "./TabLinkNavigation"; export { DateTimeField } from "./DateTimeField"; export { FieldHeader } from "./FieldHeader"; +export { EmptyStateCard } from "./EmptyState"; diff --git a/keep-ui/types/internal-config.ts b/keep-ui/types/internal-config.ts index 550828b27..c0d13710b 100644 --- a/keep-ui/types/internal-config.ts +++ b/keep-ui/types/internal-config.ts @@ -18,4 +18,5 @@ export interface InternalConfig { SENTRY_DISABLED: string; // READ ONLY READ_ONLY: boolean; + OPEN_AI_API_KEY_SET: boolean; } From d9cf4e32b8d28610471903379af2a4c12891122d Mon Sep 17 00:00:00 2001 From: Shahar Glazner Date: Sun, 24 Nov 2024 12:35:20 +0200 Subject: [PATCH 4/7] fix(docs): readme (#2516) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 611ef8f4d..7bf0bbcea 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@
- PRs Welcome - + PRs Welcome Join Slack - GitHub commit activity + + GitHub commit activity From cda1363b8000751b4802ce6acf4975a1e49a3c8e Mon Sep 17 00:00:00 2001 From: Matvey Kukuy Date: Sun, 24 Nov 2024 13:50:43 +0200 Subject: [PATCH 5/7] fix: Better Demo Alerts (#2607) --- keep/api/core/demo_mode.py | 29 ++++++++++++------- .../providers/datadog_provider/alerts_mock.py | 18 ++++++++++++ .../prometheus_provider/alerts_mock.py | 16 +++++++++- keep/server_jobs_bg.py | 3 +- 4 files changed, 54 insertions(+), 12 deletions(-) diff --git a/keep/api/core/demo_mode.py b/keep/api/core/demo_mode.py index 26ec63add..4ffc06166 100644 --- a/keep/api/core/demo_mode.py +++ b/keep/api/core/demo_mode.py @@ -29,7 +29,7 @@ { "sqlQuery": {"sql": "((name like :name_1))", "params": {"name_1": "%mq%"}}, "groupDescription": "This rule groups all alerts related to MQ.", - "ruleName": "Message Queue Buckle Up", + "ruleName": "Message queue is getting filled up", "celQuery": '(name.contains("mq"))', "timeframeInSeconds": 86400, "timeUnit": "hours", @@ -243,6 +243,14 @@ def get_or_create_topology(keep_api_key, keep_api_url): if service["name"] == existing_service["display_name"]: service["id"] = existing_service["id"] + # Check if any service does not have an id + for service in application_to_create["services"]: + if "id" not in service: + logger.error( + f"Service {service['name']} does not have an id. Application creation failed." + ) + return True + response = requests.post( f"{keep_api_url}/topology/applications", headers={"x-api-key": keep_api_key}, @@ -415,21 +423,22 @@ def simulate_alerts( time.sleep(sleep_interval) -def launch_demo_mode_thread(keep_api_url=None) -> threading.Thread | None: +def launch_demo_mode_thread(keep_api_url=None, keep_api_key=None) -> threading.Thread | None: if not KEEP_LIVE_DEMO_MODE: logger.info("Not launching the demo mode.") return logger.info("Launching demo mode.") - with get_session_sync() as session: - keep_api_key = get_or_create_api_key( - session=session, - tenant_id=SINGLE_TENANT_UUID, - created_by="system", - unique_api_key_id="simulate_alerts", - system_description="Simulate Alerts API key", - ) + if keep_api_key is None: + with get_session_sync() as session: + keep_api_key = get_or_create_api_key( + session=session, + tenant_id=SINGLE_TENANT_UUID, + created_by="system", + unique_api_key_id="simulate_alerts", + system_description="Simulate Alerts API key", + ) sleep_interval = 5 diff --git a/keep/providers/datadog_provider/alerts_mock.py b/keep/providers/datadog_provider/alerts_mock.py index 0ff032a3b..f4fcff3ce 100644 --- a/keep/providers/datadog_provider/alerts_mock.py +++ b/keep/providers/datadog_provider/alerts_mock.py @@ -35,4 +35,22 @@ "priority": ["P1", "P3", "P4"], }, }, + "mq_consumer_struggling": { + "payload": { + "title": "mq consumer is struggling", + "type": "metric alert", + "query": "avg(last_1h):min:mq_processing{*} by {host} < 10", + "message": "MQ Consumer is processing less than 10 messages per second on {{host.name}}.", + "tags": "environment:production,team:database", + "priority": 4, + "monitor_id": "1234567891", + }, + "parameters": { + "tags": [ + "environment:production,team:analytics,monitor,service:api", + "environment:staging,team:database,monitor,service:api", + ], + "priority": ["P1", "P3", "P4"], + }, + }, } diff --git a/keep/providers/prometheus_provider/alerts_mock.py b/keep/providers/prometheus_provider/alerts_mock.py index d29197074..fa5f7e922 100644 --- a/keep/providers/prometheus_provider/alerts_mock.py +++ b/keep/providers/prometheus_provider/alerts_mock.py @@ -15,7 +15,7 @@ "labels.instance": ["instance1", "instance2", "instance3"], }, }, - "mq_third_full": { + "mq_third_full (Message queue is over 33%)": { "payload": { "summary": "Message queue is over 33% capacity", "labels": { @@ -29,6 +29,20 @@ "labels.mq_manager": ["mq_manager1", "mq_manager2", "mq_manager3"], }, }, + "mq_full (Message queue is full)": { + "payload": { + "summary": "Message queue is over 90% capacity", + "labels": { + "severity": "critical", + "customer_id": "acme" + }, + }, + "parameters": { + "labels.queue": ["queue4"], + "labels.service": ["calendar-producer-java-otel-api-dd", "kafka", "queue"], + "labels.mq_manager": ["mq_manager4"], + }, + }, "disk_space_low": { "payload": { "summary": "Disk space is below 20%", diff --git a/keep/server_jobs_bg.py b/keep/server_jobs_bg.py index b1cad6390..bce6ca9c0 100644 --- a/keep/server_jobs_bg.py +++ b/keep/server_jobs_bg.py @@ -15,6 +15,7 @@ def main(): # We intentionally don't use KEEP_API_URL here to avoid going through the internet. # Script should be launched in the same environment as the server. keep_api_url = "http://localhost:" + str(os.environ.get("PORT", 8080)) + keep_api_key = os.environ.get("KEEP_LIVE_DEMO_MODE_API_KEY") while True: try: @@ -27,7 +28,7 @@ def main(): time.sleep(5) threads = [] - threads.append(launch_demo_mode_thread(keep_api_url)) + threads.append(launch_demo_mode_thread(keep_api_url, keep_api_key)) threads.append(launch_uptime_reporting_thread()) logger.info("Background server jobs threads launched, joining them.") From 71b144248f3430f5044da75d0e7c6b7370f0bf6e Mon Sep 17 00:00:00 2001 From: Tal Date: Sun, 24 Nov 2024 17:48:32 +0200 Subject: [PATCH 6/7] feat: make providers page easier to navigate (#2610) --- .../components/providers-categories/index.ts | 1 + .../providers-categories.tsx | 53 +++ .../providers-filter-by-label.tsx | 4 +- .../providers-search/providers-search.tsx | 1 + .../filter-context/filter-context.tsx | 7 +- .../(keep)/providers/filter-context/types.ts | 4 +- keep-ui/app/(keep)/providers/layout.tsx | 6 +- keep-ui/app/(keep)/providers/page.client.tsx | 26 +- .../app/(keep)/providers/provider-tile.tsx | 14 +- .../app/(keep)/providers/providers-tiles.tsx | 7 +- keep-ui/app/(keep)/providers/providers.tsx | 20 +- keep-ui/app/(signin)/layout.tsx | 4 +- keep-ui/public/icons/salesforce-icon.png | Bin 0 -> 56517 bytes keep-ui/public/icons/zendesk-icon.png | Bin 0 -> 38135 bytes keep/api/models/provider.py | 6 +- keep/providers/aks_provider/aks_provider.py | 1 + .../appdynamics_provider.py | 19 +- .../auth0_provider/auth0_provider.py | 8 +- .../axiom_provider/axiom_provider.py | 2 + .../azuremonitoring_provider.py | 1 + keep/providers/base/base_provider.py | 21 + .../bigquery_provider/bigquery_provider.py | 2 + .../centreon_provider/centreon_provider.py | 397 +++++++++--------- .../checkmk_provider/checkmk_provider.py | 174 ++++---- .../cilium_provider/cilium_provider.py | 1 + .../clickhouse_provider.py | 37 +- .../cloudwatch_provider.py | 3 +- .../coralogix_provider/coralogix_provider.py | 2 +- .../datadog_provider/datadog_provider.py | 1 + .../discord_provider/discord_provider.py | 1 + .../dynatrace_provider/dynatrace_provider.py | 2 + .../elastic_provider/elastic_provider.py | 2 + .../gcpmonitoring_provider.py | 2 +- .../github_provider/github_provider.py | 1 + .../gitlab_provider/gitlab_provider.py | 40 +- .../gitlabpipelines_provider.py | 8 +- keep/providers/gke_provider/gke_provider.py | 9 +- .../google_chat_provider.py | 17 +- .../grafana_incident_provider.py | 63 +-- .../grafana_oncall_provider.py | 2 + .../grafana_provider/grafana_provider.py | 1 + .../graylog_provider/graylog_provider.py | 35 +- .../ilert_provider/ilert_provider.py | 1 + .../incidentio_provider.py | 120 ++++-- .../incidentmanager_provider.py | 2 + keep/providers/jira_provider/jira_provider.py | 19 +- .../jiraonprem_provider.py | 26 +- .../kafka_provider/kafka_provider.py | 11 +- .../kibana_provider/kibana_provider.py | 1 + .../kubernetes_provider.py | 111 +++-- .../linear_provider/linear_provider.py | 1 + .../linearb_provider/linearb_provider.py | 1 + .../mailchimp_provider/mailchimp_provider.py | 36 +- .../mailgun_provider/mailgun_provider.py | 1 + .../mattermost_provider.py | 3 +- .../microsoft-planner-provider.py | 2 + .../mongodb_provider/mongodb_provider.py | 9 +- .../mysql_provider/mysql_provider.py | 1 + .../netdata_provider/netdata_provider.py | 1 + .../newrelic_provider/newrelic_provider.py | 1 + keep/providers/ntfy_provider/ntfy_provider.py | 1 + .../openai_provider/openai_provider.py | 1 + .../openobserve_provider.py | 2 +- .../openshift_provider/openshift_provider.py | 36 +- .../opsgenie_provider/opsgenie_provider.py | 1 + .../pagerduty_provider/pagerduty_provider.py | 2 +- .../pagertree_provider/pagertree_provider.py | 201 +++++---- .../parseable_provider/parseable_provider.py | 1 + .../pingdom_provider/pingdom_provider.py | 2 +- .../postgres_provider/postgres_provider.py | 13 +- .../prometheus_provider.py | 1 + keep/providers/providers_factory.py | 15 +- .../pushover_provider/pushover_provider.py | 1 + .../quickchart_provider.py | 1 + .../redmine_provider/redmine_provider.py | 56 ++- .../resend_provider/resend_provider.py | 2 + .../rollbar_provider/rollbar_provider.py | 2 +- .../providers/salesforce_provider/__init__.py | 0 .../salesforce_provider.py | 36 ++ .../sendgrid_provider/sendgrid_provider.py | 1 + .../sentry_provider/sentry_provider.py | 2 +- .../servicenow_provider.py | 1 + .../signalfx_provider/signalfx_provider.py | 2 + .../signl4_provider/signl4_provider.py | 37 +- .../site24x7_provider/site24x7_provider.py | 2 +- .../slack_provider/slack_provider.py | 1 + keep/providers/smtp_provider/smtp_provider.py | 1 + .../snowflake_provider/snowflake_provider.py | 1 + .../splunk_provider/splunk_provider.py | 2 +- .../squadcast_provider/squadcast_provider.py | 4 +- keep/providers/ssh_provider/ssh_provider.py | 1 + .../statuscake_provider.py | 26 +- .../sumologic_provider/sumologic_provider.py | 2 +- .../teams_provider/teams_provider.py | 4 +- .../telegram_provider/telegram_provider.py | 1 + .../trello_provider/trello_provider.py | 9 +- .../twilio_provider/twilio_provider.py | 9 +- .../uptimekuma_provider.py | 1 + .../victoriametrics_provider.py | 2 +- .../webhook_provider/webhook_provider.py | 1 + .../zabbix_provider/zabbix_provider.py | 1 + keep/providers/zendesk_provider/__init__.py | 0 .../zendesk_provider/zendesk_provider.py | 36 ++ .../zenduty_provider/zenduty_provider.py | 17 +- .../test_pushing_prometheus_alerts.py | 6 +- 105 files changed, 1215 insertions(+), 681 deletions(-) create mode 100644 keep-ui/app/(keep)/providers/components/providers-categories/index.ts create mode 100644 keep-ui/app/(keep)/providers/components/providers-categories/providers-categories.tsx create mode 100644 keep-ui/public/icons/salesforce-icon.png create mode 100644 keep-ui/public/icons/zendesk-icon.png create mode 100644 keep/providers/salesforce_provider/__init__.py create mode 100644 keep/providers/salesforce_provider/salesforce_provider.py create mode 100644 keep/providers/zendesk_provider/__init__.py create mode 100644 keep/providers/zendesk_provider/zendesk_provider.py diff --git a/keep-ui/app/(keep)/providers/components/providers-categories/index.ts b/keep-ui/app/(keep)/providers/components/providers-categories/index.ts new file mode 100644 index 000000000..79942cca9 --- /dev/null +++ b/keep-ui/app/(keep)/providers/components/providers-categories/index.ts @@ -0,0 +1 @@ +export { ProvidersCategories } from "./providers-categories"; diff --git a/keep-ui/app/(keep)/providers/components/providers-categories/providers-categories.tsx b/keep-ui/app/(keep)/providers/components/providers-categories/providers-categories.tsx new file mode 100644 index 000000000..bb657e440 --- /dev/null +++ b/keep-ui/app/(keep)/providers/components/providers-categories/providers-categories.tsx @@ -0,0 +1,53 @@ +import { TProviderCategory } from "@/app/(keep)/providers/providers"; +import { Badge } from "@tremor/react"; +import { useFilterContext } from "../../filter-context"; + +export const ProvidersCategories = () => { + const { providersSelectedCategories, setProvidersSelectedCategories } = + useFilterContext(); + + const categories: TProviderCategory[] = [ + "Monitoring", + "Incident Management", + "Cloud Infrastructure", + "Ticketing", + "Developer Tools", + "Database", + "Identity and Access Management", + "Security", + "Collaboration", + "CRM", + "Queues", + "Coming Soon", + "Others", + ]; + + const toggleCategory = (category: TProviderCategory) => { + setProvidersSelectedCategories((prev) => + prev.includes(category) + ? prev.filter((c) => c !== category) + : [...prev, category] + ); + }; + + return ( +
+ {categories.map((category) => ( + toggleCategory(category)} + > + {category} + + ))} +
+ ); +}; diff --git a/keep-ui/app/(keep)/providers/components/providers-filter-by-label/providers-filter-by-label.tsx b/keep-ui/app/(keep)/providers/components/providers-filter-by-label/providers-filter-by-label.tsx index 09f696c8e..669c966f1 100644 --- a/keep-ui/app/(keep)/providers/components/providers-filter-by-label/providers-filter-by-label.tsx +++ b/keep-ui/app/(keep)/providers/components/providers-filter-by-label/providers-filter-by-label.tsx @@ -18,8 +18,8 @@ export const ProvidersFilterByLabel: FC = (props) => { {options.map(([value, label]) => ( diff --git a/keep-ui/app/(keep)/providers/components/providers-search/providers-search.tsx b/keep-ui/app/(keep)/providers/components/providers-search/providers-search.tsx index 5d4965038..604f24bd5 100644 --- a/keep-ui/app/(keep)/providers/components/providers-search/providers-search.tsx +++ b/keep-ui/app/(keep)/providers/components/providers-search/providers-search.tsx @@ -16,6 +16,7 @@ export const ProvidersSearch: FC = () => { id="search-providers" icon={MagnifyingGlassIcon} placeholder="Filter providers..." + className="w-full" value={providersSearchString} onChange={handleChange} /> diff --git a/keep-ui/app/(keep)/providers/filter-context/filter-context.tsx b/keep-ui/app/(keep)/providers/filter-context/filter-context.tsx index 1e2866721..e61f91dba 100644 --- a/keep-ui/app/(keep)/providers/filter-context/filter-context.tsx +++ b/keep-ui/app/(keep)/providers/filter-context/filter-context.tsx @@ -2,7 +2,7 @@ import { createContext, useState, FC, PropsWithChildren } from "react"; import { IFilterContext } from "./types"; import { useSearchParams } from "next/navigation"; import { PROVIDER_LABELS_KEYS } from "./constants"; -import type { TProviderLabels } from "../providers"; +import type { TProviderCategory, TProviderLabels } from "../providers"; export const FilterContext = createContext(null); @@ -12,6 +12,9 @@ export const FilerContextProvider: FC = ({ children }) => { const [providersSearchString, setProvidersSearchString] = useState(""); + const [providersSelectedCategories, setProvidersSelectedCategories] = + useState([]); + const [providersSelectedTags, setProvidersSelectedTags] = useState< TProviderLabels[] >(() => { @@ -26,8 +29,10 @@ export const FilerContextProvider: FC = ({ children }) => { const contextValue: IFilterContext = { providersSearchString, providersSelectedTags, + providersSelectedCategories, setProvidersSelectedTags, setProvidersSearchString, + setProvidersSelectedCategories, }; return ( diff --git a/keep-ui/app/(keep)/providers/filter-context/types.ts b/keep-ui/app/(keep)/providers/filter-context/types.ts index c56f163b6..aeb6f0d5c 100644 --- a/keep-ui/app/(keep)/providers/filter-context/types.ts +++ b/keep-ui/app/(keep)/providers/filter-context/types.ts @@ -1,9 +1,11 @@ import { Dispatch, SetStateAction } from "react"; -import { TProviderLabels } from "../providers"; +import { TProviderCategory, TProviderLabels } from "../providers"; export interface IFilterContext { providersSearchString: string; providersSelectedTags: TProviderLabels[]; + providersSelectedCategories: TProviderCategory[]; setProvidersSearchString: Dispatch>; setProvidersSelectedTags: Dispatch>; + setProvidersSelectedCategories: Dispatch>; } diff --git a/keep-ui/app/(keep)/providers/layout.tsx b/keep-ui/app/(keep)/providers/layout.tsx index 83d2cc7fc..0c229eef2 100644 --- a/keep-ui/app/(keep)/providers/layout.tsx +++ b/keep-ui/app/(keep)/providers/layout.tsx @@ -3,16 +3,18 @@ import { PropsWithChildren } from "react"; import { ProvidersFilterByLabel } from "./components/providers-filter-by-label"; import { ProvidersSearch } from "./components/providers-search"; import { FilerContextProvider } from "./filter-context"; +import { ProvidersCategories } from "./components/providers-categories"; export default function ProvidersLayout({ children }: PropsWithChildren) { return (
-
-
+
+
+
{children}
diff --git a/keep-ui/app/(keep)/providers/page.client.tsx b/keep-ui/app/(keep)/providers/page.client.tsx index fd49f8ff9..b4e4131ad 100644 --- a/keep-ui/app/(keep)/providers/page.client.tsx +++ b/keep-ui/app/(keep)/providers/page.client.tsx @@ -110,7 +110,11 @@ export default function ProvidersPage({ session, isLocalhost, } = useFetchProviders(); - const { providersSearchString, providersSelectedTags } = useFilterContext(); + const { + providersSearchString, + providersSelectedTags, + providersSelectedCategories, + } = useFilterContext(); const apiUrl = useApiUrl(); const router = useRouter(); useEffect(() => { @@ -147,6 +151,21 @@ export default function ProvidersPage({ ); }; + const searchCategories = (provider: Provider) => { + if (providersSelectedCategories.includes("Coming Soon")) { + if (provider.coming_soon) { + return true; + } + } + + return ( + providersSelectedCategories.length === 0 || + provider.categories.some((category) => + providersSelectedCategories.includes(category) + ) + ); + }; + const searchTags = (provider: Provider) => { return ( providersSelectedTags.length === 0 || @@ -171,7 +190,10 @@ export default function ProvidersPage({ )} searchProviders(provider) && searchTags(provider) + (provider) => + searchProviders(provider) && + searchTags(provider) && + searchCategories(provider) )} isLocalhost={isLocalhost} /> diff --git a/keep-ui/app/(keep)/providers/provider-tile.tsx b/keep-ui/app/(keep)/providers/provider-tile.tsx index 34949538f..2e4ef9c95 100644 --- a/keep-ui/app/(keep)/providers/provider-tile.tsx +++ b/keep-ui/app/(keep)/providers/provider-tile.tsx @@ -159,16 +159,17 @@ export default function ProviderTile({ provider, onClick }: Props) { /> ); }; - return (
- {isLoading ? ( -
-

Loading...

-
- ) : ( -
- ({ - title: formatTimestamp(entry.timestamp), - })) || [] - } - hideControls - disableToolbar - borderLessCards - slideShow={false} - mode="VERTICAL" - theme={{ - primary: "orange", - secondary: "rgb(255 247 237)", - titleColor: "orange", - titleColorActive: "orange", - }} - fontSizes={{ - title: ".75rem", - }} - cardWidth={400} - cardHeight="auto" - classNames={{ - card: "hidden", - cardMedia: "hidden", - cardSubTitle: "hidden", - cardText: "hidden", - cardTitle: "hidden", - title: "mb-3", - contentDetails: "w-full !m-0", - }} - > - {content} - -
- )} + + {isLoading ? ( +
+

Loading...

+
+ ) : ( +
+ ({ + title: formatTimestamp(entry.timestamp), + })) || [] + } + hideControls + disableToolbar + borderLessCards + slideShow={false} + mode="VERTICAL" + theme={{ + primary: "orange", + secondary: "rgb(255 247 237)", + titleColor: "orange", + titleColorActive: "orange", + }} + fontSizes={{ + title: ".75rem", + }} + cardWidth={400} + cardHeight="auto" + classNames={{ + card: "hidden", + cardMedia: "hidden", + cardSubTitle: "hidden", + cardText: "hidden", + cardTitle: "hidden", + title: "mb-3", + contentDetails: "w-full !m-0", + }} + > + {content} + +
+ )} +
); }; diff --git a/keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx b/keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx index 0c803d477..2249e6d1e 100644 --- a/keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx @@ -2,7 +2,7 @@ import { AlertDto } from "@/app/(keep)/alerts/models"; import { IncidentDto } from "@/entities/incidents/model"; -import { useUsers } from "@/utils/hooks/useUsers"; +import { useUsers } from "@/entities/users/model/useUsers"; import Image from "next/image"; import UserAvatar from "@/components/navbar/UserAvatar"; import "./incident-activity.css"; diff --git a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx index 799afbc28..0da0da3ce 100644 --- a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx @@ -1,4 +1,4 @@ -import { Icon } from "@tremor/react"; +import { Button, Icon } from "@tremor/react"; import { AlertDto } from "@/app/(keep)/alerts/models"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { toast } from "react-toastify"; @@ -43,14 +43,18 @@ export default function IncidentAlertMenu({ incidentId, alert }: Props) { } return ( -
- +
); } diff --git a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx index 7d3d828e8..7d3f52ef2 100644 --- a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx @@ -8,6 +8,7 @@ import { } from "@tanstack/react-table"; import { Card, + Icon, Table, TableBody, TableCell, @@ -33,6 +34,9 @@ import { getCommonPinningStylesAndClassNames } from "@/components/ui/table/utils import { EmptyStateCard } from "@/components/ui"; import { useRouter } from "next/navigation"; import { TablePagination } from "@/shared/ui"; +import { AlertSeverityBorder } from "@/app/(keep)/alerts/alert-severity-border"; +import { getStatusIcon } from "@/shared/lib/status-utils"; +import { getStatusColor } from "@/shared/lib/status-utils"; interface Props { incident: IncidentDto; @@ -110,15 +114,17 @@ export default function IncidentAlerts({ incident }: Props) { // /> // ), // }), - columnHelper.accessor("severity", { + columnHelper.display({ id: "severity", - header: "Severity", - minSize: 80, + maxSize: 4, + header: () => <>, cell: (context) => ( -
- -
+ ), + meta: { + tdClassName: "p-0", + thClassName: "p-0", + }, }), columnHelper.display({ id: "name", @@ -144,17 +150,28 @@ export default function IncidentAlerts({ incident }: Props) { id: "status", minSize: 100, header: "Status", + cell: (context) => ( + + + {context.getValue()} + + ), }), columnHelper.accessor("is_created_by_ai", { id: "is_created_by_ai", - header: "🔗", + header: "🔗 Correlation type", minSize: 50, cell: (context) => ( <> {context.getValue() ? ( -
🤖
+
🤖 AI
) : ( -
👨‍💻
+
👨‍💻 Manually
)} ), @@ -186,7 +203,7 @@ export default function IncidentAlerts({ incident }: Props) { }), columnHelper.display({ id: "remove", - header: "", + header: "Correlation", cell: (context) => incident.is_confirmed && ( diff --git a/keep-ui/app/(keep)/settings/auth/groups-tab.tsx b/keep-ui/app/(keep)/settings/auth/groups-tab.tsx index da420f719..dcc23901b 100644 --- a/keep-ui/app/(keep)/settings/auth/groups-tab.tsx +++ b/keep-ui/app/(keep)/settings/auth/groups-tab.tsx @@ -15,7 +15,7 @@ import { } from "@tremor/react"; import Loading from "@/app/(keep)/loading"; import { useGroups } from "utils/hooks/useGroups"; -import { useUsers } from "utils/hooks/useUsers"; +import { useUsers } from "@/entities/users/model/useUsers"; import { useRoles } from "utils/hooks/useRoles"; import { useState, useEffect, useMemo } from "react"; import GroupsSidebar from "./groups-sidebar"; diff --git a/keep-ui/app/(keep)/settings/auth/permissions-tab.tsx b/keep-ui/app/(keep)/settings/auth/permissions-tab.tsx index 08f69e159..69b0c4d17 100644 --- a/keep-ui/app/(keep)/settings/auth/permissions-tab.tsx +++ b/keep-ui/app/(keep)/settings/auth/permissions-tab.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import { Title, Subtitle, Card, TextInput } from "@tremor/react"; import { usePermissions } from "utils/hooks/usePermissions"; -import { useUsers } from "utils/hooks/useUsers"; +import { useUsers } from "@/entities/users/model/useUsers"; import { useGroups } from "utils/hooks/useGroups"; import { useRoles } from "utils/hooks/useRoles"; import { usePresets } from "utils/hooks/usePresets"; diff --git a/keep-ui/app/(keep)/settings/auth/users-settings.tsx b/keep-ui/app/(keep)/settings/auth/users-settings.tsx index 78d2952d2..54cfeb03d 100644 --- a/keep-ui/app/(keep)/settings/auth/users-settings.tsx +++ b/keep-ui/app/(keep)/settings/auth/users-settings.tsx @@ -4,7 +4,7 @@ import Loading from "@/app/(keep)/loading"; import { User as AuthUser } from "next-auth"; import { TiUserAdd } from "react-icons/ti"; import { AuthType } from "utils/authenticationType"; -import { useUsers } from "utils/hooks/useUsers"; +import { useUsers } from "@/entities/users/model/useUsers"; import { useRoles } from "utils/hooks/useRoles"; import { useGroups } from "utils/hooks/useGroups"; import { useConfig } from "utils/hooks/useConfig"; diff --git a/keep-ui/components/navbar/UserAvatar.tsx b/keep-ui/components/navbar/UserAvatar.tsx index d90300363..c2ae3c996 100644 --- a/keep-ui/components/navbar/UserAvatar.tsx +++ b/keep-ui/components/navbar/UserAvatar.tsx @@ -1,8 +1,10 @@ +import clsx from "clsx"; import Image from "next/image"; interface Props { image: string | null | undefined; name: string; + size?: "sm" | "xs"; } export const getInitials = (name: string) => @@ -10,17 +12,30 @@ export const getInitials = (name: string) => .join("") .toUpperCase(); -export default function UserAvatar({ image, name }: Props) { +export default function UserAvatar({ image, name, size = "sm" }: Props) { + const sizeClass = (function (size: "sm" | "xs") { + if (size === "sm") return "w-7 h-7"; + if (size === "xs") return "w-5 h-5"; + })(size); + const sizeValue = (function (size: "sm" | "xs") { + if (size === "sm") return 28; + if (size === "xs") return 20; + })(size); return image ? ( user avatar ) : ( - + {getInitials(name)} diff --git a/keep-ui/entities/users/model/useUser.ts b/keep-ui/entities/users/model/useUser.ts new file mode 100644 index 000000000..f765314cd --- /dev/null +++ b/keep-ui/entities/users/model/useUser.ts @@ -0,0 +1,6 @@ +import { useUsers } from "./useUsers"; + +export function useUser(email: string) { + const { data: users = [] } = useUsers(); + return users.find((user) => user.email === email) ?? null; +} diff --git a/keep-ui/utils/hooks/useUsers.ts b/keep-ui/entities/users/model/useUsers.ts similarity index 73% rename from keep-ui/utils/hooks/useUsers.ts rename to keep-ui/entities/users/model/useUsers.ts index 48368b77c..d87bb235f 100644 --- a/keep-ui/utils/hooks/useUsers.ts +++ b/keep-ui/entities/users/model/useUsers.ts @@ -2,7 +2,7 @@ import { User } from "@/app/(keep)/settings/models"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; import { SWRConfiguration } from "swr"; import useSWRImmutable from "swr/immutable"; -import { useApiUrl } from "./useConfig"; +import { useApiUrl } from "../../../utils/hooks/useConfig"; import { fetcher } from "utils/fetcher"; export const useUsers = (options: SWRConfiguration = {}) => { @@ -10,8 +10,8 @@ export const useUsers = (options: SWRConfiguration = {}) => { const { data: session } = useSession(); return useSWRImmutable( - () => (session ? `${apiUrl}/auth/users` : null), - (url) => fetcher(url, session?.accessToken), + () => (session ? "/auth/users" : null), + (url) => fetcher(apiUrl + url, session?.accessToken), options ); }; diff --git a/keep-ui/entities/users/ui/UserStatefulAvatar.tsx b/keep-ui/entities/users/ui/UserStatefulAvatar.tsx new file mode 100644 index 000000000..53d9bf43d --- /dev/null +++ b/keep-ui/entities/users/ui/UserStatefulAvatar.tsx @@ -0,0 +1,28 @@ +import UserAvatar from "@/components/navbar/UserAvatar"; +import { useUser } from "../model/useUser"; +import { Icon } from "@tremor/react"; +import { UserCircleIcon } from "@heroicons/react/24/outline"; +import clsx from "clsx"; + +export function UserStatefulAvatar({ + email, + size = "sm", +}: { + email: string; + size?: "sm" | "xs"; +}) { + const user = useUser(email); + const sizeClass = (function (size: "sm" | "xs") { + if (size === "sm") return "[&>svg]:w-7 [&>svg]:h-7"; + if (size === "xs") return "[&>svg]:w-5 [&>svg]:h-5"; + })(size); + if (!user) { + return ( + + ); + } + return ; +} diff --git a/keep-ui/entities/users/ui/index.ts b/keep-ui/entities/users/ui/index.ts new file mode 100644 index 000000000..b419f1b44 --- /dev/null +++ b/keep-ui/entities/users/ui/index.ts @@ -0,0 +1 @@ +export { UserStatefulAvatar } from "./UserStatefulAvatar"; diff --git a/keep-ui/features/create-or-update-incident/ui/create-or-update-incident-form.tsx b/keep-ui/features/create-or-update-incident/ui/create-or-update-incident-form.tsx index ddf50f976..0458f6007 100644 --- a/keep-ui/features/create-or-update-incident/ui/create-or-update-incident-form.tsx +++ b/keep-ui/features/create-or-update-incident/ui/create-or-update-incident-form.tsx @@ -10,7 +10,7 @@ import { SelectItem, } from "@tremor/react"; import { FormEvent, useEffect, useState } from "react"; -import { useUsers } from "utils/hooks/useUsers"; +import { useUsers } from "@/entities/users/model/useUsers"; import { useIncidentActions } from "@/entities/incidents/model"; import type { IncidentDto } from "@/entities/incidents/model"; import { getIncidentName } from "@/entities/incidents/lib/utils"; diff --git a/keep-ui/features/incident-list/ui/incidents-table.tsx b/keep-ui/features/incident-list/ui/incidents-table.tsx index f71c702a5..7f34c5813 100644 --- a/keep-ui/features/incident-list/ui/incidents-table.tsx +++ b/keep-ui/features/incident-list/ui/incidents-table.tsx @@ -35,6 +35,7 @@ import { useIncidentActions } from "@/entities/incidents/model"; import { IncidentSeverityBadge } from "@/entities/incidents/ui"; import { getIncidentName } from "@/entities/incidents/lib/utils"; import { DateTimeField, TablePagination } from "@/shared/ui"; +import { UserStatefulAvatar } from "@/entities/users/ui"; function SelectedRowActions({ selectedRowIds, @@ -228,7 +229,9 @@ export default function IncidentsTable({ columnHelper.display({ id: "assignee", header: "Assignee", - cell: ({ row }) => row.original.assignee, + cell: ({ row }) => ( + + ), }), columnHelper.accessor("creation_time", { id: "creation_time", diff --git a/keep-ui/package-lock.json b/keep-ui/package-lock.json index 551791941..11e155b6a 100644 --- a/keep-ui/package-lock.json +++ b/keep-ui/package-lock.json @@ -24,6 +24,7 @@ "@heroicons/react": "^2.1.5", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-tooltip": "^1.1.4", "@sentry/nextjs": "^8.38.0", "@svgr/webpack": "^8.0.1", "@tanstack/react-table": "^8.11.0", @@ -6331,6 +6332,39 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.4.tgz", + "integrity": "sha512-QpObUH/ZlpaO4YgHSaYzrLO2VuO+ZBFFgGzjMUPwtiYnAzzNNDPJeEGRrT7qNOrWm/Jr08M1vlp+vTHtnSQ0Uw==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", diff --git a/keep-ui/package.json b/keep-ui/package.json index 42a3e8395..172d2dfd2 100644 --- a/keep-ui/package.json +++ b/keep-ui/package.json @@ -25,6 +25,7 @@ "@heroicons/react": "^2.1.5", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-tooltip": "^1.1.4", "@sentry/nextjs": "^8.38.0", "@svgr/webpack": "^8.0.1", "@tanstack/react-table": "^8.11.0", diff --git a/keep-ui/shared/lib/status-utils.ts b/keep-ui/shared/lib/status-utils.ts new file mode 100644 index 000000000..7eabd2f8f --- /dev/null +++ b/keep-ui/shared/lib/status-utils.ts @@ -0,0 +1,37 @@ +import { + ExclamationCircleIcon, + CheckCircleIcon, + CircleStackIcon, + PauseIcon, +} from "@heroicons/react/24/outline"; +import { IoIosGitPullRequest } from "react-icons/io"; + +export const getStatusIcon = (status: string) => { + switch (status.toLowerCase()) { + case "firing": + return ExclamationCircleIcon; + case "resolved": + return CheckCircleIcon; + case "acknowledged": + return PauseIcon; + case "merged": + return IoIosGitPullRequest; + default: + return CircleStackIcon; + } +}; + +export const getStatusColor = (status: string) => { + switch (status.toLowerCase()) { + case "firing": + return "red"; + case "resolved": + return "green"; + case "acknowledged": + return "gray"; + case "merged": + return "purple"; + default: + return "gray"; + } +}; diff --git a/keep-ui/shared/ui/FieldHeader.tsx b/keep-ui/shared/ui/FieldHeader.tsx index 5c99f4c23..c40813331 100644 --- a/keep-ui/shared/ui/FieldHeader.tsx +++ b/keep-ui/shared/ui/FieldHeader.tsx @@ -1,3 +1,13 @@ -export const FieldHeader = ({ children }: { children: React.ReactNode }) => ( -

{children}

+import clsx from "clsx"; + +export const FieldHeader = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => ( +

+ {children} +

); diff --git a/keep-ui/shared/ui/Tooltip/Tooltip.tsx b/keep-ui/shared/ui/Tooltip/Tooltip.tsx new file mode 100644 index 000000000..7da4c271c --- /dev/null +++ b/keep-ui/shared/ui/Tooltip/Tooltip.tsx @@ -0,0 +1,93 @@ +// Tremor Tooltip [v0.1.0] + +import React from "react"; +import * as TooltipPrimitives from "@radix-ui/react-tooltip"; + +import clsx from "clsx"; + +interface TooltipProps + extends Omit, + Pick< + TooltipPrimitives.TooltipProps, + "open" | "defaultOpen" | "onOpenChange" | "delayDuration" + > { + content: React.ReactNode; + onClick?: React.MouseEventHandler; + side?: "bottom" | "left" | "top" | "right"; + showArrow?: boolean; +} + +const Tooltip = React.forwardRef< + React.ElementRef, + TooltipProps +>( + ( + { + children, + className, + content, + delayDuration, + defaultOpen, + open, + onClick, + onOpenChange, + showArrow = true, + side, + sideOffset = 10, + asChild, + ...props + }: TooltipProps, + forwardedRef + ) => { + return ( + + + + {children} + + + + {content} + {showArrow ? ( + + + + + ); + } +); + +Tooltip.displayName = "Tooltip"; + +export { Tooltip, type TooltipProps }; diff --git a/keep-ui/shared/ui/Tooltip/index.ts b/keep-ui/shared/ui/Tooltip/index.ts new file mode 100644 index 000000000..cf5cc016b --- /dev/null +++ b/keep-ui/shared/ui/Tooltip/index.ts @@ -0,0 +1,2 @@ +export { Tooltip } from "./Tooltip"; +export type { TooltipProps } from "./Tooltip"; diff --git a/keep-ui/shared/ui/index.ts b/keep-ui/shared/ui/index.ts index 307db1bbb..fc196b355 100644 --- a/keep-ui/shared/ui/index.ts +++ b/keep-ui/shared/ui/index.ts @@ -3,3 +3,6 @@ export { TabLinkNavigation, TabNavigationLink } from "./TabLinkNavigation"; export { DateTimeField } from "./DateTimeField"; export { FieldHeader } from "./FieldHeader"; export { EmptyStateCard } from "./EmptyState"; +export { Tooltip } from "./Tooltip"; + +export type { TooltipProps } from "./Tooltip"; diff --git a/keep-ui/tailwind.config.js b/keep-ui/tailwind.config.js index da4514760..9553ecaf2 100644 --- a/keep-ui/tailwind.config.js +++ b/keep-ui/tailwind.config.js @@ -105,11 +105,42 @@ module.exports = { "tremor-title": ["1.125rem", { lineHeight: "1.75rem" }], "tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }], }, + keyframes: { + hide: { + from: { opacity: "1" }, + to: { opacity: "0" }, + }, + slideDownAndFade: { + from: { opacity: "0", transform: "translateY(-6px)" }, + to: { opacity: "1", transform: "translateY(0)" }, + }, + slideLeftAndFade: { + from: { opacity: "0", transform: "translateX(6px)" }, + to: { opacity: "1", transform: "translateX(0)" }, + }, + slideUpAndFade: { + from: { opacity: "0", transform: "translateY(6px)" }, + to: { opacity: "1", transform: "translateY(0)" }, + }, + slideRightAndFade: { + from: { opacity: "0", transform: "translateX(-6px)" }, + to: { opacity: "1", transform: "translateX(0)" }, + }, + }, animation: { "scroll-shadow-left": "auto linear 0s 1 normal none running scroll-shadow-left", "scroll-shadow-right": "auto linear 0s 1 normal none running scroll-shadow-right", + // Tremor tooltip + hide: "hide 150ms cubic-bezier(0.16, 1, 0.3, 1)", + slideDownAndFade: + "slideDownAndFade 150ms cubic-bezier(0.16, 1, 0.3, 1)", + slideLeftAndFade: + "slideLeftAndFade 150ms cubic-bezier(0.16, 1, 0.3, 1)", + slideUpAndFade: "slideUpAndFade 150ms cubic-bezier(0.16, 1, 0.3, 1)", + slideRightAndFade: + "slideRightAndFade 150ms cubic-bezier(0.16, 1, 0.3, 1)", }, }, },