mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-11 03:19:00 +08:00
feat: onboarding flow - enable users to submit request for a new data… (#4786)
* feat: onboarding flow - enable users to submit request for a new data source , environment * chore: request data source to be available for all modules * chore: remove hardcoded value
This commit is contained in:
parent
8c02f8ec31
commit
1610b95b84
28
frontend/src/api/common/logEvent.ts
Normal file
28
frontend/src/api/common/logEvent.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { EventSuccessPayloadProps } from 'types/api/events/types';
|
||||||
|
|
||||||
|
const logEvent = async (
|
||||||
|
eventName: string,
|
||||||
|
attributes: Record<string, unknown>,
|
||||||
|
): Promise<SuccessResponse<EventSuccessPayloadProps> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/event', {
|
||||||
|
eventName,
|
||||||
|
attributes,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default logEvent;
|
@ -58,10 +58,15 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
|
transition: 0.3s;
|
||||||
|
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.moduleTitleStyle {
|
.moduleTitleStyle {
|
||||||
@ -73,6 +78,7 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.moduleStyles.selected {
|
.moduleStyles.selected {
|
||||||
@ -107,8 +113,8 @@
|
|||||||
|
|
||||||
.actionButtonsContainer {
|
.actionButtonsContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@ -137,3 +143,16 @@
|
|||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.request-entity-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 0.5px solid rgba(78, 116, 248, 0.2);
|
||||||
|
background: rgba(69, 104, 220, 0.1);
|
||||||
|
padding: 12px;
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
@ -22,6 +22,7 @@ div[class*='-setup-instructions-container'] {
|
|||||||
|
|
||||||
.form-container {
|
.form-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
@ -36,3 +37,7 @@ div[class*='-setup-instructions-container'] {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.service-name-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
import './DataSource.styles.scss';
|
import './DataSource.styles.scss';
|
||||||
|
|
||||||
import { Card, Form, Input, Select, Typography } from 'antd';
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Card, Form, Input, Select, Space, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
|
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
|
||||||
import { useCases } from 'container/OnboardingContainer/OnboardingContainer';
|
import { useCases } from 'container/OnboardingContainer/OnboardingContainer';
|
||||||
@ -11,7 +13,10 @@ import {
|
|||||||
getSupportedFrameworks,
|
getSupportedFrameworks,
|
||||||
hasFrameworks,
|
hasFrameworks,
|
||||||
} from 'container/OnboardingContainer/utils/dataSourceUtils';
|
} from 'container/OnboardingContainer/utils/dataSourceUtils';
|
||||||
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import { Check } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
export interface DataSourceType {
|
export interface DataSourceType {
|
||||||
@ -23,6 +28,7 @@ export interface DataSourceType {
|
|||||||
|
|
||||||
export default function DataSource(): JSX.Element {
|
export default function DataSource(): JSX.Element {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const { t } = useTranslation(['common']);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
serviceName,
|
serviceName,
|
||||||
@ -42,6 +48,15 @@ export default function DataSource(): JSX.Element {
|
|||||||
DataSourceType[]
|
DataSourceType[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
|
const requestedDataSourceName = Form.useWatch('requestedDataSourceName', form);
|
||||||
|
|
||||||
|
const [
|
||||||
|
isSubmittingRequestForDataSource,
|
||||||
|
setIsSubmittingRequestForDataSource,
|
||||||
|
] = useState(false);
|
||||||
|
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
const [enableFrameworks, setEnableFrameworks] = useState(false);
|
const [enableFrameworks, setEnableFrameworks] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -74,12 +89,49 @@ export default function DataSource(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [selectedModule, selectedDataSource]);
|
}, [selectedModule, selectedDataSource]);
|
||||||
|
|
||||||
|
const handleRequestDataSourceSubmit = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
setIsSubmittingRequestForDataSource(true);
|
||||||
|
const response = await logEvent('Onboarding V2: Data Source Requested', {
|
||||||
|
module: selectedModule?.id,
|
||||||
|
dataSource: requestedDataSourceName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
notifications.success({
|
||||||
|
message: 'Data Source Request Submitted',
|
||||||
|
});
|
||||||
|
|
||||||
|
form.setFieldValue('requestedDataSourceName', '');
|
||||||
|
|
||||||
|
setIsSubmittingRequestForDataSource(false);
|
||||||
|
} else {
|
||||||
|
notifications.error({
|
||||||
|
message:
|
||||||
|
response.error ||
|
||||||
|
t('something_went_wrong', {
|
||||||
|
ns: 'common',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsSubmittingRequestForDataSource(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error({
|
||||||
|
message: t('something_went_wrong', {
|
||||||
|
ns: 'common',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsSubmittingRequestForDataSource(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-container">
|
<div className="module-container">
|
||||||
<Typography.Text className="data-source-title">
|
<Typography.Text className="data-source-title">
|
||||||
<span className="required-symbol">*</span> Select Data Source
|
<span className="required-symbol">*</span> Select Data Source
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
|
|
||||||
<div className="supported-languages-container">
|
<div className="supported-languages-container">
|
||||||
{supportedDataSources?.map((dataSource) => (
|
{supportedDataSources?.map((dataSource) => (
|
||||||
<Card
|
<Card
|
||||||
@ -112,7 +164,6 @@ export default function DataSource(): JSX.Element {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedModule?.id === useCases.APM.id && (
|
|
||||||
<div className="form-container">
|
<div className="form-container">
|
||||||
<div className="service-name-container">
|
<div className="service-name-container">
|
||||||
<Form
|
<Form
|
||||||
@ -127,13 +178,15 @@ export default function DataSource(): JSX.Element {
|
|||||||
updateServiceName(serviceName);
|
updateServiceName(serviceName);
|
||||||
}}
|
}}
|
||||||
name="data-source-form"
|
name="data-source-form"
|
||||||
style={{ minWidth: '300px' }}
|
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
validateTrigger="onBlur"
|
validateTrigger="onBlur"
|
||||||
>
|
>
|
||||||
|
{selectedModule?.id === useCases.APM.id && (
|
||||||
|
<>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="serviceName"
|
name="serviceName"
|
||||||
label="Service Name"
|
label="Service Name"
|
||||||
|
style={{ width: 300 }}
|
||||||
rules={[{ required: true, message: 'Please enter service name' }]}
|
rules={[{ required: true, message: 'Please enter service name' }]}
|
||||||
validateTrigger="onBlur"
|
validateTrigger="onBlur"
|
||||||
>
|
>
|
||||||
@ -150,7 +203,7 @@ export default function DataSource(): JSX.Element {
|
|||||||
<Select
|
<Select
|
||||||
value={selectedFramework}
|
value={selectedFramework}
|
||||||
getPopupContainer={popupContainer}
|
getPopupContainer={popupContainer}
|
||||||
style={{ minWidth: 120 }}
|
style={{ width: 300 }}
|
||||||
placeholder="Select Framework"
|
placeholder="Select Framework"
|
||||||
onChange={(value): void => updateSelectedFramework(value)}
|
onChange={(value): void => updateSelectedFramework(value)}
|
||||||
options={supportedframeworks}
|
options={supportedframeworks}
|
||||||
@ -158,10 +211,47 @@ export default function DataSource(): JSX.Element {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="request-entity-container">
|
||||||
|
<Typography.Text>
|
||||||
|
Cannot find what you’re looking for? Request a data source
|
||||||
|
</Typography.Text>
|
||||||
|
|
||||||
|
<div className="form-section">
|
||||||
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
|
<Form.Item
|
||||||
|
name="requestedDataSourceName"
|
||||||
|
style={{ width: 300, marginBottom: 0 }}
|
||||||
|
>
|
||||||
|
<Input placeholder="Enter data source name..." />
|
||||||
|
</Form.Item>
|
||||||
|
<Button
|
||||||
|
className="periscope-btn primary"
|
||||||
|
icon={
|
||||||
|
isSubmittingRequestForDataSource ? (
|
||||||
|
<LoadingOutlined />
|
||||||
|
) : (
|
||||||
|
<Check size={12} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type="primary"
|
||||||
|
onClick={handleRequestDataSourceSubmit}
|
||||||
|
disabled={
|
||||||
|
isSubmittingRequestForDataSource ||
|
||||||
|
!requestedDataSourceName ||
|
||||||
|
requestedDataSourceName?.trim().length === 0
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Space.Compact>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
import { Card, Typography } from 'antd';
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Card, Form, Input, Space, Typography } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
|
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
|
||||||
import { useCases } from 'container/OnboardingContainer/OnboardingContainer';
|
import { useCases } from 'container/OnboardingContainer/OnboardingContainer';
|
||||||
import { Server } from 'lucide-react';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import { Check, Server } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface SupportedEnvironmentsProps {
|
interface SupportedEnvironmentsProps {
|
||||||
name: string;
|
name: string;
|
||||||
@ -33,16 +38,78 @@ const supportedEnvironments: SupportedEnvironmentsProps[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function EnvironmentDetails(): JSX.Element {
|
export default function EnvironmentDetails(): JSX.Element {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const { t } = useTranslation(['common']);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedEnvironment,
|
selectedEnvironment,
|
||||||
updateSelectedEnvironment,
|
updateSelectedEnvironment,
|
||||||
selectedModule,
|
selectedModule,
|
||||||
|
selectedDataSource,
|
||||||
|
selectedFramework,
|
||||||
errorDetails,
|
errorDetails,
|
||||||
updateErrorDetails,
|
updateErrorDetails,
|
||||||
} = useOnboardingContext();
|
} = useOnboardingContext();
|
||||||
|
|
||||||
|
const requestedEnvironmentName = Form.useWatch(
|
||||||
|
'requestedEnvironmentName',
|
||||||
|
form,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
|
const [
|
||||||
|
isSubmittingRequestForEnvironment,
|
||||||
|
setIsSubmittingRequestForEnvironment,
|
||||||
|
] = useState(false);
|
||||||
|
|
||||||
|
const handleRequestedEnvironmentSubmit = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
setIsSubmittingRequestForEnvironment(true);
|
||||||
|
const response = await logEvent('Onboarding V2: Environment Requested', {
|
||||||
|
module: selectedModule?.id,
|
||||||
|
dataSource: selectedDataSource?.id,
|
||||||
|
framework: selectedFramework,
|
||||||
|
environment: requestedEnvironmentName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
notifications.success({
|
||||||
|
message: 'Environment Request Submitted',
|
||||||
|
});
|
||||||
|
|
||||||
|
form.setFieldValue('requestedEnvironmentName', '');
|
||||||
|
|
||||||
|
setIsSubmittingRequestForEnvironment(false);
|
||||||
|
} else {
|
||||||
|
notifications.error({
|
||||||
|
message:
|
||||||
|
response.error ||
|
||||||
|
t('something_went_wrong', {
|
||||||
|
ns: 'common',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsSubmittingRequestForEnvironment(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error({
|
||||||
|
message: t('something_went_wrong', {
|
||||||
|
ns: 'common',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsSubmittingRequestForEnvironment(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Form
|
||||||
|
initialValues={{}}
|
||||||
|
form={form}
|
||||||
|
name="environment-form"
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
<Typography.Text className="environment-title">
|
<Typography.Text className="environment-title">
|
||||||
<span className="required-symbol">*</span> Select Environment
|
<span className="required-symbol">*</span> Select Environment
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@ -80,11 +147,47 @@ export default function EnvironmentDetails(): JSX.Element {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="request-entity-container">
|
||||||
|
<Typography.Text>
|
||||||
|
Cannot find what you’re looking for? Request a data source
|
||||||
|
</Typography.Text>
|
||||||
|
|
||||||
|
<div className="form-section">
|
||||||
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
|
<Form.Item
|
||||||
|
name="requestedEnvironmentName"
|
||||||
|
style={{ width: 300, marginBottom: 0 }}
|
||||||
|
>
|
||||||
|
<Input placeholder="Enter environment name..." />
|
||||||
|
</Form.Item>
|
||||||
|
<Button
|
||||||
|
className="periscope-btn primary"
|
||||||
|
icon={
|
||||||
|
isSubmittingRequestForEnvironment ? (
|
||||||
|
<LoadingOutlined />
|
||||||
|
) : (
|
||||||
|
<Check size={12} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type="primary"
|
||||||
|
onClick={handleRequestedEnvironmentSubmit}
|
||||||
|
disabled={
|
||||||
|
isSubmittingRequestForEnvironment ||
|
||||||
|
!requestedEnvironmentName ||
|
||||||
|
requestedEnvironmentName?.trim().length === 0
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Space.Compact>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{errorDetails && (
|
{errorDetails && (
|
||||||
<div className="error-container">
|
<div className="error-container">
|
||||||
<Typography.Text type="danger"> {errorDetails} </Typography.Text>
|
<Typography.Text type="danger"> {errorDetails} </Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,11 @@
|
|||||||
background-color: #4566d6;
|
background-color: #4566d6;
|
||||||
box-shadow: 0 2px 0 rgba(62, 86, 245, 0.09);
|
box-shadow: 0 2px 0 rgba(62, 86, 245, 0.09);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.periscope-tab {
|
.periscope-tab {
|
||||||
|
9
frontend/src/types/api/events/types.ts
Normal file
9
frontend/src/types/api/events/types.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface EventSuccessPayloadProps {
|
||||||
|
status: string;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventRequestPayloadProps {
|
||||||
|
eventName: string;
|
||||||
|
attributes: Record<string, unknown>;
|
||||||
|
}
|
@ -6,6 +6,7 @@ declare global {
|
|||||||
interface Window {
|
interface Window {
|
||||||
store: Store;
|
store: Store;
|
||||||
clarity: ClarityType<string>;
|
clarity: ClarityType<string>;
|
||||||
|
Intercom: any;
|
||||||
analytics: Record<string, any>;
|
analytics: Record<string, any>;
|
||||||
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: typeof compose;
|
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: typeof compose;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user