[Feat]: Download as CSV and Execl and Search option for key-operation table (#3848)

* refactor: done with the download option for key-operation

* refactor: added search option for key operation metrics

* refactor: done with the download option for key-operation

* refactor: added search option for key operation metrics

* refactor: updated downloadable data

* refactor: updated downloadable data for metrics key operation

* refactor: updated the data

* refactor: map with the correct value for export

* refactor: updated downloabable data for metrics

* refactor: updated the data for metrics

* refactor: added safety check

---------

Co-authored-by: Ankit Nayan <ankit@signoz.io>
This commit is contained in:
Rajat Dabade 2023-11-01 17:53:31 +05:30 committed by GitHub
parent f94a5f4481
commit 64f0ff05f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 252 additions and 98 deletions

View File

@ -0,0 +1,4 @@
.download-button {
display: flex;
align-items: center;
}

View File

@ -0,0 +1,77 @@
import './Download.styles.scss';
import { CloudDownloadOutlined } from '@ant-design/icons';
import { Button, Dropdown, MenuProps } from 'antd';
import { Excel } from 'antd-table-saveas-excel';
import { unparse } from 'papaparse';
import { DownloadProps } from './Download.types';
function Download({ data, isLoading, fileName }: DownloadProps): JSX.Element {
const downloadExcelFile = (): void => {
const headers = Object.keys(Object.assign({}, ...data)).map((item) => {
const updatedTitle = item
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return {
title: updatedTitle,
dataIndex: item,
};
});
const excel = new Excel();
excel
.addSheet(fileName)
.addColumns(headers)
.addDataSource(data, {
str2Percent: true,
})
.saveAs(`${fileName}.xlsx`);
};
const downloadCsvFile = (): void => {
const csv = unparse(data);
const csvBlob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const csvUrl = URL.createObjectURL(csvBlob);
const downloadLink = document.createElement('a');
downloadLink.href = csvUrl;
downloadLink.download = `${fileName}.csv`;
downloadLink.click();
downloadLink.remove();
};
const menu: MenuProps = {
items: [
{
key: 'download-as-excel',
label: 'Excel',
onClick: downloadExcelFile,
},
{
key: 'download-as-csv',
label: 'CSV',
onClick: downloadCsvFile,
},
],
};
return (
<Dropdown menu={menu} trigger={['click']}>
<Button
className="download-button"
loading={isLoading}
size="small"
type="link"
>
<CloudDownloadOutlined />
Download
</Button>
</Dropdown>
);
}
Download.defaultProps = {
isLoading: undefined,
};
export default Download;

View File

@ -0,0 +1,10 @@
export type DownloadOptions = {
isDownloadEnabled: boolean;
fileName: string;
};
export type DownloadProps = {
data: Record<string, string>[];
isLoading?: boolean;
fileName: string;
};

View File

@ -1,15 +1,14 @@
import { CloudDownloadOutlined, FastBackwardOutlined } from '@ant-design/icons';
import { Button, Divider, Dropdown, MenuProps } from 'antd';
import { Excel } from 'antd-table-saveas-excel';
import { FastBackwardOutlined } from '@ant-design/icons';
import { Button, Divider } from 'antd';
import Controls from 'container/Controls';
import Download from 'container/Download/Download';
import { getGlobalTime } from 'container/LogsSearchFilter/utils';
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
import dayjs from 'dayjs';
import { Pagination } from 'hooks/queryPagination';
import { FlatLogData } from 'lib/logs/flatLogData';
import { OrderPreferenceItems } from 'pages/Logs/config';
import { unparse } from 'papaparse';
import { memo, useCallback, useMemo } from 'react';
import { memo, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
@ -23,7 +22,7 @@ import {
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { Container, DownloadLogButton } from './styles';
import { Container } from './styles';
function LogControls(): JSX.Element | null {
const {
@ -97,58 +96,6 @@ function LogControls(): JSX.Element | null {
[logs],
);
const downloadExcelFile = useCallback((): void => {
const headers = Object.keys(Object.assign({}, ...flattenLogData)).map(
(item) => {
const updatedTitle = item
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return {
title: updatedTitle,
dataIndex: item,
};
},
);
const excel = new Excel();
excel
.addSheet('log_data')
.addColumns(headers)
.addDataSource(flattenLogData, {
str2Percent: true,
})
.saveAs('log_data.xlsx');
}, [flattenLogData]);
const downloadCsvFile = useCallback((): void => {
const csv = unparse(flattenLogData);
const csvBlob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const csvUrl = URL.createObjectURL(csvBlob);
const downloadLink = document.createElement('a');
downloadLink.href = csvUrl;
downloadLink.download = 'log_data.csv';
downloadLink.click();
downloadLink.remove();
}, [flattenLogData]);
const menu: MenuProps = useMemo(
() => ({
items: [
{
key: 'download-as-excel',
label: 'Excel',
onClick: downloadExcelFile,
},
{
key: 'download-as-csv',
label: 'CSV',
onClick: downloadCsvFile,
},
],
}),
[downloadCsvFile, downloadExcelFile],
);
const isLoading = isLogsLoading || isLoadingAggregate;
if (liveTail !== 'STOPPED') {
@ -157,12 +104,7 @@ function LogControls(): JSX.Element | null {
return (
<Container>
<Dropdown menu={menu} trigger={['click']}>
<DownloadLogButton loading={isLoading} size="small" type="link">
<CloudDownloadOutlined />
Download
</DownloadLogButton>
</Dropdown>
<Download data={flattenLogData} isLoading={isLoading} fileName="log_data" />
<Button
loading={isLoading}
size="small"

View File

@ -1,5 +1,4 @@
import getTopOperations from 'api/metrics/getTopOperations';
import Spinner from 'components/Spinner';
import TopOperationsTable from 'container/MetricsApplication/TopOperationsTable';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
@ -36,12 +35,7 @@ function TopOperation(): JSX.Element {
const topOperationData = data || [];
return (
<>
{isLoading && <Spinner size="large" tip="Loading..." height="40vh" />}
{!isLoading && <TopOperationsTable data={topOperationData} />}
</>
);
return <TopOperationsTable data={topOperationData} isLoading={isLoading} />;
}
export default TopOperation;

View File

@ -1,4 +1,5 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { topOperationMetricsDownloadOptions } from 'container/MetricsApplication/constant';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { topOperationQueries } from 'container/MetricsApplication/MetricsPageQueries/TopOperationQueries';
import { QueryTable } from 'container/QueryTable';
@ -109,6 +110,7 @@ function TopOperationMetrics(): JSX.Element {
queryTableData={queryTableData}
loading={isLoading}
renderColumnCell={renderColumnCell}
downloadOption={topOperationMetricsDownloadOptions}
/>
);
}

View File

@ -0,0 +1,9 @@
.top-operation {
position: relative;
.top-operation--download {
position: absolute;
top: 15px;
right: 0px;
z-index: 1;
}
}

View File

@ -1,16 +1,32 @@
import { Tooltip, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import './TopOperationsTable.styles.scss';
import { SearchOutlined } from '@ant-design/icons';
import { InputRef, Tooltip, Typography } from 'antd';
import { ColumnsType, ColumnType } from 'antd/lib/table';
import { ResizeTable } from 'components/ResizeTable';
import Download from 'container/Download/Download';
import { filterDropdown } from 'container/ServiceApplication/Filter/FilterDropdown';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { useRef } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getErrorRate, navigateToTrace } from './utils';
import { IServiceName } from './Tabs/types';
import {
convertedTracesToDownloadData,
getErrorRate,
navigateToTrace,
} from './utils';
function TopOperationsTable({ data }: TopOperationsTableProps): JSX.Element {
function TopOperationsTable({
data,
isLoading,
}: TopOperationsTableProps): JSX.Element {
const searchInput = useRef<InputRef>(null);
const { servicename } = useParams<IServiceName>();
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
@ -34,19 +50,35 @@ function TopOperationsTable({ data }: TopOperationsTableProps): JSX.Element {
});
};
const getSearchOption = (): ColumnType<TopOperationList> => ({
filterDropdown,
filterIcon: <SearchOutlined />,
onFilter: (value, record): boolean =>
record.name
.toString()
.toLowerCase()
.includes((value as string).toLowerCase()),
onFilterDropdownOpenChange: (visible): void => {
if (visible) {
setTimeout(() => searchInput.current?.select(), 100);
}
},
render: (text: string): JSX.Element => (
<Tooltip placement="topLeft" title={text}>
<Typography.Link onClick={(): void => handleOnClick(text)}>
{text}
</Typography.Link>
</Tooltip>
),
});
const columns: ColumnsType<TopOperationList> = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: 100,
render: (text: string): JSX.Element => (
<Tooltip placement="topLeft" title={text}>
<Typography.Link onClick={(): void => handleOnClick(text)}>
{text}
</Typography.Link>
</Tooltip>
),
...getSearchOption(),
},
{
title: 'P50 (in ms)',
@ -92,15 +124,27 @@ function TopOperationsTable({ data }: TopOperationsTableProps): JSX.Element {
},
];
const downloadableData = convertedTracesToDownloadData(data);
return (
<ResizeTable
columns={columns}
showHeader
title={(): string => 'Key Operations'}
tableLayout="fixed"
dataSource={data}
rowKey="name"
/>
<div className="top-operation">
<div className="top-operation--download">
<Download
data={downloadableData}
isLoading={isLoading}
fileName={`top-operations-${servicename}`}
/>
</div>
<ResizeTable
columns={columns}
loading={isLoading}
showHeader
title={(): string => 'Key Operations'}
tableLayout="fixed"
dataSource={data}
rowKey="name"
/>
</div>
);
}
@ -115,6 +159,7 @@ export interface TopOperationList {
interface TopOperationsTableProps {
data: TopOperationList[];
isLoading: boolean;
}
export default TopOperationsTable;

View File

@ -1,3 +1,5 @@
import { DownloadOptions } from 'container/Download/Download.types';
export const legend = {
address: '{{address}}',
};
@ -67,3 +69,8 @@ export enum WidgetKeys {
SignozExternalCallLatencySum = 'signoz_external_call_latency_sum',
Signoz_latency_bucket = 'signoz_latency_bucket',
}
export const topOperationMetricsDownloadOptions: DownloadOptions = {
isDownloadEnabled: true,
fileName: 'top-operation',
} as const;

View File

@ -35,3 +35,19 @@ export const getNearestHighestBucketValue = (
export const convertMilSecToNanoSec = (value: number): number =>
value * 1000000000;
export const convertedTracesToDownloadData = (
originalData: TopOperationList[],
): Record<string, string>[] =>
originalData.map((item) => {
const newObj: Record<string, string> = {
Name: item.name,
'P50 (in ms)': (item.p50 / 1000000).toFixed(2),
'P95 (in ms)': (item.p95 / 1000000).toFixed(2),
'P99 (in ms)': (item.p99 / 1000000).toFixed(2),
'Number of calls': item.numCalls.toString(),
'Error Rate (%)': getErrorRate(item).toFixed(2),
};
return newObj;
});

View File

@ -1,5 +1,6 @@
import { TableProps } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { DownloadOptions } from 'container/Download/Download.types';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { ReactNode } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@ -14,4 +15,5 @@ export type QueryTableProps = Omit<
renderActionCell?: (record: RowData) => ReactNode;
modifyColumns?: (columns: ColumnsType<RowData>) => ColumnsType<RowData>;
renderColumnCell?: Record<string, (record: RowData) => ReactNode>;
downloadOption?: DownloadOptions;
};

View File

@ -0,0 +1,9 @@
.query-table {
position: relative;
.query-table--download {
position: absolute;
top: 15px;
right: 0px;
z-index: 1;
}
}

View File

@ -1,8 +1,14 @@
import './QueryTable.styles.scss';
import { ResizeTable } from 'components/ResizeTable';
import Download from 'container/Download/Download';
import { IServiceName } from 'container/MetricsApplication/Tabs/types';
import { createTableColumnsFromQuery } from 'lib/query/createTableColumnsFromQuery';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { QueryTableProps } from './QueryTable.intefaces';
import { createDownloadableData } from './utils';
export function QueryTable({
queryTableData,
@ -10,8 +16,12 @@ export function QueryTable({
renderActionCell,
modifyColumns,
renderColumnCell,
downloadOption,
...props
}: QueryTableProps): JSX.Element {
const { isDownloadEnabled = false, fileName = '' } = downloadOption || {};
const { servicename } = useParams<IServiceName>();
const { loading } = props;
const { columns, dataSource } = useMemo(
() =>
createTableColumnsFromQuery({
@ -23,16 +33,29 @@ export function QueryTable({
[query, queryTableData, renderActionCell, renderColumnCell],
);
const downloadableData = createDownloadableData(dataSource);
const tableColumns = modifyColumns ? modifyColumns(columns) : columns;
return (
<ResizeTable
columns={tableColumns}
tableLayout="fixed"
dataSource={dataSource}
scroll={{ x: true }}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
<div className="query-table">
{isDownloadEnabled && (
<div className="query-table--download">
<Download
data={downloadableData}
fileName={`${fileName}-${servicename}`}
isLoading={loading as boolean}
/>
</div>
)}
<ResizeTable
columns={tableColumns}
tableLayout="fixed"
dataSource={dataSource}
scroll={{ x: true }}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
</div>
);
}

View File

@ -0,0 +1,14 @@
import { RowData } from 'lib/query/createTableColumnsFromQuery';
export function createDownloadableData(
inputData: RowData[],
): Record<string, string>[] {
return inputData.map((row) => ({
Name: String(row.operation || ''),
'P50 (in ns)': String(row.A || ''),
'P90 (in ns)': String(row.B || ''),
'P99 (in ns)': String(row.C || ''),
'Number Of Calls': String(row.F || ''),
'Error Rate (%)': String(row.F1 && row.F1 !== 'N/A' ? row.F1 : '0'),
}));
}