From 664db29bccdd0dc9ec5e50b119921bdb5eb807a8 Mon Sep 17 00:00:00 2001 From: Luca Restagno <59067245+lucarestagno@users.noreply.github.com> Date: Tue, 17 Jan 2023 20:08:39 +0100 Subject: [PATCH] Preserve filters and sorts while switching between tables (and load from URL) [GCU-47]: https://hasurahq.atlassian.net/browse/GCU-47?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7522 GitOrigin-RevId: 92ade94f2fbd6013986b28b5a97ba5683a4fdd2e --- .../BrowseRows/BrowseRows.stories.tsx | 3 + .../src/features/BrowseRows/BrowseRows.tsx | 109 +++-- .../features/BrowseRows/BrowseRows.utils.ts | 18 + .../BrowseRowsContainer.tsx | 9 + .../hooks/useInitialWhereAndOrderBy.ts | 130 ++++++ .../components/DataGrid/DataGrid.tsx | 22 +- .../DataGrid/DataGrid.utils.test.ts | 406 +++++++++++++++++- .../components/DataGrid/DataGrid.utils.ts | 167 ++++++- .../DataGrid/parts/DataTableOptions.tsx | 7 +- .../DataGrid/parts/RowOptionsButton.tsx | 4 +- .../src/features/BrowseRows/hooks/useRows.tsx | 4 - 11 files changed, 829 insertions(+), 50 deletions(-) create mode 100644 console/src/features/BrowseRows/BrowseRows.utils.ts create mode 100644 console/src/features/BrowseRows/components/BrowseRowsContainer/hooks/useInitialWhereAndOrderBy.ts diff --git a/console/src/features/BrowseRows/BrowseRows.stories.tsx b/console/src/features/BrowseRows/BrowseRows.stories.tsx index 43fc6c3b3bf..6468d33861a 100644 --- a/console/src/features/BrowseRows/BrowseRows.stories.tsx +++ b/console/src/features/BrowseRows/BrowseRows.stories.tsx @@ -3,6 +3,7 @@ import { ReactQueryDecorator } from '@/storybook/decorators/react-query'; import { ComponentStory, ComponentMeta } from '@storybook/react'; import { userEvent, waitFor, within } from '@storybook/testing-library'; import { expect } from '@storybook/jest'; +import { action } from '@storybook/addon-actions'; import { BrowseRows } from './BrowseRows'; import { handlers } from './__mocks__/handlers.mock'; @@ -20,6 +21,7 @@ export const Basic: ComponentStory = () => { table={['Album']} dataSourceName="sqlite_test" primaryKeys={[]} + onUpdateOptions={action('onUpdateOptions')} /> ); }; @@ -30,6 +32,7 @@ export const BasicDisplayTest: ComponentStory = () => { table={['Album']} dataSourceName="sqlite_test" primaryKeys={[]} + onUpdateOptions={action('onUpdateOptions')} /> ); }; diff --git a/console/src/features/BrowseRows/BrowseRows.tsx b/console/src/features/BrowseRows/BrowseRows.tsx index 31566c14f45..d09b60727a7 100644 --- a/console/src/features/BrowseRows/BrowseRows.tsx +++ b/console/src/features/BrowseRows/BrowseRows.tsx @@ -1,7 +1,8 @@ import { getTableDisplayName } from '@/features/DatabaseRelationships'; import { Table } from '@/features/hasura-metadata-types'; import produce from 'immer'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { setWhereAndSortToUrl } from './BrowseRows.utils'; import { DataGrid, DataGridOptions, @@ -9,12 +10,13 @@ import { } from './components/DataGrid/DataGrid'; import { TableTabView } from './components/DataGrid/parts/TableTabView'; -interface BrowseRowsProps { +type BrowseRowsProps = { dataSourceName: string; table: Table; options?: DataGridOptions; primaryKeys: string[]; -} + onUpdateOptions: (options: DataGridOptions) => void; +}; type TabState = { name: string; details: DataGridProps; parentValue: string }; type OpenNewRelationshipTabProps = { @@ -94,9 +96,13 @@ const onTabClose = ( } }; -export const BrowseRows = (props: BrowseRowsProps) => { - const { dataSourceName, table, options, primaryKeys } = props; - +export const BrowseRows = ({ + dataSourceName, + table, + options, + primaryKeys, + onUpdateOptions, +}: BrowseRowsProps) => { const defaultTabState = { name: getTableDisplayName(table), details: { dataSourceName, table, options, primaryKeys: [] as string[] }, @@ -117,6 +123,21 @@ export const BrowseRows = (props: BrowseRowsProps) => { const defaultActiveTab = getTableDisplayName(table); const [activeTab, setActiveTab] = useState(defaultActiveTab); + useEffect(() => { + setOriginalTableOptions(options); + const currentTab = openTabs[0]; + + setOpenTabs([ + { + ...currentTab, + details: { + ...currentTab.details, + options, + }, + }, + ]); + }, [options]); + /** * when relationships are open, disable sorting and searching through all the views */ @@ -150,43 +171,51 @@ export const BrowseRows = (props: BrowseRowsProps) => { setActiveTab(`${openTab.name}.${relationshipName}`); }; + const onUpdateOptionsGenerator = + (index: number) => + (_options: DataGridOptions): void => { + setWhereAndSortToUrl(_options); + + setOriginalTableOptions(_options); + + setOpenTabs(_openTabs => + produce(_openTabs, draft => { + draft[index].details.options = _options; + }) + ); + + onUpdateOptions(_options); + }; + + const tableTabItems = openTabs.map((openTab, index) => { + const innerOnUpdateOptions = onUpdateOptionsGenerator(index); + return { + value: openTab.name, + label: openTab.name, + parentValue: openTab.parentValue, + content: ( + openNewRelationshipTab({ data, openTab })} + onRelationshipClose={relationshipName => + setActiveTab(`${openTab.name}.${relationshipName}`) + } + disableRunQuery={disableRunQuery} + updateOptions={innerOnUpdateOptions} + primaryKeys={primaryKeys} + /> + ), + }; + }); + return (
({ - value: openTab.name, - label: openTab.name, - parentValue: openTab.parentValue, - content: ( - - openNewRelationshipTab({ data, openTab }) - } - onRelationshipClose={relationshipName => - setActiveTab(`${openTab.name}.${relationshipName}`) - } - disableRunQuery={disableRunQuery} - updateOptions={_options => { - if (index === 0) { - // Save a copy of the parent filters before opening - setOriginalTableOptions(_options); - } - - setOpenTabs(_openTabs => - produce(_openTabs, draft => { - draft[index].details.options = _options; - }) - ); - }} - primaryKeys={primaryKeys} - /> - ), - }))} + items={tableTabItems} activeTab={activeTab} onTabClick={value => { setActiveTab(value); diff --git a/console/src/features/BrowseRows/BrowseRows.utils.ts b/console/src/features/BrowseRows/BrowseRows.utils.ts new file mode 100644 index 00000000000..de6d5dd98d2 --- /dev/null +++ b/console/src/features/BrowseRows/BrowseRows.utils.ts @@ -0,0 +1,18 @@ +import { DataGridOptions } from './components/DataGrid/DataGrid'; +import { applyWhereAndSortConditionsToQueryString } from './components/DataGrid/DataGrid.utils'; + +export const setWhereAndSortToUrl = (options: DataGridOptions) => { + const searchQueryString = applyWhereAndSortConditionsToQueryString({ + options, + search: window.location.search, + }); + + if (window.history.pushState) { + const { + location: { protocol, host, pathname }, + } = window; + + const newUrl = `${protocol}//${host}${pathname}?${searchQueryString}`; + window.history.pushState({ path: newUrl }, '', newUrl); + } +}; diff --git a/console/src/features/BrowseRows/components/BrowseRowsContainer/BrowseRowsContainer.tsx b/console/src/features/BrowseRows/components/BrowseRowsContainer/BrowseRowsContainer.tsx index 563762e239f..a345dc5b138 100644 --- a/console/src/features/BrowseRows/components/BrowseRowsContainer/BrowseRowsContainer.tsx +++ b/console/src/features/BrowseRows/components/BrowseRowsContainer/BrowseRowsContainer.tsx @@ -2,6 +2,7 @@ import { Table } from '@/features/hasura-metadata-types'; import React from 'react'; import { BrowseRows } from '../../BrowseRows'; import { useTableColumns } from '../../hooks'; +import { useInitialWhereAndOrderBy } from './hooks/useInitialWhereAndOrderBy'; interface BrowseRowsContainerProps { table: Table; @@ -22,12 +23,20 @@ export const BrowseRowsContainer = ({ .map(column => column.graphQLProperties?.name) .filter(columnName => columnName !== undefined) as string[]; + const { options, onUpdateOptions } = useInitialWhereAndOrderBy({ + columns: tableColumns?.columns, + table, + dataSourceName, + }); + return (
); diff --git a/console/src/features/BrowseRows/components/BrowseRowsContainer/hooks/useInitialWhereAndOrderBy.ts b/console/src/features/BrowseRows/components/BrowseRowsContainer/hooks/useInitialWhereAndOrderBy.ts new file mode 100644 index 00000000000..49612808ce2 --- /dev/null +++ b/console/src/features/BrowseRows/components/BrowseRowsContainer/hooks/useInitialWhereAndOrderBy.ts @@ -0,0 +1,130 @@ +import { TableColumn } from '@/features/DataSource'; +import { Table } from '@/features/hasura-metadata-types'; +import { getLSItem, setLSItem } from '@/utils'; +import { useEffect, useState } from 'react'; +import { DataGridOptions } from '../../DataGrid/DataGrid'; +import { convertUrlToDataGridOptions } from '../../DataGrid/DataGrid.utils'; + +type GetUniqueTableKeyProps = { + table: Table; + dataSourceName: string; +}; + +const getUniqueTableKey = ({ + table, + dataSourceName, +}: GetUniqueTableKeyProps) => { + if (Array.isArray(table)) { + return `${dataSourceName}.${table.join('-')}.query`; + } + + return `${dataSourceName}.${table}.query`; +}; + +type WhereAndOrderBy = Pick; + +const getWhereAndOrderByFromLocalStorage = ({ + table, + dataSourceName, +}: GetUniqueTableKeyProps): WhereAndOrderBy | undefined => { + const localStorageKey = getUniqueTableKey({ table, dataSourceName }); + const localUserQueryString = localStorageKey + ? getLSItem(localStorageKey) + : ''; + + if (localUserQueryString) { + return JSON.parse(localUserQueryString) as WhereAndOrderBy; + } + + return undefined; +}; + +type SetWhereAndOrderByToLocalStorage = { + whereAndOrderBy: WhereAndOrderBy; +} & GetUniqueTableKeyProps; + +const setWhereAndOrderByToLocalStorage = ({ + table, + dataSourceName, + whereAndOrderBy, +}: SetWhereAndOrderByToLocalStorage) => { + const localStorageKey = getUniqueTableKey({ table, dataSourceName }); + setLSItem(localStorageKey, JSON.stringify(whereAndOrderBy)); +}; + +type UseInitialWhereAndOrderByProps = { + columns: TableColumn[] | undefined; + table: Table; + dataSourceName: string; +}; + +export const useInitialWhereAndOrderBy = ({ + columns, + table, + dataSourceName, +}: UseInitialWhereAndOrderByProps) => { + const [options, setOptions] = useState( + undefined + ); + + const localStorageWhereAndOrderBy = getWhereAndOrderByFromLocalStorage({ + table, + dataSourceName, + }); + const [initialWhereAndOrderBy] = useState(localStorageWhereAndOrderBy); + + const [initialUrlSearchParams] = useState(window.location.search); + + useEffect(() => { + if (columns) { + if (initialUrlSearchParams) { + const newOptions = convertUrlToDataGridOptions( + initialUrlSearchParams, + columns + ); + + const hasWhere = newOptions.where && newOptions.where?.length > 0; + const hasOrderBy = + newOptions.order_by && newOptions.order_by?.length > 0; + + if (hasWhere || hasOrderBy) { + setWhereAndOrderByToLocalStorage({ + table, + dataSourceName, + whereAndOrderBy: { + where: newOptions?.where || [], + order_by: newOptions?.order_by || [], + }, + }); + setOptions(newOptions); + return; + } + } + + if (initialWhereAndOrderBy) { + const newOptions: DataGridOptions = { + where: initialWhereAndOrderBy.where, + order_by: initialWhereAndOrderBy.order_by, + }; + setOptions(newOptions); + } + } + }, [columns]); + + const onUpdateOptions = (_options: DataGridOptions) => { + const whereAndOrderBy: WhereAndOrderBy = { + where: _options?.where || [], + order_by: _options?.order_by || [], + }; + setWhereAndOrderByToLocalStorage({ + table, + dataSourceName, + whereAndOrderBy, + }); + }; + + return { + options, + onUpdateOptions, + }; +}; diff --git a/console/src/features/BrowseRows/components/DataGrid/DataGrid.tsx b/console/src/features/BrowseRows/components/DataGrid/DataGrid.tsx index 61d71ba528f..38cc68c5fb6 100644 --- a/console/src/features/BrowseRows/components/DataGrid/DataGrid.tsx +++ b/console/src/features/BrowseRows/components/DataGrid/DataGrid.tsx @@ -107,6 +107,12 @@ export const DataGrid = (props: DataGridProps) => { setSorting(DEFAULT_SORT_CLAUSES); setWhereClauses(DEFAULT_WHERE_CLAUSES); setOrderClauses(DEFAULT_ORDER_BY_CLAUSES); + updateOptions?.({ + limit: pageSize, + offset: pageIndex * pageSize, + where: DEFAULT_WHERE_CLAUSES, + order_by: DEFAULT_ORDER_BY_CLAUSES, + }); }; /** @@ -309,10 +315,24 @@ export const DataGrid = (props: DataGridProps) => { whereClauses, supportedOperators: tableColumnQueryResult?.supportedOperators ?? [], removeWhereClause: id => { + const newWhereClauses = whereClauses.filter((_, i) => i !== id); setWhereClauses(whereClauses.filter((_, i) => i !== id)); + updateOptions?.({ + limit: pageSize, + offset: pageIndex * pageSize, + where: newWhereClauses, + order_by: orderByClauses, + }); }, removeOrderByClause: id => { - setOrderClauses(orderByClauses.filter((_, i) => i !== id)); + const newOrderByClauses = orderByClauses.filter((_, i) => i !== id); + setOrderClauses(newOrderByClauses); + updateOptions?.({ + limit: pageSize, + offset: pageIndex * pageSize, + where: whereClauses, + order_by: newOrderByClauses, + }); }, onExportRows, onExportSelectedRows, diff --git a/console/src/features/BrowseRows/components/DataGrid/DataGrid.utils.test.ts b/console/src/features/BrowseRows/components/DataGrid/DataGrid.utils.test.ts index f2cc2c1a51d..5950b615626 100644 --- a/console/src/features/BrowseRows/components/DataGrid/DataGrid.utils.test.ts +++ b/console/src/features/BrowseRows/components/DataGrid/DataGrid.utils.test.ts @@ -1,7 +1,18 @@ -import { TableRow, WhereClause } from '../../../../features/DataSource'; +import { + TableColumn, + TableRow, + WhereClause, +} from '../../../../features/DataSource'; +import { DataGridOptions } from './DataGrid'; import { adaptSelectedRowIdsToWhereClause, AdaptSelectedRowIdsToWhereClauseArgs, + mapWhereAndSortConditions, + FilterConditions, + replaceFiltersInUrl, + applyWhereAndSortConditionsToQueryString, + convertUrlToDataGridOptions, + convertValueToGraphQL, } from './DataGrid.utils'; describe('adaptSelectedRowIdsToWhereClause', () => { @@ -50,3 +61,396 @@ describe('adaptSelectedRowIdsToWhereClause', () => { ).toEqual(expected); }); }); + +describe('mapWhereAndSortConditions', () => { + describe('when where and sort conditions are defined', () => { + it('returns the query string', () => { + const options: DataGridOptions = { + limit: 0, + offset: 0, + where: [ + { + AlbumId: { + _gte: 2, + }, + }, + { + Title: { + _like: '%foo%', + }, + }, + ], + order_by: [ + { + column: 'AlbumId', + type: 'desc', + }, + { + column: 'Title', + type: 'asc', + }, + ], + }; + + expect(mapWhereAndSortConditions(options)).toEqual([ + { + filter: 'AlbumId;_gte;2', + }, + { + filter: 'Title;_like;%foo%', + }, + { + sort: 'AlbumId;desc', + }, + { + sort: 'Title;asc', + }, + ]); + }); + }); + + describe('when only where conditions are defined', () => { + it('returns the query string', () => { + const options: DataGridOptions = { + limit: 0, + offset: 0, + where: [ + { + AlbumId: { + _gte: 2, + }, + }, + { + Title: { + _like: '%foo%', + }, + }, + ], + }; + + expect(mapWhereAndSortConditions(options)).toEqual([ + { + filter: 'AlbumId;_gte;2', + }, + { + filter: 'Title;_like;%foo%', + }, + ]); + }); + }); + + describe('when only sort conditions are defined', () => { + it('returns the query string', () => { + const options: DataGridOptions = { + limit: 0, + offset: 0, + order_by: [ + { + column: 'AlbumId', + type: 'desc', + }, + { + column: 'Title', + type: 'asc', + }, + ], + }; + + expect(mapWhereAndSortConditions(options)).toEqual([ + { + sort: 'AlbumId;desc', + }, + { + sort: 'Title;asc', + }, + ]); + }); + }); +}); + +describe('replaceFiltersInUrl', () => { + describe('when filter and sort conditions are provided', () => { + it('returns the query string', () => { + const filterConditions: FilterConditions = [ + { filter: 'AlbumId;_gte;1' }, + { filter: 'Title;_like;%foo%' }, + { sort: 'AlbumId;asc' }, + ]; + + expect( + replaceFiltersInUrl('?database=Chinook&table=Album', filterConditions) + ).toBe( + 'database=Chinook&table=Album&filter=AlbumId%3B_gte%3B1&filter=Title%3B_like%3B%25foo%25&sort=AlbumId%3Basc' + ); + }); + }); +}); + +describe('applyWhereAndSortConditionsToQueryString', () => { + it('returns the query string', () => { + const options: DataGridOptions = { + limit: 0, + offset: 0, + where: [ + { + AlbumId: { + _gte: 2, + }, + }, + { + Title: { + _like: '%foo%', + }, + }, + ], + order_by: [ + { + column: 'AlbumId', + type: 'desc', + }, + { + column: 'Title', + type: 'asc', + }, + ], + }; + + const search = '?database=Chinook&table=%5B%22Album%22%5D'; + + expect( + applyWhereAndSortConditionsToQueryString({ + options, + search, + }) + ).toBe( + 'database=Chinook&table=%5B%22Album%22%5D&filter=AlbumId%3B_gte%3B2&filter=Title%3B_like%3B%25foo%25&sort=AlbumId%3Bdesc&sort=Title%3Basc' + ); + }); +}); + +describe('convertUrlToDataGridOptions', () => { + describe('when filters and sort are defined', () => { + it('returns the options', () => { + const search = + 'database=Chinook&table=Album&filter=AlbumId%3B_gte%3B1&filter=Title%3B_like%3B%25foo%25&sort=AlbumId%3Basc'; + + const expected: DataGridOptions = { + where: [ + { + AlbumId: { + _gte: '1', + }, + }, + { + Title: { + _like: '%foo%', + }, + }, + ], + order_by: [ + { + column: 'AlbumId', + type: 'asc', + }, + ], + }; + expect(convertUrlToDataGridOptions(search)).toEqual(expected); + }); + }); + + describe('when filters are defined', () => { + it('returns the options', () => { + const search = + 'database=Chinook&table=Album&filter=AlbumId%3B_gte%3B1&filter=Title%3B_like%3B%25foo%25'; + + const expected: DataGridOptions = { + where: [ + { + AlbumId: { + _gte: '1', + }, + }, + { + Title: { + _like: '%foo%', + }, + }, + ], + order_by: [], + }; + expect(convertUrlToDataGridOptions(search)).toEqual(expected); + }); + }); + + describe('when sort are defined', () => { + it('returns the options', () => { + const search = + 'database=Chinook&table=Album&sort=AlbumId%3Basc&sort=Title%3Bdesc'; + + const expected: DataGridOptions = { + where: [], + order_by: [ + { + column: 'AlbumId', + type: 'asc', + }, + { + column: 'Title', + type: 'desc', + }, + ], + }; + expect(convertUrlToDataGridOptions(search)).toEqual(expected); + }); + }); + + describe('when filters and sort are not defined', () => { + it('returns the options', () => { + const search = 'database=Chinook&table=Album'; + + const expected: DataGridOptions = { + where: [], + order_by: [], + }; + expect(convertUrlToDataGridOptions(search)).toEqual(expected); + }); + }); + + describe('when table columns are provided', () => { + it('returns the options', () => { + const search = + 'database=Chinook&table=Album&filter=AlbumId%3B_gte%3B1&filter=Title%3B_like%3B%25foo%25&sort=AlbumId%3Basc'; + + const expected: DataGridOptions = { + where: [ + { + AlbumId: { + _gte: 1, + }, + }, + { + Title: { + _like: '%foo%', + }, + }, + ], + order_by: [ + { + column: 'AlbumId', + type: 'asc', + }, + ], + }; + + const tableColumns: TableColumn[] = [ + { + name: 'AlbumId', + dataType: 'number', + graphQLProperties: { name: 'AlbumId', scalarType: 'decimal' }, + }, + { + name: 'Title', + dataType: 'string', + graphQLProperties: { name: 'Title', scalarType: 'String' }, + }, + ]; + + expect(convertUrlToDataGridOptions(search, tableColumns)).toEqual( + expected + ); + }); + }); +}); + +describe('convertValueToGraphQL', () => { + it('converts decimal', () => { + const value = '1'; + const tableColumn: TableColumn = { + name: 'AlbumId', + dataType: 'number', + graphQLProperties: { + name: 'AlbumId', + scalarType: 'decimal', + }, + }; + expect(convertValueToGraphQL(value, tableColumn)).toBe(1); + }); + + it('converts float', () => { + const value = '1'; + const tableColumn: TableColumn = { + name: 'AlbumId', + dataType: 'number', + graphQLProperties: { + name: 'AlbumId', + scalarType: 'float', + }, + }; + expect(convertValueToGraphQL(value, tableColumn)).toBe(1); + }); + + it('converts boolean', () => { + const value = 'true'; + const tableColumn: TableColumn = { + name: 'AlbumId', + dataType: 'bool', + graphQLProperties: { + name: 'AlbumId', + scalarType: 'boolean', + }, + }; + expect(convertValueToGraphQL(value, tableColumn)).toBe(true); + }); + + it('converts string', () => { + const value = 'aString'; + const tableColumn: TableColumn = { + name: 'AlbumId', + dataType: 'string', + graphQLProperties: { + name: 'AlbumId', + scalarType: 'string', + }, + }; + expect(convertValueToGraphQL(value, tableColumn)).toBe('aString'); + }); + + it('converts array of strings', () => { + const value = '[1, 2, 3, 4]'; + const tableColumn: TableColumn = { + name: 'AlbumId', + dataType: 'string', + graphQLProperties: { + name: 'AlbumId', + scalarType: 'string', + }, + }; + expect(convertValueToGraphQL(value, tableColumn)).toBe('["1","2","3","4"]'); + }); + + it('converts array of int', () => { + const value = '[1, 2, 3, 4]'; + const tableColumn: TableColumn = { + name: 'AlbumId', + dataType: 'number', + graphQLProperties: { + name: 'AlbumId', + scalarType: 'int', + }, + }; + expect(convertValueToGraphQL(value, tableColumn)).toBe('[1,2,3,4]'); + }); + + it('converts array of float', () => { + const value = '[1.1, 2.2, 3.3, 4.4]'; + const tableColumn: TableColumn = { + name: 'AlbumId', + dataType: 'number', + graphQLProperties: { + name: 'AlbumId', + scalarType: 'float', + }, + }; + expect(convertValueToGraphQL(value, tableColumn)).toBe('[1.1,2.2,3.3,4.4]'); + }); +}); diff --git a/console/src/features/BrowseRows/components/DataGrid/DataGrid.utils.ts b/console/src/features/BrowseRows/components/DataGrid/DataGrid.utils.ts index a03ac3a48fe..3953a43d4ee 100644 --- a/console/src/features/BrowseRows/components/DataGrid/DataGrid.utils.ts +++ b/console/src/features/BrowseRows/components/DataGrid/DataGrid.utils.ts @@ -1,4 +1,5 @@ -import { TableRow, WhereClause } from '@/features/DataSource'; +import { TableRow, WhereClause, TableColumn } from '@/features/DataSource'; +import { DataGridOptions } from './DataGrid'; export type AdaptSelectedRowIdsToWhereClauseArgs = { rowsId: Record; @@ -39,3 +40,167 @@ export const adaptSelectedRowIdsToWhereClause = ({ return whereClause; }; + +export type FilterConditions = ({ filter: string } | { sort: string })[]; + +export const mapWhereAndSortConditions = ( + options: DataGridOptions +): FilterConditions => { + const { where = [], order_by = [] } = options; + + const whereQueryString = where.map(whereCondition => { + const columnName = Object.keys(whereCondition)[0]; + const operator = Object.keys(whereCondition[columnName])[0]; + const value = whereCondition[columnName][operator]; + + const filterQueryString = `${columnName};${operator};${value}`; + + return { filter: filterQueryString }; + }); + + const orderQueryString = order_by.map(orderCondition => { + const columnName = orderCondition.column; + const sortOrder = orderCondition.type; + + const sortQueryString = `${columnName};${sortOrder}`; + + return { sort: sortQueryString }; + }); + + return [...whereQueryString, ...orderQueryString]; +}; + +export const replaceFiltersInUrl = ( + currentSearch: string, + newFilterConditions: FilterConditions +): string => { + const searchParams = new URLSearchParams(currentSearch); + searchParams.delete('filter'); + searchParams.delete('sort'); + + newFilterConditions.forEach(newFilterCondition => { + if ('filter' in newFilterCondition) { + searchParams.append('filter', newFilterCondition.filter); + } + + if ('sort' in newFilterCondition) { + searchParams.append('sort', newFilterCondition.sort); + } + }); + + return searchParams.toString(); +}; + +type Args = { + options: DataGridOptions; + search: string; +}; + +export const applyWhereAndSortConditionsToQueryString = ({ + options, + search, +}: Args) => { + const whereAndSortMap = mapWhereAndSortConditions(options); + const searchQueryString = replaceFiltersInUrl(search, whereAndSortMap); + return searchQueryString; +}; + +export const convertValueToGraphQL = ( + value: string, + column: TableColumn +): number | string | boolean => { + const scalarType = column.graphQLProperties?.scalarType || column.dataType; + + if (value.includes('[')) { + const values = value.replace('[', '').replace(']', '').split(','); + if (scalarType === 'decimal' || scalarType === 'float') { + return JSON.stringify(values.map(_value => parseFloat(_value))); + } + + if (scalarType === 'int') { + return JSON.stringify(values.map(_value => parseInt(_value, 10))); + } + + if (scalarType === 'string') { + return JSON.stringify(values.map(_value => _value.trim().toString())); + } + } + + if (scalarType === 'decimal' || scalarType === 'float') { + return parseFloat(value); + } + + if (scalarType === 'int') { + return parseInt(value, 10); + } + + if (scalarType === 'boolean') { + return value === 'true'; + } + + return value; +}; + +export const convertUrlToDataGridOptions = ( + search: string, + columns: TableColumn[] | undefined = [] +): DataGridOptions => { + const searchParams = new URLSearchParams(search); + + const baseOption: DataGridOptions = { + where: [], + order_by: [], + }; + + const searchParamsArray: [string, string][] = Array.from( + searchParams.entries() + ); + + return searchParamsArray.reduce((acc, value) => { + const key = value[0]; + if (key === 'database' || key === 'table') { + return acc; + } + + if (key === 'filter') { + const where = acc?.where || []; + const [columnName, operator, filterValue] = value[1].split(';'); + + const column = columns.find(_column => _column.name === columnName); + const convertedValue = column + ? convertValueToGraphQL(filterValue, column) + : filterValue; + + return { + ...acc, + where: [ + ...where, + { + [columnName]: { + [operator]: convertedValue, + }, + }, + ], + }; + } + + if (key === 'sort') { + const order_by = acc?.order_by || []; + const [columnName, orderType] = value[1].split(';'); + if (orderType === 'asc' || orderType === 'desc') { + return { + ...acc, + order_by: [ + ...order_by, + { + column: columnName, + type: orderType, + }, + ], + }; + } + } + + return acc; + }, baseOption); +}; diff --git a/console/src/features/BrowseRows/components/DataGrid/parts/DataTableOptions.tsx b/console/src/features/BrowseRows/components/DataGrid/parts/DataTableOptions.tsx index 131f5eb1949..d54f50ef625 100644 --- a/console/src/features/BrowseRows/components/DataGrid/parts/DataTableOptions.tsx +++ b/console/src/features/BrowseRows/components/DataGrid/parts/DataTableOptions.tsx @@ -12,6 +12,7 @@ import { FaFilter, FaRegTimesCircle, FaSearch, + FaSortAmountDownAlt, FaSortAmountUpAlt, FaTimes, } from 'react-icons/fa'; @@ -95,7 +96,11 @@ const DisplayOrderByClauses = ({
- + {orderByClause.type === 'desc' ? ( + + ) : ( + + )} {orderByClause.column} ({orderByClause.type}) diff --git a/console/src/features/BrowseRows/components/DataGrid/parts/RowOptionsButton.tsx b/console/src/features/BrowseRows/components/DataGrid/parts/RowOptionsButton.tsx index 01e808af503..5b169e58a1b 100644 --- a/console/src/features/BrowseRows/components/DataGrid/parts/RowOptionsButton.tsx +++ b/console/src/features/BrowseRows/components/DataGrid/parts/RowOptionsButton.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { DropdownMenu } from '@/new-components/DropdownMenu'; -import { FaEllipsisV } from 'react-icons/fa'; +import { RiMore2Fill } from 'react-icons/ri'; export const RowOptionsButton: React.VFC<{ row: Record; @@ -19,7 +19,7 @@ export const RowOptionsButton: React.VFC<{ >
- +
diff --git a/console/src/features/BrowseRows/hooks/useRows.tsx b/console/src/features/BrowseRows/hooks/useRows.tsx index d09ba5f21e5..3ebb1f5d1b7 100644 --- a/console/src/features/BrowseRows/hooks/useRows.tsx +++ b/console/src/features/BrowseRows/hooks/useRows.tsx @@ -25,8 +25,6 @@ export const fetchRows = async ({ table, }); - console.log('>>>', columns, tableColumns); - const result = await DataSource(httpClient).getTableRows({ dataSourceName, table, @@ -34,8 +32,6 @@ export const fetchRows = async ({ options, }); - console.log('>>>', result); - return result; };