diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx index 84df755964..b457c4014b 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx @@ -42,6 +42,7 @@ function FullView({ fullViewOptions = true, version, originalName, + tableProcessedDataRef, isDependedDataLoaded = false, onToggleModelHandler, }: FullViewProps): JSX.Element { @@ -222,6 +223,7 @@ function FullView({ setGraphVisibility={setGraphsVisibilityStates} graphVisibility={graphsVisibilityStates} onDragSelect={onDragSelect} + tableProcessedDataRef={tableProcessedDataRef} /> diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts index 7440278fd8..c4e638d717 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts @@ -2,6 +2,7 @@ import { CheckboxChangeEvent } from 'antd/es/checkbox'; import { ToggleGraphProps } from 'components/Graph/types'; import { UplotProps } from 'components/Uplot/Uplot'; import { PANEL_TYPES } from 'constants/queryBuilder'; +import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin'; import { Dispatch, MutableRefObject, SetStateAction } from 'react'; import { Widgets } from 'types/api/dashboard/getAll'; @@ -50,6 +51,7 @@ export interface FullViewProps { fullViewOptions?: boolean; onClickHandler?: OnClickPluginOpts['onClick']; name: string; + tableProcessedDataRef: MutableRefObject; version?: string; originalName: string; yAxisUnit?: string; diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx index 8111f386fc..0b47403224 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx @@ -12,6 +12,7 @@ import { useNotifications } from 'hooks/useNotifications'; import useUrlQuery from 'hooks/useUrlQuery'; import createQueryParams from 'lib/createQueryParams'; import history from 'lib/history'; +import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { useDashboard } from 'providers/Dashboard/Dashboard'; import { Dispatch, @@ -33,7 +34,6 @@ import FullView from './FullView'; import { Modal } from './styles'; import { WidgetGraphComponentProps } from './types'; import { getLocalStorageGraphVisibilityState } from './utils'; -// import { getLocalStorageGraphVisibilityState } from './utils'; function WidgetGraphComponent({ widget, @@ -72,6 +72,8 @@ function WidgetGraphComponent({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const tableProcessedDataRef = useRef([]); + const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard(); const featureResponse = useSelector( @@ -284,6 +286,7 @@ function WidgetGraphComponent({ widget={widget} yAxisUnit={widget.yAxisUnit} onToggleModelHandler={onToggleModelHandler} + tableProcessedDataRef={tableProcessedDataRef} /> @@ -301,6 +304,7 @@ function WidgetGraphComponent({ headerMenuList={headerMenuList} isWarning={isWarning} isFetchingResponse={isFetchingResponse} + tableProcessedDataRef={tableProcessedDataRef} /> {queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && ( @@ -319,6 +323,7 @@ function WidgetGraphComponent({ graphVisibility={graphVisibility} onClickHandler={onClickHandler} onDragSelect={onDragSelect} + tableProcessedDataRef={tableProcessedDataRef} /> )} diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/contants.ts b/frontend/src/container/GridCardLayout/WidgetHeader/contants.ts index 6f3871bd3d..7c32a0e2c5 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/contants.ts +++ b/frontend/src/container/GridCardLayout/WidgetHeader/contants.ts @@ -4,6 +4,7 @@ export enum MenuItemKeys { Delete = 'delete', Clone = 'clone', CreateAlerts = 'createAlerts', + Download = 'download', } export const MENUITEM_KEYS_VS_LABELS = { @@ -12,4 +13,5 @@ export const MENUITEM_KEYS_VS_LABELS = { [MenuItemKeys.Delete]: 'Delete', [MenuItemKeys.Clone]: 'Clone', [MenuItemKeys.CreateAlerts]: 'Create Alerts', + [MenuItemKeys.Download]: 'Download as CSV', }; diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx index 63312a5225..1401576ca9 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx +++ b/frontend/src/container/GridCardLayout/WidgetHeader/index.tsx @@ -2,6 +2,7 @@ import './WidgetHeader.styles.scss'; import { AlertOutlined, + CloudDownloadOutlined, CopyOutlined, DeleteOutlined, EditFilled, @@ -17,6 +18,9 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts'; import useComponentPermission from 'hooks/useComponentPermission'; 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 { UseQueryResult } from 'react-query'; import { useSelector } from 'react-redux'; @@ -46,6 +50,7 @@ interface IWidgetHeaderProps { headerMenuList?: MenuItemKeys[]; isWarning: boolean; isFetchingResponse: boolean; + tableProcessedDataRef: React.MutableRefObject; } function WidgetHeader({ @@ -61,6 +66,7 @@ function WidgetHeader({ headerMenuList, isWarning, isFetchingResponse, + tableProcessedDataRef, }: IWidgetHeaderProps): JSX.Element | null { const onEditHandler = useCallback((): void => { const widgetId = widget.id; @@ -75,6 +81,17 @@ function WidgetHeader({ 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( () => ({ [MenuItemKeys.View]: onView, @@ -82,8 +99,16 @@ function WidgetHeader({ [MenuItemKeys.Delete]: onDelete, [MenuItemKeys.Clone]: onClone, [MenuItemKeys.CreateAlerts]: onCreateAlertsHandler, + [MenuItemKeys.Download]: onDownloadHandler, }), - [onDelete, onEditHandler, onView, onClone, onCreateAlertsHandler], + [ + onView, + onEditHandler, + onDelete, + onClone, + onCreateAlertsHandler, + onDownloadHandler, + ], ); const onMenuItemSelectHandler: MenuProps['onClick'] = useCallback( @@ -128,6 +153,13 @@ function WidgetHeader({ isVisible: headerMenuList?.includes(MenuItemKeys.Clone) || false, disabled: !editWidget, }, + { + key: MenuItemKeys.Download, + icon: , + label: MENUITEM_KEYS_VS_LABELS[MenuItemKeys.Download], + isVisible: widget.panelTypes === PANEL_TYPES.TABLE, + disabled: false, + }, { key: MenuItemKeys.Delete, icon: , @@ -144,7 +176,13 @@ function WidgetHeader({ disabled: false, }, ], - [headerMenuList, queryResponse.isFetching, editWidget, deleteWidget], + [ + headerMenuList, + queryResponse.isFetching, + editWidget, + deleteWidget, + widget.panelTypes, + ], ); const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]); diff --git a/frontend/src/container/GridCardLayout/WidgetHeader/utils.ts b/frontend/src/container/GridCardLayout/WidgetHeader/utils.ts index 482994aa8c..96f7e32010 100644 --- a/frontend/src/container/GridCardLayout/WidgetHeader/utils.ts +++ b/frontend/src/container/GridCardLayout/WidgetHeader/utils.ts @@ -19,4 +19,5 @@ export const isTWidgetOptions = (value: string): value is MenuItemKeys => value === MenuItemKeys.Edit || value === MenuItemKeys.Delete || value === MenuItemKeys.Clone || - value === MenuItemKeys.CreateAlerts; + value === MenuItemKeys.CreateAlerts || + value === MenuItemKeys.Download; diff --git a/frontend/src/container/GridTableComponent/index.tsx b/frontend/src/container/GridTableComponent/index.tsx index db89133553..9dbccc1fd6 100644 --- a/frontend/src/container/GridTableComponent/index.tsx +++ b/frontend/src/container/GridTableComponent/index.tsx @@ -2,8 +2,12 @@ import { ExclamationCircleFilled } from '@ant-design/icons'; import { Space, Tooltip } from 'antd'; import { Events } from 'constants/events'; import { QueryTable } from 'container/QueryTable'; -import { createTableColumnsFromQuery } from 'lib/query/createTableColumnsFromQuery'; -import { memo, ReactNode, useEffect, useMemo } from 'react'; +import { + 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 { eventEmitter } from 'utils/getEventEmitter'; @@ -15,6 +19,7 @@ function GridTableComponent({ data, query, thresholds, + tableProcessedDataRef, ...props }: GridTableComponentProps): JSX.Element { const { t } = useTranslation(['valueGraph']); @@ -27,6 +32,33 @@ function GridTableComponent({ [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) => ({ ...e, render: (text: string): ReactNode => { diff --git a/frontend/src/container/GridTableComponent/types.ts b/frontend/src/container/GridTableComponent/types.ts index 4f210eca18..8a48215884 100644 --- a/frontend/src/container/GridTableComponent/types.ts +++ b/frontend/src/container/GridTableComponent/types.ts @@ -10,6 +10,7 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData'; export type GridTableComponentProps = { query: Query; thresholds?: ThresholdProps[]; + tableProcessedDataRef?: React.MutableRefObject; } & Pick & Omit, 'columns' | 'dataSource'>; diff --git a/frontend/src/container/PanelWrapper/PanelWrapper.tsx b/frontend/src/container/PanelWrapper/PanelWrapper.tsx index 4e8d7eff96..fdc20f7d30 100644 --- a/frontend/src/container/PanelWrapper/PanelWrapper.tsx +++ b/frontend/src/container/PanelWrapper/PanelWrapper.tsx @@ -14,6 +14,7 @@ function PanelWrapper({ onClickHandler, onDragSelect, selectedGraph, + tableProcessedDataRef, }: PanelWrapperProps): JSX.Element { const Component = PanelTypeVsPanelWrapper[ selectedGraph || widget.panelTypes @@ -35,6 +36,7 @@ function PanelWrapper({ onClickHandler={onClickHandler} onDragSelect={onDragSelect} selectedGraph={selectedGraph} + tableProcessedDataRef={tableProcessedDataRef} /> ); } diff --git a/frontend/src/container/PanelWrapper/TablePanelWrapper.tsx b/frontend/src/container/PanelWrapper/TablePanelWrapper.tsx index 10a41493df..5a440f4fdc 100644 --- a/frontend/src/container/PanelWrapper/TablePanelWrapper.tsx +++ b/frontend/src/container/PanelWrapper/TablePanelWrapper.tsx @@ -6,6 +6,7 @@ import { PanelWrapperProps } from './panelWrapper.types'; function TablePanelWrapper({ widget, queryResponse, + tableProcessedDataRef, }: PanelWrapperProps): JSX.Element { const panelData = queryResponse.data?.payload?.data.newResult.data.result || []; @@ -15,6 +16,7 @@ function TablePanelWrapper({ data={panelData} query={widget.query} thresholds={thresholds} + tableProcessedDataRef={tableProcessedDataRef} // eslint-disable-next-line react/jsx-props-no-spreading {...GRID_TABLE_CONFIG} /> diff --git a/frontend/src/container/PanelWrapper/panelWrapper.types.ts b/frontend/src/container/PanelWrapper/panelWrapper.types.ts index e2b6ef70c6..6fd993f377 100644 --- a/frontend/src/container/PanelWrapper/panelWrapper.types.ts +++ b/frontend/src/container/PanelWrapper/panelWrapper.types.ts @@ -1,5 +1,6 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; import { WidgetGraphComponentProps } from 'container/GridCardLayout/GridCard/types'; +import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin'; import { Dispatch, SetStateAction } from 'react'; import { UseQueryResult } from 'react-query'; @@ -21,6 +22,7 @@ export type PanelWrapperProps = { onClickHandler?: OnClickPluginOpts['onClick']; onDragSelect: (start: number, end: number) => void; selectedGraph?: PANEL_TYPES; + tableProcessedDataRef?: React.MutableRefObject; }; export type TooltipData = {