diff --git a/package-lock.json b/package-lock.json index 3658e3146..3990c94c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "react-syntax-highlighter": "^15.5.0", "react-transition-group": "^4.4.5", "recharts": "^2.6.2", + "redux-remember": "^5.2.0", "semver": "^7.5.4", "squarify": "^1.1.0", "styled-components": "^6.1.13", @@ -17518,6 +17519,15 @@ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "license": "MIT" }, + "node_modules/redux-remember": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/redux-remember/-/redux-remember-5.2.0.tgz", + "integrity": "sha512-HqXx9V+DKzgBzpiIT5dyiXZgiiSB6zaMs4sIscwQ+Z0zVwUvJh20mqPEQWo4wbthuo5+5jGrS7Yfvv4HyOuAFw==", + "license": "MIT", + "peerDependencies": { + "redux": ">=5.0.0" + } + }, "node_modules/redux-thunk": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", diff --git a/package.json b/package.json index 750074f2e..5ef4c4d14 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,7 @@ "react-syntax-highlighter": "^15.5.0", "react-transition-group": "^4.4.5", "recharts": "^2.6.2", + "redux-remember": "^5.2.0", "semver": "^7.5.4", "squarify": "^1.1.0", "styled-components": "^6.1.13", diff --git a/src/components/Admin/Reports/CodeIssues/index.tsx b/src/components/Admin/Reports/CodeIssues/index.tsx index 3255c8a2d..c754e28b2 100644 --- a/src/components/Admin/Reports/CodeIssues/index.tsx +++ b/src/components/Admin/Reports/CodeIssues/index.tsx @@ -4,10 +4,8 @@ import { useAdminDispatch, useAdminSelector } from "../../../../containers/Admin/hooks"; -import { useMount } from "../../../../hooks/useMount"; import type { IssueCriticality } from "../../../../redux/services/types"; import { - clear, setCriticalityLevels, setPeriodInDays, setSelectedEndpoints, @@ -61,12 +59,6 @@ export const CodeIssues = () => { const dispatch = useAdminDispatch(); - useMount(() => { - return () => { - dispatch(clear()); - }; - }); - const handleTileTitleClick = ( viewLevel: IssuesReportViewLevel, target: TargetScope diff --git a/src/components/Admin/Sidebar/index.tsx b/src/components/Admin/Sidebar/index.tsx index 941f350be..4ca8b5b38 100644 --- a/src/components/Admin/Sidebar/index.tsx +++ b/src/components/Admin/Sidebar/index.tsx @@ -1,5 +1,6 @@ import { useEffect } from "react"; import { useTheme } from "styled-components"; +import { useAdminDispatch } from "../../../containers/Admin/hooks"; import { useLogoutMutation } from "../../../redux/services/auth"; import { getThemeKind } from "../../common/App/styles"; import { LogoutIcon } from "../../common/icons/16px/LogoutIcon"; @@ -10,6 +11,7 @@ export const Sidebar = () => { const theme = useTheme(); const themeKind = getThemeKind(theme); const [logout, result] = useLogoutMutation(); + const dispatch = useAdminDispatch(); const handleLogoutButtonClick = () => { void logout(); @@ -29,7 +31,7 @@ export const Sidebar = () => { window.clearTimeout(timeoutId); } }; - }, [result.isSuccess]); + }, [result.isSuccess, dispatch]); return ( diff --git a/src/components/Admin/index.tsx b/src/components/Admin/index.tsx index 7b8f96fd8..d99978ed2 100644 --- a/src/components/Admin/index.tsx +++ b/src/components/Admin/index.tsx @@ -1,21 +1,57 @@ +import { useEffect } from "react"; import { Helmet } from "react-helmet"; import { Outlet } from "react-router-dom"; +import { + useAdminDispatch, + useAdminSelector +} from "../../containers/Admin/hooks"; +import { globalClear } from "../../redux/actions"; +import { useGetUserProfileQuery } from "../../redux/services/digma"; +import { setIsInitialized } from "../../redux/slices/appSlice"; +import { setEmail } from "../../redux/slices/authSlice"; +import { Spinner } from "../common/v3/Spinner"; import { Header } from "./Header"; import { Sidebar } from "./Sidebar"; import * as s from "./styles"; -export const Admin = () => ( - - - Digma admin panel - - - - -
- - - - - -); +export const Admin = () => { + const { data: userProfile } = useGetUserProfileQuery(); + const dispatch = useAdminDispatch(); + const currentEmail = useAdminSelector((state) => state.auth.email); + const isInitialized = useAdminSelector((state) => state.app.isInitialized); + + // Clear issues report state when user changes + useEffect(() => { + if (userProfile?.email) { + if (currentEmail !== userProfile.email) { + dispatch(globalClear()); + } + dispatch(setEmail(userProfile.email)); + dispatch(setIsInitialized(true)); + } + }, [dispatch, currentEmail, userProfile?.email]); + + return ( + + + Digma admin panel + + + {isInitialized ? ( + <> + + +
+ + + + + + ) : ( + + + + )} + + ); +}; diff --git a/src/components/Admin/styles.ts b/src/components/Admin/styles.ts index b803fb4c8..b348eda1d 100644 --- a/src/components/Admin/styles.ts +++ b/src/components/Admin/styles.ts @@ -18,3 +18,10 @@ export const MainContainer = styled.main` height: 100%; overflow: auto; `; + +export const LoadingContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + flex-grow: 1; +`; diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/SpaNPlusOneInsightCard/index.tsx b/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/SpaNPlusOneInsightCard/index.tsx index e2869ff2d..0f57a63a3 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/SpaNPlusOneInsightCard/index.tsx +++ b/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/SpaNPlusOneInsightCard/index.tsx @@ -21,11 +21,14 @@ import { ContentContainer, Description, Details } from "../styles"; import * as s from "./styles"; import type { SpaNPlusOneInsightCardProps } from "./types"; -const getSelectorOption = (endpoint: NPlusOneEndpointInfo): Option => ({ +const getSelectorOption = (endpoint: NPlusOneEndpointInfo): Option => ({ route: endpoint.endpointInfo.route, serviceName: endpoint.endpointInfo.serviceName, spanCodeObjectId: endpoint.endpointInfo.entrySpanCodeObjectId, - duration: endpoint.duration + metric: { + value: endpoint.occurrences, + label: "Repeats" + } }); export const SpaNPlusOneInsightCard = ({ @@ -41,21 +44,21 @@ export const SpaNPlusOneInsightCard = ({ viewMode, onDismissalChange }: SpaNPlusOneInsightCardProps) => { - const endpoints = useMemo(() => insight.endpoints ?? [], [insight.endpoints]); - const selectorOptions = useMemo( + const endpoints = useMemo( () => - endpoints - .map(getSelectorOption) - .sort((a, b) => - a.duration && b.duration ? b.duration.raw - a.duration.raw : 0 - ), - [endpoints] + (insight.endpoints ?? []).sort((a, b) => b.occurrences - a.occurrences), + [insight.endpoints] ); - const endpointWithMaxDuration = endpoints.reduce( - (acc, cur) => (acc.duration.raw >= cur.duration.raw ? acc : cur), - endpoints[0] + + const selectorOptions = useMemo( + () => endpoints.map(getSelectorOption), + [endpoints] ); - const maxDurationString = getDurationString(endpointWithMaxDuration.duration); + const endpointWithMaxDuration = + endpoints.length > 0 ? endpoints[0] : undefined; + const maxDurationString = endpointWithMaxDuration + ? getDurationString(endpointWithMaxDuration.duration) + : undefined; const { isJaegerEnabled } = useConfigSelector(); const [selectedEndpointKey, setSelectedEndpointKey] = useState< string | undefined @@ -192,9 +195,11 @@ export const SpaNPlusOneInsightCard = ({ isMarkAsReadButtonEnabled={isMarkAsReadButtonEnabled} viewMode={viewMode} mainMetric={ - - {maxDurationString} - + maxDurationString ? ( + + {maxDurationString} + + ) : undefined } onDismissalChange={onDismissalChange} /> diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/SpanEndpointBottleneckInsightCard/index.tsx b/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/SpanEndpointBottleneckInsightCard/index.tsx index 0bf734510..e2004b590 100644 --- a/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/SpanEndpointBottleneckInsightCard/index.tsx +++ b/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/SpanEndpointBottleneckInsightCard/index.tsx @@ -23,11 +23,16 @@ import { ContentContainer, Description, Details } from "../styles"; import * as s from "./styles"; import type { SpanEndpointBottleneckInsightCardProps } from "./types"; -const getSelectorOption = (endpoint: BottleneckEndpointInfo): Option => ({ +const getSelectorOption = ( + endpoint: BottleneckEndpointInfo +): Option => ({ route: trimEndpointScheme(endpoint.endpointInfo.route), serviceName: endpoint.endpointInfo.serviceName, spanCodeObjectId: endpoint.endpointInfo.spanCodeObjectId, - duration: endpoint.avgDurationWhenBeingBottleneck + metric: { + value: getDurationString(endpoint.avgDurationWhenBeingBottleneck), + label: "Duration" + } }); export const SpanEndpointBottleneckInsightCard = ({ @@ -45,30 +50,21 @@ export const SpanEndpointBottleneckInsightCard = ({ }: SpanEndpointBottleneckInsightCardProps) => { const { isJaegerEnabled } = useConfigSelector(); const slowEndpoints = useMemo( - () => insight.slowEndpoints ?? [], + () => + (insight.slowEndpoints ?? []).sort( + (a, b) => + b.avgDurationWhenBeingBottleneck.raw - + a.avgDurationWhenBeingBottleneck.raw + ), [insight.slowEndpoints] ); - const selectorOptions: Option[] = useMemo( - () => - slowEndpoints - .map(getSelectorOption) - .sort((a, b) => - a.duration && b.duration ? b.duration.raw - a.duration.raw : 0 - ), + const selectorOptions: Option[] = useMemo( + () => slowEndpoints.map(getSelectorOption), [slowEndpoints] ); const endpointWithMaxDuration = - slowEndpoints.length > 0 - ? slowEndpoints.reduce( - (acc, cur) => - acc.avgDurationWhenBeingBottleneck.raw >= - cur.avgDurationWhenBeingBottleneck.raw - ? acc - : cur, - slowEndpoints[0] - ) - : undefined; + slowEndpoints.length > 0 ? slowEndpoints[0] : undefined; const maxDurationString = endpointWithMaxDuration ? getDurationString(endpointWithMaxDuration.avgDurationWhenBeingBottleneck) : undefined; diff --git a/src/components/common/AffectedEndpointsSelector/AffectedEndpointsSelector.stories.tsx b/src/components/common/AffectedEndpointsSelector/AffectedEndpointsSelector.stories.tsx index 2c6d72294..b1e925156 100644 --- a/src/components/common/AffectedEndpointsSelector/AffectedEndpointsSelector.stories.tsx +++ b/src/components/common/AffectedEndpointsSelector/AffectedEndpointsSelector.stories.tsx @@ -23,18 +23,30 @@ export const Default: Story = { route: "test", serviceName: "someasasdasdasdasdasdasdawerereasdsadsadsadsadsadasdsadasdsadsdhfkjdhskjfgdf;lgjhdfhglkdfhgklhsdklfghkhgdfgkdfklghrthysdfhsbfheslkbyieryiobyrieuytirosynoiuybioyustest2", - spanCodeObjectId: "spanCodeObjectId1" + spanCodeObjectId: "spanCodeObjectId1", + metric: { + value: 100, + label: "100 ms" + } }, { route: "someasasdasdasdasdasdasdawerereasdsadsadsadsadsadasdsadasdsadsdhfkjdhskjfgdf;lgjhdfhglkdfhgklhsdklfghkhgdfgkdfklghrthysdfhsbfheslkbyieryiobyrieuytirosynoiuybioyustest", serviceName: "test1", - spanCodeObjectId: "spanCodeObjectId2" + spanCodeObjectId: "spanCodeObjectId2", + metric: { + value: 200, + label: "200 ms" + } }, { route: "test", serviceName: "test1", - spanCodeObjectId: "spanCodeObjectId2" + spanCodeObjectId: "spanCodeObjectId2", + metric: { + value: 300, + label: "300 ms" + } } ] } diff --git a/src/components/common/AffectedEndpointsSelector/EndpointOption/index.tsx b/src/components/common/AffectedEndpointsSelector/EndpointOption/index.tsx index 1500d2cf9..d12770d59 100644 --- a/src/components/common/AffectedEndpointsSelector/EndpointOption/index.tsx +++ b/src/components/common/AffectedEndpointsSelector/EndpointOption/index.tsx @@ -2,7 +2,7 @@ import { Tooltip } from "../../v3/Tooltip"; import * as s from "./styles"; import type { EndpointOptionProps } from "./types"; -export const EndpointOption = ({ +export const EndpointOption = ({ serviceName, route, spanCodeObjectId, @@ -10,9 +10,9 @@ export const EndpointOption = ({ selected, hideCopyIcon, onClick, - duration, - hideDuration -}: EndpointOptionProps) => { + metric, + isHeader +}: EndpointOptionProps) => { const title = `${serviceName} ${route}`; return ( @@ -34,8 +34,10 @@ export const EndpointOption = ({ )} {!hideCopyIcon && } - {!selected && !hideDuration && duration && ( - {duration} + {!selected && metric && ( + + {isHeader ? metric.label : String(metric.value)} + )} diff --git a/src/components/common/AffectedEndpointsSelector/EndpointOption/types.ts b/src/components/common/AffectedEndpointsSelector/EndpointOption/types.ts index 78f01617c..0fe951736 100644 --- a/src/components/common/AffectedEndpointsSelector/EndpointOption/types.ts +++ b/src/components/common/AffectedEndpointsSelector/EndpointOption/types.ts @@ -1,4 +1,4 @@ -export interface EndpointOptionProps { +export interface EndpointOptionProps { serviceName: string; route: string; spanCodeObjectId?: string; @@ -6,8 +6,11 @@ export interface EndpointOptionProps { onSpanLinkClick?: (spanCodeObjectId: string) => void; hideCopyIcon?: boolean; onClick?: ((spanCodeObjectId?: string) => void) | null; - duration?: string; - hideDuration?: boolean; + metric?: { + value: T; + label: string; + }; + isHeader?: boolean; } export interface EndpointNameProps { diff --git a/src/components/common/AffectedEndpointsSelector/index.tsx b/src/components/common/AffectedEndpointsSelector/index.tsx index a9860d11d..5be1268c3 100644 --- a/src/components/common/AffectedEndpointsSelector/index.tsx +++ b/src/components/common/AffectedEndpointsSelector/index.tsx @@ -1,6 +1,5 @@ import type { ReactNode } from "react"; import { DELIMITER } from "../../../constants"; -import { getDurationString } from "../../../utils/getDurationString"; import { trimEndpointScheme } from "../../../utils/trimEndpointScheme"; import { Select } from "../../Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/common/InsightCard/Select"; import type { CustomContentProps } from "../../Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/common/InsightCard/Select/types"; @@ -11,8 +10,8 @@ import type { AffectedEndpointsSelectorProps, Option } from "./types"; export const getEndpointKey = (option: Option): string => [option.serviceName, option.spanCodeObjectId].join(DELIMITER); -const renderOptions = ( - endpoints: Option[], +const renderOptions = ( + endpoints: Option[], handleLinkClick: (spanCodeObjectId?: string) => void ): { label: string; @@ -22,11 +21,11 @@ const renderOptions = ( endpoints.map((x) => { const spanCodeObjectId = x.spanCodeObjectId; const route = trimEndpointScheme(x.route); - const durationString = x.duration && getDurationString(x.duration); + return { label: route, customContent: ({ isSelected, onClick }) => ( - serviceName={x.serviceName} route={route} selected={isSelected} @@ -34,20 +33,20 @@ const renderOptions = ( onSpanLinkClick={handleLinkClick} hideCopyIcon={!isSelected} onClick={onClick} - duration={durationString} + metric={"metric" in x ? x.metric : undefined} /> ), value: getEndpointKey(x) }; }); -export const AffectedEndpointsSelector = ({ +export const AffectedEndpointsSelector = ({ onAssetLinkClick, value, options, onChange, isDisabled -}: AffectedEndpointsSelectorProps) => { +}: AffectedEndpointsSelectorProps) => { const handleSpanLinkClick = (spanCodeObjectId?: string) => { if (spanCodeObjectId) { onAssetLinkClick(spanCodeObjectId); @@ -58,6 +57,11 @@ export const AffectedEndpointsSelector = ({ onChange(value); }; + const metric = + options.length > 0 && "metric" in options[0] + ? options[0].metric + : undefined; + return (