(
- [
-
- The following {dbStatement} query is abnormally slow. Please
- consider optimizing or adding indexes.
-
,
- {queryString}
,
-
- {spanInsight && (
- <>
- Typical duration for {dbStatement} queries in this DB:{" "}
- {getDurationString(spanInsight?.typicalDuration)}
- {"\n"}
- This query: {getDurationString(spanInsight?.duration)}
- >
- )}
-
,
- ,
- ,
- ,
-
- ],
- (i: number) => (
-
- )
- )}
- >
- );
-
- const traceId = span?.traceId;
- const attachments: Attachment[] = [
- ...(traceId
- ? [
- {
- url: `${config.jaegerURL}/api/traces/${traceId}?prettyPrint=true`,
- fileName: `trace-${traceId}.json`
- }
- ]
- : [])
- ];
-
- return (
-
- );
-};
diff --git a/src/components/Insights/insightTickets/common/InsightJiraTicket/InsightJiraTicket.stories.tsx b/src/components/Insights/insightTickets/common/InsightJiraTicket/InsightJiraTicket.stories.tsx
index e5e565e19..4764692be 100644
--- a/src/components/Insights/insightTickets/common/InsightJiraTicket/InsightJiraTicket.stories.tsx
+++ b/src/components/Insights/insightTickets/common/InsightJiraTicket/InsightJiraTicket.stories.tsx
@@ -38,7 +38,6 @@ const insight: SpanUsagesInsight = {
specifity: 4,
isRecalculateEnabled: true,
importance: 5,
- span: "DelayAsync",
sampleTrace: null,
flows: [],
scope: InsightScope.Span,
@@ -50,8 +49,7 @@ const insight: SpanUsagesInsight = {
instrumentationLibrary: "SampleInsightsController",
spanCodeObjectId: "span:SampleInsightsController$_$DelayAsync",
methodCodeObjectId: null,
- kind: "Internal",
- codeObjectId: null
+ kind: "Internal"
},
shortDisplayInfo: {
title: "",
@@ -63,7 +61,6 @@ const insight: SpanUsagesInsight = {
decorators: null,
environment: "BOB-LAPTOP[LOCAL]",
severity: 0,
- prefixedCodeObjectId: "span:SampleInsightsController$_$DelayAsync",
customStartTime: null,
actualStartTime: "2023-06-17T00:00:00.000Z",
ticketLink: null
diff --git a/src/components/Insights/insightTickets/common/InsightJiraTicket/index.tsx b/src/components/Insights/insightTickets/common/InsightJiraTicket/index.tsx
index 44756e00b..77f0015f6 100644
--- a/src/components/Insights/insightTickets/common/InsightJiraTicket/index.tsx
+++ b/src/components/Insights/insightTickets/common/InsightJiraTicket/index.tsx
@@ -1,7 +1,5 @@
import { useContext, useEffect, useState } from "react";
import { dispatcher } from "../../../../../dispatcher";
-import { getFeatureFlagValue } from "../../../../../featureFlags";
-import { FeatureFlag } from "../../../../../types";
import { isValidHttpUrl } from "../../../../../utils/isValidUrl";
import { ConfigContext } from "../../../../common/App/ConfigContext";
import { JiraTicket } from "../../../../common/JiraTicket";
@@ -9,7 +7,9 @@ import { actions } from "../../../actions";
import {
InsightJiraTicketProps,
InsightsGetDataListQuery,
- LinkTicketResponse
+ LinkTicketPayload,
+ LinkTicketResponse,
+ UnlinkTicketPayload
} from "./types";
export const InsightJiraTicket = ({
@@ -27,19 +27,14 @@ export const InsightJiraTicket = ({
);
const config = useContext(ConfigContext);
- const isLinkUnlinkInputVisible = Boolean(
- getFeatureFlagValue(config, FeatureFlag.IS_INSIGHT_TICKET_LINKAGE_ENABLED)
- );
-
const linkTicket = (link: string) => {
setTicketLink(link);
if (link && isValidHttpUrl(link)) {
- window.sendMessageToDigma({
+ window.sendMessageToDigma({
action: actions.LINK_TICKET,
payload: {
- codeObjectId:
- relatedInsight?.codeObjectId ?? insight.prefixedCodeObjectId,
- insightType: relatedInsight?.type ?? insight.type,
+ insightId: relatedInsight?.id || insight.id,
+ insightType: relatedInsight?.type || insight.type,
ticketLink: link
}
});
@@ -49,12 +44,11 @@ export const InsightJiraTicket = ({
};
const unlinkTicket = () => {
- window.sendMessageToDigma({
+ window.sendMessageToDigma({
action: actions.UNLINK_TICKET,
payload: {
- codeObjectId:
- relatedInsight?.codeObjectId ?? insight.prefixedCodeObjectId,
- insightType: relatedInsight?.type ?? insight.type
+ insightId: relatedInsight?.id || insight.id,
+ insightType: relatedInsight?.type || insight.type
}
});
};
@@ -106,7 +100,6 @@ export const InsightJiraTicket = ({
ticketLink={{ link: ticketLink, errorMessage }}
unlinkTicket={unlinkTicket}
linkTicket={linkTicket}
- showLinkButton={isLinkUnlinkInputVisible}
/>
);
};
diff --git a/src/components/Insights/insightTickets/common/InsightJiraTicket/types.ts b/src/components/Insights/insightTickets/common/InsightJiraTicket/types.ts
index 61c91f855..1d642f6d5 100644
--- a/src/components/Insights/insightTickets/common/InsightJiraTicket/types.ts
+++ b/src/components/Insights/insightTickets/common/InsightJiraTicket/types.ts
@@ -21,10 +21,19 @@ export interface LinkTicketResponse {
ticketLink: string | null;
success: boolean;
message: string | null;
- codeObjectId: string;
- insightType: string;
}
export interface InsightsGetDataListQuery {
query: InsightsQuery;
}
+
+export interface LinkTicketPayload {
+ insightId: string;
+ ticketLink: string;
+ insightType: string;
+}
+
+export interface UnlinkTicketPayload {
+ insightId: string;
+ insightType: string;
+}
diff --git a/src/components/Insights/styles.ts b/src/components/Insights/styles.ts
index 5993699b4..51a4a1279 100644
--- a/src/components/Insights/styles.ts
+++ b/src/components/Insights/styles.ts
@@ -7,7 +7,7 @@ export const Container = styled.div`
flex-direction: column;
padding: 8px 0;
gap: 8px;
- height: 100%;
+ min-height: 100%;
box-sizing: border-box;
background: ${({ theme }) => theme.colors.v3.surface.primary};
position: relative;
diff --git a/src/components/Insights/typeGuards.ts b/src/components/Insights/typeGuards.ts
index 05bf3b084..06829e4f7 100644
--- a/src/components/Insights/typeGuards.ts
+++ b/src/components/Insights/typeGuards.ts
@@ -83,6 +83,7 @@ export const isEndpointHighUsageInsight = (
/**
* @deprecated
+ * safe to delete after 2024-06-05
*/
export const isEndpointSlowestSpansInsight = (
insight: CodeObjectInsight
@@ -104,6 +105,7 @@ export const isSpanNPlusOneInsight = (
/**
* @deprecated
+ * safe to delete after 2024-06-05
*/
export const isEndpointSuspectedNPlusOneInsight = (
insight: CodeObjectInsight
@@ -117,6 +119,7 @@ export const isEndpointSpanNPlusOneInsight = (
/**
* @deprecated
+ * safe to delete after 2024-06-05
*/
export const isEndpointQueryOptimizationInsight = (
insight: CodeObjectInsight
@@ -147,6 +150,7 @@ export const isEndpointSlowdownSourceInsight = (
/**
* @deprecated
+ * safe to delete after 2024-06-05
*/
export const isEndpointDurationSlowdownInsight = (
insight: CodeObjectInsight
@@ -158,17 +162,11 @@ export const isEndpointBreakdownInsight = (
): insight is EndpointBreakdownInsight =>
insight.type === InsightType.EndpointBreakdown;
-/**
- * @deprecated
- */
export const isSpanScalingWellInsight = (
insight: CodeObjectInsight
): insight is SpanScalingWellInsight =>
insight.type === InsightType.SpanScalingWell;
-/**
- * @deprecated
- */
export const isSpanScalingInsufficientDataInsight = (
insight: CodeObjectInsight
): insight is SpanScalingInsufficientDataInsight =>
@@ -181,6 +179,7 @@ export const isSessionInViewEndpointInsight = (
/**
* @deprecated
+ * safe to delete after 2024-06-05
*/
export const isChattyApiEndpointInsight = (
insight: CodeObjectInsight
diff --git a/src/components/Insights/types.ts b/src/components/Insights/types.ts
index ca63305b4..4c0d668a1 100644
--- a/src/components/Insights/types.ts
+++ b/src/components/Insights/types.ts
@@ -178,7 +178,6 @@ export interface CodeObjectInsight extends Insight {
importance: InsightImportance;
severity: number;
isRecalculateEnabled: boolean;
- prefixedCodeObjectId: string | null;
customStartTime: string | null;
actualStartTime: string | null;
criticality: number;
@@ -205,6 +204,7 @@ export interface CodeObjectInsight extends Insight {
export interface SpanInsight extends CodeObjectInsight {
spanInfo: SpanInfo | null;
+ scope: InsightScope.Span;
}
export interface HistogramBarData {
@@ -218,38 +218,33 @@ export interface NormalizedHistogramBarData extends HistogramBarData {
normalizedCount: number;
}
+export interface PercentileDurations {
+ percentile: number;
+ currentDuration: Duration;
+ previousDuration: Duration | null;
+ changeTime: string | null;
+ changeVerified: boolean | null;
+ traceIds: string[];
+}
+
+export interface Plot {
+ bars: HistogramBarData[];
+ quantiles: {
+ timestamp: Duration;
+ quantileValue: number;
+ }[];
+}
+
export interface SpanDurationsInsight extends SpanInsight {
name: "Performance Stats";
type: InsightType.SpanDurations;
category: InsightCategory.Performance;
specifity: InsightSpecificity.OwnInsight;
isRecalculateEnabled: true;
- percentiles: {
- percentile: number;
- currentDuration: Duration;
- previousDuration: Duration | null;
- changeTime: string | null;
- changeVerified: boolean | null;
- traceIds: string[];
- }[];
+ percentiles: PercentileDurations[];
lastSpanInstanceInfo: SpanInstanceInfo | null;
isAsync: boolean;
-
- /**
- * @deprecated
- */
- spanCodeObjectId: string;
- /**
- * @deprecated
- */
- span: SpanInfo;
- histogramPlot?: {
- bars: HistogramBarData[];
- quantiles: {
- timestamp: Duration;
- quantileValue: number;
- }[];
- } | null;
+ histogramPlot?: Plot | null;
average?: Duration;
standardDeviation?: Duration;
}
@@ -277,11 +272,6 @@ export interface SpanUsagesInsight extends SpanInsight {
lastService: FlowSpan | null;
lastServiceSpan: string | null;
}[];
-
- /**
- * @deprecated
- */
- span: string;
}
interface Percentile {
@@ -306,19 +296,6 @@ export interface BottleneckEndpointInfo {
criticality: number;
requestPercentage: number;
traceId: string | null;
-
- /**
- * @deprecated
- */
- p50: Percentile;
- /**
- * @deprecated
- */
- p95: Percentile;
- /**
- * @deprecated
- */
- p99: Percentile;
}
export interface SpanEndpointBottleneckInsight extends SpanInsight {
@@ -328,23 +305,6 @@ export interface SpanEndpointBottleneckInsight extends SpanInsight {
specifity: InsightSpecificity.TargetFound;
importance: InsightImportance.Critical;
slowEndpoints: BottleneckEndpointInfo[] | null;
-
- /**
- * @deprecated
- */
- p50: Percentile;
- /**
- * @deprecated
- */
- p95: Percentile;
- /**
- * @deprecated
- */
- p99: Percentile;
- /**
- * @deprecated
- */
- span: SpanInfo;
}
export interface DurationPercentile {
@@ -369,29 +329,12 @@ export interface SpanDurationBreakdownInsight extends SpanInsight {
isRecalculateEnabled: true;
importance: InsightImportance.Info;
breakdownEntries: SpanDurationBreakdownEntry[];
-
- /**
- * @deprecated
- */
- spanName: string;
- /**
- * @deprecated
- */
- spanCodeObjectId: string;
}
-export interface EndpointInsight extends SpanInsight {
+export interface EndpointInsight extends Omit {
route: string;
serviceName: string;
-
- /**
- * @deprecated
- */
- endpointSpan: string;
- /**
- * @deprecated
- */
- spanCodeObjectId: string;
+ scope: InsightScope.EntrySpan;
}
export interface EndpointLowUsageInsight extends EndpointInsight {
@@ -426,6 +369,7 @@ export interface EndpointHighUsageInsight extends EndpointInsight {
/**
* @deprecated
+ * safe to delete after 2024-06-05
*/
export interface EndpointSlowestSpansInsight extends EndpointInsight {
name: "Bottleneck Detected";
@@ -440,19 +384,6 @@ export interface EndpointSlowestSpansInsight extends EndpointInsight {
avgDurationWhenBeingBottleneck: Duration;
criticality: number;
ticketLink: string | null;
-
- /**
- * @deprecated
- */
- p50: Percentile;
- /**
- * @deprecated
- */
- p95: Percentile;
- /**
- * @deprecated
- */
- p99: Percentile;
}[];
}
@@ -472,19 +403,6 @@ export interface EndpointBottleneckInsight extends EndpointInsight {
ticketLink: string | null;
requestPercentage: number;
traceId: string | null;
-
- /**
- * @deprecated
- */
- p50: Percentile;
- /**
- * @deprecated
- */
- p95: Percentile;
- /**
- * @deprecated
- */
- p99: Percentile;
};
}
@@ -499,35 +417,6 @@ export interface SlowEndpointInsight extends EndpointInsight {
endpointsMedianOfMedians: Duration;
endpointsP75: Duration;
median: Duration;
-
- /**
- * @deprecated
- */
- endpointsMedianOfP75: Duration;
- /**
- * @deprecated
- */
- min: Duration;
- /**
- * @deprecated
- */
- max: Duration;
- /**
- * @deprecated
- */
- mean: Duration;
- /**
- * @deprecated
- */
- p75: Duration;
- /**
- * @deprecated
- */
- p95: Duration;
- /**
- * @deprecated
- */
- p99: Duration;
}
export interface RootCauseSpanInfo extends SpanInfo {
@@ -555,15 +444,6 @@ export interface SpanScalingInsight extends SpanInsight {
rootCauseSpans: RootCauseSpanInfo[];
affectedEndpoints: AffectedEndpoint[] | null;
flowHash: string | null;
-
- /**
- * @deprecated
- */
- spanName: string;
- /**
- * @deprecated
- */
- spanInstrumentationLibrary: string;
}
export interface NPlusOneEndpointInfo {
@@ -590,21 +470,14 @@ export interface SpaNPlusOneInsight extends SpanInsight {
category: InsightCategory.Performance;
specifity: InsightSpecificity.TargetAndReasonFound;
importance: InsightImportance.Critical;
- occurrences: number;
traceId: string | null;
- clientSpanName: string | null;
- clientSpanCodeObjectId: string | null;
duration: Duration;
endpoints: NPlusOneEndpointInfo[] | null;
-
- /**
- * @deprecated
- */
- span: SpanInfo;
}
/**
* @deprecated
+ * safe to delete after 2024-06-05
*/
export interface EndpointSuspectedNPlusOneInsight extends EndpointInsight {
name: "Suspected N+1 Query";
@@ -690,11 +563,13 @@ export interface EndpointSlowdownSource {
/**
* @deprecated
+ * safe to delete after 2024-06-05
*/
export type DurationSlowdownSource = EndpointSlowdownSource;
/**
* @deprecated
+ * safe to delete after 2024-06-05
*/
export interface EndpointDurationSlowdownInsight extends EndpointInsight {
name: "Endpoint Duration Slowdown Source";
@@ -736,15 +611,11 @@ export interface EndpointBreakdownInsight extends EndpointInsight {
specifity: InsightSpecificity.OwnInsight;
importance: InsightImportance.Info;
isRecalculateEnabled: true;
- components: Component[];
- p50Components: Component[] | null;
- p95Components: Component[] | null;
+ p50Components: Component[];
+ p95Components: Component[];
hasAsyncSpans: boolean;
}
-/**
- * @deprecated
- */
export interface SpanScalingWellInsight extends SpanInsight {
name: "Scaling Well";
type: InsightType.SpanScalingWell;
@@ -757,17 +628,11 @@ export interface SpanScalingWellInsight extends SpanInsight {
flowHash: string | null;
}
-/**
- * @deprecated
- */
export interface Concurrency {
calls: number;
meanDuration: Duration;
}
-/**
- * @deprecated
- */
export interface SpanScalingInsufficientDataInsight extends SpanInsight {
name: "Scaling Insufficient Data";
type: InsightType.SpanScalingInsufficientData;
@@ -792,6 +657,7 @@ export interface EndpointSessionInViewInsight extends EndpointInsight {
/**
* @deprecated
+ * safe to delete after 2024-06-05
*/
export interface EndpointChattyApiInsight extends EndpointInsight {
name: "HTTP Chatter";
@@ -871,6 +737,7 @@ export interface EndpointQueryOptimizationSpan {
/**
* @deprecated
+ * safe to delete after 2024-06-05
*/
export interface EndpointQueryOptimizationInsight extends EndpointInsight {
name: "Query Optimization";
diff --git a/src/components/Insights/useInsightsData.ts b/src/components/Insights/useInsightsData.ts
index e56a16c0f..0229d1afe 100644
--- a/src/components/Insights/useInsightsData.ts
+++ b/src/components/Insights/useInsightsData.ts
@@ -135,7 +135,7 @@ export const useInsightsData = ({
useEffect(() => {
getData(scopedQuery, state);
setIsLoading(true);
- }, [scopedQuery, scope?.span?.spanCodeObjectId, environment?.originalName]);
+ }, [scopedQuery, scope?.span?.spanCodeObjectId, environment?.id]);
return {
isInitialLoading,
diff --git a/src/components/RecentActivity/AddEnvironmentDialog/AddEnvironmentDialog.stories.tsx b/src/components/Main/Authentication/Authentication.stories.tsx
similarity index 62%
rename from src/components/RecentActivity/AddEnvironmentDialog/AddEnvironmentDialog.stories.tsx
rename to src/components/Main/Authentication/Authentication.stories.tsx
index 9ae49149b..d8e69a2f2 100644
--- a/src/components/RecentActivity/AddEnvironmentDialog/AddEnvironmentDialog.stories.tsx
+++ b/src/components/Main/Authentication/Authentication.stories.tsx
@@ -1,10 +1,10 @@
import { Meta, StoryObj } from "@storybook/react";
-import { AddEnvironmentDialog } from ".";
+import { Authentication } from ".";
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
-const meta: Meta = {
- title: "Recent Activity/AddEnvironmentDialog",
- component: AddEnvironmentDialog,
+const meta: Meta = {
+ title: "Main/Authentication",
+ component: Authentication,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
layout: "fullscreen"
@@ -15,6 +15,4 @@ export default meta;
type Story = StoryObj;
-export const Default: Story = {
- args: {}
-};
+export const Default: Story = {};
diff --git a/src/components/Main/Authentication/Login/Login.stories.tsx b/src/components/Main/Authentication/Login/Login.stories.tsx
new file mode 100644
index 000000000..06942caa0
--- /dev/null
+++ b/src/components/Main/Authentication/Login/Login.stories.tsx
@@ -0,0 +1,46 @@
+import { Meta, StoryObj } from "@storybook/react";
+import { Login } from ".";
+import { actions } from "../../../../actions";
+
+// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
+const meta: Meta = {
+ title: "Main/Login",
+ component: Login,
+ parameters: {
+ // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
+ layout: "fullscreen"
+ }
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const WithSuccessMessage: Story = {
+ args: {
+ successMessage: "Message"
+ }
+};
+
+export const Failed: Story = {
+ play: () => {
+ setTimeout(
+ () =>
+ window.postMessage({
+ type: "digma",
+ action: actions.SET_LOGIN_RESULT,
+ payload: {
+ errors: [
+ {
+ errorCode: "Invalid password",
+ description: "Pls enter valid password"
+ }
+ ]
+ }
+ }),
+ 1000
+ );
+ }
+};
diff --git a/src/components/Main/Authentication/Login/index.tsx b/src/components/Main/Authentication/Login/index.tsx
new file mode 100644
index 000000000..7983a0974
--- /dev/null
+++ b/src/components/Main/Authentication/Login/index.tsx
@@ -0,0 +1,134 @@
+import { KeyboardEvent, useEffect } from "react";
+import { Controller, useForm } from "react-hook-form";
+import { sendUserActionTrackingEvent } from "../../../../utils/actions/sendUserActionTrackingEvent";
+import { LockIcon } from "../../../common/icons/12px/LockIcon";
+import { EnvelopeIcon } from "../../../common/icons/16px/EnvelopeIcon";
+import { Spinner } from "../../../common/v3/Spinner";
+import { TextField } from "../../../common/v3/TextField";
+import {
+ ButtonsContainer,
+ ErrorMessage,
+ Form,
+ FormContainer,
+ InfoMessage,
+ Loader,
+ SubmitButton
+} from "../styles";
+import * as s from "./../styles";
+import { LoginFormValues, LoginProps } from "./types";
+import { useLogin } from "./useLogin";
+
+const formDefaultValues: LoginFormValues = {
+ password: "",
+ email: ""
+};
+
+export const Login = ({ successMessage, onLogin }: LoginProps) => {
+ const {
+ handleSubmit,
+ control,
+ getValues,
+ clearErrors,
+ watch,
+ setError,
+ formState: { errors, isValid },
+ setFocus
+ } = useForm({
+ mode: "onChange",
+ defaultValues: formDefaultValues
+ });
+ const values = getValues();
+ const { isLoading, login, error } = useLogin();
+
+ useEffect(() => {
+ setFocus("email");
+ }, [setFocus]);
+
+ useEffect(() => {
+ if (error) {
+ setError("root", {
+ type: "validate",
+ message: error.description
+ });
+ }
+ }, [setError, error]);
+
+ useEffect(() => {
+ watch(() => {
+ clearErrors();
+ });
+ }, [clearErrors, watch]);
+
+ const onSubmit = (data: LoginFormValues) => {
+ onLogin();
+ login({ email: data.email, password: data.password });
+ sendUserActionTrackingEvent("login form submitted");
+ };
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Enter" && isValid) {
+ onSubmit(values);
+ }
+ };
+ const errorMessage =
+ Object.values(errors).length > 0 ? Object.values(errors)[0].message : "";
+
+ return (
+
+
+ {errorMessage && {errorMessage}}
+
+
+
+ {successMessage && {successMessage}}
+ Forgot password? Contact the Digma admin
+ {isLoading && (
+
+
+ Loading...
+
+ )}
+
+ );
+};
diff --git a/src/components/Main/Authentication/Login/types.ts b/src/components/Main/Authentication/Login/types.ts
new file mode 100644
index 000000000..9108a23b3
--- /dev/null
+++ b/src/components/Main/Authentication/Login/types.ts
@@ -0,0 +1,9 @@
+export interface LoginProps {
+ successMessage?: string;
+ onLogin: () => void;
+}
+
+export interface LoginFormValues {
+ password: string;
+ email: string;
+}
diff --git a/src/components/Main/Authentication/Login/useLogin.ts b/src/components/Main/Authentication/Login/useLogin.ts
new file mode 100644
index 000000000..33dc502c3
--- /dev/null
+++ b/src/components/Main/Authentication/Login/useLogin.ts
@@ -0,0 +1,40 @@
+import { useEffect, useState } from "react";
+import { actions } from "../../../../actions";
+import { dispatcher } from "../../../../dispatcher";
+import { useLoading } from "../../../Insights/insightTickets/common";
+import { LoginPayload, LoginResult } from "../../types";
+
+export const useLogin = () => {
+ const [isLoading, setIsLoading] = useLoading(false);
+ const [error, setError] = useState<{ description: string }>();
+
+ useEffect(() => {
+ const handleLogin = (data: unknown) => {
+ const result = data as LoginResult;
+ if (result.error) {
+ setError({ description: result.error });
+ }
+ setIsLoading(false);
+ };
+
+ dispatcher.addActionListener(actions.SET_LOGIN_RESULT, handleLogin);
+
+ return () => {
+ dispatcher.removeActionListener(actions.SET_LOGIN_RESULT, handleLogin);
+ };
+ }, []);
+
+ return {
+ login: (payload: LoginPayload) => {
+ setIsLoading(true);
+ window.sendMessageToDigma({
+ action: actions.LOGIN,
+ payload: {
+ ...payload
+ }
+ });
+ },
+ isLoading,
+ error
+ };
+};
diff --git a/src/components/Main/Authentication/Registration/Registration.stories.tsx b/src/components/Main/Authentication/Registration/Registration.stories.tsx
new file mode 100644
index 000000000..fafa3f464
--- /dev/null
+++ b/src/components/Main/Authentication/Registration/Registration.stories.tsx
@@ -0,0 +1,40 @@
+import { Meta, StoryObj } from "@storybook/react";
+import { Registration } from ".";
+import { actions } from "../../../../actions";
+
+// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
+const meta: Meta = {
+ title: "Main/Registration",
+ component: Registration,
+ parameters: {
+ // More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
+ layout: "fullscreen"
+ }
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const Failed: Story = {
+ play: () => {
+ setTimeout(
+ () =>
+ window.postMessage({
+ type: "digma",
+ action: actions.SET_REGISTRATION_RESULT,
+ payload: {
+ errors: [
+ {
+ errorCode: "Invalid password",
+ description: "Pls enter valid password"
+ }
+ ]
+ }
+ }),
+ 1000
+ );
+ }
+};
diff --git a/src/components/Main/Authentication/Registration/index.tsx b/src/components/Main/Authentication/Registration/index.tsx
new file mode 100644
index 000000000..0bd8b76e9
--- /dev/null
+++ b/src/components/Main/Authentication/Registration/index.tsx
@@ -0,0 +1,200 @@
+import { KeyboardEvent, useEffect, useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+import { sendUserActionTrackingEvent } from "../../../../utils/actions/sendUserActionTrackingEvent";
+import { isAlphanumeric } from "../../../../utils/isAlphanumeric";
+import { isValidEmailFormat } from "../../../../utils/isValidEmailFormat";
+import { LockIcon } from "../../../common/icons/12px/LockIcon";
+import { EnvelopeIcon } from "../../../common/icons/16px/EnvelopeIcon";
+import { Spinner } from "../../../common/v3/Spinner";
+import { TextField } from "../../../common/v3/TextField";
+import * as s from "./../styles";
+import { Loader } from "./../styles";
+import { RegisterFormValues, RegistrationProps } from "./types";
+import { useRegistration } from "./useRegistration";
+
+const validateEmail = (email: string): string | boolean => {
+ const emailMessage = "Please enter a valid email address";
+
+ if (email.length === 0) {
+ return emailMessage;
+ }
+
+ if (!isValidEmailFormat(email)) {
+ return emailMessage;
+ }
+
+ return true;
+};
+
+const validatePassword = (password: string): string | boolean => {
+ if (password.length < 6) {
+ return "Password must be at least 6.";
+ }
+
+ if (isAlphanumeric(password)) {
+ return "Password must contain one special character";
+ }
+
+ return true;
+};
+
+const formDefaultValues: RegisterFormValues = {
+ password: "",
+ email: "",
+ confirmPassword: ""
+};
+
+export const Registration = ({ onRegister }: RegistrationProps) => {
+ const {
+ handleSubmit,
+ control,
+ getValues,
+ watch,
+ setError,
+ clearErrors,
+ resetField,
+ formState: { errors, isValid, touchedFields },
+ setFocus
+ } = useForm({
+ mode: "onChange",
+ defaultValues: formDefaultValues
+ });
+ const values = getValues();
+ const {
+ isLoading,
+ isSucceed,
+ register,
+ errors: resultErrors
+ } = useRegistration();
+
+ const [responseStatus, setResponseStatus] = useState();
+
+ useEffect(() => {
+ if (resultErrors?.length > 0) {
+ setResponseStatus(resultErrors.map((x) => x.description).join("\n"));
+ }
+ }, [setError, resultErrors]);
+
+ useEffect(() => {
+ setFocus("email");
+ }, [setFocus]);
+
+ useEffect(() => {
+ watch(() => {
+ setResponseStatus(undefined);
+ });
+ }, [clearErrors, watch, setResponseStatus]);
+
+ useEffect(() => {
+ if (isSucceed) {
+ onRegister();
+ }
+ }, [isSucceed, onRegister]);
+
+ const onSubmit = (data: RegisterFormValues) => {
+ register({ email: data.email, password: data.password });
+ sendUserActionTrackingEvent("registration form submitted");
+ };
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Enter" && isValid) {
+ onSubmit(values);
+ }
+ };
+
+ const errorMessage =
+ Object.values(errors).length > 0 ? Object.values(errors)[0].message : "";
+
+ return (
+
+ {
+ e.preventDefault();
+ void handleSubmit(onSubmit)(e);
+ }}
+ >
+ (
+
+ )}
+ />
+ (
+ {
+ if (touchedFields.confirmPassword) {
+ resetField("confirmPassword");
+ }
+ field.onChange && field.onChange(args);
+ }}
+ />
+ )}
+ />
+ {
+ if (repeat?.length === 0) {
+ return "Confirm your password";
+ }
+ if (repeat !== values.password) {
+ return "Those passwords didn’t match. Try again.";
+ }
+ return true;
+ }
+ }}
+ render={({ field }) => (
+
+ )}
+ />
+
+ {errorMessage && {errorMessage}}
+ {responseStatus && {responseStatus}}
+
+
+
+ {isLoading && (
+
+
+ Loading...
+
+ )}
+
+ );
+};
diff --git a/src/components/Main/Authentication/Registration/types.ts b/src/components/Main/Authentication/Registration/types.ts
new file mode 100644
index 000000000..e59bfc626
--- /dev/null
+++ b/src/components/Main/Authentication/Registration/types.ts
@@ -0,0 +1,9 @@
+export interface RegistrationProps {
+ onRegister: () => void;
+}
+
+export interface RegisterFormValues {
+ password: string;
+ email: string;
+ confirmPassword: string;
+}
diff --git a/src/components/Main/Authentication/Registration/useRegistration.ts b/src/components/Main/Authentication/Registration/useRegistration.ts
new file mode 100644
index 000000000..23af1e626
--- /dev/null
+++ b/src/components/Main/Authentication/Registration/useRegistration.ts
@@ -0,0 +1,53 @@
+import { useEffect, useState } from "react";
+import { actions } from "../../../../actions";
+import { dispatcher } from "../../../../dispatcher";
+import { useLoading } from "../../../Insights/insightTickets/common";
+import { ErrorData, RegisterPayload, RegisterResult } from "../../types";
+
+export const useRegistration = () => {
+ const [isLoading, setIsLoading] = useLoading(false);
+ const [errors, setErrors] = useState([]);
+ const [isSucceed, setIsSucceed] = useState(undefined);
+
+ useEffect(() => {
+ const handleRegister = (data: unknown) => {
+ const result = data as RegisterResult;
+ if (result.errors) {
+ setErrors(result.errors);
+ setIsSucceed(false);
+ } else {
+ setErrors([]);
+ setIsSucceed(true);
+ }
+
+ setIsLoading(false);
+ };
+
+ dispatcher.addActionListener(
+ actions.SET_REGISTRATION_RESULT,
+ handleRegister
+ );
+
+ return () => {
+ dispatcher.removeActionListener(
+ actions.SET_REGISTRATION_RESULT,
+ handleRegister
+ );
+ };
+ }, []);
+
+ return {
+ isLoading,
+ errors,
+ isSucceed,
+ register: (payload: RegisterPayload) => {
+ setIsLoading(true);
+ window.sendMessageToDigma({
+ action: actions.REGISTER,
+ payload: {
+ ...payload
+ }
+ });
+ }
+ };
+};
diff --git a/src/components/Main/Authentication/index.tsx b/src/components/Main/Authentication/index.tsx
new file mode 100644
index 000000000..a91f32142
--- /dev/null
+++ b/src/components/Main/Authentication/index.tsx
@@ -0,0 +1,79 @@
+import { forwardRef, useState } from "react";
+import { SLACK_WORKSPACE_URL } from "../../../constants";
+import { openURLInDefaultBrowser } from "../../../utils/actions/openURLInDefaultBrowser";
+import { SlackLogoIcon } from "../../common/icons/16px/SlackLogoIcon";
+import { DigmaLoginLogo } from "../../common/icons/DigmaLoginLogo";
+import { Toggle } from "../../common/v3/Toggle";
+import { Login } from "./Login";
+import { Registration } from "./Registration";
+import * as s from "./styles";
+
+const AuthenticationComponent = () => {
+ const [option, setOption] = useState<"login" | "register">("login");
+ const [loginSuccessMessage, setLoginSuccessMessage] = useState();
+
+ const handleRegister = () => {
+ setOption("login");
+ setLoginSuccessMessage("Account registered successfully! Please login");
+ };
+
+ const handleLogin = () => {
+ setLoginSuccessMessage("");
+ };
+
+ const handleSlackLinkClick = () => {
+ openURLInDefaultBrowser(SLACK_WORKSPACE_URL);
+ };
+
+ const handleToggleValueChange = (option: "login" | "register") => {
+ setOption(option);
+ setLoginSuccessMessage("");
+ };
+
+ return (
+
+
+
+
+
+ Welcome to Digma
+
+ In order to find issues, analytics and errors in your code, please
+ sign in, or register new account
+
+
+
+
+ Sign In,
+ value: "login"
+ },
+ {
+ label: Sign Up,
+ value: "register"
+ }
+ ]}
+ value={option}
+ onValueChange={handleToggleValueChange}
+ />
+ {option === "login" ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ Join Our Digma Channel
+
+
+
+ );
+};
+
+export const Authentication = forwardRef(AuthenticationComponent);
diff --git a/src/components/Main/Authentication/styles.ts b/src/components/Main/Authentication/styles.ts
new file mode 100644
index 000000000..fe1fbfeb1
--- /dev/null
+++ b/src/components/Main/Authentication/styles.ts
@@ -0,0 +1,142 @@
+import styled from "styled-components";
+import {
+ bodyMediumTypography,
+ bodyRegularTypography,
+ caption1RegularTypography,
+ subscriptRegularTypography
+} from "../../common/App/typographies";
+import { Button } from "../../common/v3/Button";
+import { Link } from "../../common/v3/Link";
+
+export const Container = styled.div`
+ padding: 10px;
+ min-height: 100%;
+ gap: 16px;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+ background: ${({ theme }) => theme.colors.v3.surface.secondary};
+`;
+
+export const Header = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ align-items: center;
+`;
+
+export const Message = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ text-align: center;
+`;
+
+export const Title = styled.div`
+ color: ${({ theme }) => theme.colors.v3.text.white};
+ ${bodyMediumTypography}
+`;
+
+export const Description = styled.div`
+ color: ${({ theme }) => theme.colors.v3.text.secondary};
+ ${subscriptRegularTypography}
+`;
+
+export const ToggleOptions = styled.div`
+ padding: 4px 8px;
+ height: 16px;
+ width: 95px;
+
+ ${subscriptRegularTypography}
+`;
+
+export const Footer = styled.div`
+ margin-top: auto;
+ display: flex;
+ justify-content: center;
+`;
+
+export const FormContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+`;
+
+export const ContentContainer = styled(FormContainer)`
+ margin-top: 10%;
+`;
+
+export const Inputs = styled.div`
+ gap: 8px;
+ display: flex;
+ flex-direction: column;
+`;
+
+export const Form = styled.form`
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ width: 100%;
+`;
+
+export const ButtonsContainer = styled.div`
+ display: flex;
+ gap: 8px;
+ width: 100%;
+`;
+
+const StatusMessage = styled.span`
+ display: flex;
+ font-size: 13px;
+ height: 15px;
+ align-items: center;
+ align-self: flex-start;
+`;
+
+export const ErrorMessage = styled(StatusMessage)`
+ color: ${({ theme }) => theme.colors.v3.status.high};
+`;
+
+export const SuccessMessage = styled(StatusMessage)`
+ color: ${({ theme }) => theme.colors.v3.status.success};
+ text-align: center;
+`;
+
+export const SubmitButton = styled(Button)`
+ align-self: flex-end;
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ height: 28px;
+`;
+
+export const InputForm = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ max-width: 250px;
+`;
+
+export const InfoMessage = styled.div`
+ ${caption1RegularTypography}
+ color: ${({ theme }) => theme.colors.v3.text.secondary};
+`;
+
+export const SlackLink = styled(Link)`
+ ${bodyRegularTypography}
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ text-decoration: underline;
+ padding: 15px;
+`;
+
+export const Loader = styled.div`
+ display: flex;
+ align-items: center;
+ ${caption1RegularTypography}
+ gap: 4px;
+ color: ${({ theme }) => theme.colors.v3.text.primary};
+`;
diff --git a/src/components/Main/actions.ts b/src/components/Main/actions.ts
index efd223b29..01cedee40 100644
--- a/src/components/Main/actions.ts
+++ b/src/components/Main/actions.ts
@@ -6,5 +6,7 @@ export const actions = addPrefix(ACTION_PREFIX, {
INITIALIZE: "INITIALIZE",
SET_VIEWS: "SET_VIEWS",
GET_HIGHLIGHTS_TOP_ISSUES_DATA: "GET_HIGHLIGHTS_TOP_ISSUES_DATA",
- SET_HIGHLIGHTS_TOP_ISSUES_DATA: "SET_HIGHLIGHTS_TOP_ISSUES_DATA"
+ SET_HIGHLIGHTS_TOP_ISSUES_DATA: "SET_HIGHLIGHTS_TOP_ISSUES_DATA",
+ GET_HIGHLIGHTS_PERFORMANCE_DATA: "GET_HIGHLIGHTS_PERFORMANCE_DATA",
+ SET_HIGHLIGHTS_PERFORMANCE_DATA: "SET_HIGHLIGHTS_PERFORMANCE_DATA"
});
diff --git a/src/components/Main/index.tsx b/src/components/Main/index.tsx
index 8cc9d73c0..68f36f72d 100644
--- a/src/components/Main/index.tsx
+++ b/src/components/Main/index.tsx
@@ -1,16 +1,19 @@
-import { useLayoutEffect, useState } from "react";
+import { useContext, useLayoutEffect, useState } from "react";
import { dispatcher } from "../../dispatcher";
import { Assets } from "../Assets";
import { Highlights } from "../Highlights";
import { Insights } from "../Insights";
import { SetViewsPayload } from "../Navigation/types";
import { Tests } from "../Tests";
+import { ConfigContext } from "../common/App/ConfigContext";
+import { Authentication } from "./Authentication";
import { actions } from "./actions";
import { isView } from "./typeGuards";
import { View } from "./types";
export const Main = () => {
const [view, setView] = useState("insights");
+ const config = useContext(ConfigContext);
useLayoutEffect(() => {
window.sendMessageToDigma({
@@ -30,7 +33,11 @@ export const Main = () => {
return () => {
dispatcher.removeActionListener(actions.SET_VIEWS, handleSetViewsData);
};
- }, []);
+ }, [config.userInfo?.id]);
+
+ if (!config.userInfo?.id && config.backendInfo?.centralize) {
+ return ;
+ }
switch (view) {
case "highlights":
diff --git a/src/components/Main/types.ts b/src/components/Main/types.ts
index 33bd424cf..729aacbe7 100644
--- a/src/components/Main/types.ts
+++ b/src/components/Main/types.ts
@@ -13,3 +13,27 @@ export interface GetHighlightsTopIssuesDataPayload {
environments: string[];
};
}
+
+export interface LoginPayload {
+ email: string;
+ password: string;
+}
+
+export interface ErrorData {
+ errorCode: string;
+ description: string;
+}
+
+export interface LoginResult {
+ error: string;
+}
+
+export interface RegisterPayload {
+ email: string;
+ password: string;
+}
+
+export interface RegisterResult {
+ errors?: ErrorData[];
+ success: string;
+}
diff --git a/src/components/Navigation/CodeButton/AnimatedCodeButton/AnimatedCodeButton.stories.tsx b/src/components/Navigation/CodeButton/AnimatedCodeButton/AnimatedCodeButton.stories.tsx
index 4dabe329e..4b9da87c3 100644
--- a/src/components/Navigation/CodeButton/AnimatedCodeButton/AnimatedCodeButton.stories.tsx
+++ b/src/components/Navigation/CodeButton/AnimatedCodeButton/AnimatedCodeButton.stories.tsx
@@ -17,6 +17,4 @@ export default meta;
type Story = StoryObj;
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
-export const Default: Story = {
- args: {}
-};
+export const Default: Story = {};
diff --git a/src/components/Navigation/EnvironmentBar/EnvironmentBar.stories.tsx b/src/components/Navigation/EnvironmentBar/EnvironmentBar.stories.tsx
index 1ae27638b..ea346d0c8 100644
--- a/src/components/Navigation/EnvironmentBar/EnvironmentBar.stories.tsx
+++ b/src/components/Navigation/EnvironmentBar/EnvironmentBar.stories.tsx
@@ -20,13 +20,11 @@ type Story = StoryObj;
export const Default: Story = {
args: {
selectedEnvironment: {
- originalName: "DEV",
+ id: "DEV",
name: "DEV",
- type: "local"
+ type: "Private"
}
}
};
-export const Disabled: Story = {
- args: {}
-};
+export const Disabled: Story = {};
diff --git a/src/components/Navigation/KebabMenu/KebabMenu.stories.tsx b/src/components/Navigation/KebabMenu/KebabMenu.stories.tsx
index c0d201a76..80908c9f2 100644
--- a/src/components/Navigation/KebabMenu/KebabMenu.stories.tsx
+++ b/src/components/Navigation/KebabMenu/KebabMenu.stories.tsx
@@ -17,6 +17,4 @@ export default meta;
type Story = StoryObj;
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
-export const Default: Story = {
- args: {}
-};
+export const Default: Story = {};
diff --git a/src/components/Navigation/KebabMenu/index.tsx b/src/components/Navigation/KebabMenu/index.tsx
index d1dd1ae28..bd7e58baf 100644
--- a/src/components/Navigation/KebabMenu/index.tsx
+++ b/src/components/Navigation/KebabMenu/index.tsx
@@ -8,6 +8,7 @@ import { DigmaLogoFlatIcon } from "../../common/icons/16px/DigmaLogoFlatIcon";
import { FourPointedStarIcon } from "../../common/icons/16px/FourPointedStarIcon";
import { LocalEngineIcon } from "../../common/icons/LocalEngineIcon";
import { MenuList } from "../common/MenuList";
+import { MenuItem } from "../common/MenuList/types";
import { Popup } from "../common/Popup";
import { trackingEvents } from "../tracking";
import { OpenDocumentationPayload } from "../types";
@@ -48,37 +49,48 @@ export const KebabMenu = (props: KebabMenuProps) => {
props.onClose();
};
+ const handleLogoutClick = () => {
+ sendUserActionTrackingEvent(trackingEvents.LOGOUT_CLICKED);
+ window.sendMessageToDigma({
+ action: globalActions.LOGOUT
+ });
+ props.onClose();
+ };
+
+ const items: Array