Skip to content

Commit

Permalink
Refactor EuiInMemoryTable's cell rendering methods and execute_ast (#915
Browse files Browse the repository at this point in the history
)

* Refactor EuiInMemoryTable's cell rendering methods to reduce indirection around the render and align props.
* Destructure column prop when rendering cells to avoid passing through unexpected properties via the rest element.
* Deduplicate logic shared between renderItemFieldDataCell and renderItemComputedCell methods.
* Refactor execute_ast for clarity.
  • Loading branch information
cjcenizal authored Jun 11, 2018
1 parent 6cc98e2 commit 140157e
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 113 deletions.
143 changes: 71 additions & 72 deletions src/components/basic_table/basic_table.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,54 +412,66 @@ export class EuiBasicTable extends Component {
}

columns.forEach((column, index) => {
const {
actions,
width,
name,
field,
align,
dataType,
sortable,
isMobileHeader,
hideForMobile,
} = column;

const columnAlign = align || this.getAlignForDataType(dataType);

// actions column
if (column.actions) {
if (actions) {
headers.push(
<EuiTableHeaderCell
key={`_actions_h_${index}`}
align="right"
width={column.width}
width={width}
>
{column.name}
{name}
</EuiTableHeaderCell>
);
return;
}

const align = this.resolveColumnAlign(column);

// computed column
if (!column.field) {
if (!field) {
headers.push(
<EuiTableHeaderCell
key={`_computed_column_h_${index}`}
align={align}
width={column.width}
align={columnAlign}
width={width}
>
{column.name}
{name}
</EuiTableHeaderCell>
);
return;
}

// field data column
const sorting = {};
if (this.props.sorting && column.sortable) {
if (this.props.sorting && sortable) {
const sortDirection = this.resolveColumnSortDirection(column);
sorting.isSorted = !!sortDirection;
sorting.isSortAscending = sortDirection ? SortDirection.isAsc(sortDirection) : undefined;
sorting.onSort = this.resolveColumnOnSort(column);
}
headers.push(
<EuiTableHeaderCell
key={`_data_h_${column.field}_${index}`}
align={align}
width={column.width}
isMobileHeader={column.isMobileHeader}
hideForMobile={column.hideForMobile}
key={`_data_h_${field}_${index}`}
align={columnAlign}
width={width}
isMobileHeader={isMobileHeader}
hideForMobile={hideForMobile}
{...sorting}
>
{column.name}
{name}
</EuiTableHeaderCell>
);
});
Expand Down Expand Up @@ -656,88 +668,71 @@ export class EuiBasicTable extends Component {

const key = `record_actions_${itemId}_${columnIndex}`;
return (
<EuiTableRowCell showOnHover={true} key={key} align="right" textOnly={false} hasActions={true} >
<EuiTableRowCell
showOnHover={true}
key={key}
align="right"
textOnly={false}
hasActions={true}
>
{tools}
</EuiTableRowCell>
);
}

renderItemFieldDataCell(itemId, item, column, columnIndex) {
const {
field,
render,
textOnly,
name, // eslint-disable-line no-unused-vars
description, // eslint-disable-line no-unused-vars
dataType, // eslint-disable-line no-unused-vars
sortable, // eslint-disable-line no-unused-vars
...rest
} = column;
const { field, render, dataType } = column;

const key = `_data_column_${field}_${itemId}_${columnIndex}`;
const align = this.resolveColumnAlign(column);
const contentRenderer = render || this.getRendererForDataType(dataType);
const value = get(item, field);
const contentRenderer = this.resolveContentRenderer(column);
const content = contentRenderer(value, item);

const { cellProps: cellPropsCallback } = this.props;
const cellProps = getCellProps(item, column, cellPropsCallback);

return (
<EuiTableRowCell
key={key}
align={align}
header={column.name}
// If there's no render function defined then we're only going to render text.
textOnly={textOnly || !render}
{...cellProps}
{...rest}
>
{content}
</EuiTableRowCell>
);
return this.renderItemCell(item, column, key, content);
}

renderItemComputedCell(itemId, item, column, columnIndex) {
const { render, dataType } = column;

const key = `_computed_column_${itemId}_${columnIndex}`;
const contentRenderer = render || this.getRendererForDataType(dataType);
const content = contentRenderer(item);

return this.renderItemCell(item, column, key, content);
}

renderItemCell(item, column, key, content) {
const {
align,
render,
dataType,
isExpander,
name,
textOnly,
field, // eslint-disable-line no-unused-vars
render, // eslint-disable-line no-unused-vars
name, // eslint-disable-line no-unused-vars
description, // eslint-disable-line no-unused-vars
dataType, // eslint-disable-line no-unused-vars
sortable, // eslint-disable-line no-unused-vars
...rest
} = column;
const columnAlign = align || this.getAlignForDataType(dataType);
const { cellProps: cellPropsCallback } = this.props;
const cellProps = getCellProps(item, column, cellPropsCallback);

const key = `_computed_column_${itemId}_${columnIndex}`;
const align = this.resolveColumnAlign(column);
const contentRenderer = this.resolveContentRenderer(column);
const content = contentRenderer(item);
return (
<EuiTableRowCell
key={key}
align={align}
header={column.name}
isExpander={column.isExpander}
align={columnAlign}
header={name}
isExpander={isExpander}
textOnly={textOnly || !render}
{...cellProps}
{...rest}
>
{content}
</EuiTableRowCell>
);
}

resolveColumnAlign(column) {
if (column.align) {
return column.align;
}
const dataType = column.dataType || 'auto';
const profile = dataTypesProfiles[dataType];
if (!profile) {
throw new Error(`Unknown dataType [${dataType}]. The supported data types are [${DATA_TYPES.join(', ')}]`);
}
return profile.align;
}

resolveColumnSortDirection = (column) => {
const { sorting } = this.props;
if (!sorting || !sorting.sort || !column.sortable) {
Expand All @@ -760,18 +755,22 @@ export class EuiBasicTable extends Component {
return () => this.onColumnSortChange(column);
}

resolveContentRenderer(column) {
if (column.render) {
return column.render;
}
const dataType = column.dataType || 'auto';
getRendererForDataType(dataType = 'auto') {
const profile = dataTypesProfiles[dataType];
if (!profile) {
throw new Error(`Unknown dataType [${dataType}]. The supported data types are [${DATA_TYPES.join(', ')}]`);
}
return profile.render;
}

getAlignForDataType(dataType = 'auto') {
const profile = dataTypesProfiles[dataType];
if (!profile) {
throw new Error(`Unknown dataType [${dataType}]. The supported data types are [${DATA_TYPES.join(', ')}]`);
}
return profile.align;
}

renderPaginationBar() {
const { error, pagination, onChange } = this.props;
if (!error && pagination) {
Expand Down
104 changes: 63 additions & 41 deletions src/components/search_bar/query/execute_ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ import { AST } from './ast';

const EXPLAIN_FIELD = '__explain';

const operators = {
const nameToOperatorMap = {
[AST.Operator.EQ]: eq,
[AST.Operator.GT]: gt,
[AST.Operator.GTE]: gte,
[AST.Operator.LT]: lt,
[AST.Operator.LTE]: lte
[AST.Operator.LTE]: lte,
};

const defaultIsClauseMatcher = (record, clause, explain) => {
const defaultIsClauseMatcher = (item, clause, explain) => {
const { type, flag, match } = clause;
const value = get(record, clause.flag);
const value = get(item, clause.flag);
const must = AST.Match.isMustClause(clause);
const hit = !!value === must;
if (explain && hit) {
Expand All @@ -24,81 +24,103 @@ const defaultIsClauseMatcher = (record, clause, explain) => {
return hit;
};

const fieldClauseMatcher = (record, field, clauses = [], explain) => {
const fieldClauseMatcher = (item, field, clauses = [], explain) => {
return clauses.every(clause => {
const { type, value, match } = clause;
let operator = operators[clause.operator];
let operator = nameToOperatorMap[clause.operator];
if (!operator) { // unknown matcher
return true;
}
if (!AST.Match.isMust(match)) {
operator = (value, token) => !operators[clause.operator](value, token);
operator = (value, token) => !nameToOperatorMap[clause.operator](value, token);
}
const recordValue = get(record, field);
const itemValue = get(item, field);
const hit = isArray(value) ?
value.some(v => operator(recordValue, v)) :
operator(recordValue, value);
value.some(v => operator(itemValue, v)) :
operator(itemValue, value);
if (explain && hit) {
explain.push({ hit, type, field, value, match, operator });
}
return hit;
});
};

const resolveStringFields = (record) => {
return Object.keys(record).reduce((fields, key) => {
if (isString(record[key])) {
const extractStringFieldsFromItem = (item) => {
return Object.keys(item).reduce((fields, key) => {
if (isString(item[key])) {
fields.push(key);
}
return fields;
}, []);
};

const termClauseMatcher = (record, fields, clauses = [], explain) => {
fields = fields || resolveStringFields(record);
const termClauseMatcher = (item, fields, clauses = [], explain) => {
const searchableFields = fields || extractStringFieldsFromItem(item);
return clauses.every(clause => {
const { type, value, match } = clause;
const operator = operators[AST.Operator.EQ];
if (AST.Match.isMustClause(clause)) {
return fields.some(field => {
const recordValue = get(record, field);
const hit = operator(recordValue, value);
if (explain && hit) {
const isMustClause = AST.Match.isMustClause(clause);
const equals = nameToOperatorMap[AST.Operator.EQ];

const containsMatches = searchableFields.some(field => {
const itemValue = get(item, field);
const isMatch = equals(itemValue, value);

if (explain) {
// If testing for the presence of a term, then we record a match as a match.
// If testing for the absence of a term, then we invert this logic: we record a
// non-match as a match.
const hit = (isMustClause && isMatch) || (!isMustClause && !isMatch);
if (hit) {
explain.push({ hit, type, field, match, value });
}
return hit;
});
} else {
const notMatcher = (value, token) => !operator(value, token);
return fields.every(field => {
const recordValue = get(record, field);
const hit = notMatcher(recordValue, value);
if (explain && hit) {
explain.push({ hit, type, field, value, match });
}
return hit;
});
}

return isMatch;
});

if (isMustClause) {
// If we're testing for the presence of a term, then we only need 1 field to match.
return containsMatches;
}

// If we're testing for the absence of a term, we can't have any matching fields at all.
return !containsMatches;
});
};

export const createFilter = (ast, defaultFields, isClauseMatcher = defaultIsClauseMatcher, explain = false) => {
return (record) => {
// Return items which pass ALL conditions: matches the terms entered, the specified field values,
// and the specified "is" clauses.
return (item) => {
const explainLines = explain ? [] : undefined;

if (explainLines) {
item[EXPLAIN_FIELD] = explainLines;
}

const termClauses = ast.getTermClauses();
const fields = ast.getFieldNames();
const isClauses = ast.getIsClauses();
const match = termClauseMatcher(record, defaultFields, termClauses, explainLines) &&
fields.every(field => fieldClauseMatcher(record, field, ast.getFieldClauses(field), explainLines)) &&
isClauses.every(clause => isClauseMatcher(record, clause, explainLines));
if (explainLines) {
record[EXPLAIN_FIELD] = explainLines;

const isTermMatch = termClauseMatcher(item, defaultFields, termClauses, explainLines);
if (!isTermMatch) {
return false;
}

const isFieldsMatch = fields.every(field => fieldClauseMatcher(item, field, ast.getFieldClauses(field), explainLines));
if (!isFieldsMatch) {
return false;
}
return match;

const isIsMatch = isClauses.every(clause => isClauseMatcher(item, clause, explainLines));
if (!isIsMatch) {
return false;
}

return true;
};
};


export const executeAst = (ast, items, options = {}) => {
const { isClauseMatcher, defaultFields, explain } = options;
const filter = createFilter(ast, defaultFields, isClauseMatcher, explain);
Expand Down

0 comments on commit 140157e

Please sign in to comment.