Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: implement schema browser as a react component #3140

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions client/app/assets/less/ant.less
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,34 @@
.ant-popover {
z-index: 1000; // make sure it doesn't cover drawer
}

// schema browser table expand icon override
.schema-container {
.schema-browser-title-item .copy-arrow {
display: none;
position: absolute;
right: 5px;
top: 5px;
cursor: pointer;

svg {
transform: scale(0.6);
}
}
.table-open {
display: none;

.copyArrow {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kebab-case

top: 2px;
}
}
.schema-table-expanded .table-open {
display: block;
}
.table-name:hover,
.table-open:hover {
.copy-arrow {
display: block;
}
}
}
4 changes: 2 additions & 2 deletions client/app/components/dynamic-table/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ function createRowRenderTemplate(columns, $compile) {
switch (column.displayAs) {
case 'json':
return `
<dynamic-table-json-cell column="columns[${index}]"
<dynamic-table-json-cell column="columns[${index}]"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it somehow related to this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An earlier linter was whining about this though it is otherwise unrelated. Will gladly remove the whitespace change if needed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. What errors did you get from linter?

Copy link
Collaborator

@kravets-levko kravets-levko Mar 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping 🙂 I'm really wondering why there are any codestyle warnings for this code - because that's string literal and all linters shouldn't touch it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am probably not remembering right. I think (in november of last year) there was something the linter required here (possibly unused import or something) and after I made the change my code editor removed the trailing whitespace. I will go into vi and re-add it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which editor do you use? Because my one also removes trailing whitespaces, but keeps in multiline string literals (because in general case it's unsafe to remove them)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And while it's not so important, but I think you should revert this - because the entire file is not related to changes declared in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep agreed.

value="row[columns[${index}].name]"></dynamic-table-json-cell>
`;
default:
return `
<dynamic-table-default-cell column="columns[${index}]"
<dynamic-table-default-cell column="columns[${index}]"
row="row"></dynamic-table-default-cell>
`;
}
Expand Down
135 changes: 135 additions & 0 deletions client/app/components/queries/SchemaBrowser.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React from 'react';
import { react2angular } from 'react2angular';
import PropTypes from 'prop-types';
import { Icon } from 'antd';
import { debounce } from 'lodash';
import { $rootScope } from '@/services/ng';

import { Schema } from '../proptypes';

const isSchemaEmpty = schema => schema === undefined || !schema.length;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isArray(schema) && (schema.length > 0):

  1. it's self-explaining: we expect that schema is an array with non-zero length.
  2. it's more reliable (what if schema will be null? object?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about:
const isSchemaValid = schema => schema.constructor === Array && schema.length;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isArray === schema.constructor === Array, but it's shorter and literally means what we need to check - that argument "is array" (vs. "argument's constructor is equal to Array") - that's rather a question of semantics.


// Return true if the search term is contained within the table name
// or any of the table's columns.
const doesTableContainSearchTerm = (searchTerm, table) => {
const regEx = new RegExp(searchTerm, 'gi');

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more notice about this function: avoid using regex, especially in this form. First of all, it allows users to use arbitrary regex to filter schema, as well as it breaks previous behavior. Also, using g modifier makes regex "sticky" - it will remember last position where match was found, and next call to test will start matching from that position.

// If the table name matches the search term we can return early.
if (regEx.test(table.name)) return true;

return !!table.columns.filter(column => regEx.test(column)).length;
};

const doCopy = (evt, hierarchy) => {
// eslint-disable-next-line
$rootScope.$broadcast('query-editor.command', 'paste', hierarchy.join('.'));
evt.stopPropagation();
};

class SchemaBrowser extends React.Component {
static propTypes = {
schema: Schema, // eslint-disable-line react/no-unused-prop-types
doRefresh: PropTypes.func.isRequired,
};

static defaultProps = {
schema: null,
};

constructor(props) {
super(props);

this.state = {
searchTerm: '',
schema: [],
schemaInitialized: false,
};

// Candidate to be abstracted out to a set of config constants.
// Performs search filtering after the number of milliseconds set below.
this.doSearch = debounce(this.doSearch, 150);
}

componentDidUpdate(prevProps, prevState) {
if (prevProps.schema && (this.props.schema && prevState.schema.length !== this.props.schema.length)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On query page, $scope.schema is set to a new array each time it's updated. So this check could be simplified to prevProps.schema !== this.props.schema. Also, it's the only condition needed - component should be ready that this prop may change - in this case filter should be applied to new schema.

// setState() is fine here within the right conditions.
// eslint-disable-next-line
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use this with a list of rules you want to disable

if (!this.state.schemaInitialized) this.setState({ schema: this.props.schema });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the line where eslint should fail (we require block even for a single statement).

}
}

handleSearch = (evt) => {
if (!evt.target.value) {
this.setState({ schema: this.props.schema, searchTerm: '' });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

evt.target.value is always string; here you should just update state and call doSearch, which should properly handle empty search term (return entire schema).

return;
}
this.setState({ searchTerm: evt.target.value });
this.doSearch(evt.target.value);
}

doSearch = (searchTerm) => {
const schema = this.props.schema.filter(table => doesTableContainSearchTerm(searchTerm, table));
this.setState({ searchTerm, schema });
Copy link
Collaborator

@kravets-levko kravets-levko Mar 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems you don't need handleSearch at all. This function should look line this:

doSearch = (searchTerm) => {
  let { schema } = this.props;
  if (searchTerm !== '') {
    schema = filter(schema, table => doesTableContainSearchTerm(searchTerm, table));
  }
  this.setState({ searchTerm, schema });
}

Then use it directly with input: onChange={event => this.doSearch(event.target.value)}.

Also, move debounce call here as well.

}

getTitleElement = (title, isTableRow) => (
<span className="schema-browser-title-item">
{isTableRow && <Icon type="table" />}
<span>{` ${title} `}</span>
<Icon
onClick={(evt) => { doCopy(evt, [title]); }}
aria-hidden="true"
type="double-right"
className="copy-arrow"
/>
</span>
);

doRowToggle = ({ target }) => {
target.closest('.table-item').classList.toggle('schema-table-expanded');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be implemented in more "native" way: create a component that will represent a single table. That component will accept table name and columns via props (also add other necessary props), and will have the only state field - isExpanded. Also, it's a good case for using React hooks (useState).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW - with this approach you can even remove css class, and just conditionally render columns only when table is expanded.

}

render() {
if (!this.props.schema) {
return null;
}
return (
<div className="schema-container">
<div className="schema-control">
<input
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Input and Button components from Ant

type="text"
placeholder="Search schema..."
className="form-control"
disabled={isSchemaEmpty(this.props.schema)}
value={this.state.searchTerm}
onChange={this.handleSearch}
/>
<button
className="btn btn-default"
title="Refresh Schema"
type="button"
onClick={this.props.doRefresh}
>
<span className="zmdi zmdi-refresh" />
</button>
</div>
<div className="schema-browser">
{this.state.schema.map(table => (
<div key={table.name} className="table-item">
<div className="table-name" onClick={this.doRowToggle}>{this.getTitleElement(table.name, true)}</div>
{table.columns.map(column => (
<div className="table-open" key={column}>{this.getTitleElement(column)}</div>
))}
</div>
))}
</div>
</div>
);
}
}

export default function init(ngModule) {
ngModule.component('schemaBrowser', react2angular(SchemaBrowser));
}

init.init = true;
30 changes: 0 additions & 30 deletions client/app/components/queries/schema-browser.html

This file was deleted.

57 changes: 0 additions & 57 deletions client/app/components/queries/schema-browser.js

This file was deleted.

2 changes: 0 additions & 2 deletions client/app/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import uiSelect from 'ui-select';
import ngMessages from 'angular-messages';
import toastr from 'angular-toastr';
import ngUpload from 'angular-base64-upload';
import vsRepeat from 'angular-vs-repeat';
import 'brace';
import 'angular-ui-ace';
import 'angular-resizable';
Expand Down Expand Up @@ -53,7 +52,6 @@ const requirements = [
'ui.ace',
ngUpload,
'angularResizable',
vsRepeat,
'ui.sortable',
];

Expand Down
6 changes: 5 additions & 1 deletion client/app/pages/queries/query.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ <h3>
</div>

<div class="editor__left__schema" ng-if="sourceMode">
<schema-browser class="schema-container" schema="schema" on-refresh="refreshSchema()"></schema-browser>
<schema-browser
class="schema-container"
schema="schema"
do-refresh="refreshSchema">
</schema-browser>
</div>
<div ng-if="!sourceMode" style="flex-grow: 1;">&nbsp;</div>

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
"angular-toastr": "^2.1.1",
"angular-ui-ace": "^0.2.3",
"angular-ui-bootstrap": "^2.5.0",
"angular-vs-repeat": "^1.1.7",
"antd": "^3.12.3",
"bootstrap": "^3.3.7",
"brace": "^0.11.0",
Expand Down