diff --git a/frontend/package.json b/frontend/package.json
index 79a79c9f94..b9dcc6f74f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -90,7 +90,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",
"motion": "12.4.13",
"overlayscrollbars": "^2.8.1",
diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts
index 6e3668b1fa..cb650e772f 100644
--- a/frontend/src/AppRoutes/pageComponents.ts
+++ b/frontend/src/AppRoutes/pageComponents.ts
@@ -295,3 +295,7 @@ export const MetricsExplorer = Loadable(
() =>
import(/* webpackChunkName: "MetricsExplorer" */ 'pages/MetricsExplorer'),
);
+
+export const ApiMonitoring = Loadable(
+ () => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'),
+);
diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts
index 4182e063e4..b0492a8a18 100644
--- a/frontend/src/AppRoutes/routes.ts
+++ b/frontend/src/AppRoutes/routes.ts
@@ -8,6 +8,7 @@ import {
AllAlertChannels,
AllErrors,
APIKeys,
+ ApiMonitoring,
BillingPage,
CreateAlertChannelAlerts,
CreateNewAlerts,
@@ -497,6 +498,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 = {
diff --git a/frontend/src/components/QuickFilters/QuickFilters.tsx b/frontend/src/components/QuickFilters/QuickFilters.tsx
index ffd7ee3a25..fc50363ec1 100644
--- a/frontend/src/components/QuickFilters/QuickFilters.tsx
+++ b/frontend/src/components/QuickFilters/QuickFilters.tsx
@@ -63,30 +63,31 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
return (
- {source !== QuickFiltersSource.INFRA_MONITORING && (
-
-
-
- Filters for
-
- {lastQueryName}
-
-
+ {source !== QuickFiltersSource.INFRA_MONITORING &&
+ source !== QuickFiltersSource.API_MONITORING && (
+
+
+
+ Filters for
+
+ {lastQueryName}
+
+
-
-
- )}
+ )}
{config.map((filter) => {
diff --git a/frontend/src/components/QuickFilters/types.ts b/frontend/src/components/QuickFilters/types.ts
index 0d5766f862..601bfee72a 100644
--- a/frontend/src/components/QuickFilters/types.ts
+++ b/frontend/src/components/QuickFilters/types.ts
@@ -39,4 +39,5 @@ export enum QuickFiltersSource {
LOGS_EXPLORER = 'logs-explorer',
INFRA_MONITORING = 'infra-monitoring',
TRACES_EXPLORER = 'traces-explorer',
+ API_MONITORING = 'api-monitoring',
}
diff --git a/frontend/src/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts
index d6f2ee3693..f96778df2b 100644
--- a/frontend/src/constants/reactQueryKeys.ts
+++ b/frontend/src/constants/reactQueryKeys.ts
@@ -51,6 +51,21 @@ export const REACT_QUERY_KEY = {
GET_METRICS_LIST_FILTER_VALUES: 'GET_METRICS_LIST_FILTER_VALUES',
GET_METRIC_DETAILS: 'GET_METRIC_DETAILS',
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_FUNNEL_DETAILS: 'GET_FUNNEL_DETAILS',
} as const;
diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts
index 226953c045..4ec3c4e3eb 100644
--- a/frontend/src/constants/routes.ts
+++ b/frontend/src/constants/routes.ts
@@ -71,6 +71,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',
METRICS_EXPLORER_BASE: '/metrics-explorer',
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
HOME_PAGE: '/',
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/AllEndPoints.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/AllEndPoints.tsx
new file mode 100644
index 0000000000..be638aa795
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/AllEndPoints.tsx
@@ -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([]);
+
+ 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(
+ (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> =>
+ 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 => (
+
+ );
+
+ 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 (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
Endpoint overview
+
} />,
+ }}
+ dataSource={isLoading || isRefetching ? [] : formattedEndPointsData}
+ locale={{
+ emptyText:
+ isLoading || isRefetching ? null : (
+
+
+

+
+
+ This query had no results. Edit your query and try again!
+
+
+
+ ),
+ }}
+ 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'
+ }
+ />
+
+
+ );
+}
+
+export default AllEndPoints;
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.styles.scss b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.styles.scss
new file mode 100644
index 0000000000..d2fb7fb6c7
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.styles.scss
@@ -0,0 +1,1031 @@
+.domain-details-drawer-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .domain-navigate-cta {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 2px 0px 0px 2px;
+ border: 1px solid var(--bg-slate-400);
+ background: var(--bg-ink-300);
+ box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
+ }
+}
+
+.domain-detail-drawer {
+ border-left: 1px solid var(--bg-slate-500);
+ background: var(--bg-ink-400);
+ box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
+
+ .ant-drawer-header {
+ padding: 8px 16px;
+ border-bottom: none;
+
+ align-items: stretch;
+
+ border-bottom: 1px solid var(--bg-slate-500);
+ background: var(--bg-ink-400);
+ }
+
+ .ant-drawer-close {
+ margin-inline-end: 0px;
+ }
+
+ .ant-drawer-body {
+ display: flex;
+ flex-direction: column;
+ padding: 16px;
+ }
+
+ .title {
+ color: var(--text-vanilla-400);
+ font-family: 'Geist Mono';
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 20px;
+ letter-spacing: -0.07px;
+ }
+
+ .radio-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding-top: var(--padding-1);
+ border: 1px solid var(--bg-slate-400);
+ background: var(--bg-ink-300);
+ box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
+ }
+
+ .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;
+ }
+
+ .domain-detail-drawer__endpoint {
+ .domain-details-grid {
+ .labels-row,
+ .values-row {
+ display: grid;
+ grid-template-columns: 1.5fr 1.5fr 1.5fr 1.5fr;
+ gap: 30px;
+ align-items: center;
+ }
+
+ .labels-row {
+ margin-bottom: 8px;
+ }
+
+ .domain-details-metadata-label {
+ color: var(--text-vanilla-400);
+ font-family: Inter;
+ font-size: 11px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 18px;
+ letter-spacing: 0.44px;
+ text-transform: uppercase;
+ }
+
+ .domain-details-metadata-value {
+ color: var(--text-vanilla-400);
+ font-family: 'Geist Mono';
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 20px;
+ letter-spacing: -0.07px;
+
+ width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .error-rate {
+ width: 100px;
+ padding-right: 8px;
+ }
+
+ .status-tag {
+ margin: 0;
+
+ &.active {
+ color: var(--success-500);
+ background: var(--success-100);
+ border-color: var(--success-500);
+ }
+
+ &.inactive {
+ color: var(--error-500);
+ background: var(--error-100);
+ border-color: var(--error-500);
+ }
+ }
+
+ .progress-container {
+ width: 158px;
+ .ant-progress {
+ margin: 0;
+
+ .ant-progress-text {
+ font-weight: 600;
+ }
+ }
+ }
+
+ .ant-card {
+ &.ant-card-bordered {
+ border: 1px solid var(--bg-slate-500) !important;
+ }
+ }
+ }
+ }
+
+ .views-tabs-container {
+ margin-top: 1.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid var(--bg-slate-400);
+
+ .views-tabs {
+ color: var(--text-vanilla-400);
+
+ .view-title {
+ display: flex;
+ gap: var(--margin-2);
+ align-items: center;
+ justify-content: center;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: var(--font-weight-normal);
+ }
+
+ .tab {
+ padding: 0 32px;
+ background: var(--bg-ink-300);
+ border: 1px solid var(--bg-slate-400);
+ width: auto;
+ }
+
+ .tab::before {
+ background: var(--bg-slate-400);
+ }
+
+ .selected_view {
+ background: none;
+ border: 1px solid var(--bg-slate-400);
+ border-bottom: none;
+ }
+
+ .selected_view::before {
+ background: var(--bg-slate-400);
+ }
+ }
+ }
+}
+
+.all-endpoints-container {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ padding-top: 20px;
+
+ .group-by-container {
+ display: flex;
+ align-items: center;
+
+ .group-by-label {
+ display: flex;
+ padding: 6px 15px;
+ justify-content: center;
+ align-items: center;
+ gap: 4px;
+ flex-shrink: 0;
+
+ border-radius: 2px 0px 0px 2px;
+ border: 1px solid var(--bg-slate-400);
+ border-right: none;
+ background: var(--bg-ink-300);
+
+ color: var(--Vanilla-400, #c0c1c3);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 18px;
+ letter-spacing: -0.07px;
+ }
+
+ .group-by-select {
+ width: 100%;
+ .ant-select-selector {
+ font-size: 14px;
+ border-radius: 2px 0px 0px 2px;
+ border: 1px solid var(--bg-slate-400);
+ background: var(--bg-ink-300);
+ }
+ }
+ }
+
+ .endpoints-table-container {
+ display: flex;
+ flex-direction: column;
+ border-radius: 3px;
+ border: 1px solid var(--bg-slate-500);
+
+ .endpoints-table-header {
+ padding: 12px;
+ color: var(--Vanilla-100, #fff);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 18px; /* 128.571% */
+ }
+
+ .ant-table {
+ .ant-table-thead > tr > th {
+ padding: 12px;
+ font-weight: 500;
+ font-size: 12px;
+ line-height: 18px;
+ border-bottom: none;
+
+ color: var(--text-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(.endpoint-name-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(.endpoint-name-value) {
+ background: var(--bg-ink-300);
+ opacity: 0.6;
+ }
+
+ .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 {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 4px;
+ }
+
+ .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);
+ }
+
+ .endpoint-name-value {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ .no-filtered-endpoints-message-container {
+ height: 30vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ .no-filtered-endpoints-message-content {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+
+ width: fit-content;
+ padding: 24px;
+ }
+
+ .no-filtered-endpoints-message {
+ margin-top: 8px;
+ }
+ }
+ }
+}
+
+.all-endpoints-error-state-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ padding-top: 20px;
+}
+
+.endpoint-details-container {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ padding-top: 20px;
+
+ .endpoint-details-filters-container {
+ display: flex;
+ flex-direction: row;
+
+ .endpoint-details-filters-container-dropdown {
+ width: 120px;
+ }
+
+ .endpoint-details-filters-container-search {
+ flex: 1;
+ }
+ }
+
+ .status-code-table-container {
+ border-radius: 3px;
+ border: 1px solid var(--bg-slate-500);
+ .ant-table {
+ .ant-table-thead > tr > th {
+ padding: 12px;
+ font-weight: 500;
+ font-size: 12px;
+ line-height: 18px;
+ border-bottom: none;
+
+ color: var(--text-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(.status-code-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(.status-code-value) {
+ background: var(--bg-ink-300);
+ opacity: 0.6;
+ }
+
+ .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 {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 4px;
+ }
+
+ .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);
+ }
+
+ .endpoint-name-value {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ .no-status-code-data-message-container {
+ height: 30vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ .no-status-code-data-message-content {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+
+ width: fit-content;
+ padding: 24px;
+ }
+
+ .no-status-code-data-message {
+ margin-top: 8px;
+ }
+ }
+ }
+}
+
+.error-state-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+ padding: 16px 0px 16px 0px;
+
+ border-radius: 6px;
+ border: 1px dashed var(--bg-slate-500);
+ font-family: 'Inter';
+
+ .error-state-content-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ .error-state-content {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+
+ .error-state-text {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ }
+ }
+
+ .refresh-cta {
+ display: inline-flex;
+ border-radius: 2px;
+ background: var(--bg-slate-400);
+ border: none;
+ align-items: center;
+ width: fit-content;
+ }
+ }
+}
+
+.endpoint-details-card {
+ margin: 8px 0 1rem 0;
+ height: 370px;
+ padding: 10px;
+
+ border: 1px solid var(--bg-slate-500);
+
+ .header {
+ display: flex;
+ justify-content: space-between;
+ }
+ .graph-container {
+ height: 300px;
+ }
+
+ .ant-card-body {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 0;
+ height: 100%;
+
+ color: var(--text-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 18px;
+ }
+
+ .chart-container {
+ width: 100%;
+ height: 100%;
+ }
+
+ .no-data-container {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ .views-tabs {
+ color: var(--text-vanilla-400);
+
+ .ant-btn {
+ box-shadow: none;
+ }
+
+ .tab {
+ border: 1px solid var(--bg-slate-400);
+ width: 114px;
+ }
+
+ .tab::before {
+ background: var(--bg-ink-400);
+ }
+
+ .selected_view {
+ background: var(--bg-slate-400);
+ color: var(--text-vanilla-100);
+ border: 1px solid var(--bg-slate-400);
+ }
+
+ .selected_view::before {
+ background: var(--bg-ink-400);
+ }
+ }
+}
+
+.top-services-content {
+ display: flex;
+ flex-direction: column;
+ font-family: 'Geist Mono' !important;
+
+ border-radius: 3px;
+ border: 1px solid var(--bg-slate-500);
+
+ .top-services-title {
+ border-bottom: 1px solid var(--bg-slate-500);
+ padding: 10px 12px;
+ border-radius: 3px 3px 0px 0px;
+ background: rgba(171, 189, 255, 0.04);
+
+ .title-wrapper {
+ display: inline-flex;
+ padding: 1px 2px;
+ align-items: center;
+ border-radius: 2px;
+ background: rgba(113, 144, 249, 0.08);
+
+ color: var(--bg-robin-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 18px;
+ letter-spacing: -0.07px;
+ }
+ }
+ .dependent-services-container {
+ padding: 10px 12px;
+ .top-services-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ .top-services-item-progress {
+ display: flex;
+ gap: 12px;
+ height: 34px;
+ width: 100%;
+ color: var(--bg-vanilla-100);
+ padding: 0 12px;
+ margin-right: 12px;
+ align-items: center;
+ position: relative;
+
+ .top-services-item-key {
+ z-index: 2;
+ }
+
+ .top-services-item-count {
+ background-color: var(--bg-slate-300);
+ border-radius: 50px;
+ padding: 2px 6px;
+ color: var(--bg-vanilla-400);
+ z-index: 2;
+ }
+
+ .top-services-item-progress-bar {
+ background-color: var(--bg-slate-400);
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+ }
+ }
+
+ .top-services-item-percentage {
+ color: var(--bg-vanilla-400);
+ z-index: 2;
+ }
+
+ .top-services-load-more {
+ border-top: 1px solid var(--bg-slate-500);
+ padding-top: 10px;
+
+ color: var(--text-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px;
+ letter-spacing: -0.07px;
+
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ cursor: pointer;
+ }
+
+ .no-dependent-services-message-container {
+ height: 30vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ .no-dependent-services-message-content {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+
+ width: fit-content;
+ padding: 24px;
+ }
+
+ .no-dependent-services-message {
+ margin-top: 8px;
+ }
+ }
+ }
+}
+
+.end-point-details-zero-state-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ height: 100%;
+
+ .end-point-details-zero-state-content {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+ gap: 4px;
+ height: 100%;
+ }
+
+ .end-point-details-zero-state-content-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: flex-start;
+ gap: 12px;
+
+ .end-point-details-zero-state-text-content {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+ gap: 8px;
+
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px;
+ letter-spacing: -0.07px;
+
+ .title {
+ color: var(--text-vanilla-100);
+ }
+
+ .description {
+ color: var(--text-vanilla-400);
+ }
+ }
+
+ .ant-select {
+ width: 100%;
+ }
+ }
+}
+
+.lightMode {
+ .ant-drawer-header {
+ border-bottom: 1px solid var(--bg-vanilla-400);
+ background: var(--bg-vanilla-100);
+ }
+
+ .domain-navigate-cta {
+ border: 1px solid var(--bg-vanilla-400);
+ background: var(--bg-vanilla-100);
+ color: var(--text-ink-300);
+ }
+
+ .domain-detail-drawer {
+ .title {
+ color: var(--text-ink-300);
+ }
+
+ .domain-detail-drawer__endpoint {
+ .ant-typography {
+ color: var(--text-ink-300);
+ background: transparent;
+ }
+ }
+
+ .radio-button {
+ border: 1px solid var(--bg-vanilla-400);
+ background: var(--bg-vanilla-100);
+ color: var(--text-ink-300);
+ }
+
+ .views-tabs {
+ .tab {
+ background: var(--bg-vanilla-100);
+ }
+
+ .selected_view {
+ background: var(--bg-vanilla-300);
+ border: 1px solid var(--bg-slate-300);
+ color: var(--text-ink-400);
+ }
+
+ .selected_view::before {
+ background: var(--bg-vanilla-300);
+ border-left: 1px solid var(--bg-slate-300);
+ }
+ }
+ }
+
+ .round-metric-tag {
+ color: var(--bg-vanilla-100);
+ }
+
+ .group-by-container {
+ .group-by-label {
+ background: var(--bg-vanilla-100);
+ color: var(--text-ink-300);
+ }
+
+ .group-by-select {
+ width: 100%;
+ .ant-select-selector {
+ background: var(--bg-vanilla-100);
+ }
+ }
+ }
+
+ .endpoints-table-container {
+ .ant-table {
+ .ant-table-thead > tr > th {
+ background: var(--bg-vanilla-100);
+ color: var(--text-ink-300);
+ }
+
+ .ant-table-thead > tr > th:has(.endpoint-name-header) {
+ background: var(--bg-vanilla-100);
+ }
+
+ .ant-table-cell {
+ background: var(--bg-vanilla-100);
+ color: var(--bg-ink-500);
+ }
+
+ .ant-table-cell:has(.endpoint-name-value) {
+ background: var(--bg-vanilla-100);
+ }
+
+ .ant-table-tbody > tr:hover > td {
+ background: rgba(0, 0, 0, 0.04);
+ }
+
+ .table-row-light {
+ background: none;
+ color: var(--bg-ink-500);
+ }
+
+ .table-row-dark {
+ background: none;
+ color: var(--bg-ink-500);
+ }
+
+ .round-metric-tag {
+ color: var(--bg-vanilla-100);
+ }
+ }
+ }
+
+ .status-code-table-container {
+ .ant-table {
+ .ant-table-thead > tr > th {
+ background: var(--bg-vanilla-100);
+ color: var(--text-ink-300);
+ }
+
+ .ant-table-cell {
+ background: var(--bg-vanilla-100);
+ color: var(--bg-ink-500);
+ }
+
+ .ant-table-thead > tr > th:has(.status-code-header) {
+ background: var(--bg-vanilla-100);
+ color: var(--text-ink-300);
+ }
+
+ .ant-table-cell {
+ color: var(--text-ink-300);
+ }
+
+ .ant-table-cell:has(.status-code-value) {
+ background: var(--bg-vanilla-100);
+ color: var(--text-ink-300);
+ }
+
+ .ant-table-tbody > tr:hover > td {
+ background: rgba(0, 0, 0, 0.04);
+ }
+
+ .table-row-light {
+ background: none;
+ color: var(--bg-ink-500);
+ }
+
+ .table-row-dark {
+ background: none;
+ color: var(--bg-ink-500);
+ }
+
+ .round-metric-tag {
+ color: var(--bg-vanilla-100);
+ }
+ }
+ }
+
+ .dependent-services-container {
+ padding: 10px 12px;
+ .top-services-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ .top-services-item-progress {
+ display: flex;
+ gap: 12px;
+ height: 34px;
+ width: 100%;
+ color: var(--bg-vanilla-100);
+ padding: 0 12px;
+ margin-right: 12px;
+ align-items: center;
+ position: relative;
+
+ .top-services-item-key {
+ color: var(--text-ink-300);
+ }
+
+ .top-services-item-count {
+ background-color: var(--bg-slate-300);
+ color: var(--bg-vanilla-400);
+ }
+
+ .top-services-item-progress-bar {
+ background-color: var(--bg-vanilla-300);
+ border: 1px solid var(--bg-slate-300);
+ }
+ }
+ }
+
+ .top-services-item-percentage {
+ color: var(--text-ink-300);
+ }
+
+ .top-services-load-more {
+ color: var(--text-ink-300);
+ }
+ }
+
+ .error-state-container {
+ .error-state-content-wrapper {
+ .refresh-cta {
+ background: var(--bg-vanilla-100);
+ color: var(--text-ink-300);
+ border: 1px solid var(--bg-slate-300);
+ }
+ }
+ }
+
+ .end-point-details-zero-state-wrapper {
+ .end-point-details-zero-state-content-wrapper {
+ .end-point-details-zero-state-text-content {
+ .title {
+ color: var(--text-ink-300);
+ }
+
+ .description {
+ color: var(--text-ink-100);
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.tsx
new file mode 100644
index 0000000000..70109e9df0
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.tsx
@@ -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.ALL_ENDPOINTS);
+ const [selectedEndPointName, setSelectedEndPointName] = useState('');
+ const [endPointsGroupBy, setEndPointsGroupBy] = useState<
+ IBuilderQuery['groupBy']
+ >([]);
+ const isDarkMode = useIsDarkMode();
+
+ const handleTabChange = (e: RadioChangeEvent): void => {
+ setSelectedView(e.target.value);
+ };
+
+ return (
+
+
+
+
+ {domainData.domainName}
+
+
+
+
+
+ }
+ 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={}
+ >
+ {domainData && (
+ <>
+
+
+
+
+ All Endpoints
+
+
+ Endpoint Details
+
+
+
+ {selectedView === VIEW_TYPES.ALL_ENDPOINTS && (
+
+ )}
+
+ {selectedView === VIEW_TYPES.ENDPOINT_DETAILS && (
+
+ )}
+ >
+ )}
+
+ );
+}
+
+export default DomainDetails;
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/EndPointDetails.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/EndPointDetails.tsx
new file mode 100644
index 0000000000..4b8959b4bb
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/EndPointDetails.tsx
@@ -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(
+ (state) => state.globalTime,
+ );
+
+ const currentQuery = initialQueriesMap[DataSource.TRACES];
+
+ const [filters, setFilters] = useState({
+ 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> =>
+ 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 (
+
+
+
+
+
+
+ {
+ setFilters(searchFilters);
+ }}
+ placeholder="Search for filters..."
+ />
+
+
+
+ {!isServicesFilterApplied && (
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default EndPointDetails;
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/EndPointDetailsWrapper.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/EndPointDetailsWrapper.tsx
new file mode 100644
index 0000000000..cdbdc7e4ea
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/EndPointDetailsWrapper.tsx
@@ -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(
+ (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> =>
+ GetMetricQueryRange(payload, ENTITY_VERSION_V4),
+ enabled: !!payload,
+ })),
+ );
+
+ const [endPointZeroStateDataQuery] = useMemo(
+ () => [endPointZeroStateDataQueries[0]],
+ [endPointZeroStateDataQueries],
+ );
+
+ if (endPointName === '') {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+export default EndPointDetailsWrapper;
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/DependentServices.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/DependentServices.tsx
new file mode 100644
index 0000000000..f6b31787b4
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/DependentServices.tsx
@@ -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, 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 ;
+ }
+
+ if (isError) {
+ return ;
+ }
+
+ return (
+
+
+ Dependent Services
+
+
+ {renderItems.length === 0 ? (
+
+
+

+
+
+ This query had no results. Edit your query and try again!
+
+
+
+ ) : (
+ renderItems.map((item) => (
+
+
+
{item.serviceName}
+
{item.count}
+
+
+
+ {item.percentage.toFixed(2)}%
+
+
+ ))
+ )}
+
+ {currentRenderCount < dependentServicesData.length && (
+
setCurrentRenderCount(dependentServicesData.length)}
+ onKeyDown={(e): void => {
+ if (e.key === 'Enter') {
+ setCurrentRenderCount(dependentServicesData.length);
+ }
+ }}
+ role="button"
+ tabIndex={0}
+ >
+
+ Show more...
+
+ )}
+
+
+ );
+}
+
+export default DependentServices;
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/DomainMetrics.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/DomainMetrics.tsx
new file mode 100644
index 0000000000..c390fb96eb
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/DomainMetrics.tsx
@@ -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 (
+
+
+
+
+ EXTERNAL API
+
+
+ AVERAGE LATENCY
+
+
+ ERROR RATE
+
+
+ LAST USED
+
+
+
+
+
+
+ {domainData.endpointCount}
+
+
+ {/* // update the tooltip as well */}
+
+
+
+ {(domainData.latency / 1000).toFixed(3)}s
+
+
+
+ {/* // update the tooltip as well */}
+
+
+
+
+ {/* // update the tooltip as well */}
+
+
+ {getLastUsedRelativeTime(domainData.lastUsed)}
+
+
+
+
+
+ );
+}
+
+export default DomainMetrics;
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/EndPointDetailsZeroState.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/EndPointDetailsZeroState.tsx
new file mode 100644
index 0000000000..981dfdaac0
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/EndPointDetailsZeroState.tsx
@@ -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>;
+}): JSX.Element {
+ return (
+
+
+

+
+
+
No endpoint selected yet
+
Select an endpoint to see the details
+
+
+
+
+
+ );
+}
+
+export default EndPointDetailsZeroState;
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/EndPointMetrics.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/EndPointMetrics.tsx
new file mode 100644
index 0000000000..dd60b89a59
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/EndPointMetrics.tsx
@@ -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, 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 ;
+ }
+
+ return (
+
+
+
+
+ Rate
+
+
+ AVERAGE LATENCY
+
+
+ ERROR RATE
+
+
+ LAST USED
+
+
+
+
+
+ {isLoading || isRefetching ? (
+
+ ) : (
+
+ {metricsData?.rate}/sec
+
+ )}
+
+
+ {isLoading || isRefetching ? (
+
+ ) : (
+
+ {metricsData?.latency}ms
+
+ )}
+
+
+ {isLoading || isRefetching ? (
+
+ ) : (
+
+
+ )}
+
+
+ {isLoading || isRefetching ? (
+
+ ) : (
+ {metricsData?.lastUsed}
+ )}
+
+
+
+
+ );
+}
+
+export default EndPointMetrics;
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/EndPointsDropDown.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/EndPointsDropDown.tsx
new file mode 100644
index 0000000000..5cb4f30670
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/EndPointsDropDown.tsx
@@ -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, 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 (
+
+ );
+}
+
+EndPointsDropDown.defaultProps = defaultProps;
+
+export default EndPointsDropDown;
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/ErrorState.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/ErrorState.tsx
new file mode 100644
index 0000000000..929102216c
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/ErrorState.tsx
@@ -0,0 +1,31 @@
+import { Button, Typography } from 'antd';
+import { RotateCw } from 'lucide-react';
+
+function ErrorState({ refetch }: { refetch: () => void }): JSX.Element {
+ return (
+
+
+
+
+

+
+
+ Uh-oh :/ We ran into an error.
+
+ Please refresh this panel.
+
+
+
+
refetch()}
+ icon={}
+ >
+ Refresh this panel
+
+
+
+ );
+}
+
+export default ErrorState;
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/ExpandedRow.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/ExpandedRow.tsx
new file mode 100644
index 0000000000..e8cf25c93d
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/ExpandedRow.tsx
@@ -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(
+ (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> =>
+ GetMetricQueryRange(payload, ENTITY_VERSION_V4),
+ enabled: !!payload && !!selectedRowData,
+ }))
+ : [],
+ );
+
+ const groupedByRowQuery = groupedByRowQueries[0];
+ return (
+
+ {groupedByRowQuery?.isFetching || groupedByRowQuery?.isLoading ? (
+
+ ) : (
+
+
[]}
+ 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: } />,
+ }}
+ onRow={(record): { onClick: () => void; className: string } => ({
+ onClick: (): void => {
+ setSelectedEndPointName(record.endpointName);
+ setSelectedView(VIEW_TYPES.ENDPOINT_DETAILS);
+ },
+ className: 'expanded-clickable-row',
+ })}
+ />
+
+ )}
+
+ );
+}
+
+export default ExpandedRow;
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/MetricOverTimeGraph.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/MetricOverTimeGraph.tsx
new file mode 100644
index 0000000000..fd4b3bb262
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/MetricOverTimeGraph.tsx
@@ -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, unknown>;
+ widgetInfoIndex: number;
+ endPointName: string;
+}): JSX.Element {
+ const { data } = metricOverTimeDataQuery;
+
+ const { minTime, maxTime } = useSelector(
+ (state) => state.globalTime,
+ );
+
+ const graphRef = useRef(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, unknown>): JSX.Element => {
+ if (query.isLoading) {
+ return ;
+ }
+
+ if (query.error) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+ },
+ [options, chartData],
+ );
+
+ return (
+
+
+ {apiWidgetInfo[widgetInfoIndex].title}
+
+ {renderCardContent(metricOverTimeDataQuery)}
+
+
+
+ );
+}
+
+export default MetricOverTimeGraph;
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/StatusCodeBarCharts.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/StatusCodeBarCharts.tsx
new file mode 100644
index 0000000000..bba17d1f21
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/StatusCodeBarCharts.tsx
@@ -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,
+ unknown
+ >;
+ endPointStatusCodeLatencyBarChartsDataQuery: UseQueryResult<
+ SuccessResponse,
+ 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(
+ (state) => state.globalTime,
+ );
+
+ const graphRef = useRef(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, unknown>): JSX.Element => {
+ if (query.isLoading) {
+ return ;
+ }
+
+ if (query.error) {
+ return ;
+ }
+ return (
+
+
+
+ );
+ },
+ [options, chartData],
+ );
+
+ return (
+
+
+
+ Call response status
+
+ setCurrentWidgetInfoIndex(0)}
+ >
+ Number of calls
+
+ setCurrentWidgetInfoIndex(1)}
+ >
+ Latency
+
+
+
+
+ {renderCardContent(endPointStatusCodeBarChartsDataQuery)}
+
+
+
+ );
+}
+export default StatusCodeBarCharts;
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/StatusCodeTable.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/StatusCodeTable.tsx
new file mode 100644
index 0000000000..02a31ce27e
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/StatusCodeTable.tsx
@@ -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, 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 ;
+ }
+
+ return (
+
+
+ index % 2 === 0 ? 'table-row-dark' : 'table-row-light'
+ }
+ locale={{
+ emptyText:
+ isLoading || isRefetching ? null : (
+
+
+

+
+
+ This query had no results. Edit your query and try again!
+
+
+
+ ),
+ }}
+ />
+
+ );
+}
+
+export default StatusCodeTable;
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/constants.ts b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/constants.ts
new file mode 100644
index 0000000000..9b3314d855
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/constants.ts
@@ -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,
+};
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainList.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainList.tsx
new file mode 100644
index 0000000000..b326feb519
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainList.tsx
@@ -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(-1);
+ const { maxTime, minTime } = useSelector(
+ (state) => state.globalTime,
+ );
+
+ const fetchApiOverview = async (): Promise<
+ SuccessResponse | 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 (
+
+
+
+ handleChangeQueryData('filters', searchFilters)
+ }
+ placeholder="Search filters..."
+ hardcodedAttributeKeys={hardcodedAttributeKeys}
+ />
+
+
+ } />,
+ }}
+ locale={{
+ emptyText:
+ isFetching || isLoading ? null : (
+
+
+

+
+
+ This query had no results. Edit your query and try again!
+
+
+
+ ),
+ }}
+ 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 && (
+ {
+ setSelectedDomainIndex(-1);
+ }}
+ />
+ )}
+
+ );
+}
+
+export default DomainList;
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Explorer.styles.scss b/frontend/src/container/ApiMonitoring/Explorer/Explorer.styles.scss
new file mode 100644
index 0000000000..f05767113e
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Explorer.styles.scss
@@ -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);
+ }
+ }
+ }
+}
diff --git a/frontend/src/container/ApiMonitoring/Explorer/Explorer.tsx b/frontend/src/container/ApiMonitoring/Explorer/Explorer.tsx
new file mode 100644
index 0000000000..5a1de332c1
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/Explorer/Explorer.tsx
@@ -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(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 (
+ }>
+
+
+
+
+ Filters
+
+
+
+ Show IP addresses
+ {
+ setShowIP((showIP) => !showIP);
+ }}
+ />
+
+
+ {}}
+ onFilterChange={(query: Query): void =>
+ handleChangeQueryData('filters', query.builder.queryData[0].filters)
+ }
+ />
+
+
+
+
+ );
+}
+
+export default Explorer;
diff --git a/frontend/src/container/ApiMonitoring/utils.tsx b/frontend/src/container/ApiMonitoring/utils.tsx
new file mode 100644
index 0000000000..1706934708
--- /dev/null
+++ b/frontend/src/container/ApiMonitoring/utils.tsx
@@ -0,0 +1,2087 @@
+/* eslint-disable sonarjs/no-duplicate-string */
+import { Color } from '@signozhq/design-tokens';
+import { Progress, Tag } from 'antd';
+import { ColumnType } from 'antd/es/table';
+import {
+ FiltersType,
+ IQuickFiltersConfig,
+} from 'components/QuickFilters/types';
+import { PANEL_TYPES } from 'constants/queryBuilder';
+import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
+import dayjs from 'dayjs';
+import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
+import { cloneDeep } from 'lodash-es';
+import { ArrowUpDown, ChevronDown, ChevronRight } from 'lucide-react';
+import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
+import {
+ BaseAutocompleteData,
+ DataTypes,
+} from 'types/api/queryBuilder/queryAutocompleteResponse';
+import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
+import { QueryData } from 'types/api/widgets/getQuery';
+import { EQueryType } from 'types/common/dashboard';
+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',
+ 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,
+ },
+];
+
+export const getLastUsedRelativeTime = (lastRefresh: number): string => {
+ const currentTime = dayjs();
+
+ const secondsDiff = currentTime.diff(lastRefresh, 'seconds');
+
+ const minutedDiff = currentTime.diff(lastRefresh, 'minutes');
+ const hoursDiff = currentTime.diff(lastRefresh, 'hours');
+ const daysDiff = currentTime.diff(lastRefresh, 'days');
+ const monthsDiff = currentTime.diff(lastRefresh, 'months');
+
+ if (monthsDiff > 0) {
+ return `${monthsDiff} ${monthsDiff === 1 ? 'month' : 'months'} ago`;
+ }
+
+ if (daysDiff > 0) {
+ return `${daysDiff} ${daysDiff === 1 ? 'day' : 'days'} ago`;
+ }
+
+ if (hoursDiff > 0) {
+ return `${hoursDiff}h ago`;
+ }
+
+ if (minutedDiff > 0) {
+ return `${minutedDiff}m ago`;
+ }
+
+ return `${secondsDiff}s ago`;
+};
+
+// Rename this to a proper name
+export const columnsConfig: ColumnType[] = [
+ {
+ title: Domain
,
+ dataIndex: 'domainName',
+ key: 'domainName',
+ width: '23.7%',
+ ellipsis: true,
+ sorter: false,
+ className: 'column column-domain-name',
+ align: 'left',
+ render: (domainName: string): React.ReactNode => (
+ {domainName}
+ ),
+ },
+ {
+ title: Endpoints in use
,
+ dataIndex: 'endpointCount',
+ key: 'endpointCount',
+ width: '14.2%',
+ ellipsis: true,
+ sorter: false,
+ align: 'right',
+ className: `column`,
+ },
+ {
+ title: Last used
,
+ dataIndex: 'lastUsed',
+ key: 'lastUsed',
+ width: '14.2%',
+ sorter: false,
+ align: 'right',
+ className: `column`,
+ render: (lastUsed: number): string => getLastUsedRelativeTime(lastUsed),
+ },
+ {
+ title: (
+
+ Rate /s
+
+ ),
+ dataIndex: 'rate',
+ key: 'rate',
+ width: '14.2%',
+ sorter: false,
+ align: 'right',
+ className: `column`,
+ },
+ {
+ title: (
+
+ Error rate %
+
+ ),
+ dataIndex: 'errorRate',
+ key: 'errorRate',
+ width: '14.2%',
+ sorter: false,
+ align: 'right',
+ className: `column`,
+ render: (errorRate: number): React.ReactNode => (
+