diff --git a/client/app/assets/less/ant.less b/client/app/assets/less/ant.less
index c42a83e2a7..3d650e2159 100644
--- a/client/app/assets/less/ant.less
+++ b/client/app/assets/less/ant.less
@@ -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 {
+ top: 2px;
+ }
+ }
+ .schema-table-expanded .table-open {
+ display: block;
+ }
+ .table-name:hover,
+ .table-open:hover {
+ .copy-arrow {
+ display: block;
+ }
+ }
+}
diff --git a/client/app/components/dynamic-table/index.js b/client/app/components/dynamic-table/index.js
index a51f661e52..d59c944910 100644
--- a/client/app/components/dynamic-table/index.js
+++ b/client/app/components/dynamic-table/index.js
@@ -69,12 +69,12 @@ function createRowRenderTemplate(columns, $compile) {
switch (column.displayAs) {
case 'json':
return `
-
`;
default:
return `
-
`;
}
diff --git a/client/app/components/queries/SchemaBrowser.jsx b/client/app/components/queries/SchemaBrowser.jsx
new file mode 100644
index 0000000000..502184973c
--- /dev/null
+++ b/client/app/components/queries/SchemaBrowser.jsx
@@ -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;
+
+// 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');
+
+ // 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)) {
+ // setState() is fine here within the right conditions.
+ // eslint-disable-next-line
+ if (!this.state.schemaInitialized) this.setState({ schema: this.props.schema });
+ }
+ }
+
+ handleSearch = (evt) => {
+ if (!evt.target.value) {
+ this.setState({ schema: this.props.schema, searchTerm: '' });
+ 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 });
+ }
+
+ getTitleElement = (title, isTableRow) => (
+
+ {isTableRow && }
+ {` ${title} `}
+ { doCopy(evt, [title]); }}
+ aria-hidden="true"
+ type="double-right"
+ className="copy-arrow"
+ />
+
+ );
+
+ doRowToggle = ({ target }) => {
+ target.closest('.table-item').classList.toggle('schema-table-expanded');
+ }
+
+ render() {
+ if (!this.props.schema) {
+ return null;
+ }
+ return (
+
+
+
+
+
+
+ {this.state.schema.map(table => (
+
+
{this.getTitleElement(table.name, true)}
+ {table.columns.map(column => (
+
{this.getTitleElement(column)}
+ ))}
+
+ ))}
+
+
+ );
+ }
+}
+
+export default function init(ngModule) {
+ ngModule.component('schemaBrowser', react2angular(SchemaBrowser));
+}
+
+init.init = true;
diff --git a/client/app/components/queries/schema-browser.html b/client/app/components/queries/schema-browser.html
deleted file mode 100644
index 6e3f518059..0000000000
--- a/client/app/components/queries/schema-browser.html
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- {{table.name}}
- ({{table.size}})
-
-
-
-
-
-
-
diff --git a/client/app/components/queries/schema-browser.js b/client/app/components/queries/schema-browser.js
deleted file mode 100644
index 34615aa590..0000000000
--- a/client/app/components/queries/schema-browser.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import template from './schema-browser.html';
-
-function SchemaBrowserCtrl($rootScope, $scope) {
- 'ngInject';
-
- this.showTable = (table) => {
- table.collapsed = !table.collapsed;
- $scope.$broadcast('vsRepeatTrigger');
- };
-
- this.getSize = (table) => {
- let size = 22;
-
- if (!table.collapsed) {
- size += 18 * table.columns.length;
- }
-
- return size;
- };
-
- this.isEmpty = function isEmpty() {
- return this.schema === undefined || this.schema.length === 0;
- };
-
- this.itemSelected = ($event, hierarchy) => {
- $rootScope.$broadcast('query-editor.command', 'paste', hierarchy.join('.'));
- $event.preventDefault();
- $event.stopPropagation();
- };
-
- this.splitFilter = (filter) => {
- filter = filter.replace(/ {2}/g, ' ');
- if (filter.includes(' ')) {
- const splitTheFilter = filter.split(' ');
- this.schemaFilterObject = { name: splitTheFilter[0], columns: splitTheFilter[1] };
- this.schemaFilterColumn = splitTheFilter[1];
- } else {
- this.schemaFilterObject = filter;
- this.schemaFilterColumn = '';
- }
- };
-}
-
-const SchemaBrowser = {
- bindings: {
- schema: '<',
- onRefresh: '&',
- },
- controller: SchemaBrowserCtrl,
- template,
-};
-
-export default function init(ngModule) {
- ngModule.component('schemaBrowser', SchemaBrowser);
-}
-
-init.init = true;
diff --git a/client/app/config/index.js b/client/app/config/index.js
index fe3a83def5..33a74bdde3 100644
--- a/client/app/config/index.js
+++ b/client/app/config/index.js
@@ -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';
@@ -53,7 +52,6 @@ const requirements = [
'ui.ace',
ngUpload,
'angularResizable',
- vsRepeat,
'ui.sortable',
];
diff --git a/client/app/pages/queries/query.html b/client/app/pages/queries/query.html
index d95b0b76db..39cb454fda 100644
--- a/client/app/pages/queries/query.html
+++ b/client/app/pages/queries/query.html
@@ -93,7 +93,11 @@
-
+
+
diff --git a/package.json b/package.json
index 620281bae0..de4f69bb4f 100644
--- a/package.json
+++ b/package.json
@@ -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",