feat: [SIG-526]: UI Integrations V0 (#4595)

* feat: integrations v0 base setup routes and components

* chore: typecheck fix

* feat: integrations landing page changes

* feat: initial header setup

* feat: integrations list page setup

* feat: integrations details content root setup

* feat: integration detail content setup

* feat: added overview tab

* feat: added data tab

* feat: handle configuration tab

* feat: add min height for the container

* feat: generate apis and hooks for usage

* feat: added remove integration modal

* feat: added remove integration modal

* feat: added remove integration modal

* feat: added test connection bars

* chore: add bottom margins

* feat: added test connection modal

* feat: add all types of test connection

* feat: add all types of test connection

* fix: address review comments

* fix: address review comments

* feat: added get all integrations API and search bar implemnetation

* feat: navigate to overview section in case of row click and configure in btn

* feat: integrate get integration details api

* feat: handle integration details page gracefully

* feat: integrate uninstall API and the connection states

* feat: add install integration API call

* feat: added api error handling

* feat: handle error states for list and details api

* feat: handle the logs and metrics columns

* feat: add TODOs for pending tasks

* feat: comment from side nav

* feat: added support for custom tags in react markdown

* chore: revert the temporary change for merge

* feat: integrate the status api calls and polling logic

* chore: add markdown components and correct the polling issue

* chore: handle light mode

* chore: remove integrations from sideNav

* fix: address review comments

* fix: address review comments
This commit is contained in:
Vikrant Gupta 2024-03-06 22:25:02 +05:30 committed by GitHub
parent 7136ecc2fe
commit 0c4149225f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 2814 additions and 6 deletions

View File

@ -107,6 +107,7 @@
"react-virtuoso": "4.0.3",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",
"stream": "^0.0.2",
"style-loader": "1.3.0",
"styled-components": "^5.3.11",
@ -203,6 +204,7 @@
"jest-styled-components": "^7.0.8",
"lint-staged": "^12.5.0",
"msw": "1.3.2",
"npm-run-all": "latest",
"portfinder-sync": "^0.0.2",
"prettier": "2.2.1",
"raw-loader": "4.0.2",
@ -216,8 +218,7 @@
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.0.1",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.2",
"npm-run-all": "latest"
"webpack-cli": "^4.9.2"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [

View File

@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M23.06 17.526c-1.281.668-7.916 3.396-9.328 4.132-1.413.736-2.198.73-3.314.196C9.303 21.32 2.242 18.468.97 17.86c-.636-.303-.97-.56-.97-.802v-2.426s9.192-2.001 10.676-2.534c1.484-.532 1.999-.551 3.262-.089 1.263.463 8.814 1.826 10.062 2.283v2.391c0 .24-.288.503-.94.843z" fill="#912626"/><path d="M23.06 15.114c-1.281.668-7.916 3.396-9.329 4.132-1.412.737-2.197.73-3.313.196C9.302 18.91 2.242 16.056.97 15.45c-1.272-.608-1.298-1.027-.049-1.516 1.25-.49 8.271-3.244 9.755-3.776 1.484-.533 1.999-.552 3.262-.09 1.263.463 7.858 3.088 9.106 3.546 1.248.457 1.296.834.015 1.501z" fill="#C6302B"/><path d="M23.06 13.6c-1.281.668-7.916 3.396-9.328 4.133-1.413.736-2.198.73-3.314.196S2.242 14.543.97 13.935c-.636-.304-.97-.56-.97-.802v-2.426s9.192-2.001 10.676-2.534c1.484-.532 1.999-.551 3.262-.089C15.2 8.547 22.752 9.91 24 10.366v2.392c0 .24-.288.503-.94.843z" fill="#912626"/><path d="M23.06 11.19c-1.281.667-7.916 3.395-9.329 4.131-1.412.737-2.197.73-3.313.196-1.116-.533-8.176-3.386-9.448-3.993-1.272-.608-1.298-1.027-.049-1.516 1.25-.49 8.271-3.244 9.755-3.776 1.484-.533 1.999-.552 3.262-.09 1.263.463 7.858 3.088 9.106 3.545 1.248.458 1.296.835.015 1.502z" fill="#C6302B"/><path d="M23.06 9.53c-1.281.668-7.916 3.396-9.328 4.132-1.413.737-2.198.73-3.314.196-1.116-.533-8.176-3.386-9.448-3.993C.334 9.56 0 9.305 0 9.062V6.636s9.192-2 10.676-2.533c1.484-.533 1.999-.552 3.262-.09C15.2 4.477 22.752 5.84 24 6.297v2.392c0 .24-.288.502-.94.842z" fill="#912626"/><path d="M23.06 7.118c-1.281.668-7.916 3.396-9.329 4.132-1.412.737-2.197.73-3.313.196C9.303 10.913 2.242 8.061.97 7.453-.302 6.845-.328 6.427.921 5.937c1.25-.489 8.271-3.244 9.755-3.776 1.484-.532 1.999-.552 3.262-.089 1.263.463 7.858 3.088 9.106 3.545 1.248.457 1.296.834.015 1.501z" fill="#C6302B"/><path d="M14.933 4.758l-2.064.215-.462 1.111-.746-1.24L9.28 4.63l1.778-.641-.534-.985 1.665.651 1.569-.513-.424 1.017 1.6.6zm-2.649 5.393l-3.85-1.597 5.517-.847-1.667 2.444zM6.945 5.376c1.63 0 2.95.512 2.95 1.143 0 .632-1.32 1.144-2.95 1.144-1.629 0-2.95-.512-2.95-1.144 0-.63 1.321-1.143 2.95-1.143z" fill="#fff"/><path d="M17.371 5.062l3.266 1.29-3.263 1.29-.003-2.58z" fill="#621B1C"/><path d="M13.758 6.492l3.613-1.43.003 2.58-.354.139-3.262-1.29z" fill="#9A2928"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -43,5 +43,6 @@
"LOGS_SAVE_VIEWS": "SigNoz | Logs Saved Views",
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
"DEFAULT": "Open source Observability Platform | SigNoz",
"SHORTCUTS": "SigNoz | Shortcuts"
"SHORTCUTS": "SigNoz | Shortcuts",
"INTEGRATIONS_INSTALLED": "SigNoz | Integrations"
}

View File

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

View File

@ -1,6 +1,4 @@
import ROUTES from 'constants/routes';
import Shortcuts from 'pages/Shortcuts/Shortcuts';
import WorkspaceBlocked from 'pages/WorkspaceLocked';
import { RouteProps } from 'react-router-dom';
import {
@ -16,6 +14,8 @@ import {
EditRulesPage,
ErrorDetails,
IngestionSettings,
InstalledIntegrations,
IntegrationsMarketPlace,
LicensePage,
ListAllALertsPage,
LiveLogs,
@ -35,6 +35,7 @@ import {
ServiceMetricsPage,
ServicesTablePage,
SettingsPage,
ShortcutsPage,
SignupPage,
SomethingWentWrong,
StatusPage,
@ -45,6 +46,7 @@ import {
TracesSaveViews,
UnAuthorized,
UsageExplorerPage,
WorkspaceBlocked,
} from './pageComponents';
const routes: AppRoutes[] = [
@ -331,10 +333,24 @@ const routes: AppRoutes[] = [
{
path: ROUTES.SHORTCUTS,
exact: true,
component: Shortcuts,
component: ShortcutsPage,
isPrivate: true,
key: 'SHORTCUTS',
},
{
path: ROUTES.INTEGRATIONS_INSTALLED,
exact: true,
component: InstalledIntegrations,
isPrivate: true,
key: 'INTEGRATIONS_INSTALLED',
},
{
path: ROUTES.INTEGRATIONS_MARKETPLACE,
exact: true,
component: IntegrationsMarketPlace,
isPrivate: true,
key: 'INTEGRATIONS_MARKETPLACE',
},
];
export const SUPPORT_ROUTE: AppRoutes = {

View File

@ -0,0 +1,7 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import { AllIntegrationsProps } from 'types/api/integrations/types';
export const getAllIntegrations = (): Promise<
AxiosResponse<AllIntegrationsProps>
> => axios.get(`/integrations`);

View File

@ -0,0 +1,11 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import {
GetIntegrationPayloadProps,
GetIntegrationProps,
} from 'types/api/integrations/types';
export const getIntegration = (
props: GetIntegrationPayloadProps,
): Promise<AxiosResponse<GetIntegrationProps>> =>
axios.get(`/integrations/${props.integrationId}`);

View File

@ -0,0 +1,11 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import {
GetIntegrationPayloadProps,
GetIntegrationStatusProps,
} from 'types/api/integrations/types';
export const getIntegrationStatus = (
props: GetIntegrationPayloadProps,
): Promise<AxiosResponse<GetIntegrationStatusProps>> =>
axios.get(`/integrations/${props.integrationId}/connection_status`);

View File

@ -0,0 +1,31 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
InstalledIntegrationsSuccessResponse,
InstallIntegrationKeyProps,
} from 'types/api/integrations/types';
const installIntegration = async (
props: InstallIntegrationKeyProps,
): Promise<
SuccessResponse<InstalledIntegrationsSuccessResponse> | ErrorResponse
> => {
try {
const response = await axios.post('/integrations/install', {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default installIntegration;

View File

@ -0,0 +1,31 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
UninstallIntegrationProps,
UninstallIntegrationSuccessResponse,
} from 'types/api/integrations/types';
const unInstallIntegration = async (
props: UninstallIntegrationProps,
): Promise<
SuccessResponse<UninstallIntegrationSuccessResponse> | ErrorResponse
> => {
try {
const response = await axios.post('/integrations/uninstall', {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default unInstallIntegration;

View File

@ -1,10 +1,12 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import ReactMarkdown from 'react-markdown';
import { CodeProps } from 'react-markdown/lib/ast-to-react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { a11yDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import rehypeRaw from 'rehype-raw';
import CodeCopyBtn from './CodeCopyBtn/CodeCopyBtn';
@ -74,6 +76,10 @@ const interpolateMarkdown = (
return interpolatedContent;
};
function CustomTag({ color }: { color: string }): JSX.Element {
return <h1 style={{ color }}>This is custom element</h1>;
}
function MarkdownRenderer({
markdownContent,
variables,
@ -85,12 +91,14 @@ function MarkdownRenderer({
return (
<ReactMarkdown
rehypePlugins={[rehypeRaw as any]}
components={{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
a: Link,
pre: Pre,
code: Code,
customtag: CustomTag,
}}
>
{interpolatedMarkdown}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,13 @@
import { getAllIntegrations } from 'api/Integrations/getAllIntegrations';
import { AxiosError, AxiosResponse } from 'axios';
import { useQuery, UseQueryResult } from 'react-query';
import { AllIntegrationsProps } from 'types/api/integrations/types';
export const useGetAllIntegrations = (): UseQueryResult<
AxiosResponse<AllIntegrationsProps>,
AxiosError
> =>
useQuery<AxiosResponse<AllIntegrationsProps>, AxiosError>({
queryKey: ['Integrations'],
queryFn: () => getAllIntegrations(),
});

View File

@ -0,0 +1,18 @@
import { getIntegration } from 'api/Integrations/getIntegration';
import { AxiosError, AxiosResponse } from 'axios';
import { useQuery, UseQueryResult } from 'react-query';
import {
GetIntegrationPayloadProps,
GetIntegrationProps,
} from 'types/api/integrations/types';
export const useGetIntegration = ({
integrationId,
}: GetIntegrationPayloadProps): UseQueryResult<
AxiosResponse<GetIntegrationProps>,
AxiosError
> =>
useQuery<AxiosResponse<GetIntegrationProps>, AxiosError>({
queryKey: ['Integration', integrationId],
queryFn: () => getIntegration({ integrationId }),
});

View File

@ -0,0 +1,20 @@
import { getIntegrationStatus } from 'api/Integrations/getIntegrationStatus';
import { AxiosError, AxiosResponse } from 'axios';
import { useQuery, UseQueryResult } from 'react-query';
import {
GetIntegrationPayloadProps,
GetIntegrationStatusProps,
} from 'types/api/integrations/types';
export const useGetIntegrationStatus = ({
integrationId,
enabled,
}: GetIntegrationPayloadProps): UseQueryResult<
AxiosResponse<GetIntegrationStatusProps>,
AxiosError
> =>
useQuery<AxiosResponse<GetIntegrationStatusProps>, AxiosError>({
queryKey: ['Integration', integrationId, Date.now()],
queryFn: () => getIntegrationStatus({ integrationId }),
enabled,
});

View File

@ -0,0 +1,37 @@
import './Integrations.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Input, Typography } from 'antd';
import { Search } from 'lucide-react';
import { Dispatch, SetStateAction } from 'react';
interface HeaderProps {
searchTerm: string;
setSearchTerm: Dispatch<SetStateAction<string>>;
}
function Header(props: HeaderProps): JSX.Element {
const { searchTerm, setSearchTerm } = props;
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>): void => {
setSearchTerm(e.target.value);
};
return (
<div className="integrations-header">
<Typography.Title className="title">Integrations</Typography.Title>
<Typography.Text className="subtitle">
Manage Integrations for this workspace
</Typography.Text>
<Input
placeholder="Search for an integration..."
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
value={searchTerm}
onChange={handleSearch}
className="integrations-search-input"
/>
</div>
);
}
export default Header;

View File

@ -0,0 +1,79 @@
import './IntegrationDetailPage.styles.scss';
import { Button, Tabs, TabsProps, Typography } from 'antd';
import { Drum, Hammer, Table2 } from 'lucide-react';
import { IntegrationDetailedProps } from 'types/api/integrations/types';
import Configure from './IntegrationDetailContentTabs/Configure';
import DataCollected from './IntegrationDetailContentTabs/DataCollected';
import Overview from './IntegrationDetailContentTabs/Overview';
interface IntegrationDetailContentProps {
activeDetailTab: string;
integrationData: IntegrationDetailedProps;
}
function IntegrationDetailContent(
props: IntegrationDetailContentProps,
): JSX.Element {
const { activeDetailTab, integrationData } = props;
const items: TabsProps['items'] = [
{
key: 'overview',
label: (
<Button
type="text"
className="integration-tab-btns"
icon={<Drum size={14} />}
>
<Typography.Text className="typography">Overview</Typography.Text>
</Button>
),
children: (
<Overview
categories={integrationData.categories}
assets={integrationData.assets}
overviewContent={integrationData.overview}
/>
),
},
{
key: 'configuration',
label: (
<Button
type="text"
className="integration-tab-btns"
icon={<Hammer size={14} />}
>
<Typography.Text className="typography">Configure</Typography.Text>
</Button>
),
children: <Configure configuration={integrationData.configuration} />,
},
{
key: 'dataCollected',
label: (
<Button
type="text"
className="integration-tab-btns"
icon={<Table2 size={14} />}
>
<Typography.Text className="typography">Data Collected</Typography.Text>
</Button>
),
children: (
<DataCollected
logsData={integrationData.data_collected.logs}
metricsData={integrationData.data_collected.metrics}
/>
),
},
];
return (
<div className="integration-detail-container">
<Tabs defaultActiveKey={activeDetailTab} items={items} />
</div>
);
}
export default IntegrationDetailContent;

View File

@ -0,0 +1,48 @@
import './IntegrationDetailContentTabs.styles.scss';
import { Button, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import { useState } from 'react';
interface ConfigurationProps {
configuration: Array<{ title: string; instructions: string }>;
}
function Configure(props: ConfigurationProps): JSX.Element {
// TODO Mardown renderer support once instructions are ready
const { configuration } = props;
const [selectedConfigStep, setSelectedConfigStep] = useState(0);
const handleMenuClick = (index: number): void => {
setSelectedConfigStep(index);
};
return (
<div className="integration-detail-configure">
<div className="configure-menu">
{configuration.map((config, index) => (
<Tooltip title={config.title} key={config.title} placement="left">
<Button
key={config.title}
type="text"
className={cx('configure-menu-item', {
active: selectedConfigStep === index,
})}
onClick={(): void => handleMenuClick(index)}
>
<Typography.Text ellipsis>{config.title}</Typography.Text>
</Button>
</Tooltip>
))}
</div>
<div className="markdown-container">
<MarkdownRenderer
variables={{}}
markdownContent={configuration[selectedConfigStep].instructions}
/>
</div>
</div>
);
}
export default Configure;

View File

@ -0,0 +1,85 @@
import './IntegrationDetailContentTabs.styles.scss';
import { Table, Typography } from 'antd';
import { BarChart2, ScrollText } from 'lucide-react';
interface DataCollectedProps {
logsData: Array<any>;
metricsData: Array<any>;
}
function DataCollected(props: DataCollectedProps): JSX.Element {
const { logsData, metricsData } = props;
const logsColumns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'Path',
dataIndex: 'path',
key: 'path',
},
{
title: 'Type',
dataIndex: 'type',
key: 'type',
},
];
const metricsColumns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'Type',
dataIndex: 'type',
key: 'type',
},
{
title: 'Unit',
dataIndex: 'unit',
key: 'unit',
},
];
return (
<div className="integration-data-collected">
<div className="logs-section">
<div className="logs-heading">
<ScrollText size={14} />
<Typography.Text>Logs</Typography.Text>
</div>
<Table
columns={logsColumns}
rowClassName={(_, index): string =>
index % 2 === 0 ? 'table-row-dark' : ''
}
dataSource={logsData}
pagination={{ pageSize: 3 }}
className="logs-section-table"
/>
</div>
<div className="metrics-section">
<div className="metrics-heading">
<BarChart2 size={14} />
<Typography.Text>Metrics</Typography.Text>
</div>
<Table
columns={metricsColumns}
rowClassName={(_, index): string =>
index % 2 === 0 ? 'table-row-dark' : ''
}
dataSource={metricsData}
pagination={{ pageSize: 3 }}
className="metrics-section-table"
/>
</div>
</div>
);
}
export default DataCollected;

View File

@ -0,0 +1,296 @@
.integration-detail-overview {
display: flex;
.integration-detail-overview-left-container {
display: flex;
flex-direction: column;
width: 25%;
gap: 26px;
border-right: 1px solid var(--bg-slate-500);
padding: 16px 0;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 145.455% */
letter-spacing: 0.44px;
text-transform: uppercase;
.integration-detail-overview-category {
display: flex;
flex-direction: column;
.heading {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 16px; /* 145.455% */
letter-spacing: 0.44px;
text-transform: uppercase;
}
.category-tabs {
display: flex;
gap: 6px;
flex-flow: wrap;
margin-top: 12px;
.category-tab {
padding: 2px 8px;
border-radius: 4px;
border: 1px solid rgba(173, 127, 88, 0.2);
background: rgba(173, 127, 88, 0.1);
color: var(--bg-sienna-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
text-transform: none;
}
}
}
.integration-detail-overview-assets {
display: flex;
flex-direction: column;
.heading {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 16px; /* 145.455% */
letter-spacing: 0.44px;
text-transform: uppercase;
}
.assets-list {
margin-left: 5px;
margin-top: 12px;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
padding-inline-start: 16px !important;
text-transform: none;
}
}
}
.integration-detail-overview-right-container {
width: 75%;
padding: 16px 0 0 16px;
max-height: 600px;
overflow-y: auto;
}
}
.integration-data-collected {
display: flex;
flex-direction: column;
gap: 32px;
margin-top: 8px;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.logs-section {
display: flex;
flex-direction: column;
gap: 8px;
.table-row-dark {
background: rgba(255, 255, 255, 0.01);
}
.logs-section-table {
border-radius: 6px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-400);
.ant-table-thead {
text-transform: uppercase;
}
.ant-table-cell {
background: unset !important;
border-bottom: none !important;
}
.ant-table-cell::before {
background-color: unset !important;
}
}
.logs-heading {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 6px;
}
}
.metrics-section {
display: flex;
flex-direction: column;
gap: 8px;
.table-row-dark {
background: rgba(255, 255, 255, 0.01);
}
.metrics-section-table {
border-radius: 6px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-400);
.ant-table-thead {
text-transform: uppercase;
}
.ant-table-cell {
background: unset !important;
border-bottom: none !important;
}
.ant-table-cell::before {
background-color: unset !important;
}
}
.metrics-heading {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 6px;
}
}
}
.integration-detail-configure {
display: flex;
.configure-menu {
display: flex;
flex-direction: column;
width: 25%;
padding: 16px 16px 0px 0px;
border-right: 1px solid var(--bg-slate-500);
gap: 8px;
.configure-menu-item {
padding: 4px 8px;
text-align: start;
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
}
.configure-menu-item:hover {
background-color: rgba(255, 255, 255, 0.08);
}
.active {
color: rgba(255, 255, 255, 0.85);
background-color: rgba(255, 255, 255, 0.08);
}
}
.markdown-container {
width: 75%;
padding: 16px 0px 0px 16px;
max-height: 600px;
overflow-y: auto;
}
}
.lightMode {
.integration-detail-overview {
.integration-detail-overview-left-container {
border-right: 1px solid var(--bg-vanilla-400);
color: var(--bg-slate-100);
.integration-detail-overview-category {
.heading {
color: var(--bg-slate-100);
}
.category-tabs {
.category-tab {
border: 1px solid rgba(173, 127, 88, 0.2);
background: rgba(173, 127, 88, 0.1);
color: var(--bg-sienna-500);
}
}
}
.integration-detail-overview-assets {
.heading {
color: var(--bg-slate-100);
}
.assets-list {
color: var(--bg-slate-100);
}
}
}
}
.integration-data-collected {
color: var(--bg-vanilla-400);
.logs-section {
.table-row-dark {
background: rgba(255, 255, 255, 0.01);
}
.logs-section-table {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-300);
}
}
.metrics-section {
.table-row-dark {
background: rgba(255, 255, 255, 0.01);
}
.metrics-section-table {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-300);
}
}
}
.integration-detail-configure {
.configure-menu {
border-right: 1px solid var(--bg-vanilla-400);
.configure-menu-item {
color: var(--bg-vanilla-100);
}
.configure-menu-item:hover {
background-color: var(--bg-vanilla-200);
}
.active {
color: rgba(255, 255, 255, 0.85);
background-color: var(--bg-vanilla-200);
}
}
}
}

View File

@ -0,0 +1,63 @@
import './IntegrationDetailContentTabs.styles.scss';
import { Typography } from 'antd';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
interface OverviewProps {
categories: string[];
assets: {
logs: {
pipelines: Array<any>;
};
dashboards: Array<any>;
alerts: Array<any>;
};
overviewContent: string;
}
function Overview(props: OverviewProps): JSX.Element {
const { categories, assets, overviewContent } = props;
const assetsCount = [
assets.logs.pipelines.length,
assets.dashboards.length,
assets.alerts.length,
];
const assetLabelMap = ['Pipelines', 'Dashboards', 'Alerts'];
return (
<div className="integration-detail-overview">
<div className="integration-detail-overview-left-container">
<div className="integration-detail-overview-category">
<Typography.Text className="heading">Category</Typography.Text>
<div className="category-tabs">
{categories.map((category) => (
<div key={category} className="category-tab">
{category}
</div>
))}
</div>
</div>
<div className="integration-detail-overview-assets">
<Typography.Text className="heading">Assets</Typography.Text>
<ul className="assets-list">
{assetsCount.map((count, index) => {
if (count === 0) {
return undefined;
}
return (
<li key={assetLabelMap[index]}>
{count} {assetLabelMap[index]}
</li>
);
})}
</ul>
</div>
</div>
<div className="integration-detail-overview-right-container">
<MarkdownRenderer variables={{}} markdownContent={overviewContent} />
</div>
</div>
);
}
export default Overview;

View File

@ -0,0 +1,190 @@
/* eslint-disable no-nested-ternary */
import './IntegrationDetailPage.styles.scss';
import { Button, Modal, Typography } from 'antd';
import installIntegration from 'api/Integrations/installIntegration';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import dayjs from 'dayjs';
import { useNotifications } from 'hooks/useNotifications';
import { ArrowLeftRight, Check } from 'lucide-react';
import { useState } from 'react';
import { useMutation } from 'react-query';
import { IntegrationStatusProps } from 'types/api/integrations/types';
import TestConnection, { ConnectionStates } from './TestConnection';
interface IntegrationDetailHeaderProps {
id: string;
title: string;
description: string;
icon: string;
refetchIntegrationDetails: () => void;
connectionState: ConnectionStates;
connectionData: IntegrationStatusProps['connection_status'];
}
function IntegrationDetailHeader(
props: IntegrationDetailHeaderProps,
): JSX.Element {
const {
id,
title,
icon,
description,
connectionState,
connectionData,
refetchIntegrationDetails,
} = props;
const [isModalOpen, setIsModalOpen] = useState(false);
const { notifications } = useNotifications();
const showModal = (): void => {
setIsModalOpen(true);
};
const handleOk = (): void => {
setIsModalOpen(false);
};
const handleCancel = (): void => {
setIsModalOpen(false);
};
const { mutate, isLoading: isInstallLoading } = useMutation(
installIntegration,
{
onSuccess: () => {
refetchIntegrationDetails();
},
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
},
);
let latestData: {
last_received_ts_ms: number | null;
last_received_from: string | null;
} = {
last_received_ts_ms: null,
last_received_from: null,
};
if (
connectionData.logs?.last_received_ts_ms &&
connectionData.metrics?.last_received_ts_ms
) {
if (
connectionData.logs.last_received_ts_ms >
connectionData.metrics.last_received_ts_ms
) {
latestData = {
last_received_ts_ms: connectionData.logs.last_received_ts_ms,
last_received_from: connectionData.logs.last_received_from,
};
} else {
latestData = {
last_received_ts_ms: connectionData.metrics.last_received_ts_ms,
last_received_from: connectionData.metrics.last_received_from,
};
}
} else if (connectionData.logs?.last_received_ts_ms) {
latestData = {
last_received_ts_ms: connectionData.logs.last_received_ts_ms,
last_received_from: connectionData.logs.last_received_from,
};
} else if (connectionData.metrics?.last_received_ts_ms) {
latestData = {
last_received_ts_ms: connectionData.metrics.last_received_ts_ms,
last_received_from: connectionData.metrics.last_received_from,
};
}
return (
<div className="integration-connection-header">
<div className="integration-detail-header" key={id}>
<div style={{ display: 'flex', gap: '10px' }}>
<div className="image-container">
<img src={icon} alt={title} className="image" />
</div>
<div className="details">
<Typography.Text className="heading">{title}</Typography.Text>
<Typography.Text className="description">{description}</Typography.Text>
</div>
</div>
<Button
className="configure-btn"
icon={<ArrowLeftRight size={14} />}
disabled={isInstallLoading}
onClick={(): void => {
if (connectionState === ConnectionStates.NotInstalled) {
mutate({ integration_id: id, config: {} });
} else {
showModal();
}
}}
>
{connectionState === ConnectionStates.NotInstalled
? `Connect ${title}`
: `Test Connection`}
</Button>
</div>
{connectionState !== ConnectionStates.NotInstalled && (
<TestConnection connectionState={connectionState} />
)}
<Modal
className="test-connection-modal"
open={isModalOpen}
title="Test Connection"
onOk={handleOk}
onCancel={handleCancel}
okText="I understand"
okButtonProps={{ className: 'understandBtn', icon: <Check size={14} /> }}
cancelButtonProps={{ style: { display: 'none' } }}
>
<div className="connection-content">
<TestConnection connectionState={connectionState} />
{connectionState === ConnectionStates.Connected ||
connectionState === ConnectionStates.NoDataSinceLong ? (
<>
<div className="data-info">
<Typography.Text className="last-data">
Last recieved from
</Typography.Text>
<Typography.Text className="last-value">
{latestData.last_received_from}
</Typography.Text>
</div>
<div className="data-info">
<Typography.Text className="last-data">
Last recieved at
</Typography.Text>
<Typography.Text className="last-value">
{latestData.last_received_ts_ms
? dayjs(latestData.last_received_ts_ms).format('DD MMM YYYY HH:mm')
: ''}
</Typography.Text>
</div>
</>
) : connectionState === ConnectionStates.TestingConnection ? (
<div className="data-test-connection">
<div className="last-data">
After adding the {title} integration, you need to manually configure
your Redis data source to start sending data to SigNoz.
</div>
<div className="last-data">
The status bar above would turn green if we are successfully receiving
the data.
</div>
</div>
) : null}
</div>
</Modal>
</div>
);
}
export default IntegrationDetailHeader;

View File

@ -0,0 +1,665 @@
.integration-detail-content {
display: flex;
flex-direction: column;
gap: 16px;
margin: 12px 0px 20px 0px;
.error-container {
display: flex;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
align-items: center;
justify-content: center;
flex-direction: column;
.error-content {
display: flex;
flex-direction: column;
justify-content: center;
height: 300px;
gap: 15px;
.error-btns {
display: flex;
flex-direction: row;
gap: 16px;
align-items: center;
.retry-btn {
display: flex;
align-items: center;
}
.contact-support {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
.text {
color: var(--text-robin-400);
font-weight: 500;
}
}
}
.error-state-svg {
height: 40px;
width: 40px;
}
}
}
.loading-integration-details {
display: flex;
height: 400px;
justify-content: center;
align-items: center;
}
.all-integrations-btn {
width: fit-content;
display: flex;
justify-content: center;
align-items: center;
height: 24px;
padding-left: 0px;
color: #c0c1c3;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
}
.all-integrations-btn:hover {
&.ant-btn-text {
background-color: unset !important;
}
}
.integration-connection-header {
display: flex;
flex-direction: column;
padding: 16px;
gap: 12px;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
.integration-detail-header {
display: flex;
gap: 10px;
justify-content: space-between;
.image-container {
height: 40px;
width: 40px;
flex-shrink: 0;
border-radius: 2px;
border: 1px solid var(--bg-ink-50);
background: var(--bg-ink-300);
display: flex;
align-items: center;
justify-content: center;
.image {
height: 24px;
width: 24px;
}
}
.details {
display: flex;
flex-direction: column;
.heading {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.description {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
}
}
.configure-btn {
display: flex;
justify-content: center;
align-items: center;
align-self: flex-start;
gap: 2px;
flex-shrink: 0;
min-width: 143px;
height: 30px;
padding: 6px;
border-radius: 2px;
border: 1px solid var(--bg-ink-50);
background: var(--bg-robin-500);
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 10px; /* 83.333% */
letter-spacing: 0.12px;
}
}
.connection-container {
padding: 0 18px;
height: 37px;
display: flex;
align-items: center;
.connection-text {
margin: 0px;
padding: 0px 0px 0px 10px;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
}
}
.testingConnection {
border-radius: 4px;
border: 1px solid rgba(255, 205, 86, 0.1);
background: rgba(255, 205, 86, 0.1);
color: var(--bg-amber-400);
}
.connected {
border-radius: 4px;
border: 1px solid rgba(37, 225, 146, 0.1);
background: rgba(37, 225, 146, 0.1);
color: var(--bg-forest-400);
}
.connectionFailed {
border-radius: 4px;
border: 1px solid rgba(218, 85, 101, 0.2);
background: rgba(218, 85, 101, 0.06);
color: var(--bg-cherry-500);
}
.noDataSinceLong {
border-radius: 4px;
border: 1px solid rgba(78, 116, 248, 0.1);
background: rgba(78, 116, 248, 0.1);
color: var(--bg-robin-400);
}
}
.integration-detail-container {
border-radius: 6px;
padding: 10px 16px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400, #121317);
min-height: 300px;
.integration-tab-btns {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 8px 18px 8px !important;
.typography {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.integration-tab-btns:hover {
&.ant-btn-text {
background-color: unset !important;
}
}
.ant-tabs-nav-list {
gap: 24px;
}
.ant-tabs-nav {
padding: 0px !important;
}
.ant-tabs-tab {
padding: 0 !important;
}
.ant-tabs-tab + .ant-tabs-tab {
margin: 0px !important;
}
}
.uninstall-integration-bar {
display: flex;
padding: 16px;
border-radius: 4px;
border: 1px solid rgba(218, 85, 101, 0.2);
background: rgba(218, 85, 101, 0.06);
.unintall-integration-bar-text {
display: flex;
flex-direction: column;
gap: 6px;
.heading {
color: var(--bg-cherry-500);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: -0.07px;
}
.subtitle {
color: var(--bg-cherry-300);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
}
}
.uninstall-integration-btn {
border-radius: 2px;
background: var(--Accent---Secondary-Cherry, #da5565);
border-color: unset !important;
padding: 9px 13px;
display: flex;
align-items: center;
justify-content: center;
color: var(--bg-ink-300);
text-align: center;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 13.3px; /* 110.833% */
}
.uninstall-integration-btn:hover {
&.ant-btn-default {
color: var(--bg-ink-300) !important;
}
}
}
}
.remove-integration-modal {
.ant-modal-content {
width: 384px;
min-height: 200px;
flex-shrink: 0;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
background: var(--bg-ink-400);
}
.ant-modal-footer {
margin-top: 28px;
}
.ant-modal-header {
background: unset;
margin-bottom: 8px;
}
.ant-modal-title {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.remove-integration-text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.test-connection-modal {
.ant-modal-content {
width: 512px;
min-height: 170px;
flex-shrink: 0;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-header {
margin-bottom: 16px;
}
.ant-modal-body {
border-top: 1px solid var(--bg-slate-500);
padding-top: 16px;
}
.ant-modal-footer {
margin-top: 25px;
display: flex;
flex-direction: row-reverse;
.understandBtn {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
box-shadow: none;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 10px; /* 83.333% */
letter-spacing: 0.12px;
display: flex;
justify-content: center;
align-items: center;
width: 131px;
height: 30px;
padding: 6px;
flex-shrink: 0;
}
}
}
.ant-modal-header {
background: unset;
}
.connection-content {
display: flex;
flex-direction: column;
gap: 16px;
.connection-container {
padding: 0 10px;
height: 37px;
display: flex;
align-items: center;
.connection-text {
margin: 0px;
padding: 0px 0px 0px 10px;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
}
}
.data-test-connection {
display: flex;
flex-direction: column;
gap: 16px;
}
.data-info {
display: flex;
justify-content: space-between;
.last-data {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
}
.last-value {
color: var(--bg-vanilla-100);
font-family: 'Space Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
}
}
.testingConnection {
border-radius: 4px;
border: 1px solid rgba(255, 205, 86, 0.1);
background: rgba(255, 205, 86, 0.1);
color: var(--bg-amber-400);
}
.connected {
border-radius: 4px;
border: 1px solid rgba(37, 225, 146, 0.1);
background: rgba(37, 225, 146, 0.1);
color: var(--bg-forest-400);
}
.connectionFailed {
border-radius: 4px;
border: 1px solid rgba(218, 85, 101, 0.2);
background: rgba(218, 85, 101, 0.06);
color: var(--bg-cherry-500);
}
.noDataSinceLong {
border-radius: 4px;
border: 1px solid rgba(78, 116, 248, 0.1);
background: rgba(78, 116, 248, 0.1);
color: var(--bg-robin-400);
}
}
}
.lightMode {
.integration-detail-content {
.error-container {
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
.error-content {
.error-btns {
.contact-support {
.text {
color: var(--text-robin-400);
font-weight: 500;
}
}
}
}
}
.all-integrations-btn {
color: var(--bg-slate-400);
}
.all-integrations-btn:hover {
&.ant-btn-text {
background-color: unset !important;
}
}
.integration-connection-header {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-300);
.integration-detail-header {
.image-container {
border: 1px solid var(--bg-ink-50);
background: var(--bg-vanilla-200);
}
.details {
.heading {
color: var(--bg-slate-400);
}
.description {
color: var(--bg-slate-100);
}
}
}
.testingConnection {
border: 1px solid rgba(255, 205, 86, 0.1);
background: rgba(255, 205, 86, 0.1);
color: var(--bg-amber-600);
}
.connected {
border: 1px solid rgba(37, 225, 146, 0.1);
background: rgba(37, 225, 146, 0.1);
color: var(--bg-forest-600);
}
.connectionFailed {
border: 1px solid rgba(218, 85, 101, 0.2);
background: rgba(218, 85, 101, 0.06);
color: var(--bg-cherry-500);
}
.noDataSinceLong {
border: 1px solid rgba(78, 116, 248, 0.1);
background: rgba(78, 116, 248, 0.1);
color: var(--bg-robin-400);
}
}
.integration-detail-container {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-300);
.integration-tab-btns {
.typography {
color: var(--bg-slate-100);
}
}
}
.uninstall-integration-bar {
border: 1px solid rgba(218, 85, 101, 0.2);
background: rgba(218, 85, 101, 0.06);
.unintall-integration-bar-text {
.heading {
color: var(--bg-cherry-500);
}
.subtitle {
color: var(--bg-cherry-400);
}
}
.uninstall-integration-btn {
background: var(--Accent---Secondary-Cherry, #da5565);
border-color: var(--bg-cherry-300) !important;
color: var(--bg-vanilla-100);
}
.uninstall-integration-btn:hover {
&.ant-btn-default {
color: var(--bg-vanilla-300) !important;
}
}
}
}
.remove-integration-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
background: var(--bg-vanilla-300);
}
.ant-modal-title {
color: var(--bg-slate-100);
}
.remove-integration-text {
color: var(--bg-slate-400);
}
}
.test-connection-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-300);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-body {
border-top: 1px solid var(--bg-vanilla-400);
}
.ant-modal-footer {
.understandBtn {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-400);
color: var(--bg-slate-400);
}
}
}
.connection-content {
.data-info {
.last-data {
color: var(--bg-slate-400);
}
.last-value {
color: var(--bg-slate-100);
}
}
.testingConnection {
border: 1px solid rgba(255, 205, 86, 0.1);
background: rgba(255, 205, 86, 0.1);
color: var(--bg-amber-600);
}
.connected {
border: 1px solid rgba(37, 225, 146, 0.1);
background: rgba(37, 225, 146, 0.1);
color: var(--bg-forest-600);
}
.connectionFailed {
border: 1px solid rgba(218, 85, 101, 0.2);
background: rgba(218, 85, 101, 0.06);
color: var(--bg-cherry-500);
}
.noDataSinceLong {
border: 1px solid rgba(78, 116, 248, 0.1);
background: rgba(78, 116, 248, 0.1);
color: var(--bg-robin-400);
}
}
}
}

View File

@ -0,0 +1,156 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable no-nested-ternary */
import './IntegrationDetailPage.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, Typography } from 'antd';
import { useGetIntegration } from 'hooks/Integrations/useGetIntegration';
import { useGetIntegrationStatus } from 'hooks/Integrations/useGetIntegrationStatus';
import { defaultTo } from 'lodash-es';
import { ArrowLeft, MoveUpRight, RotateCw } from 'lucide-react';
import { useEffect } from 'react';
import { isCloudUser } from 'utils/app';
import { handleContactSupport } from '../utils';
import IntegrationDetailContent from './IntegrationDetailContent';
import IntegrationDetailHeader from './IntegrationDetailHeader';
import IntergrationsUninstallBar from './IntegrationsUninstallBar';
import { ConnectionStates } from './TestConnection';
import { getConnectionStatesFromConnectionStatus } from './utils';
interface IntegrationDetailPageProps {
selectedIntegration: string;
setSelectedIntegration: (id: string | null) => void;
activeDetailTab: string;
}
function IntegrationDetailPage(props: IntegrationDetailPageProps): JSX.Element {
const { selectedIntegration, setSelectedIntegration, activeDetailTab } = props;
const {
data,
isLoading,
isFetching,
refetch,
isRefetching,
isError,
} = useGetIntegration({
integrationId: selectedIntegration,
});
const {
data: integrationStatus,
refetch: refetchStatus,
isLoading: isStatusLoading,
} = useGetIntegrationStatus({
integrationId: selectedIntegration,
enabled: false,
});
const loading = isLoading || isFetching || isRefetching || isStatusLoading;
const integrationData = data?.data.data;
const connectionStatus = getConnectionStatesFromConnectionStatus(
integrationData?.installation,
defaultTo(
integrationStatus?.data.data.connection_status,
defaultTo(integrationData?.connection_status, { logs: null, metrics: null }),
),
);
useEffect(() => {
// we should once get data on load and then keep polling every 5 seconds
refetchStatus();
const timer = setInterval(() => {
refetchStatus();
}, 5000);
return (): void => {
clearInterval(timer);
};
}, [refetchStatus]);
return (
<div className="integration-detail-content">
<Button
type="text"
icon={<ArrowLeft size={14} />}
className="all-integrations-btn"
onClick={(): void => {
setSelectedIntegration(null);
}}
>
All Integrations
</Button>
{loading ? (
<div className="loading-integration-details">
Please wait.. While we load the integration details
</div>
) : isError ? (
<div className="error-container">
<div className="error-content">
<img
src="/Icons/awwSnap.svg"
alt="error-emoji"
className="error-state-svg"
/>
<Typography.Text>
Something went wrong :/ Please retry or contact support.
</Typography.Text>
<div className="error-btns">
<Button
type="primary"
className="retry-btn"
onClick={(): Promise<any> => refetch()}
icon={<RotateCw size={14} />}
>
Retry
</Button>
<div
className="contact-support"
onClick={(): void => handleContactSupport(isCloudUser())}
>
<Typography.Link className="text">Contact Support </Typography.Link>
<MoveUpRight size={14} color={Color.BG_ROBIN_400} />
</div>
</div>
</div>
</div>
) : (
integrationData && (
<>
<IntegrationDetailHeader
id={selectedIntegration}
title={defaultTo(integrationData?.title, '')}
description={defaultTo(integrationData?.description, '')}
icon={defaultTo(integrationData?.icon, '')}
connectionState={connectionStatus}
connectionData={defaultTo(
integrationStatus?.data.data.connection_status,
{ logs: null, metrics: null },
)}
refetchIntegrationDetails={refetch}
/>
<IntegrationDetailContent
activeDetailTab={activeDetailTab}
integrationData={integrationData}
/>
{connectionStatus !== ConnectionStates.NotInstalled && (
<IntergrationsUninstallBar
integrationTitle={defaultTo(integrationData?.title, '')}
integrationId={selectedIntegration}
refetchIntegrationDetails={refetch}
/>
)}
</>
)
)}
</div>
);
}
export default IntegrationDetailPage;

View File

@ -0,0 +1,89 @@
import './IntegrationDetailPage.styles.scss';
import { Button, Modal, Typography } from 'antd';
import unInstallIntegration from 'api/Integrations/uninstallIntegration';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { useNotifications } from 'hooks/useNotifications';
import { X } from 'lucide-react';
import { useState } from 'react';
import { useMutation } from 'react-query';
interface IntergrationsUninstallBarProps {
integrationTitle: string;
integrationId: string;
refetchIntegrationDetails: () => void;
}
function IntergrationsUninstallBar(
props: IntergrationsUninstallBarProps,
): JSX.Element {
const { integrationTitle, integrationId, refetchIntegrationDetails } = props;
const { notifications } = useNotifications();
const [isModalOpen, setIsModalOpen] = useState(false);
const {
mutate: uninstallIntegration,
isLoading: isUninstallLoading,
} = useMutation(unInstallIntegration, {
onSuccess: () => {
refetchIntegrationDetails();
setIsModalOpen(false);
},
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
const showModal = (): void => {
setIsModalOpen(true);
};
const handleOk = (): void => {
uninstallIntegration({
integrationId,
});
};
const handleCancel = (): void => {
setIsModalOpen(false);
};
return (
<div className="uninstall-integration-bar">
<div className="unintall-integration-bar-text">
<Typography.Text className="heading">Remove Integration</Typography.Text>
<Typography.Text className="subtitle">
Removing the {integrationTitle} integration would make your workspace stop
listening for data from {integrationTitle} instances.
</Typography.Text>
</div>
<Button
className="uninstall-integration-btn"
icon={<X size={14} />}
onClick={(): void => showModal()}
>
Remove from SigNoz
</Button>
<Modal
className="remove-integration-modal"
open={isModalOpen}
title="Remove integration"
onOk={handleOk}
onCancel={handleCancel}
okText="Remove Integration"
okButtonProps={{
danger: true,
disabled: isUninstallLoading,
}}
>
<Typography.Text className="remove-integration-text">
Removing this integration makes SigNoz stop listening for data from{' '}
{integrationTitle} instances. You would still have to manually remove the
configuration in your code to stop sending data.
</Typography.Text>
</Modal>
</div>
);
}
export default IntergrationsUninstallBar;

View File

@ -0,0 +1,35 @@
import './IntegrationDetailPage.styles.scss';
import cx from 'classnames';
export enum ConnectionStates {
Connected = 'connected',
TestingConnection = 'testingConnection',
NoDataSinceLong = 'noDataSinceLong',
NotInstalled = 'notInstalled',
}
const ConnectionStatesLabelMap = {
[ConnectionStates.Connected]: 'This integration is working properly',
[ConnectionStates.TestingConnection]: 'Listening for data...',
[ConnectionStates.NoDataSinceLong]:
'This integration has not received data in a while :/',
[ConnectionStates.NotInstalled]: '',
};
interface TestConnectionProps {
connectionState: ConnectionStates;
}
function TestConnection(props: TestConnectionProps): JSX.Element {
const { connectionState } = props;
return (
<div className={cx('connection-container', connectionState)}>
<ul className="connection-text">
<li>{ConnectionStatesLabelMap[connectionState]}</li>
</ul>
</div>
);
}
export default TestConnection;

View File

@ -0,0 +1,55 @@
import dayjs from 'dayjs';
import { isNull, isUndefined } from 'lodash-es';
import { ConnectionStates } from './TestConnection';
export function getConnectionStatesFromConnectionStatus(
installation:
| {
installed_at: string;
}
| null
| undefined,
connection_status: {
logs:
| {
last_received_ts_ms: number;
last_received_from: string;
}
| null
| undefined;
metrics:
| {
last_received_ts_ms: number;
last_received_from: string;
}
| null
| undefined;
},
): ConnectionStates {
if (isNull(installation) || isUndefined(installation)) {
return ConnectionStates.NotInstalled;
}
if (
(isNull(connection_status.logs) || isUndefined(connection_status.logs)) &&
(isNull(connection_status.metrics) || isUndefined(connection_status.metrics))
) {
const installationDate = dayjs(installation.installed_at);
if (installationDate.isBefore(dayjs().subtract(7, 'days'))) {
return ConnectionStates.NoDataSinceLong;
}
return ConnectionStates.TestingConnection;
}
const logsDate = dayjs(connection_status.logs?.last_received_ts_ms);
const metricsDate = dayjs(connection_status.metrics?.last_received_ts_ms);
if (
logsDate.isBefore(dayjs().subtract(7, 'days')) &&
metricsDate.isBefore(dayjs().subtract(7, 'days'))
) {
return ConnectionStates.NoDataSinceLong;
}
return ConnectionStates.Connected;
}

View File

@ -0,0 +1,228 @@
.integrations-container {
margin-top: 24px;
display: flex;
justify-content: center;
width: 100%;
.integrations-content {
width: calc(100% - 30px);
max-width: 736px;
.integrations-header {
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-lg);
font-style: normal;
line-height: 28px; /* 155.556% */
letter-spacing: -0.09px;
font-family: Inter;
font-weight: 500;
}
.subtitle {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
font-family: Inter;
font-weight: 400;
}
.integrations-search-input {
margin-top: 1rem;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
.ant-input {
background-color: unset;
}
}
}
.integrations-list {
margin-top: 16px;
.error-container {
display: flex;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
align-items: center;
justify-content: center;
flex-direction: column;
.error-content {
display: flex;
flex-direction: column;
justify-content: center;
height: 300px;
gap: 15px;
.error-btns {
display: flex;
flex-direction: row;
gap: 16px;
align-items: center;
.retry-btn {
display: flex;
align-items: center;
}
.contact-support {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
.text {
color: var(--text-robin-400);
font-weight: 500;
}
}
}
.error-state-svg {
height: 40px;
width: 40px;
}
}
}
.ant-list-items {
gap: 16px;
display: flex;
flex-direction: column;
}
.integrations-list-item {
display: flex;
gap: 10px;
padding: 16px;
border-radius: 6px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
cursor: pointer;
.list-item-image-container {
height: 40px;
width: 40px;
flex-shrink: 0;
border-radius: 2px;
border: 1px solid var(--bg-ink-50);
background: var(--bg-ink-300);
display: flex;
align-items: center;
justify-content: center;
.list-item-image {
height: 24px;
width: 24px;
}
}
.list-item-details {
display: flex;
flex-direction: column;
.heading {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
margin-bottom: 8px;
}
.description {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
}
}
.configure-btn {
display: flex;
justify-content: center;
align-items: center;
align-self: flex-start;
gap: 2px;
flex-shrink: 0;
width: 78px;
height: 24px;
padding: 6px 1px;
border-radius: 2px;
border: 1px solid #303540;
background: var(--bg-ink-200);
box-shadow: none;
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 10px; /* 83.333% */
letter-spacing: 0.12px;
}
}
}
}
}
.lightMode {
.integrations-container {
.integrations-content {
.integrations-header {
.title {
color: var(--bg-slate-400);
}
.subtitle {
color: var(--bg-slate-100);
}
.integrations-search-input {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-100);
}
}
.integrations-list {
.error-container {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-300);
}
.integrations-list-item {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-300);
.list-item-image-container {
border: 1px solid var(--bg-ink-50);
background: var(--bg-vanilla-200);
}
.list-item-details {
.heading {
color: var(--bg-slate-400);
}
.description {
color: var(--bg-slate-100);
}
}
.configure-btn {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-200);
color: var(--bg-slate-400);
}
}
}
}
}
}

View File

@ -0,0 +1,41 @@
import './Integrations.styles.scss';
import { useState } from 'react';
import Header from './Header';
import IntegrationDetailPage from './IntegrationDetailPage/IntegrationDetailPage';
import IntegrationsList from './IntegrationsList';
function Integrations(): JSX.Element {
const [selectedIntegration, setSelectedIntegration] = useState<string | null>(
null,
);
const [activeDetailTab, setActiveDetailTab] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState<string>('');
return (
<div className="integrations-container">
<div className="integrations-content">
{selectedIntegration && activeDetailTab ? (
<IntegrationDetailPage
selectedIntegration={selectedIntegration}
setSelectedIntegration={setSelectedIntegration}
activeDetailTab={activeDetailTab}
/>
) : (
<>
<Header setSearchTerm={setSearchTerm} searchTerm={searchTerm} />
<IntegrationsList
setSelectedIntegration={setSelectedIntegration}
searchTerm={searchTerm}
setActiveDetailTab={setActiveDetailTab}
/>
</>
)}
</div>
</div>
);
}
export default Integrations;

View File

@ -0,0 +1,120 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './Integrations.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button, List, Typography } from 'antd';
import { useGetAllIntegrations } from 'hooks/Integrations/useGetAllIntegrations';
import { MoveUpRight, RotateCw } from 'lucide-react';
import { Dispatch, SetStateAction, useMemo } from 'react';
import { isCloudUser } from 'utils/app';
import { handleContactSupport } from './utils';
interface IntegrationsListProps {
setSelectedIntegration: (id: string) => void;
setActiveDetailTab: Dispatch<SetStateAction<string | null>>;
searchTerm: string;
}
function IntegrationsList(props: IntegrationsListProps): JSX.Element {
const { setSelectedIntegration, searchTerm, setActiveDetailTab } = props;
const {
data,
isFetching,
isLoading,
isRefetching,
isError,
refetch,
} = useGetAllIntegrations();
const filteredDataList = useMemo(() => {
if (data?.data.data.integrations) {
return data?.data.data.integrations.filter((item) =>
item.title.toLowerCase().includes(searchTerm.toLowerCase()),
);
}
return [];
}, [data?.data.data.integrations, searchTerm]);
const loading = isLoading || isFetching || isRefetching;
return (
<div className="integrations-list">
{!loading && isError && (
<div className="error-container">
<div className="error-content">
<img
src="/Icons/awwSnap.svg"
alt="error-emoji"
className="error-state-svg"
/>
<Typography.Text>
Something went wrong :/ Please retry or contact support.
</Typography.Text>
<div className="error-btns">
<Button
type="primary"
className="retry-btn"
onClick={(): Promise<any> => refetch()}
icon={<RotateCw size={14} />}
>
Retry
</Button>
<div
className="contact-support"
onClick={(): void => handleContactSupport(isCloudUser())}
>
<Typography.Link className="text">Contact Support </Typography.Link>
<MoveUpRight size={14} color={Color.BG_ROBIN_400} />
</div>
</div>
</div>
</div>
)}
{!isError && (
<List
dataSource={filteredDataList}
loading={loading}
itemLayout="horizontal"
renderItem={(item): JSX.Element => (
<List.Item
key={item.id}
className="integrations-list-item"
onClick={(): void => {
setSelectedIntegration(item.id);
setActiveDetailTab('overview');
}}
>
<div style={{ display: 'flex', gap: '10px' }}>
<div className="list-item-image-container">
<img src={item.icon} alt={item.title} className="list-item-image" />
</div>
<div className="list-item-details">
<Typography.Text className="heading">{item.title}</Typography.Text>
<Typography.Text className="description">
{item.description}
</Typography.Text>
</div>
</div>
<Button
className="configure-btn"
onClick={(event): void => {
event.stopPropagation();
setSelectedIntegration(item.id);
setActiveDetailTab('configuration');
}}
>
Configure
</Button>
</List.Item>
)}
/>
)}
</div>
);
}
export default IntegrationsList;

View File

@ -0,0 +1,3 @@
import Integrations from './Integrations';
export default Integrations;

View File

@ -0,0 +1,9 @@
import history from 'lib/history';
export const handleContactSupport = (isCloudUser: boolean): void => {
if (isCloudUser) {
history.push('/support');
} else {
window.open('https://signoz.io/slack', '_blank');
}
};

View File

@ -0,0 +1,9 @@
function IntegrationsMarketPlace(): JSX.Element {
return (
<div>
<h1>IntegrationsMarketPlace</h1>
</div>
);
}
export default IntegrationsMarketPlace;

View File

@ -0,0 +1,3 @@
import IntegrationsMarketPlace from './IntegrationsMarketPlace';
export default IntegrationsMarketPlace;

View File

@ -0,0 +1,27 @@
.integrations-module-container {
.ant-tabs-nav {
padding: 0 16px;
margin-bottom: 0px;
&::before {
border-bottom: 1px solid var(--bg-slate-400) !important;
}
}
.tab-item {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
}
.lightMode {
.integrations-module-container {
.ant-tabs-nav {
&::before {
border-bottom: 1px solid var(--bg-vanilla-400) !important;
}
}
}
}

View File

@ -0,0 +1,21 @@
import './IntegrationsModulePage.styles.scss';
import RouteTab from 'components/RouteTab';
import { TabRoutes } from 'components/RouteTab/types';
import history from 'lib/history';
import { useLocation } from 'react-use';
import { installedIntegrations } from './constants';
function IntegrationsModulePage(): JSX.Element {
const { pathname } = useLocation();
const routes: TabRoutes[] = [installedIntegrations];
return (
<div className="integrations-module-container">
<RouteTab routes={routes} activeKey={pathname} history={history} />
</div>
);
}
export default IntegrationsModulePage;

View File

@ -0,0 +1,15 @@
import { TabRoutes } from 'components/RouteTab/types';
import ROUTES from 'constants/routes';
import { Compass } from 'lucide-react';
import Integrations from 'pages/Integrations';
export const installedIntegrations: TabRoutes = {
Component: Integrations,
name: (
<div className="tab-item">
<Compass size={16} /> Integrations
</div>
),
route: ROUTES.INTEGRATIONS_INSTALLED,
key: ROUTES.INTEGRATIONS_INSTALLED,
};

View File

@ -0,0 +1,3 @@
import IntegrationsModulePage from './IntegrationsModulePage';
export default IntegrationsModulePage;

View File

@ -0,0 +1,105 @@
interface IntegrationsProps {
author: {
email: string;
homepage: string;
name: string;
};
description: string;
id: string;
icon: string;
is_installed: boolean;
title: string;
}
export interface AllIntegrationsProps {
status: string;
data: {
integrations: IntegrationsProps[];
};
}
export interface IntegrationDetailedProps {
description: string;
id: string;
installation: {
installed_at: string;
} | null;
title: string;
author: {
email: string;
homepage: string;
name: string;
};
icon: string;
connection_status: {
logs: {
last_received_ts_ms: number;
last_received_from: string;
} | null;
metrics: {
last_received_ts_ms: number;
last_received_from: string;
} | null;
};
categories: string[];
assets: {
logs: {
pipelines: [];
};
dashboards: [];
alerts: [];
};
overview: string;
configuration: [
{
title: string;
instructions: string;
},
];
data_collected: {
logs: string[];
metrics: string[];
};
}
export interface GetIntegrationProps {
data: IntegrationDetailedProps;
}
export interface IntegrationStatusProps {
connection_status: {
logs: {
last_received_ts_ms: number;
last_received_from: string;
} | null;
metrics: {
last_received_ts_ms: number;
last_received_from: string;
} | null;
};
}
export interface GetIntegrationStatusProps {
data: IntegrationStatusProps;
}
export interface GetIntegrationPayloadProps {
integrationId: string;
enabled?: boolean;
}
export interface InstallIntegrationKeyProps {
integration_id: string;
config: any;
}
export interface InstalledIntegrationsSuccessResponse {
data: IntegrationsProps;
}
export interface UninstallIntegrationProps {
integrationId: string;
}
export interface UninstallIntegrationSuccessResponse {
data: any;
}

View File

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

View File

@ -4198,6 +4198,13 @@
dependencies:
"@types/unist" "^2"
"@types/hast@^3.0.0":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa"
integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==
dependencies:
"@types/unist" "*"
"@types/history@^4.7.11":
version "4.7.11"
resolved "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz"
@ -4297,6 +4304,13 @@
dependencies:
"@types/unist" "^2"
"@types/mdast@^4.0.0":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.3.tgz#1e011ff013566e919a4232d1701ad30d70cab333"
integrity sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==
dependencies:
"@types/unist" "*"
"@types/mdx@^2.0.0":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.7.tgz#c7482e995673e01b83f8e96df83b3843ea76401f"
@ -4577,6 +4591,11 @@
resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz"
integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==
"@types/unist@*", "@types/unist@^3.0.0":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20"
integrity sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==
"@types/unist@^2", "@types/unist@^2.0.0":
version "2.0.8"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.8.tgz#bb197b9639aa1a04cf464a617fe800cccd92ad5c"
@ -4799,6 +4818,11 @@
resolved "https://registry.npmjs.org/@ungap/custom-elements/-/custom-elements-1.2.0.tgz"
integrity sha512-zdSuu79stAwVUtzkQU9B5jhGh2LavtkeX4kxd2jtMJmZt7QqRJ1KJW5bukt/vUOaUs3z674GHd+nqYm0bu0Gyg==
"@ungap/structured-clone@^1.0.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
"@volar/language-core@1.11.1", "@volar/language-core@~1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-1.11.1.tgz#ecdf12ea8dc35fb8549e517991abcbf449a5ad4f"
@ -7620,6 +7644,13 @@ detect-node@^2.0.4, detect-node@^2.1.0:
resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz"
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
devlop@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018"
integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==
dependencies:
dequal "^2.0.0"
diff-sequences@^27.5.1:
version "27.5.1"
resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz"
@ -9376,6 +9407,20 @@ hast-util-from-parse5@^7.0.0:
vfile-location "^4.0.0"
web-namespaces "^2.0.0"
hast-util-from-parse5@^8.0.0:
version "8.0.1"
resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz#654a5676a41211e14ee80d1b1758c399a0327651"
integrity sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==
dependencies:
"@types/hast" "^3.0.0"
"@types/unist" "^3.0.0"
devlop "^1.0.0"
hastscript "^8.0.0"
property-information "^6.0.0"
vfile "^6.0.0"
vfile-location "^5.0.0"
web-namespaces "^2.0.0"
hast-util-has-property@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/hast-util-has-property/-/hast-util-has-property-2.0.1.tgz#8ec99c3e8f02626304ee438cdb9f0528b017e083"
@ -9408,6 +9453,13 @@ hast-util-parse-selector@^3.0.0:
dependencies:
"@types/hast" "^2.0.0"
hast-util-parse-selector@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27"
integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==
dependencies:
"@types/hast" "^3.0.0"
hast-util-raw@^7.0.0, hast-util-raw@^7.2.0:
version "7.2.3"
resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-7.2.3.tgz#dcb5b22a22073436dbdc4aa09660a644f4991d99"
@ -9425,6 +9477,25 @@ hast-util-raw@^7.0.0, hast-util-raw@^7.2.0:
web-namespaces "^2.0.0"
zwitch "^2.0.0"
hast-util-raw@^9.0.0:
version "9.0.2"
resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-9.0.2.tgz#39b4a4886bd9f0a5dd42e86d02c966c2c152884c"
integrity sha512-PldBy71wO9Uq1kyaMch9AHIghtQvIwxBUkv823pKmkTM3oV1JxtsTNYdevMxvUHqcnOAuO65JKU2+0NOxc2ksA==
dependencies:
"@types/hast" "^3.0.0"
"@types/unist" "^3.0.0"
"@ungap/structured-clone" "^1.0.0"
hast-util-from-parse5 "^8.0.0"
hast-util-to-parse5 "^8.0.0"
html-void-elements "^3.0.0"
mdast-util-to-hast "^13.0.0"
parse5 "^7.0.0"
unist-util-position "^5.0.0"
unist-util-visit "^5.0.0"
vfile "^6.0.0"
web-namespaces "^2.0.0"
zwitch "^2.0.0"
hast-util-select@^5.0.5, hast-util-select@~5.0.1:
version "5.0.5"
resolved "https://registry.yarnpkg.com/hast-util-select/-/hast-util-select-5.0.5.tgz#be9ccb71d2278681ca024727f12abd4f93b3e9bc"
@ -9496,6 +9567,19 @@ hast-util-to-parse5@^7.0.0:
web-namespaces "^2.0.0"
zwitch "^2.0.0"
hast-util-to-parse5@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz#477cd42d278d4f036bc2ea58586130f6f39ee6ed"
integrity sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==
dependencies:
"@types/hast" "^3.0.0"
comma-separated-tokens "^2.0.0"
devlop "^1.0.0"
property-information "^6.0.0"
space-separated-tokens "^2.0.0"
web-namespaces "^2.0.0"
zwitch "^2.0.0"
hast-util-to-string@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz#b008b0a4ea472bf34dd390b7eea1018726ae152a"
@ -9530,6 +9614,17 @@ hastscript@^7.0.0:
property-information "^6.0.0"
space-separated-tokens "^2.0.0"
hastscript@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-8.0.0.tgz#4ef795ec8dee867101b9f23cc830d4baf4fd781a"
integrity sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==
dependencies:
"@types/hast" "^3.0.0"
comma-separated-tokens "^2.0.0"
hast-util-parse-selector "^4.0.0"
property-information "^6.0.0"
space-separated-tokens "^2.0.0"
he@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz"
@ -9643,6 +9738,11 @@ html-void-elements@^2.0.0:
resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-2.0.1.tgz#29459b8b05c200b6c5ee98743c41b979d577549f"
integrity sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==
html-void-elements@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7"
integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==
html-webpack-plugin@5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz#c3911936f57681c1f9f4d8b68c158cd9dfe52f50"
@ -11880,6 +11980,21 @@ mdast-util-to-hast@^12.1.0:
unist-util-position "^4.0.0"
unist-util-visit "^4.0.0"
mdast-util-to-hast@^13.0.0:
version "13.1.0"
resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz#1ae54d903150a10fe04d59f03b2b95fd210b2124"
integrity sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==
dependencies:
"@types/hast" "^3.0.0"
"@types/mdast" "^4.0.0"
"@ungap/structured-clone" "^1.0.0"
devlop "^1.0.0"
micromark-util-sanitize-uri "^2.0.0"
trim-lines "^3.0.0"
unist-util-position "^5.0.0"
unist-util-visit "^5.0.0"
vfile "^6.0.0"
mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz#c13343cb3fc98621911d33b5cd42e7d0731171c6"
@ -12216,6 +12331,14 @@ micromark-util-character@^1.0.0:
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
micromark-util-character@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.0.tgz#31320ace16b4644316f6bf057531689c71e2aee1"
integrity sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==
dependencies:
micromark-util-symbol "^2.0.0"
micromark-util-types "^2.0.0"
micromark-util-chunked@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b"
@ -12262,6 +12385,11 @@ micromark-util-encode@^1.0.0:
resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5"
integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==
micromark-util-encode@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1"
integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==
micromark-util-events-to-acorn@^1.0.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-1.2.3.tgz#a4ab157f57a380e646670e49ddee97a72b58b557"
@ -12304,6 +12432,15 @@ micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0:
micromark-util-encode "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-sanitize-uri@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de"
integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==
dependencies:
micromark-util-character "^2.0.0"
micromark-util-encode "^2.0.0"
micromark-util-symbol "^2.0.0"
micromark-util-subtokenize@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1"
@ -12319,11 +12456,21 @@ micromark-util-symbol@^1.0.0:
resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142"
integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==
micromark-util-symbol@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044"
integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==
micromark-util-types@^1.0.0, micromark-util-types@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283"
integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==
micromark-util-types@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e"
integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==
micromark@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9"
@ -13265,6 +13412,13 @@ parse5@6.0.1, parse5@^6.0.0, parse5@^6.0.1:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
parse5@^7.0.0:
version "7.1.2"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32"
integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==
dependencies:
entities "^4.4.0"
parseurl@~1.3.2, parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz"
@ -15013,6 +15167,15 @@ rehype-prism-plus@~1.6.1:
unist-util-filter "^4.0.0"
unist-util-visit "^4.0.0"
rehype-raw@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-7.0.0.tgz#59d7348fd5dbef3807bbaa1d443efd2dd85ecee4"
integrity sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==
dependencies:
"@types/hast" "^3.0.0"
hast-util-raw "^9.0.0"
vfile "^6.0.0"
rehype-raw@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-6.1.1.tgz#81bbef3793bd7abacc6bf8335879d1b6c868c9d4"
@ -16844,6 +17007,13 @@ unist-util-is@^5.0.0:
dependencies:
"@types/unist" "^2.0.0"
unist-util-is@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424"
integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==
dependencies:
"@types/unist" "^3.0.0"
unist-util-position-from-estree@^1.0.0, unist-util-position-from-estree@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/unist-util-position-from-estree/-/unist-util-position-from-estree-1.1.2.tgz#8ac2480027229de76512079e377afbcabcfcce22"
@ -16858,6 +17028,13 @@ unist-util-position@^4.0.0:
dependencies:
"@types/unist" "^2.0.0"
unist-util-position@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4"
integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==
dependencies:
"@types/unist" "^3.0.0"
unist-util-remove-position@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-4.0.2.tgz#a89be6ea72e23b1a402350832b02a91f6a9afe51"
@ -16873,6 +17050,13 @@ unist-util-stringify-position@^3.0.0:
dependencies:
"@types/unist" "^2.0.0"
unist-util-stringify-position@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2"
integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==
dependencies:
"@types/unist" "^3.0.0"
unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1:
version "5.1.3"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb"
@ -16881,6 +17065,14 @@ unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1:
"@types/unist" "^2.0.0"
unist-util-is "^5.0.0"
unist-util-visit-parents@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815"
integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==
dependencies:
"@types/unist" "^3.0.0"
unist-util-is "^6.0.0"
unist-util-visit@^4.0.0, unist-util-visit@^4.1.0, unist-util-visit@^4.1.2, unist-util-visit@~4.1.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2"
@ -16890,6 +17082,15 @@ unist-util-visit@^4.0.0, unist-util-visit@^4.1.0, unist-util-visit@^4.1.2, unist
unist-util-is "^5.0.0"
unist-util-visit-parents "^5.1.1"
unist-util-visit@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6"
integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==
dependencies:
"@types/unist" "^3.0.0"
unist-util-is "^6.0.0"
unist-util-visit-parents "^6.0.0"
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
@ -17126,6 +17327,14 @@ vfile-location@^4.0.0:
"@types/unist" "^2.0.0"
vfile "^5.0.0"
vfile-location@^5.0.0:
version "5.0.2"
resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.2.tgz#220d9ca1ab6f8b2504a4db398f7ebc149f9cb464"
integrity sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==
dependencies:
"@types/unist" "^3.0.0"
vfile "^6.0.0"
vfile-message@^3.0.0:
version "3.1.4"
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea"
@ -17134,6 +17343,14 @@ vfile-message@^3.0.0:
"@types/unist" "^2.0.0"
unist-util-stringify-position "^3.0.0"
vfile-message@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181"
integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==
dependencies:
"@types/unist" "^3.0.0"
unist-util-stringify-position "^4.0.0"
vfile@^5.0.0:
version "5.3.7"
resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7"
@ -17144,6 +17361,15 @@ vfile@^5.0.0:
unist-util-stringify-position "^3.0.0"
vfile-message "^3.0.0"
vfile@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.1.tgz#1e8327f41eac91947d4fe9d237a2dd9209762536"
integrity sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==
dependencies:
"@types/unist" "^3.0.0"
unist-util-stringify-position "^4.0.0"
vfile-message "^4.0.0"
vite-plugin-dts@^3.6.4:
version "3.7.0"
resolved "https://registry.yarnpkg.com/vite-plugin-dts/-/vite-plugin-dts-3.7.0.tgz#654ee7c38c0cdd4589b9bc198a264f34172bd870"