From 1d868dcebad0876f14925db3115f97999d4eae27 Mon Sep 17 00:00:00 2001 From: justinpark Date: Fri, 9 Aug 2024 11:26:58 +0900 Subject: [PATCH 01/20] feat(sqllab): Replace FilterableTable by AgGrid Table --- superset-frontend/package-lock.json | 20 ++ superset-frontend/package.json | 2 + .../FilterableTable/FilterableTable.test.tsx | 33 ++- .../src/components/FilterableTable/index.tsx | 250 +++++------------- .../components/GridTable/GridTable.test.tsx | 48 ++++ .../src/components/GridTable/Header.test.tsx | 85 ++++++ .../src/components/GridTable/Header.tsx | 181 +++++++++++++ .../components/GridTable/HeaderMenu.test.tsx | 195 ++++++++++++++ .../src/components/GridTable/HeaderMenu.tsx | 214 +++++++++++++++ .../src/components/GridTable/index.tsx | 238 +++++++++++++++++ .../src/components/Icons/AntdEnhanced.tsx | 12 + 11 files changed, 1079 insertions(+), 199 deletions(-) create mode 100644 superset-frontend/src/components/GridTable/GridTable.test.tsx create mode 100644 superset-frontend/src/components/GridTable/Header.test.tsx create mode 100644 superset-frontend/src/components/GridTable/Header.tsx create mode 100644 superset-frontend/src/components/GridTable/HeaderMenu.test.tsx create mode 100644 superset-frontend/src/components/GridTable/HeaderMenu.tsx create mode 100644 superset-frontend/src/components/GridTable/index.tsx diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 8f9a07604ad5..3ee1a093fbdd 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -56,6 +56,8 @@ "@visx/xychart": "^3.5.1", "abortcontroller-polyfill": "^1.7.8", "ace-builds": "^1.36.3", + "ag-grid-community": "31.0.3", + "ag-grid-react": "31.0.3", "antd": "4.10.3", "antd-v5": "npm:antd@^5.18.0", "bootstrap": "^3.4.1", @@ -14478,6 +14480,24 @@ "node": ">= 10.0.0" } }, + "node_modules/ag-grid-community": { + "version": "31.0.3", + "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-31.0.3.tgz", + "integrity": "sha512-k81YmLaOQOab9BavYD+Pw2smZSl6TXOJqj9hRuf70XQl3EknOHCGcra7joJxZRJLMKE/HdR+u33TNyX4TCuWfg==" + }, + "node_modules/ag-grid-react": { + "version": "31.0.3", + "resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-31.0.3.tgz", + "integrity": "sha512-CE4Z5Rdb0H3MFBhmge9854AyDvI4DoG7ZoUJRFB1p2pu5ANVe6tv7cwEI/ugHemEXRRP6s5uvW+ndv0Q6x1/+Q==", + "dependencies": { + "ag-grid-community": "~31.0.3", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 7edc690f35e3..c2e64c8901e3 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -123,6 +123,8 @@ "@visx/xychart": "^3.5.1", "abortcontroller-polyfill": "^1.7.8", "ace-builds": "^1.36.3", + "ag-grid-community": "31.0.3", + "ag-grid-react": "31.0.3", "antd": "4.10.3", "antd-v5": "npm:antd@^5.18.0", "bootstrap": "^3.4.1", diff --git a/superset-frontend/src/components/FilterableTable/FilterableTable.test.tsx b/superset-frontend/src/components/FilterableTable/FilterableTable.test.tsx index b7ce92be3fa6..577d0760cb5f 100644 --- a/superset-frontend/src/components/FilterableTable/FilterableTable.test.tsx +++ b/superset-frontend/src/components/FilterableTable/FilterableTable.test.tsx @@ -38,7 +38,7 @@ describe('FilterableTable', () => { const { getByRole, getByText } = render( , ); - expect(getByRole('table')).toBeInTheDocument(); + expect(getByRole('treegrid')).toBeInTheDocument(); mockedProps.data.forEach(({ b: columnBContent }) => { expect(getByText(columnBContent)).toBeInTheDocument(); }); @@ -78,11 +78,10 @@ describe('FilterableTable sorting - RTL', () => { }; render(); - const stringColumn = within(screen.getByRole('table')) + const stringColumn = within(screen.getByRole('treegrid')) .getByText('columnA') - .closest('th'); - // Antd 4.x Table does not follow the table role structure. Need a hacky selector to point the cell item - const gridCells = screen.getByTitle('Bravo').closest('.virtual-grid'); + .closest('[role=button]'); + const gridCells = screen.getByText('Bravo').closest('[role=rowgroup]'); // Original order expect(gridCells?.textContent).toEqual( @@ -124,10 +123,10 @@ describe('FilterableTable sorting - RTL', () => { }; render(); - const integerColumn = within(screen.getByRole('table')) + const integerColumn = within(screen.getByRole('treegrid')) .getByText('columnB') - .closest('th'); - const gridCells = screen.getByTitle('21').closest('.virtual-grid'); + .closest('[role=button]'); + const gridCells = screen.getByText('21').closest('[role=rowgroup]'); // Original order expect(gridCells?.textContent).toEqual(['21', '0', '623'].join('')); @@ -159,10 +158,10 @@ describe('FilterableTable sorting - RTL', () => { }; render(); - const floatColumn = within(screen.getByRole('table')) + const floatColumn = within(screen.getByRole('treegrid')) .getByText('columnC') - .closest('th'); - const gridCells = screen.getByTitle('45.67').closest('.virtual-grid'); + .closest('[role=button]'); + const gridCells = screen.getByText('45.67').closest('[role=rowgroup]'); // Original order expect(gridCells?.textContent).toEqual( @@ -214,10 +213,10 @@ describe('FilterableTable sorting - RTL', () => { }; render(); - const mixedFloatColumn = within(screen.getByRole('table')) + const mixedFloatColumn = within(screen.getByRole('treegrid')) .getByText('columnD') - .closest('th'); - const gridCells = screen.getByTitle('48710.92').closest('.virtual-grid'); + .closest('[role=button]'); + const gridCells = screen.getByText('48710.92').closest('[role=rowgroup]'); // Original order expect(gridCells?.textContent).toEqual( @@ -312,10 +311,10 @@ describe('FilterableTable sorting - RTL', () => { }; render(); - const dsColumn = within(screen.getByRole('table')) + const dsColumn = within(screen.getByRole('treegrid')) .getByText('columnDS') - .closest('th'); - const gridCells = screen.getByTitle('2021-01-01').closest('.virtual-grid'); + .closest('[role=button]'); + const gridCells = screen.getByText('2021-01-01').closest('[role=rowgroup]'); // Original order expect(gridCells?.textContent).toEqual( diff --git a/superset-frontend/src/components/FilterableTable/index.tsx b/superset-frontend/src/components/FilterableTable/index.tsx index 04429ebf845f..c30574b93bbb 100644 --- a/superset-frontend/src/components/FilterableTable/index.tsx +++ b/superset-frontend/src/components/FilterableTable/index.tsx @@ -16,55 +16,20 @@ * specific language governing permissions and limitations * under the License. */ -import _JSONbig from 'json-bigint'; -import { useEffect, useRef, useState, useMemo } from 'react'; -import { getMultipleTextDimensions, styled } from '@superset-ui/core'; -import { useDebounceValue } from 'src/hooks/useDebounceValue'; +import { useMemo, useRef, useCallback } from 'react'; +import { styled } from '@superset-ui/core'; import { useCellContentParser } from './useCellContentParser'; import { renderResultCell } from './utils'; -import { Table, TableSize } from '../Table'; +import GridTable, { GridSize, ColDef } from '../GridTable'; -const JSONbig = _JSONbig({ - storeAsString: true, - constructorAction: 'preserve', -}); - -const SCROLL_BAR_HEIGHT = 15; // This regex handles all possible number formats in javascript, including ints, floats, // exponential notation, NaN, and Infinity. // See https://stackoverflow.com/a/30987109 for more details const ONLY_NUMBER_REGEX = /^(NaN|-?((\d*\.\d+|\d+)([Ee][+-]?\d+)?|Infinity))$/; const StyledFilterableTable = styled.div` - ${({ theme }) => ` - height: 100%; - overflow: hidden; - - .ant-table-cell { - font-weight: ${theme.typography.weights.bold}; - background-color: ${theme.colors.grayscale.light5}; - } - - .ant-table-cell, - .virtual-table-cell { - min-width: 0px; - align-self: center; - font-size: ${theme.typography.sizes.s}px; - } - - .even-row { - background: ${theme.colors.grayscale.light4}; - } - - .odd-row { - background: ${theme.colors.grayscale.light5}; - } - - .cell-text-for-measuring { - font-family: ${theme.typography.families.sansSerif}; - font-size: ${theme.typography.sizes.s}px; - } - `} + height: 100%; + overflow: hidden; `; type CellDataType = string | number | null; @@ -79,12 +44,38 @@ export interface FilterableTableProps { overscanColumnCount?: number; overscanRowCount?: number; rowHeight?: number; - // need antd 5.0 to support striped color pattern striped?: boolean; expandedColumns?: string[]; allowHTML?: boolean; } +const parseNumberFromString = (value: string | number | null) => { + if (typeof value === 'string' && ONLY_NUMBER_REGEX.test(value)) { + return parseFloat(value); + } + return value; +}; + +const sortResults = (valueA: string | number, valueB: string | number) => { + const aValue = parseNumberFromString(valueA); + const bValue = parseNumberFromString(valueB); + + // equal items sort equally + if (aValue === bValue) { + return 0; + } + + // nulls sort after anything else + if (aValue === null) { + return 1; + } + if (bValue === null) { + return -1; + } + + return aValue < bValue ? -1 : 1; +}; + const FilterableTable = ({ orderedColumnKeys, data, @@ -92,83 +83,13 @@ const FilterableTable = ({ filterText = '', expandedColumns = [], allowHTML = true, + striped, }: FilterableTableProps) => { - const formatTableData = (data: Record[]): Datum[] => - data.map(row => { - const newRow: Record = {}; - Object.entries(row).forEach(([key, val]) => { - if (['string', 'number'].indexOf(typeof val) >= 0) { - newRow[key] = val; - } else { - newRow[key] = val === null ? null : JSONbig.stringify(val); - } - }); - return newRow; - }); - - const [fitted, setFitted] = useState(false); - const [list] = useState(() => formatTableData(data)); - const getCellContent = useCellContentParser({ columnKeys: orderedColumnKeys, expandedColumns, }); - const getWidthsForColumns = () => { - const PADDING = 50; // accounts for cell padding and width of sorting icon - const widthsByColumnKey: Record = {}; - const cellContent = ([] as string[]).concat( - ...orderedColumnKeys.map(key => { - const cellContentList = list.map((data: Datum) => - getCellContent({ cellData: data[key], columnKey: key }), - ); - cellContentList.push(key); - return cellContentList; - }), - ); - - const colWidths = getMultipleTextDimensions({ - className: 'cell-text-for-measuring', - texts: cellContent, - }).map(dimension => dimension.width); - - orderedColumnKeys.forEach((key, index) => { - // we can't use Math.max(...colWidths.slice(...)) here since the number - // of elements might be bigger than the number of allowed arguments in a - // JavaScript function - widthsByColumnKey[key] = - colWidths - .slice(index * (list.length + 1), (index + 1) * (list.length + 1)) - .reduce((a, b) => Math.max(a, b)) + PADDING; - }); - - return widthsByColumnKey; - }; - - const [widthsForColumnsByKey] = useState>(() => - getWidthsForColumns(), - ); - - const totalTableWidth = useRef( - orderedColumnKeys - .map(key => widthsForColumnsByKey[key]) - .reduce((curr, next) => curr + next), - ); - const container = useRef(null); - - const fitTableToWidthIfNeeded = () => { - const containerWidth = container.current?.clientWidth ?? 0; - if (totalTableWidth.current < containerWidth) { - // fit table width if content doesn't fill the width of the container - totalTableWidth.current = containerWidth; - } - setFitted(true); - }; - - useEffect(() => { - fitTableToWidthIfNeeded(); - }, []); - const hasMatch = (text: string, row: Datum) => { const values: string[] = []; Object.keys(row).forEach(key => { @@ -188,86 +109,51 @@ const FilterableTable = ({ return values.some(v => v.includes(lowerCaseText)); }; - // Parse any numbers from strings so they'll sort correctly - const parseNumberFromString = (value: string | number | null) => { - if (typeof value === 'string') { - if (ONLY_NUMBER_REGEX.test(value)) { - return parseFloat(value); - } - } - - return value; - }; - - const sortResults = (key: string, a: Datum, b: Datum) => { - const aValue = parseNumberFromString(a[key]); - const bValue = parseNumberFromString(b[key]); - - // equal items sort equally - if (aValue === bValue) { - return 0; - } - - // nulls sort after anything else - if (aValue === null) { - return 1; - } - if (bValue === null) { - return -1; - } - - return aValue < bValue ? -1 : 1; - }; - - const keyword = useDebounceValue(filterText); - - const filteredList = useMemo( + const columns = useMemo( () => - keyword ? list.filter((row: Datum) => hasMatch(keyword, row)) : list, - [list, keyword], + orderedColumnKeys.map(key => ({ + key, + label: key, + fieldName: key, + headerName: key, + comparator: sortResults, + render: ({ value, colDef }: { value: CellDataType; colDef: ColDef }) => + renderResultCell({ + cellData: value, + columnKey: colDef.field, + allowHTML, + getCellContent, + }), + })), + [orderedColumnKeys, allowHTML, getCellContent], ); - // exclude the height of the horizontal scroll bar from the height of the table - // and the height of the table container if the content overflows - const totalTableHeight = - container.current && totalTableWidth.current > container.current.clientWidth - ? height - SCROLL_BAR_HEIGHT - : height; + const keyword = useRef(filterText); + keyword.current = filterText; - const columns = orderedColumnKeys.map(key => ({ - key, - title: key, - dataIndex: key, - width: widthsForColumnsByKey[key], - sorter: (a: Datum, b: Datum) => sortResults(key, a, b), - render: (text: CellDataType) => - renderResultCell({ - cellData: text, - columnKey: key, - allowHTML, - getCellContent, - }), - })); + const keywordFilter = useCallback(node => { + if (keyword.current && node.data) { + return hasMatch(keyword.current, node.data); + } + return true; + }, []); return ( - {fitted && ( - - )} + ); }; diff --git a/superset-frontend/src/components/GridTable/GridTable.test.tsx b/superset-frontend/src/components/GridTable/GridTable.test.tsx new file mode 100644 index 000000000000..5b4e26ac3247 --- /dev/null +++ b/superset-frontend/src/components/GridTable/GridTable.test.tsx @@ -0,0 +1,48 @@ +import { render } from 'spec/helpers/testing-library'; +import GridTable from '.'; + +jest.mock('src/components/ErrorBoundary', () => ({ + __esModule: true, + default: ({ children }: { children: React.ReactNode }) => <>{children}, +})); + +const mockedProps = { + queryId: 'abc', + columns: ['a', 'b', 'c'].map(key => ({ + key, + label: key, + headerName: key, + render: ({ value }: { value: any }) => value, + })), + data: [ + { a: 'a1', b: 'b1', c: 'c1', d: 0 }, + { a: 'a2', b: 'b2', c: 'c2', d: 100 }, + { a: null, b: 'b3', c: 'c3', d: 50 }, + ], + height: 500, +}; + +test('renders a grid with 3 Table rows', () => { + const { queryByText } = render(); + mockedProps.data.forEach(({ b: columnBContent }) => { + expect(queryByText(columnBContent)).toBeTruthy(); + }); +}); + +test('sorts strings correctly', () => { + const stringProps = { + ...mockedProps, + columns: ['columnA'].map(key => ({ + key, + label: key, + headerName: key, + render: ({ value }: { value: any }) => value, + })), + data: [{ columnA: 'Bravo' }, { columnA: 'Alpha' }, { columnA: 'Charlie' }], + height: 500, + }; + const { container } = render(); + + // Original order + expect(container).toHaveTextContent(['Bravo', 'Alpha', 'Charlie'].join('')); +}); diff --git a/superset-frontend/src/components/GridTable/Header.test.tsx b/superset-frontend/src/components/GridTable/Header.test.tsx new file mode 100644 index 000000000000..086960df0d3d --- /dev/null +++ b/superset-frontend/src/components/GridTable/Header.test.tsx @@ -0,0 +1,85 @@ +import type { Column, GridApi } from 'ag-grid-community'; +import { act, fireEvent, render } from 'spec/helpers/testing-library'; +import Header, { PIVOT_COL_ID } from './Header'; + +jest.mock('src/components/Dropdown', () => ({ + Dropdown: () =>
, +})); + +jest.mock('src/components/Icons', () => ({ + Sort: () =>
, + SortAsc: () =>
, + SortDesc: () =>
, +})); + +class MockApi extends EventTarget { + getAllDisplayedColumns() { + return []; + } +} + +const mockedProps = { + displayName: 'test column', + setSort: jest.fn(), + column: { + getColId: () => '123', + isPinnedLeft: () => true, + isPinnedRight: () => false, + getSort: () => 'asc', + getSortIndex: () => null, + } as any as Column, + api: new MockApi() as any as GridApi, +}; + +test('renders display name for the column', () => { + const { queryByText } = render(
); + expect(queryByText(mockedProps.displayName)).toBeTruthy(); +}); + +test('sorts by clicking a column header', () => { + const { getByText, queryByTestId } = render(
); + fireEvent.click(getByText(mockedProps.displayName)); + expect(mockedProps.setSort).toBeCalledWith('asc', false); + expect(queryByTestId('mock-sort-asc')).toBeInTheDocument(); + fireEvent.click(getByText(mockedProps.displayName)); + expect(mockedProps.setSort).toBeCalledWith('desc', false); + expect(queryByTestId('mock-sort-desc')).toBeInTheDocument(); + fireEvent.click(getByText(mockedProps.displayName)); + expect(mockedProps.setSort).toBeCalledWith(null, false); + expect(queryByTestId('mock-sort-asc')).not.toBeInTheDocument(); + expect(queryByTestId('mock-sort-desc')).not.toBeInTheDocument(); +}); + +test('synchronizes the current sort when sortChanged event occured', async () => { + const { findByTestId } = render(
); + act(() => { + mockedProps.api.dispatchEvent(new Event('sortChanged')); + }); + const sortAsc = await findByTestId('mock-sort-asc'); + expect(sortAsc).toBeInTheDocument(); +}); + +test('disable menu when enableMenu is false', () => { + const { queryByText, queryByTestId } = render( +
, + ); + expect(queryByText(mockedProps.displayName)).toBeTruthy(); + expect(queryByTestId('mock-dropdown')).toBeFalsy(); +}); + +test('hide display name for PIVOT_COL_ID', () => { + const { queryByText } = render( +
PIVOT_COL_ID, + isPinnedLeft: () => true, + isPinnedRight: () => false, + getSortIndex: () => null, + } as any as Column + } + />, + ); + expect(queryByText(mockedProps.displayName)).not.toBeInTheDocument(); +}); diff --git a/superset-frontend/src/components/GridTable/Header.tsx b/superset-frontend/src/components/GridTable/Header.tsx new file mode 100644 index 000000000000..bfcf29b77f5a --- /dev/null +++ b/superset-frontend/src/components/GridTable/Header.tsx @@ -0,0 +1,181 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { styled, useTheme, t } from '@superset-ui/core'; +import type { Column, GridApi } from 'ag-grid-community'; + +import Icons from 'src/components/Icons'; +import HeaderMenu from './HeaderMenu'; + +interface Params { + enableMenu?: boolean; + enableSorting?: boolean; + displayName: string; + column: Column; + api: GridApi; + setSort: (sort: string | null, multiSort: boolean) => void; +} + +export const PIVOT_COL_ID = '-1'; +const SORT_DIRECTION = [null, 'asc', 'desc']; + +const HeaderCell = styled.div` + display: flex; + flex: 1; + &[role='button'] { + cursor: pointer; + } +`; + +const HeaderCellSort = styled.div` + position: relative; + display: inline-flex; + align-items: center; +`; + +const SortSeqLabel = styled.span` + position: absolute; + right: 0; +`; + +const HeaderAction = styled.div` + display: none; + position: absolute; + right: ${({ theme }) => theme.gridUnit * 3}px; + &.main { + margin: 0 auto; + right: auto; + left: ${({ theme }) => theme.gridUnit * 2.5 - 1}px; + } + & .ant-dropdown-trigger { + cursor: context-menu; + padding: ${({ theme }) => theme.gridUnit * 2}px; + background-color: var(--ag-background-color); + box-shadow: 0 0 2px var(--ag-chip-border-color); + border-radius: 50%; + &:hover { + box-shadow: 0 0 4px ${({ theme }) => theme.colors.grayscale.light1}; + } + } +`; + +const IconPlaceholder = styled.div` + position: absolute; + top: 0; +`; + +const Header: React.FC = ({ + enableMenu, + enableSorting, + displayName, + setSort, + column, + api, +}: Params) => { + const theme = useTheme(); + const colId = column.getColId(); + const pinnedLeft = column.isPinnedLeft(); + const pinnedRight = column.isPinnedRight(); + const sortOption = useRef(0); + const [invisibleColumns, setInvisibleColumns] = useState([]); + const [currentSort, setCurrentSort] = useState(null); + const [sortIndex, setSortIndex] = useState(); + const onSort = useCallback( + event => { + sortOption.current = (sortOption.current + 1) % SORT_DIRECTION.length; + const sort = SORT_DIRECTION[sortOption.current]; + setSort(sort, event.shiftKey); + setCurrentSort(sort); + }, + [setSort], + ); + const onVisibleChange = useCallback( + (isVisible: boolean) => { + if (isVisible) { + setInvisibleColumns( + api.getColumns()?.filter(c => !c.isVisible()) || [], + ); + } + }, + [api], + ); + + const onSortChanged = useCallback(() => { + const hasMultiSort = + api.getAllDisplayedColumns().findIndex(c => c.getSortIndex()) !== -1; + const updatedSortIndex = column.getSortIndex(); + sortOption.current = SORT_DIRECTION.indexOf(column.getSort() ?? null); + setCurrentSort(column.getSort() ?? null); + setSortIndex(hasMultiSort ? updatedSortIndex : null); + }, [api, column]); + + useEffect(() => { + api.addEventListener('sortChanged', onSortChanged); + + return () => { + if (api.isDestroyed()) return; + api.removeEventListener('sortChanged', onSortChanged); + }; + }, [api, onSortChanged]); + + return ( + <> + {colId !== PIVOT_COL_ID && ( + +
{displayName}
+ {enableSorting && ( + + + + {currentSort === 'asc' && ( + + )} + {currentSort === 'desc' && ( + + )} + + {typeof sortIndex === 'number' && ( + {sortIndex + 1} + )} + + )} +
+ )} + {enableMenu && colId && api && ( + + {colId && ( + + )} + + )} + + ); +}; + +export default Header; diff --git a/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx b/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx new file mode 100644 index 000000000000..159a29a355dd --- /dev/null +++ b/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx @@ -0,0 +1,195 @@ +import type { Column, GridApi } from 'ag-grid-community'; +import { + fireEvent, + render, + waitFor, + screen, +} from 'spec/helpers/testing-library'; +import HeaderMenu from './HeaderMenu'; + +jest.mock('src/components/Menu', () => { + const Menu = ({ children }: { children: React.ReactChild }) => ( +
{children}
+ ); + Menu.Item = ({ + children, + onClick, + }: { + children: React.ReactChild; + onClick: () => void; + }) => ( + + ); + Menu.SubMenu = ({ + title, + children, + }: { + title: React.ReactNode; + children: React.ReactNode; + }) => ( +
+ {title} + +
+ ); + Menu.Divider = () =>
; + return { Menu }; +}); + +jest.mock('src/components/Icons', () => ({ + DownloadOutlined: () =>
, + CopyOutlined: () =>
, + UnlockOutlined: () =>
, + VerticalRightOutlined: () =>
, + VerticalLeftOutlined: () =>
, + EyeInvisibleOutlined: () =>
, + EyeOutlined: () =>
, +})); + +jest.mock('src/components/Dropdown', () => ({ + Dropdown: ({ overlay }: { overlay: React.ReactChild }) => ( +
{overlay}
+ ), +})); + +jest.mock('src/utils/copy', () => jest.fn().mockImplementation(f => f())); + +const mockInvisibleColumn = { + getColId: jest.fn().mockReturnValue('column2'), + getColDef: jest.fn().mockReturnValue({ headerName: 'column2' }), + getDataAsCsv: jest.fn().mockReturnValue('csv'), +} as any as Column; + +const mockInvisibleColumn3 = { + getColId: jest.fn().mockReturnValue('column3'), + getColDef: jest.fn().mockReturnValue({ headerName: 'column3' }), + getDataAsCsv: jest.fn().mockReturnValue('csv'), +} as any as Column; + +const mockGridApi = { + setColumnPinned: jest.fn(), + getColumns: jest.fn().mockReturnValue([]), + getDataAsCsv: jest.fn().mockReturnValue('csv'), + exportDataAsCsv: jest.fn().mockReturnValue('csv'), + getAllDisplayedColumns: jest.fn().mockReturnValue([]), + setColumnsVisible: jest.fn(), + setColumnVisible: jest.fn(), +} as any as GridApi; + +const mockedProps = { + colId: 'column1', + invisibleColumns: [], + api: mockGridApi, + onVisibleChange: jest.fn(), +}; + +afterEach(() => { + (mockGridApi.setColumnPinned as jest.Mock).mockClear(); + (mockGridApi.setColumnVisible as jest.Mock).mockClear(); +}); + +test('renders copy data', () => { + const { queryByText } = render(); + expect(queryByText('Copy')).toBeTruthy(); +}); + +test('renders buttons pinning both side', () => { + const { queryByText, getByText } = render(); + expect(queryByText('Pin Left')).toBeTruthy(); + expect(queryByText('Pin Right')).toBeTruthy(); + fireEvent.click(getByText('Pin Left')); + expect(mockGridApi.setColumnPinned).toHaveBeenCalledTimes(1); + expect(mockGridApi.setColumnPinned).toHaveBeenCalledWith( + mockedProps.colId, + 'left', + ); + fireEvent.click(getByText('Pin Right')); + expect(mockGridApi.setColumnPinned).toHaveBeenLastCalledWith( + mockedProps.colId, + 'right', + ); +}); + +test('renders unpin on pinned left', () => { + const { queryByText, getByText } = render( + , + ); + expect(queryByText('Pin Left')).toBeFalsy(); + expect(queryByText('Unpin')).toBeTruthy(); + fireEvent.click(getByText('Unpin')); + expect(mockGridApi.setColumnPinned).toHaveBeenCalledTimes(1); + expect(mockGridApi.setColumnPinned).toHaveBeenCalledWith( + mockedProps.colId, + null, + ); +}); + +test('renders unpin on pinned right', () => { + const { queryByText } = render(); + expect(queryByText('Pin Right')).toBeFalsy(); + expect(queryByText('Unpin')).toBeTruthy(); +}); + +test('renders unhide when invisible column exists', async () => { + const { queryByText } = render( + , + ); + expect(queryByText('Unhide')).toBeTruthy(); + const unhideColumnsButton = await screen.findByText('column2'); + fireEvent.click(unhideColumnsButton); + expect(mockGridApi.setColumnVisible).toHaveBeenCalledTimes(1); + expect(mockGridApi.setColumnVisible).toHaveBeenCalledWith('column2', true); +}); + +describe('for main menu', () => { + test('renders Copy to Clipboard', async () => { + const { getByText, queryByTestId } = render( + , + ); + expect(queryByTestId('mock-Divider')).not.toBeInTheDocument(); + fireEvent.click(getByText('Copy the current data')); + await waitFor(() => + expect(mockGridApi.getDataAsCsv).toHaveBeenCalledTimes(1), + ); + expect(mockGridApi.getDataAsCsv).toHaveBeenCalledWith({ + columnKeys: [], + columnSeparator: '\t', + suppressQuotes: true, + }); + }); + + test('renders Download to CSV', async () => { + const { getByText } = render(); + fireEvent.click(getByText('Download to CSV')); + await waitFor(() => + expect(mockGridApi.exportDataAsCsv).toHaveBeenCalledTimes(1), + ); + expect(mockGridApi.exportDataAsCsv).toHaveBeenCalledWith({ + columnKeys: [], + }); + }); + + test('renders all unhide all hidden columns when multiple invisible columns exist', async () => { + const { queryByTestId } = render( + , + ); + expect(queryByTestId('mock-Divider')).toBeInTheDocument(); + const unhideColumnsButton = await screen.findByText( + `All ${2} hidden columns`, + ); + fireEvent.click(unhideColumnsButton); + expect(mockGridApi.setColumnsVisible).toHaveBeenCalledTimes(1); + expect(mockGridApi.setColumnsVisible).toHaveBeenCalledWith( + [mockInvisibleColumn, mockInvisibleColumn3], + true, + ); + }); +}); diff --git a/superset-frontend/src/components/GridTable/HeaderMenu.tsx b/superset-frontend/src/components/GridTable/HeaderMenu.tsx new file mode 100644 index 000000000000..177fc43a5324 --- /dev/null +++ b/superset-frontend/src/components/GridTable/HeaderMenu.tsx @@ -0,0 +1,214 @@ +import { useCallback } from 'react'; +import { css, t } from '@superset-ui/core'; +import type { Column, ColumnPinnedType, GridApi } from 'ag-grid-community'; + +import Icons from 'src/components/Icons'; +import { Dropdown, DropdownProps } from 'src/components/Dropdown'; +import { Menu } from 'src/components/Menu'; +import copyTextToClipboard from 'src/utils/copy'; + +type Params = { + colId: string; + column?: Column; + api: GridApi; + pinnedLeft?: boolean; + pinnedRight?: boolean; + invisibleColumns: Column[]; + isMain?: boolean; + onVisibleChange: DropdownProps['onVisibleChange']; +}; + +const HeaderMenu: React.FC = ({ + colId, + api, + pinnedLeft, + pinnedRight, + invisibleColumns, + isMain, + onVisibleChange, +}: Params) => { + const pinColumn = useCallback( + (pinLoc: ColumnPinnedType) => { + api.setColumnPinned(colId, pinLoc); + }, + [api, colId], + ); + + const unHideAction = invisibleColumns.length > 0 && ( + + + {t('Unhide')} + + } + > + {invisibleColumns.length > 1 && ( + { + api.setColumnsVisible(invisibleColumns, true); + }} + > + {t('All %s hidden columns', invisibleColumns.length)} + + )} + {invisibleColumns.map(c => ( + { + api.setColumnVisible(c.getColId(), true); + }} + > + {c.getColDef().headerName} + + ))} + + ); + + if (isMain) { + return ( + + { + copyTextToClipboard( + () => + new Promise((resolve, reject) => { + const data = api.getDataAsCsv({ + columnKeys: api + .getAllDisplayedColumns() + .map(c => c.getColId()) + .filter(id => id !== colId), + suppressQuotes: true, + columnSeparator: '\t', + }); + if (data) { + resolve(data); + } else { + reject(); + } + }), + ); + }} + css={css` + display: flex; + align-items: center; + `} + > + {t('Copy the current data')} + + { + api.exportDataAsCsv({ + columnKeys: api + .getAllDisplayedColumns() + .map(c => c.getColId()) + .filter(id => id !== colId), + }); + }} + css={css` + display: flex; + align-items: center; + `} + > + {t('Download to CSV')} + + {invisibleColumns.length > 0 && } + {unHideAction} + + } + /> + ); + } + + return ( + + { + copyTextToClipboard( + () => + new Promise((resolve, reject) => { + const data = api.getDataAsCsv({ + columnKeys: [colId], + suppressQuotes: true, + }); + if (data) { + resolve(data); + } else { + reject(); + } + }), + ); + }} + css={css` + display: flex; + align-items: center; + `} + > + {t('Copy')} + + {(pinnedLeft || pinnedRight) && ( + pinColumn(null)} + css={css` + display: flex; + align-items: center; + `} + > + {t('Unpin')} + + )} + {!pinnedLeft && ( + pinColumn('left')} + css={css` + display: flex; + align-items: center; + `} + > + + {t('Pin Left')} + + )} + {!pinnedRight && ( + pinColumn('right')} + css={css` + display: flex; + align-items: center; + `} + > + + {t('Pin Right')} + + )} + + { + api.setColumnVisible(colId, false); + }} + css={css` + display: flex; + align-items: center; + `} + disabled={api.getColumns()?.length === invisibleColumns.length + 1} + > + + {t('Hide Column')} + + {unHideAction} + + } + /> + ); +}; + +export default HeaderMenu; diff --git a/superset-frontend/src/components/GridTable/index.tsx b/superset-frontend/src/components/GridTable/index.tsx new file mode 100644 index 000000000000..16957ddfa8f8 --- /dev/null +++ b/superset-frontend/src/components/GridTable/index.tsx @@ -0,0 +1,238 @@ +/** + * 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 { useCallback, useMemo } from 'react'; +import { Global } from '@emotion/react'; +import { css, useTheme } from '@superset-ui/core'; + +import type { Column } from 'ag-grid-community'; +import { AgGridReact, AgReactUiProps } from 'ag-grid-react'; + +import 'ag-grid-community/styles/ag-grid.css'; +import 'ag-grid-community/styles/ag-theme-alpine.css'; + +import copyTextToClipboard from 'src/utils/copy'; +import ErrorBoundary from 'src/components/ErrorBoundary'; + +import Header, { PIVOT_COL_ID } from './Header'; + +const gridComponents = { + agColumnHeader: Header, +}; + +export enum GridSize { + Small = 'small', + Middle = 'middle', +} + +export type ColDef = { + type: string; + field: string; +}; + +export interface TableProps { + /** + * Data that will populate the each row and map to the column key. + */ + data: RecordType[]; + /** + * Table column definitions. + */ + columns: { + label: string; + headerName?: string; + width?: number; + comparator?: (valueA: string | number, valueB: string | number) => number; + render?: (value: any) => React.ReactNode; + }[]; + + size?: GridSize; + + externalFilter?: AgReactUiProps['doesExternalFilterPass']; + + height: number; + + cellSelectable?: boolean; + + sortable?: boolean; + + enableActions?: boolean; + + showRowNumber?: boolean; + + usePagination?: boolean; + + striped?: boolean; +} + +const onSortChanged: AgReactUiProps['onSortChanged'] = ({ api }) => + api.refreshCells(); + +function GridTable({ + data, + columns, + sortable = true, + height, + externalFilter, + showRowNumber, + enableActions, + size = GridSize.Middle, + striped, +}: TableProps) { + const theme = useTheme(); + const isExternalFilterPresent = useCallback( + () => Boolean(externalFilter), + [externalFilter], + ); + const rowIndexLength = `${data.length}}`.length; + const onKeyDown: AgReactUiProps>['onCellKeyDown'] = + useCallback(({ event, column, data, value, api }) => { + if ( + !document.getSelection?.()?.toString?.() && + event && + event.key === 'c' && + (event.ctrlKey || event.metaKey) + ) { + const columns = + column.getColId() === PIVOT_COL_ID + ? api + .getAllDisplayedColumns() + .filter((column: Column) => column.getColId() !== PIVOT_COL_ID) + : [column]; + const record = + column.getColId() === PIVOT_COL_ID + ? [ + columns.map((column: Column) => column.getColId()).join('\t'), + columns + .map((column: Column) => data?.[column.getColId()]) + .join('\t'), + ].join('\n') + : String(value); + copyTextToClipboard(() => Promise.resolve(record)); + } + }, []); + const columnDefs = useMemo( + () => + [ + { + field: PIVOT_COL_ID, + valueGetter: 'node.rowIndex+1', + cellClass: 'locked-col', + width: 20 + rowIndexLength * 6, + suppressNavigable: true, + resizable: false, + pinned: 'left' as const, + sortable: false, + }, + ...columns.map( + ( + { label, headerName, width, render: cellRenderer, comparator }, + index, + ) => ({ + field: label, + headerName, + cellRenderer, + sortable, + comparator, + ...(index === columns.length - 1 && { + flex: 1, + width, + minWidth: 200, + }), + }), + ), + ].slice(showRowNumber ? 0 : 1), + [columns, sortable, showRowNumber, rowIndexLength], + ); + const defaultColDef: AgReactUiProps['defaultColDef'] = { + suppressMovable: true, + resizable: true, + sortable, + filter: Boolean(enableActions), + }; + + const rowHeight = theme.gridUnit * (size === GridSize.Middle ? 9 : 7); + + return ( + + css` + #grid-table.ag-theme-alpine { + --ag-icon-font-family: agGridMaterial; + --ag-grid-size: ${theme.gridUnit}px; + --ag-font-size: ${theme.typography.sizes.s - 1}px; + --ag-font-family: ${theme.typography.families.sansSerif}; + --ag-row-height: ${rowHeight}px; + ${!striped && + `--ag-odd-row-background-color: ${theme.colors.grayscale.light5};`} + --ag-border-color: ${theme.colors.grayscale.light2}; + --ag-row-border-color: ${theme.colors.grayscale.light2}; + --ag-header-background-color: ${theme.colors.grayscale.light4}; + } + #grid-table .ag-cell { + -webkit-font-smoothing: antialiased; + } + .locked-col { + background: var(--ag-header-background-color); + padding: 0; + text-align: center; + font-size: calc(var(--ag-font-size) * 0.9); + color: var(--ag-disabled-foreground-color); + } + .ag-row-hover .locked-col { + background: var(--ag-row-hover-color); + } + .ag-header-cell { + overflow: hidden; + } + & [role='columnheader']:hover .customHeaderAction { + display: block; + } + `} + /> +
+ +
+
+ ); +} + +export default GridTable; diff --git a/superset-frontend/src/components/Icons/AntdEnhanced.tsx b/superset-frontend/src/components/Icons/AntdEnhanced.tsx index 3032a8a82691..fbd13b344d8f 100644 --- a/superset-frontend/src/components/Icons/AntdEnhanced.tsx +++ b/superset-frontend/src/components/Icons/AntdEnhanced.tsx @@ -27,7 +27,9 @@ import { BarChartOutlined, BellOutlined, BookOutlined, + CaretDownOutlined, CalendarOutlined, + CaretUpOutlined, CheckOutlined, CheckSquareOutlined, CloseOutlined, @@ -38,6 +40,7 @@ import { DatabaseOutlined, DeleteFilled, DownOutlined, + DownloadOutlined, EditOutlined, ExclamationCircleOutlined, EyeOutlined, @@ -65,8 +68,11 @@ import { StopOutlined, SyncOutlined, TagsOutlined, + UnlockOutlined, UpOutlined, UserOutlined, + VerticalLeftOutlined, + VerticalRightOutlined, } from '@ant-design/icons'; import { StyledIcon } from './Icon'; import IconType from './IconType'; @@ -80,7 +86,9 @@ const AntdIcons = { BarChartOutlined, BellOutlined, BookOutlined, + CaretDownOutlined, CalendarOutlined, + CaretUpOutlined, CheckOutlined, CheckSquareOutlined, CloseOutlined, @@ -91,6 +99,7 @@ const AntdIcons = { DatabaseOutlined, DeleteFilled, DownOutlined, + DownloadOutlined, EditOutlined, ExclamationCircleOutlined, EyeOutlined, @@ -118,8 +127,11 @@ const AntdIcons = { StopOutlined, SyncOutlined, TagsOutlined, + UnlockOutlined, UpOutlined, UserOutlined, + VerticalLeftOutlined, + VerticalRightOutlined, }; const AntdEnhancedIcons = Object.keys(AntdIcons) From 59794810003b80c081b200ee5daec971eb4b7081 Mon Sep 17 00:00:00 2001 From: justinpark Date: Fri, 9 Aug 2024 11:43:51 +0900 Subject: [PATCH 02/20] missing license --- .../components/GridTable/GridTable.test.tsx | 18 ++++++++++++++++++ .../src/components/GridTable/Header.test.tsx | 18 ++++++++++++++++++ .../src/components/GridTable/Header.tsx | 18 ++++++++++++++++++ .../components/GridTable/HeaderMenu.test.tsx | 18 ++++++++++++++++++ .../src/components/GridTable/HeaderMenu.tsx | 18 ++++++++++++++++++ 5 files changed, 90 insertions(+) diff --git a/superset-frontend/src/components/GridTable/GridTable.test.tsx b/superset-frontend/src/components/GridTable/GridTable.test.tsx index 5b4e26ac3247..8d38283ee405 100644 --- a/superset-frontend/src/components/GridTable/GridTable.test.tsx +++ b/superset-frontend/src/components/GridTable/GridTable.test.tsx @@ -1,3 +1,21 @@ +/** + * 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 { render } from 'spec/helpers/testing-library'; import GridTable from '.'; diff --git a/superset-frontend/src/components/GridTable/Header.test.tsx b/superset-frontend/src/components/GridTable/Header.test.tsx index 086960df0d3d..3163ed2b670e 100644 --- a/superset-frontend/src/components/GridTable/Header.test.tsx +++ b/superset-frontend/src/components/GridTable/Header.test.tsx @@ -1,3 +1,21 @@ +/** + * 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 type { Column, GridApi } from 'ag-grid-community'; import { act, fireEvent, render } from 'spec/helpers/testing-library'; import Header, { PIVOT_COL_ID } from './Header'; diff --git a/superset-frontend/src/components/GridTable/Header.tsx b/superset-frontend/src/components/GridTable/Header.tsx index bfcf29b77f5a..80929801769d 100644 --- a/superset-frontend/src/components/GridTable/Header.tsx +++ b/superset-frontend/src/components/GridTable/Header.tsx @@ -1,3 +1,21 @@ +/** + * 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 { useCallback, useEffect, useRef, useState } from 'react'; import { styled, useTheme, t } from '@superset-ui/core'; import type { Column, GridApi } from 'ag-grid-community'; diff --git a/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx b/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx index 159a29a355dd..9afd20fb395e 100644 --- a/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx +++ b/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx @@ -1,3 +1,21 @@ +/** + * 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 type { Column, GridApi } from 'ag-grid-community'; import { fireEvent, diff --git a/superset-frontend/src/components/GridTable/HeaderMenu.tsx b/superset-frontend/src/components/GridTable/HeaderMenu.tsx index 177fc43a5324..4e04bcaae350 100644 --- a/superset-frontend/src/components/GridTable/HeaderMenu.tsx +++ b/superset-frontend/src/components/GridTable/HeaderMenu.tsx @@ -1,3 +1,21 @@ +/** + * 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 { useCallback } from 'react'; import { css, t } from '@superset-ui/core'; import type { Column, ColumnPinnedType, GridApi } from 'ag-grid-community'; From 3ae9a8059da1b89415331aaba91242a3d340b05a Mon Sep 17 00:00:00 2001 From: justinpark Date: Fri, 9 Aug 2024 21:52:55 +0900 Subject: [PATCH 03/20] fix unit test --- .../src/SqlLab/components/ResultSet/ResultSet.test.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx index 565cebe0a516..188946d4c726 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx @@ -354,7 +354,7 @@ describe('ResultSet', () => { ); }); const { getByRole } = setup(mockedProps, mockStore(initialState)); - expect(getByRole('table')).toBeInTheDocument(); + expect(getByRole('treegrid')).toBeInTheDocument(); }); test('renders if there is a limit in query.results but not queryLimit', async () => { @@ -372,7 +372,7 @@ describe('ResultSet', () => { }, }), ); - expect(getByRole('table')).toBeInTheDocument(); + expect(getByRole('treegrid')).toBeInTheDocument(); }); test('Async queries - renders "Fetch data preview" button when data preview has no results', () => { @@ -400,7 +400,7 @@ describe('ResultSet', () => { name: /fetch data preview/i, }), ).toBeVisible(); - expect(screen.queryByRole('table')).not.toBeInTheDocument(); + expect(screen.queryByRole('treegrid')).not.toBeInTheDocument(); }); test('Async queries - renders "Refetch results" button when a query has no results', () => { @@ -429,7 +429,7 @@ describe('ResultSet', () => { name: /refetch results/i, }), ).toBeVisible(); - expect(screen.queryByRole('table')).not.toBeInTheDocument(); + expect(screen.queryByRole('treegrid')).not.toBeInTheDocument(); }); test('Async queries - renders on the first call', () => { @@ -449,7 +449,7 @@ describe('ResultSet', () => { }, }), ); - expect(screen.getByRole('table')).toBeVisible(); + expect(screen.getByRole('treegrid')).toBeVisible(); expect( screen.queryByRole('button', { name: /fetch data preview/i, From bdde6e0c26424ffef2b1670e504743eaf1213e21 Mon Sep 17 00:00:00 2001 From: justinpark Date: Sun, 11 Aug 2024 19:09:17 +0900 Subject: [PATCH 04/20] fix unit test --- .../src/components/GridTable/Header.test.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/superset-frontend/src/components/GridTable/Header.test.tsx b/superset-frontend/src/components/GridTable/Header.test.tsx index 3163ed2b670e..3045616e3536 100644 --- a/superset-frontend/src/components/GridTable/Header.test.tsx +++ b/superset-frontend/src/components/GridTable/Header.test.tsx @@ -34,11 +34,16 @@ class MockApi extends EventTarget { getAllDisplayedColumns() { return []; } + + isDestroyed() { + return false; + } } const mockedProps = { displayName: 'test column', setSort: jest.fn(), + enableSorting: true, column: { getColId: () => '123', isPinnedLeft: () => true, @@ -57,13 +62,13 @@ test('renders display name for the column', () => { test('sorts by clicking a column header', () => { const { getByText, queryByTestId } = render(
); fireEvent.click(getByText(mockedProps.displayName)); - expect(mockedProps.setSort).toBeCalledWith('asc', false); + expect(mockedProps.setSort).toHaveBeenCalledWith('asc', false); expect(queryByTestId('mock-sort-asc')).toBeInTheDocument(); fireEvent.click(getByText(mockedProps.displayName)); - expect(mockedProps.setSort).toBeCalledWith('desc', false); + expect(mockedProps.setSort).toHaveBeenCalledWith('desc', false); expect(queryByTestId('mock-sort-desc')).toBeInTheDocument(); fireEvent.click(getByText(mockedProps.displayName)); - expect(mockedProps.setSort).toBeCalledWith(null, false); + expect(mockedProps.setSort).toHaveBeenCalledWith(null, false); expect(queryByTestId('mock-sort-asc')).not.toBeInTheDocument(); expect(queryByTestId('mock-sort-desc')).not.toBeInTheDocument(); }); From c0b5fcb457aafb0080a6f74cf15c433f7eeab67b Mon Sep 17 00:00:00 2001 From: justinpark Date: Thu, 15 Aug 2024 16:15:52 +0900 Subject: [PATCH 05/20] centering header main position --- superset-frontend/src/components/GridTable/Header.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/superset-frontend/src/components/GridTable/Header.tsx b/superset-frontend/src/components/GridTable/Header.tsx index 80929801769d..a0ce2f4c0a4d 100644 --- a/superset-frontend/src/components/GridTable/Header.tsx +++ b/superset-frontend/src/components/GridTable/Header.tsx @@ -60,8 +60,9 @@ const HeaderAction = styled.div` right: ${({ theme }) => theme.gridUnit * 3}px; &.main { margin: 0 auto; - right: auto; - left: ${({ theme }) => theme.gridUnit * 2.5 - 1}px; + left: 0; + right: 0; + width: 20px; } & .ant-dropdown-trigger { cursor: context-menu; From 58c6cd60d7ad4711bcff92025e46dc69b6bcd325 Mon Sep 17 00:00:00 2001 From: justinpark Date: Tue, 20 Aug 2024 08:07:12 +0900 Subject: [PATCH 06/20] update theme by quartz --- superset-frontend/src/components/GridTable/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/superset-frontend/src/components/GridTable/index.tsx b/superset-frontend/src/components/GridTable/index.tsx index 16957ddfa8f8..c97ec2ad5179 100644 --- a/superset-frontend/src/components/GridTable/index.tsx +++ b/superset-frontend/src/components/GridTable/index.tsx @@ -24,7 +24,7 @@ import type { Column } from 'ag-grid-community'; import { AgGridReact, AgReactUiProps } from 'ag-grid-react'; import 'ag-grid-community/styles/ag-grid.css'; -import 'ag-grid-community/styles/ag-theme-alpine.css'; +import 'ag-grid-community/styles/ag-theme-quartz.css'; import copyTextToClipboard from 'src/utils/copy'; import ErrorBoundary from 'src/components/ErrorBoundary'; @@ -172,7 +172,7 @@ function GridTable({ css` - #grid-table.ag-theme-alpine { + #grid-table.ag-theme-quartz { --ag-icon-font-family: agGridMaterial; --ag-grid-size: ${theme.gridUnit}px; --ag-font-size: ${theme.typography.sizes.s - 1}px; @@ -207,7 +207,7 @@ function GridTable({ />
Date: Tue, 20 Aug 2024 11:36:55 +0900 Subject: [PATCH 07/20] adjust fontsize by size --- superset-frontend/src/components/GridTable/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/superset-frontend/src/components/GridTable/index.tsx b/superset-frontend/src/components/GridTable/index.tsx index c97ec2ad5179..92424b673399 100644 --- a/superset-frontend/src/components/GridTable/index.tsx +++ b/superset-frontend/src/components/GridTable/index.tsx @@ -175,7 +175,9 @@ function GridTable({ #grid-table.ag-theme-quartz { --ag-icon-font-family: agGridMaterial; --ag-grid-size: ${theme.gridUnit}px; - --ag-font-size: ${theme.typography.sizes.s - 1}px; + --ag-font-size: ${theme.typography.sizes[ + size === GridSize.Middle ? 'm' : 's' + ]}px; --ag-font-family: ${theme.typography.families.sansSerif}; --ag-row-height: ${rowHeight}px; ${!striped && From 284ad5b77b64acbdb2510b56dba37239a6c91d1a Mon Sep 17 00:00:00 2001 From: justinpark Date: Tue, 20 Aug 2024 11:47:07 +0900 Subject: [PATCH 08/20] set size to small --- superset-frontend/src/components/FilterableTable/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/components/FilterableTable/index.tsx b/superset-frontend/src/components/FilterableTable/index.tsx index c30574b93bbb..0ff1e0ded9d7 100644 --- a/superset-frontend/src/components/FilterableTable/index.tsx +++ b/superset-frontend/src/components/FilterableTable/index.tsx @@ -144,7 +144,7 @@ const FilterableTable = ({ data-test="table-container" > Date: Wed, 21 Aug 2024 16:48:22 +0900 Subject: [PATCH 09/20] allow multi row selection --- superset-frontend/src/components/GridTable/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/superset-frontend/src/components/GridTable/index.tsx b/superset-frontend/src/components/GridTable/index.tsx index 92424b673399..7a91886f9e76 100644 --- a/superset-frontend/src/components/GridTable/index.tsx +++ b/superset-frontend/src/components/GridTable/index.tsx @@ -228,6 +228,7 @@ function GridTable({ ensureDomOrder: true, suppressFieldDotNotation: true, headerHeight: rowHeight, + rowSelection: 'multiple', rowHeight, }} onCellKeyDown={onKeyDown} From 57fdf860d874b17ca80ef0817cbaacb28ecd6e2a Mon Sep 17 00:00:00 2001 From: justinpark Date: Thu, 22 Aug 2024 18:59:59 +0900 Subject: [PATCH 10/20] Add column movable and autosize --- .../src/components/FilterableTable/index.tsx | 1 + .../src/components/GridTable/Header.tsx | 2 +- .../src/components/GridTable/HeaderMenu.tsx | 117 ++++++++++-------- .../src/components/GridTable/constants.ts | 24 ++++ .../src/components/GridTable/index.tsx | 16 +-- .../src/components/Icons/AntdEnhanced.tsx | 2 + superset/config.py | 4 +- 7 files changed, 105 insertions(+), 61 deletions(-) create mode 100644 superset-frontend/src/components/GridTable/constants.ts diff --git a/superset-frontend/src/components/FilterableTable/index.tsx b/superset-frontend/src/components/FilterableTable/index.tsx index 0ff1e0ded9d7..920e7d68cd62 100644 --- a/superset-frontend/src/components/FilterableTable/index.tsx +++ b/superset-frontend/src/components/FilterableTable/index.tsx @@ -153,6 +153,7 @@ const FilterableTable = ({ showRowNumber striped={striped} enableActions + columnReorderable /> ); diff --git a/superset-frontend/src/components/GridTable/Header.tsx b/superset-frontend/src/components/GridTable/Header.tsx index a0ce2f4c0a4d..d6a9026b56f5 100644 --- a/superset-frontend/src/components/GridTable/Header.tsx +++ b/superset-frontend/src/components/GridTable/Header.tsx @@ -21,6 +21,7 @@ import { styled, useTheme, t } from '@superset-ui/core'; import type { Column, GridApi } from 'ag-grid-community'; import Icons from 'src/components/Icons'; +import { PIVOT_COL_ID } from './constants'; import HeaderMenu from './HeaderMenu'; interface Params { @@ -32,7 +33,6 @@ interface Params { setSort: (sort: string | null, multiSort: boolean) => void; } -export const PIVOT_COL_ID = '-1'; const SORT_DIRECTION = [null, 'asc', 'desc']; const HeaderCell = styled.div` diff --git a/superset-frontend/src/components/GridTable/HeaderMenu.tsx b/superset-frontend/src/components/GridTable/HeaderMenu.tsx index 4e04bcaae350..ecb51dde5b7a 100644 --- a/superset-frontend/src/components/GridTable/HeaderMenu.tsx +++ b/superset-frontend/src/components/GridTable/HeaderMenu.tsx @@ -17,13 +17,22 @@ * under the License. */ import { useCallback } from 'react'; -import { css, t } from '@superset-ui/core'; +import { styled, t } from '@superset-ui/core'; import type { Column, ColumnPinnedType, GridApi } from 'ag-grid-community'; import Icons from 'src/components/Icons'; import { Dropdown, DropdownProps } from 'src/components/Dropdown'; import { Menu } from 'src/components/Menu'; import copyTextToClipboard from 'src/utils/copy'; +import { PIVOT_COL_ID } from './constants'; + +const IconMenuItem = styled(Menu.Item)` + display: flex; + align-items: center; +`; +const IconEmpty = styled.span` + width: 20px; +`; type Params = { colId: string; @@ -72,6 +81,7 @@ const HeaderMenu: React.FC = ({ )} {invisibleColumns.map(c => ( { api.setColumnVisible(c.getColId(), true); }} @@ -90,7 +100,7 @@ const HeaderMenu: React.FC = ({ onVisibleChange={onVisibleChange} overlay={ - { copyTextToClipboard( () => @@ -111,14 +121,10 @@ const HeaderMenu: React.FC = ({ }), ); }} - css={css` - display: flex; - align-items: center; - `} > {t('Copy the current data')} - - + { api.exportDataAsCsv({ columnKeys: api @@ -127,15 +133,42 @@ const HeaderMenu: React.FC = ({ .filter(id => id !== colId), }); }} - css={css` - display: flex; - align-items: center; - `} > {t('Download to CSV')} - - {invisibleColumns.length > 0 && } + + + { + api.autoSizeAllColumns(); + }} + > + + {t('Autosize all columns')} + {unHideAction} + + { + api.setColumnsVisible(invisibleColumns, true); + const columns = api.getColumns(); + if (columns) { + const pinnedColumns = columns.filter( + c => c.getColId() !== PIVOT_COL_ID && c.isPinned(), + ); + api.setColumnsPinned(pinnedColumns, null); + api.moveColumns(columns, 0); + const firstColumn = columns.find( + c => c.getColId() !== PIVOT_COL_ID, + ); + if (firstColumn) { + api.ensureColumnVisible(firstColumn, 'start'); + } + } + }} + > + + {t('Reset columns')} + } /> @@ -149,7 +182,7 @@ const HeaderMenu: React.FC = ({ onVisibleChange={onVisibleChange} overlay={ - { copyTextToClipboard( () => @@ -166,62 +199,46 @@ const HeaderMenu: React.FC = ({ }), ); }} - css={css` - display: flex; - align-items: center; - `} > {t('Copy')} - + {(pinnedLeft || pinnedRight) && ( - pinColumn(null)} - css={css` - display: flex; - align-items: center; - `} - > + pinColumn(null)}> {t('Unpin')} - + )} {!pinnedLeft && ( - pinColumn('left')} - css={css` - display: flex; - align-items: center; - `} - > + pinColumn('left')}> {t('Pin Left')} - + )} {!pinnedRight && ( - pinColumn('right')} - css={css` - display: flex; - align-items: center; - `} - > + pinColumn('right')}> {t('Pin Right')} - + )} - { + const column = api.getColumn(colId); + column?.setColDef(column?.getColDef(), { minWidth: undefined }); + api.autoSizeColumn(colId); + }} + > + + {t('Autosize Column')} + + { api.setColumnVisible(colId, false); }} - css={css` - display: flex; - align-items: center; - `} disabled={api.getColumns()?.length === invisibleColumns.length + 1} > {t('Hide Column')} - + {unHideAction} } diff --git a/superset-frontend/src/components/GridTable/constants.ts b/superset-frontend/src/components/GridTable/constants.ts new file mode 100644 index 000000000000..42f88fd8cc55 --- /dev/null +++ b/superset-frontend/src/components/GridTable/constants.ts @@ -0,0 +1,24 @@ +/** + * 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. + */ +export const PIVOT_COL_ID = '-1'; + +export enum GridSize { + Small = 'small', + Middle = 'middle', +} diff --git a/superset-frontend/src/components/GridTable/index.tsx b/superset-frontend/src/components/GridTable/index.tsx index 7a91886f9e76..ef826f2c6fa6 100644 --- a/superset-frontend/src/components/GridTable/index.tsx +++ b/superset-frontend/src/components/GridTable/index.tsx @@ -29,16 +29,14 @@ import 'ag-grid-community/styles/ag-theme-quartz.css'; import copyTextToClipboard from 'src/utils/copy'; import ErrorBoundary from 'src/components/ErrorBoundary'; -import Header, { PIVOT_COL_ID } from './Header'; +import { PIVOT_COL_ID, GridSize } from './constants'; +import Header from './Header'; const gridComponents = { agColumnHeader: Header, }; -export enum GridSize { - Small = 'small', - Middle = 'middle', -} +export { GridSize }; export type ColDef = { type: string; @@ -67,7 +65,7 @@ export interface TableProps { height: number; - cellSelectable?: boolean; + columnReorderable?: boolean; sortable?: boolean; @@ -87,6 +85,7 @@ function GridTable({ data, columns, sortable = true, + columnReorderable, height, externalFilter, showRowNumber, @@ -138,6 +137,7 @@ function GridTable({ resizable: false, pinned: 'left' as const, sortable: false, + ...(columnReorderable && { suppressMovable: true }), }, ...columns.map( ( @@ -157,10 +157,10 @@ function GridTable({ }), ), ].slice(showRowNumber ? 0 : 1), - [columns, sortable, showRowNumber, rowIndexLength], + [rowIndexLength, columnReorderable, columns, showRowNumber, sortable], ); const defaultColDef: AgReactUiProps['defaultColDef'] = { - suppressMovable: true, + ...(!columnReorderable && { suppressMovable: true }), resizable: true, sortable, filter: Boolean(enableActions), diff --git a/superset-frontend/src/components/Icons/AntdEnhanced.tsx b/superset-frontend/src/components/Icons/AntdEnhanced.tsx index fbd13b344d8f..eadd0ea2edc3 100644 --- a/superset-frontend/src/components/Icons/AntdEnhanced.tsx +++ b/superset-frontend/src/components/Icons/AntdEnhanced.tsx @@ -33,6 +33,7 @@ import { CheckOutlined, CheckSquareOutlined, CloseOutlined, + ColumnWidthOutlined, CommentOutlined, ConsoleSqlOutlined, CopyOutlined, @@ -92,6 +93,7 @@ const AntdIcons = { CheckOutlined, CheckSquareOutlined, CloseOutlined, + ColumnWidthOutlined, CommentOutlined, ConsoleSqlOutlined, CopyOutlined, diff --git a/superset/config.py b/superset/config.py index 8554b2a26451..ae4561f1ce9d 100644 --- a/superset/config.py +++ b/superset/config.py @@ -1603,7 +1603,7 @@ def EMAIL_HEADER_MUTATOR( # pylint: disable=invalid-name,unused-argument # noq TALISMAN_CONFIG = { "content_security_policy": { "base-uri": ["'self'"], - "default-src": ["'self'"], + "default-src": ["'self'", "data:"], "img-src": [ "'self'", "blob:", @@ -1634,7 +1634,7 @@ def EMAIL_HEADER_MUTATOR( # pylint: disable=invalid-name,unused-argument # noq TALISMAN_DEV_CONFIG = { "content_security_policy": { "base-uri": ["'self'"], - "default-src": ["'self'"], + "default-src": ["'self'", "data:"], "img-src": [ "'self'", "blob:", From d124e3248efd2ecb114314e437556d7aa6d9d4f2 Mon Sep 17 00:00:00 2001 From: justinpark Date: Thu, 22 Aug 2024 20:53:41 +0900 Subject: [PATCH 11/20] fix test specs --- .../src/components/GridTable/Header.test.tsx | 3 ++- .../src/components/GridTable/HeaderMenu.test.tsx | 9 +++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/superset-frontend/src/components/GridTable/Header.test.tsx b/superset-frontend/src/components/GridTable/Header.test.tsx index 3045616e3536..c1154e7476c3 100644 --- a/superset-frontend/src/components/GridTable/Header.test.tsx +++ b/superset-frontend/src/components/GridTable/Header.test.tsx @@ -18,7 +18,8 @@ */ import type { Column, GridApi } from 'ag-grid-community'; import { act, fireEvent, render } from 'spec/helpers/testing-library'; -import Header, { PIVOT_COL_ID } from './Header'; +import Header from './Header'; +import { PIVOT_COL_ID } from './constants'; jest.mock('src/components/Dropdown', () => ({ Dropdown: () =>
, diff --git a/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx b/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx index 9afd20fb395e..0233308d0640 100644 --- a/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx +++ b/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx @@ -66,6 +66,7 @@ jest.mock('src/components/Icons', () => ({ VerticalLeftOutlined: () =>
, EyeInvisibleOutlined: () =>
, EyeOutlined: () =>
, + ColumnWidthOutlined: () =>
, })); jest.mock('src/components/Dropdown', () => ({ @@ -165,10 +166,7 @@ test('renders unhide when invisible column exists', async () => { describe('for main menu', () => { test('renders Copy to Clipboard', async () => { - const { getByText, queryByTestId } = render( - , - ); - expect(queryByTestId('mock-Divider')).not.toBeInTheDocument(); + const { getByText } = render(); fireEvent.click(getByText('Copy the current data')); await waitFor(() => expect(mockGridApi.getDataAsCsv).toHaveBeenCalledTimes(1), @@ -192,14 +190,13 @@ describe('for main menu', () => { }); test('renders all unhide all hidden columns when multiple invisible columns exist', async () => { - const { queryByTestId } = render( + render( , ); - expect(queryByTestId('mock-Divider')).toBeInTheDocument(); const unhideColumnsButton = await screen.findByText( `All ${2} hidden columns`, ); From ef8b92a2ec4f73fdd1920aa9f5dac24654a01543 Mon Sep 17 00:00:00 2001 From: justinpark Date: Fri, 23 Aug 2024 08:28:05 +0900 Subject: [PATCH 12/20] Add specs for additional menu actions --- .../components/GridTable/HeaderMenu.test.tsx | 68 ++++++++++++++++++- .../src/components/GridTable/HeaderMenu.tsx | 8 ++- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx b/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx index 0233308d0640..3a187e4cf868 100644 --- a/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx +++ b/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx @@ -89,14 +89,24 @@ const mockInvisibleColumn3 = { getDataAsCsv: jest.fn().mockReturnValue('csv'), } as any as Column; +const mockSetColDef = jest.fn(); + const mockGridApi = { + autoSizeColumn: jest.fn(), + autoSizeAllColumns: jest.fn(), setColumnPinned: jest.fn(), + getColumn: jest.fn().mockReturnValue({ + setColDef: mockSetColDef, + getColDef: jest.fn().mockReturnValue({}), + }), getColumns: jest.fn().mockReturnValue([]), getDataAsCsv: jest.fn().mockReturnValue('csv'), exportDataAsCsv: jest.fn().mockReturnValue('csv'), getAllDisplayedColumns: jest.fn().mockReturnValue([]), + setColumnsPinned: jest.fn(), setColumnsVisible: jest.fn(), setColumnVisible: jest.fn(), + moveColumns: jest.fn(), } as any as GridApi; const mockedProps = { @@ -107,13 +117,27 @@ const mockedProps = { }; afterEach(() => { + mockSetColDef.mockClear(); + (mockGridApi.getDataAsCsv as jest.Mock).mockClear(); (mockGridApi.setColumnPinned as jest.Mock).mockClear(); (mockGridApi.setColumnVisible as jest.Mock).mockClear(); + (mockGridApi.setColumnsVisible as jest.Mock).mockClear(); + (mockGridApi.setColumnsPinned as jest.Mock).mockClear(); + (mockGridApi.autoSizeColumn as jest.Mock).mockClear(); + (mockGridApi.autoSizeAllColumns as jest.Mock).mockClear(); + (mockGridApi.moveColumns as jest.Mock).mockClear(); }); -test('renders copy data', () => { - const { queryByText } = render(); - expect(queryByText('Copy')).toBeTruthy(); +test('renders copy data', async () => { + const { getByText } = render(); + fireEvent.click(getByText('Copy')); + await waitFor(() => + expect(mockGridApi.getDataAsCsv).toHaveBeenCalledTimes(1), + ); + expect(mockGridApi.getDataAsCsv).toHaveBeenCalledWith({ + columnKeys: [mockedProps.colId], + suppressQuotes: true, + }); }); test('renders buttons pinning both side', () => { @@ -153,6 +177,15 @@ test('renders unpin on pinned right', () => { expect(queryByText('Unpin')).toBeTruthy(); }); +test('renders autosize column', async () => { + const { getByText } = render(); + fireEvent.click(getByText('Autosize Column')); + await waitFor(() => + expect(mockGridApi.autoSizeColumn).toHaveBeenCalledTimes(1), + ); + expect(mockSetColDef).toHaveBeenCalledTimes(1); +}); + test('renders unhide when invisible column exists', async () => { const { queryByText } = render( , @@ -189,6 +222,14 @@ describe('for main menu', () => { }); }); + test('renders autosize column', async () => { + const { getByText } = render(); + fireEvent.click(getByText('Autosize all columns')); + await waitFor(() => + expect(mockGridApi.autoSizeAllColumns).toHaveBeenCalledTimes(1), + ); + }); + test('renders all unhide all hidden columns when multiple invisible columns exist', async () => { render( { true, ); }); + + test('reset columns configuration', async () => { + const { getByText } = render( + , + ); + fireEvent.click(getByText('Reset columns')); + await waitFor(() => + expect(mockGridApi.setColumnsVisible).toHaveBeenCalledTimes(1), + ); + expect(mockGridApi.setColumnsVisible).toHaveBeenCalledWith( + [mockInvisibleColumn], + true, + ); + expect(mockGridApi.setColumnsPinned).toHaveBeenCalledTimes(1); + expect(mockGridApi.setColumnsPinned).toHaveBeenCalledWith([], null); + expect(mockGridApi.moveColumns).toHaveBeenCalledTimes(1); + }); }); diff --git a/superset-frontend/src/components/GridTable/HeaderMenu.tsx b/superset-frontend/src/components/GridTable/HeaderMenu.tsx index ecb51dde5b7a..38c6ab74171d 100644 --- a/superset-frontend/src/components/GridTable/HeaderMenu.tsx +++ b/superset-frontend/src/components/GridTable/HeaderMenu.tsx @@ -223,7 +223,13 @@ const HeaderMenu: React.FC = ({ { const column = api.getColumn(colId); - column?.setColDef(column?.getColDef(), { minWidth: undefined }); + column?.setColDef( + { + ...column?.getColDef(), + minWidth: undefined, + }, + column?.getColDef(), + ); api.autoSizeColumn(colId); }} > From 6be3975bd1461585e206f6cb770ef5491a5e30e5 Mon Sep 17 00:00:00 2001 From: justinpark Date: Wed, 2 Oct 2024 10:00:33 -0700 Subject: [PATCH 13/20] bump to 32.2.1 and minor updates --- superset-frontend/package-lock.json | 26 ++++++++++++------- superset-frontend/package.json | 4 +-- .../src/components/GridTable/Header.test.tsx | 8 ------ .../src/components/GridTable/Header.tsx | 5 ++-- .../src/components/GridTable/index.tsx | 2 +- 5 files changed, 22 insertions(+), 23 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 3ee1a093fbdd..75f9f39d16e8 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -56,8 +56,8 @@ "@visx/xychart": "^3.5.1", "abortcontroller-polyfill": "^1.7.8", "ace-builds": "^1.36.3", - "ag-grid-community": "31.0.3", - "ag-grid-react": "31.0.3", + "ag-grid-community": "32.2.1", + "ag-grid-react": "32.2.1", "antd": "4.10.3", "antd-v5": "npm:antd@^5.18.0", "bootstrap": "^3.4.1", @@ -14480,17 +14480,25 @@ "node": ">= 10.0.0" } }, + "node_modules/ag-charts-types": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-10.2.0.tgz", + "integrity": "sha512-PUqH1QtugpYLnlbMdeSZVf5PpT1XZVsP69qN1JXhetLtQpVC28zaj7ikwu9CMA9N9b+dBboA9QcjUQUJZVUokQ==" + }, "node_modules/ag-grid-community": { - "version": "31.0.3", - "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-31.0.3.tgz", - "integrity": "sha512-k81YmLaOQOab9BavYD+Pw2smZSl6TXOJqj9hRuf70XQl3EknOHCGcra7joJxZRJLMKE/HdR+u33TNyX4TCuWfg==" + "version": "32.2.1", + "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-32.2.1.tgz", + "integrity": "sha512-mrnm1DnLI9Wd408mMwP+6p7lbTC3FYgzNIUPygBvNh3SzZnbzTEUJF/BTKXi+MARWtG5S0IMUYy4hqBiLbobaQ==", + "dependencies": { + "ag-charts-types": "10.2.0" + } }, "node_modules/ag-grid-react": { - "version": "31.0.3", - "resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-31.0.3.tgz", - "integrity": "sha512-CE4Z5Rdb0H3MFBhmge9854AyDvI4DoG7ZoUJRFB1p2pu5ANVe6tv7cwEI/ugHemEXRRP6s5uvW+ndv0Q6x1/+Q==", + "version": "32.2.1", + "resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-32.2.1.tgz", + "integrity": "sha512-lojTKsT/ncRZ81mrDa7qkIhZePfYlLCHIiAL1WbzL1mNPrglaa7QQKkE6hhhuAXvAm2uUhK1OfkMPnrqsEFldA==", "dependencies": { - "ag-grid-community": "~31.0.3", + "ag-grid-community": "32.2.1", "prop-types": "^15.8.1" }, "peerDependencies": { diff --git a/superset-frontend/package.json b/superset-frontend/package.json index c2e64c8901e3..ca423f246555 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -123,8 +123,8 @@ "@visx/xychart": "^3.5.1", "abortcontroller-polyfill": "^1.7.8", "ace-builds": "^1.36.3", - "ag-grid-community": "31.0.3", - "ag-grid-react": "31.0.3", + "ag-grid-community": "32.2.1", + "ag-grid-react": "32.2.1", "antd": "4.10.3", "antd-v5": "npm:antd@^5.18.0", "bootstrap": "^3.4.1", diff --git a/superset-frontend/src/components/GridTable/Header.test.tsx b/superset-frontend/src/components/GridTable/Header.test.tsx index c1154e7476c3..e0c31a12a0f1 100644 --- a/superset-frontend/src/components/GridTable/Header.test.tsx +++ b/superset-frontend/src/components/GridTable/Header.test.tsx @@ -83,14 +83,6 @@ test('synchronizes the current sort when sortChanged event occured', async () => expect(sortAsc).toBeInTheDocument(); }); -test('disable menu when enableMenu is false', () => { - const { queryByText, queryByTestId } = render( -
, - ); - expect(queryByText(mockedProps.displayName)).toBeTruthy(); - expect(queryByTestId('mock-dropdown')).toBeFalsy(); -}); - test('hide display name for PIVOT_COL_ID', () => { const { queryByText } = render(
= ({ - enableMenu, enableSorting, displayName, setSort, @@ -174,7 +173,7 @@ const Header: React.FC = ({ )} )} - {enableMenu && colId && api && ( + {colId && api && ( ({ -webkit-font-smoothing: antialiased; } .locked-col { - background: var(--ag-header-background-color); + background: var(--ag-row-border-color); padding: 0; text-align: center; font-size: calc(var(--ag-font-size) * 0.9); From 5168c43fa3b096aed1c753668803da27d7515140 Mon Sep 17 00:00:00 2001 From: justinpark Date: Wed, 2 Oct 2024 14:26:44 -0700 Subject: [PATCH 14/20] update types for 32.2 --- .../src/components/GridTable/HeaderMenu.tsx | 14 +++----------- .../src/components/GridTable/index.tsx | 12 ++++++------ 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/superset-frontend/src/components/GridTable/HeaderMenu.tsx b/superset-frontend/src/components/GridTable/HeaderMenu.tsx index 38c6ab74171d..1b1ba58bc2de 100644 --- a/superset-frontend/src/components/GridTable/HeaderMenu.tsx +++ b/superset-frontend/src/components/GridTable/HeaderMenu.tsx @@ -83,7 +83,7 @@ const HeaderMenu: React.FC = ({ { - api.setColumnVisible(c.getColId(), true); + api.setColumnsVisible([c.getColId()], true); }} > {c.getColDef().headerName} @@ -222,15 +222,7 @@ const HeaderMenu: React.FC = ({ { - const column = api.getColumn(colId); - column?.setColDef( - { - ...column?.getColDef(), - minWidth: undefined, - }, - column?.getColDef(), - ); - api.autoSizeColumn(colId); + api.autoSizeColumns([colId]); }} > @@ -238,7 +230,7 @@ const HeaderMenu: React.FC = ({ { - api.setColumnVisible(colId, false); + api.setColumnsVisible([colId], false); }} disabled={api.getColumns()?.length === invisibleColumns.length + 1} > diff --git a/superset-frontend/src/components/GridTable/index.tsx b/superset-frontend/src/components/GridTable/index.tsx index 18e8131f948a..1311148adc82 100644 --- a/superset-frontend/src/components/GridTable/index.tsx +++ b/superset-frontend/src/components/GridTable/index.tsx @@ -21,7 +21,7 @@ import { Global } from '@emotion/react'; import { css, useTheme } from '@superset-ui/core'; import type { Column } from 'ag-grid-community'; -import { AgGridReact, AgReactUiProps } from 'ag-grid-react'; +import { AgGridReact, type AgGridReactProps } from 'ag-grid-react'; import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-quartz.css'; @@ -61,7 +61,7 @@ export interface TableProps { size?: GridSize; - externalFilter?: AgReactUiProps['doesExternalFilterPass']; + externalFilter?: AgGridReactProps['doesExternalFilterPass']; height: number; @@ -78,7 +78,7 @@ export interface TableProps { striped?: boolean; } -const onSortChanged: AgReactUiProps['onSortChanged'] = ({ api }) => +const onSortChanged: AgGridReactProps['onSortChanged'] = ({ api }) => api.refreshCells(); function GridTable({ @@ -99,7 +99,7 @@ function GridTable({ [externalFilter], ); const rowIndexLength = `${data.length}}`.length; - const onKeyDown: AgReactUiProps>['onCellKeyDown'] = + const onKeyDown: AgGridReactProps>['onCellKeyDown'] = useCallback(({ event, column, data, value, api }) => { if ( !document.getSelection?.()?.toString?.() && @@ -152,14 +152,14 @@ function GridTable({ ...(index === columns.length - 1 && { flex: 1, width, - minWidth: 200, + minWidth: 150, }), }), ), ].slice(showRowNumber ? 0 : 1), [rowIndexLength, columnReorderable, columns, showRowNumber, sortable], ); - const defaultColDef: AgReactUiProps['defaultColDef'] = { + const defaultColDef: AgGridReactProps['defaultColDef'] = { ...(!columnReorderable && { suppressMovable: true }), resizable: true, sortable, From 7ddc6e709f59aed79fdef53529bfbe8ec8453d88 Mon Sep 17 00:00:00 2001 From: justinpark Date: Wed, 2 Oct 2024 14:51:50 -0700 Subject: [PATCH 15/20] set enableFilterButton --- .../src/components/GridTable/Header.test.tsx | 8 ++++++++ superset-frontend/src/components/GridTable/Header.tsx | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/superset-frontend/src/components/GridTable/Header.test.tsx b/superset-frontend/src/components/GridTable/Header.test.tsx index e0c31a12a0f1..7b085b00bc86 100644 --- a/superset-frontend/src/components/GridTable/Header.test.tsx +++ b/superset-frontend/src/components/GridTable/Header.test.tsx @@ -83,6 +83,14 @@ test('synchronizes the current sort when sortChanged event occured', async () => expect(sortAsc).toBeInTheDocument(); }); +test('disable menu when enableFilterButton is false', () => { + const { queryByText, queryByTestId } = render( +
, + ); + expect(queryByText(mockedProps.displayName)).toBeTruthy(); + expect(queryByTestId('mock-dropdown')).toBeFalsy(); +}); + test('hide display name for PIVOT_COL_ID', () => { const { queryByText } = render(
= ({ + enableFilterButton, enableSorting, displayName, setSort, @@ -173,7 +174,7 @@ const Header: React.FC = ({ )} )} - {colId && api && ( + {enableFilterButton && colId && api && ( Date: Fri, 4 Oct 2024 10:16:02 -0700 Subject: [PATCH 16/20] fix tests related to 32 ver upgrade --- .../components/GridTable/HeaderMenu.test.tsx | 36 ++++++++----------- .../src/components/GridTable/HeaderMenu.tsx | 2 +- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx b/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx index 3a187e4cf868..ddca71c03506 100644 --- a/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx +++ b/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx @@ -89,14 +89,10 @@ const mockInvisibleColumn3 = { getDataAsCsv: jest.fn().mockReturnValue('csv'), } as any as Column; -const mockSetColDef = jest.fn(); - const mockGridApi = { - autoSizeColumn: jest.fn(), + autoSizeColumns: jest.fn(), autoSizeAllColumns: jest.fn(), - setColumnPinned: jest.fn(), getColumn: jest.fn().mockReturnValue({ - setColDef: mockSetColDef, getColDef: jest.fn().mockReturnValue({}), }), getColumns: jest.fn().mockReturnValue([]), @@ -117,13 +113,12 @@ const mockedProps = { }; afterEach(() => { - mockSetColDef.mockClear(); (mockGridApi.getDataAsCsv as jest.Mock).mockClear(); - (mockGridApi.setColumnPinned as jest.Mock).mockClear(); - (mockGridApi.setColumnVisible as jest.Mock).mockClear(); + (mockGridApi.setColumnsPinned as jest.Mock).mockClear(); + (mockGridApi.setColumnsVisible as jest.Mock).mockClear(); (mockGridApi.setColumnsVisible as jest.Mock).mockClear(); (mockGridApi.setColumnsPinned as jest.Mock).mockClear(); - (mockGridApi.autoSizeColumn as jest.Mock).mockClear(); + (mockGridApi.autoSizeColumns as jest.Mock).mockClear(); (mockGridApi.autoSizeAllColumns as jest.Mock).mockClear(); (mockGridApi.moveColumns as jest.Mock).mockClear(); }); @@ -145,14 +140,14 @@ test('renders buttons pinning both side', () => { expect(queryByText('Pin Left')).toBeTruthy(); expect(queryByText('Pin Right')).toBeTruthy(); fireEvent.click(getByText('Pin Left')); - expect(mockGridApi.setColumnPinned).toHaveBeenCalledTimes(1); - expect(mockGridApi.setColumnPinned).toHaveBeenCalledWith( - mockedProps.colId, + expect(mockGridApi.setColumnsPinned).toHaveBeenCalledTimes(1); + expect(mockGridApi.setColumnsPinned).toHaveBeenCalledWith( + [mockedProps.colId], 'left', ); fireEvent.click(getByText('Pin Right')); - expect(mockGridApi.setColumnPinned).toHaveBeenLastCalledWith( - mockedProps.colId, + expect(mockGridApi.setColumnsPinned).toHaveBeenLastCalledWith( + [mockedProps.colId], 'right', ); }); @@ -164,9 +159,9 @@ test('renders unpin on pinned left', () => { expect(queryByText('Pin Left')).toBeFalsy(); expect(queryByText('Unpin')).toBeTruthy(); fireEvent.click(getByText('Unpin')); - expect(mockGridApi.setColumnPinned).toHaveBeenCalledTimes(1); - expect(mockGridApi.setColumnPinned).toHaveBeenCalledWith( - mockedProps.colId, + expect(mockGridApi.setColumnsPinned).toHaveBeenCalledTimes(1); + expect(mockGridApi.setColumnsPinned).toHaveBeenCalledWith( + [mockedProps.colId], null, ); }); @@ -181,9 +176,8 @@ test('renders autosize column', async () => { const { getByText } = render(); fireEvent.click(getByText('Autosize Column')); await waitFor(() => - expect(mockGridApi.autoSizeColumn).toHaveBeenCalledTimes(1), + expect(mockGridApi.autoSizeColumns).toHaveBeenCalledTimes(1), ); - expect(mockSetColDef).toHaveBeenCalledTimes(1); }); test('renders unhide when invisible column exists', async () => { @@ -193,8 +187,8 @@ test('renders unhide when invisible column exists', async () => { expect(queryByText('Unhide')).toBeTruthy(); const unhideColumnsButton = await screen.findByText('column2'); fireEvent.click(unhideColumnsButton); - expect(mockGridApi.setColumnVisible).toHaveBeenCalledTimes(1); - expect(mockGridApi.setColumnVisible).toHaveBeenCalledWith('column2', true); + expect(mockGridApi.setColumnsVisible).toHaveBeenCalledTimes(1); + expect(mockGridApi.setColumnsVisible).toHaveBeenCalledWith(['column2'], true); }); describe('for main menu', () => { diff --git a/superset-frontend/src/components/GridTable/HeaderMenu.tsx b/superset-frontend/src/components/GridTable/HeaderMenu.tsx index 1b1ba58bc2de..648c5e213872 100644 --- a/superset-frontend/src/components/GridTable/HeaderMenu.tsx +++ b/superset-frontend/src/components/GridTable/HeaderMenu.tsx @@ -56,7 +56,7 @@ const HeaderMenu: React.FC = ({ }: Params) => { const pinColumn = useCallback( (pinLoc: ColumnPinnedType) => { - api.setColumnPinned(colId, pinLoc); + api.setColumnsPinned([colId], pinLoc); }, [api, colId], ); From c7826492c6b7107981af573e46738bd768f2d549 Mon Sep 17 00:00:00 2001 From: justinpark Date: Wed, 15 Jan 2025 09:52:04 -0800 Subject: [PATCH 17/20] lint --- .../src/components/GridTable/GridTable.test.tsx | 2 +- .../src/components/GridTable/Header.test.tsx | 6 +++--- .../src/components/GridTable/HeaderMenu.test.tsx | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/superset-frontend/src/components/GridTable/GridTable.test.tsx b/superset-frontend/src/components/GridTable/GridTable.test.tsx index 8d38283ee405..1f603c75f335 100644 --- a/superset-frontend/src/components/GridTable/GridTable.test.tsx +++ b/superset-frontend/src/components/GridTable/GridTable.test.tsx @@ -43,7 +43,7 @@ const mockedProps = { test('renders a grid with 3 Table rows', () => { const { queryByText } = render(); mockedProps.data.forEach(({ b: columnBContent }) => { - expect(queryByText(columnBContent)).toBeTruthy(); + expect(queryByText(columnBContent)).toBeInTheDocument(); }); }); diff --git a/superset-frontend/src/components/GridTable/Header.test.tsx b/superset-frontend/src/components/GridTable/Header.test.tsx index 7b085b00bc86..7657dbe7b4a7 100644 --- a/superset-frontend/src/components/GridTable/Header.test.tsx +++ b/superset-frontend/src/components/GridTable/Header.test.tsx @@ -57,7 +57,7 @@ const mockedProps = { test('renders display name for the column', () => { const { queryByText } = render(
); - expect(queryByText(mockedProps.displayName)).toBeTruthy(); + expect(queryByText(mockedProps.displayName)).toBeInTheDocument(); }); test('sorts by clicking a column header', () => { @@ -87,8 +87,8 @@ test('disable menu when enableFilterButton is false', () => { const { queryByText, queryByTestId } = render(
, ); - expect(queryByText(mockedProps.displayName)).toBeTruthy(); - expect(queryByTestId('mock-dropdown')).toBeFalsy(); + expect(queryByText(mockedProps.displayName)).toBeInTheDocument(); + expect(queryByTestId('mock-dropdown')).not.toBeInTheDocument(); }); test('hide display name for PIVOT_COL_ID', () => { diff --git a/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx b/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx index ddca71c03506..d84551f4c27f 100644 --- a/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx +++ b/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx @@ -137,8 +137,8 @@ test('renders copy data', async () => { test('renders buttons pinning both side', () => { const { queryByText, getByText } = render(); - expect(queryByText('Pin Left')).toBeTruthy(); - expect(queryByText('Pin Right')).toBeTruthy(); + expect(queryByText('Pin Left')).toBeInTheDocument(); + expect(queryByText('Pin Right')).toBeInTheDocument(); fireEvent.click(getByText('Pin Left')); expect(mockGridApi.setColumnsPinned).toHaveBeenCalledTimes(1); expect(mockGridApi.setColumnsPinned).toHaveBeenCalledWith( @@ -156,8 +156,8 @@ test('renders unpin on pinned left', () => { const { queryByText, getByText } = render( , ); - expect(queryByText('Pin Left')).toBeFalsy(); - expect(queryByText('Unpin')).toBeTruthy(); + expect(queryByText('Pin Left')).not.toBeInTheDocument(); + expect(queryByText('Unpin')).toBeInTheDocument(); fireEvent.click(getByText('Unpin')); expect(mockGridApi.setColumnsPinned).toHaveBeenCalledTimes(1); expect(mockGridApi.setColumnsPinned).toHaveBeenCalledWith( @@ -168,8 +168,8 @@ test('renders unpin on pinned left', () => { test('renders unpin on pinned right', () => { const { queryByText } = render(); - expect(queryByText('Pin Right')).toBeFalsy(); - expect(queryByText('Unpin')).toBeTruthy(); + expect(queryByText('Pin Right')).not.toBeInTheDocument(); + expect(queryByText('Unpin')).toBeInTheDocument(); }); test('renders autosize column', async () => { @@ -184,7 +184,7 @@ test('renders unhide when invisible column exists', async () => { const { queryByText } = render( , ); - expect(queryByText('Unhide')).toBeTruthy(); + expect(queryByText('Unhide')).toBeInTheDocument(); const unhideColumnsButton = await screen.findByText('column2'); fireEvent.click(unhideColumnsButton); expect(mockGridApi.setColumnsVisible).toHaveBeenCalledTimes(1); From 65911eca39d4adad9e1852182a1cea3173b13a58 Mon Sep 17 00:00:00 2001 From: justinpark Date: Wed, 22 Jan 2025 13:06:17 -0800 Subject: [PATCH 18/20] spell --- superset-frontend/src/components/GridTable/HeaderMenu.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx b/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx index d84551f4c27f..691cddb4aa74 100644 --- a/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx +++ b/superset-frontend/src/components/GridTable/HeaderMenu.test.tsx @@ -135,7 +135,7 @@ test('renders copy data', async () => { }); }); -test('renders buttons pinning both side', () => { +test('renders buttons pinning both sides', () => { const { queryByText, getByText } = render(); expect(queryByText('Pin Left')).toBeInTheDocument(); expect(queryByText('Pin Right')).toBeInTheDocument(); From fd0df787d3359854e120785d9954900f8b6b3e7f Mon Sep 17 00:00:00 2001 From: justinpark Date: Wed, 22 Jan 2025 15:57:03 -0800 Subject: [PATCH 19/20] update data access for font-src --- superset/config.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/superset/config.py b/superset/config.py index ae4561f1ce9d..2633024aac3c 100644 --- a/superset/config.py +++ b/superset/config.py @@ -1603,7 +1603,7 @@ def EMAIL_HEADER_MUTATOR( # pylint: disable=invalid-name,unused-argument # noq TALISMAN_CONFIG = { "content_security_policy": { "base-uri": ["'self'"], - "default-src": ["'self'", "data:"], + "default-src": ["'self'"], "img-src": [ "'self'", "blob:", @@ -1624,6 +1624,10 @@ def EMAIL_HEADER_MUTATOR( # pylint: disable=invalid-name,unused-argument # noq "'self'", "'unsafe-inline'", ], + "font-src": [ + "'self'", + "data:" + ], "script-src": ["'self'", "'strict-dynamic'"], }, "content_security_policy_nonce_in": ["script-src"], @@ -1634,7 +1638,7 @@ def EMAIL_HEADER_MUTATOR( # pylint: disable=invalid-name,unused-argument # noq TALISMAN_DEV_CONFIG = { "content_security_policy": { "base-uri": ["'self'"], - "default-src": ["'self'", "data:"], + "default-src": ["'self'"], "img-src": [ "'self'", "blob:", @@ -1655,6 +1659,10 @@ def EMAIL_HEADER_MUTATOR( # pylint: disable=invalid-name,unused-argument # noq "'self'", "'unsafe-inline'", ], + "font-src": [ + "'self'", + "data:" + ], "script-src": ["'self'", "'unsafe-inline'", "'unsafe-eval'"], }, "content_security_policy_nonce_in": ["script-src"], From de95ae8934fc7b88f205da9992c5d580a6e625d1 Mon Sep 17 00:00:00 2001 From: justinpark Date: Mon, 27 Jan 2025 16:42:35 -0800 Subject: [PATCH 20/20] remove default font-style override --- superset/config.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/superset/config.py b/superset/config.py index 2633024aac3c..8554b2a26451 100644 --- a/superset/config.py +++ b/superset/config.py @@ -1624,10 +1624,6 @@ def EMAIL_HEADER_MUTATOR( # pylint: disable=invalid-name,unused-argument # noq "'self'", "'unsafe-inline'", ], - "font-src": [ - "'self'", - "data:" - ], "script-src": ["'self'", "'strict-dynamic'"], }, "content_security_policy_nonce_in": ["script-src"], @@ -1659,10 +1655,6 @@ def EMAIL_HEADER_MUTATOR( # pylint: disable=invalid-name,unused-argument # noq "'self'", "'unsafe-inline'", ], - "font-src": [ - "'self'", - "data:" - ], "script-src": ["'self'", "'unsafe-inline'", "'unsafe-eval'"], }, "content_security_policy_nonce_in": ["script-src"],