Files
signoz/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx

1253 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { ArrowRight, Check, Goal, Search, UserPlus, X } from '@signozhq/icons';
import {
Button,
Flex,
Input,
Layout,
Modal,
Skeleton,
Space,
Steps,
} from 'antd';
import { Button as SignozButton } from '@signozhq/ui/button';
import { toast } from '@signozhq/ui/sonner';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { DOCS_BASE_URL } from 'constants/app';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useGetGlobalConfig } from 'api/generated/services/global';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { isEmpty } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { isModifierKeyPressed } from 'utils/app';
import signozBrandLogoUrl from '@/assets/Logos/signoz-brand-logo.svg';
import OnboardingIngestionDetails from '../IngestionDetails/IngestionDetails';
import InviteMembers from 'components/InviteMembers/InviteMembers';
import onboardingConfigWithLinks from '../onboarding-configs/onboarding-config-with-links';
import '../OnboardingV2.styles.scss';
const { Header } = Layout;
interface OptionGroup {
id: string;
category: string;
items: string[];
}
export interface Question {
id: string;
title: string;
description: string;
options: OptionGroup[];
uiConfig?: {
showSearch?: boolean;
filterByCategory?: boolean;
};
}
interface Option {
imgUrl?: string;
label: string;
link?: string;
entityID?: string;
internalRedirect?: boolean;
question?: {
desc: string;
options: Option[];
entityID?: string;
helpText?: string;
helpLink?: string;
helpLinkText?: string;
};
}
interface Entity {
imgUrl?: string;
label: string;
dataSource: string;
entityID: string;
module: string;
question?: {
desc: string;
options: Option[];
entityID: string;
helpText?: string;
helpLink?: string;
helpLinkText?: string;
question?: {
desc: string;
options: Option[];
entityID: string;
};
};
tags: string[];
relatedSearchKeywords?: string[];
link?: string;
internalRedirect?: boolean;
}
const setupStepItemsBase = [
{
title: 'Org Setup',
description: <Typography.Text>&nbsp;</Typography.Text>,
},
{
title: 'Add your first data source',
description: ' ',
},
{
title: 'Configure Your Product',
description: ' ',
},
];
const ONBOARDING_V3_ANALYTICS_EVENTS_MAP = {
BASE: 'Onboarding V3',
STARTED: 'Started',
DATA_SOURCE_SELECTED: 'Datasource selected',
FRAMEWORK_SELECTED: 'Framework selected',
ENVIRONMENT_SELECTED: 'Environment selected',
CONFIGURED_PRODUCT: 'Configure clicked',
BACK_BUTTON_CLICKED: 'Back clicked',
CONTINUE_BUTTON_CLICKED: 'Continue clicked',
GET_HELP_BUTTON_CLICKED: 'Get help clicked',
GET_EXPERT_ASSISTANCE_BUTTON_CLICKED: 'Get expert assistance clicked',
INVITE_TEAM_MEMBER_BUTTON_CLICKED: 'Invite team member clicked',
INVITE_TEAM_MEMBER_SEND_CLICKED: 'Send invites clicked',
INVITE_TEAM_MEMBER_SUCCESS: 'Invite team members success',
INVITE_TEAM_MEMBER_PARTIAL_SUCCESS: 'Invite team members partial success',
INVITE_TEAM_MEMBER_FAILED: 'Invite team members failed',
CLOSE_ONBOARDING_CLICKED: 'Close onboarding clicked',
DATA_SOURCE_REQUESTED: 'Datasource requested',
DATA_SOURCE_SEARCHED: 'Searched',
};
const groupDataSourcesByTags = (
dataSources: Entity[],
): { [tag: string]: Entity[] } => {
const groupedDataSources: { [tag: string]: Entity[] } = {};
dataSources.forEach((dataSource) => {
dataSource.tags.forEach((tag) => {
if (!groupedDataSources[tag]) {
groupedDataSources[tag] = [];
}
groupedDataSources[tag].push(dataSource);
});
});
return groupedDataSources;
};
const allGroupedDataSources = groupDataSourcesByTags(
onboardingConfigWithLinks as Entity[],
);
// eslint-disable-next-line sonarjs/cognitive-complexity
function OnboardingAddDataSource(): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
const [groupedDataSources, setGroupedDataSources] = useState<{
[tag: string]: Entity[];
}>(allGroupedDataSources);
const { org } = useAppContext();
const { data: globalConfig } = useGetGlobalConfig();
const [setupStepItems, setSetupStepItems] = useState(setupStepItemsBase);
const [searchQuery, setSearchQuery] = useState<string>('');
const question2Ref = useRef<HTMLDivElement | null>(null);
const question3Ref = useRef<HTMLDivElement | null>(null);
const configureProdRef = useRef<HTMLDivElement | null>(null);
const [showConfigureProduct, setShowConfigureProduct] =
useState<boolean>(false);
const [currentStep, setCurrentStep] = useState(1);
const [dataSourceRequest, setDataSourceRequest] = useState<string>('');
const [hasMoreQuestions, setHasMoreQuestions] = useState<boolean>(true);
const [showRequestDataSourceModal, setShowRequestDataSourceModal] =
useState<boolean>(false);
const [showInviteTeamMembersModal, setShowInviteTeamMembersModal] =
useState<boolean>(false);
const [docsUrl, setDocsUrl] = useState<string>(
`${DOCS_BASE_URL}/docs/instrumentation/`,
);
const [selectedDataSource, setSelectedDataSource] = useState<Entity | null>(
null,
);
const [selectedFramework, setSelectedFramework] = useState<Entity | null>(
null,
);
const [selectedEnvironment, setSelectedEnvironment] = useState<Entity | null>(
null,
);
const [selectedCategory, setSelectedCategory] = useState<string>('All');
const [dataSourceRequestSubmitted, setDataSourceRequestSubmitted] =
useState<boolean>(false);
const handleScrollToStep = (ref: React.RefObject<HTMLDivElement>): void => {
setTimeout(() => {
ref.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest',
});
}, 100);
};
const getStartedSource = urlQuery.get(QueryParams.getStartedSource);
const getStartedSourceService = urlQuery.get(
QueryParams.getStartedSourceService,
);
useEffect(() => {
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.STARTED}`,
{},
);
}, []);
const orgName = org?.[0]?.displayName;
useEffect(() => {
if (!getStartedSource || selectedDataSource) {
return;
}
const matchingDataSource = onboardingConfigWithLinks.find(
(ds) => ds.dataSource === getStartedSource,
) as Entity | undefined;
if (!matchingDataSource) {
return;
}
setSelectedDataSource(matchingDataSource);
setHasMoreQuestions(false);
updateUrl(matchingDataSource.link || '', null);
setCurrentStep(2);
setSetupStepItems([
{
...setupStepItemsBase[0],
description: orgName || '',
},
{
...setupStepItemsBase[1],
description: matchingDataSource.label,
},
...setupStepItemsBase.slice(2),
]);
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.DATA_SOURCE_SELECTED}`,
{
dataSource: matchingDataSource.label,
source: 'query_param',
},
);
// oxlint-disable-next-line react-hooks/exhaustive-deps Ignore update url since it's not stable
}, [getStartedSource, orgName, selectedDataSource]);
const updateUrl = (url: string, selectedEnvironment: string | null): void => {
if (!url || url === '') {
return;
}
// Step 1: Parse the URL
const fullUrl = url.startsWith('/') ? `${DOCS_BASE_URL}${url}` : url;
const urlObj = new URL(fullUrl);
// Step 2: Update or add the 'source' parameter
urlObj.searchParams.set('source', 'onboarding');
if (selectedEnvironment) {
urlObj.searchParams.set('environment', selectedEnvironment);
}
if (getStartedSourceService) {
urlObj.searchParams.set('service', getStartedSourceService);
}
const ingestionUrl = globalConfig?.data?.ingestion_url;
if (ingestionUrl) {
const parts = ingestionUrl.split('.');
if (parts?.length > 1 && parts[0]?.includes('ingest')) {
const region = parts[1];
urlObj.searchParams.set('region', region);
}
}
// Step 3: Return the updated URL as a string
const updatedUrl = urlObj.toString();
setDocsUrl(updatedUrl);
};
const handleSelectDataSource = (dataSource: Entity): void => {
setSelectedDataSource(dataSource);
setSelectedFramework(null);
setSelectedEnvironment(null);
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.DATA_SOURCE_SELECTED}`,
{
dataSource: dataSource.label,
},
);
if (dataSource.question) {
setHasMoreQuestions(true);
setTimeout(() => {
handleScrollToStep(question2Ref);
}, 100);
} else {
setHasMoreQuestions(false);
updateUrl(dataSource?.link || '', null);
setShowConfigureProduct(true);
}
};
const handleSelectFramework = (option: any): void => {
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.FRAMEWORK_SELECTED}`,
{
dataSource: selectedDataSource?.label,
framework: option.label,
},
);
setSelectedFramework(option);
if (option.question) {
setHasMoreQuestions(true);
updateUrl(option?.link, null);
setTimeout(() => {
handleScrollToStep(question3Ref);
}, 100);
} else {
updateUrl(option.link, null);
setHasMoreQuestions(false);
setShowConfigureProduct(true);
}
};
// Base Assumption:
// Environment is the last question in the onboarding flow and no more question will be shown regarless of the configuration
// We will have to handle this in the future
const handleSelectEnvironment = (
selectedEnvironment: any,
baseURL?: string,
): void => {
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.ENVIRONMENT_SELECTED}`,
{
dataSource: selectedDataSource?.label,
framework: selectedFramework?.label,
environment: selectedEnvironment?.label,
},
);
setSelectedEnvironment(selectedEnvironment);
setHasMoreQuestions(false);
updateUrl(baseURL || docsUrl, selectedEnvironment?.key);
setShowConfigureProduct(true);
};
const debouncedUpdate = useDebouncedFn((query) => {
setSearchQuery(query as string);
setDataSourceRequestSubmitted(false);
if (query === '') {
setGroupedDataSources(allGroupedDataSources);
return;
}
const filteredDataSources = onboardingConfigWithLinks.filter(
(dataSource) =>
dataSource.label.toLowerCase().includes(query as string) ||
dataSource.tags.some((tag) =>
tag.toLowerCase().includes(query as string),
) ||
dataSource.relatedSearchKeywords?.some((keyword) =>
keyword?.toLowerCase().includes(query as string),
),
);
setGroupedDataSources(
groupDataSourcesByTags(filteredDataSources as Entity[]),
);
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.DATA_SOURCE_SEARCHED}`,
{
searchedDataSource: query,
resultCount: filteredDataSources.length,
},
);
}, 300);
const handleSearch = useCallback(
(e: React.ChangeEvent<HTMLInputElement>): void => {
const query = e.target.value.trim().toLowerCase();
debouncedUpdate(query || '');
},
[debouncedUpdate],
);
const handleFilterByCategory = (category: string): void => {
setSelectedDataSource(null);
setSelectedFramework(null);
setSelectedEnvironment(null);
setSelectedCategory(category);
if (category === 'All') {
setGroupedDataSources(allGroupedDataSources);
return;
}
if (allGroupedDataSources[category]) {
setGroupedDataSources({
[category]: allGroupedDataSources[category],
});
} else {
// Fallback if somehow the category key doesn't strictly match or relies on partial match
// This preserves the old behavior as a fallback, though sidebar clicks should be exact matches
const filteredDataSources = onboardingConfigWithLinks.filter(
(dataSource) =>
dataSource.tags.includes(category) ||
dataSource.tags.some((tag) => tag.toLowerCase().includes(category)),
);
setGroupedDataSources(
groupDataSourcesByTags(filteredDataSources as Entity[]),
);
}
};
useEffect(() => {
setSetupStepItems([
{
...setupStepItemsBase[0],
description: org?.[0]?.displayName || '',
},
...setupStepItemsBase.slice(1),
]);
}, [org]);
const handleUpdateCurrentStep = (
step: number,
event?: React.MouseEvent,
): void => {
setCurrentStep(step);
if (step === 1) {
setSetupStepItems([
{
...setupStepItemsBase[0],
description: org?.[0]?.displayName || '',
},
{
...setupStepItemsBase[1],
description: '',
},
...setupStepItemsBase.slice(2),
]);
} else if (step === 2) {
setSetupStepItems([
{
...setupStepItemsBase[0],
description: org?.[0]?.displayName || '',
},
{
...setupStepItemsBase[1],
description: `${selectedDataSource?.label} ${
selectedFramework?.label ? `- ${selectedFramework?.label}` : ''
}`,
},
...setupStepItemsBase.slice(2),
]);
} else if (step === 3) {
let targetPath: string;
switch (selectedDataSource?.module) {
case 'apm':
targetPath = ROUTES.APPLICATION;
break;
case 'logs':
targetPath = ROUTES.LOGS;
break;
case 'metrics':
targetPath = ROUTES.METRICS_EXPLORER;
break;
case 'dashboards':
targetPath = ROUTES.ALL_DASHBOARD;
break;
case 'infra-monitoring-hosts':
targetPath = ROUTES.INFRASTRUCTURE_MONITORING_HOSTS;
break;
case 'infra-monitoring-k8s':
targetPath = ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES;
break;
case 'messaging-queues-kafka':
targetPath = ROUTES.MESSAGING_QUEUES_KAFKA;
break;
case 'messaging-queues-celery':
targetPath = ROUTES.MESSAGING_QUEUES_CELERY_TASK;
break;
case 'integrations':
targetPath = ROUTES.INTEGRATIONS;
break;
case 'home':
targetPath = ROUTES.HOME;
break;
case 'api-monitoring':
targetPath = ROUTES.API_MONITORING;
break;
default:
targetPath = ROUTES.APPLICATION;
}
safeNavigate(targetPath, { newTab: !!event && isModifierKeyPressed(event) });
}
};
const handleShowInviteTeamMembersModal = (): void => {
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.INVITE_TEAM_MEMBER_BUTTON_CLICKED}`,
{
dataSource: selectedDataSource?.label,
framework: selectedFramework?.label,
environment: selectedEnvironment?.label,
currentPage: setupStepItems[currentStep]?.title || '',
},
);
setShowInviteTeamMembersModal(true);
};
const handleSubmitDataSourceRequest = (): void => {
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.DATA_SOURCE_REQUESTED}`,
{
requestedDataSource: dataSourceRequest,
},
);
setShowRequestDataSourceModal(false);
setDataSourceRequestSubmitted(true);
};
const handleRequestDataSource = (): void => {
setShowRequestDataSourceModal(true);
};
const handleRaiseRequest = (): void => {
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.DATA_SOURCE_REQUESTED}`,
{
requestedDataSource: searchQuery,
},
);
setDataSourceRequestSubmitted(true);
};
const renderRequestDataSource = (): JSX.Element => {
const isSearchQueryEmpty = searchQuery.length === 0;
const isNoResultsFound = Object.keys(groupedDataSources).length === 0;
return (
<div className="request-data-source-container">
{!isNoResultsFound && (
<>
<Typography.Text>Cant find what youre looking for?</Typography.Text>
<svg
xmlns="http://www.w3.org/2000/svg"
width="279"
height="2"
viewBox="0 0 279 2"
fill="none"
>
<path
d="M0 1L279 1"
stroke="#7190F9"
strokeOpacity="0.2"
strokeDasharray="4 4"
/>
</svg>
{!dataSourceRequestSubmitted && (
<Button
type="default"
className="periscope-btn request-data-source-btn secondary"
icon={<Goal size={16} />}
onClick={handleRequestDataSource}
>
Request Data Source
</Button>
)}
{dataSourceRequestSubmitted && (
<Button
type="default"
className="periscope-btn request-data-source-btn success"
icon={<Check size={16} />}
>
Request raised
</Button>
)}
</>
)}
{isNoResultsFound && !isSearchQueryEmpty && (
<>
<Typography.Text>
Our team can help add{' '}
<span className="request-data-source-search-query">{searchQuery}</span>{' '}
support for you
</Typography.Text>
<svg
xmlns="http://www.w3.org/2000/svg"
width="279"
height="2"
viewBox="0 0 279 2"
fill="none"
>
<path
d="M0 1L279 1"
stroke="#7190F9"
strokeOpacity="0.2"
strokeDasharray="4 4"
/>
</svg>
{!dataSourceRequestSubmitted && (
<Button
type="default"
className="periscope-btn request-data-source-btn secondary"
icon={<Goal size={16} />}
onClick={handleRaiseRequest}
>
Raise request
</Button>
)}
{dataSourceRequestSubmitted && (
<Button
type="default"
className="periscope-btn request-data-source-btn success"
icon={<Check size={16} />}
>
Request raised
</Button>
)}
</>
)}
</div>
);
};
const progressText = `Get Started (${Math.min(
currentStep + 1,
setupStepItems.length,
)}/${setupStepItems.length})`;
return (
<div className="onboarding-v2">
<Layout>
<div className="setup-flow__header">
<div className="onboarding-header-container">
<div className="header-left-section">
<X
size={14}
className="onboarding-header-container-close-icon"
onClick={(e): void => {
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CLOSE_ONBOARDING_CLICKED}`,
{
currentPage: setupStepItems[currentStep]?.title || '',
},
);
safeNavigate(ROUTES.HOME, { newTab: isModifierKeyPressed(e) });
}}
/>
<Typography.Text>{progressText}</Typography.Text>
</div>
<div className="header-right-section">
<Button
type="default"
className="periscope-btn invite-teammate-btn outlined"
onClick={handleShowInviteTeamMembersModal}
icon={<UserPlus size={16} />}
>
Invite a teammate
</Button>
<LaunchChatSupport
attributes={{
dataSource: selectedDataSource?.dataSource,
framework: selectedFramework?.label,
environment: selectedEnvironment?.label,
currentPage: setupStepItems[currentStep]?.title || '',
}}
eventName={`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.GET_HELP_BUTTON_CLICKED}`}
message=""
buttonText="Contact Support"
className="periscope-btn get-help-btn outlined"
/>
</div>
</div>
</div>
<Header
className="setup-flow__header setup-flow__header--sticky"
style={{ position: 'sticky', top: 0, zIndex: 1, width: '100%' }}
>
<Steps
size="small"
className="setup-flow__steps"
current={currentStep}
items={setupStepItems}
/>
</Header>
<div className="onboarding-product-setup-container">
<div
className={`onboarding-product-setup-container_left-section ${
currentStep === 1 ? 'step-id-1' : 'step-id-2'
}`}
>
<div className="perlian-bg" />
{currentStep === 1 && (
<div className="onboarding-add-data-source-container step-1">
<div className="onboarding-data-sources-container">
<div className="onboarding-question-header">
<div className="question-title-container">
<Typography.Title
level={3}
className="question-title onboarding-question"
>
Select your data source
</Typography.Title>
<Typography.Text className="question-sub-title">
Select from a host of services to start sending data to SigNoz
</Typography.Text>
</div>
</div>
<div className="questionnaire-container">
<div
className={`question-1 question-block data-sources-and-filters-container ${
selectedDataSource ? 'answered' : ''
}`}
>
<div className="data-sources-container">
<div className="onboarding-data-source-search">
<Input
placeholder="Search"
maxLength={20}
onChange={handleSearch}
addonAfter={<Search size="md" />}
/>
</div>
{Object.keys(groupedDataSources).map((tag) => (
<div key={tag} className="onboarding-data-source-group">
<Typography.Title level={5} className="onboarding-title">
{tag} ({groupedDataSources[tag].length})
</Typography.Title>
<div className="onboarding-data-source-list">
{groupedDataSources[tag].map((dataSource) => (
<Button
key={dataSource.dataSource}
className={`onboarding-data-source-button ${
selectedDataSource?.label === dataSource.label
? 'selected'
: ''
}`}
type="primary"
onClick={(): void => handleSelectDataSource(dataSource)}
>
<img
src={dataSource.imgUrl}
alt={dataSource.label}
className="onboarding-data-source-button-img"
/>
{dataSource.label}
</Button>
))}
</div>
</div>
))}
{Object.keys(groupedDataSources).length === 0 && (
<div className="no-results-found-container">
<Typography.Text>No results for {searchQuery} :/</Typography.Text>
</div>
)}
{!selectedDataSource && renderRequestDataSource()}
</div>
<div className="data-source-categories-filter-container">
<div className="onboarding-data-source-category">
<Typography.Title level={5} className="onboarding-filters-title">
{' '}
Filters{' '}
</Typography.Title>
<div
key="all"
className="onboarding-data-source-category-item"
onClick={(): void => handleFilterByCategory('All')}
role="button"
tabIndex={0}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
handleFilterByCategory('All');
}
}}
>
<Typography.Title
level={5}
className={`onboarding-filters-item-title ${
selectedCategory === 'All' ? 'selected' : ''
}`}
>
All
</Typography.Title>
<div className="line-divider" />
<Typography.Text className="onboarding-filters-item-count">
{onboardingConfigWithLinks.length}
</Typography.Text>
</div>
{Object.keys(allGroupedDataSources).map((tag) => (
<div
key={tag}
className="onboarding-data-source-category-item"
onClick={(): void => handleFilterByCategory(tag)}
role="button"
tabIndex={0}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
handleFilterByCategory(tag);
}
}}
>
<Typography.Title
level={5}
className={`onboarding-filters-item-title ${
selectedCategory === tag ? 'selected' : ''
}`}
>
{tag}
</Typography.Title>
<div className="line-divider" />
<Typography.Text className="onboarding-filters-item-count">
{allGroupedDataSources[tag].length}
</Typography.Text>
</div>
))}
</div>
</div>
</div>
{selectedDataSource &&
selectedDataSource?.question &&
!isEmpty(selectedDataSource?.question) && (
<div
className={`question-2 question-block ${
selectedFramework ? 'answered' : ''
}`}
ref={question2Ref}
>
{selectedDataSource?.question?.desc && (
<>
<div className="question-title-container">
<Typography.Title
level={3}
className="question-title onboarding-text"
>
{selectedDataSource?.question?.desc}
</Typography.Title>
{selectedDataSource?.question?.helpText && (
<Typography.Text className="question-help-text">
{selectedDataSource?.question?.helpText}
{selectedDataSource?.question?.helpLink && (
<a
href={`${DOCS_BASE_URL}${selectedDataSource?.question?.helpLink}`}
target="_blank"
rel="noopener noreferrer"
className="question-help-link"
>
{selectedDataSource?.question?.helpLinkText ||
'Learn more →'}
</a>
)}
</Typography.Text>
)}
</div>
<div className="onboarding-data-source-options">
{selectedDataSource?.question?.options.map((option) => (
<Button
key={option.label}
className={`onboarding-data-source-button ${
selectedFramework?.label === option.label ? 'selected' : ''
}`}
type="primary"
onClick={(): void => {
if (
selectedDataSource?.question?.entityID === 'environment'
) {
handleSelectEnvironment(option, option.link);
} else {
handleSelectFramework(option);
}
}}
>
{option.imgUrl && (
<img
src={option.imgUrl || signozBrandLogoUrl}
alt={option.label}
className="onboarding-data-source-button-img"
/>
)}
{option.label}
</Button>
))}
</div>
</>
)}
</div>
)}
{selectedFramework &&
selectedFramework?.question &&
!isEmpty(selectedFramework?.question) && (
<div
className={`question-3 question-block ${
selectedEnvironment ? 'answered' : ''
}`}
ref={question3Ref}
>
{selectedFramework?.question?.desc && (
<>
<div className="question-title-container">
<Typography.Title
level={3}
className="question-title onboarding-text"
>
{selectedFramework?.question?.desc}
</Typography.Title>
{selectedFramework?.question?.helpText && (
<Typography.Text className="question-help-text">
{selectedFramework?.question?.helpText}
{selectedFramework?.question?.helpLink && (
<a
href={`${DOCS_BASE_URL}${selectedFramework?.question?.helpLink}`}
target="_blank"
rel="noopener noreferrer"
className="question-help-link"
>
{selectedFramework?.question?.helpLinkText ||
'Learn more →'}
</a>
)}
</Typography.Text>
)}
</div>
<div className="onboarding-data-source-options">
{selectedFramework?.question?.options.map((option) => (
<Button
key={option.label}
className={`onboarding-data-source-button ${
selectedEnvironment?.label === option.label ? 'selected' : ''
}`}
type="primary"
onClick={(): void =>
handleSelectEnvironment(option, option.link)
}
>
<img
src={option.imgUrl || signozBrandLogoUrl}
alt={option.label}
className="onboarding-data-source-button-img"
/>
{option.label}
</Button>
))}
</div>
</>
)}
</div>
)}
{!hasMoreQuestions && showConfigureProduct && (
<div className="questionaire-footer" ref={configureProdRef}>
<Button
type="primary"
disabled={!selectedDataSource}
shape="round"
onClick={(e): void => {
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONFIGURED_PRODUCT}`,
{
dataSource: selectedDataSource?.label,
framework: selectedFramework?.label,
environment: selectedEnvironment?.label,
},
);
const currentEntity =
selectedEnvironment || selectedFramework || selectedDataSource;
if (currentEntity?.internalRedirect && currentEntity?.link) {
safeNavigate(currentEntity.link, {
newTab: isModifierKeyPressed(e),
});
} else {
handleUpdateCurrentStep(2);
}
}}
>
Next: Configure your product
</Button>
</div>
)}
</div>
</div>
</div>
)}
{currentStep === 2 && (
<div className="onboarding-configure-container step-2">
<div className="configure-product-docs-section">
<div className="loading-container">
<Flex gap="middle" vertical>
<Space align="end">
<Skeleton.Avatar
style={{ width: 40, height: 40 }}
shape="square"
active
/>
</Space>
<Space align="start">
<Skeleton.Button active size="small" block />
<Skeleton.Input active size="small" />
<Skeleton.Input active size="small" />
</Space>
<Skeleton.Button active size="small" block />
<Skeleton.Button active size="small" block />
</Flex>
</div>
<iframe
title="docs"
src={docsUrl}
className="configure-product-docs-section-iframe"
referrerPolicy="unsafe-url"
loading="lazy"
allow="clipboard-write; encrypted-media; web-share"
allowFullScreen
/>
</div>
<div className="onboarding-footer">
<Button
type="default"
shape="round"
onClick={(): void => {
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BACK_BUTTON_CLICKED}`,
{
dataSource: selectedDataSource?.label,
framework: selectedFramework?.label,
environment: selectedEnvironment?.label,
currentPage: setupStepItems[currentStep]?.title || '',
},
);
handleUpdateCurrentStep(1);
}}
>
Back
</Button>
<Button
type="primary"
shape="round"
onClick={(e): void => {
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.CONTINUE_BUTTON_CLICKED}`,
{
dataSource: selectedDataSource?.label,
framework: selectedFramework?.label,
environment: selectedEnvironment?.label,
currentPage: setupStepItems[currentStep]?.title || '',
},
);
handleFilterByCategory('All');
handleUpdateCurrentStep(3, e);
}}
>
Continue
</Button>
</div>
</div>
)}
</div>
<div className="onboarding-product-setup-container_right-section">
{currentStep === 2 && <OnboardingIngestionDetails />}
</div>
</div>
<Modal
className="invite-team-member-modal"
title={<span className="title">Invite a team member</span>}
open={showInviteTeamMembersModal}
closable
onCancel={(): void => setShowInviteTeamMembersModal(false)}
width="640px"
footer={null}
destroyOnClose
>
<div className="invite-team-member-modal-content">
<InviteMembers
onSuccess={(): void => {
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.INVITE_TEAM_MEMBER_SUCCESS}`,
{},
);
setShowInviteTeamMembersModal(false);
toast.success('Invites sent successfully', { position: 'top-center' });
}}
onPartialSuccess={(): void => {
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.INVITE_TEAM_MEMBER_PARTIAL_SUCCESS}`,
{},
);
}}
onAllFailed={(): void => {
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.INVITE_TEAM_MEMBER_FAILED}`,
{},
);
}}
renderFooter={({ submit, canSubmit, isSubmitting }): JSX.Element => (
<div className="invite-team-member-modal-footer">
<SignozButton
variant="solid"
color="secondary"
onClick={(): void => setShowInviteTeamMembersModal(false)}
>
Cancel
</SignozButton>
<SignozButton
variant="solid"
onClick={(): void => {
void logEvent(
`${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.BASE}: ${ONBOARDING_V3_ANALYTICS_EVENTS_MAP?.INVITE_TEAM_MEMBER_SEND_CLICKED}`,
{},
);
void submit();
}}
disabled={!canSubmit}
loading={isSubmitting}
suffix={<ArrowRight size={14} />}
>
Send Invites
</SignozButton>
</div>
)}
/>
</div>
</Modal>
<Modal
className="request-data-source-modal"
title={<span className="title">Request Data Source</span>}
open={showRequestDataSourceModal}
closable
onCancel={(): void => setShowRequestDataSourceModal(false)}
width="640px"
footer={[
<Button
type="default"
className="periscope-btn outlined"
key="back"
onClick={(): void => setShowRequestDataSourceModal(false)}
icon={<X size={16} />}
>
Cancel
</Button>,
<Button
key="submit"
type="primary"
className="periscope-btn primary"
disabled={dataSourceRequest.length <= 0}
onClick={handleSubmitDataSourceRequest}
icon={<Check size={16} />}
>
Submit request
</Button>,
]}
destroyOnClose
>
<div className="request-data-source-modal-content">
<Typography.Text>Enter your request</Typography.Text>
<Input
placeholder="Eg: Kotlin"
className="request-data-source-modal-input"
onChange={(e): void => setDataSourceRequest(e.target.value)}
/>
</div>
</Modal>
</Layout>
</div>
);
}
export default OnboardingAddDataSource;