mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-28 04:12:02 +08:00
feat: implement download logs feature for logs explorer new design (#4728)
* feat: implement download logs feature for logs explorer new design * feat: address review comments * feat: added timestamp and body to the start --------- Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
This commit is contained in:
parent
0df86454ce
commit
0df3c26f04
@ -27,7 +27,7 @@
|
|||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: var(--bg-slate-200, #52575c);
|
color: #52575c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-items {
|
.menu-items {
|
||||||
@ -65,7 +65,7 @@
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: var(--bg-slate-200, #52575c);
|
color: #52575c;
|
||||||
font-family: Inter;
|
font-family: Inter;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@ -149,7 +149,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
color: var(--bg-slate-200, #52575c);
|
color: #52575c;
|
||||||
font-family: Inter;
|
font-family: Inter;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
84
frontend/src/container/DownloadV2/DownloadV2.styles.scss
Normal file
84
frontend/src/container/DownloadV2/DownloadV2.styles.scss
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
.download-logs-popover {
|
||||||
|
.ant-popover-inner {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: linear-gradient(
|
||||||
|
139deg,
|
||||||
|
rgba(18, 19, 23, 0.8) 0%,
|
||||||
|
rgba(18, 19, 23, 0.9) 98.68%
|
||||||
|
);
|
||||||
|
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
padding: 12px 18px 12px 14px;
|
||||||
|
|
||||||
|
.download-logs-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.action-btns {
|
||||||
|
padding: 4px 0px !important;
|
||||||
|
width: 159px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: 0.14px;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.ant-btn-icon {
|
||||||
|
margin-inline-end: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btns:hover {
|
||||||
|
&.ant-btn-text {
|
||||||
|
background-color: rgba(171, 189, 255, 0.04) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-heading {
|
||||||
|
color: #52575c;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 18px; /* 163.636% */
|
||||||
|
letter-spacing: 0.88px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.download-logs-popover {
|
||||||
|
.ant-popover-inner {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: linear-gradient(
|
||||||
|
139deg,
|
||||||
|
rgba(255, 255, 255, 0.8) 0%,
|
||||||
|
rgba(255, 255, 255, 0.9) 98.68%
|
||||||
|
);
|
||||||
|
|
||||||
|
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
|
.download-logs-content {
|
||||||
|
.action-btns {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
.action-btns:hover {
|
||||||
|
&.ant-btn-text {
|
||||||
|
background-color: var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.export-heading {
|
||||||
|
color: var(--bg-ink-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
frontend/src/container/DownloadV2/DownloadV2.tsx
Normal file
84
frontend/src/container/DownloadV2/DownloadV2.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import './DownloadV2.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Popover, Typography } from 'antd';
|
||||||
|
import { Excel } from 'antd-table-saveas-excel';
|
||||||
|
import { FileDigit, FileDown, Sheet } from 'lucide-react';
|
||||||
|
import { unparse } from 'papaparse';
|
||||||
|
|
||||||
|
import { DownloadProps } from './DownloadV2.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();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
trigger={['click']}
|
||||||
|
placement="bottomRight"
|
||||||
|
rootClassName="download-logs-popover"
|
||||||
|
arrow={false}
|
||||||
|
content={
|
||||||
|
<div className="download-logs-content">
|
||||||
|
<Typography.Text className="export-heading">Export As</Typography.Text>
|
||||||
|
<Button
|
||||||
|
icon={<Sheet size={14} />}
|
||||||
|
type="text"
|
||||||
|
onClick={downloadExcelFile}
|
||||||
|
className="action-btns"
|
||||||
|
>
|
||||||
|
Excel (.xlsx)
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<FileDigit size={14} />}
|
||||||
|
type="text"
|
||||||
|
onClick={downloadCsvFile}
|
||||||
|
className="action-btns"
|
||||||
|
>
|
||||||
|
CSV
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className="periscope-btn"
|
||||||
|
loading={isLoading}
|
||||||
|
icon={<FileDown size={14} />}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Download.defaultProps = {
|
||||||
|
isLoading: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Download;
|
10
frontend/src/container/DownloadV2/DownloadV2.types.ts
Normal file
10
frontend/src/container/DownloadV2/DownloadV2.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;
|
||||||
|
};
|
@ -14,6 +14,7 @@ import {
|
|||||||
PANEL_TYPES,
|
PANEL_TYPES,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
|
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
|
||||||
|
import Download from 'container/DownloadV2/DownloadV2';
|
||||||
import ExplorerOptions from 'container/ExplorerOptions/ExplorerOptions';
|
import ExplorerOptions from 'container/ExplorerOptions/ExplorerOptions';
|
||||||
import GoToTop from 'container/GoToTop';
|
import GoToTop from 'container/GoToTop';
|
||||||
import LogsExplorerChart from 'container/LogsExplorerChart';
|
import LogsExplorerChart from 'container/LogsExplorerChart';
|
||||||
@ -21,6 +22,7 @@ import LogsExplorerList from 'container/LogsExplorerList';
|
|||||||
import LogsExplorerTable from 'container/LogsExplorerTable';
|
import LogsExplorerTable from 'container/LogsExplorerTable';
|
||||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
||||||
import { LogTimeRange } from 'hooks/logs/types';
|
import { LogTimeRange } from 'hooks/logs/types';
|
||||||
@ -33,8 +35,9 @@ import useClickOutside from 'hooks/useClickOutside';
|
|||||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||||
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
|
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
|
||||||
import { defaultTo, isEmpty } from 'lodash-es';
|
import { defaultTo, isEmpty, omit } from 'lodash-es';
|
||||||
import { Sliders } from 'lucide-react';
|
import { Sliders } from 'lucide-react';
|
||||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
@ -523,6 +526,23 @@ function LogsExplorerViews({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const flattenLogData = useMemo(
|
||||||
|
() =>
|
||||||
|
logs.map((log) => {
|
||||||
|
const timestamp =
|
||||||
|
typeof log.timestamp === 'string'
|
||||||
|
? dayjs(log.timestamp).format()
|
||||||
|
: dayjs(log.timestamp / 1e6).format();
|
||||||
|
|
||||||
|
return FlatLogData({
|
||||||
|
timestamp,
|
||||||
|
body: log.body,
|
||||||
|
...omit(log, 'timestamp', 'body'),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
[logs],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="logs-explorer-views-container">
|
<div className="logs-explorer-views-container">
|
||||||
{showHistogram && (
|
{showHistogram && (
|
||||||
@ -578,6 +598,11 @@ function LogsExplorerViews({
|
|||||||
<div className="logs-actions-container">
|
<div className="logs-actions-container">
|
||||||
{selectedPanelType === PANEL_TYPES.LIST && (
|
{selectedPanelType === PANEL_TYPES.LIST && (
|
||||||
<div className="tab-options">
|
<div className="tab-options">
|
||||||
|
<Download
|
||||||
|
data={flattenLogData}
|
||||||
|
isLoading={isFetching}
|
||||||
|
fileName="log_data"
|
||||||
|
/>
|
||||||
<div className="format-options-container" ref={menuRef}>
|
<div className="format-options-container" ref={menuRef}>
|
||||||
<Button
|
<Button
|
||||||
className="periscope-btn"
|
className="periscope-btn"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user