feat: added custom cell rendering in gridcard, added new table view in all endpoints

This commit is contained in:
sawhil 2025-04-25 20:37:41 +05:30 committed by Sahil Khan
parent 552b103e8b
commit d6e4e3c5ed
12 changed files with 385 additions and 179 deletions

View File

@ -1,34 +1,12 @@
import { LoadingOutlined } from '@ant-design/icons';
import {
Select,
Spin,
Table,
TablePaginationConfig,
TableProps,
Typography,
} from 'antd';
import { SorterResult } from 'antd/lib/table/interface';
import logEvent from 'api/common/logEvent';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
EndPointsTableRowData,
formatEndPointsDataForTable,
getEndPointsColumnsConfig,
getEndPointsQueryPayload,
} from 'container/ApiMonitoring/utils';
import { Select } from 'antd';
import { getAllEndpointsWidgetData } from 'container/ApiMonitoring/utils';
import GridCard from 'container/GridCardLayout/GridCard';
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 { 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 ErrorState from './components/ErrorState';
import ExpandedRow from './components/ExpandedRow';
import { VIEW_TYPES, VIEWS } from './constants';
import { VIEWS } from './constants';
function AllEndPoints({
domainName,
@ -63,13 +41,6 @@ function AllEndPoints({
{ value: string; label: string }[]
>([]);
const [orderBy, setOrderBy] = useState<{
columnName: string;
order: 'asc' | 'desc';
} | null>(null);
const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>([]);
const handleGroupByChange = useCallback(
(value: IBuilderQuery['groupBy']) => {
const groupBy = [];
@ -101,109 +72,11 @@ function AllEndPoints({
}
}, [groupByFiltersData]);
const { startTime: minTime, endTime: maxTime } = timeRange;
const queryPayloads = useMemo(
() => getEndPointsQueryPayload(groupBy, domainName, minTime, maxTime),
[groupBy, domainName, minTime, maxTime],
const allEndpointsWidgetData = useMemo(
() => getAllEndpointsWidgetData(groupBy, domainName),
[groupBy, domainName],
);
// 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}
orderBy={orderBy}
/>
);
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_STATS);
logEvent('API Monitoring: Endpoint name row clicked', {});
} else {
handleGroupByRowClick(record); // this will prepare the nested query payload
}
};
const handleTableChange: TableProps<EndPointsTableRowData>['onChange'] = useCallback(
(
_pagination: TablePaginationConfig,
_filters: Record<string, (string | number | boolean)[] | null>,
sorter:
| SorterResult<EndPointsTableRowData>
| SorterResult<EndPointsTableRowData>[],
): void => {
if ('field' in sorter && sorter.order) {
setOrderBy({
columnName: sorter.field as string,
order: sorter.order === 'ascend' ? 'asc' : 'desc',
});
} else {
setOrderBy(null);
}
},
[],
);
const formattedEndPointsData = useMemo(
() =>
formatEndPointsDataForTable(
allEndPointsData?.payload?.data?.result[0]?.table?.rows,
groupBy,
orderBy,
),
[groupBy, allEndPointsData, orderBy],
);
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">
@ -221,47 +94,16 @@ function AllEndPoints({
/>{' '}
</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 />} />,
<GridCard
widget={allEndpointsWidgetData}
isQueryEnabled
onDragSelect={(): void => {}}
customOnDragSelect={(): void => {}}
customTimeRange={timeRange}
customOnRowClick={(props): void => {
setSelectedEndPointName(props['http.url'] as string);
setSelectedView(VIEWS.ENDPOINT_STATS);
}}
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'
}
onChange={handleTableChange}
/>
</div>
</div>

View File

@ -3276,6 +3276,329 @@ export const END_POINT_DETAILS_QUERY_KEYS_ARRAY = [
REACT_QUERY_KEY.GET_ENDPOINT_STATUS_CODE_LATENCY_BAR_CHARTS_DATA,
];
export const getAllEndpointsWidgetData = (
groupBy: BaseAutocompleteData[],
domainName: string,
// eslint-disable-next-line sonarjs/cognitive-complexity
): Widgets => {
const isGroupedByAttribute = groupBy.length > 0;
const widget = getWidgetQueryBuilder(
getWidgetQuery({
title: 'Endpoint Overview',
description: 'Endpoint Overview',
panelTypes: PANEL_TYPES.TABLE,
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: 'span_id',
type: '',
},
aggregateOperator: 'count',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'A',
filters: {
items: [
{
id: 'ec316e57',
key: {
dataType: DataTypes.String,
id: 'net.peer.name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'net.peer.name',
type: 'tag',
},
op: '=',
value: domainName,
},
{
id: '212678b9',
key: {
key: 'kind_string',
dataType: DataTypes.String,
type: '',
isColumn: true,
isJSON: false,
id: 'kind_string--string----true',
},
op: '=',
value: 'Client',
},
],
op: 'AND',
},
functions: [],
groupBy: isGroupedByAttribute
? [...defaultGroupBy, ...groupBy]
: defaultGroupBy,
having: [],
legend: 'Num of Calls',
limit: 1000,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'count',
},
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
aggregateOperator: 'p99',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'B',
filters: {
items: [
{
id: '46d57857',
key: {
dataType: DataTypes.String,
id: 'net.peer.name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'net.peer.name',
type: 'tag',
},
op: '=',
value: domainName,
},
{
id: '212678b9',
key: {
key: 'kind_string',
dataType: DataTypes.String,
type: '',
isColumn: true,
isJSON: false,
id: 'kind_string--string----true',
},
op: '=',
value: 'Client',
},
],
op: 'AND',
},
functions: [],
groupBy: isGroupedByAttribute
? [...defaultGroupBy, ...groupBy]
: defaultGroupBy,
having: [],
legend: 'Latency (ms)',
limit: 1000,
orderBy: [],
queryName: 'B',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'p99',
},
{
aggregateAttribute: {
dataType: DataTypes.String,
id: 'timestamp------false',
isColumn: false,
key: 'timestamp',
type: '',
},
aggregateOperator: 'max',
dataSource: DataSource.TRACES,
disabled: false,
expression: 'C',
filters: {
items: [
{
id: '4a237616',
key: {
dataType: DataTypes.String,
id: 'net.peer.name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'net.peer.name',
type: 'tag',
},
op: '=',
value: domainName,
},
{
id: '212678b9',
key: {
key: 'kind_string',
dataType: DataTypes.String,
type: '',
isColumn: true,
isJSON: false,
id: 'kind_string--string----true',
},
op: '=',
value: 'Client',
},
],
op: 'AND',
},
functions: [],
groupBy: isGroupedByAttribute
? [...defaultGroupBy, ...groupBy]
: defaultGroupBy,
having: [],
legend: 'Last Used',
limit: 1000,
orderBy: [],
queryName: 'C',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'max',
},
{
aggregateAttribute: {
dataType: DataTypes.String,
id: 'span_id--string----true',
isColumn: true,
isJSON: false,
key: 'span_id',
type: '',
},
aggregateOperator: 'count',
dataSource: DataSource.TRACES,
disabled: true,
expression: 'D',
filters: {
items: [
{
id: 'f162de1e',
key: {
dataType: DataTypes.String,
id: 'net.peer.name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'net.peer.name',
type: 'tag',
},
op: '=',
value: domainName,
},
{
id: '3df0ac1d',
key: {
dataType: DataTypes.bool,
id: 'has_error--bool----true',
isColumn: true,
isJSON: false,
key: 'has_error',
type: '',
},
op: '=',
value: 'true',
},
{
id: '212678b9',
key: {
key: 'kind_string',
dataType: DataTypes.String,
type: '',
isColumn: true,
isJSON: false,
id: 'kind_string--string----true',
},
op: '=',
value: 'Client',
},
],
op: 'AND',
},
functions: [],
groupBy: isGroupedByAttribute
? [...defaultGroupBy, ...groupBy]
: defaultGroupBy,
having: [],
legend: '',
limit: 1000,
orderBy: [],
queryName: 'D',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'count',
},
],
queryFormulas: [
{
queryName: 'F1',
expression: '(D/A)*100',
disabled: false,
legend: 'error percentage',
},
],
yAxisUnit: 'ops/s',
}),
);
widget.renderColumnCell = {
A: (numOfCalls: any): ReactNode => (
<span>
{numOfCalls === 'n/a' || numOfCalls === undefined ? '-' : numOfCalls}
</span>
),
B: (latency: any): ReactNode => (
<span>
{latency === 'n/a' || latency === undefined
? '-'
: `${Math.round(Number(latency) / 1000000)} ms`}
</span>
),
C: (lastUsed: any): ReactNode => (
<span>
{lastUsed === 'n/a' || lastUsed === undefined
? '-'
: getLastUsedRelativeTime(
new Date(
new Date(Math.floor(Number(lastUsed) / 1000000)).toISOString(),
).getTime(),
)}
</span>
),
F1: (errorRate: any): ReactNode => (
<Progress
status="active"
percent={Number(
((errorRate === 'n/a' || errorRate === '-'
? 0
: errorRate) as number).toFixed(1),
)}
strokeLinecap="butt"
size="small"
strokeColor={((): // eslint-disable-next-line sonarjs/no-identical-functions
string => {
const errorRatePercent = Number(
((errorRate === 'n/a' || errorRate === '-'
? 0
: errorRate) as number).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 error-rate"
/>
),
};
return widget;
};
export const getRateOverTimeWidgetData = (
domainName: string,
endPointName: string,

View File

@ -56,6 +56,7 @@ function WidgetGraphComponent({
onOpenTraceBtnClick,
customSeries,
customErrorMessage,
customOnRowClick,
}: WidgetGraphComponentProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [deleteModal, setDeleteModal] = useState(false);
@ -380,6 +381,7 @@ function WidgetGraphComponent({
openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick}
customSeries={customSeries}
customOnRowClick={customOnRowClick}
/>
</div>
)}

View File

@ -48,6 +48,7 @@ function GridCardGraph({
end,
analyticsEvent,
customTimeRange,
customOnRowClick,
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>();
@ -287,6 +288,7 @@ function GridCardGraph({
onOpenTraceBtnClick={onOpenTraceBtnClick}
customSeries={customSeries}
customErrorMessage={isInternalServerError ? customErrorMessage : undefined}
customOnRowClick={customOnRowClick}
/>
)}
</div>

View File

@ -39,6 +39,7 @@ export interface WidgetGraphComponentProps {
onOpenTraceBtnClick?: (record: RowData) => void;
customSeries?: (data: QueryData[]) => uPlot.Series[];
customErrorMessage?: string;
customOnRowClick?: (record: RowData) => void;
}
export interface GridCardGraphProps {
@ -65,6 +66,7 @@ export interface GridCardGraphProps {
startTime: number;
endTime: number;
};
customOnRowClick?: (record: RowData) => void;
}
export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@ -43,6 +43,7 @@ function GridTableComponent({
sticky,
openTracesButton,
onOpenTraceBtnClick,
customOnRowClick,
widgetId,
...props
}: GridTableComponentProps): JSX.Element {
@ -214,6 +215,18 @@ function GridTableComponent({
[newColumnData],
);
const newColumnsWithRenderColumnCell = useMemo(
() =>
newColumnData.map((column) => ({
...column,
...('dataIndex' in column &&
props.renderColumnCell?.[column.dataIndex as string]
? { render: props.renderColumnCell[column.dataIndex as string] }
: {}),
})),
[newColumnData, props.renderColumnCell],
);
useEffect(() => {
eventEmitter.emit(Events.TABLE_COLUMNS_DATA, {
columns: newColumnData,
@ -227,15 +240,22 @@ function GridTableComponent({
query={query}
queryTableData={data}
loading={false}
columns={openTracesButton ? columnDataWithOpenTracesButton : newColumnData}
columns={
openTracesButton
? columnDataWithOpenTracesButton
: newColumnsWithRenderColumnCell
}
dataSource={dataSource}
sticky={sticky}
widgetId={widgetId}
onRow={
openTracesButton
openTracesButton || customOnRowClick
? (record): React.HTMLAttributes<HTMLElement> => ({
onClick: (): void => {
onOpenTraceBtnClick?.(record);
if (openTracesButton) {
onOpenTraceBtnClick?.(record);
}
customOnRowClick?.(record);
},
})
: undefined

View File

@ -4,6 +4,7 @@ import {
ThresholdOperators,
ThresholdProps,
} from 'container/NewWidget/RightContainer/Threshold/types';
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { ColumnUnit } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@ -17,7 +18,9 @@ export type GridTableComponentProps = {
searchTerm?: string;
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
customOnRowClick?: (record: RowData) => void;
widgetId?: string;
renderColumnCell?: QueryTableProps['renderColumnCell'];
} & Pick<LogsExplorerTableProps, 'data'> &
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;

View File

@ -20,6 +20,7 @@ function PanelWrapper({
openTracesButton,
onOpenTraceBtnClick,
customSeries,
customOnRowClick,
}: PanelWrapperProps): JSX.Element {
const Component = PanelTypeVsPanelWrapper[
selectedGraph || widget.panelTypes
@ -46,6 +47,7 @@ function PanelWrapper({
searchTerm={searchTerm}
openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick}
customOnRowClick={customOnRowClick}
customSeries={customSeries}
/>
);

View File

@ -11,6 +11,7 @@ function TablePanelWrapper({
searchTerm,
openTracesButton,
onOpenTraceBtnClick,
customOnRowClick,
}: PanelWrapperProps): JSX.Element {
const panelData =
(queryResponse.data?.payload?.data?.result?.[0] as any)?.table || [];
@ -26,7 +27,9 @@ function TablePanelWrapper({
searchTerm={searchTerm}
openTracesButton={openTracesButton}
onOpenTraceBtnClick={onOpenTraceBtnClick}
customOnRowClick={customOnRowClick}
widgetId={widget.id}
renderColumnCell={widget.renderColumnCell}
// eslint-disable-next-line react/jsx-props-no-spreading
{...GRID_TABLE_CONFIG}
/>

View File

@ -28,6 +28,7 @@ export type PanelWrapperProps = {
customTooltipElement?: HTMLDivElement;
openTracesButton?: boolean;
onOpenTraceBtnClick?: (record: RowData) => void;
customOnRowClick?: (record: RowData) => void;
customSeries?: (data: QueryData[]) => uPlot.Series[];
};

View File

@ -3,7 +3,10 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types';
import { Widgets } from 'types/api/dashboard/getAll';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderFormula,
IBuilderQuery,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
@ -12,6 +15,7 @@ interface GetWidgetQueryProps {
title: string;
description: string;
queryData: IBuilderQuery[];
queryFormulas?: IBuilderFormula[];
panelTypes?: PANEL_TYPES;
yAxisUnit?: string;
columnUnits?: Record<string, string>;
@ -67,7 +71,7 @@ export function getWidgetQuery(
promql: [],
builder: {
queryData: props.queryData,
queryFormulas: [],
queryFormulas: (props.queryFormulas as IBuilderFormula[]) || [],
},
clickhouse_sql: [],
id: uuid(),

View File

@ -1,6 +1,7 @@
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
import { ReactNode } from 'react';
import { Layout } from 'react-grid-layout';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@ -113,6 +114,7 @@ export interface IBaseWidget {
}
export interface Widgets extends IBaseWidget {
query: Query;
renderColumnCell?: QueryTableProps['renderColumnCell'];
}
export interface PromQLWidgets extends IBaseWidget {