feat: add functionality to export dashboard as json from listing page

This commit is contained in:
Amlan Kumar Nandy 2024-12-16 12:10:21 +05:30 committed by GitHub
commit 9a1cd65b73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 70 additions and 3 deletions

View File

@ -27,6 +27,8 @@ import { AxiosError } from 'axios';
import cx from 'classnames'; import cx from 'classnames';
import { ENTITY_VERSION_V4 } from 'constants/app'; import { ENTITY_VERSION_V4 } from 'constants/app';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { sanitizeDashboardData } from 'container/NewDashboard/DashboardDescription';
import { downloadObjectAsJson } from 'container/NewDashboard/DashboardDescription/utils';
import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils'; import { Base64Icons } from 'container/NewDashboard/DashboardSettings/General/utils';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard'; import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
@ -44,6 +46,7 @@ import {
EllipsisVertical, EllipsisVertical,
Expand, Expand,
ExternalLink, ExternalLink,
FileJson,
Github, Github,
HdmiPort, HdmiPort,
LayoutGrid, LayoutGrid,
@ -67,12 +70,18 @@ import {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { generatePath, Link } from 'react-router-dom'; import { generatePath, Link } from 'react-router-dom';
import { useCopyToClipboard } from 'react-use'; import { useCopyToClipboard } from 'react-use';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll'; import {
Dashboard,
IDashboardVariable,
WidgetRow,
Widgets,
} from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app'; import { isCloudUser } from 'utils/app';
@ -261,6 +270,11 @@ function DashboardsList(): JSX.Element {
isLocked: !!e.isLocked || false, isLocked: !!e.isLocked || false,
lastUpdatedBy: e.updated_by, lastUpdatedBy: e.updated_by,
image: e.data.image || Base64Icons[0], image: e.data.image || Base64Icons[0],
variables: e.data.variables,
widgets: e.data.widgets,
layout: e.data.layout,
panelMap: e.data.panelMap,
version: e.data.version,
refetchDashboardList, refetchDashboardList,
})) || []; })) || [];
@ -413,6 +427,15 @@ function DashboardsList(): JSX.Element {
}); });
}; };
const handleJsonExport = (event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation();
event.preventDefault();
downloadObjectAsJson(
sanitizeDashboardData({ ...dashboard, title: dashboard.name }),
dashboard.name,
);
};
return ( return (
<div className="dashboard-list-item" onClick={onClickHandler}> <div className="dashboard-list-item" onClick={onClickHandler}>
<div className="title-with-action"> <div className="title-with-action">
@ -486,6 +509,14 @@ function DashboardsList(): JSX.Element {
> >
Copy Link Copy Link
</Button> </Button>
<Button
type="text"
className="action-btn"
icon={<FileJson size={12} />}
onClick={handleJsonExport}
>
Export JSON
</Button>
</section> </section>
<section className="section-2"> <section className="section-2">
<DeleteButton <DeleteButton
@ -504,6 +535,7 @@ function DashboardsList(): JSX.Element {
<EllipsisVertical <EllipsisVertical
className="dashboard-action-icon" className="dashboard-action-icon"
size={14} size={14}
data-testid="dashboard-action-icon"
onClick={(e): void => { onClick={(e): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -1068,6 +1100,11 @@ export interface Data {
isLocked: boolean; isLocked: boolean;
id: string; id: string;
image?: string; image?: string;
widgets?: Array<WidgetRow | Widgets>;
layout?: Layout[];
panelMap?: Record<string, { widgets: Layout[]; collapsed: boolean }>;
variables: Record<string, IDashboardVariable>;
version?: string;
} }
export default DashboardsList; export default DashboardsList;

View File

@ -65,7 +65,7 @@ interface DashboardDescriptionProps {
handle: FullScreenHandle; handle: FullScreenHandle;
} }
function sanitizeDashboardData( export function sanitizeDashboardData(
selectedData: DashboardData, selectedData: DashboardData,
): Omit<DashboardData, 'uuid'> { ): Omit<DashboardData, 'uuid'> {
if (!selectedData?.variables) { if (!selectedData?.variables) {

View File

@ -1,13 +1,21 @@
/* eslint-disable sonarjs/no-duplicate-string */ /* eslint-disable sonarjs/no-duplicate-string */
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import DashboardsList from 'container/ListOfDashboard'; import DashboardsList from 'container/ListOfDashboard';
import { dashboardEmptyState } from 'mocks-server/__mockdata__/dashboards'; import * as dashboardUtils from 'container/NewDashboard/DashboardDescription';
import {
dashboardEmptyState,
dashboardSuccessResponse,
} from 'mocks-server/__mockdata__/dashboards';
import { server } from 'mocks-server/server'; import { server } from 'mocks-server/server';
import { rest } from 'msw'; import { rest } from 'msw';
import { DashboardProvider } from 'providers/Dashboard/Dashboard'; import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { MemoryRouter, useLocation } from 'react-router-dom'; import { MemoryRouter, useLocation } from 'react-router-dom';
import { fireEvent, render, waitFor } from 'tests/test-utils'; import { fireEvent, render, waitFor } from 'tests/test-utils';
jest.mock('container/NewDashboard/DashboardDescription', () => ({
sanitizeDashboardData: jest.fn(),
}));
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), ...jest.requireActual('react-router-dom'),
useLocation: jest.fn(), useLocation: jest.fn(),
@ -204,4 +212,26 @@ describe('dashboard list page', () => {
), ),
); );
}); });
it('ensure that the export JSON popover action works correctly', async () => {
const { getByText, getAllByTestId } = render(<DashboardsList />);
await waitFor(() => {
const popovers = getAllByTestId('dashboard-action-icon');
expect(popovers).toHaveLength(dashboardSuccessResponse.data.length);
fireEvent.click([...popovers[0].children][0]);
});
const exportJsonBtn = getByText('Export JSON');
expect(exportJsonBtn).toBeInTheDocument();
fireEvent.click(exportJsonBtn);
const firstDashboardData = dashboardSuccessResponse.data[0];
expect(dashboardUtils.sanitizeDashboardData).toHaveBeenCalledWith(
expect.objectContaining({
id: firstDashboardData.uuid,
title: firstDashboardData.data.title,
createdAt: firstDashboardData.created_at,
}),
);
});
}); });