diff --git a/client/cypress/integration/visualizations/table/.mocks/wide-dataset.js b/client/cypress/integration/visualizations/table/.mocks/wide-dataset.js new file mode 100644 index 0000000000..e72384ec53 --- /dev/null +++ b/client/cypress/integration/visualizations/table/.mocks/wide-dataset.js @@ -0,0 +1,33 @@ +const loremIpsum = +"Lorem ipsum dolor sit amet consectetur adipiscing elit" + +"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"; + +export const query = ` + SELECT '${loremIpsum}' AS a, '${loremIpsum}' AS b, '${loremIpsum}' AS c, '${loremIpsum}' AS d, '${loremIpsum}' as e +`; + +export const config = { + itemsPerPage: 10, + columns: [ + { + name: "a", + displayAs: "string", + }, + { + name: "b", + displayAs: "string", + }, + { + name: "c", + displayAs: "string", + }, + { + name: "d", + displayAs: "string", + }, + { + name: "e", + displayAs: "string", + } + ] +} \ No newline at end of file diff --git a/client/cypress/integration/visualizations/table/table_spec.js b/client/cypress/integration/visualizations/table/table_spec.js index b0fa8ec61c..2d499633d4 100644 --- a/client/cypress/integration/visualizations/table/table_spec.js +++ b/client/cypress/integration/visualizations/table/table_spec.js @@ -8,6 +8,7 @@ import * as AllCellTypes from "./.mocks/all-cell-types"; import * as MultiColumnSort from "./.mocks/multi-column-sort"; import * as SearchInData from "./.mocks/search-in-data"; import * as LargeDataset from "./.mocks/large-dataset"; +import * as WideDataSet from "./.mocks/wide-dataset"; function prepareVisualization(query, type, name, options) { return cy @@ -98,6 +99,50 @@ describe("Table", () => { }); }); + describe("Fixing columns", () => { + it("fixes the correct number of columns", () => { + const { query, config } = WideDataSet; + prepareVisualization(query, "TABLE", "All cell types", config); + cy.getByTestId("EditVisualization").click(); + cy.contains("span", "Grid").click(); + cy.getByTestId("FixedColumns").click(); + cy.contains(".ant-select-item-option-content", "1").click(); + cy.contains("Save").click(); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(500); //add some waiting to make sure table visualization is saved + + cy.get(".ant-table-thead") + .find("th.ant-table-cell-fix-left") + .then(fixedCols => { + expect(fixedCols.length).to.equal(1); + }); + + cy.get(".ant-table-content").scrollTo("right", { duration: 1000 }); + cy.get(".ant-table-content").scrollTo("left", { duration: 1000 }); + }); + + it("doesn't let user fix too many columns", () => { + const { query, config } = MultiColumnSort; + prepareVisualization(query, "TABLE", "Test data", config); + cy.getByTestId("EditVisualization").click(); + cy.contains("span", "Grid").click(); + cy.getByTestId("FixedColumns").click(); + cy.get(".ant-select-item-option-content"); + cy.contains(".ant-select-item-option-content", "3").should("not.exist"); + cy.contains(".ant-select-item-option-content", "4").should("not.exist"); + }); + + it("doesn't cause issues when freezing column off of page", () => { + const { query, config } = WideDataSet; + prepareVisualization(query, "TABLE", "Test data", config); + cy.getByTestId("EditVisualization").click(); + cy.contains("span", "Grid").click(); + cy.getByTestId("FixedColumns").click(); + cy.contains(".ant-select-item-option-content", "4").click(); + cy.contains("Save").click(); + }); + }); + it("searches in multiple columns", () => { const { query, config } = SearchInData; prepareVisualization(query, "TABLE", "Search", config).then(({ visualizationId }) => { diff --git a/client/cypress/support/visualizations/table.js b/client/cypress/support/visualizations/table.js index 2095b9fb88..7b290a1250 100644 --- a/client/cypress/support/visualizations/table.js +++ b/client/cypress/support/visualizations/table.js @@ -1,12 +1,12 @@ export function expectTableToHaveLength(length) { cy.getByTestId("TableVisualization") - .find("tbody tr") + .find("tbody tr.ant-table-row") .should("have.length", length); } export function expectFirstColumnToHaveMembers(values) { cy.getByTestId("TableVisualization") - .find("tbody tr td:first-child") + .find("tbody tr.ant-table-row td:first-child") .then($cell => Cypress.$.map($cell, item => Cypress.$(item).text())) .then(firstColumnCells => expect(firstColumnCells).to.have.members(values)); } diff --git a/viz-lib/src/visualizations/table/Editor/GridSettings.tsx b/viz-lib/src/visualizations/table/Editor/GridSettings.tsx index 73aa57ac21..dc0688b135 100644 --- a/viz-lib/src/visualizations/table/Editor/GridSettings.tsx +++ b/viz-lib/src/visualizations/table/Editor/GridSettings.tsx @@ -1,28 +1,51 @@ import { map } from "lodash"; -import React from "react"; +import React, { useState } from "react"; import { Section, Select } from "@/components/visualizations/editor"; import { EditorPropTypes } from "@/visualizations/prop-types"; const ALLOWED_ITEM_PER_PAGE = [5, 10, 15, 20, 25, 50, 100, 150, 200, 250, 500]; +const ALLOWED_COLS_TO_FIX = [0, 1, 2, 3, 4] + export default function GridSettings({ options, onOptionsChange }: any) { + const numCols = options.columns.length; + const maxColsToFix = Math.min(4, numCols - 1); + return ( - // @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message -
- -
+ + {/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never' but its value is 'Element'. */} +
+ +
+ {/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never' but its value is 'Element'. */} +
+ +
+
); } diff --git a/viz-lib/src/visualizations/table/Editor/__snapshots__/ColumnsSettings.test.tsx.snap b/viz-lib/src/visualizations/table/Editor/__snapshots__/ColumnsSettings.test.tsx.snap index 669b2e57e0..b2175a0c4d 100644 --- a/viz-lib/src/visualizations/table/Editor/__snapshots__/ColumnsSettings.test.tsx.snap +++ b/viz-lib/src/visualizations/table/Editor/__snapshots__/ColumnsSettings.test.tsx.snap @@ -13,6 +13,7 @@ Object { ], "dateTimeFormat": undefined, "displayAs": "string", + "fixed": false, "highlightLinks": false, "imageHeight": "", "imageTitleTemplate": "{{ @ }}", @@ -46,6 +47,7 @@ Object { ], "dateTimeFormat": undefined, "displayAs": "number", + "fixed": false, "highlightLinks": false, "imageHeight": "", "imageTitleTemplate": "{{ @ }}", @@ -79,6 +81,7 @@ Object { ], "dateTimeFormat": undefined, "displayAs": "string", + "fixed": false, "highlightLinks": false, "imageHeight": "", "imageTitleTemplate": "{{ @ }}", @@ -112,6 +115,7 @@ Object { ], "dateTimeFormat": undefined, "displayAs": "string", + "fixed": false, "highlightLinks": false, "imageHeight": "", "imageTitleTemplate": "{{ @ }}", @@ -145,6 +149,7 @@ Object { ], "dateTimeFormat": undefined, "displayAs": "string", + "fixed": false, "highlightLinks": false, "imageHeight": "", "imageTitleTemplate": "{{ @ }}", diff --git a/viz-lib/src/visualizations/table/Renderer.tsx b/viz-lib/src/visualizations/table/Renderer.tsx index 3812e82b82..6dbb57abd5 100644 --- a/viz-lib/src/visualizations/table/Renderer.tsx +++ b/viz-lib/src/visualizations/table/Renderer.tsx @@ -84,6 +84,13 @@ export default function Renderer({ options, data }: any) { const [searchTerm, setSearchTerm] = useState(""); const [orderBy, setOrderBy] = useState([]); + const columnsToFix = new Set(); + for (let i = 0; i < options.fixedColumns; i++) { + if (options.columns[i]) { + columnsToFix.add(options.columns[i].name); + } + } + const searchColumns = useMemo(() => filter(options.columns, "allowSearch"), [options.columns]); const tableColumns = useMemo(() => { @@ -97,7 +104,7 @@ export default function Renderer({ options, data }: any) { // Remove text selection - may occur accidentally // @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'. document.getSelection().removeAllRanges(); - }); + }, columnsToFix); }, [options.columns, searchColumns, orderBy]); const preparedRows = useMemo(() => sortRows(filterRows(initRows(data.rows), searchTerm, searchColumns), orderBy), [ @@ -134,6 +141,7 @@ export default function Renderer({ options, data }: any) { showSizeChanger: false, }} showSorterTooltip={false} + scroll = {{x : 'max-content'}} /> ); diff --git a/viz-lib/src/visualizations/table/getOptions.ts b/viz-lib/src/visualizations/table/getOptions.ts index bcde21bbd6..2c4a090e39 100644 --- a/viz-lib/src/visualizations/table/getOptions.ts +++ b/viz-lib/src/visualizations/table/getOptions.ts @@ -4,6 +4,7 @@ import { visualizationsSettings } from "@/visualizations/visualizationsSettings" const DEFAULT_OPTIONS = { itemsPerPage: 25, paginationSize: "default", // not editable through Editor + fixedColumns: 0, }; const filterTypes = ["filter", "multi-filter", "multiFilter"]; @@ -56,6 +57,7 @@ function getDefaultColumnsOptions(columns: any) { // `string` cell options allowHTML: true, highlightLinks: false, + fixed: false, })); } diff --git a/viz-lib/src/visualizations/table/renderer.less b/viz-lib/src/visualizations/table/renderer.less index 1c538037ab..16408381e1 100644 --- a/viz-lib/src/visualizations/table/renderer.less +++ b/viz-lib/src/visualizations/table/renderer.less @@ -21,7 +21,6 @@ left: 0; top: 0; border-top: 0; - z-index: 1; background: #fafafa !important; } } @@ -157,3 +156,11 @@ color: @text-color-secondary; } } + +.ant-table-cell-fix-left{ + background-color: #fff !important; +} + +.ant-table-tbody > tr.ant-table-row:hover > .ant-table-cell-fix-left { + background-color: rgb(248, 249, 250) !important; +} \ No newline at end of file diff --git a/viz-lib/src/visualizations/table/utils.tsx b/viz-lib/src/visualizations/table/utils.tsx index 298cdb7f96..64ceef1d30 100644 --- a/viz-lib/src/visualizations/table/utils.tsx +++ b/viz-lib/src/visualizations/table/utils.tsx @@ -50,7 +50,7 @@ function getOrderByInfo(orderBy: any) { return result; } -export function prepareColumns(columns: any, searchInput: any, orderBy: any, onOrderByChange: any) { +export function prepareColumns(columns: any, searchInput: any, orderBy: any, onOrderByChange: any, columnsToFix: Set) { columns = filter(columns, "visible"); columns = sortBy(columns, "order"); @@ -96,6 +96,7 @@ export function prepareColumns(columns: any, searchInput: any, orderBy: any, onO }), onClick: (event: any) => onOrderByChange(toggleOrderBy(column.name, orderBy, event.shiftKey)), }), + fixed: columnsToFix.has(column.name) ? 'left' : false }; // @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message