From 3b31a333ccb1bdf2a06cb05903de0d2784b62d29 Mon Sep 17 00:00:00 2001 From: Kirill Chernakov Date: Sun, 24 Nov 2024 20:35:06 +0400 Subject: [PATCH] feat: consistent severity visuals (#2611) Co-authored-by: Tal --- keep-ui/app/(keep)/alerts/alert-assignee.tsx | 2 +- .../(keep)/alerts/alert-severity-border.tsx | 90 +++++++-- keep-ui/app/(keep)/alerts/alert-sidebar.tsx | 117 +++++++---- .../alerts/alert-table-alert-facets.tsx | 12 +- .../(keep)/alerts/alert-table-facet-utils.tsx | 39 +--- .../(keep)/alerts/alert-table-facet-value.tsx | 186 +++++++++++++----- .../app/(keep)/alerts/alert-table-headers.tsx | 14 +- .../app/(keep)/alerts/alert-table-utils.tsx | 12 ++ keep-ui/app/(keep)/alerts/alert-table.tsx | 3 +- keep-ui/app/(keep)/alerts/alert-timeline.tsx | 92 ++++----- .../[id]/activity/incident-activity.tsx | 2 +- .../[id]/alerts/incident-alert-menu.tsx | 12 +- .../incidents/[id]/alerts/incident-alerts.tsx | 37 +++- .../(keep)/settings/auth/groups-sidebar.tsx | 6 +- .../app/(keep)/settings/auth/groups-tab.tsx | 2 +- .../(keep)/settings/auth/permissions-tab.tsx | 2 +- .../(keep)/settings/auth/users-settings.tsx | 2 +- keep-ui/components/navbar/UserAvatar.tsx | 25 ++- keep-ui/entities/users/model/useUser.ts | 6 + .../users/model}/useUsers.ts | 6 +- .../entities/users/ui/UserStatefulAvatar.tsx | 28 +++ keep-ui/entities/users/ui/index.ts | 1 + .../ui/create-or-update-incident-form.tsx | 2 +- .../incident-list/ui/incidents-table.tsx | 5 +- keep-ui/package-lock.json | 34 ++++ keep-ui/package.json | 1 + keep-ui/shared/lib/status-utils.ts | 37 ++++ keep-ui/shared/ui/FieldHeader.tsx | 14 +- keep-ui/shared/ui/Tooltip/Tooltip.tsx | 93 +++++++++ keep-ui/shared/ui/Tooltip/index.ts | 2 + keep-ui/shared/ui/index.ts | 3 + keep-ui/tailwind.config.js | 31 +++ 32 files changed, 671 insertions(+), 247 deletions(-) create mode 100644 keep-ui/entities/users/model/useUser.ts rename keep-ui/{utils/hooks => entities/users/model}/useUsers.ts (73%) create mode 100644 keep-ui/entities/users/ui/UserStatefulAvatar.tsx create mode 100644 keep-ui/entities/users/ui/index.ts create mode 100644 keep-ui/shared/lib/status-utils.ts create mode 100644 keep-ui/shared/ui/Tooltip/Tooltip.tsx create mode 100644 keep-ui/shared/ui/Tooltip/index.ts diff --git a/keep-ui/app/(keep)/alerts/alert-assignee.tsx b/keep-ui/app/(keep)/alerts/alert-assignee.tsx index c3dc67627..525d00224 100644 --- a/keep-ui/app/(keep)/alerts/alert-assignee.tsx +++ b/keep-ui/app/(keep)/alerts/alert-assignee.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { NameInitialsAvatar } from "react-name-initials-avatar"; -import { useUsers } from "utils/hooks/useUsers"; +import { useUsers } from "@/entities/users/model/useUsers"; interface Props { assignee: string | undefined; diff --git a/keep-ui/app/(keep)/alerts/alert-severity-border.tsx b/keep-ui/app/(keep)/alerts/alert-severity-border.tsx index 34b0b6d3e..f38add3d4 100644 --- a/keep-ui/app/(keep)/alerts/alert-severity-border.tsx +++ b/keep-ui/app/(keep)/alerts/alert-severity-border.tsx @@ -1,27 +1,59 @@ import clsx from "clsx"; import { Severity } from "./models"; +const getSeverityBgClassName = (severity?: Severity) => { + switch (severity) { + case "critical": + return "bg-red-500"; + case "high": + case "error": + return "bg-orange-500"; + case "warning": + return "bg-yellow-500"; + case "info": + return "bg-blue-500"; + default: + return "bg-emerald-500"; + } +}; + +const getSeverityLabelClassName = (severity?: Severity) => { + switch (severity) { + case "critical": + return "bg-red-100"; + case "high": + case "error": + return "bg-orange-100"; + case "warning": + return "bg-yellow-100"; + case "info": + return "bg-blue-100"; + default: + return "bg-emerald-100"; + } +}; + +const getSeverityTextClassName = (severity?: Severity) => { + switch (severity) { + case "critical": + return "text-red-500"; + case "high": + case "error": + return "text-orange-500"; + case "warning": + return "text-amber-900"; + case "info": + return "text-blue-500"; + default: + return "text-emerald-500"; + } +}; + export function AlertSeverityBorder({ severity, }: { severity: Severity | undefined; }) { - const getSeverityBgClassName = (severity?: Severity) => { - switch (severity) { - case "critical": - return "bg-red-500"; - case "high": - case "error": - return "bg-orange-500"; - case "warning": - return "bg-yellow-500"; - case "info": - return "bg-blue-500"; - default: - return "bg-emerald-500"; - } - }; - return (
); } + +export function AlertSeverityBorderIcon({ severity }: { severity: Severity }) { + return ( +
+ ); +} + +export function AlertSeverityLabel({ severity }: { severity: Severity }) { + return ( + +
+ + {severity} + + + ); +} diff --git a/keep-ui/app/(keep)/alerts/alert-sidebar.tsx b/keep-ui/app/(keep)/alerts/alert-sidebar.tsx index bf985960e..41ae7b46c 100644 --- a/keep-ui/app/(keep)/alerts/alert-sidebar.tsx +++ b/keep-ui/app/(keep)/alerts/alert-sidebar.tsx @@ -2,12 +2,20 @@ import { Fragment } from "react"; import Image from "next/image"; import { Dialog, Transition } from "@headlessui/react"; import { AlertDto } from "./models"; -import { Button, Title, Card, Badge } from "@tremor/react"; +import { Button, Title, Badge } from "@tremor/react"; import { IoMdClose } from "react-icons/io"; import AlertTimeline from "./alert-timeline"; import { useAlerts } from "utils/hooks/useAlerts"; import { TopologyMap } from "../topology/ui/map"; import { TopologySearchProvider } from "@/app/(keep)/topology/TopologySearchContext"; +import { + AlertSeverityBorderIcon, + AlertSeverityLabel, +} from "./alert-severity-border"; +import { FieldHeader } from "@/shared/ui/FieldHeader"; +import { QuestionMarkCircleIcon } from "@heroicons/react/20/solid"; +import { Tooltip } from "@/shared/ui/Tooltip"; +import { Link } from "@/components/ui"; type AlertSidebarProps = { isOpen: boolean; @@ -57,11 +65,14 @@ const AlertSidebar = ({ isOpen, toggle, alert }: AlertSidebarProps) => { {/*Will add soon*/} {/**/} {/**/} - - Alert Details - - Beta - + + {alert?.severity && ( + + )} + {alert?.name ? alert.name : "Alert Details"}
@@ -72,43 +83,63 @@ const AlertSidebar = ({ isOpen, toggle, alert }: AlertSidebarProps) => {
{alert && (
- -
-

- Name: {alert.name} -

-

- Service: {alert.service} -

-

- Severity: {alert.severity} -

-

- {alert.source![0]} -

-

- Description: {alert.description} -

-

- Fingerprint: {alert.fingerprint} -

-
-
- - - +
+

+ Name + {alert.name} +

+

+ Service + + {alert.service} + +

+

+ Source + {alert.source![0]} +

+

+ Description + {alert.description} +

+

+ + Fingerprint + + Fingerprints are unique identifiers associated with + alert instances in Keep. Every provider declares the + fields fingerprints are calculated upon.{" "} + + Docs + + + } + className="z-50" + > + + + + {alert.fingerprint} +

+
+ Related Services = ({ if (a.label === "n/a") return 1; if (b.label === "n/a") return -1; const orderDiff = - getSeverityOrder(a.label) - getSeverityOrder(b.label); + getSeverityOrder(b.label) - getSeverityOrder(a.label); if (orderDiff !== 0) return orderDiff; return b.count - a.count; }); @@ -253,7 +249,7 @@ export const AlertFacets: React.FC = ({ showSkeleton={showSkeleton} /> diff --git a/keep-ui/app/(keep)/alerts/alert-table-facet-utils.tsx b/keep-ui/app/(keep)/alerts/alert-table-facet-utils.tsx index a26f8ab1d..51c5ff196 100644 --- a/keep-ui/app/(keep)/alerts/alert-table-facet-utils.tsx +++ b/keep-ui/app/(keep)/alerts/alert-table-facet-utils.tsx @@ -1,16 +1,5 @@ import { FacetFilters } from "./alert-table-facet-types"; -import { AlertDto, Severity } from "./models"; -import { - ChevronDownIcon, - ChevronRightIcon, - UserCircleIcon, - BellIcon, - ExclamationCircleIcon, - CheckCircleIcon, - CircleStackIcon, - BellSlashIcon, - FireIcon, -} from "@heroicons/react/24/outline"; +import { AlertDto } from "./models"; import { isQuickPresetRange } from "@/components/ui/DateRangePicker"; export const getFilteredAlertsForFacet = ( @@ -83,32 +72,6 @@ export const getFilteredAlertsForFacet = ( }); }; -export const getStatusIcon = (status: string) => { - switch (status.toLowerCase()) { - case "firing": - return ExclamationCircleIcon; - case "resolved": - return CheckCircleIcon; - case "acknowledged": - return CircleStackIcon; - default: - return CircleStackIcon; - } -}; - -export const getStatusColor = (status: string) => { - switch (status.toLowerCase()) { - case "firing": - return "red"; - case "resolved": - return "green"; - case "acknowledged": - return "blue"; - default: - return "gray"; - } -}; - export const getSeverityOrder = (severity: string): number => { switch (severity) { case "low": diff --git a/keep-ui/app/(keep)/alerts/alert-table-facet-value.tsx b/keep-ui/app/(keep)/alerts/alert-table-facet-value.tsx index 15bc1d991..83b588c03 100644 --- a/keep-ui/app/(keep)/alerts/alert-table-facet-value.tsx +++ b/keep-ui/app/(keep)/alerts/alert-table-facet-value.tsx @@ -1,17 +1,22 @@ -import React from "react"; +import React, { useCallback, useMemo } from "react"; import { Icon } from "@tremor/react"; import Image from "next/image"; import { Text } from "@tremor/react"; import { FacetValueProps } from "./alert-table-facet-types"; -import { getStatusIcon, getStatusColor } from "./alert-table-facet-utils"; -import { - UserCircleIcon, - BellIcon, - BellSlashIcon, - FireIcon, -} from "@heroicons/react/24/outline"; -import AlertSeverity from "./alert-severity"; +import { getStatusIcon, getStatusColor } from "@/shared/lib/status-utils"; +import { BellIcon, BellSlashIcon, FireIcon } from "@heroicons/react/24/outline"; import { Severity } from "./models"; +import { AlertSeverityBorderIcon } from "./alert-severity-border"; +import clsx from "clsx"; +import { useIncidents } from "@/utils/hooks/useIncidents"; +import { getIncidentName } from "@/entities/incidents/lib/utils"; +import { UserStatefulAvatar } from "@/entities/users/ui"; +import { useUser } from "@/entities/users/model/useUser"; + +const AssigneeLabel = ({ email }: { email: string }) => { + const user = useUser(email); + return user ? user.name : email; +}; export const FacetValue: React.FC = ({ label, @@ -22,6 +27,31 @@ export const FacetValue: React.FC = ({ showIcon = false, facetFilters, }) => { + const { data: incidents } = useIncidents( + true, + 100, + undefined, + undefined, + undefined, + { + revalidateOnFocus: false, + } + ); + + const incidentMap = useMemo(() => { + return new Map( + incidents?.items.map((incident) => [ + incident.id.replaceAll("-", ""), + incident, + ]) || [] + ); + }, [incidents]); + + const incident = useMemo( + () => (facetKey === "incident" ? incidentMap.get(label) : null), + [incidentMap, facetKey, label] + ); + const handleCheckboxClick = (e: React.MouseEvent) => { e.stopPropagation(); onSelect(label, false, false); @@ -41,6 +71,95 @@ export const FacetValue: React.FC = ({ } }; + const getValueIcon = useCallback( + (label: string, facetKey: string) => { + if (facetKey === "source") { + return ( + {label} + ); + } + if (facetKey === "severity") { + return ; + } + if (facetKey === "assignee") { + return ; + } + if (facetKey === "status") { + return ( + + ); + } + if (facetKey === "dismissed") { + return ( + + ); + } + if (facetKey === "incident") { + if (incident) { + return ( + + ); + } + return ( + + ); + } + return null; + }, + [incident] + ); + + const humanizeLabel = useCallback( + (label: string, facetKey: string) => { + if (facetKey === "assignee") { + if (label === "n/a") { + return "Not assigned"; + } + return ; + } + if (facetKey === "incident") { + if (label === "n/a") { + return "No incident"; + } + if (incident) { + return getIncidentName(incident); + } else { + return label; + } + } + if (facetKey === "dismissed") { + return label === "true" ? "Dismissed" : "Not dismissed"; + } + return {label}; + }, + [incident] + ); + const currentFilter = facetFilters[facetKey] || []; const isValueSelected = !currentFilter?.length || currentFilter.includes(label); @@ -61,54 +180,13 @@ export const FacetValue: React.FC = ({ />
-
+
{showIcon && ( -
- {facetKey === "source" && ( - {label} - )} - {facetKey === "severity" && ( - - )} - {facetKey === "assignee" && ( - - )} - {facetKey === "status" && ( - - )} - {facetKey === "dismissed" && ( - - )} - {facetKey === "incident" && ( - - )} +
+ {getValueIcon(label, facetKey)}
)} - {label} + {humanizeLabel(label, facetKey)}
diff --git a/keep-ui/app/(keep)/alerts/alert-table-headers.tsx b/keep-ui/app/(keep)/alerts/alert-table-headers.tsx index d1606ffc9..b70e563cd 100644 --- a/keep-ui/app/(keep)/alerts/alert-table-headers.tsx +++ b/keep-ui/app/(keep)/alerts/alert-table-headers.tsx @@ -63,8 +63,8 @@ const DraggableHeaderCell = ({ column.getIsPinned() !== false ? "default" : isDragging - ? "grabbing" - : "grab", + ? "grabbing" + : "grab", }; // TODO: fix multiple pinned columns @@ -82,8 +82,11 @@ const DraggableHeaderCell = ({ >
{/* Flex container */} + {children} {/* Column name or text */} {column.getCanSort() && ( // Sorting icon to the left <> + {/* Custom styled vertical line separator */} +
{ @@ -96,8 +99,8 @@ const DraggableHeaderCell = ({ column.getNextSortingOrder() === "asc" ? "Sort ascending" : column.getNextSortingOrder() === "desc" - ? "Sort descending" - : "Clear sort" + ? "Sort descending" + : "Clear sort" } > {/* Icon logic */} @@ -111,11 +114,8 @@ const DraggableHeaderCell = ({ )} - {/* Custom styled vertical line separator */} -
)} - {children} {/* Column name or text */}
{column.getIsPinned() === false && ( diff --git a/keep-ui/app/(keep)/alerts/alert-table-utils.tsx b/keep-ui/app/(keep)/alerts/alert-table-utils.tsx index bd41cbc22..0f5924559 100644 --- a/keep-ui/app/(keep)/alerts/alert-table-utils.tsx +++ b/keep-ui/app/(keep)/alerts/alert-table-utils.tsx @@ -22,6 +22,7 @@ import { MdOutlineNotificationsOff, } from "react-icons/md"; import { AlertSeverityBorder } from "./alert-severity-border"; +import { getStatusIcon, getStatusColor } from "@/shared/lib/status-utils"; export const DEFAULT_COLS = [ "severity", @@ -272,6 +273,17 @@ export const useAlertTableCols = ( id: "status", minSize: 100, header: "Status", + cell: (context) => ( + + + {context.getValue()} + + ), }), columnHelper.accessor("lastReceived", { id: "lastReceived", diff --git a/keep-ui/app/(keep)/alerts/alert-table.tsx b/keep-ui/app/(keep)/alerts/alert-table.tsx index 24b66ec7e..d61cb2167 100644 --- a/keep-ui/app/(keep)/alerts/alert-table.tsx +++ b/keep-ui/app/(keep)/alerts/alert-table.tsx @@ -1,8 +1,7 @@ import { useRef, useState } from "react"; -import { Table, Callout, Card } from "@tremor/react"; +import { Table, Card } from "@tremor/react"; import { AlertsTableBody } from "./alerts-table-body"; import { AlertDto } from "./models"; -import { CircleStackIcon } from "@heroicons/react/24/outline"; import { getCoreRowModel, useReactTable, diff --git a/keep-ui/app/(keep)/alerts/alert-timeline.tsx b/keep-ui/app/(keep)/alerts/alert-timeline.tsx index 02fe484e0..6653ea45e 100644 --- a/keep-ui/app/(keep)/alerts/alert-timeline.tsx +++ b/keep-ui/app/(keep)/alerts/alert-timeline.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Subtitle, Button } from "@tremor/react"; +import { Subtitle, Button, Card, Title } from "@tremor/react"; import { Chrono } from "react-chrono"; import Image from "next/image"; import { ArrowPathIcon } from "@heroicons/react/24/outline"; @@ -71,9 +71,9 @@ const AlertTimeline: React.FC = ({ )); return ( -
+
- Timeline + Timeline
- {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)", }, }, },