diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index 7625687f3..a244eef30 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -2,6 +2,7 @@ import type { Preview, StoryFn } from "@storybook/react";
// TODO: remove React import due to new JSX transform
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import React, { useEffect, useState } from "react";
+import { Provider } from "react-redux";
import { withRouter } from "storybook-addon-remix-react-router";
import {
cancelMessage,
@@ -11,6 +12,7 @@ import {
import { App, THEMES } from "../src/components/common/App";
import { dispatcher } from "../src/dispatcher";
import type { Theme } from "../src/globals";
+import { store } from "./store";
const preview: Preview = {
decorators: [
@@ -32,9 +34,11 @@ const preview: Preview = {
}, []);
return isInitialized ? (
-
-
-
+
+
+
+
+
) : (
<>Initializing...>
);
diff --git a/.storybook/store.ts b/.storybook/store.ts
new file mode 100644
index 000000000..4dbc4aa07
--- /dev/null
+++ b/.storybook/store.ts
@@ -0,0 +1,13 @@
+import { configureStore } from "@reduxjs/toolkit";
+import { setupListeners } from "@reduxjs/toolkit/query";
+import { digmaApi } from "../src/redux/services/digma";
+
+export const store = configureStore({
+ reducer: {
+ [digmaApi.reducerPath]: digmaApi.reducer
+ },
+ middleware: (getDefaultMiddleware) =>
+ getDefaultMiddleware().concat(digmaApi.middleware)
+});
+
+setupListeners(store.dispatch);
diff --git a/package-lock.json b/package-lock.json
index 0828d5d12..e57f3c006 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,16 +1,17 @@
{
"name": "digma-ui",
- "version": "6.1.0",
+ "version": "6.2.0-alpha.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "digma-ui",
- "version": "6.1.0",
+ "version": "6.2.0-alpha.2",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.25.1",
"@formkit/auto-animate": "^0.8.2",
+ "@react-oauth/google": "^0.12.1",
"@reduxjs/toolkit": "^2.5.0",
"@tanstack/react-table": "^8.7.8",
"allotment": "^1.19.0",
@@ -4153,6 +4154,16 @@
"@octokit/openapi-types": "^22.2.0"
}
},
+ "node_modules/@react-oauth/google": {
+ "version": "0.12.1",
+ "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.1.tgz",
+ "integrity": "sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/@reduxjs/toolkit": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz",
diff --git a/package.json b/package.json
index 9b1726946..d921b91a6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "digma-ui",
- "version": "6.1.0",
+ "version": "6.2.0-alpha.2",
"description": "Digma UI",
"scripts": {
"lint:eslint": "eslint --cache .",
@@ -119,6 +119,7 @@
"dependencies": {
"@floating-ui/react": "^0.25.1",
"@formkit/auto-animate": "^0.8.2",
+ "@react-oauth/google": "^0.12.1",
"@reduxjs/toolkit": "^2.5.0",
"@tanstack/react-table": "^8.7.8",
"allotment": "^1.19.0",
diff --git a/src/components/Admin/Reports/CodeIssues/index.tsx b/src/components/Admin/Reports/CodeIssues/index.tsx
index 44e984cce..3255c8a2d 100644
--- a/src/components/Admin/Reports/CodeIssues/index.tsx
+++ b/src/components/Admin/Reports/CodeIssues/index.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useRef, useState } from "react";
import { CSSTransition } from "react-transition-group";
import {
useAdminDispatch,
@@ -22,6 +22,7 @@ import {
type IssuesReportViewMode
} from "../../../../redux/slices/issuesReportSlice";
import { IssuesReport } from "../../../common/IssuesReport";
+import type { TargetScope } from "../../../common/IssuesReport/types";
import { IssuesSidebar } from "./IssuesSidebar";
import * as s from "./styles";
@@ -31,6 +32,8 @@ export const CodeIssues = () => {
const [activeTileIds, setActiveTileIds] = useState(
undefined
);
+ const sidebarContainerRef = useRef(null);
+ const overlayRef = useRef(null);
const selectedEnvironmentId = useAdminSelector(
(state) => state.codeIssuesReport.selectedEnvironmentId
@@ -64,14 +67,21 @@ export const CodeIssues = () => {
};
});
- const handleTileIssuesStatsClick = (
- _: IssuesReportViewLevel,
- target: { value: string; displayName?: string }
+ const handleTileTitleClick = (
+ viewLevel: IssuesReportViewLevel,
+ target: TargetScope
) => {
- if (!selectedEnvironmentId) {
- return;
+ if (viewMode === "table" && viewLevel === "endpoints") {
+ setScope(target);
+ setIsSidebarOpen(true);
+ setActiveTileIds([target.value]);
}
+ };
+ const handleTileIssuesStatsClick = (
+ _: IssuesReportViewLevel,
+ target: TargetScope
+ ) => {
setScope(target);
setIsSidebarOpen(true);
setActiveTileIds([target.value]);
@@ -131,6 +141,7 @@ export const CodeIssues = () => {
viewMode={viewMode}
timeMode={timeMode}
defaultHeaderTitle={"Code Issues"}
+ onTileTitleClick={handleTileTitleClick}
onTileIssuesStatsClick={handleTileIssuesStatsClick}
onSelectedEnvironmentIdChange={handleSelectedEnvironmentIdChange}
onSelectedServicesChange={handleSelectedServicesChange}
@@ -149,8 +160,10 @@ export const CodeIssues = () => {
classNames={s.overlayTransitionClassName}
mountOnEnter={true}
unmountOnExit={true}
+ nodeRef={overlayRef}
>
{
classNames={s.sidebarContainerTransitionClassName}
mountOnEnter={true}
unmountOnExit={true}
+ nodeRef={sidebarContainerRef}
>
diff --git a/src/components/Assets/AssetsFilter/index.tsx b/src/components/Assets/AssetsFilter/index.tsx
index e95c5458e..ed267b528 100644
--- a/src/components/Assets/AssetsFilter/index.tsx
+++ b/src/components/Assets/AssetsFilter/index.tsx
@@ -23,6 +23,7 @@ import { trackingEvents } from "../tracking";
import * as s from "./styles";
import type {
AssetFilterCategory,
+ AssetFilterEntry,
AssetsFiltersData,
GetAssetFiltersDataParams,
GetAssetFiltersDataPayload
@@ -57,10 +58,14 @@ const renderFilterCategory = (
placeholder: string,
selectedValues: string[],
onChange: (value: string | string[], categoryName?: string) => void,
- transformLabel?: (value: string) => string
+ transformLabel?: (value: string) => string,
+ sorter?: (a: AssetFilterEntry, b: AssetFilterEntry) => number
): JSX.Element => {
+ const sortedEntries =
+ (sorter ? category.entries?.sort(sorter) : category.entries) ?? [];
+
const items =
- category.entries?.map((entry) => ({
+ sortedEntries.map((entry) => ({
value: entry.name,
label: transformLabel ? transformLabel(entry.name) : entry.name,
enabled: entry.enabled,
@@ -76,7 +81,7 @@ const renderFilterCategory = (
placeholder={placeholder}
multiselect={true}
icon={icon}
- disabled={category.entries?.length === 0}
+ disabled={sortedEntries.length === 0}
/>
);
};
@@ -524,7 +529,9 @@ export const AssetsFilter = () => {
WrenchIcon,
selectedServices.length > 0 ? "Services" : "All",
selectedServices,
- handleSelectedItemsChange
+ handleSelectedItemsChange,
+ undefined,
+ (a, b) => a.name.localeCompare(b.name)
)
});
}
diff --git a/src/components/Dashboard/MetricsReport/index.tsx b/src/components/Dashboard/MetricsReport/index.tsx
index 5ed53a3d3..303f8eecf 100644
--- a/src/components/Dashboard/MetricsReport/index.tsx
+++ b/src/components/Dashboard/MetricsReport/index.tsx
@@ -23,6 +23,7 @@ import { isString } from "../../../typeGuards/isString";
import { SCOPE_CHANGE_EVENTS } from "../../../types";
import { changeScope } from "../../../utils/actions/changeScope";
import { IssuesReport } from "../../common/IssuesReport";
+import type { TargetScope } from "../../common/IssuesReport/types";
import { DigmaLogoIcon } from "../../common/icons/16px/DigmaLogoIcon";
import { actions } from "../actions";
import * as s from "./styles";
@@ -96,11 +97,11 @@ export const MetricsReport = () => {
const handleTileTitleClick = (
viewLevel: IssuesReportViewLevel,
- value: string
+ target: TargetScope
) => {
if (viewLevel === "endpoints" && selectedEnvironmentId && selectedService) {
goToEndpointIssues({
- spanCodeObjectId: value,
+ spanCodeObjectId: target.value,
service: selectedService,
environmentId: selectedEnvironmentId
});
@@ -109,7 +110,7 @@ export const MetricsReport = () => {
const handleIssuesStatsClick = (
viewLevel: IssuesReportViewLevel,
- target: { value: string; displayName?: string }
+ target: TargetScope
) => {
if (viewLevel === "services") {
changeScope({
diff --git a/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/index.tsx b/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/index.tsx
index d61763b4c..f249ea6b3 100644
--- a/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/index.tsx
+++ b/src/components/Errors/GlobalErrorsList/GlobalErrorsFilters/index.tsx
@@ -210,12 +210,14 @@ export const GlobalErrorsFilters = () => {
const servicesFilterOptions: SelectItem[] = useMemo(
() =>
- services?.map((x) => ({
- label: x,
- value: x,
- selected: selectedServices.includes(x),
- enabled: true
- })) ?? [],
+ services
+ ?.map((x) => ({
+ label: x,
+ value: x,
+ selected: selectedServices.includes(x),
+ enabled: true
+ }))
+ .sort((a, b) => a.label.localeCompare(b.label)) ?? [],
[services, selectedServices]
);
diff --git a/src/components/Insights/InsightTicketRenderer/insightTickets/EndpointBottleneckInsightTicket/EndpointBottleneckInsightTicket.stories.tsx b/src/components/Insights/InsightTicketRenderer/insightTickets/EndpointBottleneckInsightTicket/EndpointBottleneckInsightTicket.stories.tsx
index 10652869e..e34184179 100644
--- a/src/components/Insights/InsightTicketRenderer/insightTickets/EndpointBottleneckInsightTicket/EndpointBottleneckInsightTicket.stories.tsx
+++ b/src/components/Insights/InsightTicketRenderer/insightTickets/EndpointBottleneckInsightTicket/EndpointBottleneckInsightTicket.stories.tsx
@@ -4,7 +4,8 @@ import { mockedEndpointBottleneckInsight } from "../../../InsightsCatalog/Insigh
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = {
- title: "Insights/insightTickets/EndpointBottleneckInsightTicket",
+ title:
+ "Insights/InsightTicketRenderer/insightTickets/EndpointBottleneckInsightTicket",
component: EndpointBottleneckInsightTicket,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
diff --git a/src/components/Insights/InsightTicketRenderer/insightTickets/common/CommitInfos/CommitInfos.stories.tsx b/src/components/Insights/InsightTicketRenderer/insightTickets/common/CommitInfos/CommitInfos.stories.tsx
index 5d97159b5..e425ae556 100644
--- a/src/components/Insights/InsightTicketRenderer/insightTickets/common/CommitInfos/CommitInfos.stories.tsx
+++ b/src/components/Insights/InsightTicketRenderer/insightTickets/common/CommitInfos/CommitInfos.stories.tsx
@@ -4,7 +4,7 @@ import { mockedSpaNPlusOneInsight } from "../../../../InsightsCatalog/InsightsPa
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = {
- title: "Insights/insightTickets/common/CommitInfos",
+ title: "Insights/InsightTicketRenderer/insightTickets/common/CommitInfos",
component: CommitInfos,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
diff --git a/src/components/Insights/InsightTicketRenderer/insightTickets/common/InsightJiraTicket/InsightJiraTicket.stories.tsx b/src/components/Insights/InsightTicketRenderer/insightTickets/common/InsightJiraTicket/InsightJiraTicket.stories.tsx
index 603a9f244..c5593ee46 100644
--- a/src/components/Insights/InsightTicketRenderer/insightTickets/common/InsightJiraTicket/InsightJiraTicket.stories.tsx
+++ b/src/components/Insights/InsightTicketRenderer/insightTickets/common/InsightJiraTicket/InsightJiraTicket.stories.tsx
@@ -6,7 +6,8 @@ import { InsightCategory, InsightScope } from "../../../../types";
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = {
- title: "Insights/insightTickets/common/InsightJiraTicket",
+ title:
+ "Insights/InsightTicketRenderer/insightTickets/common/InsightJiraTicket",
component: InsightJiraTicket,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
diff --git a/src/components/Insights/InsightTicketRenderer/insightTickets/common/NPlusOneEndpoints/NPlusOneEndpoints.stories.tsx b/src/components/Insights/InsightTicketRenderer/insightTickets/common/NPlusOneEndpoints/NPlusOneEndpoints.stories.tsx
index 7d403a85d..8a081ce97 100644
--- a/src/components/Insights/InsightTicketRenderer/insightTickets/common/NPlusOneEndpoints/NPlusOneEndpoints.stories.tsx
+++ b/src/components/Insights/InsightTicketRenderer/insightTickets/common/NPlusOneEndpoints/NPlusOneEndpoints.stories.tsx
@@ -4,7 +4,8 @@ import { mockedSpaNPlusOneInsight } from "../../../../InsightsCatalog/InsightsPa
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = {
- title: "Insights/insightTickets/common/NPlusOneEndpoints",
+ title:
+ "Insights/InsightTicketRenderer/insightTickets/common/NPlusOneEndpoints",
component: NPlusOneEndpoints,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
diff --git a/src/components/Insights/InsightTicketRenderer/insightTickets/common/QueryOptimizationEndpoints/QueryOptimizationEndpoints.stories.tsx b/src/components/Insights/InsightTicketRenderer/insightTickets/common/QueryOptimizationEndpoints/QueryOptimizationEndpoints.stories.tsx
index 509662f6c..f097f112d 100644
--- a/src/components/Insights/InsightTicketRenderer/insightTickets/common/QueryOptimizationEndpoints/QueryOptimizationEndpoints.stories.tsx
+++ b/src/components/Insights/InsightTicketRenderer/insightTickets/common/QueryOptimizationEndpoints/QueryOptimizationEndpoints.stories.tsx
@@ -4,7 +4,8 @@ import { mockedSpanQueryOptimizationInsight } from "../../../../InsightsCatalog/
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = {
- title: "Insights/insightTickets/common/QueryOptimizationEndpoints",
+ title:
+ "Insights/InsightTicketRenderer/insightTickets/common/QueryOptimizationEndpoints",
component: QueryOptimizationEndpoints,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
diff --git a/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/IssuesFilter.stories.tsx b/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/IssuesFilter.stories.tsx
index 060bf8211..969ea7846 100644
--- a/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/IssuesFilter.stories.tsx
+++ b/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/IssuesFilter.stories.tsx
@@ -5,7 +5,7 @@ import { actions } from "../../../Issues/actions";
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = {
- title: "Insights/Issues/InsightsCatalog/FilterPanel/IssuesFilter",
+ title: "Insights/InsightsCatalog/FilterPanel/IssuesFilter",
component: IssuesFilter,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
diff --git a/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/index.tsx b/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/index.tsx
index da9fd024f..2e8f80bbf 100644
--- a/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/index.tsx
+++ b/src/components/Insights/InsightsCatalog/FilterPanel/IssuesFilter/index.tsx
@@ -219,12 +219,14 @@ export const IssuesFilter = () => {
};
const servicesFilterOptions: SelectItem[] =
- data?.services?.map((entry) => ({
- value: entry,
- label: entry,
- enabled: true,
- selected: selectedServices.includes(entry)
- })) ?? [];
+ data?.services
+ ?.map((entry) => ({
+ value: entry,
+ label: entry,
+ enabled: true,
+ selected: selectedServices.includes(entry)
+ }))
+ .sort((a, b) => a.label.localeCompare(b.label)) ?? [];
const issueTypesFilterOptions: SelectItem[] =
data?.issueTypeFilters?.map((entry) => ({
diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/EndpointChattyApiV2InsightCard/index.tsx b/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/EndpointChattyApiV2InsightCard/index.tsx
index 6bbe33f41..6f3005abd 100644
--- a/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/EndpointChattyApiV2InsightCard/index.tsx
+++ b/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/EndpointChattyApiV2InsightCard/index.tsx
@@ -1,6 +1,9 @@
+import { Tag } from "../../../../../../common/v3/Tag";
import type { InsightType, Trace } from "../../../../../types";
import { InsightCard } from "../common/InsightCard";
-import { AssetLink, Description } from "../styles";
+import { ColumnsContainer } from "../common/InsightCard/ColumnsContainer";
+import { KeyValue } from "../common/InsightCard/KeyValue";
+import { AssetLink } from "../styles";
import * as s from "./styles";
import type { EndpointChattyApiV2InsightCardProps } from "./types";
@@ -30,19 +33,26 @@ export const EndpointChattyApiV2InsightCard = ({
const spanName = insight.span.clientSpan.displayName;
const traceId = insight.span.traceId;
const spanCodeObjectId = insight.span.clientSpan.spanCodeObjectId;
+ const repeats = insight.span.repeats;
return (
-
- Excessive API calls to specific endpoint found
-
- handleSpanLinkClick(spanCodeObjectId)}
- />
+
+
+ handleSpanLinkClick(spanCodeObjectId)}
+ />
+
+
+
+
+
}
onRecalculate={onRecalculate}
diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/EndpointChattyApiV2InsightCard/styles.ts b/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/EndpointChattyApiV2InsightCard/styles.ts
index 1a47b2866..44894a341 100644
--- a/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/EndpointChattyApiV2InsightCard/styles.ts
+++ b/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/EndpointChattyApiV2InsightCard/styles.ts
@@ -1,6 +1,11 @@
import styled from "styled-components";
+import { KeyValue } from "../common/InsightCard/KeyValue";
import { ContentContainer } from "../styles";
export const Container = styled(ContentContainer)`
gap: 4px;
`;
+
+export const DescriptionColumn = styled(KeyValue)`
+ flex-grow: 2;
+`;
diff --git a/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/EndpointSpanNPlusOneInsightInsightCard/index.tsx b/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/EndpointSpanNPlusOneInsightInsightCard/index.tsx
index 6068aed74..f64f54a97 100644
--- a/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/EndpointSpanNPlusOneInsightInsightCard/index.tsx
+++ b/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/EndpointSpanNPlusOneInsightInsightCard/index.tsx
@@ -78,7 +78,7 @@ export const EndpointSpanNPlusOneInsightCard = ({
{span.occurrences}
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 37fe464e5..2e59a81fe 100644
--- a/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/SpaNPlusOneInsightCard/index.tsx
+++ b/src/components/Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/SpaNPlusOneInsightCard/index.tsx
@@ -90,14 +90,26 @@ export const SpaNPlusOneInsightCard = ({
setSelectedEndpointKey(endpointKey);
};
- const selectedEndpoint = useMemo(
+ const selectedOption = useMemo(
() =>
selectedEndpointKey
+ ? selectorOptions.find((x) => getEndpointKey(x) === selectedEndpointKey)
+ : undefined,
+ [selectedEndpointKey, selectorOptions]
+ );
+
+ const selectedEndpoint = useMemo(
+ () =>
+ selectedOption
? endpoints.find(
- (x) => getEndpointKey(x.endpointInfo) === selectedEndpointKey
+ (x) =>
+ x.endpointInfo.route === selectedOption.route &&
+ x.endpointInfo.serviceName === selectedOption.serviceName &&
+ x.endpointInfo.entrySpanCodeObjectId ===
+ selectedOption.spanCodeObjectId
)
: undefined,
- [selectedEndpointKey, endpoints]
+ [selectedOption, endpoints]
);
return (
@@ -135,7 +147,6 @@ export const SpaNPlusOneInsightCard = ({
)}
-
{selectedEndpoint && (
= {
title:
- "Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightsCards/common/DurationChange",
+ "Insights/InsightsCatalog/InsightsPage/InsightCardRenderer/insightCards/common/DurationChange",
component: DurationChange,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
diff --git a/src/components/Login/GoogleSignInButton/index.tsx b/src/components/Login/GoogleSignInButton/index.tsx
new file mode 100644
index 000000000..6eecf766a
--- /dev/null
+++ b/src/components/Login/GoogleSignInButton/index.tsx
@@ -0,0 +1,34 @@
+import { useGoogleLogin } from "@react-oauth/google";
+import { GoogleLogoIcon } from "../../common/icons/20px/GoogleLogoIcon";
+import * as s from "./styles";
+import type { GoogleSignInButtonProps } from "./types";
+
+export const GoogleSignInButton = ({
+ onSuccess,
+ onError,
+ onNonOAuthError
+}: GoogleSignInButtonProps) => {
+ const googleLogin = useGoogleLogin({
+ flow: "auth-code",
+ onSuccess: (response) => {
+ onSuccess(response);
+ },
+ onError: (error) => {
+ onError(error);
+ },
+ onNonOAuthError: (error) => {
+ onNonOAuthError(error);
+ }
+ });
+
+ const handleClick = () => {
+ googleLogin();
+ };
+
+ return (
+
+
+ Sign in with Google
+
+ );
+};
diff --git a/src/components/Login/GoogleSignInButton/styles.ts b/src/components/Login/GoogleSignInButton/styles.ts
new file mode 100644
index 000000000..476463a53
--- /dev/null
+++ b/src/components/Login/GoogleSignInButton/styles.ts
@@ -0,0 +1,20 @@
+import styled from "styled-components";
+import { getMainFont } from "../../common/App/styles";
+
+export const Button = styled.button`
+ ${getMainFont("Roboto")}
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ padding: 10px 12px;
+ border-radius: 4px;
+ border: 1px solid #747775;
+ background: #fff;
+ color: #1f1f1f;
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 20px;
+ cursor: pointer;
+`;
diff --git a/src/components/Login/GoogleSignInButton/types.ts b/src/components/Login/GoogleSignInButton/types.ts
new file mode 100644
index 000000000..ed624a52f
--- /dev/null
+++ b/src/components/Login/GoogleSignInButton/types.ts
@@ -0,0 +1,11 @@
+import type { CodeResponse, NonOAuthError } from "@react-oauth/google";
+
+export interface GoogleSignInButtonProps {
+ onSuccess: (
+ response: Omit
+ ) => void;
+ onError: (
+ response: Pick
+ ) => void;
+ onNonOAuthError: (error: NonOAuthError) => void;
+}
diff --git a/src/components/Login/index.tsx b/src/components/Login/index.tsx
index be491f4e8..32d1ac9c4 100644
--- a/src/components/Login/index.tsx
+++ b/src/components/Login/index.tsx
@@ -1,8 +1,11 @@
+import type { CodeResponse } from "@react-oauth/google";
import axios, { isAxiosError } from "axios";
import type { ChangeEvent, FormEvent } from "react";
import { useState } from "react";
import { Helmet } from "react-helmet";
+import { isString } from "../../typeGuards/isString";
import { TextField } from "../common/v3/TextField";
+import { GoogleSignInButton } from "./GoogleSignInButton";
import * as s from "./styles";
export const Login = () => {
@@ -34,7 +37,7 @@ export const Login = () => {
}
})
.catch((error: Error) => {
- let errorMessage = "Unknown error";
+ let errorMessage = "Failed to sign in";
if (isAxiosError(error)) {
if (error.response) {
@@ -52,6 +55,31 @@ export const Login = () => {
});
};
+ const handleGoogleSignInSuccess = (
+ response: Omit
+ ) => {
+ axios
+ .post("/auth/google", {
+ code: response.code
+ })
+ .then(() => {
+ const urlParams = new URLSearchParams(window.location.search);
+ const returnUrl = urlParams.get("return_url");
+ if (returnUrl) {
+ window.location.href = returnUrl;
+ }
+ })
+ .catch((error: Error) => {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ setError("Failed to sign in with Google");
+ });
+ };
+
+ const handleGoogleSignInError = () => {
+ setError("Failed to sign in with Google");
+ };
+
return (
@@ -70,12 +98,14 @@ export const Login = () => {
type={"text"}
value={username}
onChange={handleUsernameChange}
+ autoComplete={"email"}
/>
{
isDisabled={Boolean(!username || !password)}
/>
+ {isString(window.googleClientId) &&
+ window.googleClientId.length > 0 && (
+ <>
+
+
+ or
+
+
+
+ >
+ )}
By signing in you agree with
theme.colors.v3.surface.secondary};
@@ -60,7 +61,7 @@ export const Subtitle = styled.span`
`;
export const Form = styled.form`
- margin-top: 16px;
+ margin-top: 4px;
display: flex;
flex-direction: column;
gap: 16px;
@@ -71,6 +72,22 @@ export const SignInButton = styled(NewButton)`
justify-content: center;
`;
+export const SignInSeparator = styled.div`
+ ${caption1BoldTypography}
+
+ display: flex;
+ align-items: center;
+ text-align: center;
+ color: ${({ theme }) => theme.colors.v3.text.tertiary};
+ text-transform: uppercase;
+ gap: 8px;
+`;
+
+export const Divider = styled.div`
+ flex-grow: 1;
+ border-bottom: 1px solid ${({ theme }) => theme.colors.v3.stroke.dark};
+`;
+
export const Footer = styled.div`
${subscriptRegularTypography}
diff --git a/src/components/RecentActivity/CreateEnvironmentWizard/ErrorCard/index.tsx b/src/components/RecentActivity/CreateEnvironmentWizard/ErrorCard/index.tsx
index dc9ef0ef3..c5d233b2b 100644
--- a/src/components/RecentActivity/CreateEnvironmentWizard/ErrorCard/index.tsx
+++ b/src/components/RecentActivity/CreateEnvironmentWizard/ErrorCard/index.tsx
@@ -13,6 +13,7 @@ const DEFAULT_TRANSITION_DURATION = 500; // in milliseconds
export const ErrorCard = ({ title, description }: ErrorCardProps) => {
const [isVisible, setIsVisible] = useState(true);
const hideTimerId = useRef();
+ const containerRef = useRef(null);
const startTimer = () => {
hideTimerId.current = window.setTimeout(() => {
@@ -33,8 +34,10 @@ export const ErrorCard = ({ title, description }: ErrorCardProps) => {
timeout={DEFAULT_TRANSITION_DURATION}
classNames={TRANSITION_CLASS_NAME}
unmountOnExit={true}
+ nodeRef={containerRef}
>
{
diff --git a/src/components/RecentActivity/Digmathon/ProgressView/index.tsx b/src/components/RecentActivity/Digmathon/ProgressView/index.tsx
index 1955ad774..35c49fdc5 100644
--- a/src/components/RecentActivity/Digmathon/ProgressView/index.tsx
+++ b/src/components/RecentActivity/Digmathon/ProgressView/index.tsx
@@ -1,4 +1,4 @@
-import { useContext, useEffect } from "react";
+import { useContext, useEffect, useRef } from "react";
import { CSSTransition } from "react-transition-group";
import { isString } from "../../../../typeGuards/isString";
import { openURLInDefaultBrowser } from "../../../../utils/actions/openURLInDefaultBrowser";
@@ -20,7 +20,9 @@ const DIGMATHON_URL = "https://www.digma.ai/digmathon";
const DIGMATHON_LEADERBOARD_URL = "https://www.digma.ai/digmathon/#leaderboard";
export const ProgressView = ({ data }: ProgressViewProps) => {
+ const newIssuesFoundMessageRef = useRef(null);
const config = useContext(ConfigContext);
+
useEffect(() => {
sendTrackingEvent(trackingEvents.DIGMATHON_PROGRESS_VIEWED);
}, []);
@@ -103,8 +105,10 @@ export const ProgressView = ({ data }: ProgressViewProps) => {
classNames={s.NEW_ISSUES_FOUND_MESSAGE_ANIMATION_CLASS_NAME}
unmountOnExit={true}
mountOnEnter={true}
+ nodeRef={newIssuesFoundMessageRef}
>
= {
- title: "Insights/InsightsCatalog/InsightPage/AffectedEndpointsSelector",
+ title: "Insights/InsightsCatalog/InsightsPage/AffectedEndpointsSelector",
component: AffectedEndpointsSelector,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
diff --git a/src/components/common/AffectedEndpointsSelector/index.tsx b/src/components/common/AffectedEndpointsSelector/index.tsx
index fe29379ae..e1ffc6313 100644
--- a/src/components/common/AffectedEndpointsSelector/index.tsx
+++ b/src/components/common/AffectedEndpointsSelector/index.tsx
@@ -7,9 +7,8 @@ import { EndpointOption } from "./EndpointOption";
import * as s from "./styles";
import type { AffectedEndpointsSelectorProps, Option } from "./types";
-export const getEndpointKey = (option: Option): string => {
- return [option.serviceName, option.spanCodeObjectId].join(DELIMITER);
-};
+export const getEndpointKey = (option: Option): string =>
+ [option.serviceName, option.spanCodeObjectId].join(DELIMITER);
const renderOptions = (
endpoints: Option[],
diff --git a/src/components/common/App/ConfigContext.ts b/src/components/common/App/ConfigContext.ts
index 6dce4be31..b4b9d6ebc 100644
--- a/src/components/common/App/ConfigContext.ts
+++ b/src/components/common/App/ConfigContext.ts
@@ -34,7 +34,8 @@ export const initialState: ConfigContextData = {
userId: isString(window.userId) ? window.userId : "",
isDigmathonGameFinished: window.isDigmathonGameFinished === true,
runConfig: undefined,
- areInsightSuggestionsEnabled: window.areInsightSuggestionsEnabled === true
+ areInsightSuggestionsEnabled: window.areInsightSuggestionsEnabled === true,
+ googleClientId: isString(window.googleClientId) ? window.googleClientId : ""
};
export const ConfigContext = createContext(initialState);
diff --git a/src/components/common/App/types.ts b/src/components/common/App/types.ts
index 247cf034e..fd3029091 100644
--- a/src/components/common/App/types.ts
+++ b/src/components/common/App/types.ts
@@ -183,6 +183,7 @@ export interface ConfigContextData {
userInfo?: UserInfo;
runConfig?: RunConfiguration;
areInsightSuggestionsEnabled: boolean;
+ googleClientId: string;
}
export interface InsightStats {
diff --git a/src/components/common/IssuesReport/index.tsx b/src/components/common/IssuesReport/index.tsx
index 8b0b15347..68476e6ee 100644
--- a/src/components/common/IssuesReport/index.tsx
+++ b/src/components/common/IssuesReport/index.tsx
@@ -11,7 +11,8 @@ import type {
EndpointIssuesData,
GetMetricsReportDataPayloadV1,
GetMetricsReportDataPayloadV2,
- ServiceIssuesData
+ ServiceIssuesData,
+ SetEndpointsIssuesPayload
} from "../../../redux/services/types";
import { FeatureFlag } from "../../../types";
import { Chart } from "./Chart";
@@ -26,6 +27,13 @@ import {
transformServicesData
} from "./utils";
+const getEndpointDisplayName = (
+ endpointsIssues: SetEndpointsIssuesPayload | undefined,
+ value: string
+): string | undefined =>
+ endpointsIssues?.reports.find((x) => x.spanCodeObjectId === value)
+ ?.displayName;
+
export const IssuesReport = ({
viewMode,
viewLevel,
@@ -171,22 +179,22 @@ export const IssuesReport = ({
onSelectedServiceChange(value);
onViewLevelChange("endpoints");
}
- onTileTitleClick?.(viewLevel, value);
+
+ const displayName =
+ viewLevel === "endpoints"
+ ? getEndpointDisplayName(endpointsIssues, value)
+ : undefined;
+
+ onTileTitleClick?.(viewLevel, { value, displayName });
};
const handleIssuesStatsClick = (value: string) => {
- if (viewLevel === "services") {
- onTileIssuesStatsClick(viewLevel, { value });
- }
+ const displayName =
+ viewLevel === "endpoints"
+ ? getEndpointDisplayName(endpointsIssues, value)
+ : undefined;
- if (viewLevel === "endpoints") {
- onTileIssuesStatsClick(viewLevel, {
- value,
- displayName: endpointsIssues?.reports.find(
- (x) => x.spanCodeObjectId === value
- )?.displayName
- });
- }
+ onTileIssuesStatsClick(viewLevel, { value, displayName });
};
const handleGoBack = () => {
diff --git a/src/components/common/IssuesReport/types.ts b/src/components/common/IssuesReport/types.ts
index 3cda22ce0..093e2ee79 100644
--- a/src/components/common/IssuesReport/types.ts
+++ b/src/components/common/IssuesReport/types.ts
@@ -6,6 +6,11 @@ import type {
} from "../../../redux/slices/issuesReportSlice";
import type { Severity } from "../../common/IssuesReport/Table/types";
+export interface TargetScope {
+ value: string;
+ displayName?: string;
+}
+
export interface IssuesReportProps {
viewMode: IssuesReportViewMode;
viewLevel: IssuesReportViewLevel;
@@ -24,10 +29,13 @@ export interface IssuesReportProps {
onPeriodInDaysChange: (periodInDays: number) => void;
onTimeModeChange: (timeMode: IssuesReportTimeMode) => void;
onViewModeChange: (viewMode: IssuesReportViewMode) => void;
- onTileTitleClick?: (viewLevel: IssuesReportViewLevel, value: string) => void;
+ onTileTitleClick?: (
+ viewLevel: IssuesReportViewLevel,
+ target: TargetScope
+ ) => void;
onTileIssuesStatsClick: (
viewLevel: IssuesReportViewLevel,
- target: { value: string; displayName?: string }
+ target: TargetScope
) => void;
onViewLevelChange: (viewLevel: IssuesReportViewLevel) => void;
onSelectedServiceChange: (service: string | null) => void;
diff --git a/src/components/common/icons/20px/GoogleLogoIcon.tsx b/src/components/common/icons/20px/GoogleLogoIcon.tsx
new file mode 100644
index 000000000..4f5fa025a
--- /dev/null
+++ b/src/components/common/icons/20px/GoogleLogoIcon.tsx
@@ -0,0 +1,44 @@
+import React from "react";
+import { useIconProps } from "../hooks";
+import type { IconProps } from "../types";
+
+export const GoogleLogoIconComponent = (props: IconProps) => {
+ const { size } = useIconProps(props);
+
+ return (
+
+ );
+};
+
+export const GoogleLogoIcon = React.memo(GoogleLogoIconComponent);
diff --git a/src/components/common/v3/TextField/index.tsx b/src/components/common/v3/TextField/index.tsx
index 6fbd4814b..6a19392fb 100644
--- a/src/components/common/v3/TextField/index.tsx
+++ b/src/components/common/v3/TextField/index.tsx
@@ -15,7 +15,8 @@ const TextFieldComponent = (
disabled,
inputEndContent,
error,
- alwaysRenderError
+ alwaysRenderError,
+ autoComplete
}: TextFieldProps,
ref: ForwardedRef
) => {
@@ -49,6 +50,7 @@ const TextFieldComponent = (
disabled={disabled}
$isInvalid={isInvalid}
ref={ref}
+ autoComplete={autoComplete}
/>
{inputEndContent}
diff --git a/src/components/common/v3/TextField/types.ts b/src/components/common/v3/TextField/types.ts
index 6e2050f3d..aea2a2c3a 100644
--- a/src/components/common/v3/TextField/types.ts
+++ b/src/components/common/v3/TextField/types.ts
@@ -1,6 +1,7 @@
import type {
ChangeEventHandler,
ComponentType,
+ HTMLInputAutoCompleteAttribute,
HTMLInputTypeAttribute,
ReactNode
} from "react";
@@ -18,6 +19,7 @@ export interface TextFieldProps {
type?: HTMLInputTypeAttribute;
icon?: ComponentType;
alwaysRenderError?: boolean;
+ autoComplete?: HTMLInputAutoCompleteAttribute;
}
export interface ContainerProps {
diff --git a/src/containers/Login/GoogleAuthHoC/index.tsx b/src/containers/Login/GoogleAuthHoC/index.tsx
new file mode 100644
index 000000000..e1cf86e60
--- /dev/null
+++ b/src/containers/Login/GoogleAuthHoC/index.tsx
@@ -0,0 +1,18 @@
+import { GoogleOAuthProvider } from "@react-oauth/google";
+import { useConfigSelector } from "../../../store/config/useConfigSelector";
+import { isString } from "../../../typeGuards/isString";
+import type { GoogleAuthHocProps } from "./types";
+
+export const GoogleAuthHoC = ({ children }: GoogleAuthHocProps) => {
+ const { googleClientId } = useConfigSelector();
+
+ if (!isString(googleClientId) || !googleClientId.length) {
+ return children;
+ }
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/containers/Login/GoogleAuthHoC/types.ts b/src/containers/Login/GoogleAuthHoC/types.ts
new file mode 100644
index 000000000..f11f3a8d0
--- /dev/null
+++ b/src/containers/Login/GoogleAuthHoC/types.ts
@@ -0,0 +1,5 @@
+import type { ReactNode } from "react";
+
+export interface GoogleAuthHocProps {
+ children: ReactNode;
+}
diff --git a/src/containers/Login/index.tsx b/src/containers/Login/index.tsx
index f6314783a..0a6fa624c 100644
--- a/src/containers/Login/index.tsx
+++ b/src/containers/Login/index.tsx
@@ -2,6 +2,7 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "../../components/common/App";
import { Login } from "../../components/Login";
+import { GoogleAuthHoC } from "./GoogleAuthHoC";
const APP_ID = "login";
@@ -12,7 +13,9 @@ if (rootElement) {
root.render(
-
+
+
+
);
diff --git a/src/globals.d.ts b/src/globals.d.ts
index 065bdfa7d..0315580ea 100644
--- a/src/globals.d.ts
+++ b/src/globals.d.ts
@@ -53,6 +53,7 @@ declare global {
isDigmathonGameFinished?: unknown;
isLoggingEnabled?: unknown;
areInsightSuggestionsEnabled?: unknown;
+ googleClientId?: unknown;
}
}
diff --git a/src/store/config/configSlice.ts b/src/store/config/configSlice.ts
index 28c528c8a..936659619 100644
--- a/src/store/config/configSlice.ts
+++ b/src/store/config/configSlice.ts
@@ -41,6 +41,7 @@ export interface ConfigState {
selectedServices: string[] | null;
scope: Scope | null;
areInsightSuggestionsEnabled: boolean | null;
+ googleClientId: string | null;
}
const initialState: ConfigState = {
@@ -94,7 +95,8 @@ const initialState: ConfigState = {
scope: null,
areInsightSuggestionsEnabled: isBoolean(window.areInsightSuggestionsEnabled)
? window.areInsightSuggestionsEnabled
- : null
+ : null,
+ googleClientId: isString(window.googleClientId) ? window.googleClientId : null
};
const set = (update: Partial) => (state: ConfigState) => ({