diff --git a/caravel/assets/javascripts/SqlLab/components/ColumnElement.jsx b/caravel/assets/javascripts/SqlLab/components/ColumnElement.jsx new file mode 100644 index 0000000000000..24e71b207c633 --- /dev/null +++ b/caravel/assets/javascripts/SqlLab/components/ColumnElement.jsx @@ -0,0 +1,59 @@ +import React from 'react'; + +import { OverlayTrigger, Tooltip } from 'react-bootstrap'; + +const propTypes = { + column: React.PropTypes.object.isRequired, +}; + +const iconMap = { + pk: 'fa-key', + fk: 'fa-link', + index: 'fa-bookmark', +}; +const tooltipTitleMap = { + pk: 'Primary Key', + fk: 'Foreign Key', + index: 'Index', +}; + +class ColumnElement extends React.PureComponent { + render() { + const col = this.props.column; + let name = col.name; + let icons; + if (col.keys && col.keys.length > 0) { + name = {col.name}; + icons = col.keys.map((key, i) => ( + + + {tooltipTitleMap[key.type]} +
+
+                  {JSON.stringify(key, null, '  ')}
+                
+ + } + > + +
+
+ )); + } + return ( +
+
+ {name}{icons} +
+
+ {col.type} +
+
); + } +} +ColumnElement.propTypes = propTypes; + +export default ColumnElement; diff --git a/caravel/assets/javascripts/SqlLab/components/TableElement.jsx b/caravel/assets/javascripts/SqlLab/components/TableElement.jsx index 7295a0343e6a2..52e7d2a6514f7 100644 --- a/caravel/assets/javascripts/SqlLab/components/TableElement.jsx +++ b/caravel/assets/javascripts/SqlLab/components/TableElement.jsx @@ -5,6 +5,7 @@ import shortid from 'shortid'; import CopyToClipboard from '../../components/CopyToClipboard'; import Link from './Link'; +import ColumnElement from './ColumnElement'; import ModalTrigger from '../../components/ModalTrigger'; const propTypes = { @@ -119,21 +120,9 @@ class TableElement extends React.PureComponent {
{this.renderHeader()}
- {cols && cols.map((col) => { - let name = col.name; - if (col.indexed) { - name = {col.name}; - } - return ( -
-
- {name} -
-
- {col.type} -
-
); - })} + {cols && cols.map(col => ( + + ))}
@@ -147,7 +136,6 @@ class TableElement extends React.PureComponent { render() { const table = this.props.table; - let keyLink; if (table.indexes && table.indexes.length > 0) { keyLink = ( @@ -157,13 +145,13 @@ class TableElement extends React.PureComponent { Keys for table {table.name} } - modalBody={ -
{JSON.stringify(table.indexes, null, 4)}
- } + modalBody={table.indexes.map((ix, i) => ( +
{JSON.stringify(ix, null, '  ')}
+ ))} triggerNode={ } /> diff --git a/caravel/assets/javascripts/SqlLab/main.css b/caravel/assets/javascripts/SqlLab/main.css index 61960f5949b5e..89701fb9806b9 100644 --- a/caravel/assets/javascripts/SqlLab/main.css +++ b/caravel/assets/javascripts/SqlLab/main.css @@ -258,3 +258,13 @@ a.Link { padding: 3px 5px; margin: 3px 5px; } +.tooltip pre { + background: transparent; + border: none; + text-align: left; + color: white; + font-size: 10px; +} +.tooltip-inner { + max-width: 500px; +} diff --git a/caravel/assets/spec/javascripts/sqllab/ColumnElement_spec.jsx b/caravel/assets/spec/javascripts/sqllab/ColumnElement_spec.jsx new file mode 100644 index 0000000000000..2d5a8a3f2d1ea --- /dev/null +++ b/caravel/assets/spec/javascripts/sqllab/ColumnElement_spec.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import ColumnElement from '../../../javascripts/SqlLab/components/ColumnElement'; +import { mockedActions, table } from './fixtures'; +import { mount } from 'enzyme'; +import { describe, it } from 'mocha'; +import { expect } from 'chai'; + + +describe('ColumnElement', () => { + const mockedProps = { + actions: mockedActions, + column: table.columns[0], + }; + it('is valid with props', () => { + expect( + React.isValidElement() + ).to.equal(true); + }); + it('renders a proper primary key', () => { + const wrapper = mount(); + expect(wrapper.find('i.fa-key')).to.have.length(1); + expect(wrapper.find('.col-name').first().text()).to.equal('id'); + }); + it('renders a multi-key column', () => { + const wrapper = mount(); + expect(wrapper.find('i.fa-link')).to.have.length(1); + expect(wrapper.find('i.fa-bookmark')).to.have.length(1); + expect(wrapper.find('.col-name').first().text()).to.equal('first_name'); + }); + it('renders a column with no keys', () => { + const wrapper = mount(); + expect(wrapper.find('i')).to.have.length(0); + expect(wrapper.find('.col-name').first().text()).to.equal('last_name'); + }); +}); diff --git a/caravel/assets/spec/javascripts/sqllab/TableElement_spec.jsx b/caravel/assets/spec/javascripts/sqllab/TableElement_spec.jsx index 90a73b9afdd8c..9859785547417 100644 --- a/caravel/assets/spec/javascripts/sqllab/TableElement_spec.jsx +++ b/caravel/assets/spec/javascripts/sqllab/TableElement_spec.jsx @@ -1,6 +1,7 @@ import React from 'react'; import Link from '../../../javascripts/SqlLab/components/Link'; import TableElement from '../../../javascripts/SqlLab/components/TableElement'; +import ColumnElement from '../../../javascripts/SqlLab/components/ColumnElement'; import { mockedActions, table } from './fixtures'; import { mount, shallow } from 'enzyme'; import { describe, it } from 'mocha'; @@ -29,7 +30,7 @@ describe('TableElement', () => { }); it('has 14 columns', () => { const wrapper = shallow(); - expect(wrapper.find('div.table-column')).to.have.length(14); + expect(wrapper.find(ColumnElement)).to.have.length(14); }); it('mounts', () => { mount(); @@ -37,10 +38,10 @@ describe('TableElement', () => { it('sorts columns', () => { const wrapper = mount(); expect(wrapper.state().sortColumns).to.equal(false); - expect(wrapper.find('.col-name').first().text()).to.equal('id'); + expect(wrapper.find(ColumnElement).first().props().column.name).to.equal('id'); wrapper.find('.sort-cols').simulate('click'); expect(wrapper.state().sortColumns).to.equal(true); - expect(wrapper.find('.col-name').first().text()).to.equal('last_login'); + expect(wrapper.find(ColumnElement).first().props().column.name).to.equal('last_login'); }); it('calls the collapseTable action', () => { const wrapper = mount(); diff --git a/caravel/assets/spec/javascripts/sqllab/fixtures.js b/caravel/assets/spec/javascripts/sqllab/fixtures.js index f32f3db31dedf..3ff6d922f67bb 100644 --- a/caravel/assets/spec/javascripts/sqllab/fixtures.js +++ b/caravel/assets/spec/javascripts/sqllab/fixtures.js @@ -54,12 +54,42 @@ export const table = { longType: 'INTEGER(11)', type: 'INTEGER', name: 'id', + keys: [ + { + column_names: ['id'], + type: 'pk', + name: null, + }, + ], }, { indexed: false, longType: 'VARCHAR(64)', type: 'VARCHAR', name: 'first_name', + keys: [ + { + column_names: [ + 'first_name', + ], + name: 'slices_ibfk_1', + referred_columns: [ + 'id', + ], + referred_table: 'datasources', + type: 'fk', + referred_schema: 'carapal', + options: {}, + }, + { + unique: false, + column_names: [ + 'druid_datasource_id', + ], + type: 'index', + name: 'druid_datasource_id', + }, + ], }, { indexed: false, diff --git a/caravel/models.py b/caravel/models.py index 17fdbfb54194f..8371f83d6d5f1 100644 --- a/caravel/models.py +++ b/caravel/models.py @@ -802,6 +802,12 @@ def get_columns(self, table_name, schema=None): def get_indexes(self, table_name, schema=None): return self.inspector.get_indexes(table_name, schema) + def get_pk_constraint(self, table_name, schema=None): + return self.inspector.get_pk_constraint(table_name, schema) + + def get_foreign_keys(self, table_name, schema=None): + return self.inspector.get_foreign_keys(table_name, schema) + @property def sqlalchemy_uri_decrypted(self): conn = sqla.engine.url.make_url(self.sqlalchemy_uri) diff --git a/caravel/views.py b/caravel/views.py index 5e120c44e3348..4f4d9cc93fac8 100755 --- a/caravel/views.py +++ b/caravel/views.py @@ -1947,13 +1947,24 @@ def table(self, database_id, table_name, schema): try: t = mydb.get_columns(table_name, schema) indexes = mydb.get_indexes(table_name, schema) + primary_key = mydb.get_pk_constraint(table_name, schema) + foreign_keys = mydb.get_foreign_keys(table_name, schema) except Exception as e: return Response( json.dumps({'error': utils.error_msg_from_exception(e)}), mimetype="application/json") - indexed_columns = set() - for index in indexes: - indexed_columns |= set(index.get('column_names', [])) + keys = [] + if primary_key and primary_key.get('constrained_columns'): + primary_key['column_names'] = primary_key.pop('constrained_columns') + primary_key['type'] = 'pk' + keys += [primary_key] + for fk in foreign_keys: + fk['column_names'] = fk.pop('constrained_columns') + fk['type'] = 'fk' + keys += foreign_keys + for idx in indexes: + idx['type'] = 'index' + keys += indexes for col in t: dtype = "" @@ -1965,14 +1976,19 @@ def table(self, database_id, table_name, schema): 'name': col['name'], 'type': dtype.split('(')[0] if '(' in dtype else dtype, 'longType': dtype, - 'indexed': col['name'] in indexed_columns, + 'keys': [ + k for k in keys + if col['name'] in k.get('column_names') + ], }) tbl = { 'name': table_name, 'columns': cols, 'selectStar': mydb.select_star( table_name, schema=schema, show_cols=True, indent=True), - 'indexes': indexes, + 'primaryKey': primary_key, + 'foreignKeys': foreign_keys, + 'indexes': keys, } return Response(json.dumps(tbl), mimetype="application/json") diff --git a/tests/core_tests.py b/tests/core_tests.py index 704ada0b9a161..e65522ea1d0f1 100644 --- a/tests/core_tests.py +++ b/tests/core_tests.py @@ -519,6 +519,28 @@ def test_templated_sql_json(self): data = self.run_sql(sql, "admin", "fdaklj3ws") self.assertEqual(data['data'][0]['test'], "2017-01-01T00:00:00") + def test_table_metadata(self): + maindb = self.get_main_database(db.session) + data = self.get_json_resp( + "/caravel/table/{}/ab_user/null/".format(maindb.id)) + self.assertEqual(data['name'], 'ab_user') + assert len(data['columns']) > 5 + assert data.get('selectStar').startswith('SELECT') + + # Engine specific tests + backend = maindb.backend + if backend in ('mysql', 'postgresql'): + self.assertEqual(data.get('primaryKey').get('type'), 'pk') + self.assertEqual( + data.get('primaryKey').get('column_names')[0], 'id') + self.assertEqual(len(data.get('foreignKeys')), 2) + if backend == 'mysql': + self.assertEqual(len(data.get('indexes')), 7) + elif backend == 'postgresql': + self.assertEqual(len(data.get('indexes')), 5) + + + if __name__ == '__main__': unittest.main()