From 71b144248f3430f5044da75d0e7c6b7370f0bf6e Mon Sep 17 00:00:00 2001 From: Tal Date: Sun, 24 Nov 2024 17:48:32 +0200 Subject: [PATCH 1/2] 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 ( ); } 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)", }, }, },