mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
console: integrate redux into the new filters section component
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5958 GitOrigin-RevId: 58f565ed293dcad71bb9239eb511531e44d63618
This commit is contained in:
parent
dd37456949
commit
66a698cbef
@ -6,7 +6,6 @@ import {
|
||||
downloadObjectAsJsonFile,
|
||||
downloadObjectAsCsvFile,
|
||||
getCurrTimeForFileName,
|
||||
getConfirmation,
|
||||
} from '../../../Common/utils/jsUtils';
|
||||
|
||||
const LOADING = 'ViewTable/FilterQuery/LOADING';
|
||||
@ -121,59 +120,48 @@ const runQuery = tableSchema => {
|
||||
|
||||
const exportDataQuery = (tableSchema, type) => {
|
||||
return (dispatch, getState) => {
|
||||
const count = getState().tables.view.count;
|
||||
|
||||
const confirmed = getConfirmation(
|
||||
`There ${
|
||||
count === 1 ? 'is 1 row' : `are ${count} rows`
|
||||
} selected for export.`
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = getState().tables.view.curFilter;
|
||||
let finalWhereClauses = state.where.$and.filter(w => {
|
||||
const colName = Object.keys(w)[0].trim();
|
||||
const filteredWhereClauses = state.where.$and.filter(whereClause => {
|
||||
const colName = Object.keys(whereClause)[0].trim();
|
||||
if (colName === '') {
|
||||
return false;
|
||||
}
|
||||
const opName = Object.keys(w[colName])[0].trim();
|
||||
const opName = Object.keys(whereClause[colName])[0].trim();
|
||||
if (opName === '') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
finalWhereClauses = finalWhereClauses.map(w => {
|
||||
const colName = Object.keys(w)[0];
|
||||
const opName = Object.keys(w[colName])[0];
|
||||
const val = w[colName][opName];
|
||||
const finalWhereClauses = filteredWhereClauses.map(whereClause => {
|
||||
const colName = Object.keys(whereClause)[0];
|
||||
const opName = Object.keys(whereClause[colName])[0];
|
||||
const val = whereClause[colName][opName];
|
||||
|
||||
if (['$in', '$nin'].includes(opName)) {
|
||||
w[colName][opName] = parseArray(val);
|
||||
return w;
|
||||
whereClause[colName][opName] = parseArray(val);
|
||||
return whereClause;
|
||||
}
|
||||
|
||||
const colType = tableSchema.columns.find(
|
||||
c => c.column_name === colName
|
||||
).data_type;
|
||||
if (Integers.indexOf(colType) > 0) {
|
||||
w[colName][opName] = parseInt(val, 10);
|
||||
return w;
|
||||
whereClause[colName][opName] = parseInt(val, 10);
|
||||
return whereClause;
|
||||
}
|
||||
if (Reals.indexOf(colType) > 0) {
|
||||
w[colName][opName] = parseFloat(val);
|
||||
return w;
|
||||
whereClause[colName][opName] = parseFloat(val);
|
||||
return whereClause;
|
||||
}
|
||||
if (colType === 'boolean') {
|
||||
if (val === 'true') {
|
||||
w[colName][opName] = true;
|
||||
whereClause[colName][opName] = true;
|
||||
} else if (val === 'false') {
|
||||
w[colName][opName] = false;
|
||||
whereClause[colName][opName] = false;
|
||||
}
|
||||
}
|
||||
return w;
|
||||
return whereClause;
|
||||
});
|
||||
const newQuery = {
|
||||
where: { $and: finalWhereClauses },
|
||||
@ -190,10 +178,18 @@ const exportDataQuery = (tableSchema, type) => {
|
||||
const fileName = `export_${table_schema}_${table_name}_${getCurrTimeForFileName()}`;
|
||||
|
||||
dispatch({ type: 'ViewTable/V_SET_QUERY_OPTS', queryStuff: newQuery });
|
||||
dispatch(vMakeExportRequest()).then(d => {
|
||||
if (d) {
|
||||
if (type === 'JSON') downloadObjectAsJsonFile(fileName, d);
|
||||
else if (type === 'CSV') downloadObjectAsCsvFile(fileName, d);
|
||||
dispatch(vMakeExportRequest()).then(rows => {
|
||||
if (!rows) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'JSON') {
|
||||
downloadObjectAsJsonFile(fileName, rows);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'CSV') {
|
||||
downloadObjectAsCsvFile(fileName, rows);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -1,347 +0,0 @@
|
||||
/*
|
||||
Use state exactly the way columns in create table do.
|
||||
dispatch actions using a given function,
|
||||
but don't listen to state.
|
||||
derive everything through viewtable as much as possible.
|
||||
*/
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { createHistory } from 'history';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
|
||||
import {
|
||||
setFilterCol,
|
||||
setFilterOp,
|
||||
setFilterVal,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
} from './FilterActions.js';
|
||||
import {
|
||||
setOrderCol,
|
||||
setOrderType,
|
||||
addOrder,
|
||||
removeOrder,
|
||||
} from './FilterActions.js';
|
||||
import {
|
||||
setDefaultQuery,
|
||||
runQuery,
|
||||
exportDataQuery,
|
||||
setOffset,
|
||||
} from './FilterActions';
|
||||
import { Button } from '@/new-components/Button';
|
||||
import ReloadEnumValuesButton from '../Common/Components/ReloadEnumValuesButton';
|
||||
import { getPersistedPageSize } from './tableUtils';
|
||||
import { isEmpty } from '../../../Common/utils/jsUtils';
|
||||
import ExportData from './ExportData';
|
||||
import { dataSource, getTableCustomColumnName } from '../../../../dataSources';
|
||||
import { inputStyles } from '../../Actions/constants.js';
|
||||
|
||||
const history = createHistory();
|
||||
|
||||
const renderCols = (
|
||||
colName,
|
||||
tableSchema,
|
||||
onChange,
|
||||
usage,
|
||||
key,
|
||||
skipColumns
|
||||
) => {
|
||||
let columns = tableSchema.columns.map(c => c.column_name);
|
||||
if (skipColumns) {
|
||||
columns = columns.filter(n => !skipColumns.includes(n) || n === colName);
|
||||
}
|
||||
|
||||
return (
|
||||
<select
|
||||
className={inputStyles}
|
||||
onChange={onChange}
|
||||
value={colName.trim()}
|
||||
data-test={
|
||||
usage === 'sort' ? `sort-column-${key}` : `filter-column-${key}`
|
||||
}
|
||||
>
|
||||
{colName.trim() === '' ? (
|
||||
<option disabled value="">
|
||||
-- column --
|
||||
</option>
|
||||
) : null}
|
||||
{columns.map((c, i) => {
|
||||
const col_name = getTableCustomColumnName(tableSchema, c) ?? c;
|
||||
return (
|
||||
<option key={i} value={c}>
|
||||
{col_name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOps = (opName, onChange, key) => (
|
||||
<select
|
||||
className={inputStyles}
|
||||
onChange={onChange}
|
||||
value={opName.trim()}
|
||||
data-test={`filter-op-${key}`}
|
||||
>
|
||||
{opName.trim() === '' ? (
|
||||
<option disabled value="">
|
||||
-- op --
|
||||
</option>
|
||||
) : null}
|
||||
{dataSource.operators.map((o, i) => (
|
||||
<option key={i} value={o.value}>
|
||||
{`[${o.graphqlOp}] ${o.name}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
|
||||
const getDefaultValue = (possibleValue, opName) => {
|
||||
if (possibleValue) {
|
||||
if (Array.isArray(possibleValue)) return JSON.stringify(possibleValue);
|
||||
return possibleValue;
|
||||
}
|
||||
|
||||
const operator = dataSource.operators.find(op => op.value === opName);
|
||||
return operator && operator.defaultValue ? operator.defaultValue : '';
|
||||
};
|
||||
|
||||
const renderWheres = (whereAnd, tableSchema, dispatch) => {
|
||||
return whereAnd.map((clause, i) => {
|
||||
const colName = Object.keys(clause)[0];
|
||||
const opName = Object.keys(clause[colName])[0];
|
||||
const dSetFilterCol = e => {
|
||||
dispatch(setFilterCol(e.target.value, i));
|
||||
};
|
||||
const dSetFilterOp = e => {
|
||||
dispatch(setFilterOp(e.target.value, i));
|
||||
};
|
||||
let removeIcon = null;
|
||||
if (i + 1 < whereAnd.length) {
|
||||
removeIcon = (
|
||||
<FaTimes
|
||||
onClick={() => {
|
||||
dispatch(removeFilter(i));
|
||||
}}
|
||||
data-test={`clear-filter-${i}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={i} className="flex mb-xs">
|
||||
<div className="w-4/12">
|
||||
{renderCols(colName, tableSchema, dSetFilterCol, 'filter', i, [])}
|
||||
</div>
|
||||
<div className="w-3/12 ml-xs">{renderOps(opName, dSetFilterOp, i)}</div>
|
||||
<div className="w-3/12 ml-xs">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="-- value --"
|
||||
value={getDefaultValue(clause[colName][opName], opName)}
|
||||
onChange={e => {
|
||||
dispatch(setFilterVal(e.target.value, i));
|
||||
if (i + 1 === whereAnd.length) {
|
||||
dispatch(addFilter());
|
||||
}
|
||||
}}
|
||||
data-test={`filter-value-${i}`}
|
||||
className={inputStyles}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/12">{removeIcon}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const renderSorts = (orderBy, tableSchema, dispatch) => {
|
||||
const currentOrderBy = orderBy.map(o => o.column);
|
||||
return orderBy.map((c, i) => {
|
||||
const dSetOrderCol = e => {
|
||||
dispatch(setOrderCol(e.target.value, i));
|
||||
if (i + 1 === orderBy.length) {
|
||||
dispatch(addOrder());
|
||||
}
|
||||
};
|
||||
let removeIcon = null;
|
||||
if (i + 1 < orderBy.length) {
|
||||
removeIcon = (
|
||||
<FaTimes
|
||||
onClick={() => {
|
||||
dispatch(removeOrder(i));
|
||||
}}
|
||||
data-test={`clear-sorts-${i}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={i} className="flex mb-xs">
|
||||
<div className="w-6/12 mr-xs">
|
||||
{renderCols(
|
||||
c.column,
|
||||
tableSchema,
|
||||
dSetOrderCol,
|
||||
'sort',
|
||||
i,
|
||||
currentOrderBy
|
||||
)}
|
||||
</div>
|
||||
<div className="w-6/12">
|
||||
<select
|
||||
value={c.column ? c.type : ''}
|
||||
className={inputStyles}
|
||||
onChange={e => {
|
||||
dispatch(setOrderType(e.target.value, i));
|
||||
}}
|
||||
data-test={`sort-order-${i}`}
|
||||
>
|
||||
<option disabled value="">
|
||||
--
|
||||
</option>
|
||||
<option value="asc">Asc</option>
|
||||
<option value="desc">Desc</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="">{removeIcon}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
class FilterQuery extends Component {
|
||||
componentDidMount() {
|
||||
const { dispatch, tableSchema, curQuery } = this.props;
|
||||
const limit = getPersistedPageSize();
|
||||
if (isEmpty(this.props.urlQuery)) {
|
||||
dispatch(setDefaultQuery({ ...curQuery, limit }));
|
||||
return;
|
||||
}
|
||||
|
||||
let urlFilters = [];
|
||||
if (typeof this.props.urlQuery.filter === 'string') {
|
||||
urlFilters = [this.props.urlQuery.filter];
|
||||
} else if (Array.isArray(this.props.urlQuery.filter)) {
|
||||
urlFilters = this.props.urlQuery.filter;
|
||||
}
|
||||
const where = {
|
||||
$and: urlFilters.map(filter => {
|
||||
const parts = filter.split(';');
|
||||
const col = parts[0];
|
||||
const op = parts[1];
|
||||
const value = parts[2];
|
||||
return { [col]: { [op]: value } };
|
||||
}),
|
||||
};
|
||||
|
||||
let urlSorts = [];
|
||||
if (typeof this.props.urlQuery.sort === 'string') {
|
||||
urlSorts = [this.props.urlQuery.sort];
|
||||
} else if (Array.isArray(this.props.urlQuery.sort)) {
|
||||
urlSorts = this.props.urlQuery.sort;
|
||||
}
|
||||
|
||||
const order_by = urlSorts.map(sort => {
|
||||
const parts = sort.split(';');
|
||||
const column = parts[0];
|
||||
const type = parts[1];
|
||||
const nulls = 'last';
|
||||
return { column, type, nulls };
|
||||
});
|
||||
|
||||
dispatch(setDefaultQuery({ where, order_by, limit }));
|
||||
dispatch(runQuery(tableSchema));
|
||||
}
|
||||
|
||||
setParams(query = { filters: [], sorts: [] }) {
|
||||
const searchParams = new URLSearchParams();
|
||||
query.filters.forEach(filter => searchParams.append('filter', filter));
|
||||
query.sorts.forEach(sort => searchParams.append('sort', sort));
|
||||
return searchParams.toString();
|
||||
}
|
||||
|
||||
setUrlParams(whereAnd, orderBy) {
|
||||
const sorts = orderBy
|
||||
.filter(order => order.column)
|
||||
.map(order => `${order.column};${order.type}`);
|
||||
const filters = whereAnd
|
||||
.filter(
|
||||
where => Object.keys(where).length === 1 && Object.keys(where)[0] !== ''
|
||||
)
|
||||
.map(where => {
|
||||
const col = Object.keys(where)[0];
|
||||
const op = Object.keys(where[col])[0];
|
||||
const value = where[col][op];
|
||||
return `${col};${op};${value}`;
|
||||
});
|
||||
const url = this.setParams({ filters, sorts });
|
||||
history.push({
|
||||
pathname: history.getCurrentLocation().pathname,
|
||||
search: `?${url}`,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dispatch, whereAnd, tableSchema, orderBy } = this.props; // eslint-disable-line no-unused-vars
|
||||
const exportData = type => {
|
||||
dispatch(exportDataQuery(tableSchema, type));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-sm">
|
||||
<form
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
dispatch(setOffset(0));
|
||||
this.setUrlParams(whereAnd, orderBy);
|
||||
dispatch(runQuery(tableSchema));
|
||||
}}
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="w-1/2 pl-0">
|
||||
<div className="text-lg font-bold pb-md">Filter</div>
|
||||
{renderWheres(whereAnd, tableSchema, dispatch)}
|
||||
</div>
|
||||
<div className="w-1/2 pl-0">
|
||||
<div className="text-lg font-bold pb-md">Sort</div>
|
||||
{renderSorts(orderBy, tableSchema, dispatch)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pr-sm clear-both mt-sm">
|
||||
<Button
|
||||
type="submit"
|
||||
mode="primary"
|
||||
data-test="run-query"
|
||||
className="mr-sm"
|
||||
>
|
||||
Run query
|
||||
</Button>
|
||||
<ExportData onExport={exportData} />
|
||||
{tableSchema.is_enum ? (
|
||||
<ReloadEnumValuesButton
|
||||
dispatch={dispatch}
|
||||
tooltipStyle="ml-sm"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FilterQuery.propTypes = {
|
||||
curQuery: PropTypes.object.isRequired,
|
||||
tableSchema: PropTypes.object.isRequired,
|
||||
whereAnd: PropTypes.array.isRequired,
|
||||
orderBy: PropTypes.array.isRequired,
|
||||
limit: PropTypes.number.isRequired,
|
||||
count: PropTypes.number,
|
||||
tableName: PropTypes.string,
|
||||
offset: PropTypes.number.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default FilterQuery;
|
@ -32,19 +32,20 @@ import {
|
||||
|
||||
import { Button } from '@/new-components/Button';
|
||||
|
||||
import { FilterSectionContainer } from '@/features/BrowseRows/FiltersSection/FiltersSectionContainer';
|
||||
import { PaginationWithOnlyNavContainer } from '@/new-components/PaginationWithOnlyNav/PaginationWithOnlyNavContainer';
|
||||
|
||||
import {
|
||||
setOrderCol,
|
||||
setOrderType,
|
||||
removeOrder,
|
||||
runQuery,
|
||||
setOffset,
|
||||
setLimit,
|
||||
addOrder,
|
||||
} from './FilterActions';
|
||||
|
||||
import _push from '../push';
|
||||
import { ordinalColSort } from '../utils';
|
||||
import FilterQuery from './FilterQuery';
|
||||
import Spinner from '../../../Common/Spinner/Spinner';
|
||||
|
||||
import { E_SET_EDITITEM } from './EditActions';
|
||||
@ -66,10 +67,8 @@ import {
|
||||
getPersistedCollapsedColumns,
|
||||
persistColumnOrderChange,
|
||||
getPersistedColumnsOrder,
|
||||
persistPageSizeChange,
|
||||
} from './tableUtils';
|
||||
import { compareRows, isTableWithPK } from './utils';
|
||||
import { PaginationWithOnlyNav } from '../../../../new-components/PaginationWithOnlyNav/PaginationWithOnlyNav';
|
||||
|
||||
const ViewRows = props => {
|
||||
const {
|
||||
@ -92,7 +91,6 @@ const ViewRows = props => {
|
||||
count,
|
||||
expandedRow,
|
||||
manualTriggers = [],
|
||||
location,
|
||||
readOnlyMode,
|
||||
shouldHidePagination,
|
||||
currentSource,
|
||||
@ -721,44 +719,6 @@ const ViewRows = props => {
|
||||
disableBulkSelect
|
||||
);
|
||||
|
||||
const getFilterQuery = () => {
|
||||
let _filterQuery = null;
|
||||
|
||||
if (!isSingleRow) {
|
||||
if (curRelName === activePath[curDepth] || curDepth === 0) {
|
||||
// Rendering only if this is the activePath or this is the root
|
||||
|
||||
let wheres = [{ '': { '': '' } }];
|
||||
if ('where' in curFilter && '$and' in curFilter.where) {
|
||||
wheres = [...curFilter.where.$and];
|
||||
}
|
||||
|
||||
let orderBy = [{ column: '', type: 'asc', nulls: 'last' }];
|
||||
if ('order_by' in curFilter) {
|
||||
orderBy = [...curFilter.order_by];
|
||||
}
|
||||
|
||||
const offset = 'offset' in curFilter ? curFilter.offset : 0;
|
||||
|
||||
_filterQuery = (
|
||||
<FilterQuery
|
||||
curQuery={curQuery}
|
||||
whereAnd={wheres}
|
||||
tableSchema={tableSchema}
|
||||
orderBy={orderBy}
|
||||
dispatch={dispatch}
|
||||
count={count}
|
||||
tableName={curTableName}
|
||||
offset={offset}
|
||||
urlQuery={location && location.query}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return _filterQuery;
|
||||
};
|
||||
|
||||
const getSelectedRowsSection = () => {
|
||||
const handleDeleteItems = () => {
|
||||
const pkClauses = selectedRows.map(row =>
|
||||
@ -873,6 +833,11 @@ const ViewRows = props => {
|
||||
return _childComponent;
|
||||
};
|
||||
|
||||
const [userQuery, setUserQuery] = useState({
|
||||
where: { $and: [] },
|
||||
order_by: [],
|
||||
});
|
||||
|
||||
const renderTableBody = () => {
|
||||
if (isProgressing) {
|
||||
return (
|
||||
@ -976,31 +941,28 @@ const ViewRows = props => {
|
||||
|
||||
const handlePageChange = page => {
|
||||
if (curFilter.offset !== page * curFilter.limit) {
|
||||
dispatch(setOffset(page * curFilter.limit));
|
||||
dispatch(runQuery(tableSchema));
|
||||
setSelectedRows([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageSizeChange = size => {
|
||||
if (curFilter.size !== size) {
|
||||
dispatch(setLimit(size));
|
||||
dispatch(setOffset(0));
|
||||
dispatch(runQuery(tableSchema));
|
||||
setSelectedRows([]);
|
||||
persistPageSizeChange(size);
|
||||
}
|
||||
};
|
||||
|
||||
const paginationProps = {};
|
||||
if (useCustomPagination) {
|
||||
paginationProps.PaginationComponent = () => (
|
||||
<PaginationWithOnlyNav
|
||||
offset={curFilter.offset}
|
||||
<PaginationWithOnlyNavContainer
|
||||
limit={curFilter.limit}
|
||||
changePage={handlePageChange}
|
||||
changePageSize={handlePageSizeChange}
|
||||
offset={curFilter.offset}
|
||||
onChangePage={handlePageChange}
|
||||
onChangePageSize={handlePageSizeChange}
|
||||
pageSize={curFilter.size}
|
||||
rows={curRows}
|
||||
tableSchema={tableSchema}
|
||||
userQuery={userQuery}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1045,9 +1007,18 @@ const ViewRows = props => {
|
||||
isVisible = true;
|
||||
}
|
||||
|
||||
const isFilterSectionVisible =
|
||||
!isSingleRow && (curRelName === activePath[curDepth] || curDepth === 0);
|
||||
|
||||
return (
|
||||
<div className={isVisible ? '' : 'hide '}>
|
||||
{getFilterQuery()}
|
||||
{isFilterSectionVisible && (
|
||||
<FilterSectionContainer
|
||||
dataSourceName={currentSource}
|
||||
table={{ schema: tableSchema.table_schema, name: curTableName }}
|
||||
onRunQuery={newUserQuery => setUserQuery(newUserQuery)}
|
||||
/>
|
||||
)}
|
||||
<div className="w-fit ml-0 mt-md">
|
||||
{getSelectedRowsSection()}
|
||||
<div>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { MetadataDataSource } from '../../../../metadata/types';
|
||||
import { ReduxState } from './../../../../types';
|
||||
|
||||
type TableSchema = {
|
||||
export type TableSchema = {
|
||||
primary_key?: { columns: string[] };
|
||||
columns: Array<{ column_name: string }>;
|
||||
columns: Array<{ column_name: string; data_type: string }>;
|
||||
};
|
||||
|
||||
type TableSchemaWithPK = {
|
||||
|
@ -120,7 +120,12 @@ export interface DataSourcesAPI {
|
||||
TIME?: string;
|
||||
TIMETZ?: string;
|
||||
};
|
||||
operators: Array<{ name: string; value: string; graphqlOp: string }>;
|
||||
operators: Array<{
|
||||
name: string;
|
||||
value: string;
|
||||
graphqlOp: string;
|
||||
defaultValue?: string;
|
||||
}>;
|
||||
getFetchTablesListQuery: (options: {
|
||||
schemas: string[];
|
||||
tables?: QualifiedTable[];
|
||||
|
@ -3,11 +3,11 @@ import { SelectItem } from '@/components/Common/SelectInputSplitField/SelectInpu
|
||||
import { Button } from '@/new-components/Button';
|
||||
import { FieldArrayWithId } from 'react-hook-form';
|
||||
import { FilterRow, OperatorItem } from './FilterRow';
|
||||
import { FormValues } from './types';
|
||||
import { FiltersAndSortFormValues } from './types';
|
||||
|
||||
export type FilterRowsProps = {
|
||||
columns: SelectItem[];
|
||||
fields: FieldArrayWithId<FormValues, 'filter', 'id'>[];
|
||||
fields: FieldArrayWithId<FiltersAndSortFormValues, 'filter', 'id'>[];
|
||||
onAdd: () => void;
|
||||
onRemove: (index: number) => void;
|
||||
operators: OperatorItem[];
|
||||
|
@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import { SelectItem } from '@/components/Common/SelectInputSplitField/SelectInputSplitField';
|
||||
import ExportData, {
|
||||
ExportDataProps,
|
||||
} from '@/components/Services/Data/TableBrowseRows/ExportData';
|
||||
import { Button } from '@/new-components/Button';
|
||||
import { DropdownMenu } from '@/new-components/DropdownMenu';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { FaFileExport } from 'react-icons/fa';
|
||||
import { OperatorItem } from './FilterRow';
|
||||
import { FilterRows } from './FilterRows';
|
||||
import { SortItem } from './SortRow';
|
||||
@ -13,24 +12,38 @@ import {
|
||||
defaultColumn,
|
||||
defaultOperator,
|
||||
defaultOrder,
|
||||
FormValues,
|
||||
FiltersAndSortFormValues,
|
||||
} from './types';
|
||||
import { useFilterRows } from './hooks/useFilterRows';
|
||||
import { useSortRows } from './hooks/useSortRows';
|
||||
|
||||
/*
|
||||
NOTE:
|
||||
Component created out of from FilterQuery.
|
||||
We'll need to delete FilterQuery in the future, when the integration of Redux is complete
|
||||
and we'll integrate FiltersSection in the Browse Rows tab.
|
||||
*/
|
||||
export const sortPlaceholder = '--';
|
||||
|
||||
export const sortOptions: SortItem[] = [
|
||||
{
|
||||
label: sortPlaceholder,
|
||||
value: sortPlaceholder,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
label: 'Asc',
|
||||
value: 'asc',
|
||||
},
|
||||
{
|
||||
label: 'Desc',
|
||||
value: 'desc',
|
||||
},
|
||||
];
|
||||
|
||||
type FiltersSectionProps = {
|
||||
columns: SelectItem[];
|
||||
operators: OperatorItem[];
|
||||
orders: SortItem[];
|
||||
onExport: ExportDataProps['onExport'];
|
||||
onSubmit: (values: FormValues) => void;
|
||||
onExport: (
|
||||
type: 'CSV' | 'JSON',
|
||||
formValues: FiltersAndSortFormValues
|
||||
) => void;
|
||||
onSubmit: (values: FiltersAndSortFormValues) => void;
|
||||
};
|
||||
|
||||
export const FiltersSection = ({
|
||||
@ -40,7 +53,7 @@ export const FiltersSection = ({
|
||||
onExport,
|
||||
onSubmit,
|
||||
}: FiltersSectionProps) => {
|
||||
const methods = useForm<FormValues>({
|
||||
const methods = useForm<FiltersAndSortFormValues>({
|
||||
defaultValues: {
|
||||
filter: [{ column: defaultColumn, operator: defaultOperator, value: '' }],
|
||||
sort: [{ column: defaultColumn, order: defaultOrder }],
|
||||
@ -59,6 +72,11 @@ export const FiltersSection = ({
|
||||
const { onRemoveSortRow, onAddSortRow, sortFields, showFirstRemoveOnSort } =
|
||||
useSortRows({ methods });
|
||||
|
||||
const exportItems = [
|
||||
[<div onClick={handleSubmit(arg => onExport('CSV', arg))}>CSV</div>],
|
||||
[<div onClick={handleSubmit(arg => onExport('JSON', arg))}>JSON</div>],
|
||||
];
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
@ -90,8 +108,11 @@ export const FiltersSection = ({
|
||||
Run query
|
||||
</Button>
|
||||
<div>
|
||||
{/* TODO: fix the dropdown */}
|
||||
<ExportData onExport={onExport} />
|
||||
<DropdownMenu items={exportItems}>
|
||||
<Button icon={<FaFileExport />} iconPosition="start">
|
||||
Export data
|
||||
</Button>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,126 @@
|
||||
import { dataSource } from '@/dataSources';
|
||||
import { useAppDispatch, useAppSelector } from '@/store';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
downloadObjectAsCsvFile,
|
||||
downloadObjectAsJsonFile,
|
||||
getCurrTimeForFileName,
|
||||
} from '@/components/Common/utils/jsUtils';
|
||||
import { vMakeExportRequest } from '../../../components/Services/Data/TableBrowseRows/ViewActions';
|
||||
import { setOffset } from '../../../components/Services/Data/TableBrowseRows/FilterActions';
|
||||
import { OperatorItem } from './FilterRow';
|
||||
import {
|
||||
adaptFormValuesToQuery,
|
||||
filterValidUserQuery,
|
||||
getColumns,
|
||||
runFilterQuery,
|
||||
setUrlParams,
|
||||
} from './FiltersSectionContainer.utils';
|
||||
import { FiltersSection, sortOptions } from './FiltersSection';
|
||||
import { FiltersAndSortFormValues, UserQuery } from './types';
|
||||
import { Table, useTableSchema } from './hooks/useTableSchema';
|
||||
import { useTableColumns } from './hooks/useTableColumns';
|
||||
import { useTableName } from './hooks/useTableName';
|
||||
|
||||
type FilterSectionContainerProps = {
|
||||
onRunQuery: (userQuery: UserQuery) => void | null;
|
||||
dataSourceName: string;
|
||||
table: Table;
|
||||
};
|
||||
|
||||
const replaceAllDotsWithUnderscore = (text: string) => text.replace(/\./g, '_');
|
||||
const getFileName = (tableName: string) => {
|
||||
const replacedTableName = replaceAllDotsWithUnderscore(tableName);
|
||||
const currentTime = getCurrTimeForFileName();
|
||||
return `export_${replacedTableName}_${currentTime}`;
|
||||
};
|
||||
|
||||
export const FilterSectionContainer = ({
|
||||
onRunQuery,
|
||||
dataSourceName,
|
||||
table,
|
||||
}: FilterSectionContainerProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const query = useAppSelector(state => state.tables.view.query);
|
||||
const curFilter = useAppSelector(state => state.tables.view.curFilter);
|
||||
const limit = curFilter.limit;
|
||||
const offset = curFilter.offset;
|
||||
|
||||
const tableSchema = useTableSchema(table);
|
||||
|
||||
const tableColumns = useTableColumns({ dataSourceName, table });
|
||||
|
||||
const columns = getColumns(query.columns);
|
||||
|
||||
const [operators, setOperators] = useState<OperatorItem[]>([]);
|
||||
useEffect(() => {
|
||||
const operatorItems: OperatorItem[] = dataSource.operators.map(op => {
|
||||
return {
|
||||
label: `[${op.graphqlOp}] ${op.name}`,
|
||||
value: op.value,
|
||||
defaultValue: op?.defaultValue,
|
||||
};
|
||||
});
|
||||
setOperators(operatorItems);
|
||||
}, []);
|
||||
|
||||
const onSubmit = (userQuery: UserQuery) => {
|
||||
dispatch(setOffset(0));
|
||||
setUrlParams(userQuery.where.$and, userQuery.order_by);
|
||||
dispatch(
|
||||
runFilterQuery({
|
||||
tableSchema,
|
||||
whereAnd: userQuery.where.$and,
|
||||
orderBy: userQuery.order_by,
|
||||
limit,
|
||||
offset,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const tableName = useTableName({ dataSourceName, table });
|
||||
const onExportData = (
|
||||
type: 'CSV' | 'JSON',
|
||||
formValues: FiltersAndSortFormValues
|
||||
) => {
|
||||
const userQuery = filterValidUserQuery(
|
||||
adaptFormValuesToQuery(formValues, tableColumns)
|
||||
);
|
||||
|
||||
const fileName = getFileName(tableName);
|
||||
dispatch({ type: 'ViewTable/V_SET_QUERY_OPTS', queryStuff: userQuery });
|
||||
dispatch(vMakeExportRequest()).then((rows: Record<string, unknown>[]) => {
|
||||
if (!rows || rows.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'JSON':
|
||||
downloadObjectAsJsonFile(fileName, rows);
|
||||
break;
|
||||
case 'CSV':
|
||||
downloadObjectAsCsvFile(fileName, rows);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<FiltersSection
|
||||
columns={columns}
|
||||
operators={operators}
|
||||
orders={sortOptions}
|
||||
onExport={onExportData}
|
||||
onSubmit={(values: FiltersAndSortFormValues) => {
|
||||
const userQuery = filterValidUserQuery(
|
||||
adaptFormValuesToQuery(values, tableColumns)
|
||||
);
|
||||
if (onRunQuery) {
|
||||
onRunQuery(userQuery);
|
||||
}
|
||||
onSubmit(userQuery);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,77 @@
|
||||
import { TableColumn } from '../../DataSource';
|
||||
import { adaptFormValuesToQuery } from './FiltersSectionContainer.utils';
|
||||
import { FiltersAndSortFormValues, UserQuery } from './types';
|
||||
|
||||
describe('adaptFormValuesToQuery', () => {
|
||||
it('adapts the form values into a query', () => {
|
||||
const input: FiltersAndSortFormValues = {
|
||||
filter: [
|
||||
{ column: 'text', operator: '$eq', value: 'aaaa' },
|
||||
{ column: 'id', operator: '$eq', value: '1' },
|
||||
],
|
||||
sort: [{ column: 'id', order: 'asc' }],
|
||||
};
|
||||
const expected: UserQuery = {
|
||||
where: {
|
||||
$and: [
|
||||
{
|
||||
text: {
|
||||
$eq: 'aaaa',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: {
|
||||
$eq: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
order_by: [
|
||||
{
|
||||
column: 'id',
|
||||
type: 'asc',
|
||||
nulls: 'last',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const columnDataTypes: TableColumn[] = [
|
||||
{
|
||||
name: 'id',
|
||||
dataType: 'integer',
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
dataType: 'text',
|
||||
},
|
||||
];
|
||||
expect(adaptFormValuesToQuery(input, columnDataTypes)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('converts the $in values into array', () => {
|
||||
const input: FiltersAndSortFormValues = {
|
||||
filter: [{ column: 'id', operator: '$in', value: '[1, 2]' }],
|
||||
sort: [],
|
||||
};
|
||||
const expected: UserQuery = {
|
||||
where: {
|
||||
$and: [
|
||||
{
|
||||
id: {
|
||||
$in: [1, 2],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
order_by: [],
|
||||
};
|
||||
|
||||
const columnDataTypes: TableColumn[] = [
|
||||
{
|
||||
name: 'id',
|
||||
dataType: 'integer',
|
||||
},
|
||||
];
|
||||
expect(adaptFormValuesToQuery(input, columnDataTypes)).toEqual(expected);
|
||||
});
|
||||
});
|
@ -0,0 +1,229 @@
|
||||
import { SelectItem } from '@/components/Common/SelectInputSplitField/SelectInputSplitField';
|
||||
import { TableColumn } from '@/features/DataSource';
|
||||
import { createHistory } from 'history';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { AnyAction } from 'redux';
|
||||
import { ReduxState } from '@/types';
|
||||
import { TableSchema } from '@/components/Services/Data/TableBrowseRows/utils';
|
||||
import { Integers, Reals } from '../../../components/Services/Data/constants';
|
||||
import { vMakeTableRequests } from '../../../components/Services/Data/TableBrowseRows/ViewActions';
|
||||
import { sortPlaceholder } from './FiltersSection';
|
||||
import { FiltersAndSortFormValues, OrderCondition, UserQuery } from './types';
|
||||
|
||||
export const columnPlaceholder = '-- column --';
|
||||
|
||||
const convertArray = (arrayString: string): number[] | string[] =>
|
||||
JSON.parse(arrayString);
|
||||
|
||||
const convertValue = (
|
||||
value: string | string[] | number[],
|
||||
tableColumnType: string
|
||||
) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (tableColumnType === 'integer') {
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
|
||||
if (tableColumnType === 'boolean') {
|
||||
return Boolean(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export const adaptFormValuesToQuery = (
|
||||
formValues: FiltersAndSortFormValues,
|
||||
columnDataTypes: TableColumn[]
|
||||
): UserQuery => {
|
||||
const where =
|
||||
formValues?.filter?.map(filter => {
|
||||
const columnDataType = columnDataTypes.find(
|
||||
col => col.name === filter.column
|
||||
);
|
||||
|
||||
let partialValue: string | string[] | number[] = filter.value;
|
||||
if (filter.operator === '$in' || filter.operator === '$nin') {
|
||||
partialValue = convertArray(filter.value);
|
||||
}
|
||||
|
||||
const value = !columnDataType
|
||||
? filter.value
|
||||
: convertValue(partialValue, columnDataType.dataType);
|
||||
|
||||
return {
|
||||
[filter.column]: {
|
||||
[filter.operator]: value,
|
||||
},
|
||||
};
|
||||
}) ?? [];
|
||||
|
||||
const orderBy =
|
||||
formValues?.sort?.map(order => {
|
||||
const orderCondition: OrderCondition = {
|
||||
column: order.column,
|
||||
type: order.order,
|
||||
nulls: 'last',
|
||||
};
|
||||
return orderCondition;
|
||||
}) ?? [];
|
||||
|
||||
return {
|
||||
where: {
|
||||
$and: where,
|
||||
},
|
||||
order_by: orderBy,
|
||||
};
|
||||
};
|
||||
|
||||
type SetParamsArgs = {
|
||||
filters: string[];
|
||||
sorts: string[];
|
||||
};
|
||||
|
||||
const setParams = (query: SetParamsArgs = { filters: [], sorts: [] }) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
query.filters.forEach(filter => searchParams.append('filter', filter));
|
||||
query.sorts.forEach(sort => searchParams.append('sort', sort));
|
||||
return searchParams.toString();
|
||||
};
|
||||
|
||||
export const setUrlParams = (
|
||||
whereAnd: UserQuery['where']['$and'],
|
||||
orderBy: UserQuery['order_by']
|
||||
) => {
|
||||
const sorts = orderBy
|
||||
.filter(order => order.column)
|
||||
.map(order => `${order.column};${order.type}`);
|
||||
const filters = whereAnd
|
||||
.filter(
|
||||
where => Object.keys(where).length === 1 && Object.keys(where)[0] !== ''
|
||||
)
|
||||
.map(where => {
|
||||
const col = Object.keys(where)[0];
|
||||
const op = Object.keys(where[col])[0];
|
||||
const value = where[col][op];
|
||||
return `${col};${op};${value}`;
|
||||
});
|
||||
const url = setParams({ filters, sorts });
|
||||
const history = createHistory();
|
||||
|
||||
history.push({
|
||||
pathname: history.getCurrentLocation().pathname,
|
||||
search: `?${url}`,
|
||||
});
|
||||
};
|
||||
|
||||
const parseArray = (val: string | number | boolean | string[] | number[]) => {
|
||||
if (Array.isArray(val)) return val;
|
||||
if (typeof val === 'string') {
|
||||
try {
|
||||
return JSON.parse(val);
|
||||
} catch (err) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return val;
|
||||
};
|
||||
|
||||
const defaultColumns: SelectItem[] = [
|
||||
{
|
||||
label: columnPlaceholder,
|
||||
value: columnPlaceholder,
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const getColumns = (columns: string[]) => {
|
||||
const columnsSelectItems: SelectItem[] = columns.map((columnName: string) => {
|
||||
return {
|
||||
label: columnName,
|
||||
value: columnName,
|
||||
};
|
||||
});
|
||||
|
||||
return defaultColumns.concat(columnsSelectItems);
|
||||
};
|
||||
|
||||
export const filterValidUserQuery = (userQuery: UserQuery): UserQuery => {
|
||||
const filteredWhere = userQuery.where.$and.filter(
|
||||
condition => Object.keys(condition)[0] !== columnPlaceholder
|
||||
);
|
||||
const filteredOrderBy = userQuery.order_by.filter(
|
||||
order => order.type !== sortPlaceholder
|
||||
);
|
||||
return {
|
||||
...userQuery,
|
||||
where: { $and: filteredWhere },
|
||||
order_by: filteredOrderBy,
|
||||
};
|
||||
};
|
||||
|
||||
type RunFilterQuery = {
|
||||
tableSchema: TableSchema;
|
||||
whereAnd: UserQuery['where']['$and'];
|
||||
orderBy: UserQuery['order_by'];
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
export const runFilterQuery =
|
||||
({ tableSchema, whereAnd, orderBy, limit, offset }: RunFilterQuery) =>
|
||||
(dispatch: ThunkDispatch<ReduxState, unknown, AnyAction>) => {
|
||||
const whereClauses = whereAnd.filter(w => {
|
||||
const colName = Object.keys(w)[0].trim();
|
||||
if (colName === '') {
|
||||
return false;
|
||||
}
|
||||
const opName = Object.keys(w[colName])[0].trim();
|
||||
if (opName === '') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const finalWhereClauses = whereClauses.map(whereClause => {
|
||||
const colName = Object.keys(whereClause)[0];
|
||||
const opName = Object.keys(whereClause[colName])[0];
|
||||
const val = whereClause[colName][opName];
|
||||
|
||||
if (['$in', '$nin'].includes(opName)) {
|
||||
whereClause[colName][opName] = parseArray(val);
|
||||
return whereClause;
|
||||
}
|
||||
|
||||
const colType =
|
||||
tableSchema?.columns?.find(column => column.column_name === colName)
|
||||
?.data_type || '';
|
||||
|
||||
if (Integers.indexOf(colType) > 0 && typeof val === 'string') {
|
||||
whereClause[colName][opName] = parseInt(val, 10);
|
||||
return whereClause;
|
||||
}
|
||||
if (Reals.indexOf(colType) > 0 && typeof val === 'string') {
|
||||
whereClause[colName][opName] = parseFloat(val);
|
||||
return whereClause;
|
||||
}
|
||||
if (colType === 'boolean') {
|
||||
if (val === 'true') {
|
||||
whereClause[colName][opName] = true;
|
||||
return whereClause;
|
||||
}
|
||||
if (val === 'false') {
|
||||
whereClause[colName][opName] = false;
|
||||
return whereClause;
|
||||
}
|
||||
}
|
||||
return whereClause;
|
||||
});
|
||||
const newQuery = {
|
||||
where: { $and: finalWhereClauses },
|
||||
limit,
|
||||
offset,
|
||||
order_by: orderBy.filter(w => w.column.trim() !== ''),
|
||||
};
|
||||
dispatch({ type: 'ViewTable/V_SET_QUERY_OPTS', queryStuff: newQuery });
|
||||
dispatch(vMakeTableRequests());
|
||||
};
|
@ -3,11 +3,11 @@ import { SelectItem } from '@/components/Common/SelectInputSplitField/SelectInpu
|
||||
import { Button } from '@/new-components/Button';
|
||||
import { FieldArrayWithId } from 'react-hook-form';
|
||||
import { SortItem, SortRow } from './SortRow';
|
||||
import { FormValues } from './types';
|
||||
import { FiltersAndSortFormValues } from './types';
|
||||
|
||||
export type SortRowsProps = {
|
||||
columns: SelectItem[];
|
||||
fields: FieldArrayWithId<FormValues, 'sort', 'id'>[];
|
||||
fields: FieldArrayWithId<FiltersAndSortFormValues, 'sort', 'id'>[];
|
||||
onAdd: () => void;
|
||||
onRemove: (index: number) => void;
|
||||
orders: SortItem[];
|
||||
|
@ -1,10 +1,14 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useFieldArray, UseFormReturn } from 'react-hook-form';
|
||||
import { OperatorItem } from '../FilterRow';
|
||||
import { defaultColumn, defaultOperator, FormValues } from '../types';
|
||||
import {
|
||||
defaultColumn,
|
||||
defaultOperator,
|
||||
FiltersAndSortFormValues,
|
||||
} from '../types';
|
||||
|
||||
type UseFilterRowsProps = {
|
||||
methods: UseFormReturn<FormValues, any>;
|
||||
methods: UseFormReturn<FiltersAndSortFormValues, any>;
|
||||
operators: OperatorItem[];
|
||||
};
|
||||
|
||||
@ -22,41 +26,38 @@ export const useFilterRows = ({ methods, operators }: UseFilterRowsProps) => {
|
||||
const [showFirstRemove, setShowFirstRemove] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch(
|
||||
(formValues, { name: fieldName, type: eventType }) => {
|
||||
console.log(formValues, fieldName, eventType);
|
||||
const rowId = getRowIndex(fieldName || '');
|
||||
const subscription = watch((formValues, { name: fieldName }) => {
|
||||
const rowId = getRowIndex(fieldName || '');
|
||||
|
||||
if (
|
||||
rowId === 0 &&
|
||||
fieldName?.endsWith('.column') &&
|
||||
getValues(fieldName) !== defaultColumn
|
||||
) {
|
||||
setShowFirstRemove(true);
|
||||
}
|
||||
|
||||
if (
|
||||
rowId === 0 &&
|
||||
fieldName?.endsWith('.value') &&
|
||||
getValues(fieldName) !== ''
|
||||
) {
|
||||
setShowFirstRemove(true);
|
||||
}
|
||||
|
||||
if (fieldName?.endsWith('.operator')) {
|
||||
const operatorValue = getValues(fieldName);
|
||||
const operatorDefinition = operators.find(
|
||||
op => op.value === operatorValue
|
||||
);
|
||||
setValue(
|
||||
`filter.${rowId}.value`,
|
||||
operatorDefinition?.defaultValue || ''
|
||||
);
|
||||
}
|
||||
if (
|
||||
rowId === 0 &&
|
||||
fieldName?.endsWith('.column') &&
|
||||
getValues(fieldName) !== defaultColumn
|
||||
) {
|
||||
setShowFirstRemove(true);
|
||||
}
|
||||
);
|
||||
|
||||
if (
|
||||
rowId === 0 &&
|
||||
fieldName?.endsWith('.value') &&
|
||||
getValues(fieldName) !== ''
|
||||
) {
|
||||
setShowFirstRemove(true);
|
||||
}
|
||||
|
||||
if (fieldName?.endsWith('.operator')) {
|
||||
const operatorValue = getValues(fieldName);
|
||||
const operatorDefinition = operators.find(
|
||||
op => op.value === operatorValue
|
||||
);
|
||||
setValue(
|
||||
`filter.${rowId}.value`,
|
||||
operatorDefinition?.defaultValue || ''
|
||||
);
|
||||
}
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [watch]);
|
||||
}, [watch, operators]);
|
||||
|
||||
const onAdd = () =>
|
||||
append({
|
@ -1,9 +1,13 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { UseFormReturn, useFieldArray } from 'react-hook-form';
|
||||
import { defaultColumn, defaultOrder, FormValues } from '../types';
|
||||
import {
|
||||
defaultColumn,
|
||||
defaultOrder,
|
||||
FiltersAndSortFormValues,
|
||||
} from '../types';
|
||||
|
||||
type UseSortRowsProps = {
|
||||
methods: UseFormReturn<FormValues, any>;
|
||||
methods: UseFormReturn<FiltersAndSortFormValues, any>;
|
||||
};
|
||||
|
||||
export const useSortRows = ({ methods }: UseSortRowsProps) => {
|
||||
@ -41,7 +45,7 @@ export const useSortRows = ({ methods }: UseSortRowsProps) => {
|
||||
}
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [watch]);
|
||||
}, [watch, getValues]);
|
||||
|
||||
const onRemove = (index: number) => {
|
||||
if (index > 0) {
|
@ -0,0 +1,37 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { DataSource, TableColumn } from '@/features/DataSource';
|
||||
import { useHttpClient } from '@/features/Network';
|
||||
import { useIsUnmounted } from '@/components/Services/Data';
|
||||
|
||||
type UseTableColumnsProps = {
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
};
|
||||
|
||||
export const useTableColumns = ({
|
||||
dataSourceName,
|
||||
table,
|
||||
}: UseTableColumnsProps) => {
|
||||
const httpClient = useHttpClient();
|
||||
const [tableColumns, setTableColumns] = useState<TableColumn[]>([]);
|
||||
const isUnMounted = useIsUnmounted();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchTableColumns() {
|
||||
const tableColumnDefinitions = await DataSource(
|
||||
httpClient
|
||||
).getTableColumns({
|
||||
dataSourceName,
|
||||
table,
|
||||
});
|
||||
|
||||
if (isUnMounted()) {
|
||||
return;
|
||||
}
|
||||
setTableColumns(tableColumnDefinitions);
|
||||
}
|
||||
fetchTableColumns();
|
||||
}, [dataSourceName, httpClient, table, isUnMounted]);
|
||||
|
||||
return tableColumns;
|
||||
};
|
@ -0,0 +1,34 @@
|
||||
import { useIsUnmounted } from '@/components/Services/Data';
|
||||
import { getTableName } from '@/features/Data';
|
||||
import { DataSource } from '@/features/DataSource';
|
||||
import { useHttpClient } from '@/features/Network';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
type UseTableNameProps = {
|
||||
dataSourceName: string;
|
||||
table: unknown;
|
||||
};
|
||||
|
||||
export const useTableName = ({ dataSourceName, table }: UseTableNameProps) => {
|
||||
const httpClient = useHttpClient();
|
||||
const [tableName, setTableName] = useState('');
|
||||
const isUnMounted = useIsUnmounted();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchTableHierarchy() {
|
||||
const databaseHierarchy = await DataSource(
|
||||
httpClient
|
||||
).getDatabaseHierarchy({ dataSourceName });
|
||||
|
||||
if (isUnMounted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const aTableName = getTableName(table, databaseHierarchy);
|
||||
setTableName(aTableName);
|
||||
}
|
||||
fetchTableHierarchy();
|
||||
}, [dataSourceName, table, httpClient, isUnMounted]);
|
||||
|
||||
return tableName;
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
import { NormalizedTable } from '@/dataSources/types';
|
||||
import { useAppSelector } from '@/store';
|
||||
|
||||
// NOTE: temporary type, to be replaced when GDC is ready
|
||||
export type Table = { schema: string; name: string };
|
||||
|
||||
export const useTableSchema = (table: Table) => {
|
||||
const tableSchema = useAppSelector(state =>
|
||||
state.tables.allSchemas.find((schema: NormalizedTable) => {
|
||||
return (
|
||||
schema.table_schema === table?.schema &&
|
||||
schema.table_name === table?.name
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
return tableSchema;
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
export type FormValues = {
|
||||
export type FiltersAndSortFormValues = {
|
||||
filter: {
|
||||
column: string;
|
||||
operator: string;
|
||||
@ -6,10 +6,31 @@ export type FormValues = {
|
||||
}[];
|
||||
sort: {
|
||||
column: string;
|
||||
order: string;
|
||||
order: 'asc' | 'desc' | '--';
|
||||
}[];
|
||||
};
|
||||
|
||||
export const defaultColumn = '-- column --';
|
||||
export const defaultOperator = '$eq';
|
||||
export const defaultOrder = '--';
|
||||
|
||||
type ColumnName = string;
|
||||
|
||||
type OperatorType = string; // TODO: find the list of possible operators
|
||||
type OperatorCondition = Record<
|
||||
OperatorType,
|
||||
string | number | number[] | string[] | boolean
|
||||
>;
|
||||
|
||||
export type WhereCondition = Record<ColumnName, OperatorCondition>;
|
||||
|
||||
export type OrderCondition = {
|
||||
column: string;
|
||||
type: 'desc' | 'asc' | '--';
|
||||
nulls: 'last';
|
||||
};
|
||||
|
||||
export type UserQuery = {
|
||||
where: Record<'$and', WhereCondition[]>;
|
||||
order_by: OrderCondition[];
|
||||
};
|
||||
|
@ -0,0 +1,3 @@
|
||||
export { runFilterQuery } from './FiltersSection/FiltersSectionContainer.utils';
|
||||
|
||||
export type { UserQuery } from './FiltersSection/types';
|
@ -1,3 +1,4 @@
|
||||
export * from './ManageContainer';
|
||||
export * from './components';
|
||||
export * from './hooks';
|
||||
export { getTableName } from './TrackTables/hooks/useTables';
|
||||
|
@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { persistPageSizeChange } from '@/components/Services/Data/TableBrowseRows/tableUtils';
|
||||
import { runFilterQuery } from '@/features/BrowseRows';
|
||||
import type { UserQuery } from '@/features/BrowseRows';
|
||||
import { useAppDispatch } from '@/store';
|
||||
import { TableSchema } from '@/components/Services/Data/TableBrowseRows/utils';
|
||||
import {
|
||||
setLimit,
|
||||
setOffset,
|
||||
} from '../../components/Services/Data/TableBrowseRows/FilterActions';
|
||||
import {
|
||||
PaginationWithOnlyNav,
|
||||
PaginationWithOnlyNavProps,
|
||||
} from './PaginationWithOnlyNav';
|
||||
|
||||
type PaginationWithOnlyNavContainerProps = {
|
||||
limit: PaginationWithOnlyNavProps['limit'];
|
||||
offset: PaginationWithOnlyNavProps['offset'];
|
||||
onChangePage: PaginationWithOnlyNavProps['changePage'];
|
||||
onChangePageSize: PaginationWithOnlyNavProps['changePageSize'];
|
||||
pageSize: number;
|
||||
rows: PaginationWithOnlyNavProps['rows'];
|
||||
tableSchema: TableSchema;
|
||||
userQuery: UserQuery;
|
||||
};
|
||||
|
||||
export const PaginationWithOnlyNavContainer = ({
|
||||
limit,
|
||||
offset,
|
||||
onChangePage,
|
||||
onChangePageSize,
|
||||
pageSize,
|
||||
rows,
|
||||
tableSchema,
|
||||
userQuery,
|
||||
}: PaginationWithOnlyNavContainerProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const changePageHandler = (newPage: number) => {
|
||||
if (offset !== newPage * limit) {
|
||||
const newOffset = newPage * limit;
|
||||
dispatch(setOffset(newPage * limit));
|
||||
dispatch(
|
||||
runFilterQuery({
|
||||
tableSchema,
|
||||
whereAnd: userQuery.where.$and,
|
||||
orderBy: userQuery.order_by,
|
||||
limit,
|
||||
offset: newOffset,
|
||||
})
|
||||
);
|
||||
onChangePage(newPage);
|
||||
}
|
||||
};
|
||||
|
||||
const changePageSizeHandler = (newPageSize: number) => {
|
||||
if (pageSize !== newPageSize) {
|
||||
dispatch(setLimit(newPageSize));
|
||||
dispatch(setOffset(0));
|
||||
dispatch(
|
||||
runFilterQuery({
|
||||
tableSchema,
|
||||
whereAnd: userQuery.where.$and,
|
||||
orderBy: userQuery.order_by,
|
||||
limit: newPageSize,
|
||||
offset: 0,
|
||||
})
|
||||
);
|
||||
persistPageSizeChange(newPageSize);
|
||||
onChangePageSize(newPageSize);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PaginationWithOnlyNav
|
||||
offset={offset}
|
||||
limit={limit}
|
||||
changePage={changePageHandler}
|
||||
changePageSize={changePageSizeHandler}
|
||||
rows={rows}
|
||||
/>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user