diff --git a/keep-ui/app/alerts/[id]/page.tsx b/keep-ui/app/alerts/[id]/page.tsx index 89a007659..2137123d8 100644 --- a/keep-ui/app/alerts/[id]/page.tsx +++ b/keep-ui/app/alerts/[id]/page.tsx @@ -1,4 +1,4 @@ -import AlertsPage from "../alerts.client"; +import AlertsPage from "../alerts"; type PageProps = { params: { id: string }; diff --git a/keep-ui/app/alerts/alert-table-alert-facets.tsx b/keep-ui/app/alerts/alert-table-alert-facets.tsx index 1366558b4..3c57b14ed 100644 --- a/keep-ui/app/alerts/alert-table-alert-facets.tsx +++ b/keep-ui/app/alerts/alert-table-alert-facets.tsx @@ -16,6 +16,7 @@ import { AddFacetModal, } from "./alert-table-facet-dynamic"; import { PlusIcon } from "@heroicons/react/24/outline"; +import { usePathname } from "next/navigation"; export const AlertFacets: React.FC = ({ alerts, @@ -26,7 +27,9 @@ export const AlertFacets: React.FC = ({ onDelete, className, table, + showSkeleton, }) => { + const pathname = usePathname(); const timeRangeFilter = table .getState() .columnFilters.find((filter) => filter.id === "lastReceived"); @@ -35,7 +38,7 @@ export const AlertFacets: React.FC = ({ | { start: Date; end: Date; isFromCalendar: boolean } | undefined; - const presetName = window.location.pathname.split("/").pop() || "default"; + const presetName = pathname?.split("/").pop() || "default"; const [isModalOpen, setIsModalOpen] = useLocalStorage( `addFacetModalOpen-${presetName}`, @@ -207,6 +210,7 @@ export const AlertFacets: React.FC = ({ } facetKey="severity" facetFilters={facetFilters} + showSkeleton={showSkeleton} /> = ({ } facetKey="status" facetFilters={facetFilters} + showSkeleton={showSkeleton} /> = ({ } facetKey="source" facetFilters={facetFilters} + showSkeleton={showSkeleton} /> = ({ } facetKey="assignee" facetFilters={facetFilters} + showSkeleton={showSkeleton} /> = ({ } facetKey="dismissed" facetFilters={facetFilters} + showSkeleton={showSkeleton} /> = ({ handleSelect("incident", value, exclusive, isAllOnly) } facetFilters={facetFilters} + showSkeleton={showSkeleton} /> {/* Dynamic facets */} {dynamicFacets.map((facet) => ( diff --git a/keep-ui/app/alerts/alert-table-facet-types.tsx b/keep-ui/app/alerts/alert-table-facet-types.tsx index d07439d4b..bcee24fd2 100644 --- a/keep-ui/app/alerts/alert-table-facet-types.tsx +++ b/keep-ui/app/alerts/alert-table-facet-types.tsx @@ -29,6 +29,7 @@ export interface FacetProps { facetKey: string; facetFilters: FacetFilters; showIcon?: boolean; + showSkeleton?: boolean; } export interface AlertFacetsProps { @@ -44,4 +45,5 @@ export interface AlertFacetsProps { onDelete: (facetKey: string) => void; className?: string; table: Table; + showSkeleton?: boolean; } diff --git a/keep-ui/app/alerts/alert-table-facet.tsx b/keep-ui/app/alerts/alert-table-facet.tsx index 03fa7556c..83a53a909 100644 --- a/keep-ui/app/alerts/alert-table-facet.tsx +++ b/keep-ui/app/alerts/alert-table-facet.tsx @@ -4,6 +4,8 @@ import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/20/solid"; import { FacetProps } from "./alert-table-facet-types"; import { FacetValue } from "./alert-table-facet-value"; import { useLocalStorage } from "utils/hooks/useLocalStorage"; +import { usePathname } from "next/navigation"; +import Skeleton from "react-loading-skeleton"; export const Facet: React.FC = ({ name, @@ -12,9 +14,11 @@ export const Facet: React.FC = ({ facetKey, facetFilters, showIcon = true, + showSkeleton, }) => { + const pathname = usePathname(); // Get preset name from URL - const presetName = window.location.pathname.split("/").pop() || "default"; + const presetName = pathname?.split("/").pop() || "default"; // Store open/close state in localStorage with a unique key per preset and facet const [isOpen, setIsOpen] = useLocalStorage( @@ -60,7 +64,17 @@ export const Facet: React.FC = ({ )}
- {values.length > 0 ? ( + {showSkeleton ? ( + Array.from({ length: 3 }).map((_, index) => ( +
+ + +
+ )) + ) : values.length > 0 ? ( filteredValues.map((value) => ( ( { // if presetName is alert-history, do not open sidebar @@ -303,21 +303,12 @@ export function AlertTable({ setDynamicFacets={setDynamicFacets} onDelete={handleFacetDelete} table={table} + showSkeleton={showSkeleton} />
- {isAsyncLoading && ( - - Alerts will show up in this table as they are added to Keep... - - )} {/* For dynamic preset, add alert tabs*/} {!presetStatic && ( + {Array(20) + .fill("") + .map((index, rowIndex) => ( + + {table.getAllColumns().map((c, cellIndex) => ( + + + + ))} + + ))} + + ); + } + return ( {table.getRowModel().rows.map((row) => { @@ -100,11 +124,7 @@ export function AlertsTableBody({ "relative z-[1]" // Ensure cell content is above the border )} > - {showSkeleton ? ( - - ) : ( - flexRender(cell.column.columnDef.cell, cell.getContext()) - )} + {flexRender(cell.column.columnDef.cell, cell.getContext())} ); })} diff --git a/keep-ui/app/alerts/alerts.client.tsx b/keep-ui/app/alerts/alerts.client.tsx deleted file mode 100644 index f409867bf..000000000 --- a/keep-ui/app/alerts/alerts.client.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { useSession } from "next-auth/react"; -import Loading from "../loading"; -import Alerts from "./alerts"; - -type AlertsPageProps = { - presetName: string; -}; - -export default function AlertsPage({ presetName }: AlertsPageProps) { - const { data: session, status } = useSession(); - - const router = useRouter(); - - if (status === "loading") { - return ; - } - - if (status === "unauthenticated") { - console.log("unauthenticated"); - router.push("/signin"); - } - - if (session && !session.tenantId) { - console.log("no tenantId"); - router.push("/signin"); - } - - return ; -} diff --git a/keep-ui/app/alerts/alerts.tsx b/keep-ui/app/alerts/alerts.tsx index d70c6318d..3aa011e64 100644 --- a/keep-ui/app/alerts/alerts.tsx +++ b/keep-ui/app/alerts/alerts.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useEffect, useMemo, useState } from "react"; import { Preset } from "./models"; import { useAlerts } from "utils/hooks/useAlerts"; @@ -16,7 +18,8 @@ import { useRouter, useSearchParams } from "next/navigation"; import AlertChangeStatusModal from "./alert-change-status-modal"; import { useAlertPolling } from "utils/hooks/usePusher"; import NotFound from "@/app/not-found"; -import NotAuthorized from "@/app/not-authorized"; +import { useMounted } from "@/shared/lib/hooks/useMounted"; +import { useSession } from "next-auth/react"; const defaultPresets: Preset[] = [ { @@ -107,6 +110,11 @@ export default function Alerts({ presetName }: AlertsProps) { mutate: mutateAlerts, error: alertsError, } = usePresetAlerts(selectedPreset ? selectedPreset.name : ""); + + // const isMounted = useMounted(); + const { status: sessionStatus } = useSession(); + const isLoading = isAsyncLoading || sessionStatus === "loading"; + useEffect(() => { const fingerprint = searchParams?.get("alertPayloadFingerprint"); if (fingerprint) { @@ -126,14 +134,6 @@ export default function Alerts({ presetName }: AlertsProps) { if (!selectedPreset) { return ; } - if (alertsError) { - if (alertsError.statusCode === 401) { - console.log("unauthenticated 401"); - window.location.href = "/signin"; - return null; - } - return ; - } return ( <> @@ -141,7 +141,7 @@ export default function Alerts({ presetName }: AlertsProps) { key={selectedPreset.name} preset={selectedPreset} alerts={alerts} - isAsyncLoading={isAsyncLoading} + isAsyncLoading={isLoading} setTicketModalAlert={setTicketModalAlert} setNoteModalAlert={setNoteModalAlert} setRunWorkflowModalAlert={setRunWorkflowModalAlert} diff --git a/keep-ui/components/navbar/AlertsLinks.tsx b/keep-ui/components/navbar/AlertsLinks.tsx index 71b998bf6..ce094f415 100644 --- a/keep-ui/components/navbar/AlertsLinks.tsx +++ b/keep-ui/components/navbar/AlertsLinks.tsx @@ -14,6 +14,7 @@ import { useLocalStorage } from "utils/hooks/useLocalStorage"; import { ActionMeta, MultiValue } from "react-select"; import { useTags } from "utils/hooks/useTags"; import { usePresets } from "utils/hooks/usePresets"; +import { useMounted } from "@/shared/lib/hooks/useMounted"; import clsx from "clsx"; type AlertsLinksProps = { @@ -22,6 +23,8 @@ type AlertsLinksProps = { export const AlertsLinks = ({ session }: AlertsLinksProps) => { const [isTagModalOpen, setIsTagModalOpen] = useState(false); + const isMounted = useMounted(); + const [storedTags, setStoredTags] = useLocalStorage( "selectedTags", [] @@ -59,7 +62,7 @@ export const AlertsLinks = ({ session }: AlertsLinksProps) => { // Determine if we should show the feed link const shouldShowFeed = (() => { // If we have server data, check if feed preset exists - if (staticPresets) { + if (staticPresets.length > 0) { return staticPresets.some((preset) => preset.name === "feed"); } @@ -69,13 +72,19 @@ export const AlertsLinks = ({ session }: AlertsLinksProps) => { return staticPresetsOrderFromLS?.some((preset) => preset.name === "feed"); } - // If we're still loading (no data and no error), show based on cache + // For the initial render on the server, always show feed + if (!isMounted) { + return true; + } + return staticPresetsOrderFromLS?.some((preset) => preset.name === "feed"); })(); // Get the current alerts count only if we should show feed const currentAlertsCount = (() => { - if (!shouldShowFeed) return 0; + if (!shouldShowFeed) { + return 0; + } // First try to get from server data const serverPreset = staticPresets?.find( @@ -89,7 +98,7 @@ export const AlertsLinks = ({ session }: AlertsLinksProps) => { const cachedPreset = staticPresetsOrderFromLS?.find( (preset) => preset.name === "feed" ); - return cachedPreset?.alerts_count ?? 0; + return cachedPreset?.alerts_count ?? undefined; })(); return ( @@ -141,7 +150,7 @@ export const AlertsLinks = ({ session }: AlertsLinksProps) => { )} - {session && ( + {session && isMounted && ( {/* React Player for playing alert sound */} ); diff --git a/keep-ui/components/navbar/UserInfo.tsx b/keep-ui/components/navbar/UserInfo.tsx index 6c993d565..40173f5cf 100644 --- a/keep-ui/components/navbar/UserInfo.tsx +++ b/keep-ui/components/navbar/UserInfo.tsx @@ -17,7 +17,8 @@ import * as Frigade from "@frigade/react"; import { useState } from "react"; import Onboarding from "./Onboarding"; import { useSignOut } from "@/shared/lib/useSignOut"; -import { useSetSentryUser } from "@/shared/lib/useSetSentryUser"; + +const ONBOARDING_FLOW_ID = "flow_FHDz1hit"; type UserDropdownProps = { session: Session; @@ -89,7 +90,7 @@ type UserInfoProps = { }; export const UserInfo = ({ session }: UserInfoProps) => { - const [isOnboardingComplete, setIsOnboardingComplete] = useState(false); + const { flow } = Frigade.useFlow(ONBOARDING_FLOW_ID); const [isOnboardingOpen, setIsOnboardingOpen] = useState(false); return ( @@ -113,14 +114,11 @@ export const UserInfo = ({ session }: UserInfoProps) => { Join our Slack - {session && } - {isOnboardingComplete === false && ( + {flow?.isCompleted === false && (
  • setIsOnboardingComplete(true)} + flowId={ONBOARDING_FLOW_ID} onClick={() => setIsOnboardingOpen(true)} - // css={{ backgroundColor: "#F9FAFB" }} /> { />
  • )} + {session && } ); diff --git a/keep-ui/shared/lib/hooks/useMounted.tsx b/keep-ui/shared/lib/hooks/useMounted.tsx new file mode 100644 index 000000000..fe2b29974 --- /dev/null +++ b/keep-ui/shared/lib/hooks/useMounted.tsx @@ -0,0 +1,11 @@ +import { useState, useEffect } from "react"; + +export function useMounted() { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + return isMounted; +} diff --git a/keep-ui/utils/hooks/useAlerts.ts b/keep-ui/utils/hooks/useAlerts.ts index 28fa523d0..4bbe5fd65 100644 --- a/keep-ui/utils/hooks/useAlerts.ts +++ b/keep-ui/utils/hooks/useAlerts.ts @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { AlertDto } from "app/alerts/models"; import { useSession } from "next-auth/react"; import useSWR, { SWRConfiguration } from "swr"; @@ -53,40 +53,37 @@ export const useAlerts = () => { presetName: string, options: SWRConfiguration = { revalidateOnFocus: false } ) => { - const [alertsMap, setAlertsMap] = useState>( - new Map() - ); - const { data: alertsFromEndpoint = [], mutate, isLoading, - error, + error: alertsError, } = useAllAlerts(presetName, options); - useEffect(() => { - if (alertsFromEndpoint.length) { - const newAlertsMap = new Map( - alertsFromEndpoint.map((alertFromEndpoint) => [ - alertFromEndpoint.fingerprint, - { - ...alertFromEndpoint, - lastReceived: toDateObjectWithFallback( - alertFromEndpoint.lastReceived - ), - }, - ]) - ); - - setAlertsMap(newAlertsMap); + const alertsValue = useMemo(() => { + if (!alertsFromEndpoint.length) { + return []; } + + const alertsMap = new Map( + alertsFromEndpoint.map((alertFromEndpoint) => [ + alertFromEndpoint.fingerprint, + { + ...alertFromEndpoint, + lastReceived: toDateObjectWithFallback( + alertFromEndpoint.lastReceived + ), + }, + ]) + ); + return Array.from(alertsMap.values()); }, [alertsFromEndpoint]); return { - data: Array.from(alertsMap.values()), + data: alertsValue, mutate: mutate, isLoading: isLoading, - error: error, + error: alertsError, }; }; diff --git a/keep-ui/utils/hooks/usePresets.ts b/keep-ui/utils/hooks/usePresets.ts index 00e49588f..a0a0d76e2 100644 --- a/keep-ui/utils/hooks/usePresets.ts +++ b/keep-ui/utils/hooks/usePresets.ts @@ -8,8 +8,7 @@ import { useLocalStorage } from "utils/hooks/useLocalStorage"; import { useConfig } from "./useConfig"; import useSWRSubscription from "swr/subscription"; import { useWebsocket } from "./usePusher"; -import { usePathname, useSearchParams } from "next/navigation"; -import moment from "moment"; +import { useSearchParams } from "next/navigation"; export const usePresets = (type?: string, useFilters?: boolean) => { const { data: session } = useSession(); diff --git a/tests/e2e_tests/test_end_to_end.py b/tests/e2e_tests/test_end_to_end.py index 7dcabfa25..80236d1df 100644 --- a/tests/e2e_tests/test_end_to_end.py +++ b/tests/e2e_tests/test_end_to_end.py @@ -33,10 +33,9 @@ # - Spin up the environment using docker-compose. # - Run "playwright codegen localhost:3000" # - Copy the generated code to a new test function. -import re import string import sys - +from datetime import datetime # Running the tests in GitHub Actions: # - Look at the test-pr-e2e.yml file in the .github/workflows directory. @@ -44,31 +43,50 @@ # os.environ["PLAYWRIGHT_HEADLESS"] = "false" -def test_sanity(browser): +def setup_console_listener(page, log_entries): + """Set up console listener to capture logs.""" + page.on("console", lambda msg: (log_entries.append(f"{datetime.now()}: {msg.text}, location: {msg.location}"))) + +def save_failure_artifacts(page, log_entries): + """Save screenshots, HTML content, and console logs on test failure.""" + # Generate unique name for the dump files + current_test_name = ( + "playwright_dump_" + + os.path.basename(__file__)[:-3] + + "_" + + sys._getframe().f_code.co_name + ) + + # Save screenshot + page.screenshot(path=current_test_name + ".png") + + # Save HTML content + with open(current_test_name + ".html", "w", encoding="utf-8") as f: + f.write(page.content()) + + # Save console logs + with open(current_test_name + "_console.log", "w", encoding="utf-8") as f: + f.write("\n".join(log_entries)) + +def test_sanity(browser): # browser is actually a page object + log_entries = [] + setup_console_listener(browser, log_entries) + try: browser.goto("http://localhost:3000/") browser.wait_for_url("http://localhost:3000/incidents") assert "Keep" in browser.title() except Exception: - # Current file + test name for unique html and png dump. - current_test_name = ( - "playwright_dump_" - + os.path.basename(__file__)[:-3] - + "_" - + sys._getframe().f_code.co_name - ) - - browser.screenshot(path=current_test_name + ".png") - with open(current_test_name + ".html", "w") as f: - f.write(browser.content()) + save_failure_artifacts(browser, log_entries) raise - -def test_insert_new_alert(browser): +def test_insert_new_alert(browser): # browser is actually a page object """ Test to insert a new alert - """ + log_entries = [] + setup_console_listener(browser, log_entries) + try: browser.goto( "http://localhost:3000/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fproviders" @@ -86,6 +104,8 @@ def test_insert_new_alert(browser): browser.wait_for_timeout(10000) # refresh the page browser.reload() + # wait for badge counter to update + browser.wait_for_timeout(500) feed_badge = browser.get_by_test_id("menu-alerts-feed-badge") feed_count = int(feed_badge.text_content()) assert feed_count > feed_count_before @@ -94,17 +114,7 @@ def test_insert_new_alert(browser): feed_link.click() except Exception: - # Current file + test name for unique html and png dump. - current_test_name = ( - "playwright_dump_" - + os.path.basename(__file__)[:-3] - + "_" - + sys._getframe().f_code.co_name - ) - - browser.screenshot(path=current_test_name + ".png") - with open(current_test_name + ".html", "w") as f: - f.write(browser.content()) + save_failure_artifacts(browser, log_entries) raise