mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-06 22:50:38 +01:00
1253 lines
36 KiB
TypeScript
1253 lines
36 KiB
TypeScript
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> </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>Can’t find what you’re 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;
|