diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/drilltodetail.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/drilltodetail.test.ts index 64522a21054..2323b749c58 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/drilltodetail.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/drilltodetail.test.ts @@ -157,62 +157,47 @@ describe('Drill to detail modal', () => { it('refreshes the data', () => { openModalFromMenu('big_number_total'); // move to the last page - cy.get(".pagination-container [role='navigation'] [role='button']") - .eq(7) - .click(); + cy.get('.ant-pagination-item').eq(5).click(); + // skips error on pagination + cy.on('uncaught:exception', () => false); cy.wait('@samples'); // reload cy.get("[aria-label='reload']").click(); cy.wait('@samples'); // make sure it started back from first page - cy.get(".pagination-container [role='navigation'] li.active").should( - 'contain', - '1', - ); + cy.get('.ant-pagination-item-active').should('contain', '1'); }); it('paginates', () => { openModalFromMenu('big_number_total'); // checking the data cy.getBySel('row-count-label').should('contain', '75.7k rows'); - cy.get(".ant-modal-body [role='rowgroup'] [role='row']") - .should('have.length', 50) - .then($rows => { - expect($rows).to.contain('Amy'); - }); + cy.get('.virtual-table-cell').then($rows => { + expect($rows).to.contain('Amy'); + }); // checking the paginated data - cy.get(".pagination-container [role='navigation'] [role='button']") - .should('have.length', 9) + cy.get('.ant-pagination-item') + .should('have.length', 6) .then($pages => { expect($pages).to.contain('1'); expect($pages).to.contain('1514'); }); - cy.get(".pagination-container [role='navigation'] [role='button']") - .eq(7) - .click(); + cy.get('.ant-pagination-item').eq(4).click(); + // skips error on pagination + cy.on('uncaught:exception', () => false); cy.wait('@samples'); - cy.get("[role='rowgroup'] [role='row']") - .should('have.length', 43) - .then($rows => { - expect($rows).to.contain('Victoria'); - }); + cy.get('.virtual-table-cell').then($rows => { + expect($rows).to.contain('Kelly'); + }); // verify scroll top on pagination - cy.getBySelLike('Number-modal') - .find('.table-condensed') - .scrollTo(0, 100); + cy.getBySelLike('Number-modal').find('.virtual-grid').scrollTo(0, 200); - cy.get("[role='rowgroup'] [role='row']") - .contains('Miguel') - .should('not.be.visible'); + cy.get('.virtual-grid').contains('Juan').should('not.be.visible'); - cy.get(".pagination-container [role='navigation'] [role='button']") - .eq(1) - .click(); + cy.get('.ant-pagination-item').eq(0).click(); - cy.get("[role='rowgroup'] [role='row']") - .contains('Aaron') - .should('be.visible'); + cy.get('.virtual-grid').contains('Aaron').should('be.visible'); }); }); @@ -478,8 +463,8 @@ describe('Drill to detail modal', () => { // checking the filter cy.getBySel('filter-val').should('contain', 'boy'); cy.getBySel('row-count-label').should('contain', '39.2k rows'); - cy.get(".pagination-container [role='navigation'] [role='button']") - .should('have.length', 9) + cy.get('.ant-pagination-item') + .should('have.length', 6) .then($pages => { expect($pages).to.contain('1'); expect($pages).to.contain('785'); @@ -489,12 +474,9 @@ describe('Drill to detail modal', () => { cy.getBySel('filter-col').find("[aria-label='close']").click(); cy.wait('@samples'); cy.getBySel('row-count-label').should('contain', '75.7k rows'); - cy.get(".pagination-container [role='navigation'] li.active").should( - 'contain', - '1', - ); - cy.get(".pagination-container [role='navigation'] [role='button']") - .should('have.length', 9) + cy.get('.ant-pagination-item-active').should('contain', '1'); + cy.get('.ant-pagination-item') + .should('have.length', 6) .then($pages => { expect($pages).to.contain('1'); expect($pages).to.contain('1514'); diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx index 160796c308b..e1207f8aac5 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx @@ -110,6 +110,7 @@ export default function DrillDetailModal({ }} draggable destroyOnClose + maskClosable={false} > diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx index 4b42c39da8b..a6c8fd06b07 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx @@ -134,7 +134,12 @@ test('should render the table with results', async () => { fetchWithData(); await waitForRender(); expect(screen.getByRole('table')).toBeInTheDocument(); - expect(screen.getAllByRole('row')).toHaveLength(4); + expect(screen.getByText('1996')).toBeInTheDocument(); + expect(screen.getByText('11.27')).toBeInTheDocument(); + expect(screen.getByText('1989')).toBeInTheDocument(); + expect(screen.getByText('23.2')).toBeInTheDocument(); + expect(screen.getByText('1999')).toBeInTheDocument(); + expect(screen.getByText('9')).toBeInTheDocument(); expect( screen.getByRole('columnheader', { name: 'year' }), ).toBeInTheDocument(); diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.tsx index e90b30e1d7a..f3e33298d11 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.tsx @@ -22,6 +22,7 @@ import React, { useMemo, useCallback, useRef, + ReactElement, } from 'react'; import { useSelector } from 'react-redux'; import { @@ -32,24 +33,54 @@ import { useTheme, QueryFormData, JsonObject, + GenericDataType, } from '@superset-ui/core'; +import { useResizeDetector } from 'react-resize-detector'; import Loading from 'src/components/Loading'; +import BooleanCell from 'src/components/Table/cell-renderers/BooleanCell'; +import NullCell from 'src/components/Table/cell-renderers/NullCell'; +import TimeCell from 'src/components/Table/cell-renderers/TimeCell'; import { EmptyStateMedium } from 'src/components/EmptyState'; -import TableView, { EmptyWrapperType } from 'src/components/TableView'; -import { useTableColumns } from 'src/explore/components/DataTableControl'; import { getDatasourceSamples } from 'src/components/Chart/chartAction'; +import Table, { + ColumnsType, + TablePaginationConfig, + TableSize, +} from 'src/components/Table'; import MetadataBar, { ContentType, MetadataType, } from 'src/components/MetadataBar'; import Alert from 'src/components/Alert'; import { useApiV1Resource } from 'src/hooks/apiResources'; +import HeaderWithRadioGroup from 'src/components/Table/header-renderers/HeaderWithRadioGroup'; import TableControls from './DrillDetailTableControls'; import { getDrillPayload } from './utils'; import { Dataset, ResultsPage } from './types'; const PAGE_SIZE = 50; +interface DataType { + [key: string]: any; +} + +// Must be outside of the main component due to problems in +// react-resize-detector with conditional rendering +// https://github.com/maslianok/react-resize-detector/issues/178 +function Resizable({ children }: { children: ReactElement }) { + const { ref, height } = useResizeDetector(); + return ( +
+ {React.cloneElement(children, { height })} +
+ ); +} + +enum TimeFormatting { + Original, + Formatted, +} + export default function DrillDetailPane({ formData, initialFilters, @@ -66,6 +97,7 @@ export default function DrillDetailPane({ const [resultsPages, setResultsPages] = useState>( new Map(), ); + const [timeFormatting, setTimeFormatting] = useState({}); const SAMPLES_ROW_LIMIT = useSelector( (state: { common: { conf: JsonObject } }) => @@ -89,29 +121,68 @@ export default function DrillDetailPane({ return resultsPages.get(lastPageIndex.current); }, [pageIndex, resultsPages]); - // this is to preserve the order of the columns, even if there are integer values, - // while also only grabbing the first column's keys - const columns = useTableColumns( - resultsPage?.colNames, - resultsPage?.colTypes, - resultsPage?.data, - formData.datasource, - ); - - // Disable sorting on columns - const sortDisabledColumns = useMemo( + const mappedColumns: ColumnsType = useMemo( () => - columns.map(column => ({ - ...column, - disableSortBy: true, - })), - [columns], + resultsPage?.colNames.map((column, index) => ({ + key: column, + dataIndex: column, + title: + resultsPage?.colTypes[index] === GenericDataType.TEMPORAL ? ( + + setTimeFormatting(state => ({ ...state, [column]: value })) + } + /> + ) : ( + column + ), + render: value => { + if (value === true || value === false) { + return ; + } + if (value === null) { + return ; + } + if ( + resultsPage?.colTypes[index] === GenericDataType.TEMPORAL && + timeFormatting[column] !== TimeFormatting.Original && + (typeof value === 'number' || value instanceof Date) + ) { + return ; + } + return String(value); + }, + width: 150, + })) || [], + [resultsPage?.colNames, resultsPage?.colTypes, timeFormatting], ); - // Update page index on pagination click - const onServerPagination = useCallback(({ pageIndex }) => { - setPageIndex(pageIndex); - }, []); + const data: DataType[] = useMemo( + () => + resultsPage?.data.map((row, index) => + resultsPage?.colNames.reduce( + (acc, curr) => ({ ...acc, [curr]: row[curr] }), + { + key: index, + }, + ), + ) || [], + [resultsPage?.colNames, resultsPage?.data], + ); // Clear cache on reload button click const handleReload = useCallback(() => { @@ -222,29 +293,22 @@ export default function DrillDetailPane({ } else { // Render table if at least one page has successfully loaded tableContent = ( - + + setPageIndex(pagination.current ? pagination.current - 1 : 0) } - `} - scrollTopOnPagination - /> + resizable + virtualize + /> + ); } diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailTableControls.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailTableControls.tsx index 26965a2f18d..523cbaff3e4 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailTableControls.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailTableControls.tsx @@ -82,6 +82,7 @@ export default function TableControls({ display: flex; justify-content: space-between; padding: ${theme.gridUnit / 2}px 0; + margin-bottom: ${theme.gridUnit * 2}px; `} >
= args => { + const [orderDateFormatting, setOrderDateFormatting] = useState('formatted'); + const [priceLocale, setPriceLocale] = useState(LocaleCode.en_US); + const shoppingColumns: ColumnsType = [ + { + title: 'Item', + dataIndex: 'item', + key: 'item', + width: 200, + }, + { + title: () => ( + setOrderDateFormatting(value)} + /> + ), + dataIndex: 'orderDate', + key: 'orderDate', + width: 200, + render: value => + orderDateFormatting === 'original' ? value : , + }, + { + title: () => ( + setPriceLocale(value as LocaleCode)} + /> + ), + dataIndex: 'price', + key: 'price', + width: 200, + render: value => ( + + ), + }, + ]; + + return ( +
+ ); +}; diff --git a/superset-frontend/src/components/Table/VirtualTable.tsx b/superset-frontend/src/components/Table/VirtualTable.tsx index 713eca6b79a..fc7b7ee27d3 100644 --- a/superset-frontend/src/components/Table/VirtualTable.tsx +++ b/superset-frontend/src/components/Table/VirtualTable.tsx @@ -24,7 +24,7 @@ import { VariableSizeGrid as Grid } from 'react-window'; import { StyledComponent } from '@emotion/styled'; import { useTheme, styled } from '@superset-ui/core'; import { TablePaginationConfig } from 'antd/lib/table'; -import { TableProps, TableSize, HEIGHT_OFFSET, ETableAction } from './index'; +import { TableProps, TableSize, ETableAction } from './index'; const StyledCell: StyledComponent = styled('div')( ({ theme, height }) => ` @@ -176,7 +176,7 @@ const VirtualTable = (props: TableProps) => { const { width = DEFAULT_COL_WIDTH } = mergedColumns[index]; return width as number; }} - height={height ? height - HEIGHT_OFFSET : (scroll!.y as number)} + height={height || (scroll!.y as number)} rowCount={rawData.length} rowHeight={() => cellSize} width={tableWidth} diff --git a/superset-frontend/src/components/Table/cell-renderers/BooleanCell/BooleanCell.stories.tsx b/superset-frontend/src/components/Table/cell-renderers/BooleanCell/BooleanCell.stories.tsx new file mode 100644 index 00000000000..8a98f18299c --- /dev/null +++ b/superset-frontend/src/components/Table/cell-renderers/BooleanCell/BooleanCell.stories.tsx @@ -0,0 +1,34 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import BooleanCell from '.'; + +export default { + title: 'Design System/Components/Table/Cell Renderers/BooleanCell', + component: BooleanCell, +} as ComponentMeta; + +export const Basic: ComponentStory = args => ( + +); + +Basic.args = { + value: true, +}; diff --git a/superset-frontend/src/components/Table/cell-renderers/BooleanCell/BooleanCell.test.tsx b/superset-frontend/src/components/Table/cell-renderers/BooleanCell/BooleanCell.test.tsx new file mode 100644 index 00000000000..fa83db6271d --- /dev/null +++ b/superset-frontend/src/components/Table/cell-renderers/BooleanCell/BooleanCell.test.tsx @@ -0,0 +1,37 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { render, screen } from 'spec/helpers/testing-library'; +import { BOOL_FALSE_DISPLAY, BOOL_TRUE_DISPLAY } from 'src/constants'; +import BooleanCell from '.'; + +test('renders true value', async () => { + render(); + expect(screen.getByText(BOOL_TRUE_DISPLAY)).toBeInTheDocument(); +}); + +test('renders false value', async () => { + render(); + expect(screen.getByText(BOOL_FALSE_DISPLAY)).toBeInTheDocument(); +}); + +test('renders falsy value', async () => { + render(); + expect(screen.getByText(BOOL_FALSE_DISPLAY)).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/components/Table/cell-renderers/BooleanCell/index.tsx b/superset-frontend/src/components/Table/cell-renderers/BooleanCell/index.tsx new file mode 100644 index 00000000000..2707d522417 --- /dev/null +++ b/superset-frontend/src/components/Table/cell-renderers/BooleanCell/index.tsx @@ -0,0 +1,30 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { BOOL_FALSE_DISPLAY, BOOL_TRUE_DISPLAY } from 'src/constants'; + +export interface BooleanCellProps { + value?: boolean; +} + +function BooleanCell({ value }: BooleanCellProps) { + return {value ? BOOL_TRUE_DISPLAY : BOOL_FALSE_DISPLAY}; +} + +export default BooleanCell; diff --git a/superset-frontend/src/components/Table/cell-renderers/NullCell/NullCell.stories.tsx b/superset-frontend/src/components/Table/cell-renderers/NullCell/NullCell.stories.tsx new file mode 100644 index 00000000000..ff39cd25528 --- /dev/null +++ b/superset-frontend/src/components/Table/cell-renderers/NullCell/NullCell.stories.tsx @@ -0,0 +1,28 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import NullCell from '.'; + +export default { + title: 'Design System/Components/Table/Cell Renderers/NullCell', + component: NullCell, +} as ComponentMeta; + +export const Basic: ComponentStory = () => ; diff --git a/superset-frontend/src/components/Table/cell-renderers/NullCell/NullCell.test.tsx b/superset-frontend/src/components/Table/cell-renderers/NullCell/NullCell.test.tsx new file mode 100644 index 00000000000..3a3fd9d416c --- /dev/null +++ b/superset-frontend/src/components/Table/cell-renderers/NullCell/NullCell.test.tsx @@ -0,0 +1,35 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { render, screen } from 'spec/helpers/testing-library'; +import { supersetTheme } from '@superset-ui/core'; +import { NULL_DISPLAY } from 'src/constants'; +import NullCell from '.'; + +test('renders null value', async () => { + render(); + expect(screen.getByText(NULL_DISPLAY)).toBeInTheDocument(); +}); + +test('renders with gray font', async () => { + render(); + expect(screen.getByText(NULL_DISPLAY)).toHaveStyle( + `color: ${supersetTheme.colors.grayscale.light1}`, + ); +}); diff --git a/superset-frontend/src/components/Table/cell-renderers/NullCell/index.tsx b/superset-frontend/src/components/Table/cell-renderers/NullCell/index.tsx new file mode 100644 index 00000000000..f1c9139fd9f --- /dev/null +++ b/superset-frontend/src/components/Table/cell-renderers/NullCell/index.tsx @@ -0,0 +1,37 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { css, SupersetTheme } from '@superset-ui/core'; +import { NULL_DISPLAY } from 'src/constants'; + +function NullCell() { + return ( + + css` + color: ${theme.colors.grayscale.light1}; + ` + } + > + {NULL_DISPLAY} + + ); +} + +export default NullCell; diff --git a/superset-frontend/src/components/Table/cell-renderers/TimeCell/TimeCell.stories.tsx b/superset-frontend/src/components/Table/cell-renderers/TimeCell/TimeCell.stories.tsx new file mode 100644 index 00000000000..f37c5b8c6af --- /dev/null +++ b/superset-frontend/src/components/Table/cell-renderers/TimeCell/TimeCell.stories.tsx @@ -0,0 +1,43 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { TimeFormats } from '@superset-ui/core'; +import TimeCell from '.'; + +export default { + title: 'Design System/Components/Table/Cell Renderers/TimeCell', + component: TimeCell, +} as ComponentMeta; + +export const Basic: ComponentStory = args => ( + +); + +Basic.args = { + value: Date.now(), +}; + +Basic.argTypes = { + format: { + defaultValue: TimeFormats.DATABASE_DATETIME, + control: 'select', + options: Object.values(TimeFormats), + }, +}; diff --git a/superset-frontend/src/components/Table/cell-renderers/TimeCell/TimeCell.test.tsx b/superset-frontend/src/components/Table/cell-renderers/TimeCell/TimeCell.test.tsx new file mode 100644 index 00000000000..81ccbed38ac --- /dev/null +++ b/superset-frontend/src/components/Table/cell-renderers/TimeCell/TimeCell.test.tsx @@ -0,0 +1,49 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { TimeFormats } from '@superset-ui/core'; +import React from 'react'; +import { render, screen } from 'spec/helpers/testing-library'; +import TimeCell from '.'; + +const DATE = Date.parse('2022-01-01'); + +test('renders with default format', async () => { + render(); + expect(screen.getByText('2022-01-01 00:00:00')).toBeInTheDocument(); +}); + +test('renders with custom format', async () => { + render(); + expect(screen.getByText('2022-01-01')).toBeInTheDocument(); +}); + +test('renders with number', async () => { + render(); + expect(screen.getByText('2022-01-01 00:00:00')).toBeInTheDocument(); +}); + +test('renders with no value', async () => { + render(); + expect(screen.getByText('N/A')).toBeInTheDocument(); +}); + +test('renders with invalid date format', async () => { + render(); + expect(screen.getByText('aaa-bbb-ccc')).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/components/Table/cell-renderers/TimeCell/index.tsx b/superset-frontend/src/components/Table/cell-renderers/TimeCell/index.tsx new file mode 100644 index 00000000000..31a9dac2472 --- /dev/null +++ b/superset-frontend/src/components/Table/cell-renderers/TimeCell/index.tsx @@ -0,0 +1,38 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { getTimeFormatter, TimeFormats } from '@superset-ui/core'; +import NullCell from '../NullCell'; + +export interface TimeCellProps { + format?: string; + value?: number | Date; +} + +function TimeCell({ + format = TimeFormats.DATABASE_DATETIME, + value, +}: TimeCellProps) { + if (value) { + return {getTimeFormatter(format).format(value)}; + } + return ; +} + +export default TimeCell; diff --git a/superset-frontend/src/components/Table/header-renderers/HeaderWithRadioGroup.tsx b/superset-frontend/src/components/Table/header-renderers/HeaderWithRadioGroup.tsx new file mode 100644 index 00000000000..65fd435119f --- /dev/null +++ b/superset-frontend/src/components/Table/header-renderers/HeaderWithRadioGroup.tsx @@ -0,0 +1,94 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useState } from 'react'; +import { css, useTheme } from '@superset-ui/core'; +import { Radio } from 'src/components/Radio'; +import { Space } from 'src/components'; +import Icons from 'src/components/Icons'; +import Popover from 'src/components/Popover'; + +export interface HeaderWithRadioGroupProps { + headerTitle: string; + groupTitle: string; + groupOptions: { label: string; value: string | number }[]; + value?: string | number; + onChange: (value: string) => void; +} + +function HeaderWithRadioGroup(props: HeaderWithRadioGroupProps) { + const { headerTitle, groupTitle, groupOptions, value, onChange } = props; + const theme = useTheme(); + const [popoverVisible, setPopoverVisible] = useState(false); + + return ( +
+ +
+ {groupTitle} +
+ { + onChange(e.target.value); + setPopoverVisible(false); + }} + > + + {groupOptions.map(option => ( + + {option.label} + + ))} + + +
+ } + placement="bottomLeft" + arrowPointAtCenter + > + setPopoverVisible(true)} + /> + + {headerTitle} + + ); +} + +export default HeaderWithRadioGroup; diff --git a/superset-frontend/src/components/Table/index.tsx b/superset-frontend/src/components/Table/index.tsx index 99d70312da0..55b004570ad 100644 --- a/superset-frontend/src/components/Table/index.tsx +++ b/superset-frontend/src/components/Table/index.tsx @@ -200,14 +200,15 @@ export enum TableSize { } const defaultRowSelection: React.Key[] = []; -// This accounts for the tables header and pagination if user gives table instance a height. this is a temp solution -export const HEIGHT_OFFSET = 108; + +const PAGINATION_HEIGHT = 40; +const HEADER_HEIGHT = 68; const StyledTable: StyledComponent = styled(AntTable)( ({ theme, height }) => ` .ant-table-body { overflow: auto; - height: ${height ? `${height - HEIGHT_OFFSET}px` : undefined}; + height: ${height ? `${height}px` : undefined}; } th.ant-table-cell { @@ -348,6 +349,8 @@ export function Table(props: TableProps) { setMergedLocale(updatedLocale); }, [locale]); + useEffect(() => setDerivedColumns(columns), [columns]); + useEffect(() => { if (interactiveTableUtils.current) { interactiveTableUtils.current?.clearListeners(); @@ -403,6 +406,16 @@ export function Table(props: TableProps) { paginationSettings.total = recordCount; } + let bodyHeight = height; + if (bodyHeight) { + bodyHeight -= HEADER_HEIGHT; + const hasPagination = + usePagination && recordCount && recordCount > pageSize; + if (hasPagination) { + bodyHeight -= PAGINATION_HEIGHT; + } + } + const sharedProps = { loading: { spinning: loading ?? false, indicator: }, hasData: hideData ? false : data, @@ -414,7 +427,7 @@ export function Table(props: TableProps) { showSorterTooltip: false, onChange, theme, - height, + height: bodyHeight, }; return (