Compare commits
5 Commits
main
...
feat/api-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aac2647b67 | ||
|
|
785a0f2a48 | ||
|
|
b46adb5927 | ||
|
|
ae87499679 | ||
|
|
f0b0889d2e |
@@ -21,6 +21,7 @@ module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
"less": "^4.1.2",
|
||||
"less-loader": "^10.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "0.379.0",
|
||||
"lucide-react": "0.427.0",
|
||||
"mini-css-extract-plugin": "2.4.5",
|
||||
"overlayscrollbars": "^2.8.1",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
|
||||
@@ -269,3 +269,7 @@ export const MetricsExplorer = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "MetricsExplorer" */ 'pages/MetricsExplorer'),
|
||||
);
|
||||
|
||||
export const ApiMonitoring = Loadable(
|
||||
() => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'),
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
AllAlertChannels,
|
||||
AllErrors,
|
||||
APIKeys,
|
||||
ApiMonitoring,
|
||||
BillingPage,
|
||||
CreateAlertChannelAlerts,
|
||||
CreateNewAlerts,
|
||||
@@ -457,6 +458,13 @@ const routes: AppRoutes[] = [
|
||||
key: 'METRICS_EXPLORER_VIEWS',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.API_MONITORING,
|
||||
exact: true,
|
||||
component: ApiMonitoring,
|
||||
key: 'API_MONITORING',
|
||||
isPrivate: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const SUPPORT_ROUTE: AppRoutes = {
|
||||
|
||||
@@ -63,30 +63,31 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="quick-filters">
|
||||
{source !== QuickFiltersSource.INFRA_MONITORING && (
|
||||
<section className="header">
|
||||
<section className="left-actions">
|
||||
<FilterOutlined />
|
||||
<Typography.Text className="text">Filters for</Typography.Text>
|
||||
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||
</Tooltip>
|
||||
</section>
|
||||
{source !== QuickFiltersSource.INFRA_MONITORING &&
|
||||
source !== QuickFiltersSource.API_MONITORING && (
|
||||
<section className="header">
|
||||
<section className="left-actions">
|
||||
<FilterOutlined />
|
||||
<Typography.Text className="text">Filters for</Typography.Text>
|
||||
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||
</Tooltip>
|
||||
</section>
|
||||
|
||||
<section className="right-actions">
|
||||
<Tooltip title="Reset All">
|
||||
<SyncOutlined className="sync-icon" onClick={handleReset} />
|
||||
</Tooltip>
|
||||
<div className="divider-filter" />
|
||||
<Tooltip title="Collapse Filters">
|
||||
<VerticalAlignTopOutlined
|
||||
rotate={270}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
/>
|
||||
</Tooltip>
|
||||
<section className="right-actions">
|
||||
<Tooltip title="Reset All">
|
||||
<SyncOutlined className="sync-icon" onClick={handleReset} />
|
||||
</Tooltip>
|
||||
<div className="divider-filter" />
|
||||
<Tooltip title="Collapse Filters">
|
||||
<VerticalAlignTopOutlined
|
||||
rotate={270}
|
||||
onClick={handleFilterVisibilityChange}
|
||||
/>
|
||||
</Tooltip>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
)}
|
||||
)}
|
||||
|
||||
<section className="filters">
|
||||
{config.map((filter) => {
|
||||
|
||||
@@ -39,4 +39,5 @@ export enum QuickFiltersSource {
|
||||
LOGS_EXPLORER = 'logs-explorer',
|
||||
INFRA_MONITORING = 'infra-monitoring',
|
||||
TRACES_EXPLORER = 'traces-explorer',
|
||||
API_MONITORING = 'api-monitoring',
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ const ROUTES = {
|
||||
METRICS_EXPLORER: '/metrics-explorer/summary',
|
||||
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
|
||||
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
|
||||
API_MONITORING: '/api-monitoring/explorer',
|
||||
} as const;
|
||||
|
||||
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_SAVE_VIEWS';
|
||||
|
||||
const isApiMonitoringView = (): boolean => routeKey === 'API_MONITORING';
|
||||
|
||||
const isTracesView = (): boolean =>
|
||||
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
|
||||
|
||||
@@ -433,7 +435,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
isAlertOverview() ||
|
||||
isMessagingQueues() ||
|
||||
isCloudIntegrationPage() ||
|
||||
isInfraMonitoring()
|
||||
isInfraMonitoring() ||
|
||||
isApiMonitoringView()
|
||||
? 0
|
||||
: '0 1rem',
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ interface QueryBuilderSearchV2Props {
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
suffixIcon?: React.ReactNode;
|
||||
hardcodedAttributeKeys?: BaseAutocompleteData[];
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
@@ -118,6 +119,7 @@ function QueryBuilderSearchV2(
|
||||
className,
|
||||
suffixIcon,
|
||||
whereClauseConfig,
|
||||
hardcodedAttributeKeys,
|
||||
} = props;
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
@@ -232,7 +234,7 @@ function QueryBuilderSearchV2(
|
||||
},
|
||||
{
|
||||
queryKey: [searchParams],
|
||||
enabled: isQueryEnabled && !isLogsDataSource,
|
||||
enabled: isQueryEnabled && !isLogsDataSource && !hardcodedAttributeKeys,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -673,6 +675,18 @@ function QueryBuilderSearchV2(
|
||||
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 {
|
||||
setDropdownOptions(
|
||||
data?.payload?.attributeKeys?.map((key) => ({
|
||||
@@ -749,6 +763,7 @@ function QueryBuilderSearchV2(
|
||||
);
|
||||
}
|
||||
}, [
|
||||
hardcodedAttributeKeys,
|
||||
attributeValues?.payload,
|
||||
currentFilterItem?.key?.dataType,
|
||||
currentState,
|
||||
@@ -981,6 +996,7 @@ QueryBuilderSearchV2.defaultProps = {
|
||||
className: '',
|
||||
suffixIcon: null,
|
||||
whereClauseConfig: {},
|
||||
hardcodedAttributeKeys: undefined,
|
||||
};
|
||||
|
||||
export default QueryBuilderSearchV2;
|
||||
|
||||
@@ -3,6 +3,7 @@ import ROUTES from 'constants/routes';
|
||||
import {
|
||||
BarChart2,
|
||||
BellDot,
|
||||
Binoculars,
|
||||
Boxes,
|
||||
BugIcon,
|
||||
Cloudy,
|
||||
@@ -105,6 +106,11 @@ const menuItems: SidebarItem[] = [
|
||||
label: 'Messaging Queues',
|
||||
icon: <ListMinus size={16} />,
|
||||
},
|
||||
{
|
||||
key: ROUTES.API_MONITORING,
|
||||
label: 'API Monitoring',
|
||||
icon: <Binoculars size={16} />,
|
||||
},
|
||||
{
|
||||
key: ROUTES.LIST_ALL_ALERT,
|
||||
label: 'Alerts',
|
||||
|
||||
@@ -222,6 +222,7 @@ export const routesToSkip = [
|
||||
ROUTES.METRICS_EXPLORER,
|
||||
ROUTES.METRICS_EXPLORER_EXPLORER,
|
||||
ROUTES.METRICS_EXPLORER_VIEWS,
|
||||
ROUTES.API_MONITORING,
|
||||
];
|
||||
|
||||
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_EXPLORER: ['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"
|
||||
integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==
|
||||
|
||||
lucide-react@0.379.0:
|
||||
version "0.379.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.379.0.tgz#29e34eeffae7fb241b64b09868cbe3ab888ef7cc"
|
||||
integrity sha512-KcdeVPqmhRldldAAgptb8FjIunM2x2Zy26ZBh1RsEUcdLIvsEmbcw7KpzFYUy5BbpGeWhPu9Z9J5YXfStiXwhg==
|
||||
lucide-react@0.427.0:
|
||||
version "0.427.0"
|
||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.427.0.tgz#e06974514bbd591049f9d736b3d3ae99d4ede8c9"
|
||||
integrity sha512-lv9s6c5BDF/ccuA0EgTdskTxIe11qpwBDmzRZHJAKtp8LTewAvDvOM+pTES9IpbBuTqkjiMhOmGpJ/CB+mKjFw==
|
||||
|
||||
lz-string@^1.4.4:
|
||||
version "1.5.0"
|
||||
|
||||
Reference in New Issue
Block a user