mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 07:29:05 +08:00
[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:
parent
f94a5f4481
commit
64f0ff05f9
4
frontend/src/container/Download/Download.styles.scss
Normal file
4
frontend/src/container/Download/Download.styles.scss
Normal file
@ -0,0 +1,4 @@
|
||||
.download-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
77
frontend/src/container/Download/Download.tsx
Normal file
77
frontend/src/container/Download/Download.tsx
Normal 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;
|
10
frontend/src/container/Download/Download.types.ts
Normal file
10
frontend/src/container/Download/Download.types.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export type DownloadOptions = {
|
||||
isDownloadEnabled: boolean;
|
||||
fileName: string;
|
||||
};
|
||||
|
||||
export type DownloadProps = {
|
||||
data: Record<string, string>[];
|
||||
isLoading?: boolean;
|
||||
fileName: string;
|
||||
};
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
.top-operation {
|
||||
position: relative;
|
||||
.top-operation--download {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 0px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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;
|
||||
};
|
||||
|
9
frontend/src/container/QueryTable/QueryTable.styles.scss
Normal file
9
frontend/src/container/QueryTable/QueryTable.styles.scss
Normal file
@ -0,0 +1,9 @@
|
||||
.query-table {
|
||||
position: relative;
|
||||
.query-table--download {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 0px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
14
frontend/src/container/QueryTable/utils.ts
Normal file
14
frontend/src/container/QueryTable/utils.ts
Normal 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'),
|
||||
}));
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user