diff --git a/env/qat.js b/env/qat.js index 677c72abf..84bea097e 100644 --- a/env/qat.js +++ b/env/qat.js @@ -4,7 +4,7 @@ module.exports = { API_BASE_URL: 'https://api.fiscaldata.treasury.gov', DATA_DOWNLOAD_BASE_URL: 'https://fiscaldata.treasury.gov', WEB_SOCKET_BASE_URL: 'wss://downloads.fiscaldata.treasury.gov/main', - EXPERIMENTAL_WHITELIST: ['experimental-page', 'afg-overview', 'publishedReportsSection'], + EXPERIMENTAL_WHITELIST: ['experimental-page', 'afg-overview', 'publishedReportsSection', 'dataPreview'], ADDITIONAL_DATASETS: {}, USE_MOCK_RELEASE_CALENDAR_DATA_ON_API_FAIL: true, ADDITIONAL_ENDPOINTS: {}, diff --git a/src/components/__tests__/year-range-filter.bypass.js b/src/components/__tests__/year-range-filter.bypass.js deleted file mode 100644 index df9d4de6b..000000000 --- a/src/components/__tests__/year-range-filter.bypass.js +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import renderer from 'react-test-renderer'; -import YearRangeFilter from '../data-preview/year-range-filter/year-range-filter'; - -let component; -let instance; -let startYear; -let endYear; -let startYearOpts; -let endYearOpts; -let outboundStartYear; -let outboundEndYear; - -const mockChangeHandler = (startYear, endYear) => { - outboundStartYear = startYear; - outboundEndYear = endYear; - return { startYear, endYear }; -}; - -beforeEach(() => { - component = renderer.create(); - renderer.act(() => { - component = renderer.create( - - ); - }); - instance = component.root; - [startYear, endYear] = instance.findAllByType('select'); - startYearOpts = startYear.findAllByType('option'); - endYearOpts = endYear.findAllByType('option'); -}); -describe('YearRangeFilter', () => { - it('contains to and from selectable year options for all years within range passed in', () => { - expect(startYearOpts.length).toBe(10); - expect(startYearOpts[5].props.value).toBe(2016); - expect(endYearOpts.length).toBe(10); - expect(startYearOpts[5].props.value).toBe(2016); - }); - - it('selects correct initial values based on incoming props', () => { - expect(startYear.props.value).toBe(2015); - expect(endYear.props.value).toBe(2016); - }); - - it('hands the correct values to its changeHandler when user updates start year', () => { - renderer.act(() => { - startYear.props.onChange({ - target: { value: 2014 }, - }); - }); - expect(outboundStartYear).toBe(2014); - expect(outboundEndYear).toBe(2016); - }); - - it('hands the correct values to its changeHandler when user updates end year', () => { - renderer.act(() => { - return endYear.props.onChange({ - target: { value: 2017 }, - }); - }); - expect(outboundStartYear).toBe(2015); - expect(outboundEndYear).toBe(2017); - }); - - it('if start-year selected is greater than end year, end-year will update to same value as start-year', () => { - renderer.act(() => { - return startYear.props.onChange({ - target: { value: 2019 }, - }); - }); - expect(outboundStartYear).toBe(2019); - expect(outboundEndYear).toBe(2019); - }); - - it('if end-year selected is less than end year, start-year will update to same value as end-year', () => { - renderer.act(() => - endYear.props.onChange({ - target: { value: 2013 }, - }) - ); - expect(outboundStartYear).toBe(2013); - expect(outboundEndYear).toBe(2013); - }); -}); diff --git a/src/components/data-preview/__snapshots__/data-preview.spec.js.snap b/src/components/data-preview/__snapshots__/data-preview.spec.js.snap new file mode 100644 index 000000000..6a4a8ae4d --- /dev/null +++ b/src/components/data-preview/__snapshots__/data-preview.spec.js.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DataPreview correctly prepares pivoted data with aggregation and summing and handles non-numeric + values 1`] = ` +{ + "data": [ + { + "CHART_DATE": "2020-05-01", + "Federal Bank": 1010.1010000000001, + "Medical Safe": 3020202, + "record_calendar_year__record_calendar_month": "May 2020", + }, + { + "CHART_DATE": "2020-04-01", + "Federal Bank": "1000.0001", + "Medical Safe": 3000000.7, + "record_calendar_year__record_calendar_month": "Apr 2020", + }, + ], + "meta": { + "dataTypes": { + "Federal Bank": "CURRENCY", + "Medical Safe": "CURRENCY", + "record_calendar_year__record_calendar_month": "AGGREGATION_DATE", + }, + "labels": { + "Federal Bank": "Federal Bank", + "Medical Safe": "Medical Safe", + "record_calendar_year__record_calendar_month": "Time Period", + }, + }, + "pivotApplied": "By Classification:cost", +} +`; + +exports[`DataPreview correctly prepares pivoted data without aggregation 1`] = ` +{ + "data": [ + { + "Federal Financing Bank": "2.685", + "Total Marketable": "1.964", + "Treasury Bills": "0.596", + "Treasury Bonds": "3.764", + "Treasury Floating Rate Note (FRN)": "0.285", + "Treasury Inflation-Protected Securities (TIPS)": "0.751", + "Treasury Notes": "2.069", + "reporting_date": "2020-04-30", + }, + { + "Federal Financing Bank": "2.385", + "Total Marketable": "1.264", + "Treasury Bills": "1.596", + "Treasury Bonds": "3.164", + "Treasury Nickels": "3.864", + "reporting_date": "2020-05-31", + }, + ], + "meta": { + "dataTypes": { + "Federal Financing Bank": "PERCENTAGE", + "Total Marketable": "PERCENTAGE", + "Treasury Bills": "PERCENTAGE", + "Treasury Bonds": "PERCENTAGE", + "Treasury Floating Rate Note (FRN)": "PERCENTAGE", + "Treasury Inflation-Protected Securities (TIPS)": "PERCENTAGE", + "Treasury Nickels": "PERCENTAGE", + "Treasury Notes": "PERCENTAGE", + "reporting_date": "DATE", + }, + "labels": { + "Federal Financing Bank": "Federal Financing Bank", + "Total Marketable": "Total Marketable", + "Treasury Bills": "Treasury Bills", + "Treasury Bonds": "Treasury Bonds", + "Treasury Floating Rate Note (FRN)": "Treasury Floating Rate Note (FRN)", + "Treasury Inflation-Protected Securities (TIPS)": "Treasury Inflation-Protected Securities (TIPS)", + "Treasury Nickels": "Treasury Nickels", + "Treasury Notes": "Treasury Notes", + "reporting_date": "Calendar Date", + }, + }, + "pivotApplied": "by sec type:avg_interest_rate_amt", +} +`; diff --git a/src/components/data-preview/data-preview-data-table/data-preview-data-table-body/data-preview-data-table-body.module.scss b/src/components/data-preview/data-preview-data-table/data-preview-data-table-body/data-preview-data-table-body.module.scss new file mode 100644 index 000000000..e3532925f --- /dev/null +++ b/src/components/data-preview/data-preview-data-table/data-preview-data-table-body/data-preview-data-table-body.module.scss @@ -0,0 +1,41 @@ +@import 'src/variables.module'; + +.fillCellGrey, +.fillCellWhite { + color: #555555; +} + +.detailButton { + background-color: transparent; + border: none; + color: $primary; + text-decoration: underline; + cursor: pointer; + font-size: $font-size-16; + font-weight: 400; +} + +.fillCellGrey { + background: #f1f1f1; +} + +.fillCellWhite { + background: white; +} + +.cellBorder { + border-top: 1px solid #d9d9d9; + border-bottom: 1px solid #d9d9d9; +} + +.rightAlignText { + text-align: right; +} + +.cellText { + vertical-align: top; +} + +.hidden { + overflow: hidden; +} diff --git a/src/components/data-preview/data-preview-data-table/data-preview-data-table-body/data-preview-data-table-body.tsx b/src/components/data-preview/data-preview-data-table/data-preview-data-table-body/data-preview-data-table-body.tsx new file mode 100644 index 000000000..ef501b7bc --- /dev/null +++ b/src/components/data-preview/data-preview-data-table/data-preview-data-table-body/data-preview-data-table-body.tsx @@ -0,0 +1,74 @@ +import React, { FunctionComponent, ReactNode } from 'react'; +import { IDataTableBody } from '../../../../models/IDataTableBody'; +import { + cellBorder, + cellText, + detailButton, + fillCellGrey, + fillCellWhite, + hidden, + rightAlignText, +} from '../../../data-table/data-table-body/data-table-body.module.scss'; +import classNames from 'classnames'; +import { rightAlign } from '../../../data-table/data-table-helper'; +import { flexRender } from '@tanstack/react-table'; + +const DataPreviewDataTableBody: FunctionComponent = ({ + table, + dataTypes, + allowColumnWrap, + detailViewConfig, + setDetailViewState, + setSummaryValues, +}) => { + let fillCell = false; + const handleDetailClick = (rowConfig: {}, cellValue: string) => { + const currentRow = rowConfig[0]?.row.original; + const secondaryFilterValue = detailViewConfig?.secondaryField ? currentRow[detailViewConfig.secondaryField] : null; + setDetailViewState({ value: cellValue, secondary: secondaryFilterValue }); + setSummaryValues(currentRow); + }; + + return ( + + {table.getRowModel().rows.map(row => { + fillCell = !fillCell; + const rowConfig = row.getVisibleCells(); + return ( + + {rowConfig.map(cell => { + const cellValue = cell.getValue()?.toString(); + const display = !cellValue || cellValue === 'null'; + const wrapStyle = allowColumnWrap?.includes(cell.column.id); + const detailViewButton = detailViewConfig?.field === cell.column.id; + const cellDisplay = (children: ReactNode) => + detailViewButton ? ( + + ) : ( + <>{children} + ); + + return ( + + {display ?
: <>{cellDisplay(flexRender(cell.column.columnDef.cell, cell.getContext()))}} + + ); + })} + + ); + })} + + ); +}; + +export default DataPreviewDataTableBody; diff --git a/src/components/data-preview/data-preview-data-table/data-preview-data-table-header/data-preview-data-table-header.module.scss b/src/components/data-preview/data-preview-data-table/data-preview-data-table-header/data-preview-data-table-header.module.scss new file mode 100644 index 000000000..36dff8f23 --- /dev/null +++ b/src/components/data-preview/data-preview-data-table/data-preview-data-table-header/data-preview-data-table-header.module.scss @@ -0,0 +1,94 @@ +@import '../../../../variables.module.scss'; + +.sortArrowPill, +.defaultSortArrowPill { + height: 1.25rem; + width: 1.75rem; + min-width: 1.75rem; + border-radius: 1.25rem; + display: flex; + align-items: center; + justify-content: center; + margin-left: 0.375rem; + cursor: pointer; +} + +.stickyHeader { + position: sticky; + top: 0; +} + +.sortArrowPill { + background-color: $primary; +} + +.defaultSortArrowPill:hover { + background-color: #cfe8ff; +} + +.sortArrow, +.defaultSortArrow { + width: 1rem !important; +} + +.sortArrow { + margin: auto 0.375rem; + color: white; +} + +.defaultSortArrow { + color: $primary; +} + +.colHeader { + display: flex; + align-items: center; + padding: 0.5rem 0; + white-space: nowrap; + + &.rightAlignText { + justify-content: flex-end; + } + + &.noFilter { + margin-bottom: 2.5rem; + } +} + +.colHeaderText { + text-overflow: ellipsis; + overflow: hidden; + min-width: 1.25rem; +} + +.resizer { + position: absolute; + right: 0; + top: 0; + height: 100%; + width: 0.3125rem; + background: $font-body-copy; + cursor: col-resize; + user-select: none; + touch-action: none; + border-radius: 1.25rem; +} + +.resizer.isResizing { + background: $font-body-copy; + opacity: 1; +} + +.columnMinWidth { + min-width: 160px; +} + +@media (hover: hover) { + .resizer { + opacity: 0; + } + + *:hover > .resizer { + opacity: 1; + } +} diff --git a/src/components/data-preview/data-preview-data-table/data-preview-data-table-header/data-preview-data-table-header.tsx b/src/components/data-preview/data-preview-data-table/data-preview-data-table-header/data-preview-data-table-header.tsx new file mode 100644 index 000000000..a21dbca9a --- /dev/null +++ b/src/components/data-preview/data-preview-data-table/data-preview-data-table-header/data-preview-data-table-header.tsx @@ -0,0 +1,138 @@ +import React, { FunctionComponent } from 'react'; +import { IDataTableHeader } from '../../../../models/IDataTableHeader'; +import { withStyles } from '@material-ui/core/styles'; +import Tooltip from '@material-ui/core/Tooltip'; +import { + colHeader, + colHeaderText, + columnMinWidth, + defaultSortArrow, + defaultSortArrowPill, + isResizing, + resizer, + rightAlignText, + sortArrow, + sortArrowPill, + stickyHeader, +} from '../../../data-table/data-table-header/data-table-header.module.scss'; +import { getColumnFilter, rightAlign } from '../../../data-table/data-table-helper'; +import { flexRender } from '@tanstack/react-table'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowDownWideShort, faArrowRightArrowLeft, faArrowUpShortWide } from '@fortawesome/free-solid-svg-icons'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; + +const DataPreviewDataTableHeader: FunctionComponent = ({ table, dataTypes, allActiveFilters, setAllActiveFilters }) => { + const LightTooltip = withStyles(() => ({ + tooltip: { + color: '#555555', + fontSize: 16, + fontWeight: 600, + fontFamily: 'Source Sans Pro', + marginLeft: '1.25rem', + marginTop: '0.25rem', + borderRadius: '0.25rem', + background: '#FFF', + boxShadow: '0.25rem 0.25rem 1rem 0 rgba(0, 0, 0, 0.15), 0 0 0.125rem 0 rgba(0, 0, 0, 0.20)', + }, + }))(Tooltip); + + const iconClick = (state, header, e) => { + if (e.key === undefined || e.key === 'Enter') { + header.column.toggleSorting(); + if (state === 'asc' || state === 'false') { + if (allActiveFilters && !allActiveFilters.includes(`${header.column.id}-sort`)) { + const currentFilters = allActiveFilters?.filter(item => !item.includes('sort')); + currentFilters.push(`${header.column.id}-sort`); + setAllActiveFilters(currentFilters); + } + } else { + const currentFilters = allActiveFilters?.filter(item => item !== `${header.column.id}-sort`); + setAllActiveFilters(currentFilters); + } + } + }; + + return ( + + {table.getHeaderGroups().map(headerGroup => { + return ( + + {headerGroup.headers.map((header, index) => { + let isLastColumn = false; + const columnDataType = dataTypes[header.id]; + const rightAlignStyle = rightAlign(columnDataType) ? rightAlignText : null; + if (!headerGroup.headers[index + 1]) { + isLastColumn = true; + } + return ( + + {header.isPlaceholder ? null : ( + <> +
+ +
{flexRender(header.column.columnDef.header, header.getContext())}
+
+ {{ + asc: ( +
iconClick('asc', header, e)} + onKeyDown={e => iconClick('asc', header, e)} + > + +
+ ), + desc: ( +
iconClick('desc', header, e)} + onKeyDown={e => iconClick('desc', header, e)} + > + +
+ ), + false: ( +
iconClick('false', header, e)} + onKeyDown={e => iconClick('false', header, e)} + > + +
+ ), + }[header.column.getIsSorted() as string] ?? null} +
+ + )} +
+ + ); + })} + + ); + })} + + ); +}; + +export default DataPreviewDataTableHeader; diff --git a/src/components/data-preview/data-preview-data-table/data-preview-data-table.module.scss b/src/components/data-preview/data-preview-data-table/data-preview-data-table.module.scss new file mode 100644 index 000000000..a235a0ab0 --- /dev/null +++ b/src/components/data-preview/data-preview-data-table/data-preview-data-table.module.scss @@ -0,0 +1,119 @@ +@import '../../../variables.module.scss'; + +$borderRadius: 5px; + +.tableStyle { + overflow-x: auto; + width: 100%; + order: 1; + + .rawDataTableContainer, .nonRawDataTableContainer { + position: relative; + box-sizing: border-box; + border-top: 0; + background-color: white; + overflow-y: auto; + } + + .rawDataTableContainer { + height: 521px; + } + .nonRawDataTableContainer { + max-height: 521px; + } + + table { + margin-top: -3px; + } + + table, + .divTable { + border-bottom: 1px solid lightgray; + width: fit-content; + } + + th, + .th { + cursor: default; + background-color: white; + position: relative; + font-weight: bold; + text-align: center; + height: 1.875rem; + padding: 0.25rem 0.5rem 0.75rem; + } + + .tr { + display: flex; + } + + tr, + .tr { + width: fit-content; + height: 1.875rem; + } + + td, + .td { + padding: 0.25rem 0.5rem; + } + + #th-parent { + display: flex; + flex-direction: row; + justify-content: flex-end; + } + + td, + .td { + height: 1.875rem; + } + + +} +.range { + font-weight: $semi-bold-weight; +} + +.overlayContainer, +.overlayContainerNoFooter { + position: relative; +} + +.overlayContainerNoFooter { + border: 1px solid $dd-border-color; + border-top: 0; + border-bottom-left-radius: $borderRadius; + border-bottom-right-radius: $borderRadius; +} + +.selectColumnsWrapper { + display: flex; + + .selectColumnPanelInactive { + display: none; + } + + .selectColumnPanelActive { + display: block; + border-left: 1px solid $border-color; + margin-top: -2.75rem; + } + + .selectColumnPanelActive, + .selectColumnPanelInactive { + height: 565px; + order: 2; + width: 450px; + } +} + +.downloadLinkContainer { + display: flex; + flex-direction: row; + gap: 0.5rem; +} + +.downloadLinkIcon { + color: $primary; +} diff --git a/src/components/data-preview/data-preview-data-table/data-preview-data-table.spec.js b/src/components/data-preview/data-preview-data-table/data-preview-data-table.spec.js new file mode 100644 index 000000000..3d08687ca --- /dev/null +++ b/src/components/data-preview/data-preview-data-table/data-preview-data-table.spec.js @@ -0,0 +1,586 @@ +import { render, within } from '@testing-library/react'; +import React from 'react'; +import { fireEvent } from '@testing-library/dom'; +import { RecoilRoot } from 'recoil'; +import { + mockTableData, + mockPublishedReports, + mockMeta, + defaultColumnsTypeCheckMock, + defaultSelectedColumnsMock, + mockGenericTableData, + allColLabels, + mockTableData1Row, + mockGenericTableColumns, + defaultColLabels, + additionalColLabels, + mockColumnConfig, + mockDetailViewColumnConfig, + mockDetailApiData, +} from './../../data-table/data-table-test-helper'; +import userEvent from '@testing-library/user-event'; +import DataPreviewDataTable from './data-preview-data-table'; + +describe('react-table', () => { + const setTableColumnSortData = jest.fn(); + + global.fetch = jest.fn(() => { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockDetailApiData), + }); + }); + + it('table renders', () => { + const instance = render( + + + + ); + expect(instance).toBeTruthy(); + }); + + it('table renders with select columns option', () => { + const instance = render( + + + + ); + expect(instance).toBeTruthy(); + }); + + it('table renders generic non raw data table', () => { + const instance = render( + + + + ); + expect(instance).toBeTruthy(); + }); + + it('renders headers', () => { + const mostResetFilter = jest.fn(); + const { getByRole } = render( + + + + ); + expect(getByRole('columnheader', { name: 'Record Date' })).toBeInTheDocument(); + }); + + it('column sort keyboard accessibility', () => { + const mockSorting = jest.fn(); + const { getAllByTestId, getByRole } = render( + + + + ); + // Column header + expect(getByRole('columnheader', { name: 'Record Date' })).toBeInTheDocument(); + // Rows render + expect(getAllByTestId('row').length).toEqual(6); + const header = getByRole('columnheader', { name: 'Record Date' }); + const sortButton = within(header).getAllByRole('img', { hidden: true })[0]; + expect(sortButton).toHaveClass('defaultSortArrow'); + expect(getAllByTestId('row')[0].innerHTML).toContain('7/12/2023'); + fireEvent.keyDown(sortButton, { key: 'Enter' }); + // Now sorted in desc order + expect(mockSorting).toHaveBeenCalledWith(['record_date-sort']); + fireEvent.keyDown(sortButton, { key: 'Enter' }); + fireEvent.keyDown(sortButton, { key: 'Enter' }); + //Sorting should be reset + expect(getAllByTestId('row')[0].innerHTML).toContain('7/12/2023'); + }); + + // it('Filter column by text search', () => { + // const { getAllByTestId, getByRole } = render( + // + // + // + // ); + // // Column header + // const header = getByRole('columnheader', { name: 'Debt Held by the Public' }); + // expect(header).toBeInTheDocument(); + // // Rows render + // expect(getAllByTestId('row').length).toEqual(6); + // const columnFilter = within(header).getByRole('textbox'); + // expect(columnFilter).toBeInTheDocument(); + // fireEvent.change(columnFilter, { target: { value: '25633821130387.02' } }); + // // Rows filtered down to 1 + // expect(getAllByTestId('row').length).toEqual(1); + // expect(getAllByTestId('row')[0].innerHTML).toContain('$25,633,821,130,387.02'); + // + // //clear results to view full table + // const clearButton = within(header).getByRole('button', { name: 'Clear search bar' }); + // userEvent.click(clearButton); + // expect(getAllByTestId('row').length).toEqual(6); + // }); + + // it('Filter column by text search with null string value', () => { + // const { getAllByTestId, getByRole, queryAllByTestId } = render( + // + // + // + // ); + // // Column header + // const header = getByRole('columnheader', { name: 'Mock Percent String' }); + // expect(header).toBeInTheDocument(); + // // Rows render + // expect(getAllByTestId('row').length).toEqual(6); + // const columnFilter = within(header).getByRole('textbox'); + // expect(columnFilter).toBeInTheDocument(); + // + // // Search should not match to 'null' values + // fireEvent.change(columnFilter, { target: { value: 'null' } }); + // expect(queryAllByTestId('row').length).toEqual(0); + // }); + + it('pagination', () => { + const { getAllByTestId, getByText, getByRole, getByTestId } = render( + + + + ); + + const header = getByRole('columnheader', { name: 'Record Date' }); + expect(header).toBeInTheDocument(); + // Rows render + expect(getAllByTestId('row').length).toEqual(2); + + expect(getByText('Showing', { exact: false })).toBeInTheDocument(); + expect(getByText('1 - 2', { exact: false })).toBeInTheDocument(); + expect(getByText('rows of 6 rows', { exact: false })).toBeInTheDocument(); + expect(getByTestId('page-next-button')).toBeInTheDocument(); + + userEvent.click(getByTestId('page-next-button')); + + expect(getByText('Showing', { exact: false })).toBeInTheDocument(); + expect(getByText('3 - 4', { exact: false })).toBeInTheDocument(); + }); + + it('initially renders all columns showing when no defaults specified', () => { + const { getAllByRole } = render( + + + + ); + + const visibleColumns = getAllByRole('columnheader'); + expect(visibleColumns.length).toBe(allColLabels.length); + visibleColumns.forEach(col => { + const header = col.children[0].children[0].innerHTML; + expect(allColLabels.includes(header)); + }); + }); + + it('pagination for 0 rows of data', () => { + const { getByText, getByRole } = render( + + + + ); + + const header = getByRole('columnheader', { name: 'Record Date' }); + expect(header).toBeInTheDocument(); + expect(getByText('Showing', { exact: false })).toBeInTheDocument(); + expect(getByText('rows of 0 rows', { exact: false })).toBeInTheDocument(); + }); + + it('pagination for 1 row of data', () => { + const { getByText, getByRole } = render( + + + + ); + + const header = getByRole('columnheader', { name: 'Record Date' }); + expect(header).toBeInTheDocument(); + expect(getByText('Showing', { exact: false })).toBeInTheDocument(); + expect(getByText('1 - 1', { exact: false })).toBeInTheDocument(); + expect(getByText('of 1 row', { exact: false })).toBeInTheDocument(); + }); + + it('hides specified columns', () => { + const { getAllByRole, queryByRole } = render( + + + + ); + const hiddenCol = 'Source Line Number'; + expect(queryByRole('columnheader', { name: hiddenCol })).not.toBeInTheDocument(); + + const visibleColumns = getAllByRole('columnheader'); + const allVisibleColumnLabels = allColLabels.filter(x => x !== hiddenCol); + expect(visibleColumns.length).toBe(allVisibleColumnLabels.length); + + visibleColumns.forEach(col => { + const header = col.children[0].children[0].innerHTML; + expect(allVisibleColumnLabels.includes(header)); + }); + }); + + it('initially renders only default columns showing when defaults specified', () => { + const { getAllByRole, queryAllByRole } = render( + + + + ); + + // default col in table + defaultColLabels.forEach(index => { + if (index === 'Record Date') { + index = 'Record Date'; + } + expect(getAllByRole('columnheader', { name: index })[0]).toBeInTheDocument(); + }); + + // additional col not in table + expect(queryAllByRole('columnheader').length).toEqual(3); + additionalColLabels.forEach(index => { + expect(queryAllByRole('columnheader', { name: index })[0]).not.toBeDefined(); + }); + }); + it('formats PERCENTAGE types correctly', () => { + const { getAllByTestId } = render( + + + + ); + expect(getAllByTestId('row')[0].innerHTML).toContain('4%'); + }); + + it('formats SMALL_FRACTION types correctly', () => { + const { getAllByTestId } = render( + + + + ); + expect(getAllByTestId('row')[0].innerHTML).toContain('0.00067898'); + }); + + it('formats custom NUMBER types correctly', () => { + const customFormatter = [{ type: 'NUMBER', fields: ['spread'], decimalPlaces: 6 }]; + + const { getAllByTestId } = render( + + + + ); + expect(getAllByTestId('row')[0].innerHTML).toContain('-0.120000'); + }); + + it('formats custom STRING dateList types correctly', () => { + const customFormatter = [{ type: 'STRING', fields: ['additional_date'], breakChar: ',', customType: 'dateList' }]; + + const { getAllByTestId } = render( + + + + ); + expect(getAllByTestId('row')[0].innerHTML).toContain('1/1/2024, 2/2/2023'); + }); + + it('formats CURRENCY3 types correctly', () => { + const { getAllByTestId } = render( + + + + ); + expect(getAllByTestId('row')[0].innerHTML).toContain('$6,884,574,686,385.150'); + }); + + it('formats * CURRENCY3 types correctly', () => { + const { getAllByTestId } = render( + + + + ); + expect(getAllByTestId('row')[2].innerHTML).toContain('*'); + expect(getAllByTestId('row')[2].innerHTML).not.toContain('(*)'); + }); + + it('formats negative CURRENCY3 types correctly', () => { + const { getAllByTestId } = render( + + + + ); + expect(getAllByTestId('row')[0].innerHTML).toContain('-$134.100'); + }); + + it('formats FRN Daily Index number values correctly', () => { + const { getAllByTestId } = render( + + + + ); + + expect(getAllByTestId('row')[0].innerHTML).toContain('0.111111111'); + expect(getAllByTestId('row')[0].innerHTML).toContain('0.222222222'); + expect(getAllByTestId('row')[0].innerHTML).toContain('-0.120'); + }); + + it('renders detail view links', async () => { + const setDetailViewSpy = jest.fn(); + const setSummaryValuesSpy = jest.fn(); + const { getByRole } = render( + + + + ); + const detailViewButton = getByRole('button', { name: '7/12/2023' }); + expect(detailViewButton).toBeInTheDocument(); + + userEvent.click(detailViewButton); + expect(setDetailViewSpy).toHaveBeenCalledWith({ secondary: null, value: '2023-07-12' }); + expect(setSummaryValuesSpy).toHaveBeenCalledWith(mockTableData.data[0]); + }); +}); diff --git a/src/components/data-preview/data-preview-data-table/data-preview-data-table.tsx b/src/components/data-preview/data-preview-data-table/data-preview-data-table.tsx new file mode 100644 index 000000000..b56818018 --- /dev/null +++ b/src/components/data-preview/data-preview-data-table/data-preview-data-table.tsx @@ -0,0 +1,267 @@ +import React, { FunctionComponent, useEffect, useRef, useState } from 'react'; +import { IDataTableProps } from '../../../models/IDataTableProps'; +import { useSetRecoilState } from 'recoil'; +import { + smallTableDownloadDataCSV, + smallTableDownloadDataJSON, + smallTableDownloadDataXML, + tableRowLengthState, +} from '../../../recoil/smallTableDownloadData'; +import { columnsConstructorData, columnsConstructorGeneric, getSortedColumnsData } from '../../data-table/data-table-helper'; +import { getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, Table, useReactTable } from '@tanstack/react-table'; +import { json2xml } from 'xml-js'; +import { overlayContainerNoFooter, rawDataTableContainer, selectColumnsWrapper, tableStyle } from './data-preview-data-table.module.scss'; +import DataTableFooter from '../../data-table/data-table-footer/data-table-footer'; +import DataPreviewDataTableBody from './data-preview-data-table-body/data-preview-data-table-body'; +import DataPreviewDataTableHeader from './data-preview-data-table-header/data-preview-data-table-header'; + +const DataPreviewDataTable: FunctionComponent = ({ + rawData, + defaultSelectedColumns, + setTableColumnSortData, + shouldPage, + showPaginationControls, + publishedReports, + hasPublishedReports, + resetFilters, + setResetFilters, + hideCellLinks, + tableName, + hideColumns, + pagingProps, + manualPagination, + rowsShowing, + columnConfig, + detailColumnConfig, + detailView, + detailViewAPI, + detailViewState, + setDetailViewState, + allowColumnWrap, + aria, + pivotSelected, + setSummaryValues, + customFormatting, + sorting, + setSorting, + allActiveFilters, + setAllActiveFilters, + setTableSorting, + disableDateRangeFilter, +}) => { + const [configOption, setConfigOption] = useState(columnConfig); + const setSmallTableCSVData = useSetRecoilState(smallTableDownloadDataCSV); + const setSmallTableJSONData = useSetRecoilState(smallTableDownloadDataJSON); + const setSmallTableXMLData = useSetRecoilState(smallTableDownloadDataXML); + const setTableRowSizeData = useSetRecoilState(tableRowLengthState); + + useEffect(() => { + if (!detailViewState) { + setConfigOption(columnConfig); + } else { + setConfigOption(detailColumnConfig); + } + }, [rawData]); + + const allColumns = React.useMemo(() => { + const hideCols = detailViewState ? detailViewAPI.hideColumns : hideColumns; + + const baseColumns = columnsConstructorData(rawData, hideCols, tableName, configOption, customFormatting); + + return baseColumns; + }, [rawData, configOption]); + if (hasPublishedReports && !hideCellLinks) { + // Must be able to modify allColumns, thus the ignore + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + allColumns[0].cell = ({ getValue }) => { + if ( + publishedReports.find(report => { + return report.report_date.toISOString().split('T')[0] === getValue(); + }) !== undefined + ) { + const path = publishedReports.find(report => { + return report.report_date.toISOString().split('T')[0] === getValue(); + }).path; + return {getValue()}; + } else { + return {getValue()}; + } + }; + } + + let dataTypes; + + if (rawData.meta) { + dataTypes = rawData.meta.dataTypes; + } else { + const tempDataTypes = {}; + allColumns?.forEach(column => { + tempDataTypes[column.property] = 'STRING'; + }); + dataTypes = tempDataTypes; + } + + const defaultInvisibleColumns = {}; + const [columnVisibility, setColumnVisibility] = useState( + defaultSelectedColumns && defaultSelectedColumns.length > 0 && !pivotSelected ? defaultInvisibleColumns : {} + ); + const [defaultColumns, setDefaultColumns] = useState([]); + const [additionalColumns, setAdditionalColumns] = useState([]); + const table = useReactTable({ + columns: allColumns, + data: rawData.data, + columnResizeMode: 'onChange', + initialState: { + pagination: { + pageIndex: 0, + pageSize: pagingProps.itemsPerPage, + }, + }, + state: { + columnVisibility, + sorting, + }, + onSortingChange: setSorting, + onColumnVisibilityChange: setColumnVisibility, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + manualPagination: manualPagination, + }) as Table>; + + // We need to be able to access the accessorKey (which is a type violation) hence the ts ignore + if (defaultSelectedColumns) { + for (const column of allColumns) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (defaultSelectedColumns && !defaultSelectedColumns?.includes(column.accessorKey)) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + defaultInvisibleColumns[column.accessorKey] = false; + } + } + } + + const constructDefaultColumnsFromTableData = () => { + const constructedDefaultColumns = []; + const constructedAdditionalColumns = []; + for (const column of table.getAllLeafColumns()) { + if (defaultSelectedColumns.includes(column.id)) { + constructedDefaultColumns.push(column); + } else if (!defaultSelectedColumns.includes(column.id)) { + constructedAdditionalColumns.push(column); + } + } + constructedAdditionalColumns.sort((a, b) => { + return a.id.localeCompare(b.id); + }); + setDefaultColumns(constructedDefaultColumns); + setAdditionalColumns(constructedAdditionalColumns); + }; + + useEffect(() => { + if (defaultSelectedColumns && !pivotSelected) { + constructDefaultColumnsFromTableData(); + } + if (detailViewState) { + setColumnVisibility(defaultInvisibleColumns); + } + }, [configOption]); + + useEffect(() => { + getSortedColumnsData(table, setTableColumnSortData, hideColumns, dataTypes); + if (!table.getSortedRowModel()?.flatRows[0]?.original.columnName) { + let downloadData = []; + const downloadHeaders = []; + const downloadHeaderKeys = []; + table.getHeaderGroups()[0].headers.forEach(header => { + downloadHeaders.push(header.column.columnDef.header); + downloadHeaderKeys.push(header.column.columnDef.accessorKey); + }); + + //Filter data by visible columns + table.getSortedRowModel().flatRows.forEach(row => { + const visibleRow = {}; + const allData = row.original; + downloadHeaderKeys.forEach(key => { + visibleRow[key] = allData[key]; + }); + downloadData.push(visibleRow); + }); + const xmlData = { + 'root-element': { + data: downloadData.map(row => ({ + 'data-element': row, + })), + }, + }; + setSmallTableJSONData(JSON.stringify({ data: downloadData })); + setSmallTableXMLData(json2xml(JSON.stringify(xmlData), { compact: true })); + downloadData = downloadData.map(entry => { + return Object.values(entry); + }); + downloadData.unshift(downloadHeaders); + setSmallTableCSVData(downloadData); + } + }, [columnVisibility, table.getSortedRowModel(), table.getVisibleFlatColumns()]); + + useEffect(() => { + getSortedColumnsData(table, setTableColumnSortData, hideColumns, dataTypes); + setTableSorting(sorting); + }, [sorting]); + + useEffect(() => { + if (resetFilters) { + table.resetColumnFilters(); + table.resetSorting(); + setResetFilters(false); + setAllActiveFilters([]); + } + }, [resetFilters]); + + return ( + <> +
+
+
+
+ + + +
+
+
+
+
+ {shouldPage && ( + + )} + + ); +}; + +export default DataPreviewDataTable; diff --git a/src/components/data-preview/data-preview-filter-section/data-preview-download/data-preview-download.module.scss b/src/components/data-preview/data-preview-filter-section/data-preview-download/data-preview-download.module.scss new file mode 100644 index 000000000..43fc2c323 --- /dev/null +++ b/src/components/data-preview/data-preview-filter-section/data-preview-download/data-preview-download.module.scss @@ -0,0 +1,42 @@ +@import '../../../../variables.module.scss'; + +.wrapper { + background-color: #ffffff; + height: calc(100% - 2rem); + box-shadow: 0 -10px 13px -8px rgba(0, 0, 0, 0.2); + text-align: center; + border-radius: 0 0 5px 5px; + display: flex; + padding: 1rem; + flex-direction: column; + justify-content: center; +} + +#iconDiv { + margin-bottom: 0.5rem; +} + +.icon { + border: 5px solid #000; + padding: 0.5rem 0.75rem; + border-radius: 100px; +} + +.dateStringStyle { + white-space: nowrap; +} + +.describer { + margin-bottom: 0.5rem; +} + +.downloadDescription { + margin-bottom: 0.5rem; +} + +@media (min-width: $breakpoint-sm) { + .wrapper { + box-shadow: -10px 0 13px -8px rgba(0, 0, 0, 0.2); + border-radius: 0 5px 5px 0; + } +} diff --git a/src/components/data-preview/data-preview-filter-section/data-preview-download/data-preview-download.spec.js b/src/components/data-preview/data-preview-filter-section/data-preview-download/data-preview-download.spec.js new file mode 100644 index 000000000..52ad725a1 --- /dev/null +++ b/src/components/data-preview/data-preview-filter-section/data-preview-download/data-preview-download.spec.js @@ -0,0 +1,69 @@ +import { render } from '@testing-library/react'; +import React from 'react'; +import DataPreviewDownload from './data-preview-download'; +import { enableFetchMocks } from 'jest-fetch-mock'; +import renderer from 'react-test-renderer'; +import { RecoilRoot } from 'recoil'; + +jest.mock('../../../../components/truncate/truncate.jsx', function() { + return { + __esModule: true, + default: jest.fn().mockImplementation(({ children }) => children), + }; +}); + +jest.useFakeTimers(); + +describe('data preview download', () => { + enableFetchMocks(); + let createObjectURL; + + const mockSelectedTableWithUserFilter = { + tableName: 'Table 1', + userFilter: { + label: 'Country-Currency', + field: 'country_currency_desc', + }, + }; + + const mockSelectedUserFilter = { + label: 'Atlantis-Aquabuck', + value: 'Atlantis-Aquabuck', + }; + + const mockSelectedDetailViewFilter = { + label: 'CUSIP', + field: 'cusip', + value: 'ABCD123', + }; + + beforeAll(() => { + createObjectURL = global.URL.createObjectURL; + global.URL.createObjectURL = jest.fn(); + }); + + afterAll(() => { + global.URL.createObjectURL = createObjectURL; + }); + + // Jest gives an error about the following not being implemented even though the tests pass. + HTMLCanvasElement.prototype.getContext = jest.fn(); + + const nonFilteredDate = 'ALL'; + let component = renderer.create(); + renderer.act(() => { + component = renderer.create( + + + + ); + }); + const instance = component.root; + + // const downloadItemButtons = instance.findAllByType(DownloadItemButton); + + it('renders a placeholder', () => { + const theComponent = instance.findByProps({ 'data-test-id': 'data-preview-download' }); + expect(theComponent).toBeDefined(); + }); +}); diff --git a/src/components/data-preview/data-preview-filter-section/data-preview-download/data-preview-download.tsx b/src/components/data-preview/data-preview-filter-section/data-preview-download/data-preview-download.tsx new file mode 100644 index 000000000..76769eb62 --- /dev/null +++ b/src/components/data-preview/data-preview-filter-section/data-preview-download/data-preview-download.tsx @@ -0,0 +1,225 @@ +import React, { FunctionComponent, useContext, useEffect, useState } from 'react'; +import { downloadsContext } from '../../../persist/download-persist/downloads-persist'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { reactTableFilteredDateRangeState } from '../../../../recoil/reactTableFilteredState'; +import { + calcDictionaryDownloadSize, + convertDataDictionaryToCsv, + triggerDataDictionaryDownload, +} from '../../../download-wrapper/data-dictionary-download-helper'; +import { disableDownloadButtonState } from '../../../../recoil/disableDownloadButtonState'; +import { tableRowLengthState } from '../../../../recoil/smallTableDownloadData'; +import { generateAnalyticsEvent } from '../../../../layouts/dataset-detail/helper'; +import { ensureDoubleDigitDate, formatDate } from '../../../download-wrapper/helpers'; +import Analytics from '../../../../utils/analytics/analytics'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faFileDownload, faSpinner } from '@fortawesome/free-solid-svg-icons'; +import { isValidDateRange } from '../../../../helpers/dates/date-helpers'; +import { REACT_TABLE_MAX_NON_PAGINATED_SIZE } from '../../../../utils/api-utils'; + +type DownloadProps = { + selectedTable; + allTablesSelected; + dateRange; + dataset; + isFiltered; + selectedUserFilter; + tableColumnSortData; + filteredDateRange; + selectedDetailViewFilter; +}; + +const DataPreviewDownload: FunctionComponent = ({ + selectedTable, + allTablesSelected, + dateRange, + dataset, + isFiltered, + selectedUserFilter, + tableColumnSortData, + filteredDateRange, + selectedDetailViewFilter, +}) => { + let tableName = selectedTable && selectedTable.tableName ? selectedTable.tableName : 'N/A'; + if (allTablesSelected) { + tableName = `All Data Tables (${dataset.apis.length})`; + } + + const allString = 'ALL'; + const siteDownloads = useContext(downloadsContext); + const [selectedFileType, setSelectedFileType] = useState('csv'); + const [dateString, setDateString] = useState(''); + const [open, setOpen] = useState(false); + const [disableButton, setDisableButton] = useState(false); + const [downloadLabel, setDownloadLabel] = useState(null); + const [datasetDownloadInProgress, setDatasetDownloadInProgress] = useState(false); + const [changeMadeToCriteria, setChangeMadeToCriteria] = useState(false); + const [icon, setIcon] = useState(null); + const { setDownloadRequest, downloadsInProgress, downloadsPrepared, setCancelDownloadRequest } = siteDownloads; + const setDapGaEventLabel = useSetRecoilState(reactTableFilteredDateRangeState); + const [gaEventLabel, setGaEventLabel] = useState(); + + const dataDictionaryCsv = convertDataDictionaryToCsv(dataset); + const ddSize = calcDictionaryDownloadSize(dataDictionaryCsv); + const globalDisableDownloadButton = useRecoilValue(disableDownloadButtonState); + const tableSize = useRecoilValue(tableRowLengthState); + + const makeDownloadButtonAvailable = () => { + if (datasetDownloadInProgress) { + setDatasetDownloadInProgress(false); + /** + * This is used by the downloadsInProgress useEffect to not disable the + * button again if the user happened to change something before the download + * process advances. + */ + setChangeMadeToCriteria(true); + } + }; + + const toggleButtonChange = value => { + setSelectedFileType(value); + makeDownloadButtonAvailable(); + }; + + const handleCancelRequest = value => { + generateAnalyticsEvent(gaEventLabel, cancelEventActionStr); + if (setCancelDownloadRequest) { + setCancelDownloadRequest(value); + } + }; + + const fileFromPath = path => (path && path.length ? path.substring(path.lastIndexOf('/') + 1) : null); + + const dateForFilename = fileDate => { + const fullYear = fileDate.getFullYear(); + const month = ensureDoubleDigitDate(fileDate.getMonth() + 1); + const date = ensureDoubleDigitDate(fileDate.getDate()); + + return `${fullYear}${month}${date}`; + }; + + const metadataDownloader = async () => { + Analytics.event({ + category: 'Dataset Dictionary Download', + action: 'Data Dictionary Click', + label: dataset.name, + }); + return triggerDataDictionaryDownload(dataDictionaryCsv, dataset.name); + }; + + const downloadClickHandler = event => { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + setChangeMadeToCriteria(false); + const apis = allTablesSelected ? dataset.apis.slice() : selectedTable; + const downloadName = allTablesSelected ? `${(dataset.slug + '').replace(/\//g, '')}_all_tables` : `${fileFromPath(selectedTable.downloadName)}`; + const downloadEntry = { + datasetId: dataset.datasetId, + apis: apis, + dateRange: { + from: new Date(dateRange.from.getTime()), + to: new Date(dateRange.to.getTime()), + }, + selectedFileType, + filename: `${downloadName}_${dateForFilename(dateRange.from)}_${dateForFilename(dateRange.to)}.zip`, + requestTime: Date.now(), + selectedUserFilter, + tableColumnSortData, + filteredDateRange, + selectedDetailViewFilter, + }; + setDownloadRequest(downloadEntry); + setOpen(true); + + return false; + }; + + const onClose = () => { + generateAnalyticsEvent(gaEventLabel, closeEventActionStr); + setOpen(false); + }; + + const generateDownloadLabel = inProgress => { + if (allTablesSelected && inProgress) { + return `Downloading Files`; + } else if (allTablesSelected && !inProgress) { + return `Download ${dataset.apis.length} ${selectedFileType.toUpperCase()} Files`; + } else if (!allTablesSelected && inProgress) { + return `Downloading File`; + } else if (!allTablesSelected && !inProgress) { + return `Download ${selectedFileType.toUpperCase()} File`; + } + }; + + const setIconComponent = inProgress => { + return inProgress ? ( + + ) : ( + + ); + }; + + useEffect(() => { + makeDownloadButtonAvailable(); + setDownloadLabel(generateDownloadLabel(datasetDownloadInProgress)); + }, [allTablesSelected, selectedFileType, selectedTable, dateRange]); + + useEffect(() => { + if (dateRange?.from && dateRange?.to) { + setGaEventLabel(`Table Name: ${selectedTable?.tableName}, Type: ${selectedFileType}, Date Range: ${dateRange.from}-${dateRange.to}`); + } + }, [selectedTable, dateRange, selectedFileType]); + + useEffect(() => { + setDapGaEventLabel(gaEventLabel); + }, [gaEventLabel]); + + useEffect(() => { + if (dateRange) { + const from = formatDate(dateRange.from); + const to = formatDate(dateRange.to); + const { earliestDate, latestDate } = dataset.techSpecs; + + if (isValidDateRange(from, to, earliestDate, latestDate)) { + setDateString(`${from} - ${to}`); + } + } + }, [dateRange]); + + useEffect(() => { + if (downloadsInProgress === undefined) return; + if (changeMadeToCriteria) return; + setDatasetDownloadInProgress(downloadsInProgress.some(dl => dl.datasetId === dataset.datasetId)); + }, [downloadsInProgress, changeMadeToCriteria]); + + useEffect(() => { + if (datasetDownloadInProgress === undefined) return; + setDownloadLabel(generateDownloadLabel(datasetDownloadInProgress)); + setIcon(setIconComponent(datasetDownloadInProgress)); + setDisableButton(datasetDownloadInProgress); + }, [datasetDownloadInProgress]); + + useEffect(() => { + setDisableButton(globalDisableDownloadButton); + }, [globalDisableDownloadButton]); + + const determineDirectDownload = () => { + if (tableSize !== null && tableSize <= REACT_TABLE_MAX_NON_PAGINATED_SIZE && !allTablesSelected) { + // return

This is where the download will go

; + return

this is where local download logic will go

; + } else { + return

this is where api download logic will go

; + } + }; + + return ( +
+

download placeholder

+ <>{determineDirectDownload()} +
+ ); +}; + +export default DataPreviewDownload; diff --git a/src/components/data-preview/data-preview-filter-section/data-preview-filter-section.tsx b/src/components/data-preview/data-preview-filter-section/data-preview-filter-section.tsx new file mode 100644 index 000000000..31ea01c00 --- /dev/null +++ b/src/components/data-preview/data-preview-filter-section/data-preview-filter-section.tsx @@ -0,0 +1,62 @@ +import { FunctionComponent, useEffect, useState } from 'react'; +import determineDateRange, { + generateAnalyticsEvent, + generateFormattedDate, + prepAvailableDates, +} from '../../filter-download-container/range-presets/helpers/helper'; +import { addDays, differenceInYears, subQuarters } from 'date-fns'; +import { monthNames } from '../../../utils/api-utils'; +import { fitDateRangeToTable } from '../../filter-download-container/range-presets/range-presets'; +import React from 'react'; +import DownloadWrapper from '../../download-wrapper/download-wrapper'; +import DataPreviewDownload from './data-preview-download/data-preview-download'; +import DateRangeFilter from './date-range-filter/date-range-filter'; + +type DataPreviewFilterSectionProps = { + children; + dateRange; + isFiltered; + selectedTable; + dataset; + allTablesSelected; + isCustomDateRange; + selectedUserFilter; + tableColumnSortData; + filteredDateRange; + selectedDetailViewFilter; +}; + +const DataPreviewFilterSection: FunctionComponent = ({ + children, + dateRange, + isFiltered, + selectedTable, + dataset, + allTablesSelected, + isCustomDateRange, + selectedUserFilter, + tableColumnSortData, + filteredDateRange, + selectedDetailViewFilter, +}) => { + return ( + <> +

Filtering placeholder in DataPreviewFilterSection

+
{children}
+ + + ); +}; + +export default DataPreviewFilterSection; diff --git a/src/components/data-preview/data-preview-filter-section/date-range-filter/date-range-filter.tsx b/src/components/data-preview/data-preview-filter-section/date-range-filter/date-range-filter.tsx new file mode 100644 index 000000000..624c0b72e --- /dev/null +++ b/src/components/data-preview/data-preview-filter-section/date-range-filter/date-range-filter.tsx @@ -0,0 +1,233 @@ +import React, { FunctionComponent, useEffect, useState } from 'react'; +import determineDateRange, { + generateAnalyticsEvent, + generateFormattedDate, + prepAvailableDates, +} from '../../../filter-download-container/range-presets/helpers/helper'; +import { addDays, differenceInYears, subQuarters } from 'date-fns'; +import { fitDateRangeToTable } from '../../../filter-download-container/range-presets/range-presets'; +import { monthNames } from '../../../../utils/api-utils'; +import DatePickers from '../../../filter-download-container/datepickers/datepickers'; + +type DateRangeFilterProps = { + currentDateButton; + datePreset; + customRangePreset; + selectedTable; + apiData; + handleDateRangeChange; + setIsFiltered; + setIsCustomDateRange; + allTablesSelected; + datasetDateRange; + finalDatesNotFound; + hideButtons; +}; + +const DateRangeFilter: FunctionComponent = ({ + currentDateButton, + datePreset, + customRangePreset, + selectedTable, + apiData, + handleDateRangeChange, + setIsFiltered, + setIsCustomDateRange, + allTablesSelected, + datasetDateRange, + finalDatesNotFound, + hideButtons, +}) => { + const [activePresetKey, setActivePresetKey] = useState(null); + const [availableDateRange, setAvailableDateRange] = useState(null); + const [pickerDateRange, setPickerDateRange] = useState(null); + const [dateRange, setCurDateRange] = useState(null); + const [presets, setPresets] = useState([]); + const [initialLoad, setInitialLoad] = useState(true); + const basePreset = [{ label: 'All', key: 'all', years: null }]; + const possiblePresets = [ + { label: '1 Year', key: '1yr', years: 1 }, + { label: '5 Years', key: '5yr', years: 5 }, + { label: '10 Years', key: '10yr', years: 10 }, + ]; + const customPreset = { label: 'Custom', key: 'custom', years: null }; + // Not all datasets will have 5 years of information; but, this is the ideal default preset. + let idealDefaultPreset = { key: '5yr', years: 5 }; + // If a data table has less than 5 years of data, we need to find the next best option to select + // by default. + const fallbackPresets = ['1yr', 'current', 'all']; + + const allTablesDateRange = prepAvailableDates(datasetDateRange); + /** + * DATE RANGE + */ + + const applyPreset = preset => { + let isFiltered = true; + + let label = preset.label; + if (label && label.toLowerCase() === 'custom') { + label = generateFormattedDate(dateRange); + } + generateAnalyticsEvent(label); + + setActivePresetKey(preset.key); + setIsCustomDateRange(preset.key === customPreset.key); + + if (preset.key !== customPreset.key) { + prepUpdateDateRange(preset); + } else { + handleDateRangeChange(dateRange); + } + + if (preset.key === 'all') { + isFiltered = false; + } + + setIsFiltered(isFiltered); + }; + + const prepUpdateDateRange = preset => { + const curDateRange = determineDateRange(availableDateRange, preset, currentDateButton); + updateDateRange(curDateRange); + }; + + const updateDateRange = curDateRange => { + if (curDateRange) { + setPickerDateRange(availableDateRange); + setCurDateRange(curDateRange); + handleDateRangeChange(curDateRange); + } + }; + + const placeApplicableYearPresets = ({ to, from }) => { + const curPresets = basePreset.slice(); + const dateYearDifference = differenceInYears(to, from); + + for (let i = possiblePresets.length; i--; ) { + if (possiblePresets[i].years <= dateYearDifference) { + possiblePresets.length = i + 1; + curPresets.unshift(...possiblePresets); + break; + } + } + + return curPresets; + }; + + const setMostAppropriatePreset = () => { + if (presets && presets.length) { + // If the currently selected date option is available on the newly created presets, + // then keep the current selection. + const curSelectedOption = presets.find(preset => preset.key === activePresetKey); + if (curSelectedOption) { + // We need to pass back the date range for the new data table. Note, the actual dates + // might not be the same from the previously selected table, even though the preset is + // the same. + if (curSelectedOption.key === 'custom') { + const adjustedRange = fitDateRangeToTable(dateRange, availableDateRange); + setPickerDateRange(availableDateRange); + setCurDateRange(adjustedRange); + handleDateRangeChange(adjustedRange); + } else { + prepUpdateDateRange(curSelectedOption); + } + return; + } + if (datePreset === 'current' && presets[0].key === 'current') { + idealDefaultPreset = presets[0]; + } + if (datePreset === 'all' && presets[4].key === 'all') { + idealDefaultPreset = presets[4]; + } + if (datePreset === 'custom' && customRangePreset === 'latestQuarter') { + idealDefaultPreset = presets.find(({ key }) => key === 'custom'); + + const dateObj = new Date(Date.parse(datasetDateRange.latestDate)); + const quarterRange = { + userSelected: { + from: subQuarters(addDays(dateObj, 1), 1), + to: dateObj, + }, + }; + const adjRange = fitDateRangeToTable(quarterRange, availableDateRange); + updateDateRange(adjRange); + } + // Check if the default date option is available in the preset list. If so, select the default + // preset, else select the next available option. + const defaultPresetIsFound = presets.some(preset => preset.key === idealDefaultPreset.key); + let defaultKey = null; + + // If the desired default preset is not available because of the date range on the dataset + // table, find the next appropriate button to highlight + if (!defaultPresetIsFound) { + for (let i = 0, il = fallbackPresets.length; i < il; i++) { + const fallbackPreset = presets.find(p => p.key === fallbackPresets[i]); + if (fallbackPreset) { + defaultKey = fallbackPreset; + break; + } + } + } else { + defaultKey = idealDefaultPreset; + } + applyPreset(defaultKey); + } + }; + + useEffect(() => { + setMostAppropriatePreset(); + }, [presets]); + + useEffect(() => { + if (selectedTable.userFilter && apiData?.data && initialLoad) { + setInitialLoad(false); + applyPreset(customPreset); + } + }, [apiData]); + + useEffect(() => { + if (!finalDatesNotFound) { + const availableRangeForSelection = allTablesSelected ? allTablesDateRange : prepAvailableDates(selectedTable); + setAvailableDateRange(availableRangeForSelection); + const curPresets = placeApplicableYearPresets(availableRangeForSelection); + + if (currentDateButton) { + const latestDate = availableRangeForSelection.to; + let buttonLabel; + + const month = latestDate.getMonth(); + const date = latestDate.getDate(); + const fullYear = latestDate.getFullYear(); + + if (currentDateButton === 'byDay') { + buttonLabel = latestDate ? `${monthNames[month]} ${date}, ${fullYear}` : ''; + } else if (currentDateButton === 'byLast30Days') { + buttonLabel = latestDate ? 'Last 30 Days' : ''; + } else { + buttonLabel = latestDate ? monthNames[month] + ' ' + fullYear.toString() : ''; + } + curPresets.unshift({ label: buttonLabel, key: 'current', years: null }); + } + curPresets.push(customPreset); + setPresets(curPresets); + } + }, [allTablesSelected, finalDatesNotFound, selectedTable]); + + useEffect(() => { + // This hook is used for nested tables + // when the summary view date range is locked, all rows should display + if (hideButtons) { + setActivePresetKey('all'); + } + }, [hideButtons]); + + const label = + selectedTable && selectedTable.fields + ? ` (${selectedTable.fields.find(field => field.columnName === selectedTable.dateField).prettyName})` + : null; + + return

Date range placeholder

; +}; + +export default DateRangeFilter; diff --git a/src/components/data-preview/data-preview-pivot-options/data-preview-pivot-options.tsx b/src/components/data-preview/data-preview-pivot-options/data-preview-pivot-options.tsx new file mode 100644 index 000000000..939e68a78 --- /dev/null +++ b/src/components/data-preview/data-preview-pivot-options/data-preview-pivot-options.tsx @@ -0,0 +1,84 @@ +import React, { FunctionComponent, useEffect, useState } from 'react'; +import Analytics from '../../../utils/analytics/analytics'; + +type PivotOptions = { + datasetName; + table; + pivotSelection; + setSelectedPivot; + pivotsUpdated; +}; + +const DataPreviewPivotOptions: FunctionComponent = ({ datasetName, table, pivotSelection, setSelectedPivot, pivotsUpdated }) => { + const [pivotOptions, setPivotOptions] = useState(); + const [pivotFields, setPivotFields] = useState(); + + const setAppropriatePivotValue = pivotOptions => { + let valueField = pivotOptions[0]; + const curSelectedPivotValue = pivotSelection.pivotValue; + if (curSelectedPivotValue) { + if (pivotOptions.some(pivot => pivot.columnName === curSelectedPivotValue.columnName)) { + valueField = curSelectedPivotValue; + } + } + return valueField; + }; + + const pivotViewChangeHandler = view => { + let valueField = null; + let curPivotFields = pivotFields; + if (view.dimensionField) { + const uniquePivotValues = view.uniquePivotValues; + if (uniquePivotValues && uniquePivotValues.length) { + curPivotFields = uniquePivotValues; + } + valueField = setAppropriatePivotValue(curPivotFields); + } + + Analytics.event({ + category: 'Chart Enabled', + action: 'Pivot View Click', + label: `${view.title}, ${datasetName}, ${table.tableName}`, + }); + + setSelectedPivot({ pivotView: view, pivotValue: valueField }); + setPivotOptions(view.dimensionField ? curPivotFields : [{ prettyName: '— N / A —' }]); + }; + + const pivotValueChangeHandler = valueField => { + if (valueField?.prettyName !== '— N / A —') { + Analytics.event({ + category: 'Chart Enabled', + action: 'Pivot Value Click', + label: `${valueField?.prettyName}, ${datasetName}, ${table.tableName}`, + }); + + setSelectedPivot({ pivotView: pivotSelection.pivotView, pivotValue: valueField }); + } + }; + + const getPivotFields = table => { + if (table && table.valueFieldOptions) { + return table.fields.filter(field => table.valueFieldOptions.indexOf(field.columnName) !== -1); + } else { + return null; + } + }; + + useEffect(() => { + if (table && !table.allDataTables) { + const localPivotFields = getPivotFields(table); + setPivotFields(localPivotFields); + const pivot = { + pivotView: table.dataDisplays ? table.dataDisplays[0] : null, + pivotValue: localPivotFields && table.dataDisplays[0].dimensionField ? localPivotFields[0] : null, + }; + setSelectedPivot(pivot); + setPivotOptions(pivot.pivotView.dimensionField ? localPivotFields : [{ prettyName: '— N / A —' }]); + } + }, [table, pivotsUpdated]); + + return <>; +}; + +export default DataPreviewPivotOptions; diff --git a/src/components/data-preview/data-preview-section-container/data-preview-section-container.module.scss b/src/components/data-preview/data-preview-section-container/data-preview-section-container.module.scss new file mode 100644 index 000000000..a0a144d36 --- /dev/null +++ b/src/components/data-preview/data-preview-section-container/data-preview-section-container.module.scss @@ -0,0 +1,151 @@ +@import '../../../variables.module.scss'; +@import '../../../zIndex.module.scss'; + +$table-container-margin: 8rem; + +.titleContainer { + border-bottom: 1px solid $dd-border-color; + position: relative; + z-index: $table-title; +} +.tableContainer { + min-height: 44.125rem; +} + + +.sectionBorder { + border: solid #d6d7d9 1px; + border-radius: 5px; +} + +.tableSection { + min-height: 36.5rem; +} + +.detailViewButton { + height: 28px; + width: 74px; + background-color: $primary; + border: none; + border-radius: 3px; + margin-right: 1rem; + color: $content-section-background; + padding: 4px 8px 4px 8px; + justify-content: space-between; + display: flex; + align-items: center; + cursor: pointer; +} + +.detailViewBack { + font-size: $font-size-16; + font-weight: $semi-bold-weight; +} + +.detailViewIcon { + width: 1rem; + height: 1rem; +} + +.tableContainer { + min-height: calc(#{$loading-icon-size} + #{$table-container-margin}); // height of icon + top and bottom margin + position: relative; +} + +.loadingSection { + z-index: $table-loading-section; + height: calc(100% - 5.5rem); + width: 100% ; + position: absolute; + background-color: rgba(255, 255, 255, 0.85); + transition: opacity 0.5s 0.5s, left 0.5s ease-in-out; + display: block; + animation: fadeIn 1s; + overflow: hidden; + margin-top: 5.5rem; +} + +@media screen and (max-width: $breakpoint-lg) { + .loadingSection { + height: calc(100% - 5.875rem); + margin-top: 5.875rem; + } +} + +.loadingIcon { + z-index: $table-loading-icon; + color: $font-title; + font-size: $font-size-18; + width: 100%; + top: 7rem; + text-align: center; + position: absolute; + + > * { + vertical-align: middle; + margin-right: 0.5rem; + font-size: $loading-icon-size; + color: $loading-icon-color; + } +} + +.headerWrapper { + display: flex; + align-items: center; + white-space: nowrap; + padding: 1rem 1rem 0.5rem; + + & > * { + margin-bottom: 0.5rem; + } +} + +.header { + @include headingStyle6; + margin-left: 0.5rem; + margin-right: 1rem; + text-overflow: ellipsis; + overflow: hidden; + flex-grow: 1; +} + +@media screen and (min-width: $breakpoint-md) { + .headerWrapper { + padding: 0.5rem 1rem 0; + } +} + +.barContainer { + background-color: $body-background; +} + +.noticeContainer { + padding: 0.5rem; + background-color: $body-background; +} + +.barExpander { + max-height: 0; + transform: scaleY(-0); + transition: transform 0.2s ease, max-height 0.2s ease; + + &.active { + transform: scaleY(1); + max-height: 60px; + } +} + +@media screen and (max-width: $breakpoint-sm) { + .barExpander.active { + max-height: 92px; + } + .headerWrapper { + flex-wrap: wrap; + } +} + +@media screen and (max-width: $breakpoint-md) { + .barExpander.active { + max-height: 170px; + } +} diff --git a/src/components/data-preview/data-preview-section-container/data-preview-section-container.spec.js b/src/components/data-preview/data-preview-section-container/data-preview-section-container.spec.js new file mode 100644 index 000000000..cf778971a --- /dev/null +++ b/src/components/data-preview/data-preview-section-container/data-preview-section-container.spec.js @@ -0,0 +1,634 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { active } from './data-preview-section-container.module.scss'; +import { + mockConfig, + mockDateRange, + mockTableWithPivot, + mockApiData, + mockTableWithNoChartAvailable, + selectedTableLessFields, + selectedPivot, + pivotFields, + selectedPivotWithAggregation, + mockTableWithUserFilterAvailable, + mockApiDataUserFilterable, + selectedPivotWithRoundingDenomination, + mockTableWithApiFilterAvailable, + mockDetailConfig, + mockTableWithApiFilterAvailableDisplayDefaultData, +} from '../../dataset-data/table-section-container/testHelpers'; +// import * as setNoChartMessageMod from './set-no-chart-message'; +// import AggregationNotice from './aggregation-notice/aggregation-notice'; +import GLOBALS from '../../../helpers/constants'; +import { render, fireEvent } from '@testing-library/react'; +// import NotShownMessage from './not-shown-message/not-shown-message'; +import { RecoilRoot } from 'recoil'; +import DataPreviewTable from '../data-preview-table/data-preview-table'; +import DataPreviewSectionContainer from './data-preview-section-container'; + +// describe('DataPreviewSectionContainer initial state', () => { +// let component, instance; +// const mockSetSelectedPivot = jest.fn(); +// +// beforeAll(() => { +// component = renderer.create( +// +// +// +// ); +// +// instance = component.root; +// }); + +// it('hides the table component when there is no data', () => { +// expect(instance.findAllByType(DataPreviewTable).length).toBe(0); +// }); +// }); + +describe('DataPreviewSectionContainer while loading', () => { + const mockSetSelectedPivot = jest.fn(); + let queryTestId; + beforeAll(() => { + const { queryByTestId } = render( + + + + ); + queryTestId = queryByTestId; + }); + + // it('provides the loading section while the table is loading', () => { + // expect(queryTestId('loadingSection')).toBeNull(); + // }); + it('does not show detailView on initial render', () => { + expect(queryTestId('detailViewCloseButton')).not.toBeInTheDocument(); + }); +}); + +describe('DataPreviewSectionContainer with data', () => { + const selectedTable = selectedTableLessFields; + let component = renderer.create(), + instance; + const mockSetSelectedPivot = jest.fn(); + + renderer.act(() => { + component = renderer.create( + + + + ); + }); + + instance = component.root; + + it('displays the table component when there is data', () => { + expect(instance.findAllByType(DataPreviewTable).length).toBe(1); + }); + + it('sets noBorder on the table', () => { + expect(instance.findByType(DataPreviewTable).props.tableProps.noBorder).toBeDefined(); + }); + + // it('sends slug and currentTableName props to DatasetChart', () => { + // const datasetChartElement = instance.findByType(DatasetChart); + // expect(datasetChartElement.props.slug).toBe(mockConfig.slug); + // expect(datasetChartElement.props.currentTable).toBe(selectedTable); + // }); + // + // it('shows no pivot options toggle when none are available', () => { + // renderer.act(() => { + // component.update( + // + // + // + // ); + // }); + // instance = component.root; + // expect(instance.findAllByType(PivotToggle).length).toEqual(0); + // }); +}); + +describe('DataPreviewSectionContainer with userFilter Options', () => { + // it('displays the NotShownMessage when a user filter is engaged that matches no rows', () => { + // let tableSectionContainer = {}; + // renderer.act(() => { + // tableSectionContainer = renderer.create( + // + // + // + // ); + // }); + // + // const notShownMessages = tableSectionContainer.root.findAllByType(NotShownMessage); + // expect(notShownMessages.length).toStrictEqual(2); + // notShownMessages.forEach(notShownMessage => { + // expect(notShownMessage.props.heading).toContain('The Facility Description specified does not have'); + // expect(notShownMessage.props.heading).toContain('available data within the date range selected.'); + // expect(notShownMessage.props.bodyText).toStrictEqual(mockTableWithUserFilterAvailable.userFilter.dataUnmatchedMessage); + // }); + // }); +}); + +describe('DataPreviewSectionContainer with Pivot Options', () => { + let component = renderer.create(), + instance; + const mockSetSelectedPivot = jest.fn(); + + renderer.act(() => { + component = renderer.create( + + + + ); + }); + + instance = component.root; + + // it('shows a pivot options toggle button when pivots are available', () => { + // expect(instance.findAllByType(PivotToggle).length).toEqual(1); + // }); + + // it('pivot options are in view by default', () => { + // const { getByTestId } = render( + // + // + // + // ); + // expect(getByTestId('pivotOptionsDrawer').className).toContain(active); + // }); + + // it('shows no aggregation notice when the selected pivot is not aggregated', () => { + // expect(instance.findAllByType(AggregationNotice)).toEqual([]); + // }); + + // it('collapses/expands the pivot options when the toggle button is clicked', () => { + // const { getByTestId } = render( + // + // + // + // ); + // expect(getByTestId('pivotOptionsDrawer').className).toContain(active); + // fireEvent.click(getByTestId('pivotToggle')); + // expect(getByTestId('pivotOptionsDrawer').className).not.toContain(active); + // }); + + // it('toggle pivot view with rounding denomination', () => { + // const { getByTestId } = render( + // + // + // + // ); + // fireEvent.click(getByTestId('pivotToggle')); + // }); + + it('relays an endpoint value when it receives it in the serverSidePagination prop', async () => { + renderer.act(() => { + component.update( + + + + ); + }); + const table = instance.findByType(DataPreviewTable); + expect(table.props.tableProps.serverSidePagination).toEqual('ssp-endpoint'); + }); + + // it(`calls setNoChartMessage and if it returns something truthy, + // passes along the message returned rather than a chart`, () => { + // const noChartMsg = 'No-Chart Message Mock'; + // // case with a no-chart message + // setNoChartMessageMod['SetNoChartMessage'] = jest.fn().mockImplementation(() => noChartMsg); + // + // const { getByText, queryByText } = render( + // + // + // + // ); + // + // expect(setNoChartMessageMod.SetNoChartMessage).toBeCalled(); + // + // // in place of a chart it sends the message returned to chart table toggle + // expect(getByText('No-Chart Message Mock')).toBeInTheDocument(); + // expect(queryByText('Hide Legend')).toEqual(null); + // }); + + // it('calls setNoChartMessage and if value returned is falsy, attempts to make a DatasetChart', () => { + // // case without a no-chart message + // setNoChartMessageMod['SetNoChartMessage'] = jest.fn().mockImplementation(() => undefined); + // + // const { getByText, getByTestId } = render( + // + // + // + // ); + // + // expect(setNoChartMessageMod.SetNoChartMessage).toBeCalled(); + // expect(getByText('Hide Legend')).toBeInTheDocument(); + // expect(getByTestId('dataviz-line')).toBeInTheDocument(); + // }); + + // it('displays the aggregation notice when an aggregated pivot option is selected', () => { + // let tableSectionContainer = {}; + // renderer.act(() => { + // tableSectionContainer = renderer.create( + // + // + // + // ); + // }); + // + // const aggNotice = tableSectionContainer.root.findByType(AggregationNotice); + // expect(aggNotice).toBeDefined(); + // }); + + // it(`configures the legend to be hidden by default when the screen size is tablet width + // or below and keeps legend visibility tied to window size before the user interactively toggles + // the state.`, () => { + // let tableSectionContainer = renderer.create(); + // renderer.act(() => { + // global.window.innerWidth = GLOBALS.breakpoints.large; + // tableSectionContainer = renderer.create( + // + // + // + // ); + // }); + // + // let datasetChart = tableSectionContainer.root.findByType(DatasetChart); + // + // expect(datasetChart.props.legend).toBeFalsy(); + // + // renderer.act(() => { + // global.window.innerWidth = GLOBALS.breakpoints.large + 6; + // tableSectionContainer.update( + // + // + // + // ); + // }); + // + // datasetChart = tableSectionContainer.root.findByType(DatasetChart); + // + // expect(datasetChart.props.legend).toBeTruthy(); + // + // renderer.act(() => { + // global.window.innerWidth = GLOBALS.breakpoints.large - 125; + // tableSectionContainer.update( + // + // + // + // ); + // }); + // + // datasetChart = tableSectionContainer.root.findByType(DatasetChart); + // + // expect(datasetChart.props.legend).toBeFalsy(); + // }); + // + // it(`configures the legend to be visible by default when the screen size is wider than tablet + // width, but once the user interactively toggles the state, changes in screen size are ignored + // with respect to legend visibility`, () => { + // let tableSectionContainer = renderer.create(); + // const onToggleLegendEvent = { preventDefault: jest.fn() }; + // + // renderer.act(() => { + // global.window.innerWidth = GLOBALS.breakpoints.large + 1; + // tableSectionContainer = renderer.create( + // + // + // + // ); + // }); + // let datasetChart = tableSectionContainer.root.findByType(DatasetChart); + // expect(datasetChart.props.legend).toBeTruthy(); + // + // // "interactively" toggle the legend to INVISIBLE + // const chartTableToggle = tableSectionContainer.root.findByType(ChartTableToggle); + // renderer.act(() => { + // chartTableToggle.props.onToggleLegend(onToggleLegendEvent); + // }); + // datasetChart = tableSectionContainer.root.findByType(DatasetChart); + // expect(datasetChart.props.legend).toBeFalsy(); + // + // // "interactively" toggle the legend to VISIBLE + // renderer.act(() => { + // chartTableToggle.props.onToggleLegend(onToggleLegendEvent); + // }); + // datasetChart = tableSectionContainer.root.findByType(DatasetChart); + // expect(datasetChart.props.legend).toBeTruthy(); + // + // // Change the screen size be narrower than the tablet threshold + // renderer.act(() => { + // global.window.innerWidth = GLOBALS.breakpoints.large - 5; + // tableSectionContainer.update( + // + // + // + // ); + // }); + // datasetChart = tableSectionContainer.root.findByType(DatasetChart); + // // Expect legend to still be visible after change to tablet size + // expect(datasetChart.props.legend).toBeTruthy(); + // + // // "interactively" toggle the legend to INVISIBLE + // renderer.act(() => { + // chartTableToggle.props.onToggleLegend(onToggleLegendEvent); + // }); + // datasetChart = tableSectionContainer.root.findByType(DatasetChart); + // expect(datasetChart.props.legend).toBeFalsy(); + // + // // re-widen the screen size to desktop width + // renderer.act(() => { + // global.window.innerWidth = GLOBALS.breakpoints.large + 50; + // tableSectionContainer.update( + // + // + // + // ); + // }); + // datasetChart = tableSectionContainer.root.findByType(DatasetChart); + // // Expect legend to still be invisible after change to tablet + // expect(datasetChart.props.legend).toBeFalsy(); + // }); + + // it('renders selected detail view key with the dataset header', () => { + // const { queryByTestId } = render( + // + // + // + // ); + // expect(queryByTestId('tableName')).toBeInTheDocument(); + // }); + // }); + // describe('formatDate function', () => { + // it('formats date based on custom format if provided', () => { + // const selectedTable = { + // ...selectedTableLessFields, + // customFormatting: [{ type: 'DATE', dateFormat: 'MM/DD/YYYY' }], + // }; + // + // const { getByTestId } = render( + // + // + // + // ); + // expect(getByTestId('tableName').textContent).toContain('Table 1 > 06/01/2023'); + // }); +}); + +describe('Table with API filter', () => { + it('Initializes table with an api filter', () => { + const mockSetIsLoading = jest.fn(); + + const { queryByRole } = render( + + + + ); + expect(mockSetIsLoading).toHaveBeenCalledWith(false); + expect(queryByRole('table')).not.toBeInTheDocument(); + }); + it('Initializes table with an api filter and dispalyDefaultData is true', async () => { + const mockSetIsLoading = jest.fn(); + render( + + + + ); + expect(mockSetIsLoading).not.toHaveBeenCalledWith(false); + }); +}); diff --git a/src/components/data-preview/data-preview-section-container/data-preview-section-container.tsx b/src/components/data-preview/data-preview-section-container/data-preview-section-container.tsx new file mode 100644 index 000000000..b91f81f78 --- /dev/null +++ b/src/components/data-preview/data-preview-section-container/data-preview-section-container.tsx @@ -0,0 +1,431 @@ +import React, { FunctionComponent, useEffect, useMemo, useState } from 'react'; +import GLOBALS from '../../../helpers/constants'; +import { useSetRecoilState } from 'recoil'; +import { disableDownloadButtonState } from '../../../recoil/disableDownloadButtonState'; +import moment from 'moment'; +import { buildDateFilter, buildSortParams, fetchAllTableData, fetchTableMeta, formatDateForApi, MAX_PAGE_SIZE } from '../../../utils/api-utils'; +import { queryClient } from '../../../../react-query-client'; +import { setTableConfig } from '../../dataset-data/table-section-container/set-table-config'; +import Analytics from '../../../utils/analytics/analytics'; +import { determineUserFilterUnmatchedForDateRange } from '../../filter-download-container/user-filter/user-filter'; +import { SetNoChartMessage } from '../../dataset-data/table-section-container/set-no-chart-message'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowLeftLong, faSpinner, faTable } from '@fortawesome/free-solid-svg-icons'; +import AggregationNotice from '../../dataset-data/table-section-container/aggregation-notice/aggregation-notice'; +import SummaryTable from '../../dataset-data/table-section-container/summary-table/summary-table'; +import DataPreviewTable from '../data-preview-table/data-preview-table'; +import { + active, + barContainer, + barExpander, + detailViewBack, + detailViewButton, + detailViewIcon, + header, + headerWrapper, + loadingIcon, + loadingSection, + noticeContainer, + sectionBorder, + tableContainer, + tableSection, + titleContainer, +} from './data-preview-section-container.module.scss'; +import DataPreviewPivotOptions from '../data-preview-pivot-options/data-preview-pivot-options'; + +type DataPreviewSectionProps = { + config; + dateRange; + selectedTable; + apiData; + apiError; + userFilterSelection; + setUserFilterSelection; + selectedPivot; + setSelectedPivot; + serverSidePagination; + isLoading; + setIsLoading; + selectedTab; + tabChangeHandler; + handleIgnorePivots; + ignorePivots; + allTablesSelected; + handleConfigUpdate; + tableColumnSortData; + setTableColumnSortData; + hasPublishedReports; + publishedReports; + resetFilters; + setResetFilters; + detailViewState; + setDetailViewState; + customFormatting; + summaryValues; + setSummaryValues; + allActiveFilters; + setAllActiveFilters; +}; + +const DataPreviewSectionContainer: FunctionComponent = ({ + config, + dateRange, + selectedTable, + apiData, + apiError, + userFilterSelection, + setUserFilterSelection, + selectedPivot, + setSelectedPivot, + serverSidePagination, + isLoading, + setIsLoading, + selectedTab, + tabChangeHandler, + handleIgnorePivots, + ignorePivots, + allTablesSelected, + handleConfigUpdate, + tableColumnSortData, + setTableColumnSortData, + hasPublishedReports, + publishedReports, + resetFilters, + setResetFilters, + detailViewState, + setDetailViewState, + customFormatting, + summaryValues, + setSummaryValues, + allActiveFilters, + setAllActiveFilters, +}) => { + const tableName = selectedTable.tableName; + const [showPivotBar, setShowPivotBar] = useState(true); + const [tableProps, setTableProps] = useState(); + const [legend, setLegend] = useState(window.innerWidth > GLOBALS.breakpoints.large); + const [legendToggledByUser, setLegendToggledByUser] = useState(false); + const [pivotsUpdated, setPivotsUpdated] = useState(false); + const [hasPivotOptions, setHasPivotOptions] = useState(false); + const [userFilteredData, setUserFilteredData] = useState(null); + const [noChartMessage, setNoChartMessage] = useState(null); + const [userFilterUnmatchedForDateRange, setUserFilterUnmatchedForDateRange] = useState(false); + const [apiFilterDefault, setApiFilterDefault] = useState(!!selectedTable?.apiFilter); + const [selectColumnPanel, setSelectColumnPanel] = useState(false); + const [perPage, setPerPage] = useState(null); + const [reactTableSorting, setReactTableSort] = useState([]); + const [tableMeta, setTableMeta] = useState(null); + const [manualPagination, setManualPagination] = useState(false); + const [apiErrorState, setApiError] = useState(apiError || false); + const [chartData, setChartData] = useState(null); + + const setDisableDownloadButton = useSetRecoilState(disableDownloadButtonState); + const formatDate = detailDate => { + const fieldType = selectedTable.fields.find(field => field.columnName === config.detailView?.field)?.dataType; + const customFormat = selectedTable?.customFormatting?.find(config => config.type === 'DATE'); + return customFormat?.dateFormat && fieldType === 'DATE' ? moment(detailDate).format(customFormat.dateFormat) : detailDate; + }; + + const formattedDetailViewState = formatDate(detailViewState?.value); + + const applyApiFilter = () => selectedTable?.apiFilter?.displayDefaultData || (userFilterSelection !== null && userFilterSelection?.value !== null); + + const getDepaginatedData = async () => { + if (!selectedTable?.apiFilter || (selectedTable.apiFilter && applyApiFilter())) { + const from = formatDateForApi(dateRange.from); + const to = formatDateForApi(dateRange.to); + const dateFilter = buildDateFilter(selectedTable, from, to); + const sortParam = buildSortParams(selectedTable, selectedPivot); + const apiFilterParam = + selectedTable?.apiFilter?.field && userFilterSelection?.value !== null && userFilterSelection?.value !== undefined + ? `,${selectedTable?.apiFilter?.field}:eq:${userFilterSelection.value}` + : ''; + let meta; + return await queryClient + .ensureQueryData({ + queryKey: ['tableDataMeta', selectedTable, from, to, userFilterSelection], + queryFn: () => fetchTableMeta(sortParam, selectedTable, apiFilterParam, dateFilter), + }) + .then(async res => { + const totalCount = res.meta['total-count']; + if (!selectedPivot?.pivotValue) { + meta = res.meta; + if (totalCount !== 0 && totalCount <= MAX_PAGE_SIZE * 2) { + try { + return await queryClient.ensureQueryData({ + queryKey: ['tableData', selectedTable, from, to, userFilterSelection], + queryFn: () => fetchAllTableData(sortParam, totalCount, selectedTable, apiFilterParam, dateFilter), + }); + } catch (error) { + console.warn(error); + } + } else if (totalCount === 0) { + setIsLoading(false); + setUserFilterUnmatchedForDateRange(true); + setManualPagination(false); + return null; + } + } + }) + .catch(err => { + if (err.name === 'AbortError') { + console.info('Action cancelled.'); + } else { + console.error('API error', err); + setApiError(err); + } + }) + .finally(() => { + if (meta) { + setTableMeta(meta); + setApiError(false); + } + }); + } else if (selectedTable?.apiFilter && userFilterSelection === null) { + setIsLoading(false); + } + }; + + const refreshTable = async () => { + if (allTablesSelected) return; + selectedPivot = selectedPivot || {}; + const { columnConfig, width } = setTableConfig(config, selectedTable, selectedPivot, apiData); + + // DetailColumnConfig is used for the TIPS and CPI detail view table + const { columnConfig: detailColumnConfig } = config.detailView ? setTableConfig(config, config.detailView, selectedPivot, apiData) : {}; + let displayData = apiData ? apiData.data : null; + + if (userFilterSelection?.value && apiData?.data) { + displayData = apiData.data.filter(rr => rr[selectedTable.userFilter.field] === userFilterSelection.value); + setUserFilteredData({ ...apiData, data: displayData }); + } else { + setUserFilteredData(null); + } + + // Format chart data to match table decimal formatting for currency types + if (selectedPivot.pivotValue && selectedPivot.pivotView.roundingDenomination && apiData?.data) { + const copy = JSON.parse(JSON.stringify(apiData.data)); + displayData = copy.map(d => { + columnConfig.forEach(config => { + if (d[config.property] && !isNaN(d[config.property]) && config.type.includes('CURRENCY')) { + const decimalPlaces = parseInt(config.type.split('CURRENCY')[1]); + const absVal = Math.abs(d[config.property].toString()); + d[config.property] = absVal.toFixed(decimalPlaces); + } + }); + return d; + }); + setChartData({ ...apiData, data: displayData }); + } + + setTableProps({ + dePaginated: selectedTable.isLargeDataset === true ? await getDepaginatedData() : null, + hasPublishedReports, + publishedReports, + rawData: { ...apiData, data: displayData }.data ? { ...apiData, data: displayData } : apiData, + data: displayData, //null for server-side pagination + config, + columnConfig, + detailColumnConfig, + width, + noBorder: true, + shouldPage: true, + tableName, + serverSidePagination, + selectedTable, + selectedPivot, + dateRange, + apiError: apiErrorState, + selectColumns: selectedTable.selectColumns ? selectedTable.selectColumns : [], // if selectColumns is not defined in endpointConfig.js, default to allowing all columns be selectable + hideColumns: selectedTable.hideColumns, + excludeCols: ['CHART_DATE'], + aria: { 'aria-labelledby': 'main-data-table-title' }, + customFormatting, + }); + }; + + useMemo(async () => { + await refreshTable(); + }, [apiData, userFilterSelection, apiError]); + + useMemo(async () => { + if (serverSidePagination || userFilterSelection) { + await refreshTable(); + } + }, [dateRange]); + + useEffect(async () => { + if (config?.sharedApiFilterOptions && userFilterSelection) { + await refreshTable(); + } + }, [selectedTable]); + + const handlePivotConfigUpdated = () => { + setPivotsUpdated(!pivotsUpdated); + handleConfigUpdate(); + }; + + useEffect(() => { + if (typeof window !== 'undefined' && !legendToggledByUser) { + setLegend(window.innerWidth > GLOBALS.breakpoints.large); + } + }, [window.innerWidth]); + + useEffect(() => { + const hasPivotOptions = selectedTable.dataDisplays && selectedTable.dataDisplays.length > 1; + setHasPivotOptions(hasPivotOptions); + setReactTableSort([]); + if (!config?.sharedApiFilterOptions) { + setUserFilterSelection(null); + } + }, [selectedTable, allTablesSelected]); + + useEffect(() => { + if (!allTablesSelected) { + setDisableDownloadButton(userFilterUnmatchedForDateRange || (apiFilterDefault && !selectedTable?.apiFilter?.displayDefaultData)); + } else { + setDisableDownloadButton(false); + } + }, [userFilterUnmatchedForDateRange, apiFilterDefault]); + + useEffect(() => { + if (allTablesSelected) { + setDisableDownloadButton(false); + } + if (selectedTable?.apiFilter && !selectedTable.apiFilter?.displayDefaultData && userFilterSelection?.value === null) { + setApiFilterDefault(true); + setManualPagination(false); + } + }, [userFilterSelection]); + + const legendToggler = e => { + if (e.key === undefined || e.key === 'Enter') { + if (legend) { + Analytics.event({ + category: 'Chart Enabled', + action: 'Hide Legend Click', + label: `${config.name}, ${selectedTable.tableName}`, + }); + } + e.preventDefault(); + setLegend(!legend); + setLegendToggledByUser(true); + setSelectColumnPanel(!selectColumnPanel); + } + }; + + const pivotToggler = () => { + if (showPivotBar) { + Analytics.event({ + category: 'Chart Enabled', + action: 'Hide Pivot Options Click', + label: `${config.name}, ${selectedTable.tableName}`, + }); + } + setShowPivotBar(!showPivotBar); + }; + const getDateFieldForChart = () => { + if (selectedPivot && selectedPivot.pivotView && selectedPivot.pivotView.aggregateOn && selectedPivot.pivotView.aggregateOn.length) { + return 'CHART_DATE'; // aggregation cases in pivoted data this only for charting calculation + } else { + return selectedTable.dateField; + } + }; + + const dateFieldForChart = getDateFieldForChart(); + + useEffect(() => { + const userFilterUnmatched = determineUserFilterUnmatchedForDateRange(selectedTable, userFilterSelection, userFilteredData); + setUserFilterUnmatchedForDateRange(userFilterUnmatched); + setApiFilterDefault(!allTablesSelected && selectedTable?.apiFilter && (userFilterSelection === null || userFilterSelection?.value === null)); + setNoChartMessage( + SetNoChartMessage( + selectedTable, + selectedPivot, + dateRange, + allTablesSelected, + userFilterSelection, + userFilterUnmatched, + config?.customNoChartMessage + ) + ); + }, [selectedTable, selectedPivot, dateRange, allTablesSelected, userFilterSelection, userFilteredData, config?.customNoChartMessage]); + + return ( + <> +
+
+ {dateFieldForChart === 'CHART_DATE' && ( +
+ +
+ )} +
+
+ +
+
+
+
+ {isLoading && ( +
+
+
+ Loading... +
+
+ )} + {!!detailViewState && ( + + )} +
+ {(apiData || serverSidePagination || apiError) && ( + + )} +
+
+
+ + ); +}; + +export default DataPreviewSectionContainer; diff --git a/src/components/data-preview/data-preview-table/data-preview-table.module.scss b/src/components/data-preview/data-preview-table/data-preview-table.module.scss new file mode 100644 index 000000000..528558e96 --- /dev/null +++ b/src/components/data-preview/data-preview-table/data-preview-table.module.scss @@ -0,0 +1,181 @@ +@import '../../../variables.module.scss'; +@import '../../../zIndex.module.scss'; + +$borderRadius: 5px; + +@mixin side-padding { + padding-right: 1rem; + + &:first-child { + padding-left: 1rem; + } +} + +.overlayContainer { + background-color: white; + margin: 0 -1px -2px; +} + +.overlayContainer, +.overlayContainerNoFooter { + position: relative; +} +.overlayContainerReactTableHeight { + min-height: 521px; +} + +.wrapper.noBorderStyle { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.wrapper { + width: 100%; + box-sizing: border-box; + border: 1px solid $dd-border-color; + border-radius: $borderRadius; + background-color: white; + overflow-x: auto; + min-height: calc(64px + 8rem); + + table { + width: 100%; + font-size: 1rem; + color: $font-title; + border-collapse: collapse; + z-index: $dtg-table; + + th { + padding: 0.75rem 0 0.75rem 0; + text-align: left; + font-weight: $semi-bold-weight; + vertical-align: top; + + @include side-padding; + } + + tbody { + tr { + border-top: 1px solid $dd-border-color; + vertical-align: top; + } + + tr:nth-child(odd) { + background-color: $body-background; + } + + tr:last-child { + td:first-child { + border-bottom-left-radius: $borderRadius; + } + + td:last-child { + border-bottom-right-radius: $borderRadius; + } + } + } + + td { + padding: 0.5rem 0; + height: 1.5rem; + @include side-padding; + } + } +} + +.apiErrorStyle { + padding: 1.25rem; + z-index: $dtg-api-error; + position: absolute; +} + +.formattedCell { + text-align: right; +} + +.tableFooter { + display: flex; + flex-direction: column; + align-items: flex-end; + padding-top: 1rem; + margin-right: -1px; + background-color: white; +} + +.rowsShowingStyle { + margin-bottom: 0.5rem; +} + +.overlay { + z-index: $dtg-table-overlay; + height: calc(100% - 5.5rem); + width: 100%; + position: absolute; + background-color: rgba(255, 255, 255, 0.85); + transition: opacity 0.5s 0.5s, left 0.5s ease-in-out; + display: block; + animation: fadeIn 1s; + overflow: hidden; + margin-top: 5.5rem; +} + +@media screen and (max-width: $breakpoint-lg) { + .loadingSection { + height: calc(100% - 5.875rem); + margin-top: 5.875rem; + } +} + +.loadingIcon { + z-index: $dtg-table-loading-icon; + color: $font-title; + font-size: $font-size-18; + width: 100%; + top: 1.65rem; + text-align: center; + position: absolute; + + > * { + vertical-align: middle; + margin-right: 0.5rem; + font-size: 64px; + color: $loading-icon-color; + } +} + +.selectColumnsWrapper { + display: flex; + + .selectColumnPanel { + display: none; + } + + .selectColumnPanelActive { + display: block; + border-right: 1px solid $border-color; + border-bottom: 1px solid $border-color; + } +} + +@media screen and (max-width: $breakpoint-md) { + .wrapper { + table { + -webkit-text-size-adjust: 100%; + } + } +} + +@media screen and (min-width: $breakpoint-md) { + .tableFooter { + flex-direction: row; + align-items: center; + justify-content: space-between; + } +} + +@media screen and (min-width: $breakpoint-lg) { + .rowsShowingStyle { + margin-right: 2rem; + } +} diff --git a/src/components/data-preview/data-preview-table/data-preview-table.spec.js b/src/components/data-preview/data-preview-table/data-preview-table.spec.js new file mode 100644 index 000000000..07ec43caf --- /dev/null +++ b/src/components/data-preview/data-preview-table/data-preview-table.spec.js @@ -0,0 +1,125 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { + longerPaginatedDataResponse, + mockPaginatedTableProps, + shortPaginatedDataResponse, + TestData, + TestDataOneRow, + MoreTestData, + DetailViewTestData, +} from '../../dtg-table/test-data'; +import PaginationControls from '../../pagination/pagination-controls'; +import * as ApiUtils from '../../../utils/api-utils'; +import * as helpers from '../../dtg-table/dtg-table-helper'; +import { RecoilRoot } from 'recoil'; +import { render } from '@testing-library/react'; +import DataPreviewTable from './data-preview-table'; + +describe('DataPreviewTable component', () => { + jest.useFakeTimers(); + + beforeEach(() => jest.resetAllMocks()); + + let component = renderer.create(); + renderer.act(() => { + component = renderer.create( + + + + ); + }); + const instance = component.root; + + it('does not blow up when there is no data in a table', () => { + const noDataComponent = renderer.create( + + + + ); + const noDataInstance = noDataComponent.root; + + expect(noDataInstance); + }); + + it('does not show pagination controls by default', () => { + expect(instance.findAllByType(PaginationControls)).toStrictEqual([]); + }); + + it('does not show table footer if shouldPage property is not included in tableProps', () => { + expect(instance.findAllByProps({ 'data-test-id': 'table-footer' })).toHaveLength(0); + }); + + it('assigns data with a userFilterSelection', () => { + const mockSetIsLoading = jest.fn(); + const mockSetManualPagination = jest.fn(); + const { getByRole } = render( + + + + ); + expect(getByRole('table')).toBeInTheDocument(); + expect(mockSetManualPagination).toHaveBeenCalledWith(false); + expect(mockSetIsLoading).toHaveBeenCalledWith(false); + }); +}); + +// TODO: get these to work in a later ticket +// describe('DataPreviewTable component - API Error', () => { +// let component = renderer.create(); +// renderer.act(() => { +// component = renderer.create( +// +// {' '} +// +// ); +// }); +// const componentJSON = component.toJSON(); +// const footer = componentJSON[0].children.find(e => e.props['data-test-id'] === 'table-footer'); +// +// it('shows an apiError message when apiError exists', () => { +// const table = componentJSON[0].children.find(e => e.props['data-test-id'] === 'table-content'); +// expect(table.children.filter(e => e.props['data-test-id'] === 'api-error').length).toEqual(1); +// }); +// +// it('displays "Showing 0 - 0 rows of 0 rows" when apiError exists', () => { +// const rowsShowing = footer.children.find(e => e.props['data-test-id'] === 'rows-showing'); +// expect(rowsShowing.children[0]).toMatch(`Showing 0 - 0 rows of 0 rows`); +// }); +// +// it('does not render pagination controls if apiError exists && currentPage === 1 even when shouldPage === true', () => { +// expect(footer.children.find(e => e.type === PaginationControls)).toBeUndefined(); +// }); +// }); + +describe('Data Preview Table detail view', () => { + it('renders table with detail view', () => { + const detailViewState = { value: 'Brennah', secondary: 'Smith' }; + const mockSetIsLoading = jest.fn(); + const mockSetManualPagination = jest.fn(); + const { getByRole } = render( + + + + ); + + expect(getByRole('table')).toBeInTheDocument(); + }); +}); diff --git a/src/components/data-preview/data-preview-table/data-preview-table.tsx b/src/components/data-preview/data-preview-table/data-preview-table.tsx new file mode 100644 index 000000000..cfeacfd9d --- /dev/null +++ b/src/components/data-preview/data-preview-table/data-preview-table.tsx @@ -0,0 +1,471 @@ +import React, { FunctionComponent, useEffect, useMemo, useRef, useState } from 'react'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { + nonRawDataTableContainer, + overlayContainerNoFooter, + rawDataTableContainer, + selectColumnPanelActive, + selectColumnPanelInactive, + selectColumnsWrapper, + tableStyle, +} from '../../data-table/data-table.module.scss'; +import { reactTableFilteredDateRangeState } from '../../../recoil/reactTableFilteredState'; +import { loadingTimeout, netLoadingDelay, setColumns } from '../../dtg-table/dtg-table-helper'; +import { formatDateForApi, pagedDatatableRequest, REACT_TABLE_MAX_NON_PAGINATED_SIZE } from '../../../utils/api-utils'; +import moment from 'moment'; +import NotShownMessage from '../../dataset-data/table-section-container/not-shown-message/not-shown-message'; +import PaginationControls, { defaultPerPageOptions } from '../../pagination/pagination-controls'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSpinner } from '@fortawesome/free-solid-svg-icons'; +import DtgTableApiError from '../../dtg-table/dtg-table-api-error/dtg-table-api-error'; +import { ErrorBoundary } from 'react-error-boundary'; +import { overlayContainer, overlay, loadingIcon } from './data-preview-table.module.scss'; +import GLOBALS from '../../../helpers/constants'; +import DataPreviewDataTable from '../data-preview-data-table/data-preview-data-table'; +const DEFAULT_ROWS_PER_PAGE = GLOBALS.dataTable.DEFAULT_ROWS_PER_PAGE; + +type DataPreviewTableProps = { + tableProps; + perPage; + setPerPage; + selectColumnPanel; + setSelectColumnPanel; + setTableColumnSortData; + resetFilters; + setResetFilters; + tableMeta; + tableColumnSortData; + manualPagination; + setManualPagination; + pivotSelected; + allowColumnWrap; + setDetailViewState; + detailViewState; + setSummaryValues; + setIsLoading; + isLoading; + sorting; + setSorting; + allActiveFilters; + setAllActiveFilters; + userFilterSelection; + disableDateRangeFilter; +}; + +const DataPreviewTable: FunctionComponent = ({ + tableProps, + perPage, + setPerPage, + selectColumnPanel, + setSelectColumnPanel, + setTableColumnSortData, + resetFilters, + setResetFilters, + tableMeta, + tableColumnSortData, + manualPagination, + setManualPagination, + pivotSelected, + allowColumnWrap, + setDetailViewState, + detailViewState, + setSummaryValues, + setIsLoading, + isLoading, + sorting, + setSorting, + allActiveFilters, + setAllActiveFilters, + userFilterSelection, + disableDateRangeFilter, +}) => { + const { + dePaginated, + rawData, + width, + tableName, + shouldPage, + excludeCols, + selectedTable, + selectedPivot, + dateRange, + config, + columnConfig, + detailColumnConfig, + selectColumns, + hideColumns, + hasPublishedReports, + publishedReports, + customFormatting, + } = tableProps; + + const [reactTableData, setReactTableData] = useState(null); + const data = tableProps.data !== undefined && tableProps.data !== null ? tableProps.data : []; + + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState( + perPage ? perPage : !shouldPage && data.length > DEFAULT_ROWS_PER_PAGE ? data.length : DEFAULT_ROWS_PER_PAGE + ); + const [tableData, setTableData] = useState(!shouldPage ? data : []); + const [apiError, setApiError] = useState(tableProps.apiError || false); + const [maxPage, setMaxPage] = useState(1); + const [maxRows, setMaxRows] = useState(data.length > 0 ? data.length : 1); + const [rowsShowing, setRowsShowing] = useState({ begin: 1, end: 1 }); + const [emptyDataMessage, setEmptyDataMessage] = useState(); + const [showPaginationControls, setShowPaginationControls] = useState(); + const filteredDateRange = useRecoilValue(reactTableFilteredDateRangeState); + const detailViewAPIConfig = config?.detailView ? config.apis.find(api => api.apiId === config.detailView.apiId) : null; + const [tableSorting, setTableSorting] = useState([]); + + let loadCanceled = false; + + let debounce; + let loadTimer; + + const rowText = ['rows', 'rows']; + + const tableWidth = width ? (isNaN(width) ? width : `${width}px`) : 'auto'; + + const getAllExcludedCols = () => { + const allCols = []; + + if (excludeCols !== undefined) { + allCols.push(...excludeCols); + } + if (hideColumns) { + allCols.push(...hideColumns); + } + return allCols; + }; + + const dataProperties = { + keys: tableData[0] ? Object.keys(tableData[0]) : [], + excluded: getAllExcludedCols(), + }; + const columns = setColumns(dataProperties, columnConfig); + + const handlePerPageChange = numRows => { + const numItems = numRows >= maxRows ? maxRows : numRows; + setItemsPerPage(numItems); + setRowsShowing({ + begin: 1, + end: numItems, + }); + setCurrentPage(1); + if (setPerPage) { + setPerPage(numRows); + } + }; + + const getPagedData = resetPage => { + if (debounce || loadCanceled) { + clearTimeout(debounce); + } + if (!loadCanceled) { + debounce = setTimeout(() => { + makePagedRequest(resetPage); + }, 400); + } + }; + + const makePagedRequest = async resetPage => { + if ( + selectedTable && + selectedTable.endpoint && + !loadCanceled && + (!selectedTable?.apiFilter || + ((selectedTable?.apiFilter?.displayDefaultData || userFilterSelection) && + tableMeta && + tableMeta['total-count'] > REACT_TABLE_MAX_NON_PAGINATED_SIZE)) + ) { + loadTimer = setTimeout(() => loadingTimeout(loadCanceled, setIsLoading), netLoadingDelay); + + const from = + filteredDateRange?.from && moment(dateRange.from).diff(filteredDateRange?.from) <= 0 + ? filteredDateRange?.from.format('YYYY-MM-DD') + : formatDateForApi(dateRange.from); + const to = + filteredDateRange?.from && moment(dateRange.to).diff(filteredDateRange?.to) >= 0 + ? filteredDateRange?.to.format('YYYY-MM-DD') + : formatDateForApi(dateRange.to); + const startPage = resetPage ? 1 : currentPage; + pagedDatatableRequest( + selectedTable, + from, + to, + selectedPivot, + startPage, + itemsPerPage, + tableColumnSortData, + selectedTable?.apiFilter?.field, + userFilterSelection + ) + .then(res => { + if (!loadCanceled) { + setEmptyDataMessage(null); + if (res.data.length < 1) { + setIsLoading(false); + clearTimeout(loadTimer); + setEmptyDataMessage( + + ); + } + const totalCount = res.meta['total-count']; + const start = startPage === 1 ? 0 : (startPage - 1) * itemsPerPage; + const rowsToShow = start + itemsPerPage; + const stop = rowsToShow > totalCount ? totalCount : rowsToShow; + setRowsShowing({ + begin: start + 1, + end: stop, + }); + setMaxPage(res.meta['total-pages']); + if (maxRows !== totalCount) setMaxRows(totalCount); + setTableData(res.data); + } + }) + .catch(err => { + if (startPage === 1) { + setRowsShowing({ begin: 0, end: 0 }); + setMaxRows(0); + } + console.error(err); + if (!loadCanceled) { + setApiError(err); + } + }) + .finally(() => { + if (!loadCanceled) { + setIsLoading(false); + clearTimeout(loadTimer); + } + }); + } + }; + + const getCurrentData = () => { + if (tableProps.apiError && currentPage === 1) { + setRowsShowing({ begin: 0, end: 0 }); + setMaxRows(0); + } else { + const start = currentPage === 1 ? 0 : (currentPage - 1) * itemsPerPage; + const rowsToShow = start + itemsPerPage; + const stop = rowsToShow > data.length ? data.length : rowsToShow; + setRowsShowing({ begin: start + 1, end: stop }); + setMaxPage(Math.ceil(data.length / itemsPerPage)); + setTableData(data.slice(start, stop)); + } + }; + + const handleJump = page => { + const pageNum = Math.max(1, page); + setCurrentPage(Math.min(pageNum, maxPage)); + }; + + const isPaginationControlNeeded = () => currentPage >= 1 || (!apiError && !tableProps.apiError && maxRows > defaultPerPageOptions[0]); + + const updateSmallFractionDataType = () => { + //Overwrite type for special case number format handling + if (selectedTable && selectedTable.apiId === 178) { + selectedTable.fields[2].dataType = 'SMALL_FRACTION'; + } + }; + + const updateTable = resetPage => { + setApiError(false); + const ssp = tableProps.serverSidePagination; + ssp !== undefined && ssp !== null ? getPagedData(resetPage) : getCurrentData(); + return () => { + loadCanceled = true; + }; + }; + + useMemo(() => { + if (selectedTable?.rowCount > REACT_TABLE_MAX_NON_PAGINATED_SIZE) { + updateSmallFractionDataType(); + setCurrentPage(1); + updateTable(true); + } + }, [tableSorting, filteredDateRange, selectedTable, dateRange, tableMeta]); + + useMemo(() => { + if (selectedTable?.rowCount > REACT_TABLE_MAX_NON_PAGINATED_SIZE) { + //prevent hook from triggering twice on pivot selection + if ((pivotSelected?.pivotValue && !tableProps.serverSidePagination) || !pivotSelected?.pivotValue) { + updateTable(false); + } + } + }, [tableProps.serverSidePagination, itemsPerPage, currentPage]); + + useMemo(() => { + if (data && data.length) { + setMaxRows(apiError ? 0 : data.length); + } + }, [data]); + + useEffect(() => { + if (!tableProps.data) { + setCurrentPage(1); + } + }, [tableProps.data]); + + useEffect(() => { + setShowPaginationControls(isPaginationControlNeeded()); + }, [maxRows]); + + if (maxRows === 1) { + rowText[0] = ''; + rowText[1] = 'row'; + } + const pagingProps = { + itemsPerPage, + handlePerPageChange, + handleJump, + maxPage, + tableName, + currentPage, + maxRows, + }; + + useMemo(() => { + if (tableProps && selectedTable?.rowCount <= REACT_TABLE_MAX_NON_PAGINATED_SIZE && !pivotSelected?.pivotValue) { + if (dePaginated !== null && dePaginated !== undefined) { + // large dataset tables <= 20000 rows + setReactTableData(dePaginated); + setManualPagination(false); + setIsLoading(false); + } else if (rawData !== null && rawData.hasOwnProperty('data')) { + if (detailViewState && detailViewState?.secondary !== null && config?.detailView) { + const detailViewFilteredData = rawData.data.filter(row => row[config?.detailView.secondaryField] === detailViewState?.secondary); + setReactTableData({ data: detailViewFilteredData, meta: rawData.meta }); + } else { + setReactTableData(rawData); + } + setManualPagination(false); + } + } else if (userFilterSelection && tableMeta && tableMeta['total-count'] < REACT_TABLE_MAX_NON_PAGINATED_SIZE && dePaginated !== null) { + // user filter tables <= 20000 rows + setReactTableData(dePaginated); + setManualPagination(false); + setIsLoading(false); + } + }, [rawData, dePaginated]); + + const activePivot = (data, pivot) => { + return data?.pivotApplied?.includes(pivot?.pivotValue?.columnName) && data?.pivotApplied?.includes(pivot.pivotView?.title); + }; + + const updatedData = (newData, currentData) => { + return JSON.stringify(newData) !== JSON.stringify(currentData); + }; + + useMemo(() => { + if (tableProps) { + // Pivot data + if (rawData !== null && rawData?.hasOwnProperty('data') && activePivot(rawData, pivotSelected)) { + setReactTableData(rawData); + if (setManualPagination) { + setManualPagination(false); + } + } + } + }, [pivotSelected, rawData]); + + useMemo(() => { + if ( + tableData.length > 0 && + tableMeta && + selectedTable.rowCount > REACT_TABLE_MAX_NON_PAGINATED_SIZE && + !pivotSelected?.pivotValue && + !rawData?.pivotApplied + ) { + if (tableMeta['total-count'] <= REACT_TABLE_MAX_NON_PAGINATED_SIZE) { + // data with current date range < 20000 + if (rawData) { + setReactTableData(rawData); + setManualPagination(false); + } else if (dePaginated && !userFilterSelection) { + setReactTableData(dePaginated); + setManualPagination(false); + } + } else { + if (!(reactTableData?.pivotApplied && !updatedData(tableData, reactTableData?.data.slice(0, itemsPerPage)))) { + setReactTableData({ data: tableData, meta: tableMeta }); + setManualPagination(true); + } + } + } else if (tableData && data.length === 0 && !rawData && tableMeta && tableMeta['total-count'] > REACT_TABLE_MAX_NON_PAGINATED_SIZE) { + setReactTableData({ data: tableData, meta: tableMeta }); + } + }, [tableData, tableMeta, rawData, dePaginated]); + + return ( +
+ {/* Loading Indicator */} + {!isLoading && !reactTableData && !selectedTable?.apiFilter && ( + <> +
+
+ Loading... +
+ + )} + {/* Data Dictionary and Dataset Detail tables */} + {reactTableData?.data && ( +
+ {/* API Error Message */} + {(apiError || tableProps.apiError) && !emptyDataMessage && ( + <> + + + )} + {!emptyDataMessage && ( + <>}> + + + )} +
+ )} +
+ ); +}; + +export default DataPreviewTable; diff --git a/src/components/data-preview/data-preview.module.scss b/src/components/data-preview/data-preview.module.scss index df79ba60a..093a51cd2 100644 --- a/src/components/data-preview/data-preview.module.scss +++ b/src/components/data-preview/data-preview.module.scss @@ -1,4 +1,34 @@ -@import '../../variables.module.scss'; +@import 'src/variables.module'; + +.detailViewNotice { + display: flex; + font-weight: $semi-bold-weight; + font-size: $font-size-16; + + .lockIcon { + padding-right: 0.5rem; + height: 1rem; + width: 0.875rem; + padding-top: 0.125rem; + } +} + +.placeholderText { + font-size: $font-size-18; + padding: 4.8px 0; +} + +.placeholderButton { + height: 3rem; + width: 70%; + background-color: #d9d9d9; + margin-top: 1rem; + border-radius: .125rem; +} + +.tableContainer { + min-height: 44.125rem; +} .dataPreview { display: flex; @@ -14,6 +44,12 @@ font-size: $font-size-24; font-weight: $semi-bold-weight; } +.selectedTableName { + color: $font-body-copy; + font-size: $font-size-18; + font-weight: $semi-bold-weight; +} + @media screen and (max-width: $breakpoint-sm) { .dataPreview { display: grid; @@ -22,8 +58,3 @@ padding-bottom: 1rem; } } -.selectedTableName { - color: $font-body-copy; - font-size: $font-size-18; - font-weight: $semi-bold-weight; -} diff --git a/src/components/data-preview/data-preview.spec.js b/src/components/data-preview/data-preview.spec.js index d44d4d70c..3c6f88a5a 100644 --- a/src/components/data-preview/data-preview.spec.js +++ b/src/components/data-preview/data-preview.spec.js @@ -1,11 +1,505 @@ import React from 'react'; +import renderer from 'react-test-renderer'; +import DataTableSelect from '../datatable-select/datatable-select'; +import { format } from 'date-fns'; +import { pivotData } from '../../utils/api-utils'; +import { + config, + mockApiData, + latestDate, + fivePrior, + mockLocation, + mockLocationWithTablePathName, + mockPivotableData, + mockAccumulableData, + bannerTableConfig, +} from '../../components/dataset-data/test-helper'; +import * as DatasetDataHelpers from '../../components/dataset-data/dataset-data-helper/dataset-data-helper'; +import { getPublishedDates } from '../../helpers/dataset-detail/report-helpers'; +import PublishedReports from '../published-reports/published-reports'; +import Analytics from '../../utils/analytics/analytics'; +import { whiteListIds, mockPublishedReportsMTS } from '../../helpers/published-reports/published-reports'; +import PagingOptionsMenu from '../pagination/paging-options-menu'; +import { fireEvent, render } from '@testing-library/react'; +import { reports } from '../published-reports/test-helper'; +import { RecoilRoot } from 'recoil'; import DataPreview from './data-preview'; -import { render } from '@testing-library/react'; +import DataPreviewFilterSection from './data-preview-filter-section/data-preview-filter-section'; +import DownloadWrapper from '../download-wrapper/download-wrapper'; +import RangePresets from '../filter-download-container/range-presets/range-presets'; +import DataPreviewDownload from './data-preview-filter-section/data-preview-download/data-preview-download'; +import DateRangeFilter from './data-preview-filter-section/date-range-filter/date-range-filter'; -describe('data preview', () => { - it('renders a placeholder', () => { - const instance = render(); +jest.useFakeTimers(); +jest.mock('../truncate/truncate.jsx', () => () => 'Truncator'); +jest.mock('../../helpers/dataset-detail/report-helpers', function() { + return { + __esModule: true, + getPublishedDates: jest.fn().mockImplementation(() => [ + { + path: '/downloads/mspd_reports/opdm092020.pdf', + report_group_desc: 'Entire (.pdf)', + report_date: new Date('2020-09-30'), + filesize: '188264', + report_group_sort_order_nbr: 0, + report_group_id: 3, + }, + ]), + getLatestReport: jest.fn().mockImplementation(() => ({ + path: '/downloads/mspd_reports/opdm092020.pdf', + report_group_desc: 'Entire (.pdf)', + report_date: new Date('2020-09-30'), + filesize: '188264', + report_group_sort_order_nbr: 0, + report_group_id: 3, + })), + getDateLabelForReport: jest.fn().mockImplementation(() => 'Sept 2020'), + }; +}); +jest.mock('../../variables.module.scss', () => { + return { + breakpointSm: 600, + }; +}); + +describe('DataPreview', () => { + global.console.error = jest.fn(); + global.fetch = jest.fn(() => { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiData), + }); + }); + + const analyticsSpy = jest.spyOn(Analytics, 'event'); + + let component; + let instance; + const setSelectedTableMock = jest.fn(); + const urlRewriteSpy = jest.spyOn(DatasetDataHelpers, 'rewriteUrl'); + const fetchSpy = jest.spyOn(global, 'fetch'); + + beforeEach(async () => { + await renderer.act(async () => { + component = await renderer.create( + + + + ); + instance = component.root; + }); + }); + + afterEach(() => { + fetchSpy.mockClear(); + global.fetch.mockClear(); + analyticsSpy.mockClear(); + global.console.error.mockClear(); + }); + + const updateTable = async tableName => { + const fdSectionInst = instance.findByType(DataPreviewFilterSection); + const toggleBtn = fdSectionInst.findByProps({ + 'data-testid': 'dropdownToggle', + }); + await renderer.act(() => { + toggleBtn.props.onClick(); + }); + instance.findByProps({ 'data-testid': 'dropdown-list' }); // will throw error if not found + const dropdownOptions = instance.findAllByProps({ + 'data-testid': 'dropdown-list-option', + }); + await renderer.act(async () => { + const opt = dropdownOptions.find(ddo => ddo.props.children.props.children === tableName); + await opt.props.onClick(); + }); + return dropdownOptions; + }; + + it(`renders the DataPreview component which has the expected title text at desktop mode`, () => { + const { getByTestId } = render( + + + + ); + const title = getByTestId('sectionHeader'); + expect(title.innerHTML).toBe(''); + }); + + it(`contains a DataPreviewFilterSection component`, () => { + expect(instance.findByType(DataPreviewFilterSection)).toBeDefined(); + }); + + it(`initializes the selected table to the first element in the apis array`, () => { + expect(instance.findByType(DataPreviewFilterSection).props.selectedTable.tableName).toBe(config.apis[0].tableName); + }); + + it('calls rewriteUrl to append the table name but does not send a lastUrl (in order to prevent triggering an analytics hit)', () => { + expect(urlRewriteSpy).toHaveBeenNthCalledWith(1, config.apis[0], '/mock-dataset/', { + pathname: '/datasets/mock-dataset/', + }); + }); + + it('selects the correct table when it is specified in the url', async () => { + const setSelectedTableFromUrl = jest.fn(); + render( + + + + ); + + expect(setSelectedTableFromUrl).toHaveBeenCalledWith(config.apis[2]); + }); + + it(`initializes the dateRange to the appropriate values`, () => { + const dateRange = instance.findByType(DataPreviewFilterSection).props.dateRange; + const from = format(dateRange.from, 'yyyy-MM-dd'); + const to = format(dateRange.to, 'yyyy-MM-dd'); + expect(to).toContain(latestDate); + // should be previous 5 years since the earliestDate is more than 5 years + expect(from).toContain(fivePrior); + }); + + // it(`updates date range to appropriate values when new table is selected`, async () => { + // await updateTable('Table 3'); + // const dateRange = instance.findAllByType(DataPreviewFilterSection).find(dr => dr.props && dr.props.dateRange !== undefined).props.dateRange; + // const from = format(dateRange.from, 'yyyy-MM-dd'); + // const to = format(dateRange.to, 'yyyy-MM-dd'); + // expect(to).toContain(latestDate); + // // should be earliestDate since the earliestDate is less than 5 years + // expect(from).not.toContain(fivePrior); + // }); + // + // it(`sends the updated props to DataPreviewFilterSection component when a new data table is + // selected`, async () => { + // const dropdownOptions = await updateTable('Table 2'); + // expect(dropdownOptions.length).toBe(12); + // expect(instance.findByType(DataPreviewFilterSection).props.selectedTable.tableName).toBe(config.apis[1].tableName); + // }); + // + // it(`records an analytics event when a new table is selected`, async () => { + // await updateTable('Table 2'); + // expect(analyticsSpy).toHaveBeenLastCalledWith({ + // category: 'Data Table Selector', + // action: 'Pick Table Click', + // label: 'Table 2', + // }); + // }); + + it(`correctly prepares pivoted data without aggregation`, () => { + const pivotedData = pivotData( + mockPivotableData, + 'reporting_date', + { + dimensionField: 'security_desc', + title: 'by sec type', + }, + 'avg_interest_rate_amt', + null, + '2016-03-25', + '2021-03-25' + ); + expect(pivotedData).toMatchSnapshot(); + + // ensure that when some columns are not populated on some rows, + // all columns are still captured in the meta.dataTypes & meta.labels + const dataTypes = Object.keys(pivotedData.meta.dataTypes); + expect(dataTypes.length).toEqual(9); + expect(dataTypes.includes('Treasury Nickels')).toBeTruthy(); + expect(pivotedData.meta.labels['Treasury Nickels']).toEqual('Treasury Nickels'); + expect(dataTypes.includes('Treasury Notes')).toBeTruthy(); + expect(pivotedData.meta.labels['Treasury Notes']).toEqual('Treasury Notes'); + }); + + it(`correctly prepares pivoted data with aggregation and summing and handles non-numeric + values`, () => { + const mockPivotView = { dimensionField: 'class_desc', title: 'By Classification' }; + + const mockAggregation = [ + { + field: 'record_calendar_year', + type: 'YEAR', + }, + { + field: 'record_calendar_month', + type: 'MONTH', + }, + ]; + + const pivotedData = pivotData(mockAccumulableData, 'reporting_date', mockPivotView, 'cost', mockAggregation, '2020-01-01', '2021-03-25'); + expect(pivotedData).toMatchSnapshot(); + + // ensure that the summing operation occurs instead of using lastRow snapshots + expect(pivotedData.data[0]['Federal Bank'].toFixed(4)).toEqual('1010.1010'); + expect(pivotedData.data[1]['Medical Safe'].toFixed(4)).toEqual('3000000.7000'); + }); + + it(`correctly prepares pivoted data with aggregation when configured with + lastRowSnapshot=true`, () => { + const mockPivotView = { + dimensionField: 'class_desc', + title: 'By Classification', + lastRowSnapshot: true, + }; + + const mockAggregation = [ + { + field: 'record_calendar_year', + type: 'YEAR', + }, + { + field: 'record_calendar_month', + type: 'MONTH', + }, + ]; + + const pivotedData = pivotData(mockAccumulableData, 'reporting_date', mockPivotView, 'cost', mockAggregation, '2020-01-01', '2021-03-25'); + + // ensure that last Row values are used, rather that cross-row summing + const lastRowForMayFedBankVal = mockAccumulableData.data[0]['cost']; // '1000.0000' + expect(pivotedData.data[0]['Federal Bank']).toStrictEqual(lastRowForMayFedBankVal); + const lastRowForAprilMedSafeVal = mockAccumulableData.data[11]['cost']; // '2000000' + expect(pivotedData.data[1]['Medical Safe']).toStrictEqual(lastRowForAprilMedSafeVal); + }); + + // it(`does not pass the pagination endpoint to DTGTable when the rowCount is above 5000 + // and an a pivot dimension IS active`, async () => { + // await updateTable('Table 5'); + // const tableSectionContainer = instance.findAllByType(TableSectionContainer).find(tsc => tsc.props && tsc.props.config !== undefined); + // expect(tableSectionContainer.props.serverSidePagination).toBe(null); + // expect(tableSectionContainer.props.selectedPivot.pivotView.aggregateOn.length).toEqual(2); + // expect(tableSectionContainer.props.selectedPivot.pivotView.aggregateOn[0].field).toEqual('record_calendar_year'); + // }); + + // it(`passes the endpoint to DTGTable for serverside loading when the rowCount is above + // the large table threshold and no pivot dimension or complete table chart is + // is active`, async () => { + // await updateTable('Table 2'); + // let tableSectionContainer = instance.findAllByType(TableSectionContainer).find(tsc => tsc.props && tsc.props.config !== undefined); + // expect(tableSectionContainer.props.serverSidePagination).toBeNull(); + // await updateTable('Table 4'); + // tableSectionContainer = instance.findAllByType(TableSectionContainer).find(tsc => tsc.props && tsc.props.config !== undefined); + // expect(tableSectionContainer.props.serverSidePagination).toBe('mockEndpoint4'); + // }); + // + // it(`does not send the endpoint to DTGTable for serverside loading when the rowCount is above + // the large table threshold but the Complete Table view is chartable`, async () => { + // await updateTable('Table 9'); + // const tableSectionContainer = instance.findAllByType(TableSectionContainer).find(tsc => tsc.props && tsc.props.config !== undefined); + // expect(tableSectionContainer.props.serverSidePagination).toBeNull(); + // }); + // + // it(`raises state on setSelectedTable when the table is updated`, async () => { + // await updateTable('Table 5'); + // expect(setSelectedTableMock).toHaveBeenCalledWith(config.apis[4]); + // }); + // + // it(`calls rewriteUrl with correct args including a lastUrl arg when the table is updated + // interactively`, async () => { + // const spy = jest.spyOn(DatasetDataHelpers, 'rewriteUrl'); + // await updateTable('Table 5'); + // expect(spy).toHaveBeenCalledWith(config.apis[4], '/mock-dataset/', { + // pathname: '/datasets/mock-dataset/', + // }); + // }); + // + // it(`does not duplicate API calls when a user switches between two tables with + // paginated data`, async () => { + // jest.useFakeTimers(); + // await updateTable('Table 7'); // select one paginated table + // await updateTable('Table 6'); // then change the selection to another paginated table + // // to await makePagedRequest() debounce timer in DtgTable + // await jest.advanceTimersByTime(800); + // await updateTable('Table 7'); + // // confirm that the second table's api url was called only once + // const callsToApiForUpdatedTable = fetchSpy.mock.calls.filter(callSig => callSig[0].indexOf('/mockEndpoint6?') !== -1); + // expect(callsToApiForUpdatedTable.length).toEqual(1); + // }); + + // it(`does not duplicate api calls when switching from a large table to a small one`, async () => { + // jest.useFakeTimers(); + // await updateTable('Table 7'); // select one paginated table + // await updateTable('Table 8'); // then change the selection to a non-paginated table + // // to await makePagedRequest() debounce timer in DtgTable + // const callsToApiForUpdatedTable = fetchSpy.mock.calls.filter(callSig => callSig[0].indexOf('/mockEndpoint8?') !== -1); + // expect(callsToApiForUpdatedTable.length).toEqual(1); + // }); + + it(`grabs the published reports from the publishedReports prop if the dataset is whitelisted`, async () => { + const origId = config.datasetId; + const mockDatasetId = Object.keys(mockPublishedReportsMTS)[0]; + if (whiteListIds && whiteListIds.length) { + config.datasetId = whiteListIds[0]; + } + + getPublishedDates.mockClear(); + + await renderer.act(async () => { + await renderer.create( + + + + ); + }); + expect(getPublishedDates).toBeCalledTimes(1); + expect(getPublishedDates).toHaveBeenCalledWith(mockPublishedReportsMTS[mockDatasetId]); + + config.datasetId = origId; + }); + + it(`published report dates are refreshed when the published reports `, async () => { + const mockDatasetId = Object.keys(mockPublishedReportsMTS)[0]; + const mockPublishedReports = mockPublishedReportsMTS[mockDatasetId]; + getPublishedDates.mockClear(); + const { rerender } = render( + + + + ); + expect(getPublishedDates).toBeCalledTimes(1); + expect(getPublishedDates).toHaveBeenCalledWith(mockPublishedReports); + + const updatedMockPublishedReportsMTS = [ + { + path: '/downloads/mts_reports/mts102020.pdf', + report_group_desc: 'Entire (.pdf)', + report_date: '2020-10-30', + filesize: '188264', + report_group_sort_order_nbr: 0, + report_group_id: 3, + }, + { + path: '/downloads/mts_reports/mts102020.xls', + report_group_desc: 'Primary Dealers (.xls)', + report_date: '2020-10-30', + filesize: '810496', + report_group_sort_order_nbr: 1, + report_group_id: 3, + }, + ...mockPublishedReports, + ]; + + rerender( + + + + ); + + expect(getPublishedDates).toBeCalledTimes(2); + expect(getPublishedDates).toHaveBeenCalledWith(mockPublishedReports); + }); + + // it(`transmits a preview-loaded analytics event when reports tab is first selected but not when + // toggling back and forth reveals a preview that was already loaded`, async () => { + // analyticsSpy.mockClear(); + // + // const { getByLabelText } = render( + // + // + // + // ); + // + // // it's not called at at page load + // expect(analyticsSpy.mock.calls.every(callGroup => callGroup.every(call => call.action !== 'load pdf preview'))).toBeTruthy(); + // + // // select tab to instantiate + // // and test that analytics was called to transmit a preview-loaded event + // const rawDataButton = getByLabelText('Raw Data'); + // const publishedReportsButton = getByLabelText('Published Reports'); + // + // fireEvent.click(publishedReportsButton); + // + // expect(analyticsSpy).toHaveBeenLastCalledWith({ + // action: 'Published Report Preview', + // category: 'Published Report Preview', + // label: '/downloads/mspd_reports/opdm092020.pdf', + // }); + // analyticsSpy.mockClear(); + // expect(analyticsSpy).not.toHaveBeenCalled(); + // // go back to raw data + // fireEvent.click(rawDataButton); + // + // // and then back again to published reports + // fireEvent.click(publishedReportsButton); + // + // // expect that no duplicate event for report preview-loading will have been transmitted + // expect(analyticsSpy).not.toHaveBeenCalled(); + // }); + + // it(`correctly notifies the download control when all tables are selected`, () => { + // const datatableSelect = instance.findByType(DataTableSelect); + // const downloadWrapper = instance.findByType(DataPreviewDownload); + // expect(downloadWrapper.props.allTablesSelected).toBeFalsy(); + // datatableSelect.props.setSelectedTable({ allDataTables: true }); + // const downloadWrapperAfter = instance.findByType(DataPreviewDownload); + // expect(downloadWrapperAfter.props.allTablesSelected).toBeTruthy(); + // }); + + it("supplies the dataset's full dateRange to DateRangeFilter ", () => { + const rangePresets = instance.findByType(DateRangeFilter); + expect(rangePresets.props.datasetDateRange).toEqual({ + earliestDate: '2002-01-01', + latestDate: '2020-04-13', + }); + }); + + // it(`reflects whether "All Tables" is selected to DateRangeFilter `, async () => { + // const datatableSelect = instance.findByType(DataTableSelect); + // + // // passing down a falsy value to range Presets initially + // let rangePresets = instance.findByType(DateRangeFilter); + // expect(rangePresets.props.allTablesSelected).toBeFalsy(); + // + // // select the All Tables option, and confirm the tables have been sent to Range Presets + // datatableSelect.props.setSelectedTable({ allDataTables: true }); + // rangePresets = instance.findByType(DateRangeFilter); + // expect(rangePresets.props.allTablesSelected).toBeTruthy(); + // + // // confirm that switching back to a single selected table makes downloadTables falsy again + // await updateTable('Table 2'); + // rangePresets = instance.findByType(DateRangeFilter); + // expect(rangePresets.props.allTablesSelected).toBeFalsy(); + // }); + + it(`renders the datatable banner when datatableBanner exists`, () => { + const bannerText = 'This is a test'; + const { getByTestId } = render( + + + + ); + expect(getByTestId('datatable-banner')).toHaveTextContent(bannerText); + }); +}); + +describe('Nested Data Table', () => { + global.console.error = jest.fn(); + const analyticsSpy = jest.spyOn(Analytics, 'event'); + + let instance; + const setSelectedTableMock = jest.fn(); + const fetchSpy = jest.spyOn(global, 'fetch'); + beforeEach(async () => { + instance = render( + + + + ); + }); + + afterEach(() => { + fetchSpy.mockClear(); + global.fetch.mockClear(); + analyticsSpy.mockClear(); + global.console.error.mockClear(); + }); - expect(instance).toBeTruthy(); + it('Renders the summary table', () => { + expect(instance).toBeDefined(); }); }); diff --git a/src/components/data-preview/data-preview.tsx b/src/components/data-preview/data-preview.tsx index 0509bd470..c8601fdd8 100644 --- a/src/components/data-preview/data-preview.tsx +++ b/src/components/data-preview/data-preview.tsx @@ -1,24 +1,340 @@ -import React, { FunctionComponent, useState } from 'react'; +import React, { FunctionComponent, useEffect, useState } from 'react'; import DatasetSectionContainer from '../dataset-section-container/dataset-section-container'; +import DataPreviewSectionContainer from './data-preview-section-container/data-preview-section-container'; +import { detailViewNotice, lockIcon, placeholderButton, placeholderText } from './data-preview.module.scss'; +import { faLock } from '@fortawesome/free-solid-svg-icons'; +import { useRecoilValue } from 'recoil'; +import { reactTableFilteredDateRangeState } from '../../recoil/reactTableFilteredState'; +import { ENV_ID } from 'gatsby-env-variables'; +import { isValidDateRange } from '../../helpers/dates/date-helpers'; +import { getPublishedDates } from '../../helpers/dataset-detail/report-helpers'; +import { TableCache } from '../dataset-data/table-cache/table-cache'; +import { matchTableFromApiTables, parseTableSelectionFromUrl, rewriteUrl } from '../dataset-data/dataset-data-helper/dataset-data-helper'; +import { getApiData } from '../dataset-data/dataset-data-api-helper/dataset-data-api-helper'; +import { queryClient } from '../../../react-query-client'; +import UserFilter from '../filter-download-container/user-filter/user-filter'; +import DatatableBanner from '../filter-download-container/datatable-banner/datatable-banner'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import DataPreviewFilterSection from './data-preview-filter-section/data-preview-filter-section'; +import DateRangeFilter from './data-preview-filter-section/date-range-filter/date-range-filter'; import DataPreviewTableSelectDropdown from './data-preview-dropdown/data-preview-table-select-dropdown'; import { dataPreview, dataPreviewHeader, dataPreviewTitle, selectedTableName } from './data-preview.module.scss'; type DataPreviewProp = { - placeholder: string; + config; + finalDatesNotFound; + location; + publishedReportsProp; + setSelectedTableProp; + width; }; -const DataPreview: FunctionComponent = ({ placeholder: string }) => { - const [selectedTable, setSelectedTable] = useState('Summary of Budget and Off-Budget Results and Financing of the U.S. Government'); +const DataPreview: FunctionComponent = ({ + config, + finalDatesNotFound, + location, + publishedReportsProp, + setSelectedTableProp, + width, +}) => { + // config.apis should always be available; but, fallback in case + + const apis = config ? config.apis : [null]; + const filteredApis = apis.filter(api => api?.apiId !== config?.detailView?.apiId); + const detailApi = apis.find(api => api?.apiId && api?.apiId === config?.detailView?.apiId); + const [isFiltered, setIsFiltered] = useState(true); + const [selectedTable, setSelectedTable] = useState(); + const [allTablesSelected, setAllTablesSelected] = useState(false); + const [dateRange, setDateRange] = useState(); // TODO: remove this... using before hooking date picker to table + const [isCustomDateRange, setIsCustomDateRange] = useState(false); + const [apiData, setApiData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [apiError, setApiError] = useState(false); + const [serverSidePagination, setServerSidePagination] = useState(false); + const [selectedTab, setSelectedTab] = useState(0); + const [publishedReports, setPublishedReports] = useState([]); + const [selectedPivot, setSelectedPivot] = useState(); + const [ignorePivots, setIgnorePivots] = useState(false); + const [configUpdated, setConfigUpdated] = useState(false); + const [userFilterSelection, setUserFilterSelection] = useState(null); + const [tableColumnSortData, setTableColumnSortData] = useState([]); + const [tableCaches] = useState({}); + const [resetFilters, setResetFilters] = useState(false); + const [detailViewState, setDetailViewState] = useState(null); + const [summaryValues, setSummaryValues] = useState(null); + const [detailViewDownloadFilter, setDetailViewDownloadFilter] = useState(null); + const [allActiveFilters, setAllActiveFilters] = useState([]); + + const filteredDateRange = useRecoilValue(reactTableFilteredDateRangeState); + + let loadByPage; + const title = ENV_ID === 'uat' ? 'Data Preview' : 'Preview & Download'; + const shouldUseLoadByPage = pivot => { + return selectedTable && selectedTable.isLargeDataset && pivot && pivot.pivotView && pivot.pivotView.chartType === 'none'; + }; + + const clearDisplayData = () => { + loadByPage = shouldUseLoadByPage(selectedPivot); + + if (loadByPage) { + setServerSidePagination(selectedTable.endpoint); + } else { + setServerSidePagination(null); + } + setApiData(null); + setApiError(false); + }; + + const updateDataDisplay = data => { + clearDisplayData(); + setTimeout(() => setApiData(data)); // then on the next tick, setup the new data + }; + + const handleSelectedTableChange = table => { + if (table.allDataTables) { + setAllTablesSelected(true); + } else { + setAllTablesSelected(false); + setSelectedTable(table); + } + + Analytics.event({ + category: 'Data Table Selector', + action: 'Pick Table Click', + label: table.tableName, + }); + }; + + const handleDateRangeChange = range => { + if (range && isValidDateRange(range.from, range.to, config.techSpecs.earliestDate, config.techSpecs.latestDate)) { + setDateRange(range); + } + }; + + useEffect(() => { + // todo - Use a better manner of reassigning the report_date prop to jsdates. + setPublishedReports(getPublishedDates(publishedReportsProp)); + }, [publishedReportsProp]); + + useEffect(() => { + if (configUpdated) { + tableCaches[selectedTable.apiId] = new TableCache(); + const tableFromUrl = parseTableSelectionFromUrl(location, apis); + setSelectedTable(tableFromUrl); + setConfigUpdated(false); + } + }, [configUpdated]); + + // The following useEffect fires on this component's init and when the summary metadata is + // called. If new dates are available, then we should be updating the page to reflect the + // newest dates. + useEffect(() => { + const idealSelectedTable = matchTableFromApiTables(selectedTable, apis) || parseTableSelectionFromUrl(location, apis); + if (idealSelectedTable && idealSelectedTable.tableName) { + setSelectedTable(idealSelectedTable); + } + }, [apis]); + + useEffect(() => { + if (selectedTable) { + if (!selectedTable?.apiFilter?.disableDateRangeFilter) { + setDateRange(null); + } + setSelectedPivot(null); + rewriteUrl(selectedTable, config.slug, location); + setIsFiltered(true); + setApiError(false); + if (!tableCaches[selectedTable.apiId]) { + tableCaches[selectedTable.apiId] = new TableCache(); + } + setSelectedTableProp(selectedTable); + } + }, [selectedTable]); + + useEffect(() => { + if (detailApi) { + // resetting cache index here lets table data refresh on detail view state change + tableCaches[detailApi.apiId] = null; + setDateRange(null); + setSelectedPivot(null); + setIsFiltered(true); + setApiError(false); + if (!tableCaches[detailApi.apiId]) { + tableCaches[detailApi.apiId] = new TableCache(); + } + setDetailViewDownloadFilter( + !!detailViewState ? { field: config.detailView.field, label: config.detailView.label, value: detailViewState.value } : null + ); + } + }, [detailViewState]); + + // When dateRange changes, fetch new data + useEffect(() => { + if (!finalDatesNotFound && selectedTable && (selectedPivot || ignorePivots) && dateRange && !allTablesSelected) { + const displayedTable = detailViewState ? detailApi : selectedTable; + const cache = tableCaches[displayedTable.apiId]; + const cachedDisplay = cache?.getCachedDataDisplay(dateRange, selectedPivot, displayedTable); + if (cachedDisplay) { + updateDataDisplay(cachedDisplay); + } else { + clearDisplayData(); + let canceledObj = { isCanceled: false, abortController: new AbortController() }; + if (!loadByPage || ignorePivots) { + getApiData( + dateRange, + displayedTable, + selectedPivot, + setIsLoading, + setApiData, + setApiError, + canceledObj, + tableCaches[displayedTable.apiId], + detailViewState, + config?.detailView?.field, + queryClient + ).then(() => { + // nothing to cancel if the request completes normally. + canceledObj = null; + }); + } + return () => { + if (!canceledObj) return; + canceledObj.isCanceled = true; + canceledObj.abortController.abort(); + }; + } + } + }, [dateRange, selectedPivot, ignorePivots, finalDatesNotFound]); + + useEffect(() => { + if (allTablesSelected) { + setTableColumnSortData([]); + } + setUserFilterSelection(null); + }, [allTablesSelected]); + + useEffect(() => { + setTableColumnSortData([]); + }, [selectedTable]); + return ( - +
Data Preview
- +
+
{selectedTable?.tableName}
-
{selectedTable}
+ {tableColumnSortData && ( + + {selectedTable && ( + <> + {!selectedTable?.apiFilter?.disableDateRangeFilter && ( + + )} + {selectedTable.userFilter && ( + + )} + {selectedTable.apiFilter && ( + + )} + + )} + {config.datatableBanner && } + {!selectedTable && ( +
+

Date Range

+
+
+ )} + {detailApi && !detailViewState && ( +
+ {config.detailView?.dateRangeLockCopy} +
+ )} + + )} + {dateRange && ( + setConfigUpdated(true)} + tableColumnSortData={tableColumnSortData} + setTableColumnSortData={setTableColumnSortData} + hasPublishedReports={!!publishedReports} + publishedReports={publishedReports} + resetFilters={resetFilters} + setResetFilters={setResetFilters} + setDetailViewState={setDetailViewState} + detailViewState={detailViewState} + customFormatting={selectedTable?.customFormatting} + summaryValues={summaryValues} + setSummaryValues={setSummaryValues} + allActiveFilters={allActiveFilters} + setAllActiveFilters={setAllActiveFilters} + /> + )}
); diff --git a/src/components/data-preview/dimension-control/dimension-control.jsx b/src/components/data-preview/dimension-control/dimension-control.jsx deleted file mode 100644 index d5b453c23..000000000 --- a/src/components/data-preview/dimension-control/dimension-control.jsx +++ /dev/null @@ -1,62 +0,0 @@ -/* eslint-disable */ -/* istanbul ignore file */ -import React, { useState } from 'react'; -import { useEffect } from 'react'; - -export default function DimensionControl(props) { - const dimensions = props.dimensionOptions; - const [active, setActive] = useState(false); - const [key, setKey] = useState(dimensions.keys[0]); - const [value, setValue] = useState(dimensions.values[0]); - - useEffect(() => { - const payload = {}; - - if (active) { - payload.key = key; - payload.value = value; - } - - props.changeHandler(payload); - }, [active, key, value]); - - const handleControlChange = (id, event) => { - if (id === 'key') { - setKey(event.target.value); - } else if (id === 'value') { - setValue(event.target.value); - } else { - setActive(!active); - } - }; - - return ( -
- - -   - -
- ); -} diff --git a/src/components/data-preview/filter-group/filter-group.jsx b/src/components/data-preview/filter-group/filter-group.jsx deleted file mode 100644 index a00f665cd..000000000 --- a/src/components/data-preview/filter-group/filter-group.jsx +++ /dev/null @@ -1,8 +0,0 @@ -/* eslint-disable */ - -import React from 'react'; -import './filter-group.scss'; - -export default function FilterGroup(props) { - return
{props.children}
; -} diff --git a/src/components/data-preview/filter-group/filter-group.scss b/src/components/data-preview/filter-group/filter-group.scss deleted file mode 100644 index 1818bddae..000000000 --- a/src/components/data-preview/filter-group/filter-group.scss +++ /dev/null @@ -1,15 +0,0 @@ -@import '../../../variables.module.scss'; - -.FilterGroup { - background-color: $body-background; - padding: 0.8rem; - margin-top: 0.5rem; -} - -.FilterGroup_filter { - display: inline-block; -} - -.Filter_label { - display: block; -} diff --git a/src/components/data-preview/year-range-filter/year-range-filter.jsx b/src/components/data-preview/year-range-filter/year-range-filter.jsx deleted file mode 100644 index 67e7a01b7..000000000 --- a/src/components/data-preview/year-range-filter/year-range-filter.jsx +++ /dev/null @@ -1,58 +0,0 @@ -/* eslint-disable */ - -import React, { useState } from 'react'; -import './year-range-filter.scss'; - -export default function YearRangeFilter(props) { - const years = []; - const startYear = props.startYear || 2010; - const endYear = props.endYear || new Date().getFullYear(); - - const startYearHandleChange = event => { - const selectedYear = event.target.value; - if (selectedYear !== props.filterStartYear) { - if (!props.filterEndYear || props.filterEndYear < selectedYear) { - props.changeHandler(selectedYear, selectedYear); - } else { - props.changeHandler(selectedYear, props.filterEndYear); - } - } - }; - - const endYearHandleChange = event => { - const selectedYear = event.target.value; - if (selectedYear !== props.filterEndYear) { - const selectedYear = event.target.value; - if (!props.filterStartYear || props.filterStartYear > selectedYear) { - props.changeHandler(selectedYear, selectedYear); - } else { - props.changeHandler(props.filterStartYear, selectedYear); - } - } - }; - - for (let yr = startYear; yr <= endYear; yr++) { - years.push(yr); - } - - return ( -
- - - - - -
- ); -} diff --git a/src/components/data-preview/year-range-filter/year-range-filter.scss b/src/components/data-preview/year-range-filter/year-range-filter.scss deleted file mode 100644 index 2a03d7dc3..000000000 --- a/src/components/data-preview/year-range-filter/year-range-filter.scss +++ /dev/null @@ -1,14 +0,0 @@ -@import '../../../variables.module.scss'; - -.Filter select { - padding: 0.5rem; - border: 1px solid #cccccc; - border-radius: 3px; - min-width: 6rem; - margin: 0.2rem 0.5rem; - font-size: 1rem; - color: $font-body-copy; - &:first-of-type { - margin-left: 0; - } -} diff --git a/src/components/data-table/data-table-body/data-table-body.tsx b/src/components/data-table/data-table-body/data-table-body.tsx index 896cba2e2..aece3c475 100644 --- a/src/components/data-table/data-table-body/data-table-body.tsx +++ b/src/components/data-table/data-table-body/data-table-body.tsx @@ -3,22 +3,7 @@ import { flexRender, Table } from '@tanstack/react-table'; import React, { FunctionComponent, ReactNode } from 'react'; import { fillCellGrey, fillCellWhite, cellBorder, rightAlignText, hidden, detailButton, cellText } from './data-table-body.module.scss'; import classNames from 'classnames'; - -interface IDataTableBody { - table: Table>; - dataTypes: { [key: string]: string }; - allowColumnWrap: string[]; - detailViewConfig?: { - apiId: number; - dateRangeLockCopy: string; - field: string; - secondaryField?: string; - selectColumns: string[]; - summaryTableFields: string[]; - }; - setDetailViewState: (val: { value: string; secondary: string }) => void; - setSummaryValues: (val: { field: string }[]) => void; -} +import { IDataTableBody } from '../../../models/IDataTableBody'; const DataTableBody: FunctionComponent = ({ table, diff --git a/src/components/data-table/data-table-header/data-table-header.tsx b/src/components/data-table/data-table-header/data-table-header.tsx index 0ffbe1476..121029ed2 100644 --- a/src/components/data-table/data-table-header/data-table-header.tsx +++ b/src/components/data-table/data-table-header/data-table-header.tsx @@ -19,16 +19,7 @@ import { rightAlign, getColumnFilter } from '../data-table-helper'; import React, { FunctionComponent } from 'react'; import Tooltip from '@material-ui/core/Tooltip'; import { withStyles } from '@material-ui/core/styles'; - -interface IDataTableHeader { - table: Table>; - dataTypes: { [key: string]: string }; - resetFilters: boolean; - manualPagination: boolean; - allActiveFilters: string[]; - setAllActiveFilters: (value: string[]) => void; - disableDateRangeFilter: boolean; -} +import { IDataTableHeader } from '../../../models/IDataTableHeader'; const DataTableHeader: FunctionComponent = ({ table, diff --git a/src/components/data-table/data-table.tsx b/src/components/data-table/data-table.tsx index 4209bd069..f27573a89 100644 --- a/src/components/data-table/data-table.tsx +++ b/src/components/data-table/data-table.tsx @@ -30,47 +30,48 @@ import { tableRowLengthState, } from '../../recoil/smallTableDownloadData'; import { useSetRecoilState } from 'recoil'; +import { IDataTableProps } from '../../models/IDataTableProps'; -type DataTableProps = { - // defaultSelectedColumns will be null unless the dataset has default columns specified in the dataset config - rawData; - nonRawDataColumns?; - defaultSelectedColumns: string[]; - setTableColumnSortData; - hasPublishedReports: boolean; - publishedReports; - hideCellLinks: boolean; - resetFilters: boolean; - shouldPage: boolean; - showPaginationControls: boolean; - setSelectColumnPanel; - selectColumnPanel; - setResetFilters: (value: boolean) => void; - tableName: string; - hideColumns?: string[]; - pagingProps; - manualPagination: boolean; - rowsShowing: { begin: number; end: number }; - columnConfig?; - detailColumnConfig?; - detailView?; - detailViewAPI?; - setDetailViewState?: (val: { value?: string; secondary?: string }) => void; - detailViewState?: { value?: string; secondary?: string }; - allowColumnWrap?: string[]; - aria; - pivotSelected; - setSummaryValues?; - customFormatting?; - sorting: SortingState; - setSorting: (value: SortingState) => void; - allActiveFilters: string[]; - setAllActiveFilters: (value: string[]) => void; - setTableSorting?: (value: SortingState) => void; - disableDateRangeFilter: boolean; -}; +// type DataTableProps = { +// // defaultSelectedColumns will be null unless the dataset has default columns specified in the dataset config +// rawData; +// nonRawDataColumns?; +// defaultSelectedColumns: string[]; +// setTableColumnSortData; +// hasPublishedReports: boolean; +// publishedReports; +// hideCellLinks: boolean; +// resetFilters: boolean; +// shouldPage: boolean; +// showPaginationControls: boolean; +// setSelectColumnPanel; +// selectColumnPanel; +// setResetFilters: (value: boolean) => void; +// tableName: string; +// hideColumns?: string[]; +// pagingProps; +// manualPagination: boolean; +// rowsShowing: { begin: number; end: number }; +// columnConfig?; +// detailColumnConfig?; +// detailView?; +// detailViewAPI?; +// setDetailViewState?: (val: { value?: string; secondary?: string }) => void; +// detailViewState?: { value?: string; secondary?: string }; +// allowColumnWrap?: string[]; +// aria; +// pivotSelected; +// setSummaryValues?; +// customFormatting?; +// sorting: SortingState; +// setSorting: (value: SortingState) => void; +// allActiveFilters: string[]; +// setAllActiveFilters: (value: string[]) => void; +// setTableSorting?: (value: SortingState) => void; +// disableDateRangeFilter: boolean; +// }; -const DataTable: FunctionComponent = ({ +const DataTable: FunctionComponent = ({ rawData, nonRawDataColumns, defaultSelectedColumns, diff --git a/src/helpers/constants.js b/src/helpers/constants.js index a31cd4e1a..d45de6c7e 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -107,6 +107,9 @@ const globalConstants = { stickyMinimize: 'Minimize Download Bar', stickyShowDetails: 'Show Download Details', }, + dataTable: { + DEFAULT_ROWS_PER_PAGE: 10, + }, }; // Object.freeze(globalConstants); diff --git a/src/layouts/dataset-detail/dataset-detail.jsx b/src/layouts/dataset-detail/dataset-detail.jsx index 9242a8431..39ea86f15 100644 --- a/src/layouts/dataset-detail/dataset-detail.jsx +++ b/src/layouts/dataset-detail/dataset-detail.jsx @@ -90,7 +90,13 @@ const DatasetDetail = ({ data, pageContext, location, test }) => { {ENV_ID === 'uat' ? : ''} - + >; + dataTypes: { [key: string]: string }; + allowColumnWrap: string[]; + detailViewConfig?: { + apiId: number; + dateRangeLockCopy: string; + field: string; + secondaryField?: string; + selectColumns: string[]; + summaryTableFields: string[]; + }; + setDetailViewState: (val: { value: string; secondary: string }) => void; + setSummaryValues: (val: { field: string }[]) => void; +} diff --git a/src/models/IDataTableHeader.ts b/src/models/IDataTableHeader.ts new file mode 100644 index 000000000..274db4c95 --- /dev/null +++ b/src/models/IDataTableHeader.ts @@ -0,0 +1,11 @@ +import { Table } from '@tanstack/react-table'; + +export interface IDataTableHeader { + table: Table>; + dataTypes: { [key: string]: string }; + resetFilters: boolean; + manualPagination: boolean; + allActiveFilters: string[]; + setAllActiveFilters: (value: string[]) => void; + disableDateRangeFilter: boolean; +} diff --git a/src/models/IDataTableProps.ts b/src/models/IDataTableProps.ts new file mode 100644 index 000000000..6519fc81c --- /dev/null +++ b/src/models/IDataTableProps.ts @@ -0,0 +1,38 @@ +export interface IDataTableProps { + // defaultSelectedColumns will be null unless the dataset has default columns specified in the dataset config + rawData; + nonRawDataColumns?; + defaultSelectedColumns: string[]; + setTableColumnSortData; + hasPublishedReports: boolean; + publishedReports; + hideCellLinks: boolean; + resetFilters: boolean; + shouldPage: boolean; + showPaginationControls: boolean; + setSelectColumnPanel; + selectColumnPanel; + setResetFilters: (value: boolean) => void; + tableName: string; + hideColumns?: string[]; + pagingProps; + manualPagination: boolean; + rowsShowing: { begin: number; end: number }; + columnConfig?; + detailColumnConfig?; + detailView?; + detailViewAPI?; + setDetailViewState?: (val: { value?: string; secondary?: string }) => void; + detailViewState?: { value?: string; secondary?: string }; + allowColumnWrap?: string[]; + aria; + pivotSelected; + setSummaryValues?; + customFormatting?; + sorting: SortingState; + setSorting: (value: SortingState) => void; + allActiveFilters: string[]; + setAllActiveFilters: (value: string[]) => void; + setTableSorting?: (value: SortingState) => void; + disableDateRangeFilter: boolean; +}