Compare commits
5 Commits
feat/multi
...
feat/api-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aac2647b67 | ||
|
|
785a0f2a48 | ||
|
|
b46adb5927 | ||
|
|
ae87499679 | ||
|
|
f0b0889d2e |
@@ -21,6 +21,7 @@ module.exports = {
|
|||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: './tsconfig.json',
|
project: './tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
jsx: true,
|
jsx: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
"less": "^4.1.2",
|
"less": "^4.1.2",
|
||||||
"less-loader": "^10.2.0",
|
"less-loader": "^10.2.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lucide-react": "0.379.0",
|
"lucide-react": "0.427.0",
|
||||||
"mini-css-extract-plugin": "2.4.5",
|
"mini-css-extract-plugin": "2.4.5",
|
||||||
"overlayscrollbars": "^2.8.1",
|
"overlayscrollbars": "^2.8.1",
|
||||||
"overlayscrollbars-react": "^0.5.6",
|
"overlayscrollbars-react": "^0.5.6",
|
||||||
|
|||||||
@@ -269,3 +269,7 @@ export const MetricsExplorer = Loadable(
|
|||||||
() =>
|
() =>
|
||||||
import(/* webpackChunkName: "MetricsExplorer" */ 'pages/MetricsExplorer'),
|
import(/* webpackChunkName: "MetricsExplorer" */ 'pages/MetricsExplorer'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ApiMonitoring = Loadable(
|
||||||
|
() => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'),
|
||||||
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
AllAlertChannels,
|
AllAlertChannels,
|
||||||
AllErrors,
|
AllErrors,
|
||||||
APIKeys,
|
APIKeys,
|
||||||
|
ApiMonitoring,
|
||||||
BillingPage,
|
BillingPage,
|
||||||
CreateAlertChannelAlerts,
|
CreateAlertChannelAlerts,
|
||||||
CreateNewAlerts,
|
CreateNewAlerts,
|
||||||
@@ -457,6 +458,13 @@ const routes: AppRoutes[] = [
|
|||||||
key: 'METRICS_EXPLORER_VIEWS',
|
key: 'METRICS_EXPLORER_VIEWS',
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.API_MONITORING,
|
||||||
|
exact: true,
|
||||||
|
component: ApiMonitoring,
|
||||||
|
key: 'API_MONITORING',
|
||||||
|
isPrivate: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SUPPORT_ROUTE: AppRoutes = {
|
export const SUPPORT_ROUTE: AppRoutes = {
|
||||||
|
|||||||
@@ -63,30 +63,31 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="quick-filters">
|
<div className="quick-filters">
|
||||||
{source !== QuickFiltersSource.INFRA_MONITORING && (
|
{source !== QuickFiltersSource.INFRA_MONITORING &&
|
||||||
<section className="header">
|
source !== QuickFiltersSource.API_MONITORING && (
|
||||||
<section className="left-actions">
|
<section className="header">
|
||||||
<FilterOutlined />
|
<section className="left-actions">
|
||||||
<Typography.Text className="text">Filters for</Typography.Text>
|
<FilterOutlined />
|
||||||
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
<Typography.Text className="text">Filters for</Typography.Text>
|
||||||
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||||
</Tooltip>
|
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||||
</section>
|
</Tooltip>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="right-actions">
|
<section className="right-actions">
|
||||||
<Tooltip title="Reset All">
|
<Tooltip title="Reset All">
|
||||||
<SyncOutlined className="sync-icon" onClick={handleReset} />
|
<SyncOutlined className="sync-icon" onClick={handleReset} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div className="divider-filter" />
|
<div className="divider-filter" />
|
||||||
<Tooltip title="Collapse Filters">
|
<Tooltip title="Collapse Filters">
|
||||||
<VerticalAlignTopOutlined
|
<VerticalAlignTopOutlined
|
||||||
rotate={270}
|
rotate={270}
|
||||||
onClick={handleFilterVisibilityChange}
|
onClick={handleFilterVisibilityChange}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
<section className="filters">
|
<section className="filters">
|
||||||
{config.map((filter) => {
|
{config.map((filter) => {
|
||||||
|
|||||||
@@ -39,4 +39,5 @@ export enum QuickFiltersSource {
|
|||||||
LOGS_EXPLORER = 'logs-explorer',
|
LOGS_EXPLORER = 'logs-explorer',
|
||||||
INFRA_MONITORING = 'infra-monitoring',
|
INFRA_MONITORING = 'infra-monitoring',
|
||||||
TRACES_EXPLORER = 'traces-explorer',
|
TRACES_EXPLORER = 'traces-explorer',
|
||||||
|
API_MONITORING = 'api-monitoring',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ const ROUTES = {
|
|||||||
METRICS_EXPLORER: '/metrics-explorer/summary',
|
METRICS_EXPLORER: '/metrics-explorer/summary',
|
||||||
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
|
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
|
||||||
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
|
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
|
||||||
|
API_MONITORING: '/api-monitoring/explorer',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default ROUTES;
|
export default ROUTES;
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import '../Explorer.styles.scss';
|
||||||
|
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import { Spin, Table, Typography } from 'antd';
|
||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||||
|
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { HandleChangeQueryData } from 'types/common/operations.types';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
import {
|
||||||
|
columnsConfig,
|
||||||
|
formatDataForTable,
|
||||||
|
hardcodedAttributeKeys,
|
||||||
|
} from '../../utils';
|
||||||
|
|
||||||
|
function DomainList({
|
||||||
|
query,
|
||||||
|
showIP,
|
||||||
|
handleChangeQueryData,
|
||||||
|
}: {
|
||||||
|
query: IBuilderQuery;
|
||||||
|
showIP: boolean;
|
||||||
|
handleChangeQueryData: HandleChangeQueryData;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
const fetchApiOverview = async (): Promise<
|
||||||
|
SuccessResponse<any> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
const requestBody = {
|
||||||
|
start: minTime,
|
||||||
|
end: maxTime,
|
||||||
|
show_ip: showIP,
|
||||||
|
filters: {
|
||||||
|
op: 'AND',
|
||||||
|
items: query?.filters.items,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
'/third-party-apis/overview/list',
|
||||||
|
requestBody,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, isLoading, isFetching } = useQuery(
|
||||||
|
['apiOverview', minTime, maxTime, query, showIP],
|
||||||
|
fetchApiOverview,
|
||||||
|
);
|
||||||
|
|
||||||
|
const domainListData = useMemo(
|
||||||
|
() => data?.payload?.data?.result[0]?.table?.rows,
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<section className={cx('api-module-right-section')}>
|
||||||
|
<div className={cx('api-monitoring-list-header')}>
|
||||||
|
<QueryBuilderSearchV2
|
||||||
|
query={query}
|
||||||
|
onChange={(searchFilters): void =>
|
||||||
|
handleChangeQueryData('filters', searchFilters)
|
||||||
|
}
|
||||||
|
placeholder="Search filters..."
|
||||||
|
hardcodedAttributeKeys={hardcodedAttributeKeys}
|
||||||
|
/>
|
||||||
|
<DateTimeSelectionV2
|
||||||
|
showAutoRefresh={false}
|
||||||
|
showRefreshText={false}
|
||||||
|
hideShareModal
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
className={cx('api-monitoring-domain-list-table')}
|
||||||
|
dataSource={
|
||||||
|
isFetching || isLoading ? [] : formatDataForTable(domainListData)
|
||||||
|
}
|
||||||
|
columns={columnsConfig}
|
||||||
|
loading={{
|
||||||
|
spinning: isFetching || isLoading,
|
||||||
|
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||||
|
}}
|
||||||
|
locale={{
|
||||||
|
emptyText:
|
||||||
|
isFetching || isLoading ? null : (
|
||||||
|
<div className="no-filtered-domains-message-container">
|
||||||
|
<div className="no-filtered-domains-message-content">
|
||||||
|
<img
|
||||||
|
src="/Icons/emptyState.svg"
|
||||||
|
alt="thinking-emoji"
|
||||||
|
className="empty-state-svg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography.Text className="no-filtered-domains-message">
|
||||||
|
This query had no results. Edit your query and try again!
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
scroll={{ x: true }}
|
||||||
|
tableLayout="fixed"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DomainList;
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
.api-monitoring-page {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.api-quick-filter-left-section {
|
||||||
|
width: 0%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.api-quick-filters-header {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-module-right-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.api-monitoring-list-header {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-builder-search-v2 {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-monitoring-domain-list-table {
|
||||||
|
.ant-table {
|
||||||
|
.ant-table-thead>tr>th {
|
||||||
|
padding: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
color: var(--Vanilla-400, #c0c1c3);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 18px;
|
||||||
|
/* 163.636% */
|
||||||
|
letter-spacing: 0.44px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-thead>tr>th:has(.hostname-column-header) {
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:has(.hostname-column-value) {
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hostname-column-value {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 20px;
|
||||||
|
/* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cell {
|
||||||
|
.active-tag {
|
||||||
|
color: var(--bg-forest-500);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
.ant-progress-bg {
|
||||||
|
height: 8px !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody>tr:hover>td {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:first-child {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:nth-child(2) {
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:nth-child(n + 3) {
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody>tr>td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-thead>tr>th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-empty-normal {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pagination {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: calc(100% - 64px);
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
padding: 16px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
// this is to offset intercom icon till we improve the design
|
||||||
|
padding-right: 72px;
|
||||||
|
|
||||||
|
.ant-pagination-item {
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&-active {
|
||||||
|
background: var(--bg-robin-500);
|
||||||
|
border-color: var(--bg-robin-500);
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--bg-ink-500) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-expanded-row {
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
background: var(--bg-ink-500) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table .ant-table-thead>tr>th {
|
||||||
|
padding: 4px 16px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-table-container {
|
||||||
|
border: 1px solid var(--bg-ink-400);
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-left: 48px;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.1rem;
|
||||||
|
height: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--bg-ink-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-expanded-row {
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-table-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
padding-left: 42px;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.periscope-btn {
|
||||||
|
font-size: 10px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-all-text {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.filter-visible {
|
||||||
|
.api-quick-filter-left-section {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-module-right-section {
|
||||||
|
width: calc(100% - 260px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-filtered-domains-message-container {
|
||||||
|
height: 30vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.no-filtered-domains-message-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-filtered-domains-message {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
91
frontend/src/container/ApiMonitoring/Explorer/Explorer.tsx
Normal file
91
frontend/src/container/ApiMonitoring/Explorer/Explorer.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import './Explorer.styles.scss';
|
||||||
|
|
||||||
|
import { FilterOutlined } from '@ant-design/icons';
|
||||||
|
import * as Sentry from '@sentry/react';
|
||||||
|
import { Switch, Typography } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||||
|
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { ApiMonitoringQuickFiltersConfig } from '../utils';
|
||||||
|
import DomainList from './Domains/DomainList';
|
||||||
|
|
||||||
|
function Explorer(): JSX.Element {
|
||||||
|
const [showIP, setShowIP] = useState<boolean>(true);
|
||||||
|
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const { handleChangeQueryData } = useQueryOperations({
|
||||||
|
index: 0,
|
||||||
|
query: currentQuery.builder.queryData[0],
|
||||||
|
entityVersion: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedCurrentQuery = useMemo(
|
||||||
|
() => ({
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...currentQuery.builder.queryData[0],
|
||||||
|
dataSource: DataSource.TRACES,
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
aggregateAttribute: {
|
||||||
|
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[currentQuery],
|
||||||
|
);
|
||||||
|
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||||
|
<div className={cx('api-monitoring-page', 'filter-visible')}>
|
||||||
|
<section className={cx('api-quick-filter-left-section')}>
|
||||||
|
<div className={cx('api-quick-filters-header')}>
|
||||||
|
<FilterOutlined />
|
||||||
|
<Typography.Text>Filters</Typography.Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cx('api-quick-filters-header')}>
|
||||||
|
<Typography.Text>Show IP addresses</Typography.Text>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
style={{ marginLeft: 'auto' }}
|
||||||
|
checked={showIP}
|
||||||
|
onClick={(): void => {
|
||||||
|
setShowIP((showIP) => !showIP);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<QuickFilters
|
||||||
|
source={QuickFiltersSource.API_MONITORING}
|
||||||
|
config={ApiMonitoringQuickFiltersConfig}
|
||||||
|
handleFilterVisibilityChange={(): void => {}}
|
||||||
|
onFilterChange={(query: Query): void =>
|
||||||
|
handleChangeQueryData('filters', query.builder.queryData[0].filters)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<DomainList
|
||||||
|
query={query}
|
||||||
|
showIP={showIP}
|
||||||
|
handleChangeQueryData={handleChangeQueryData}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Sentry.ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Explorer;
|
||||||
180
frontend/src/container/ApiMonitoring/utils.tsx
Normal file
180
frontend/src/container/ApiMonitoring/utils.tsx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
import { Tooltip } from 'antd';
|
||||||
|
import { ColumnType } from 'antd/es/table';
|
||||||
|
import {
|
||||||
|
FiltersType,
|
||||||
|
IQuickFiltersConfig,
|
||||||
|
} from 'components/QuickFilters/types';
|
||||||
|
import {
|
||||||
|
BaseAutocompleteData,
|
||||||
|
DataTypes,
|
||||||
|
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
export const ApiMonitoringQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Environment',
|
||||||
|
|
||||||
|
attributeKey: {
|
||||||
|
key: 'deployment.environment',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
dataSource: DataSource.TRACES,
|
||||||
|
defaultOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Service Name',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'service.name', // discuss about this with sagar once
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
dataSource: DataSource.TRACES,
|
||||||
|
defaultOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'RPC Method',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'rpc.method',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
dataSource: DataSource.TRACES,
|
||||||
|
defaultOpen: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const columnProgressBarClassName = 'column-progress-bar';
|
||||||
|
|
||||||
|
export const columnsConfig: ColumnType<APIDomainsRowData>[] = [
|
||||||
|
{
|
||||||
|
title: <div className="column-header domain-name-header">Domain</div>,
|
||||||
|
dataIndex: 'domainName',
|
||||||
|
key: 'domainName',
|
||||||
|
width: 180,
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: false,
|
||||||
|
className: 'column column-domain-name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <div className="column-header med-col">Endpoints in use</div>,
|
||||||
|
dataIndex: 'endpointCount',
|
||||||
|
key: 'endpointCount',
|
||||||
|
width: 180,
|
||||||
|
ellipsis: true,
|
||||||
|
sorter: false,
|
||||||
|
align: 'left',
|
||||||
|
className: `column ${columnProgressBarClassName}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <div className="column-header med-col">Last used</div>,
|
||||||
|
dataIndex: 'lastUsed',
|
||||||
|
key: 'lastUsed',
|
||||||
|
width: 120,
|
||||||
|
sorter: false,
|
||||||
|
align: 'left',
|
||||||
|
className: `column ${columnProgressBarClassName}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <div className="column-header">Rate (/s)</div>,
|
||||||
|
dataIndex: 'rate',
|
||||||
|
key: 'rate',
|
||||||
|
width: 80,
|
||||||
|
sorter: false,
|
||||||
|
align: 'left',
|
||||||
|
className: `column ${columnProgressBarClassName}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <div className="column-heade med-col">Error rate (%)</div>,
|
||||||
|
dataIndex: 'errorRate',
|
||||||
|
key: 'errorRate',
|
||||||
|
width: 120,
|
||||||
|
sorter: false,
|
||||||
|
align: 'left',
|
||||||
|
className: `column ${columnProgressBarClassName}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <div className="column-header med-col">Avg. Latency (ms)</div>,
|
||||||
|
dataIndex: 'latency',
|
||||||
|
key: 'latency',
|
||||||
|
width: 120,
|
||||||
|
sorter: false,
|
||||||
|
align: 'left',
|
||||||
|
className: `column ${columnProgressBarClassName}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const hardcodedAttributeKeys: BaseAutocompleteData[] = [
|
||||||
|
{
|
||||||
|
key: 'deployment.environment',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'service.name', // discuss about this with sagar once
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rpc.method',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const domainNameKey = 'net.peer.name';
|
||||||
|
|
||||||
|
interface APIMonitoringResponseRow {
|
||||||
|
data: {
|
||||||
|
endpoints: number;
|
||||||
|
error_rate: number;
|
||||||
|
lastseen: number;
|
||||||
|
[domainNameKey]: string;
|
||||||
|
p99: number;
|
||||||
|
rps: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface APIDomainsRowData {
|
||||||
|
key: string;
|
||||||
|
domainName: React.ReactNode;
|
||||||
|
endpointCount: React.ReactNode;
|
||||||
|
rate: React.ReactNode;
|
||||||
|
errorRate: React.ReactNode;
|
||||||
|
latency: React.ReactNode;
|
||||||
|
lastUsed: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatDataForTable = (
|
||||||
|
data: APIMonitoringResponseRow[],
|
||||||
|
): APIDomainsRowData[] =>
|
||||||
|
data?.map((domain) => ({
|
||||||
|
key: v4(),
|
||||||
|
domainName: (
|
||||||
|
<Tooltip title={domain.data[domainNameKey] || ''}>
|
||||||
|
{domain.data[domainNameKey] || ''}
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
endpointCount: domain.data.endpoints,
|
||||||
|
rate: domain.data.rps,
|
||||||
|
errorRate: domain.data.error_rate,
|
||||||
|
latency: Math.round(domain.data.p99 / 1000000), // Convert from nanoseconds to milliseconds
|
||||||
|
lastUsed: new Date(Math.floor(domain.data.lastseen / 1000000)).toISOString(), // Convert from nanoseconds to milliseconds
|
||||||
|
}));
|
||||||
@@ -284,6 +284,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
routeKey === 'LOGS_PIPELINES' ||
|
routeKey === 'LOGS_PIPELINES' ||
|
||||||
routeKey === 'LOGS_SAVE_VIEWS';
|
routeKey === 'LOGS_SAVE_VIEWS';
|
||||||
|
|
||||||
|
const isApiMonitoringView = (): boolean => routeKey === 'API_MONITORING';
|
||||||
|
|
||||||
const isTracesView = (): boolean =>
|
const isTracesView = (): boolean =>
|
||||||
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
|
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
|
||||||
|
|
||||||
@@ -433,7 +435,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
isAlertOverview() ||
|
isAlertOverview() ||
|
||||||
isMessagingQueues() ||
|
isMessagingQueues() ||
|
||||||
isCloudIntegrationPage() ||
|
isCloudIntegrationPage() ||
|
||||||
isInfraMonitoring()
|
isInfraMonitoring() ||
|
||||||
|
isApiMonitoringView()
|
||||||
? 0
|
? 0
|
||||||
: '0 1rem',
|
: '0 1rem',
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ interface QueryBuilderSearchV2Props {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
suffixIcon?: React.ReactNode;
|
suffixIcon?: React.ReactNode;
|
||||||
|
hardcodedAttributeKeys?: BaseAutocompleteData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Option {
|
export interface Option {
|
||||||
@@ -118,6 +119,7 @@ function QueryBuilderSearchV2(
|
|||||||
className,
|
className,
|
||||||
suffixIcon,
|
suffixIcon,
|
||||||
whereClauseConfig,
|
whereClauseConfig,
|
||||||
|
hardcodedAttributeKeys,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||||
@@ -232,7 +234,7 @@ function QueryBuilderSearchV2(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
queryKey: [searchParams],
|
queryKey: [searchParams],
|
||||||
enabled: isQueryEnabled && !isLogsDataSource,
|
enabled: isQueryEnabled && !isLogsDataSource && !hardcodedAttributeKeys,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -673,6 +675,18 @@ function QueryBuilderSearchV2(
|
|||||||
value: key,
|
value: key,
|
||||||
})) || []),
|
})) || []),
|
||||||
]);
|
]);
|
||||||
|
} else if (hardcodedAttributeKeys) {
|
||||||
|
const filteredKeys = hardcodedAttributeKeys.filter((key) =>
|
||||||
|
key.key
|
||||||
|
.toLowerCase()
|
||||||
|
.includes((searchValue?.split(' ')[0] || '').toLowerCase()),
|
||||||
|
);
|
||||||
|
setDropdownOptions(
|
||||||
|
filteredKeys.map((key) => ({
|
||||||
|
label: key.key,
|
||||||
|
value: key,
|
||||||
|
})),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
setDropdownOptions(
|
setDropdownOptions(
|
||||||
data?.payload?.attributeKeys?.map((key) => ({
|
data?.payload?.attributeKeys?.map((key) => ({
|
||||||
@@ -749,6 +763,7 @@ function QueryBuilderSearchV2(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
hardcodedAttributeKeys,
|
||||||
attributeValues?.payload,
|
attributeValues?.payload,
|
||||||
currentFilterItem?.key?.dataType,
|
currentFilterItem?.key?.dataType,
|
||||||
currentState,
|
currentState,
|
||||||
@@ -981,6 +996,7 @@ QueryBuilderSearchV2.defaultProps = {
|
|||||||
className: '',
|
className: '',
|
||||||
suffixIcon: null,
|
suffixIcon: null,
|
||||||
whereClauseConfig: {},
|
whereClauseConfig: {},
|
||||||
|
hardcodedAttributeKeys: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default QueryBuilderSearchV2;
|
export default QueryBuilderSearchV2;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import ROUTES from 'constants/routes';
|
|||||||
import {
|
import {
|
||||||
BarChart2,
|
BarChart2,
|
||||||
BellDot,
|
BellDot,
|
||||||
|
Binoculars,
|
||||||
Boxes,
|
Boxes,
|
||||||
BugIcon,
|
BugIcon,
|
||||||
Cloudy,
|
Cloudy,
|
||||||
@@ -105,6 +106,11 @@ const menuItems: SidebarItem[] = [
|
|||||||
label: 'Messaging Queues',
|
label: 'Messaging Queues',
|
||||||
icon: <ListMinus size={16} />,
|
icon: <ListMinus size={16} />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: ROUTES.API_MONITORING,
|
||||||
|
label: 'API Monitoring',
|
||||||
|
icon: <Binoculars size={16} />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: ROUTES.LIST_ALL_ALERT,
|
key: ROUTES.LIST_ALL_ALERT,
|
||||||
label: 'Alerts',
|
label: 'Alerts',
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ export const routesToSkip = [
|
|||||||
ROUTES.METRICS_EXPLORER,
|
ROUTES.METRICS_EXPLORER,
|
||||||
ROUTES.METRICS_EXPLORER_EXPLORER,
|
ROUTES.METRICS_EXPLORER_EXPLORER,
|
||||||
ROUTES.METRICS_EXPLORER_VIEWS,
|
ROUTES.METRICS_EXPLORER_VIEWS,
|
||||||
|
ROUTES.API_MONITORING,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
.api-monitoring-page {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.ant-tabs {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-nav {
|
||||||
|
padding: 0 16px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-content-holder {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.ant-tabs-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.ant-tabs-tabpane {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.api-monitoring-page {
|
||||||
|
.ant-tabs-nav {
|
||||||
|
&::before {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
frontend/src/pages/ApiMonitoring/ApiMonitoringPage.tsx
Normal file
22
frontend/src/pages/ApiMonitoring/ApiMonitoringPage.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import './ApiMonitoringPage.styles.scss';
|
||||||
|
|
||||||
|
import RouteTab from 'components/RouteTab';
|
||||||
|
import { TabRoutes } from 'components/RouteTab/types';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { useLocation } from 'react-use';
|
||||||
|
|
||||||
|
import { Explorer } from './constants';
|
||||||
|
|
||||||
|
function ApiMonitoringPage(): JSX.Element {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const routes: TabRoutes[] = [Explorer];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="api-monitoring-page">
|
||||||
|
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiMonitoringPage;
|
||||||
15
frontend/src/pages/ApiMonitoring/constants.tsx
Normal file
15
frontend/src/pages/ApiMonitoring/constants.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { TabRoutes } from 'components/RouteTab/types';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import ExplorerPage from 'container/ApiMonitoring/Explorer/Explorer';
|
||||||
|
import { Compass } from 'lucide-react';
|
||||||
|
|
||||||
|
export const Explorer: TabRoutes = {
|
||||||
|
Component: ExplorerPage,
|
||||||
|
name: (
|
||||||
|
<div className="tab-item">
|
||||||
|
<Compass size={16} /> Explorer
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
route: ROUTES.API_MONITORING,
|
||||||
|
key: ROUTES.API_MONITORING,
|
||||||
|
};
|
||||||
3
frontend/src/pages/ApiMonitoring/index.ts
Normal file
3
frontend/src/pages/ApiMonitoring/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import ApiMonitoring from './ApiMonitoringPage';
|
||||||
|
|
||||||
|
export default ApiMonitoring;
|
||||||
@@ -113,4 +113,5 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
|||||||
METRICS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
METRICS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
METRICS_EXPLORER_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
METRICS_EXPLORER_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
METRICS_EXPLORER_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
METRICS_EXPLORER_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
|
API_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11073,10 +11073,10 @@ lru-cache@^6.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3"
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3"
|
||||||
integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==
|
integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==
|
||||||
|
|
||||||
lucide-react@0.379.0:
|
lucide-react@0.427.0:
|
||||||
version "0.379.0"
|
version "0.427.0"
|
||||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.379.0.tgz#29e34eeffae7fb241b64b09868cbe3ab888ef7cc"
|
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.427.0.tgz#e06974514bbd591049f9d736b3d3ae99d4ede8c9"
|
||||||
integrity sha512-KcdeVPqmhRldldAAgptb8FjIunM2x2Zy26ZBh1RsEUcdLIvsEmbcw7KpzFYUy5BbpGeWhPu9Z9J5YXfStiXwhg==
|
integrity sha512-lv9s6c5BDF/ccuA0EgTdskTxIe11qpwBDmzRZHJAKtp8LTewAvDvOM+pTES9IpbBuTqkjiMhOmGpJ/CB+mKjFw==
|
||||||
|
|
||||||
lz-string@^1.4.4:
|
lz-string@^1.4.4:
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user