feat: add integrations to the side-nav for cloud users (#4756)

* feat: add integrations to the side-nav for cloud users

* feat: change the route from integrations/installed to /integrations

* feat: light mode table color

* feat: increase the width of the integrations panel by 25 percent

* feat: added telemetry constants and page view

* feat: added telemetry events for integrations

* feat: address review comments
This commit is contained in:
Vikrant Gupta 2024-04-01 12:40:15 +05:30 committed by GitHub
parent 39e0ef68ca
commit 00d74bfebb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 116 additions and 46 deletions

View File

@ -48,5 +48,5 @@
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views", "TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
"DEFAULT": "Open source Observability Platform | SigNoz", "DEFAULT": "Open source Observability Platform | SigNoz",
"SHORTCUTS": "SigNoz | Shortcuts", "SHORTCUTS": "SigNoz | Shortcuts",
"INTEGRATIONS_INSTALLED": "SigNoz | Integrations" "INTEGRATIONS": "SigNoz | Integrations"
} }

View File

@ -197,11 +197,3 @@ export const InstalledIntegrations = Loadable(
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage' /* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
), ),
); );
export const IntegrationsMarketPlace = Loadable(
// eslint-disable-next-line sonarjs/no-identical-functions
() =>
import(
/* webpackChunkName: "IntegrationsMarketPlace" */ 'pages/IntegrationsModulePage'
),
);

View File

@ -15,7 +15,6 @@ import {
ErrorDetails, ErrorDetails,
IngestionSettings, IngestionSettings,
InstalledIntegrations, InstalledIntegrations,
IntegrationsMarketPlace,
LicensePage, LicensePage,
ListAllALertsPage, ListAllALertsPage,
LiveLogs, LiveLogs,
@ -338,18 +337,11 @@ const routes: AppRoutes[] = [
key: 'SHORTCUTS', key: 'SHORTCUTS',
}, },
{ {
path: ROUTES.INTEGRATIONS_INSTALLED, path: ROUTES.INTEGRATIONS,
exact: true, exact: true,
component: InstalledIntegrations, component: InstalledIntegrations,
isPrivate: true, isPrivate: true,
key: 'INTEGRATIONS_INSTALLED', key: 'INTEGRATIONS',
},
{
path: ROUTES.INTEGRATIONS_MARKETPLACE,
exact: true,
component: IntegrationsMarketPlace,
isPrivate: true,
key: 'INTEGRATIONS_MARKETPLACE',
}, },
]; ];

View File

@ -51,9 +51,7 @@ const ROUTES = {
TRACES_SAVE_VIEWS: '/traces/saved-views', TRACES_SAVE_VIEWS: '/traces/saved-views',
WORKSPACE_LOCKED: '/workspace-locked', WORKSPACE_LOCKED: '/workspace-locked',
SHORTCUTS: '/shortcuts', SHORTCUTS: '/shortcuts',
INTEGRATIONS_BASE: '/integrations', INTEGRATIONS: '/integrations',
INTEGRATIONS_INSTALLED: '/integrations/installed',
INTEGRATIONS_MARKETPLACE: '/integrations/marketplace',
} as const; } as const;
export default ROUTES; export default ROUTES;

View File

@ -271,6 +271,17 @@ function SideNav({
} }
}, [isCloudUserVal, isEnterprise, isFetching]); }, [isCloudUserVal, isEnterprise, isFetching]);
useEffect(() => {
if (!isCloudUserVal) {
let updatedMenuItems = [...menuItems];
updatedMenuItems = updatedMenuItems.filter(
(item) => item.key !== ROUTES.INTEGRATIONS,
);
setMenuItems(updatedMenuItems);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [isCurrentOrgSettings] = useComponentPermission( const [isCurrentOrgSettings] = useComponentPermission(
['current_org_settings'], ['current_org_settings'],
role, role,

View File

@ -16,6 +16,7 @@ import {
ScrollText, ScrollText,
Settings, Settings,
Slack, Slack,
Unplug,
// Unplug, // Unplug,
UserPlus, UserPlus,
} from 'lucide-react'; } from 'lucide-react';
@ -90,11 +91,11 @@ const menuItems: SidebarItem[] = [
label: 'Alerts', label: 'Alerts',
icon: <BellDot size={16} />, icon: <BellDot size={16} />,
}, },
// { {
// key: ROUTES.INTEGRATIONS_INSTALLED, key: ROUTES.INTEGRATIONS,
// label: 'Integrations', label: 'Integrations',
// icon: <Unplug size={16} />, icon: <Unplug size={16} />,
// }, },
{ {
key: ROUTES.ALL_ERROR, key: ROUTES.ALL_ERROR,
label: 'Exceptions', label: 'Exceptions',
@ -127,7 +128,6 @@ export const NEW_ROUTES_MENU_ITEM_KEY_MAP: Record<string, string> = {
[ROUTES.TRACES_EXPLORER]: ROUTES.TRACE, [ROUTES.TRACES_EXPLORER]: ROUTES.TRACE,
[ROUTES.TRACE_EXPLORER]: ROUTES.TRACE, [ROUTES.TRACE_EXPLORER]: ROUTES.TRACE,
[ROUTES.LOGS_BASE]: ROUTES.LOGS_EXPLORER, [ROUTES.LOGS_BASE]: ROUTES.LOGS_EXPLORER,
[ROUTES.INTEGRATIONS_BASE]: ROUTES.INTEGRATIONS_INSTALLED,
}; };
export default menuItems; export default menuItems;

View File

@ -199,9 +199,7 @@ export const routesToSkip = [
ROUTES.TRACES_EXPLORER, ROUTES.TRACES_EXPLORER,
ROUTES.TRACES_SAVE_VIEWS, ROUTES.TRACES_SAVE_VIEWS,
ROUTES.SHORTCUTS, ROUTES.SHORTCUTS,
ROUTES.INTEGRATIONS_BASE, ROUTES.INTEGRATIONS,
ROUTES.INTEGRATIONS_INSTALLED,
ROUTES.INTEGRATIONS_MARKETPLACE,
]; ];
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS]; export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];

View File

@ -12,12 +12,13 @@ import Overview from './IntegrationDetailContentTabs/Overview';
interface IntegrationDetailContentProps { interface IntegrationDetailContentProps {
activeDetailTab: string; activeDetailTab: string;
integrationData: IntegrationDetailedProps; integrationData: IntegrationDetailedProps;
integrationId: string;
} }
function IntegrationDetailContent( function IntegrationDetailContent(
props: IntegrationDetailContentProps, props: IntegrationDetailContentProps,
): JSX.Element { ): JSX.Element {
const { activeDetailTab, integrationData } = props; const { activeDetailTab, integrationData, integrationId } = props;
const items: TabsProps['items'] = [ const items: TabsProps['items'] = [
{ {
key: 'overview', key: 'overview',
@ -49,7 +50,12 @@ function IntegrationDetailContent(
<Typography.Text className="typography">Configure</Typography.Text> <Typography.Text className="typography">Configure</Typography.Text>
</Button> </Button>
), ),
children: <Configure configuration={integrationData.configuration} />, children: (
<Configure
configuration={integrationData.configuration}
integrationId={integrationId}
/>
),
}, },
{ {
key: 'dataCollected', key: 'dataCollected',

View File

@ -3,20 +3,36 @@ import './IntegrationDetailContentTabs.styles.scss';
import { Button, Typography } from 'antd'; import { Button, Typography } from 'antd';
import cx from 'classnames'; import cx from 'classnames';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer'; import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import { useState } from 'react'; import useAnalytics from 'hooks/analytics/useAnalytics';
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
import { useEffect, useState } from 'react';
interface ConfigurationProps { interface ConfigurationProps {
configuration: Array<{ title: string; instructions: string }>; configuration: Array<{ title: string; instructions: string }>;
integrationId: string;
} }
function Configure(props: ConfigurationProps): JSX.Element { function Configure(props: ConfigurationProps): JSX.Element {
// TODO Mardown renderer support once instructions are ready // TODO Mardown renderer support once instructions are ready
const { configuration } = props; const { configuration, integrationId } = props;
const [selectedConfigStep, setSelectedConfigStep] = useState(0); const [selectedConfigStep, setSelectedConfigStep] = useState(0);
const handleMenuClick = (index: number): void => { const handleMenuClick = (index: number): void => {
setSelectedConfigStep(index); setSelectedConfigStep(index);
}; };
const { trackEvent } = useAnalytics();
useEffect(() => {
trackEvent(
INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION,
{
integration: integrationId,
},
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return ( return (
<div className="integration-detail-configure"> <div className="integration-detail-configure">
<div className="configure-menu"> <div className="configure-menu">

View File

@ -260,7 +260,7 @@
.logs-section { .logs-section {
.table-row-dark { .table-row-dark {
background: rgba(255, 255, 255, 0.01); background: var(--bg-vanilla-300);
} }
.logs-section-table { .logs-section-table {
@ -271,7 +271,7 @@
.metrics-section { .metrics-section {
.table-row-dark { .table-row-dark {
background: rgba(255, 255, 255, 0.01); background: var(--bg-vanilla-300);
} }
.metrics-section-table { .metrics-section-table {

View File

@ -5,12 +5,14 @@ import { Button, Modal, Tooltip, Typography } from 'antd';
import installIntegration from 'api/Integrations/installIntegration'; import installIntegration from 'api/Integrations/installIntegration';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { ArrowLeftRight, Check } from 'lucide-react'; import { ArrowLeftRight, Check } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import { IntegrationConnectionStatus } from 'types/api/integrations/types'; import { IntegrationConnectionStatus } from 'types/api/integrations/types';
import { INTEGRATION_TELEMETRY_EVENTS } from '../utils';
import TestConnection, { ConnectionStates } from './TestConnection'; import TestConnection, { ConnectionStates } from './TestConnection';
interface IntegrationDetailHeaderProps { interface IntegrationDetailHeaderProps {
@ -37,6 +39,8 @@ function IntegrationDetailHeader(
} = props; } = props;
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const { trackEvent } = useAnalytics();
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const showModal = (): void => { const showModal = (): void => {
@ -120,8 +124,18 @@ function IntegrationDetailHeader(
disabled={isInstallLoading} disabled={isInstallLoading}
onClick={(): void => { onClick={(): void => {
if (connectionState === ConnectionStates.NotInstalled) { if (connectionState === ConnectionStates.NotInstalled) {
trackEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_CONNECT, {
integration: id,
});
mutate({ integration_id: id, config: {} }); mutate({ integration_id: id, config: {} });
} else { } else {
trackEvent(
INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_TEST_CONNECTION,
{
integration: id,
connectionStatus: connectionState,
},
);
showModal(); showModal();
} }
}} }}

View File

@ -123,6 +123,7 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
<IntegrationDetailContent <IntegrationDetailContent
activeDetailTab={activeDetailTab} activeDetailTab={activeDetailTab}
integrationData={integrationData} integrationData={integrationData}
integrationId={selectedIntegration}
/> />
{connectionStatus !== ConnectionStates.NotInstalled && ( {connectionStatus !== ConnectionStates.NotInstalled && (
@ -130,6 +131,7 @@ function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
integrationTitle={defaultTo(integrationData?.title, '')} integrationTitle={defaultTo(integrationData?.title, '')}
integrationId={selectedIntegration} integrationId={selectedIntegration}
refetchIntegrationDetails={refetch} refetchIntegrationDetails={refetch}
connectionStatus={connectionStatus}
/> />
)} )}
</> </>

View File

@ -3,23 +3,35 @@ import './IntegrationDetailPage.styles.scss';
import { Button, Modal, Typography } from 'antd'; import { Button, Modal, Typography } from 'antd';
import unInstallIntegration from 'api/Integrations/uninstallIntegration'; import unInstallIntegration from 'api/Integrations/uninstallIntegration';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import { INTEGRATION_TELEMETRY_EVENTS } from '../utils';
import { ConnectionStates } from './TestConnection';
interface IntergrationsUninstallBarProps { interface IntergrationsUninstallBarProps {
integrationTitle: string; integrationTitle: string;
integrationId: string; integrationId: string;
refetchIntegrationDetails: () => void; refetchIntegrationDetails: () => void;
connectionStatus: ConnectionStates;
} }
function IntergrationsUninstallBar( function IntergrationsUninstallBar(
props: IntergrationsUninstallBarProps, props: IntergrationsUninstallBarProps,
): JSX.Element { ): JSX.Element {
const { integrationTitle, integrationId, refetchIntegrationDetails } = props; const {
integrationTitle,
integrationId,
refetchIntegrationDetails,
connectionStatus,
} = props;
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const { trackEvent } = useAnalytics();
const { const {
mutate: uninstallIntegration, mutate: uninstallIntegration,
isLoading: isUninstallLoading, isLoading: isUninstallLoading,
@ -40,6 +52,13 @@ function IntergrationsUninstallBar(
}; };
const handleOk = (): void => { const handleOk = (): void => {
trackEvent(
INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_REMOVE_INTEGRATION,
{
integration: integrationId,
integrationStatus: connectionStatus,
},
);
uninstallIntegration({ uninstallIntegration({
integration_id: integrationId, integration_id: integrationId,
}); });

View File

@ -6,7 +6,7 @@
.integrations-content { .integrations-content {
width: calc(100% - 30px); width: calc(100% - 30px);
max-width: 736px; max-width: 920px;
.integrations-header { .integrations-header {
.title { .title {

View File

@ -1,18 +1,22 @@
import './Integrations.styles.scss'; import './Integrations.styles.scss';
import useAnalytics from 'hooks/analytics/useAnalytics';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom'; import { useHistory, useLocation } from 'react-router-dom';
import Header from './Header'; import Header from './Header';
import IntegrationDetailPage from './IntegrationDetailPage/IntegrationDetailPage'; import IntegrationDetailPage from './IntegrationDetailPage/IntegrationDetailPage';
import IntegrationsList from './IntegrationsList'; import IntegrationsList from './IntegrationsList';
import { INTEGRATION_TELEMETRY_EVENTS } from './utils';
function Integrations(): JSX.Element { function Integrations(): JSX.Element {
const urlQuery = useUrlQuery(); const urlQuery = useUrlQuery();
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const { trackPageView, trackEvent } = useAnalytics();
const selectedIntegration = useMemo(() => urlQuery.get('integration'), [ const selectedIntegration = useMemo(() => urlQuery.get('integration'), [
urlQuery, urlQuery,
]); ]);
@ -20,6 +24,9 @@ function Integrations(): JSX.Element {
const setSelectedIntegration = useCallback( const setSelectedIntegration = useCallback(
(integration: string | null) => { (integration: string | null) => {
if (integration) { if (integration) {
trackEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_ITEM_LIST_CLICKED, {
integration,
});
urlQuery.set('integration', integration); urlQuery.set('integration', integration);
} else { } else {
urlQuery.set('integration', ''); urlQuery.set('integration', '');
@ -27,13 +34,18 @@ function Integrations(): JSX.Element {
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl); history.push(generatedUrl);
}, },
[history, location.pathname, urlQuery], [history, location.pathname, trackEvent, urlQuery],
); );
const [activeDetailTab, setActiveDetailTab] = useState<string | null>( const [activeDetailTab, setActiveDetailTab] = useState<string | null>(
'overview', 'overview',
); );
useEffect(() => {
trackPageView(location.pathname);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [searchTerm, setSearchTerm] = useState<string>(''); const [searchTerm, setSearchTerm] = useState<string>('');
return ( return (
<div className="integrations-container"> <div className="integrations-container">

View File

@ -7,3 +7,15 @@ export const handleContactSupport = (isCloudUser: boolean): void => {
window.open('https://signoz.io/slack', '_blank'); window.open('https://signoz.io/slack', '_blank');
} }
}; };
export const INTEGRATION_TELEMETRY_EVENTS = {
INTEGRATIONS_ITEM_LIST_CLICKED: 'Integrations Page: Clicked an integration',
INTEGRATIONS_DETAIL_CONNECT:
'Integrations Detail Page: Clicked connect integration button',
INTEGRATIONS_DETAIL_TEST_CONNECTION:
'Integrations Detail Page: Clicked test Connection button for integration',
INTEGRATIONS_DETAIL_REMOVE_INTEGRATION:
'Integrations Detail Page: Clicked remove Integration button for integration',
INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION:
'Integrations Detail Page: Navigated to configure an integration',
};

View File

@ -10,6 +10,6 @@ export const installedIntegrations: TabRoutes = {
<Compass size={16} /> Integrations <Compass size={16} /> Integrations
</div> </div>
), ),
route: ROUTES.INTEGRATIONS_INSTALLED, route: ROUTES.INTEGRATIONS,
key: ROUTES.INTEGRATIONS_INSTALLED, key: ROUTES.INTEGRATIONS,
}; };

View File

@ -96,7 +96,5 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
LOGS_BASE: [], LOGS_BASE: [],
OLD_LOGS_EXPLORER: [], OLD_LOGS_EXPLORER: [],
SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'], SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'],
INTEGRATIONS_BASE: ['ADMIN', 'EDITOR', 'VIEWER'], INTEGRATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
INTEGRATIONS_INSTALLED: ['ADMIN', 'EDITOR', 'VIEWER'],
INTEGRATIONS_MARKETPLACE: ['ADMIN', 'EDITOR', 'VIEWER'],
}; };