mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-01 04:01:59 +08:00
feat: api monitoring domain details (#7308)
* feat: basic scaffolding for api monitoring & page level components added * feat: added hardcoded attribute keys in querybuildersearcv2 * feat: added utils for formatting * feat: api monitoring dashboard - 0 * feat: refactored the domain list * feat: domain details drawer with all functionality added * fix: minor eslint revert * feat: adding ui styling to domain list table * feat: adding pagination and minor styling to domain list table * feat: added ui for domain details drawer all endpoints tab * feat: added ui for domain details drawer all endpoints table with groupby * feat: endpoint details tab ui revamped * feat: endpoint details tab zero state styling * fix: syntax error fixed * feat: added conditional rendering of dep service and fixed graphs * feat: added status code charts * feat: added error states and loading states * feat: added groupby persistence for endpoints * feat: added domain navigation in the domain details drawer * feat: added domain navigation in the domain details drawer - fix * feat: isolated endpoint details zerostate * feat: Implemented series aggregation with charts * feat: ui for domain list table * feat: react query keys added and basic pr comments resolved * feat: fixed types * feat: light mode fixed * feat: empty states and light mode styling * fix: bug with the endpoint filters * feat: added port column and isolated endpoint in domain details * feat: added port column and isolated endpoint in domain details - minor cleanup * fix: minor type fix * fix: pr comments incorporated - 0 * fix: pr comments incorporated - 1 --------- Co-authored-by: Sahil <sahil@Sahils-MacBook-Pro.local>
This commit is contained in:
parent
3515686daf
commit
02f3dfefb9
@ -90,7 +90,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",
|
||||||
"motion": "12.4.13",
|
"motion": "12.4.13",
|
||||||
"overlayscrollbars": "^2.8.1",
|
"overlayscrollbars": "^2.8.1",
|
||||||
|
@ -295,3 +295,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,
|
||||||
@ -497,6 +498,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',
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,21 @@ export const REACT_QUERY_KEY = {
|
|||||||
GET_METRICS_LIST_FILTER_VALUES: 'GET_METRICS_LIST_FILTER_VALUES',
|
GET_METRICS_LIST_FILTER_VALUES: 'GET_METRICS_LIST_FILTER_VALUES',
|
||||||
GET_METRIC_DETAILS: 'GET_METRIC_DETAILS',
|
GET_METRIC_DETAILS: 'GET_METRIC_DETAILS',
|
||||||
GET_RELATED_METRICS: 'GET_RELATED_METRICS',
|
GET_RELATED_METRICS: 'GET_RELATED_METRICS',
|
||||||
|
|
||||||
|
// API Monitoring Query Keys
|
||||||
|
GET_DOMAINS_LIST: 'GET_DOMAINS_LIST',
|
||||||
|
GET_ENDPOINTS_LIST_BY_DOMAIN: 'GET_ENDPOINTS_LIST_BY_DOMAIN',
|
||||||
|
GET_NESTED_ENDPOINTS_LIST: 'GET_NESTED_ENDPOINTS_LIST',
|
||||||
|
GET_ENDPOINT_METRICS_DATA: 'GET_ENDPOINT_METRICS_DATA',
|
||||||
|
GET_ENDPOINT_STATUS_CODE_DATA: 'GET_ENDPOINT_STATUS_CODE_DATA',
|
||||||
|
GET_ENDPOINT_RATE_OVER_TIME_DATA: 'GET_ENDPOINT_RATE_OVER_TIME_DATA',
|
||||||
|
GET_ENDPOINT_LATENCY_OVER_TIME_DATA: 'GET_ENDPOINT_LATENCY_OVER_TIME_DATA',
|
||||||
|
GET_ENDPOINT_DROPDOWN_DATA: 'GET_ENDPOINT_DROPDOWN_DATA',
|
||||||
|
GET_ENDPOINT_DEPENDENT_SERVICES_DATA: 'GET_ENDPOINT_DEPENDENT_SERVICES_DATA',
|
||||||
|
GET_ENDPOINT_STATUS_CODE_BAR_CHARTS_DATA:
|
||||||
|
'GET_ENDPOINT_STATUS_CODE_BAR_CHARTS_DATA',
|
||||||
|
GET_ENDPOINT_STATUS_CODE_LATENCY_BAR_CHARTS_DATA:
|
||||||
|
'GET_ENDPOINT_STATUS_CODE_LATENCY_BAR_CHARTS_DATA',
|
||||||
GET_FUNNELS_LIST: 'GET_FUNNELS_LIST',
|
GET_FUNNELS_LIST: 'GET_FUNNELS_LIST',
|
||||||
GET_FUNNEL_DETAILS: 'GET_FUNNEL_DETAILS',
|
GET_FUNNEL_DETAILS: 'GET_FUNNEL_DETAILS',
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -71,6 +71,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',
|
||||||
METRICS_EXPLORER_BASE: '/metrics-explorer',
|
METRICS_EXPLORER_BASE: '/metrics-explorer',
|
||||||
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
|
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
|
||||||
HOME_PAGE: '/',
|
HOME_PAGE: '/',
|
||||||
|
@ -0,0 +1,239 @@
|
|||||||
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import { Select, Spin, Table, Typography } from 'antd';
|
||||||
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import {
|
||||||
|
EndPointsTableRowData,
|
||||||
|
formatEndPointsDataForTable,
|
||||||
|
getEndPointsColumnsConfig,
|
||||||
|
getEndPointsQueryPayload,
|
||||||
|
} from 'container/ApiMonitoring/utils';
|
||||||
|
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||||
|
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useQueries } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
import ErrorState from './components/ErrorState';
|
||||||
|
import ExpandedRow from './components/ExpandedRow';
|
||||||
|
import { VIEW_TYPES, VIEWS } from './constants';
|
||||||
|
|
||||||
|
function AllEndPoints({
|
||||||
|
domainName,
|
||||||
|
setSelectedEndPointName,
|
||||||
|
setSelectedView,
|
||||||
|
groupBy,
|
||||||
|
setGroupBy,
|
||||||
|
}: {
|
||||||
|
domainName: string;
|
||||||
|
setSelectedEndPointName: (name: string) => void;
|
||||||
|
setSelectedView: (tab: VIEWS) => void;
|
||||||
|
groupBy: IBuilderQuery['groupBy'];
|
||||||
|
setGroupBy: (groupBy: IBuilderQuery['groupBy']) => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const {
|
||||||
|
data: groupByFiltersData,
|
||||||
|
isLoading: isLoadingGroupByFilters,
|
||||||
|
} = useGetAggregateKeys({
|
||||||
|
dataSource: DataSource.TRACES,
|
||||||
|
aggregateAttribute: '',
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
searchText: '',
|
||||||
|
tagType: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [groupByOptions, setGroupByOptions] = useState<
|
||||||
|
{ value: string; label: string }[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>([]);
|
||||||
|
|
||||||
|
const handleGroupByChange = useCallback(
|
||||||
|
(value: IBuilderQuery['groupBy']) => {
|
||||||
|
const groupBy = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < value.length; index++) {
|
||||||
|
const element = (value[index] as unknown) as string;
|
||||||
|
|
||||||
|
const key = groupByFiltersData?.payload?.attributeKeys?.find(
|
||||||
|
(key) => key.key === element,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
groupBy.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setGroupBy(groupBy);
|
||||||
|
},
|
||||||
|
[groupByFiltersData, setGroupBy],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (groupByFiltersData?.payload) {
|
||||||
|
setGroupByOptions(
|
||||||
|
groupByFiltersData?.payload?.attributeKeys?.map((filter) => ({
|
||||||
|
value: filter.key,
|
||||||
|
label: filter.key,
|
||||||
|
})) || [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [groupByFiltersData]);
|
||||||
|
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryPayloads = useMemo(
|
||||||
|
() =>
|
||||||
|
getEndPointsQueryPayload(
|
||||||
|
groupBy,
|
||||||
|
domainName,
|
||||||
|
Math.floor(minTime / 1e9),
|
||||||
|
Math.floor(maxTime / 1e9),
|
||||||
|
),
|
||||||
|
[groupBy, domainName, minTime, maxTime],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Since only one query here
|
||||||
|
const endPointsDataQueries = useQueries(
|
||||||
|
queryPayloads.map((payload) => ({
|
||||||
|
queryKey: [
|
||||||
|
REACT_QUERY_KEY.GET_ENDPOINTS_LIST_BY_DOMAIN,
|
||||||
|
payload,
|
||||||
|
ENTITY_VERSION_V4,
|
||||||
|
groupBy,
|
||||||
|
],
|
||||||
|
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||||
|
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||||
|
enabled: !!payload,
|
||||||
|
staleTime: 60 * 1000, // 1 minute stale time : optimize this part
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const endPointsDataQuery = endPointsDataQueries[0];
|
||||||
|
const {
|
||||||
|
data: allEndPointsData,
|
||||||
|
isLoading,
|
||||||
|
isRefetching,
|
||||||
|
isError,
|
||||||
|
refetch,
|
||||||
|
} = endPointsDataQuery;
|
||||||
|
|
||||||
|
const endPointsColumnsConfig = useMemo(
|
||||||
|
() => getEndPointsColumnsConfig(groupBy.length > 0, expandedRowKeys),
|
||||||
|
[groupBy.length, expandedRowKeys],
|
||||||
|
);
|
||||||
|
|
||||||
|
const expandedRowRender = (record: EndPointsTableRowData): JSX.Element => (
|
||||||
|
<ExpandedRow
|
||||||
|
domainName={domainName}
|
||||||
|
selectedRowData={record}
|
||||||
|
setSelectedEndPointName={setSelectedEndPointName}
|
||||||
|
setSelectedView={setSelectedView}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleGroupByRowClick = (record: EndPointsTableRowData): void => {
|
||||||
|
if (expandedRowKeys.includes(record.key)) {
|
||||||
|
setExpandedRowKeys(expandedRowKeys.filter((key) => key !== record.key));
|
||||||
|
} else {
|
||||||
|
setExpandedRowKeys((expandedRowKeys) => [...expandedRowKeys, record.key]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRowClick = (record: EndPointsTableRowData): void => {
|
||||||
|
if (groupBy.length === 0) {
|
||||||
|
setSelectedEndPointName(record.endpointName); // this will open up the endpoint details tab
|
||||||
|
setSelectedView(VIEW_TYPES.ENDPOINT_DETAILS);
|
||||||
|
} else {
|
||||||
|
handleGroupByRowClick(record); // this will prepare the nested query payload
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formattedEndPointsData = useMemo(
|
||||||
|
() =>
|
||||||
|
formatEndPointsDataForTable(
|
||||||
|
allEndPointsData?.payload?.data?.result[0]?.table?.rows,
|
||||||
|
groupBy,
|
||||||
|
),
|
||||||
|
[groupBy, allEndPointsData],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<div className="all-endpoints-error-state-wrapper">
|
||||||
|
<ErrorState refetch={refetch} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="all-endpoints-container">
|
||||||
|
<div className="group-by-container">
|
||||||
|
<div className="group-by-label"> Group by </div>
|
||||||
|
<Select
|
||||||
|
className="group-by-select"
|
||||||
|
loading={isLoadingGroupByFilters}
|
||||||
|
mode="multiple"
|
||||||
|
value={groupBy}
|
||||||
|
allowClear
|
||||||
|
maxTagCount="responsive"
|
||||||
|
placeholder="Search for attribute"
|
||||||
|
options={groupByOptions}
|
||||||
|
onChange={handleGroupByChange}
|
||||||
|
/>{' '}
|
||||||
|
</div>
|
||||||
|
<div className="endpoints-table-container">
|
||||||
|
<div className="endpoints-table-header">Endpoint overview</div>
|
||||||
|
<Table
|
||||||
|
columns={endPointsColumnsConfig}
|
||||||
|
loading={{
|
||||||
|
spinning: isLoading || isRefetching,
|
||||||
|
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||||
|
}}
|
||||||
|
dataSource={isLoading || isRefetching ? [] : formattedEndPointsData}
|
||||||
|
locale={{
|
||||||
|
emptyText:
|
||||||
|
isLoading || isRefetching ? null : (
|
||||||
|
<div className="no-filtered-endpoints-message-container">
|
||||||
|
<div className="no-filtered-endpoints-message-content">
|
||||||
|
<img
|
||||||
|
src="/Icons/emptyState.svg"
|
||||||
|
alt="thinking-emoji"
|
||||||
|
className="empty-state-svg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography.Text className="no-filtered-endpoints-message">
|
||||||
|
This query had no results. Edit your query and try again!
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
scroll={{ x: true }}
|
||||||
|
tableLayout="fixed"
|
||||||
|
onRow={(record): { onClick: () => void; className: string } => ({
|
||||||
|
onClick: (): void => handleRowClick(record),
|
||||||
|
className: 'clickable-row',
|
||||||
|
})}
|
||||||
|
expandable={{
|
||||||
|
expandedRowRender: groupBy.length > 0 ? expandedRowRender : undefined,
|
||||||
|
expandedRowKeys,
|
||||||
|
expandIconColumnIndex: -1,
|
||||||
|
}}
|
||||||
|
rowClassName={(_, index): string =>
|
||||||
|
index % 2 === 0 ? 'table-row-dark' : 'table-row-light'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AllEndPoints;
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,143 @@
|
|||||||
|
import './DomainDetails.styles.scss';
|
||||||
|
|
||||||
|
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||||
|
import { Button, Divider, Drawer, Radio, Typography } from 'antd';
|
||||||
|
import { RadioChangeEvent } from 'antd/lib';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { ArrowDown, ArrowUp, X } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import AllEndPoints from './AllEndPoints';
|
||||||
|
import DomainMetrics from './components/DomainMetrics';
|
||||||
|
import { VIEW_TYPES, VIEWS } from './constants';
|
||||||
|
import EndPointDetailsWrapper from './EndPointDetailsWrapper';
|
||||||
|
|
||||||
|
function DomainDetails({
|
||||||
|
domainData,
|
||||||
|
handleClose,
|
||||||
|
selectedDomainIndex,
|
||||||
|
setSelectedDomainIndex,
|
||||||
|
domainListLength,
|
||||||
|
}: {
|
||||||
|
domainData: any;
|
||||||
|
handleClose: () => void;
|
||||||
|
selectedDomainIndex: number;
|
||||||
|
setSelectedDomainIndex: (index: number) => void;
|
||||||
|
domainListLength: number;
|
||||||
|
}): JSX.Element {
|
||||||
|
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.ALL_ENDPOINTS);
|
||||||
|
const [selectedEndPointName, setSelectedEndPointName] = useState<string>('');
|
||||||
|
const [endPointsGroupBy, setEndPointsGroupBy] = useState<
|
||||||
|
IBuilderQuery['groupBy']
|
||||||
|
>([]);
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||||
|
setSelectedView(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
width="60%"
|
||||||
|
title={
|
||||||
|
<div className="domain-details-drawer-header">
|
||||||
|
<div className="domain-details-drawer-header-title">
|
||||||
|
<Divider type="vertical" />
|
||||||
|
<Typography.Text className="title">
|
||||||
|
{domainData.domainName}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Button.Group className="domain-details-drawer-header-ctas">
|
||||||
|
<Button
|
||||||
|
className="domain-navigate-cta"
|
||||||
|
onClick={(): void => {
|
||||||
|
setSelectedDomainIndex(selectedDomainIndex - 1);
|
||||||
|
setSelectedEndPointName('');
|
||||||
|
setEndPointsGroupBy([]);
|
||||||
|
setSelectedView(VIEW_TYPES.ALL_ENDPOINTS);
|
||||||
|
}}
|
||||||
|
icon={<ArrowUp size={16} />}
|
||||||
|
disabled={selectedDomainIndex === 0}
|
||||||
|
title="Previous domain"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="domain-navigate-cta"
|
||||||
|
onClick={(): void => {
|
||||||
|
setSelectedDomainIndex(selectedDomainIndex + 1);
|
||||||
|
setSelectedEndPointName('');
|
||||||
|
setEndPointsGroupBy([]);
|
||||||
|
setSelectedView(VIEW_TYPES.ALL_ENDPOINTS);
|
||||||
|
}}
|
||||||
|
icon={<ArrowDown size={16} />}
|
||||||
|
disabled={selectedDomainIndex === domainListLength - 1}
|
||||||
|
title="Next domain"
|
||||||
|
/>
|
||||||
|
</Button.Group>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placement="right"
|
||||||
|
onClose={handleClose}
|
||||||
|
open={!!domainData}
|
||||||
|
style={{
|
||||||
|
overscrollBehavior: 'contain',
|
||||||
|
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||||
|
}}
|
||||||
|
className="domain-detail-drawer"
|
||||||
|
destroyOnClose
|
||||||
|
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||||
|
>
|
||||||
|
{domainData && (
|
||||||
|
<>
|
||||||
|
<DomainMetrics domainData={domainData} />
|
||||||
|
<div className="views-tabs-container">
|
||||||
|
<Radio.Group
|
||||||
|
className="views-tabs"
|
||||||
|
onChange={handleTabChange}
|
||||||
|
value={selectedView}
|
||||||
|
>
|
||||||
|
<Radio.Button
|
||||||
|
className={
|
||||||
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
|
selectedView === VIEW_TYPES.ALL_ENDPOINTS ? 'selected_view tab' : 'tab'
|
||||||
|
}
|
||||||
|
value={VIEW_TYPES.ALL_ENDPOINTS}
|
||||||
|
>
|
||||||
|
<div className="view-title">All Endpoints</div>
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button
|
||||||
|
className={
|
||||||
|
selectedView === VIEW_TYPES.ENDPOINT_DETAILS
|
||||||
|
? 'tab selected_view'
|
||||||
|
: 'tab'
|
||||||
|
}
|
||||||
|
value={VIEW_TYPES.ENDPOINT_DETAILS}
|
||||||
|
>
|
||||||
|
<div className="view-title">Endpoint Details</div>
|
||||||
|
</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
{selectedView === VIEW_TYPES.ALL_ENDPOINTS && (
|
||||||
|
<AllEndPoints
|
||||||
|
domainName={domainData.domainName}
|
||||||
|
setSelectedEndPointName={setSelectedEndPointName}
|
||||||
|
setSelectedView={setSelectedView}
|
||||||
|
groupBy={endPointsGroupBy}
|
||||||
|
setGroupBy={setEndPointsGroupBy}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedView === VIEW_TYPES.ENDPOINT_DETAILS && (
|
||||||
|
<EndPointDetailsWrapper
|
||||||
|
domainName={domainData.domainName}
|
||||||
|
endPointName={selectedEndPointName}
|
||||||
|
setSelectedEndPointName={setSelectedEndPointName}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DomainDetails;
|
@ -0,0 +1,171 @@
|
|||||||
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
|
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||||
|
import {
|
||||||
|
END_POINT_DETAILS_QUERY_KEYS_ARRAY,
|
||||||
|
getEndPointDetailsQueryPayload,
|
||||||
|
} from 'container/ApiMonitoring/utils';
|
||||||
|
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||||
|
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { useQueries } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
import DependentServices from './components/DependentServices';
|
||||||
|
import EndPointMetrics from './components/EndPointMetrics';
|
||||||
|
import EndPointsDropDown from './components/EndPointsDropDown';
|
||||||
|
import MetricOverTimeGraph from './components/MetricOverTimeGraph';
|
||||||
|
import StatusCodeBarCharts from './components/StatusCodeBarCharts';
|
||||||
|
import StatusCodeTable from './components/StatusCodeTable';
|
||||||
|
|
||||||
|
function EndPointDetails({
|
||||||
|
domainName,
|
||||||
|
endPointName,
|
||||||
|
setSelectedEndPointName,
|
||||||
|
}: {
|
||||||
|
domainName: string;
|
||||||
|
endPointName: string;
|
||||||
|
setSelectedEndPointName: (value: string) => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentQuery = initialQueriesMap[DataSource.TRACES];
|
||||||
|
|
||||||
|
const [filters, setFilters] = useState<IBuilderQuery['filters']>({
|
||||||
|
op: 'AND',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manually update the query to include the filters
|
||||||
|
// Because using the hook is causing the global domain
|
||||||
|
// query to be updated and causing main domain list to
|
||||||
|
// refetch with the filters of endpoints
|
||||||
|
|
||||||
|
const updatedCurrentQuery = useMemo(
|
||||||
|
() => ({
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...currentQuery.builder.queryData[0],
|
||||||
|
dataSource: DataSource.TRACES,
|
||||||
|
filters,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[filters, currentQuery],
|
||||||
|
);
|
||||||
|
|
||||||
|
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||||
|
|
||||||
|
const isServicesFilterApplied = useMemo(
|
||||||
|
() => filters.items.some((item) => item.key?.key === 'service.name'),
|
||||||
|
[filters],
|
||||||
|
);
|
||||||
|
|
||||||
|
const endPointDetailsQueryPayload = useMemo(
|
||||||
|
() =>
|
||||||
|
getEndPointDetailsQueryPayload(
|
||||||
|
domainName,
|
||||||
|
endPointName,
|
||||||
|
Math.floor(minTime / 1e9),
|
||||||
|
Math.floor(maxTime / 1e9),
|
||||||
|
filters,
|
||||||
|
),
|
||||||
|
[domainName, endPointName, filters, minTime, maxTime],
|
||||||
|
);
|
||||||
|
|
||||||
|
const endPointDetailsDataQueries = useQueries(
|
||||||
|
endPointDetailsQueryPayload.map((payload, index) => ({
|
||||||
|
queryKey: [
|
||||||
|
END_POINT_DETAILS_QUERY_KEYS_ARRAY[index],
|
||||||
|
payload,
|
||||||
|
filters.items,
|
||||||
|
ENTITY_VERSION_V4,
|
||||||
|
],
|
||||||
|
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||||
|
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||||
|
enabled: !!payload,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [
|
||||||
|
endPointMetricsDataQuery,
|
||||||
|
endPointStatusCodeDataQuery,
|
||||||
|
endPointRateOverTimeDataQuery,
|
||||||
|
endPointLatencyOverTimeDataQuery,
|
||||||
|
endPointDropDownDataQuery,
|
||||||
|
endPointDependentServicesDataQuery,
|
||||||
|
endPointStatusCodeBarChartsDataQuery,
|
||||||
|
endPointStatusCodeLatencyBarChartsDataQuery,
|
||||||
|
] = useMemo(
|
||||||
|
() => [
|
||||||
|
endPointDetailsDataQueries[0],
|
||||||
|
endPointDetailsDataQueries[1],
|
||||||
|
endPointDetailsDataQueries[2],
|
||||||
|
endPointDetailsDataQueries[3],
|
||||||
|
endPointDetailsDataQueries[4],
|
||||||
|
endPointDetailsDataQueries[5],
|
||||||
|
endPointDetailsDataQueries[6],
|
||||||
|
endPointDetailsDataQueries[7],
|
||||||
|
],
|
||||||
|
[endPointDetailsDataQueries],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="endpoint-details-container">
|
||||||
|
<div className="endpoint-details-filters-container">
|
||||||
|
<div className="endpoint-details-filters-container-dropdown">
|
||||||
|
<EndPointsDropDown
|
||||||
|
selectedEndPointName={endPointName}
|
||||||
|
setSelectedEndPointName={setSelectedEndPointName}
|
||||||
|
endPointDropDownDataQuery={endPointDropDownDataQuery}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="endpoint-details-filters-container-search">
|
||||||
|
<QueryBuilderSearchV2
|
||||||
|
query={query}
|
||||||
|
onChange={(searchFilters): void => {
|
||||||
|
setFilters(searchFilters);
|
||||||
|
}}
|
||||||
|
placeholder="Search for filters..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<EndPointMetrics endPointMetricsDataQuery={endPointMetricsDataQuery} />
|
||||||
|
{!isServicesFilterApplied && (
|
||||||
|
<DependentServices
|
||||||
|
dependentServicesQuery={endPointDependentServicesDataQuery}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<StatusCodeBarCharts
|
||||||
|
endPointStatusCodeBarChartsDataQuery={endPointStatusCodeBarChartsDataQuery}
|
||||||
|
endPointStatusCodeLatencyBarChartsDataQuery={
|
||||||
|
endPointStatusCodeLatencyBarChartsDataQuery
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusCodeTable endPointStatusCodeDataQuery={endPointStatusCodeDataQuery} />
|
||||||
|
<MetricOverTimeGraph
|
||||||
|
metricOverTimeDataQuery={endPointRateOverTimeDataQuery}
|
||||||
|
widgetInfoIndex={0}
|
||||||
|
endPointName={endPointName}
|
||||||
|
/>
|
||||||
|
<MetricOverTimeGraph
|
||||||
|
metricOverTimeDataQuery={endPointLatencyOverTimeDataQuery}
|
||||||
|
widgetInfoIndex={1}
|
||||||
|
endPointName={endPointName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EndPointDetails;
|
@ -0,0 +1,76 @@
|
|||||||
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import { getEndPointZeroStateQueryPayload } from 'container/ApiMonitoring/utils';
|
||||||
|
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useQueries } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
import EndPointDetailsZeroState from './components/EndPointDetailsZeroState';
|
||||||
|
import EndPointDetails from './EndPointDetails';
|
||||||
|
|
||||||
|
function EndPointDetailsWrapper({
|
||||||
|
domainName,
|
||||||
|
endPointName,
|
||||||
|
setSelectedEndPointName,
|
||||||
|
}: {
|
||||||
|
domainName: string;
|
||||||
|
endPointName: string;
|
||||||
|
setSelectedEndPointName: (value: string) => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const endPointZeroStateQueryPayload = useMemo(
|
||||||
|
() =>
|
||||||
|
getEndPointZeroStateQueryPayload(
|
||||||
|
domainName,
|
||||||
|
Math.floor(minTime / 1e9),
|
||||||
|
Math.floor(maxTime / 1e9),
|
||||||
|
),
|
||||||
|
[domainName, minTime, maxTime],
|
||||||
|
);
|
||||||
|
|
||||||
|
const endPointZeroStateDataQueries = useQueries(
|
||||||
|
endPointZeroStateQueryPayload.map((payload) => ({
|
||||||
|
queryKey: [
|
||||||
|
// Since only one query here
|
||||||
|
REACT_QUERY_KEY.GET_ENDPOINT_DROPDOWN_DATA,
|
||||||
|
payload,
|
||||||
|
ENTITY_VERSION_V4,
|
||||||
|
],
|
||||||
|
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||||
|
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||||
|
enabled: !!payload,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [endPointZeroStateDataQuery] = useMemo(
|
||||||
|
() => [endPointZeroStateDataQueries[0]],
|
||||||
|
[endPointZeroStateDataQueries],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (endPointName === '') {
|
||||||
|
return (
|
||||||
|
<EndPointDetailsZeroState
|
||||||
|
setSelectedEndPointName={setSelectedEndPointName}
|
||||||
|
endPointDropDownDataQuery={endPointZeroStateDataQuery}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EndPointDetails
|
||||||
|
domainName={domainName}
|
||||||
|
endPointName={endPointName}
|
||||||
|
setSelectedEndPointName={setSelectedEndPointName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EndPointDetailsWrapper;
|
@ -0,0 +1,108 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
import Skeleton from 'antd/lib/skeleton';
|
||||||
|
import { getFormattedDependentServicesData } from 'container/ApiMonitoring/utils';
|
||||||
|
import { UnfoldVertical } from 'lucide-react';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { UseQueryResult } from 'react-query';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
|
||||||
|
import ErrorState from './ErrorState';
|
||||||
|
|
||||||
|
interface DependentServicesProps {
|
||||||
|
dependentServicesQuery: UseQueryResult<SuccessResponse<any>, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DependentServices({
|
||||||
|
dependentServicesQuery,
|
||||||
|
}: DependentServicesProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
refetch,
|
||||||
|
isError,
|
||||||
|
isLoading,
|
||||||
|
isRefetching,
|
||||||
|
} = dependentServicesQuery;
|
||||||
|
|
||||||
|
const [currentRenderCount, setCurrentRenderCount] = useState(0);
|
||||||
|
|
||||||
|
const dependentServicesData = useMemo(() => {
|
||||||
|
const formattedDependentServicesData = getFormattedDependentServicesData(
|
||||||
|
data?.payload?.data?.result[0].table.rows,
|
||||||
|
);
|
||||||
|
setCurrentRenderCount(Math.min(formattedDependentServicesData.length, 5));
|
||||||
|
return formattedDependentServicesData;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const renderItems = useMemo(
|
||||||
|
() => dependentServicesData.slice(0, currentRenderCount),
|
||||||
|
[currentRenderCount, dependentServicesData],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading || isRefetching) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return <ErrorState refetch={refetch} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="top-services-content">
|
||||||
|
<div className="top-services-title">
|
||||||
|
<span className="title-wrapper">Dependent Services</span>
|
||||||
|
</div>
|
||||||
|
<div className="dependent-services-container">
|
||||||
|
{renderItems.length === 0 ? (
|
||||||
|
<div className="no-dependent-services-message-container">
|
||||||
|
<div className="no-dependent-services-message-content">
|
||||||
|
<img
|
||||||
|
src="/Icons/emptyState.svg"
|
||||||
|
alt="thinking-emoji"
|
||||||
|
className="empty-state-svg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography.Text className="no-dependent-services-message">
|
||||||
|
This query had no results. Edit your query and try again!
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
renderItems.map((item) => (
|
||||||
|
<div className="top-services-item" key={item.key}>
|
||||||
|
<div className="top-services-item-progress">
|
||||||
|
<div className="top-services-item-key">{item.serviceName}</div>
|
||||||
|
<div className="top-services-item-count">{item.count}</div>
|
||||||
|
<div
|
||||||
|
className="top-services-item-progress-bar"
|
||||||
|
style={{ width: `${item.percentage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="top-services-item-percentage">
|
||||||
|
{item.percentage.toFixed(2)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentRenderCount < dependentServicesData.length && (
|
||||||
|
<div
|
||||||
|
className="top-services-load-more"
|
||||||
|
onClick={(): void => setCurrentRenderCount(dependentServicesData.length)}
|
||||||
|
onKeyDown={(e): void => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
setCurrentRenderCount(dependentServicesData.length);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<UnfoldVertical size={14} />
|
||||||
|
Show more...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DependentServices;
|
@ -0,0 +1,82 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Progress, Tooltip, Typography } from 'antd';
|
||||||
|
import { getLastUsedRelativeTime } from 'container/ApiMonitoring/utils';
|
||||||
|
|
||||||
|
function DomainMetrics({ domainData }: { domainData: any }): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="domain-detail-drawer__endpoint">
|
||||||
|
<div className="domain-details-grid">
|
||||||
|
<div className="labels-row">
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
className="domain-details-metadata-label"
|
||||||
|
>
|
||||||
|
EXTERNAL API
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
className="domain-details-metadata-label"
|
||||||
|
>
|
||||||
|
AVERAGE LATENCY
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
className="domain-details-metadata-label"
|
||||||
|
>
|
||||||
|
ERROR RATE
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
className="domain-details-metadata-label"
|
||||||
|
>
|
||||||
|
LAST USED
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="values-row">
|
||||||
|
<Typography.Text className="domain-details-metadata-value">
|
||||||
|
<Tooltip title={domainData.endpointCount}>
|
||||||
|
<span className="round-metric-tag">{domainData.endpointCount}</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Typography.Text>
|
||||||
|
{/* // update the tooltip as well */}
|
||||||
|
<Typography.Text className="domain-details-metadata-value">
|
||||||
|
<Tooltip title={domainData.latency}>
|
||||||
|
<span className="round-metric-tag">
|
||||||
|
{(domainData.latency / 1000).toFixed(3)}s
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Typography.Text>
|
||||||
|
{/* // update the tooltip as well */}
|
||||||
|
<Typography.Text className="domain-details-metadata-value error-rate">
|
||||||
|
<Tooltip title={domainData.errorRate}>
|
||||||
|
<Progress
|
||||||
|
status="active"
|
||||||
|
percent={Number((domainData.errorRate * 100).toFixed(1))}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
size="small"
|
||||||
|
strokeColor={((): string => {
|
||||||
|
const errorRatePercent = Number(
|
||||||
|
(domainData.errorRate * 100).toFixed(1),
|
||||||
|
);
|
||||||
|
if (errorRatePercent >= 90) return Color.BG_SAKURA_500;
|
||||||
|
if (errorRatePercent >= 60) return Color.BG_AMBER_500;
|
||||||
|
return Color.BG_FOREST_500;
|
||||||
|
})()}
|
||||||
|
className="progress-bar"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Typography.Text>
|
||||||
|
{/* // update the tooltip as well */}
|
||||||
|
<Typography.Text className="domain-details-metadata-value">
|
||||||
|
<Tooltip title={domainData.lastUsed}>
|
||||||
|
{getLastUsedRelativeTime(domainData.lastUsed)}
|
||||||
|
</Tooltip>
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DomainMetrics;
|
@ -0,0 +1,38 @@
|
|||||||
|
import { UseQueryResult } from 'react-query';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
|
||||||
|
import EndPointsDropDown from './EndPointsDropDown';
|
||||||
|
|
||||||
|
function EndPointDetailsZeroState({
|
||||||
|
setSelectedEndPointName,
|
||||||
|
endPointDropDownDataQuery,
|
||||||
|
}: {
|
||||||
|
setSelectedEndPointName: (endPointName: string) => void;
|
||||||
|
endPointDropDownDataQuery: UseQueryResult<SuccessResponse<any>>;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="end-point-details-zero-state-wrapper">
|
||||||
|
<div className="end-point-details-zero-state-content">
|
||||||
|
<img
|
||||||
|
src="/Icons/no-data.svg"
|
||||||
|
alt="no-data"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className="end-point-details-zero-state-icon"
|
||||||
|
/>
|
||||||
|
<div className="end-point-details-zero-state-content-wrapper">
|
||||||
|
<div className="end-point-details-zero-state-text-content">
|
||||||
|
<div className="title">No endpoint selected yet</div>
|
||||||
|
<div className="description">Select an endpoint to see the details</div>
|
||||||
|
</div>
|
||||||
|
<EndPointsDropDown
|
||||||
|
setSelectedEndPointName={setSelectedEndPointName}
|
||||||
|
endPointDropDownDataQuery={endPointDropDownDataQuery}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EndPointDetailsZeroState;
|
@ -0,0 +1,121 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Progress, Skeleton, Tooltip, Typography } from 'antd';
|
||||||
|
import { getFormattedEndPointMetricsData } from 'container/ApiMonitoring/utils';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { UseQueryResult } from 'react-query';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
|
||||||
|
import ErrorState from './ErrorState';
|
||||||
|
|
||||||
|
function EndPointMetrics({
|
||||||
|
endPointMetricsDataQuery,
|
||||||
|
}: {
|
||||||
|
endPointMetricsDataQuery: UseQueryResult<SuccessResponse<any>, unknown>;
|
||||||
|
}): JSX.Element {
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
isRefetching,
|
||||||
|
isError,
|
||||||
|
data,
|
||||||
|
refetch,
|
||||||
|
} = endPointMetricsDataQuery;
|
||||||
|
|
||||||
|
const metricsData = useMemo(() => {
|
||||||
|
if (isLoading || isRefetching || isError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFormattedEndPointMetricsData(
|
||||||
|
data?.payload?.data?.result[0].table.rows,
|
||||||
|
);
|
||||||
|
}, [data?.payload?.data?.result, isLoading, isRefetching, isError]);
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return <ErrorState refetch={refetch} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="domain-detail-drawer__endpoint">
|
||||||
|
<div className="domain-details-grid">
|
||||||
|
<div className="labels-row">
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
className="domain-details-metadata-label"
|
||||||
|
>
|
||||||
|
Rate
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
className="domain-details-metadata-label"
|
||||||
|
>
|
||||||
|
AVERAGE LATENCY
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
className="domain-details-metadata-label"
|
||||||
|
>
|
||||||
|
ERROR RATE
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
className="domain-details-metadata-label"
|
||||||
|
>
|
||||||
|
LAST USED
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="values-row">
|
||||||
|
<Typography.Text className="domain-details-metadata-value">
|
||||||
|
{isLoading || isRefetching ? (
|
||||||
|
<Skeleton.Button active size="small" />
|
||||||
|
) : (
|
||||||
|
<Tooltip title={metricsData?.rate}>
|
||||||
|
<span className="round-metric-tag">{metricsData?.rate}/sec</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text className="domain-details-metadata-value">
|
||||||
|
{isLoading || isRefetching ? (
|
||||||
|
<Skeleton.Button active size="small" />
|
||||||
|
) : (
|
||||||
|
<Tooltip title={metricsData?.latency}>
|
||||||
|
<span className="round-metric-tag">{metricsData?.latency}ms</span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text className="domain-details-metadata-value error-rate">
|
||||||
|
{isLoading || isRefetching ? (
|
||||||
|
<Skeleton.Button active size="small" />
|
||||||
|
) : (
|
||||||
|
<Tooltip title={metricsData?.errorRate}>
|
||||||
|
<Progress
|
||||||
|
percent={Number((metricsData?.errorRate ?? 0 * 100).toFixed(1))}
|
||||||
|
strokeLinecap="butt"
|
||||||
|
size="small"
|
||||||
|
strokeColor={((): string => {
|
||||||
|
const errorRatePercent = Number(
|
||||||
|
(metricsData?.errorRate ?? 0 * 100).toFixed(1),
|
||||||
|
);
|
||||||
|
if (errorRatePercent >= 90) return Color.BG_SAKURA_500;
|
||||||
|
if (errorRatePercent >= 60) return Color.BG_AMBER_500;
|
||||||
|
return Color.BG_FOREST_500;
|
||||||
|
})()}
|
||||||
|
className="progress-bar"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text className="domain-details-metadata-value">
|
||||||
|
{isLoading || isRefetching ? (
|
||||||
|
<Skeleton.Button active size="small" />
|
||||||
|
) : (
|
||||||
|
<Tooltip title={metricsData?.lastUsed}>{metricsData?.lastUsed}</Tooltip>
|
||||||
|
)}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EndPointMetrics;
|
@ -0,0 +1,48 @@
|
|||||||
|
import { Select } from 'antd';
|
||||||
|
import { getFormattedEndPointDropDownData } from 'container/ApiMonitoring/utils';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { UseQueryResult } from 'react-query';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
|
||||||
|
interface EndPointsDropDownProps {
|
||||||
|
selectedEndPointName?: string;
|
||||||
|
setSelectedEndPointName: (value: string) => void;
|
||||||
|
endPointDropDownDataQuery: UseQueryResult<SuccessResponse<any>, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
selectedEndPointName: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
function EndPointsDropDown({
|
||||||
|
selectedEndPointName,
|
||||||
|
setSelectedEndPointName,
|
||||||
|
endPointDropDownDataQuery,
|
||||||
|
}: EndPointsDropDownProps): JSX.Element {
|
||||||
|
const { data, isLoading, isFetching } = endPointDropDownDataQuery;
|
||||||
|
|
||||||
|
const handleChange = (value: string): void => {
|
||||||
|
setSelectedEndPointName(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formattedData = useMemo(
|
||||||
|
() =>
|
||||||
|
getFormattedEndPointDropDownData(data?.payload.data.result[0].table.rows),
|
||||||
|
[data?.payload.data.result],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={selectedEndPointName || undefined}
|
||||||
|
placeholder="Select endpoint"
|
||||||
|
loading={isLoading || isFetching}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onChange={handleChange}
|
||||||
|
options={formattedData}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EndPointsDropDown.defaultProps = defaultProps;
|
||||||
|
|
||||||
|
export default EndPointsDropDown;
|
@ -0,0 +1,31 @@
|
|||||||
|
import { Button, Typography } from 'antd';
|
||||||
|
import { RotateCw } from 'lucide-react';
|
||||||
|
|
||||||
|
function ErrorState({ refetch }: { refetch: () => void }): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="error-state-container">
|
||||||
|
<div className="error-state-content-wrapper">
|
||||||
|
<div className="error-state-content">
|
||||||
|
<div className="icon">
|
||||||
|
<img src="/Icons/awwSnap.svg" alt="awwSnap" width={32} height={32} />
|
||||||
|
</div>
|
||||||
|
<div className="error-state-text">
|
||||||
|
<Typography.Text>Uh-oh :/ We ran into an error.</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
Please refresh this panel.
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="refresh-cta"
|
||||||
|
onClick={(): void => refetch()}
|
||||||
|
icon={<RotateCw size={16} />}
|
||||||
|
>
|
||||||
|
Refresh this panel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorState;
|
@ -0,0 +1,127 @@
|
|||||||
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import { Spin, Table } from 'antd';
|
||||||
|
import { ColumnType } from 'antd/lib/table';
|
||||||
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import {
|
||||||
|
createFiltersForSelectedRowData,
|
||||||
|
EndPointsTableRowData,
|
||||||
|
formatEndPointsDataForTable,
|
||||||
|
getEndPointsColumnsConfig,
|
||||||
|
getEndPointsQueryPayload,
|
||||||
|
} from 'container/ApiMonitoring/utils';
|
||||||
|
import LoadingContainer from 'container/InfraMonitoringK8s/LoadingContainer';
|
||||||
|
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useQueries } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
import { VIEW_TYPES, VIEWS } from '../constants';
|
||||||
|
|
||||||
|
function ExpandedRow({
|
||||||
|
domainName,
|
||||||
|
selectedRowData,
|
||||||
|
setSelectedEndPointName,
|
||||||
|
setSelectedView,
|
||||||
|
}: {
|
||||||
|
domainName: string;
|
||||||
|
selectedRowData: EndPointsTableRowData;
|
||||||
|
setSelectedEndPointName: (name: string) => void;
|
||||||
|
setSelectedView: (view: VIEWS) => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const nestedColumns = useMemo(() => getEndPointsColumnsConfig(false, []), []);
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
const groupedByRowDataQueryPayload = useMemo(() => {
|
||||||
|
if (!selectedRowData) return null;
|
||||||
|
|
||||||
|
const filters = createFiltersForSelectedRowData(selectedRowData);
|
||||||
|
|
||||||
|
const baseQueryPayload = getEndPointsQueryPayload(
|
||||||
|
[],
|
||||||
|
domainName,
|
||||||
|
Math.floor(minTime / 1e9),
|
||||||
|
Math.floor(maxTime / 1e9),
|
||||||
|
);
|
||||||
|
|
||||||
|
return baseQueryPayload.map((currentQueryPayload) => ({
|
||||||
|
...currentQueryPayload,
|
||||||
|
query: {
|
||||||
|
...currentQueryPayload.query,
|
||||||
|
builder: {
|
||||||
|
...currentQueryPayload.query.builder,
|
||||||
|
queryData: currentQueryPayload.query.builder.queryData.map(
|
||||||
|
(queryData) => ({
|
||||||
|
...queryData,
|
||||||
|
filters: {
|
||||||
|
items: [...(queryData.filters?.items || []), ...filters.items],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, [domainName, minTime, maxTime, selectedRowData]);
|
||||||
|
|
||||||
|
const groupedByRowQueries = useQueries(
|
||||||
|
groupedByRowDataQueryPayload
|
||||||
|
? groupedByRowDataQueryPayload.map((payload) => ({
|
||||||
|
queryKey: [
|
||||||
|
`${REACT_QUERY_KEY.GET_NESTED_ENDPOINTS_LIST}-${domainName}-${selectedRowData?.key}`,
|
||||||
|
payload,
|
||||||
|
ENTITY_VERSION_V4,
|
||||||
|
selectedRowData?.key,
|
||||||
|
],
|
||||||
|
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
|
||||||
|
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
|
||||||
|
enabled: !!payload && !!selectedRowData,
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupedByRowQuery = groupedByRowQueries[0];
|
||||||
|
return (
|
||||||
|
<div className="expanded-table-container">
|
||||||
|
{groupedByRowQuery?.isFetching || groupedByRowQuery?.isLoading ? (
|
||||||
|
<LoadingContainer />
|
||||||
|
) : (
|
||||||
|
<div className="expanded-table">
|
||||||
|
<Table
|
||||||
|
columns={nestedColumns as ColumnType<EndPointsTableRowData>[]}
|
||||||
|
dataSource={
|
||||||
|
groupedByRowQuery?.data
|
||||||
|
? formatEndPointsDataForTable(
|
||||||
|
groupedByRowQuery.data?.payload.data.result[0].table?.rows,
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
pagination={false}
|
||||||
|
scroll={{ x: true }}
|
||||||
|
tableLayout="fixed"
|
||||||
|
showHeader={false}
|
||||||
|
loading={{
|
||||||
|
spinning: groupedByRowQuery?.isFetching || groupedByRowQuery?.isLoading,
|
||||||
|
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||||
|
}}
|
||||||
|
onRow={(record): { onClick: () => void; className: string } => ({
|
||||||
|
onClick: (): void => {
|
||||||
|
setSelectedEndPointName(record.endpointName);
|
||||||
|
setSelectedView(VIEW_TYPES.ENDPOINT_DETAILS);
|
||||||
|
},
|
||||||
|
className: 'expanded-clickable-row',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExpandedRow;
|
@ -0,0 +1,114 @@
|
|||||||
|
import { Card, Skeleton, Typography } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import Uplot from 'components/Uplot';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import {
|
||||||
|
apiWidgetInfo,
|
||||||
|
extractPortAndEndpoint,
|
||||||
|
getFormattedChartData,
|
||||||
|
} from 'container/ApiMonitoring/utils';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
|
import { UseQueryResult } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { Options } from 'uplot';
|
||||||
|
|
||||||
|
import ErrorState from './ErrorState';
|
||||||
|
|
||||||
|
function MetricOverTimeGraph({
|
||||||
|
metricOverTimeDataQuery,
|
||||||
|
widgetInfoIndex,
|
||||||
|
endPointName,
|
||||||
|
}: {
|
||||||
|
metricOverTimeDataQuery: UseQueryResult<SuccessResponse<any>, unknown>;
|
||||||
|
widgetInfoIndex: number;
|
||||||
|
endPointName: string;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { data } = metricOverTimeDataQuery;
|
||||||
|
|
||||||
|
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dimensions = useResizeObserver(graphRef);
|
||||||
|
|
||||||
|
const { endpoint } = extractPortAndEndpoint(endPointName);
|
||||||
|
|
||||||
|
const formattedChartData = useMemo(
|
||||||
|
() => getFormattedChartData(data?.payload, [endpoint]),
|
||||||
|
[data?.payload, endpoint],
|
||||||
|
);
|
||||||
|
|
||||||
|
const chartData = useMemo(() => getUPlotChartData(formattedChartData), [
|
||||||
|
formattedChartData,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
getUPlotChartOptions({
|
||||||
|
apiResponse: formattedChartData,
|
||||||
|
isDarkMode,
|
||||||
|
dimensions,
|
||||||
|
yAxisUnit: apiWidgetInfo[widgetInfoIndex].yAxisUnit,
|
||||||
|
softMax: null,
|
||||||
|
softMin: null,
|
||||||
|
minTimeScale: Math.floor(minTime / 1e9),
|
||||||
|
maxTimeScale: Math.floor(maxTime / 1e9),
|
||||||
|
panelType: PANEL_TYPES.TIME_SERIES,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
formattedChartData,
|
||||||
|
minTime,
|
||||||
|
maxTime,
|
||||||
|
widgetInfoIndex,
|
||||||
|
dimensions,
|
||||||
|
isDarkMode,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderCardContent = useCallback(
|
||||||
|
(query: UseQueryResult<SuccessResponse<any>, unknown>): JSX.Element => {
|
||||||
|
if (query.isLoading) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.error) {
|
||||||
|
return <ErrorState refetch={query.refetch} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx('chart-container', {
|
||||||
|
'no-data-container':
|
||||||
|
!query.isLoading && !query?.data?.payload?.data?.result?.length,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Uplot options={options as Options} data={chartData} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[options, chartData],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Card bordered className="endpoint-details-card">
|
||||||
|
<Typography.Text>{apiWidgetInfo[widgetInfoIndex].title}</Typography.Text>
|
||||||
|
<div className="graph-container" ref={graphRef}>
|
||||||
|
{renderCardContent(metricOverTimeDataQuery)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MetricOverTimeGraph;
|
@ -0,0 +1,168 @@
|
|||||||
|
import { Button, Card, Skeleton, Typography } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import Uplot from 'components/Uplot';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import {
|
||||||
|
getFormattedEndPointStatusCodeChartData,
|
||||||
|
statusCodeWidgetInfo,
|
||||||
|
} from 'container/ApiMonitoring/utils';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
import { UseQueryResult } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { Options } from 'uplot';
|
||||||
|
|
||||||
|
import ErrorState from './ErrorState';
|
||||||
|
|
||||||
|
function StatusCodeBarCharts({
|
||||||
|
endPointStatusCodeBarChartsDataQuery,
|
||||||
|
endPointStatusCodeLatencyBarChartsDataQuery,
|
||||||
|
}: {
|
||||||
|
endPointStatusCodeBarChartsDataQuery: UseQueryResult<
|
||||||
|
SuccessResponse<any>,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
endPointStatusCodeLatencyBarChartsDataQuery: UseQueryResult<
|
||||||
|
SuccessResponse<any>,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
}): JSX.Element {
|
||||||
|
// 0 : Status Code Count
|
||||||
|
// 1 : Status Code Latency
|
||||||
|
const [currentWidgetInfoIndex, setCurrentWidgetInfoIndex] = useState(0);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: endPointStatusCodeBarChartsData,
|
||||||
|
} = endPointStatusCodeBarChartsDataQuery;
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: endPointStatusCodeLatencyBarChartsData,
|
||||||
|
} = endPointStatusCodeLatencyBarChartsDataQuery;
|
||||||
|
|
||||||
|
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dimensions = useResizeObserver(graphRef);
|
||||||
|
const formattedEndPointStatusCodeBarChartsDataPayload = useMemo(
|
||||||
|
() =>
|
||||||
|
getFormattedEndPointStatusCodeChartData(
|
||||||
|
endPointStatusCodeBarChartsData?.payload,
|
||||||
|
'sum',
|
||||||
|
),
|
||||||
|
[endPointStatusCodeBarChartsData?.payload],
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedEndPointStatusCodeLatencyBarChartsDataPayload = useMemo(
|
||||||
|
() =>
|
||||||
|
getFormattedEndPointStatusCodeChartData(
|
||||||
|
endPointStatusCodeLatencyBarChartsData?.payload,
|
||||||
|
'average',
|
||||||
|
),
|
||||||
|
[endPointStatusCodeLatencyBarChartsData?.payload],
|
||||||
|
);
|
||||||
|
|
||||||
|
const chartData = useMemo(
|
||||||
|
() =>
|
||||||
|
getUPlotChartData(
|
||||||
|
currentWidgetInfoIndex === 0
|
||||||
|
? formattedEndPointStatusCodeBarChartsDataPayload
|
||||||
|
: formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
||||||
|
),
|
||||||
|
[
|
||||||
|
currentWidgetInfoIndex,
|
||||||
|
formattedEndPointStatusCodeBarChartsDataPayload,
|
||||||
|
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
getUPlotChartOptions({
|
||||||
|
apiResponse:
|
||||||
|
currentWidgetInfoIndex === 0
|
||||||
|
? formattedEndPointStatusCodeBarChartsDataPayload
|
||||||
|
: formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
||||||
|
isDarkMode,
|
||||||
|
dimensions,
|
||||||
|
yAxisUnit: statusCodeWidgetInfo[currentWidgetInfoIndex].yAxisUnit,
|
||||||
|
softMax: null,
|
||||||
|
softMin: null,
|
||||||
|
minTimeScale: Math.floor(minTime / 1e9),
|
||||||
|
maxTimeScale: Math.floor(maxTime / 1e9),
|
||||||
|
panelType: PANEL_TYPES.BAR,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
minTime,
|
||||||
|
maxTime,
|
||||||
|
currentWidgetInfoIndex,
|
||||||
|
dimensions,
|
||||||
|
formattedEndPointStatusCodeBarChartsDataPayload,
|
||||||
|
formattedEndPointStatusCodeLatencyBarChartsDataPayload,
|
||||||
|
isDarkMode,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderCardContent = useCallback(
|
||||||
|
(query: UseQueryResult<SuccessResponse<any>, unknown>): JSX.Element => {
|
||||||
|
if (query.isLoading) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.error) {
|
||||||
|
return <ErrorState refetch={query.refetch} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx('chart-container', {
|
||||||
|
'no-data-container':
|
||||||
|
!query.isLoading && !query?.data?.payload?.data?.result?.length,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Uplot options={options as Options} data={chartData} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[options, chartData],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Card bordered className="endpoint-details-card">
|
||||||
|
<div className="header">
|
||||||
|
<Typography.Text>Call response status</Typography.Text>
|
||||||
|
<Button.Group className="views-tabs">
|
||||||
|
<Button
|
||||||
|
value={0}
|
||||||
|
className={currentWidgetInfoIndex === 0 ? 'selected_view tab' : 'tab'}
|
||||||
|
disabled={false}
|
||||||
|
onClick={(): void => setCurrentWidgetInfoIndex(0)}
|
||||||
|
>
|
||||||
|
Number of calls
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
value={1}
|
||||||
|
className={currentWidgetInfoIndex === 1 ? 'selected_view tab' : 'tab'}
|
||||||
|
onClick={(): void => setCurrentWidgetInfoIndex(1)}
|
||||||
|
>
|
||||||
|
Latency
|
||||||
|
</Button>
|
||||||
|
</Button.Group>
|
||||||
|
</div>
|
||||||
|
<div className="graph-container" ref={graphRef}>
|
||||||
|
{renderCardContent(endPointStatusCodeBarChartsDataQuery)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default StatusCodeBarCharts;
|
@ -0,0 +1,72 @@
|
|||||||
|
import { Table, Typography } from 'antd';
|
||||||
|
import {
|
||||||
|
endPointStatusCodeColumns,
|
||||||
|
getFormattedEndPointStatusCodeData,
|
||||||
|
} from 'container/ApiMonitoring/utils';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { UseQueryResult } from 'react-query';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
|
||||||
|
import ErrorState from './ErrorState';
|
||||||
|
|
||||||
|
function StatusCodeTable({
|
||||||
|
endPointStatusCodeDataQuery,
|
||||||
|
}: {
|
||||||
|
endPointStatusCodeDataQuery: UseQueryResult<SuccessResponse<any>, unknown>;
|
||||||
|
}): JSX.Element {
|
||||||
|
const {
|
||||||
|
isLoading,
|
||||||
|
isRefetching,
|
||||||
|
isError,
|
||||||
|
data,
|
||||||
|
refetch,
|
||||||
|
} = endPointStatusCodeDataQuery;
|
||||||
|
|
||||||
|
const statusCodeData = useMemo(() => {
|
||||||
|
if (isLoading || isRefetching || isError) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFormattedEndPointStatusCodeData(
|
||||||
|
data?.payload?.data?.result[0].table.rows,
|
||||||
|
);
|
||||||
|
}, [data?.payload?.data?.result, isLoading, isRefetching, isError]);
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return <ErrorState refetch={refetch} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="status-code-table-container">
|
||||||
|
<Table
|
||||||
|
loading={isLoading || isRefetching}
|
||||||
|
dataSource={statusCodeData || []}
|
||||||
|
columns={endPointStatusCodeColumns}
|
||||||
|
pagination={false}
|
||||||
|
rowClassName={(_, index): string =>
|
||||||
|
index % 2 === 0 ? 'table-row-dark' : 'table-row-light'
|
||||||
|
}
|
||||||
|
locale={{
|
||||||
|
emptyText:
|
||||||
|
isLoading || isRefetching ? null : (
|
||||||
|
<div className="no-status-code-data-message-container">
|
||||||
|
<div className="no-status-code-data-message-content">
|
||||||
|
<img
|
||||||
|
src="/Icons/emptyState.svg"
|
||||||
|
alt="thinking-emoji"
|
||||||
|
className="empty-state-svg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography.Text className="no-status-code-data-message">
|
||||||
|
This query had no results. Edit your query and try again!
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StatusCodeTable;
|
@ -0,0 +1,9 @@
|
|||||||
|
export enum VIEWS {
|
||||||
|
ALL_ENDPOINTS = 'all_endpoints',
|
||||||
|
ENDPOINT_DETAILS = 'endpoint_details',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VIEW_TYPES = {
|
||||||
|
ALL_ENDPOINTS: VIEWS.ALL_ENDPOINTS,
|
||||||
|
ENDPOINT_DETAILS: VIEWS.ENDPOINT_DETAILS,
|
||||||
|
};
|
@ -0,0 +1,156 @@
|
|||||||
|
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 { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||||
|
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||||
|
import { useMemo, useState } 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';
|
||||||
|
import DomainDetails from './DomainDetails/DomainDetails';
|
||||||
|
|
||||||
|
function DomainList({
|
||||||
|
query,
|
||||||
|
showIP,
|
||||||
|
handleChangeQueryData,
|
||||||
|
}: {
|
||||||
|
query: IBuilderQuery;
|
||||||
|
showIP: boolean;
|
||||||
|
handleChangeQueryData: HandleChangeQueryData;
|
||||||
|
}): JSX.Element {
|
||||||
|
const [selectedDomainIndex, setSelectedDomainIndex] = useState<number>(-1);
|
||||||
|
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(
|
||||||
|
[REACT_QUERY_KEY.GET_DOMAINS_LIST, minTime, maxTime, query, showIP],
|
||||||
|
fetchApiOverview,
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedDataForTable = useMemo(
|
||||||
|
() => formatDataForTable(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 ? [] : formattedDataForTable}
|
||||||
|
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"
|
||||||
|
onRow={(record, index): { onClick: () => void; className: string } => ({
|
||||||
|
onClick: (): void => {
|
||||||
|
if (index !== undefined) {
|
||||||
|
const dataIndex = formattedDataForTable.findIndex(
|
||||||
|
(item) => item.key === record.key,
|
||||||
|
);
|
||||||
|
setSelectedDomainIndex(dataIndex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
className: 'expanded-clickable-row',
|
||||||
|
})}
|
||||||
|
rowClassName={(_, index): string =>
|
||||||
|
index % 2 === 0 ? 'table-row-dark' : 'table-row-light'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{selectedDomainIndex !== -1 && (
|
||||||
|
<DomainDetails
|
||||||
|
domainData={formattedDataForTable[selectedDomainIndex]}
|
||||||
|
selectedDomainIndex={selectedDomainIndex}
|
||||||
|
setSelectedDomainIndex={setSelectedDomainIndex}
|
||||||
|
domainListLength={formattedDataForTable.length}
|
||||||
|
handleClose={(): void => {
|
||||||
|
setSelectedDomainIndex(-1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DomainList;
|
@ -0,0 +1,219 @@
|
|||||||
|
.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 {
|
||||||
|
min-width: 80%;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-monitoring-domain-list-table {
|
||||||
|
.ant-table {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
padding: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 18px;
|
||||||
|
/* 163.636% */
|
||||||
|
letter-spacing: 0.44px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: none;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-thead > tr > th:has(.domain-list-name-col-header) {
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:has(.domain-list-name-col-value) {
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.round-metric-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 2px 8px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
border-radius: 50px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-slate-500);
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row-light {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row-dark {
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-rate {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.api-monitoring-domain-list-table {
|
||||||
|
.ant-table {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
color: var(--text-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-thead > tr > th:has(.domain-list-name-col-header) {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
color: var(--bg-ink-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:has(.domain-list-name-col-value) {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr:hover > td {
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row-light {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row-dark {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.round-metric-tag {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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="api-quick-filter-left-section">
|
||||||
|
<div className="api-quick-filters-header">
|
||||||
|
<FilterOutlined />
|
||||||
|
<Typography.Text>Filters</Typography.Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="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;
|
2087
frontend/src/container/ApiMonitoring/utils.tsx
Normal file
2087
frontend/src/container/ApiMonitoring/utils.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -337,6 +337,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';
|
||||||
|
|
||||||
@ -658,7 +660,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
isAlertOverview() ||
|
isAlertOverview() ||
|
||||||
isMessagingQueues() ||
|
isMessagingQueues() ||
|
||||||
isCloudIntegrationPage() ||
|
isCloudIntegrationPage() ||
|
||||||
isInfraMonitoring()
|
isInfraMonitoring() ||
|
||||||
|
isApiMonitoringView()
|
||||||
? 0
|
? 0
|
||||||
: '0 1rem',
|
: '0 1rem',
|
||||||
|
|
||||||
|
@ -87,6 +87,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 {
|
||||||
@ -119,6 +120,7 @@ function QueryBuilderSearchV2(
|
|||||||
className,
|
className,
|
||||||
suffixIcon,
|
suffixIcon,
|
||||||
whereClauseConfig,
|
whereClauseConfig,
|
||||||
|
hardcodedAttributeKeys,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||||
@ -233,7 +235,7 @@ function QueryBuilderSearchV2(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
queryKey: [searchParams],
|
queryKey: [searchParams],
|
||||||
enabled: isQueryEnabled && !isLogsDataSource,
|
enabled: isQueryEnabled && !isLogsDataSource && !hardcodedAttributeKeys,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -674,6 +676,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) => ({
|
||||||
@ -752,6 +766,7 @@ function QueryBuilderSearchV2(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
hardcodedAttributeKeys,
|
||||||
attributeValues?.payload,
|
attributeValues?.payload,
|
||||||
currentFilterItem?.key?.dataType,
|
currentFilterItem?.key?.dataType,
|
||||||
currentState,
|
currentState,
|
||||||
@ -984,6 +999,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,
|
||||||
@ -123,6 +124,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',
|
||||||
|
@ -226,6 +226,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,
|
||||||
ROUTES.CHANNELS_NEW,
|
ROUTES.CHANNELS_NEW,
|
||||||
ROUTES.CHANNELS_EDIT,
|
ROUTES.CHANNELS_EDIT,
|
||||||
ROUTES.WORKSPACE_ACCESS_RESTRICTED,
|
ROUTES.WORKSPACE_ACCESS_RESTRICTED,
|
||||||
|
@ -102,4 +102,5 @@ export interface GetQueryResultsProps {
|
|||||||
};
|
};
|
||||||
start?: number;
|
start?: number;
|
||||||
end?: number;
|
end?: number;
|
||||||
|
step?: number;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
@ -20,6 +20,16 @@ export interface QueryData {
|
|||||||
values: [number, string][];
|
values: [number, string][];
|
||||||
quantity?: number[];
|
quantity?: number[];
|
||||||
unit?: string;
|
unit?: string;
|
||||||
|
table?: {
|
||||||
|
rows: {
|
||||||
|
data: {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
columns: {
|
||||||
|
[key: string]: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SeriesItem {
|
export interface SeriesItem {
|
||||||
|
@ -117,6 +117,7 @@ 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'],
|
||||||
WORKSPACE_ACCESS_RESTRICTED: ['ADMIN', 'EDITOR', 'VIEWER'],
|
WORKSPACE_ACCESS_RESTRICTED: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
METRICS_EXPLORER_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
METRICS_EXPLORER_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
};
|
};
|
||||||
|
@ -11856,10 +11856,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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user