mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-06-23 07:32:52 +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;
|
||||
cursor: pointer;
|
||||
width: 400px;
|
||||
transition: 0.3s;
|
||||
|
||||
.ant-card-body {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.moduleTitleStyle {
|
||||
@ -73,6 +78,7 @@
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.moduleStyles.selected {
|
||||
@ -107,8 +113,8 @@
|
||||
|
||||
.actionButtonsContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
}
|
||||
@ -137,3 +143,16 @@
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
gap: 16px;
|
||||
@ -36,3 +37,7 @@ div[class*='-setup-instructions-container'] {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.service-name-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -2,7 +2,9 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
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 { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
|
||||
import { useCases } from 'container/OnboardingContainer/OnboardingContainer';
|
||||
@ -11,7 +13,10 @@ import {
|
||||
getSupportedFrameworks,
|
||||
hasFrameworks,
|
||||
} from 'container/OnboardingContainer/utils/dataSourceUtils';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { Check } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
export interface DataSourceType {
|
||||
@ -23,6 +28,7 @@ export interface DataSourceType {
|
||||
|
||||
export default function DataSource(): JSX.Element {
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation(['common']);
|
||||
|
||||
const {
|
||||
serviceName,
|
||||
@ -42,6 +48,15 @@ export default function DataSource(): JSX.Element {
|
||||
DataSourceType[]
|
||||
>([]);
|
||||
|
||||
const requestedDataSourceName = Form.useWatch('requestedDataSourceName', form);
|
||||
|
||||
const [
|
||||
isSubmittingRequestForDataSource,
|
||||
setIsSubmittingRequestForDataSource,
|
||||
] = useState(false);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const [enableFrameworks, setEnableFrameworks] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@ -74,12 +89,49 @@ export default function DataSource(): JSX.Element {
|
||||
}
|
||||
}, [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 (
|
||||
<div className="module-container">
|
||||
<Typography.Text className="data-source-title">
|
||||
<span className="required-symbol">*</span> Select Data Source
|
||||
</Typography.Text>
|
||||
|
||||
<div className="supported-languages-container">
|
||||
{supportedDataSources?.map((dataSource) => (
|
||||
<Card
|
||||
@ -112,56 +164,94 @@ export default function DataSource(): JSX.Element {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedModule?.id === useCases.APM.id && (
|
||||
<div className="form-container">
|
||||
<div className="service-name-container">
|
||||
<Form
|
||||
initialValues={{
|
||||
serviceName,
|
||||
selectFramework: selectedFramework,
|
||||
}}
|
||||
form={form}
|
||||
onValuesChange={(): void => {
|
||||
const serviceName = form.getFieldValue('serviceName');
|
||||
<div className="form-container">
|
||||
<div className="service-name-container">
|
||||
<Form
|
||||
initialValues={{
|
||||
serviceName,
|
||||
selectFramework: selectedFramework,
|
||||
}}
|
||||
form={form}
|
||||
onValuesChange={(): void => {
|
||||
const serviceName = form.getFieldValue('serviceName');
|
||||
|
||||
updateServiceName(serviceName);
|
||||
}}
|
||||
name="data-source-form"
|
||||
style={{ minWidth: '300px' }}
|
||||
layout="vertical"
|
||||
validateTrigger="onBlur"
|
||||
>
|
||||
<Form.Item
|
||||
name="serviceName"
|
||||
label="Service Name"
|
||||
rules={[{ required: true, message: 'Please enter service name' }]}
|
||||
validateTrigger="onBlur"
|
||||
>
|
||||
<Input autoFocus />
|
||||
</Form.Item>
|
||||
updateServiceName(serviceName);
|
||||
}}
|
||||
name="data-source-form"
|
||||
layout="vertical"
|
||||
validateTrigger="onBlur"
|
||||
>
|
||||
{selectedModule?.id === useCases.APM.id && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="serviceName"
|
||||
label="Service Name"
|
||||
style={{ width: 300 }}
|
||||
rules={[{ required: true, message: 'Please enter service name' }]}
|
||||
validateTrigger="onBlur"
|
||||
>
|
||||
<Input autoFocus />
|
||||
</Form.Item>
|
||||
|
||||
{enableFrameworks && (
|
||||
<div className="framework-selector">
|
||||
{enableFrameworks && (
|
||||
<div className="framework-selector">
|
||||
<Form.Item
|
||||
label="Select Framework"
|
||||
name="selectFramework"
|
||||
rules={[{ required: true, message: 'Please select framework' }]}
|
||||
>
|
||||
<Select
|
||||
value={selectedFramework}
|
||||
getPopupContainer={popupContainer}
|
||||
style={{ width: 300 }}
|
||||
placeholder="Select Framework"
|
||||
onChange={(value): void => updateSelectedFramework(value)}
|
||||
options={supportedframeworks}
|
||||
/>
|
||||
</Form.Item>
|
||||
</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
|
||||
label="Select Framework"
|
||||
name="selectFramework"
|
||||
rules={[{ required: true, message: 'Please select framework' }]}
|
||||
name="requestedDataSourceName"
|
||||
style={{ width: 300, marginBottom: 0 }}
|
||||
>
|
||||
<Select
|
||||
value={selectedFramework}
|
||||
getPopupContainer={popupContainer}
|
||||
style={{ minWidth: 120 }}
|
||||
placeholder="Select Framework"
|
||||
onChange={(value): void => updateSelectedFramework(value)}
|
||||
options={supportedframeworks}
|
||||
/>
|
||||
<Input placeholder="Enter data source name..." />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
<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>
|
||||
</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 { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
|
||||
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 {
|
||||
name: string;
|
||||
@ -33,16 +38,78 @@ const supportedEnvironments: SupportedEnvironmentsProps[] = [
|
||||
];
|
||||
|
||||
export default function EnvironmentDetails(): JSX.Element {
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation(['common']);
|
||||
|
||||
const {
|
||||
selectedEnvironment,
|
||||
updateSelectedEnvironment,
|
||||
selectedModule,
|
||||
selectedDataSource,
|
||||
selectedFramework,
|
||||
errorDetails,
|
||||
updateErrorDetails,
|
||||
} = 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 (
|
||||
<>
|
||||
<Form
|
||||
initialValues={{}}
|
||||
form={form}
|
||||
name="environment-form"
|
||||
layout="vertical"
|
||||
>
|
||||
<Typography.Text className="environment-title">
|
||||
<span className="required-symbol">*</span> Select Environment
|
||||
</Typography.Text>
|
||||
@ -80,11 +147,47 @@ export default function EnvironmentDetails(): JSX.Element {
|
||||
})}
|
||||
</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 && (
|
||||
<div className="error-container">
|
||||
<Typography.Text type="danger"> {errorDetails} </Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
@ -30,6 +30,11 @@
|
||||
background-color: #4566d6;
|
||||
box-shadow: 0 2px 0 rgba(62, 86, 245, 0.09);
|
||||
}
|
||||
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
store: Store;
|
||||
clarity: ClarityType<string>;
|
||||
Intercom: any;
|
||||
analytics: Record<string, any>;
|
||||
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__: typeof compose;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user