mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-15 23:16:01 +08:00
feat: added download as csv support for table panel type (#5067)
* feat: added download as csv support for table panel type * feat: update the position of download * fix: build issues * fix: address review comments
This commit is contained in:
parent
52e4c2d8ff
commit
76b1e40cbc
@ -42,6 +42,7 @@ function FullView({
|
|||||||
fullViewOptions = true,
|
fullViewOptions = true,
|
||||||
version,
|
version,
|
||||||
originalName,
|
originalName,
|
||||||
|
tableProcessedDataRef,
|
||||||
isDependedDataLoaded = false,
|
isDependedDataLoaded = false,
|
||||||
onToggleModelHandler,
|
onToggleModelHandler,
|
||||||
}: FullViewProps): JSX.Element {
|
}: FullViewProps): JSX.Element {
|
||||||
@ -222,6 +223,7 @@ function FullView({
|
|||||||
setGraphVisibility={setGraphsVisibilityStates}
|
setGraphVisibility={setGraphsVisibilityStates}
|
||||||
graphVisibility={graphsVisibilityStates}
|
graphVisibility={graphsVisibilityStates}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
|
tableProcessedDataRef={tableProcessedDataRef}
|
||||||
/>
|
/>
|
||||||
</GraphContainer>
|
</GraphContainer>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,7 @@ import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
|||||||
import { ToggleGraphProps } from 'components/Graph/types';
|
import { ToggleGraphProps } from 'components/Graph/types';
|
||||||
import { UplotProps } from 'components/Uplot/Uplot';
|
import { UplotProps } from 'components/Uplot/Uplot';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
import { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
import { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
@ -50,6 +51,7 @@ export interface FullViewProps {
|
|||||||
fullViewOptions?: boolean;
|
fullViewOptions?: boolean;
|
||||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||||
name: string;
|
name: string;
|
||||||
|
tableProcessedDataRef: MutableRefObject<RowData[]>;
|
||||||
version?: string;
|
version?: string;
|
||||||
originalName: string;
|
originalName: string;
|
||||||
yAxisUnit?: string;
|
yAxisUnit?: string;
|
||||||
|
@ -12,6 +12,7 @@ import { useNotifications } from 'hooks/useNotifications';
|
|||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import {
|
import {
|
||||||
Dispatch,
|
Dispatch,
|
||||||
@ -33,7 +34,6 @@ import FullView from './FullView';
|
|||||||
import { Modal } from './styles';
|
import { Modal } from './styles';
|
||||||
import { WidgetGraphComponentProps } from './types';
|
import { WidgetGraphComponentProps } from './types';
|
||||||
import { getLocalStorageGraphVisibilityState } from './utils';
|
import { getLocalStorageGraphVisibilityState } from './utils';
|
||||||
// import { getLocalStorageGraphVisibilityState } from './utils';
|
|
||||||
|
|
||||||
function WidgetGraphComponent({
|
function WidgetGraphComponent({
|
||||||
widget,
|
widget,
|
||||||
@ -72,6 +72,8 @@ function WidgetGraphComponent({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const tableProcessedDataRef = useRef<RowData[]>([]);
|
||||||
|
|
||||||
const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard();
|
const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||||
|
|
||||||
const featureResponse = useSelector<AppState, AppReducer['featureResponse']>(
|
const featureResponse = useSelector<AppState, AppReducer['featureResponse']>(
|
||||||
@ -284,6 +286,7 @@ function WidgetGraphComponent({
|
|||||||
widget={widget}
|
widget={widget}
|
||||||
yAxisUnit={widget.yAxisUnit}
|
yAxisUnit={widget.yAxisUnit}
|
||||||
onToggleModelHandler={onToggleModelHandler}
|
onToggleModelHandler={onToggleModelHandler}
|
||||||
|
tableProcessedDataRef={tableProcessedDataRef}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@ -301,6 +304,7 @@ function WidgetGraphComponent({
|
|||||||
headerMenuList={headerMenuList}
|
headerMenuList={headerMenuList}
|
||||||
isWarning={isWarning}
|
isWarning={isWarning}
|
||||||
isFetchingResponse={isFetchingResponse}
|
isFetchingResponse={isFetchingResponse}
|
||||||
|
tableProcessedDataRef={tableProcessedDataRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && (
|
{queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && (
|
||||||
@ -319,6 +323,7 @@ function WidgetGraphComponent({
|
|||||||
graphVisibility={graphVisibility}
|
graphVisibility={graphVisibility}
|
||||||
onClickHandler={onClickHandler}
|
onClickHandler={onClickHandler}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
|
tableProcessedDataRef={tableProcessedDataRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -4,6 +4,7 @@ export enum MenuItemKeys {
|
|||||||
Delete = 'delete',
|
Delete = 'delete',
|
||||||
Clone = 'clone',
|
Clone = 'clone',
|
||||||
CreateAlerts = 'createAlerts',
|
CreateAlerts = 'createAlerts',
|
||||||
|
Download = 'download',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MENUITEM_KEYS_VS_LABELS = {
|
export const MENUITEM_KEYS_VS_LABELS = {
|
||||||
@ -12,4 +13,5 @@ export const MENUITEM_KEYS_VS_LABELS = {
|
|||||||
[MenuItemKeys.Delete]: 'Delete',
|
[MenuItemKeys.Delete]: 'Delete',
|
||||||
[MenuItemKeys.Clone]: 'Clone',
|
[MenuItemKeys.Clone]: 'Clone',
|
||||||
[MenuItemKeys.CreateAlerts]: 'Create Alerts',
|
[MenuItemKeys.CreateAlerts]: 'Create Alerts',
|
||||||
|
[MenuItemKeys.Download]: 'Download as CSV',
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,7 @@ import './WidgetHeader.styles.scss';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AlertOutlined,
|
AlertOutlined,
|
||||||
|
CloudDownloadOutlined,
|
||||||
CopyOutlined,
|
CopyOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditFilled,
|
EditFilled,
|
||||||
@ -17,6 +18,9 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
|||||||
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
|
import { isEmpty } from 'lodash-es';
|
||||||
|
import { unparse } from 'papaparse';
|
||||||
import { ReactNode, useCallback, useMemo } from 'react';
|
import { ReactNode, useCallback, useMemo } from 'react';
|
||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@ -46,6 +50,7 @@ interface IWidgetHeaderProps {
|
|||||||
headerMenuList?: MenuItemKeys[];
|
headerMenuList?: MenuItemKeys[];
|
||||||
isWarning: boolean;
|
isWarning: boolean;
|
||||||
isFetchingResponse: boolean;
|
isFetchingResponse: boolean;
|
||||||
|
tableProcessedDataRef: React.MutableRefObject<RowData[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function WidgetHeader({
|
function WidgetHeader({
|
||||||
@ -61,6 +66,7 @@ function WidgetHeader({
|
|||||||
headerMenuList,
|
headerMenuList,
|
||||||
isWarning,
|
isWarning,
|
||||||
isFetchingResponse,
|
isFetchingResponse,
|
||||||
|
tableProcessedDataRef,
|
||||||
}: IWidgetHeaderProps): JSX.Element | null {
|
}: IWidgetHeaderProps): JSX.Element | null {
|
||||||
const onEditHandler = useCallback((): void => {
|
const onEditHandler = useCallback((): void => {
|
||||||
const widgetId = widget.id;
|
const widgetId = widget.id;
|
||||||
@ -75,6 +81,17 @@ function WidgetHeader({
|
|||||||
|
|
||||||
const onCreateAlertsHandler = useCreateAlerts(widget);
|
const onCreateAlertsHandler = useCreateAlerts(widget);
|
||||||
|
|
||||||
|
const onDownloadHandler = useCallback((): void => {
|
||||||
|
const csv = unparse(tableProcessedDataRef.current);
|
||||||
|
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 = `${!isEmpty(title) ? title : 'table-panel'}.csv`;
|
||||||
|
downloadLink.click();
|
||||||
|
downloadLink.remove();
|
||||||
|
}, [tableProcessedDataRef, title]);
|
||||||
|
|
||||||
const keyMethodMapping = useMemo(
|
const keyMethodMapping = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
[MenuItemKeys.View]: onView,
|
[MenuItemKeys.View]: onView,
|
||||||
@ -82,8 +99,16 @@ function WidgetHeader({
|
|||||||
[MenuItemKeys.Delete]: onDelete,
|
[MenuItemKeys.Delete]: onDelete,
|
||||||
[MenuItemKeys.Clone]: onClone,
|
[MenuItemKeys.Clone]: onClone,
|
||||||
[MenuItemKeys.CreateAlerts]: onCreateAlertsHandler,
|
[MenuItemKeys.CreateAlerts]: onCreateAlertsHandler,
|
||||||
|
[MenuItemKeys.Download]: onDownloadHandler,
|
||||||
}),
|
}),
|
||||||
[onDelete, onEditHandler, onView, onClone, onCreateAlertsHandler],
|
[
|
||||||
|
onView,
|
||||||
|
onEditHandler,
|
||||||
|
onDelete,
|
||||||
|
onClone,
|
||||||
|
onCreateAlertsHandler,
|
||||||
|
onDownloadHandler,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onMenuItemSelectHandler: MenuProps['onClick'] = useCallback(
|
const onMenuItemSelectHandler: MenuProps['onClick'] = useCallback(
|
||||||
@ -128,6 +153,13 @@ function WidgetHeader({
|
|||||||
isVisible: headerMenuList?.includes(MenuItemKeys.Clone) || false,
|
isVisible: headerMenuList?.includes(MenuItemKeys.Clone) || false,
|
||||||
disabled: !editWidget,
|
disabled: !editWidget,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: MenuItemKeys.Download,
|
||||||
|
icon: <CloudDownloadOutlined />,
|
||||||
|
label: MENUITEM_KEYS_VS_LABELS[MenuItemKeys.Download],
|
||||||
|
isVisible: widget.panelTypes === PANEL_TYPES.TABLE,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: MenuItemKeys.Delete,
|
key: MenuItemKeys.Delete,
|
||||||
icon: <DeleteOutlined />,
|
icon: <DeleteOutlined />,
|
||||||
@ -144,7 +176,13 @@ function WidgetHeader({
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[headerMenuList, queryResponse.isFetching, editWidget, deleteWidget],
|
[
|
||||||
|
headerMenuList,
|
||||||
|
queryResponse.isFetching,
|
||||||
|
editWidget,
|
||||||
|
deleteWidget,
|
||||||
|
widget.panelTypes,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]);
|
const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]);
|
||||||
|
@ -19,4 +19,5 @@ export const isTWidgetOptions = (value: string): value is MenuItemKeys =>
|
|||||||
value === MenuItemKeys.Edit ||
|
value === MenuItemKeys.Edit ||
|
||||||
value === MenuItemKeys.Delete ||
|
value === MenuItemKeys.Delete ||
|
||||||
value === MenuItemKeys.Clone ||
|
value === MenuItemKeys.Clone ||
|
||||||
value === MenuItemKeys.CreateAlerts;
|
value === MenuItemKeys.CreateAlerts ||
|
||||||
|
value === MenuItemKeys.Download;
|
||||||
|
@ -2,8 +2,12 @@ import { ExclamationCircleFilled } from '@ant-design/icons';
|
|||||||
import { Space, Tooltip } from 'antd';
|
import { Space, Tooltip } from 'antd';
|
||||||
import { Events } from 'constants/events';
|
import { Events } from 'constants/events';
|
||||||
import { QueryTable } from 'container/QueryTable';
|
import { QueryTable } from 'container/QueryTable';
|
||||||
import { createTableColumnsFromQuery } from 'lib/query/createTableColumnsFromQuery';
|
import {
|
||||||
import { memo, ReactNode, useEffect, useMemo } from 'react';
|
createTableColumnsFromQuery,
|
||||||
|
RowData,
|
||||||
|
} from 'lib/query/createTableColumnsFromQuery';
|
||||||
|
import { get, set } from 'lodash-es';
|
||||||
|
import { memo, ReactNode, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { eventEmitter } from 'utils/getEventEmitter';
|
import { eventEmitter } from 'utils/getEventEmitter';
|
||||||
|
|
||||||
@ -15,6 +19,7 @@ function GridTableComponent({
|
|||||||
data,
|
data,
|
||||||
query,
|
query,
|
||||||
thresholds,
|
thresholds,
|
||||||
|
tableProcessedDataRef,
|
||||||
...props
|
...props
|
||||||
}: GridTableComponentProps): JSX.Element {
|
}: GridTableComponentProps): JSX.Element {
|
||||||
const { t } = useTranslation(['valueGraph']);
|
const { t } = useTranslation(['valueGraph']);
|
||||||
@ -27,6 +32,33 @@ function GridTableComponent({
|
|||||||
[data, query],
|
[data, query],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const createDataInCorrectFormat = useCallback(
|
||||||
|
(dataSource: RowData[]): RowData[] =>
|
||||||
|
dataSource.map((d) => {
|
||||||
|
const finalObject = {};
|
||||||
|
const keys = Object.keys(d);
|
||||||
|
keys.forEach((k) => {
|
||||||
|
const label = get(
|
||||||
|
columns.find((c) => get(c, 'dataIndex', '') === k) || {},
|
||||||
|
'title',
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
if (label) {
|
||||||
|
set(finalObject, label as string, d[k]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return finalObject as RowData;
|
||||||
|
}),
|
||||||
|
[columns],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tableProcessedDataRef) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
tableProcessedDataRef.current = createDataInCorrectFormat(dataSource);
|
||||||
|
}
|
||||||
|
}, [createDataInCorrectFormat, dataSource, tableProcessedDataRef]);
|
||||||
|
|
||||||
const newColumnData = columns.map((e) => ({
|
const newColumnData = columns.map((e) => ({
|
||||||
...e,
|
...e,
|
||||||
render: (text: string): ReactNode => {
|
render: (text: string): ReactNode => {
|
||||||
|
@ -10,6 +10,7 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
|||||||
export type GridTableComponentProps = {
|
export type GridTableComponentProps = {
|
||||||
query: Query;
|
query: Query;
|
||||||
thresholds?: ThresholdProps[];
|
thresholds?: ThresholdProps[];
|
||||||
|
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
|
||||||
} & Pick<LogsExplorerTableProps, 'data'> &
|
} & Pick<LogsExplorerTableProps, 'data'> &
|
||||||
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;
|
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ function PanelWrapper({
|
|||||||
onClickHandler,
|
onClickHandler,
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
selectedGraph,
|
selectedGraph,
|
||||||
|
tableProcessedDataRef,
|
||||||
}: PanelWrapperProps): JSX.Element {
|
}: PanelWrapperProps): JSX.Element {
|
||||||
const Component = PanelTypeVsPanelWrapper[
|
const Component = PanelTypeVsPanelWrapper[
|
||||||
selectedGraph || widget.panelTypes
|
selectedGraph || widget.panelTypes
|
||||||
@ -35,6 +36,7 @@ function PanelWrapper({
|
|||||||
onClickHandler={onClickHandler}
|
onClickHandler={onClickHandler}
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
selectedGraph={selectedGraph}
|
selectedGraph={selectedGraph}
|
||||||
|
tableProcessedDataRef={tableProcessedDataRef}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { PanelWrapperProps } from './panelWrapper.types';
|
|||||||
function TablePanelWrapper({
|
function TablePanelWrapper({
|
||||||
widget,
|
widget,
|
||||||
queryResponse,
|
queryResponse,
|
||||||
|
tableProcessedDataRef,
|
||||||
}: PanelWrapperProps): JSX.Element {
|
}: PanelWrapperProps): JSX.Element {
|
||||||
const panelData =
|
const panelData =
|
||||||
queryResponse.data?.payload?.data.newResult.data.result || [];
|
queryResponse.data?.payload?.data.newResult.data.result || [];
|
||||||
@ -15,6 +16,7 @@ function TablePanelWrapper({
|
|||||||
data={panelData}
|
data={panelData}
|
||||||
query={widget.query}
|
query={widget.query}
|
||||||
thresholds={thresholds}
|
thresholds={thresholds}
|
||||||
|
tableProcessedDataRef={tableProcessedDataRef}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...GRID_TABLE_CONFIG}
|
{...GRID_TABLE_CONFIG}
|
||||||
/>
|
/>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { WidgetGraphComponentProps } from 'container/GridCardLayout/GridCard/types';
|
import { WidgetGraphComponentProps } from 'container/GridCardLayout/GridCard/types';
|
||||||
|
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
@ -21,6 +22,7 @@ export type PanelWrapperProps = {
|
|||||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||||
onDragSelect: (start: number, end: number) => void;
|
onDragSelect: (start: number, end: number) => void;
|
||||||
selectedGraph?: PANEL_TYPES;
|
selectedGraph?: PANEL_TYPES;
|
||||||
|
tableProcessedDataRef?: React.MutableRefObject<RowData[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TooltipData = {
|
export type TooltipData = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user