Repository: ambari Updated Branches: refs/heads/branch-2.5 90d1ff1ad -> 151d2f167
AMBARI-19898. Hive View 2.0: Ability to edit table through UI. (dipayanb) Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/151d2f16 Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/151d2f16 Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/151d2f16 Branch: refs/heads/branch-2.5 Commit: 151d2f1671c42db3e77c0305b29f6b1a5672f7a9 Parents: 90d1ff1 Author: Dipayan Bhowmick <dipayan.bhowm...@gmail.com> Authored: Wed Feb 8 21:05:23 2017 +0530 Committer: Dipayan Bhowmick <dipayan.bhowm...@gmail.com> Committed: Wed Feb 8 21:05:23 2017 +0530 ---------------------------------------------------------------------- .../view/hive20/internal/dto/ColumnInfo.java | 28 +- .../view/hive20/internal/dto/TableStats.java | 11 + .../internal/parsers/TableMetaParserImpl.java | 8 + .../generators/AlterTableQueryGenerator.java | 82 +++--- .../query/generators/QueryGenerationUtils.java | 5 +- .../view/hive20/resources/browser/DDLProxy.java | 2 +- .../src/main/resources/ui/app/adapters/table.js | 6 + .../resources/ui/app/components/column-item.js | 1 + .../resources/ui/app/components/edit-table.js | 220 ++++++++++++++ .../ui/app/components/property-item.js | 1 + .../app/components/table-advanced-settings.js | 5 + .../ui/app/components/table-columns.js | 3 +- .../ui/app/components/table-properties.js | 3 +- .../resources/ui/app/configs/edit-table-tabs.js | 48 +++ .../hive20/src/main/resources/ui/app/router.js | 1 + .../databases/database/tables/table/edit.js | 86 ++++++ .../ui/app/services/table-operations.js | 24 ++ .../src/main/resources/ui/app/styles/app.scss | 3 + .../ui/app/templates/components/column-item.hbs | 13 +- .../ui/app/templates/components/edit-table.hbs | 65 ++++ .../app/templates/components/property-item.hbs | 10 +- .../components/table-advanced-settings.hbs | 295 ++++++++++--------- .../app/templates/components/table-columns.hbs | 1 + .../templates/components/table-properties.hbs | 1 + .../templates/components/table-statistics.hbs | 4 + .../databases/database/tables/table.hbs | 2 +- .../databases/database/tables/table/edit.hbs | 45 +++ .../AlterTableQueryGenerationSpecTest.groovy | 59 ---- .../AlterTableQueryGeneratorTest.java | 161 +++++++++- 29 files changed, 911 insertions(+), 282 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/dto/ColumnInfo.java ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/dto/ColumnInfo.java b/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/dto/ColumnInfo.java index 5daab91..9f179d1 100644 --- a/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/dto/ColumnInfo.java +++ b/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/dto/ColumnInfo.java @@ -20,6 +20,8 @@ package org.apache.ambari.view.hive20.internal.dto; import org.apache.commons.lang3.builder.EqualsBuilder; +import java.util.Objects; + /** * */ @@ -82,28 +84,20 @@ public class ColumnInfo { } @Override - public int hashCode() { - int result = name.hashCode(); - result = 31 * result + type.hashCode(); - result = 31 * result + (precision != null ? precision.hashCode() : 0); - result = 31 * result + (scale != null ? scale.hashCode() : 0); - result = 31 * result + (comment != null ? comment.hashCode() : 0); - return result; - } - - @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ColumnInfo that = (ColumnInfo) o; + return ((name == that.name) || (name != null && name.equalsIgnoreCase(that.name))) && + ((type == that.type) || (type != null && type.equalsIgnoreCase(that.type))) && + Objects.equals(precision, that.precision) && + Objects.equals(scale, that.scale) && + Objects.equals(comment, that.comment); + } - return new EqualsBuilder() - .append(getName(), that.getName()) - .append(getType(), that.getType()) - .append(getComment(), that.getComment()) - .isEquals(); + @Override + public int hashCode() { + return Objects.hash(name); } @Override http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/dto/TableStats.java ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/dto/TableStats.java b/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/dto/TableStats.java index b8b4f07..3048d22 100644 --- a/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/dto/TableStats.java +++ b/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/dto/TableStats.java @@ -24,12 +24,14 @@ package org.apache.ambari.view.hive20.internal.dto; */ public class TableStats { public static final String NUM_FILES = "numFiles"; + public static final String NUM_ROWS = "numRows"; public static final String COLUMN_STATS_ACCURATE = "COLUMN_STATS_ACCURATE"; public static final String RAW_DATA_SIZE = "rawDataSize"; public static final String TOTAL_SIZE = "totalSize"; private Boolean isTableStatsEnabled; private Integer numFiles; + private Integer numRows; private String columnStatsAccurate; private Integer rawDataSize; private Integer totalSize; @@ -74,11 +76,20 @@ public class TableStats { this.totalSize = totalSize; } + public Integer getNumRows() { + return numRows; + } + + public void setNumRows(Integer numRows) { + this.numRows = numRows; + } + @Override public String toString() { final StringBuilder sb = new StringBuilder("TableStats{"); sb.append("isStatsEnabled='").append(isTableStatsEnabled).append('\''); sb.append(", numFiles='").append(numFiles).append('\''); + sb.append(", numRows='").append(numRows).append('\''); sb.append(", columnStatsAccurate='").append(columnStatsAccurate).append('\''); sb.append(", rawDataSize='").append(rawDataSize).append('\''); sb.append(", totalSize='").append(totalSize).append('\''); http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/parsers/TableMetaParserImpl.java ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/parsers/TableMetaParserImpl.java b/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/parsers/TableMetaParserImpl.java index b0c9fe4..f2a1933 100644 --- a/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/parsers/TableMetaParserImpl.java +++ b/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/parsers/TableMetaParserImpl.java @@ -86,6 +86,9 @@ public class TableMetaParserImpl implements TableMetaParser<TableMeta> { String numFiles = tableInfo.getParameters().get(TableStats.NUM_FILES); tableInfo.getParameters().remove(TableStats.NUM_FILES); + String numRows = tableInfo.getParameters().get(TableStats.NUM_ROWS); + tableInfo.getParameters().remove(TableStats.NUM_ROWS); + String columnStatsAccurate = tableInfo.getParameters().get(TableStats.COLUMN_STATS_ACCURATE); tableInfo.getParameters().remove(TableStats.COLUMN_STATS_ACCURATE); @@ -100,6 +103,11 @@ public class TableMetaParserImpl implements TableMetaParser<TableMeta> { tableStats.setNumFiles(Integer.valueOf(numFiles.trim())); } + if(!Strings.isNullOrEmpty(numRows) && !Strings.isNullOrEmpty(numRows.trim())){ + tableStats.setTableStatsEnabled(true); + tableStats.setNumRows(Integer.valueOf(numRows.trim())); + } + if(!Strings.isNullOrEmpty(rawDataSize) && !Strings.isNullOrEmpty(rawDataSize.trim())){ tableStats.setTableStatsEnabled(true); tableStats.setRawDataSize(Integer.valueOf(rawDataSize.trim())); http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/query/generators/AlterTableQueryGenerator.java ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/query/generators/AlterTableQueryGenerator.java b/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/query/generators/AlterTableQueryGenerator.java index 73f8266..b119f6a 100644 --- a/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/query/generators/AlterTableQueryGenerator.java +++ b/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/query/generators/AlterTableQueryGenerator.java @@ -31,16 +31,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import static org.apache.ambari.view.hive20.internal.query.generators.QueryGenerationUtils.isNullOrEmpty; public class AlterTableQueryGenerator implements QueryGenerator { private static final Logger LOG = LoggerFactory.getLogger(AlterTableQueryGenerator.class); + public static List<String> SYSTEM_PROPERTY_LIST = Arrays.asList("last_modified_time", "transient_lastDdlTime", "last_modified_by", "numRows", "numFiles", "rawDataSize", "totalSize", "COLUMN_STATS_ACCURATE"); + private final TableMeta oldMeta; private final TableMeta newMeta; @@ -65,11 +64,6 @@ public class AlterTableQueryGenerator implements QueryGenerator { public Optional<String> getQuery() { List<Optional<String>> queries = new LinkedList<>(); - // TODO: rename of table name has to be handled separately as other queries depend on new name. -// Optional<String> tableRenameQuery = this.generateTableRenameQuery(this.getOldMeta().getDatabase(), -// this.getOldMeta().getTable(), this.getNewMeta().getDatabase(), this.getNewMeta().getTable()); -// queries.add(tableRenameQuery); - Optional<List<Optional<String>>> columnQuery = this.generateColumnQuery(); if (columnQuery.isPresent()) { queries.addAll(columnQuery.get()); @@ -81,28 +75,30 @@ public class AlterTableQueryGenerator implements QueryGenerator { queries.add(tablePropertiesQuery); } - if (null != this.getOldMeta().getStorageInfo() && null != this.getNewMeta().getStorageInfo()) { - String oldSerde = this.getOldMeta().getStorageInfo().getSerdeLibrary(); - String newSerde = this.getNewMeta().getStorageInfo().getSerdeLibrary(); - Map<String, String> oldParameters = this.getOldMeta().getStorageInfo().getParameters(); - Map<String, String> newParameters = this.getNewMeta().getStorageInfo().getParameters(); - - Optional<String> serdeProperties = this.generateSerdeQuery(oldSerde, oldParameters, newSerde, newParameters); - queries.add(serdeProperties); - } - - if (null != this.getOldMeta().getStorageInfo() && null != this.getNewMeta().getStorageInfo()) { - List<String> oldBucketCols = this.getOldMeta().getStorageInfo().getBucketCols(); - List<ColumnOrder> oldSortCols = this.getOldMeta().getStorageInfo().getSortCols(); - String oldNumBuckets = this.getOldMeta().getStorageInfo().getNumBuckets(); - - List<String> newBucketCols = this.getNewMeta().getStorageInfo().getBucketCols(); - List<ColumnOrder> newSortCols = this.getNewMeta().getStorageInfo().getSortCols(); - String newNumBuckets = this.getNewMeta().getStorageInfo().getNumBuckets(); - - Optional<String> storagePropertyQuery = this.generateStoragePropertyQuery(oldBucketCols, oldSortCols, oldNumBuckets, newBucketCols, newSortCols, newNumBuckets); - queries.add(storagePropertyQuery); - } + // storage change is not required to be handled. +// if (null != this.getOldMeta().getStorageInfo() && null != this.getNewMeta().getStorageInfo()) { +// String oldSerde = this.getOldMeta().getStorageInfo().getSerdeLibrary(); +// String newSerde = this.getNewMeta().getStorageInfo().getSerdeLibrary(); +// Map<String, String> oldParameters = this.getOldMeta().getStorageInfo().getParameters(); +// Map<String, String> newParameters = this.getNewMeta().getStorageInfo().getParameters(); +// +// Optional<String> serdeProperties = this.generateSerdeQuery(oldSerde, oldParameters, newSerde, newParameters); +// queries.add(serdeProperties); +// } + + // change of bucketed columns is not required right now +// if (null != this.getOldMeta().getStorageInfo() && null != this.getNewMeta().getStorageInfo()) { +// List<String> oldBucketCols = this.getOldMeta().getStorageInfo().getBucketCols(); +// List<ColumnOrder> oldSortCols = this.getOldMeta().getStorageInfo().getSortCols(); +// String oldNumBuckets = this.getOldMeta().getStorageInfo().getNumBuckets(); +// +// List<String> newBucketCols = this.getNewMeta().getStorageInfo().getBucketCols(); +// List<ColumnOrder> newSortCols = this.getNewMeta().getStorageInfo().getSortCols(); +// String newNumBuckets = this.getNewMeta().getStorageInfo().getNumBuckets(); +// +// Optional<String> storagePropertyQuery = this.generateStoragePropertyQuery(oldBucketCols, oldSortCols, oldNumBuckets, newBucketCols, newSortCols, newNumBuckets); +// queries.add(storagePropertyQuery); +// } List<String> queryList = FluentIterable.from(queries).filter(new Predicate<Optional<String>>() { @@ -322,19 +318,33 @@ public class AlterTableQueryGenerator implements QueryGenerator { return Optional.absent(); } - Optional<String> generateTablePropertiesQuery(Map oldProps, Map newProps) { + Optional<String> generateTablePropertiesQuery(Map<String, String> oldProps, Map<String, String> newProps) { Optional<String> query = createTablePropertiesQuery(oldProps, newProps); if (query.isPresent()) return Optional.of(getQueryPerfix() + query.get()); else return Optional.absent(); } - static Optional<String> createTablePropertiesQuery(Map oldProps, Map newProps) { + static Optional<String> createTablePropertiesQuery(Map<String, String> oldProps, Map<String, String> newProps) { + if( null == newProps && null == oldProps){ + return Optional.absent(); + } + if (null == newProps) { - newProps = new HashMap(); + newProps = new HashMap<>(); } -// TODO ignore system generated table properties during comparison - if (!QueryGenerationUtils.isEqual(oldProps, newProps)) { + + if(null == oldProps){ + oldProps = new HashMap<>(); + } + // ignore system generated table properties during comparison + + for(String prop : SYSTEM_PROPERTY_LIST){ + newProps.remove(prop); + oldProps.remove(prop); + } + + if (!QueryGenerationUtils.isEqual(oldProps, newProps) && !newProps.isEmpty()) { return Optional.of(" SET TBLPROPERTIES (" + QueryGenerationUtils.getPropertiesAsKeyValues(newProps) + ")"); } http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/query/generators/QueryGenerationUtils.java ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/query/generators/QueryGenerationUtils.java b/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/query/generators/QueryGenerationUtils.java index d9dc6e1..db219c4 100644 --- a/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/query/generators/QueryGenerationUtils.java +++ b/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/internal/query/generators/QueryGenerationUtils.java @@ -81,8 +81,11 @@ public class QueryGenerationUtils { Map<Object, Object> modified = new HashMap<>(); Map<Object, Object> deleted = new HashMap<>(); - if(oldProps == null && newProps == null) return Optional.of(ret); + if(oldProps == null && newProps == null) return Optional.absent(); + if(oldProps == null && newProps != null){ + oldProps = new HashMap(); + } if(oldProps != null && newProps != null){ Set<Map.Entry> entrySet = oldProps.entrySet(); for(Map.Entry e : entrySet){ http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/resources/browser/DDLProxy.java ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/resources/browser/DDLProxy.java b/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/resources/browser/DDLProxy.java index f75b008..f5ecdee 100644 --- a/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/resources/browser/DDLProxy.java +++ b/contrib/views/hive20/src/main/java/org/apache/ambari/view/hive20/resources/browser/DDLProxy.java @@ -279,7 +279,7 @@ public class DDLProxy { if(alterQuery.isPresent()){ return alterQuery.get(); }else{ - throw new ServiceException("Failed to generate alter table query for table " + oldTableMeta.getDatabase() + "." + oldTableMeta.getTable()); + throw new ServiceException("Failed to generate alter table query for table " + oldTableMeta.getDatabase() + "." + oldTableMeta.getTable() + ". No difference was found."); } } http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/adapters/table.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/adapters/table.js b/contrib/views/hive20/src/main/resources/ui/app/adapters/table.js index 47174e4..e133419 100644 --- a/contrib/views/hive20/src/main/resources/ui/app/adapters/table.js +++ b/contrib/views/hive20/src/main/resources/ui/app/adapters/table.js @@ -44,6 +44,12 @@ export default DDLAdapter.extend({ return this.ajax(postURL, 'POST', { data: { tableInfo: tableMetaInfo } }); }, + editTable(tableMetaInfo) { + let postURL = this.buildURL('table', null, null, 'query', + { databaseId: tableMetaInfo.database, tableName: tableMetaInfo.table }); + return this.ajax(postURL, 'PUT', { data: { tableInfo: tableMetaInfo } }); + }, + deleteTable(database, tableName) { let deletURL = this.buildURL('table', null, null, 'query', { databaseId: database, tableName: tableName }); return this.ajax(deletURL, 'DELETE'); http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/components/column-item.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/components/column-item.js b/contrib/views/hive20/src/main/resources/ui/app/components/column-item.js index d4e43f3..f2e45bd 100644 --- a/contrib/views/hive20/src/main/resources/ui/app/components/column-item.js +++ b/contrib/views/hive20/src/main/resources/ui/app/components/column-item.js @@ -23,6 +23,7 @@ export default Ember.Component.extend({ tagName: 'tr', advancedOption: false, datatypes: Ember.copy(datatypes), + editMode: false, http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/components/edit-table.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/components/edit-table.js b/contrib/views/hive20/src/main/resources/ui/app/components/edit-table.js new file mode 100644 index 0000000..439dbcf --- /dev/null +++ b/contrib/views/hive20/src/main/resources/ui/app/components/edit-table.js @@ -0,0 +1,220 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Ember from 'ember'; +import datatypes from '../configs/datatypes'; +import Column from '../models/column'; +import Helper from '../configs/helpers'; +import TableProperty from '../models/table-property'; + +export default Ember.Component.extend({ + + init() { + this._super(...arguments); + this.set('columns', Ember.A()); + this.set('properties', []); + this.set('settings', {}); + this.set('shouldAddBuckets', null); + this.set('settingErrors', []); + }, + + didReceiveAttrs() { + this.get('tabs').setEach('active', false); + let firstTab = this.get('tabs.firstObject'); + firstTab.set('active', true); + this.set('columns', this._transformColumns()); + this.set('properties', this._extractParameters()); + this.set('settings', this._extractSettings()); + }, + + actions: { + cancel() { + this.sendAction('cancel'); + }, + + edit() { + if (this.validate()) { + this.sendAction('edit', { + database: this.get('table.database'), + table: this.get('table.table'), + columns: this.get('columns'), + settings: this.get('settings'), + properties: this.get('properties') + }); + } + } + }, + + _transformColumns() { + let columns = []; + columns.pushObjects(this.get('table.columns').map((item) => { + return this._getColumnEntry(item, false, this._isClustered(this.get('table'), item.name)) + })); + + if (!Ember.isEmpty(this.get('table.partitionInfo'))) { + columns.pushObjects(this.get('table.partitionInfo.columns').map((item) => { + return this._getColumnEntry(item, true, false); + })); + } + + return columns; + }, + + _getColumnEntry(column, isPartitioned, isClustered) { + return Column.create({ + name: column.name, + type: this._getType(column.type), + comment: column.comment, + precision: column.precision, + scale: column.scale, + isPartitioned: isPartitioned, + isClustered: isClustered, + editing: !(isPartitioned || isClustered), + newColumn: false + }); + }, + + _getType(typeString) { + return datatypes.find((item) => item.label.toLowerCase() === typeString.toLowerCase()); + }, + + _isClustered(tableInfo, columnName) { + if (!Ember.isEmpty(tableInfo.get('storageInfo.bucketCols'))) { + return tableInfo.get('storageInfo.bucketCols').contains(columnName); + } else { + return false; + } + }, + + _extractParameters() { + if (!Ember.isEmpty(this.get('table.detailedInfo.parameters'))) { + let tableProperties = this.get('table.detailedInfo.parameters'); + return Object.keys(tableProperties) + .filter((item) => item !== 'transactional') + .map((item) => { + return TableProperty.create({ + key: item, + value: tableProperties[item], + editing: false, + newProperty: false + }); + }) + } else { + return []; + } + }, + + _extractSettings() { + let settings = {}; + let tableInfo = this.get('table'); + + // filter out transaction parameter to set if transactional + if (!Ember.isEmpty(this.get('table.detailedInfo.parameters'))) { + let tableProperties = this.get('table.detailedInfo.parameters'); + let transactional = Object.keys(tableProperties) + .filter((item) => item === 'transactional'); + if (!Ember.isEmpty(transactional)) { + settings.transactional = true; + } + } + + // Find if already clustered, then set number of buckets + if (!Ember.isEmpty(tableInfo.get('storageInfo.bucketCols'))) { + settings.numBuckets = parseInt(tableInfo.get('storageInfo.numBuckets')) + this.set('shouldAddBuckets', true); + } + + return settings; + }, + + validate() { + if (!(this.checkColumnUniqueness() && + this.validateColumns())) { + this.selectTab("edit.table.columns"); + return false; + } + + if(!(this.validateNumBuckets())) { + this.selectTab("edit.table.advanced"); + return false; + } + + if (!(this.validateTableProperties())) { + this.selectTab("edit.table.properties"); + return false; + } + return true; + }, + + checkColumnUniqueness() { + let columnNames = []; + for (let i = 0; i < this.get('columns.length'); i++) { + let column = this.get('columns').objectAt(i); + column.clearError(); + if (columnNames.indexOf(column.get('name')) === -1) { + columnNames.pushObject(column.get('name')); + } else { + column.get('errors').push({type: 'name', error: 'Name should be unique'}); + return false; + } + } + + return true; + }, + + validateColumns() { + for (let i = 0; i < this.get('columns.length'); i++) { + let column = this.get('columns').objectAt(i); + if (!column.validate()) { + return false; + } + } + return true; + }, + + validateTableProperties() { + for (let i = 0; i < this.get('properties.length'); i++) { + let property = this.get('properties').objectAt(i); + if (!property.validate()) { + return false; + } + } + return true; + }, + + validateNumBuckets() { + let clusteredColumns = this.get('columns').filterBy('isClustered', true); + if(clusteredColumns.get('length') > 0 && + (Ember.isEmpty(this.get('settings.numBuckets')) || + !Helper.isInteger(this.get('settings.numBuckets')))) { + this.get('settingErrors').pushObject({type: 'numBuckets', error: "Some columns are clustered, Number of buckets are required."}); + return false; + } + + return true; + }, + + selectTab(link) { + this.get('tabs').setEach('active', false); + let selectedTab = this.get('tabs').findBy('link', link); + if (!Ember.isEmpty(selectedTab)) { + selectedTab.set('active', true); + } + } + +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/components/property-item.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/components/property-item.js b/contrib/views/hive20/src/main/resources/ui/app/components/property-item.js index 96ef473..edc2c44 100644 --- a/contrib/views/hive20/src/main/resources/ui/app/components/property-item.js +++ b/contrib/views/hive20/src/main/resources/ui/app/components/property-item.js @@ -20,6 +20,7 @@ import Ember from 'ember'; export default Ember.Component.extend({ tagName: 'tr', + editMode: false, didInsertElement() { Ember.run.later( () => { http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/components/table-advanced-settings.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/components/table-advanced-settings.js b/contrib/views/hive20/src/main/resources/ui/app/components/table-advanced-settings.js index 5e50a5c..99a9bb6 100644 --- a/contrib/views/hive20/src/main/resources/ui/app/components/table-advanced-settings.js +++ b/contrib/views/hive20/src/main/resources/ui/app/components/table-advanced-settings.js @@ -29,6 +29,8 @@ export default Ember.Component.extend({ showRowFormatInput: false, shouldAddBuckets: false, errors: [], + editMode: false, + disableTransactionInput: false, settings: {}, @@ -67,6 +69,9 @@ export default Ember.Component.extend({ this.set('selectedNullDefinition', this.get('settings.rowFormat.nullDefinedAs')); this.set('selectedEscapeDefinition', this.get('settings.rowFormat.escapeDefinedAs')); } + if(!Ember.isEmpty(this.get('settings.transactional')) && this.get('settings.transactional') && this.get('editMode')) { + this.set('disableTransactionInput', true); + } }, locationInputObserver: Ember.observer('showLocationInput', function () { http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/components/table-columns.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/components/table-columns.js b/contrib/views/hive20/src/main/resources/ui/app/components/table-columns.js index 5479496..7d83353 100644 --- a/contrib/views/hive20/src/main/resources/ui/app/components/table-columns.js +++ b/contrib/views/hive20/src/main/resources/ui/app/components/table-columns.js @@ -21,6 +21,7 @@ import Column from '../models/column'; export default Ember.Component.extend({ columns: [], + editMode: false, shouldAddBuckets: null, clusteredColumnObserver: Ember.observer('columns.@each.isClustered', function(sender, key, value, rev) { @@ -37,7 +38,7 @@ export default Ember.Component.extend({ actions: { addNewColumn() { - let newEmptyColumn = Column.create({editing: true}); + let newEmptyColumn = Column.create({editing: true, newColumn: true}); this.get('columns').pushObject(newEmptyColumn); }, http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/components/table-properties.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/components/table-properties.js b/contrib/views/hive20/src/main/resources/ui/app/components/table-properties.js index 1ba15cc..f1ee645 100644 --- a/contrib/views/hive20/src/main/resources/ui/app/components/table-properties.js +++ b/contrib/views/hive20/src/main/resources/ui/app/components/table-properties.js @@ -21,10 +21,11 @@ import TableProperty from '../models/table-property'; export default Ember.Component.extend({ properties: [], + editMode: false, actions: { addNewRow() { - let emptyProperty = TableProperty.create({editing: true}); + let emptyProperty = TableProperty.create({editing: true, newProperty: true}); this.get('properties').pushObject(emptyProperty); }, http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/configs/edit-table-tabs.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/configs/edit-table-tabs.js b/contrib/views/hive20/src/main/resources/ui/app/configs/edit-table-tabs.js new file mode 100644 index 0000000..49e702f --- /dev/null +++ b/contrib/views/hive20/src/main/resources/ui/app/configs/edit-table-tabs.js @@ -0,0 +1,48 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Ember from 'ember'; + +let editTableTabs = [ + Ember.Object.create({ + name: 'columns', + label: 'COLUMNS', + transition: false, + link: 'edit.table.columns', + faIcon: 'list' + }), + + Ember.Object.create({ + name: 'advanced', + label: 'ADVANCED', + transition: false, + link: 'edit.table.advanced', + faIcon: 'file-text-o' + }), + + Ember.Object.create({ + name: 'properties', + label: 'TABLE PROPERTIES', + transition: false, + link: 'edit.table.properties', + faIcon: 'file-text-o' + }) + +]; + +export default editTableTabs; http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/router.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/router.js b/contrib/views/hive20/src/main/resources/ui/app/router.js index c781a34..ffb0f83 100644 --- a/contrib/views/hive20/src/main/resources/ui/app/router.js +++ b/contrib/views/hive20/src/main/resources/ui/app/router.js @@ -45,6 +45,7 @@ Router.map(function() { this.route('new'); this.route('upload-table'); this.route('table', {path: '/:name'}, function() { + this.route('edit'); this.route('rename'); this.route('columns'); this.route('partitions'); http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/routes/databases/database/tables/table/edit.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/routes/databases/database/tables/table/edit.js b/contrib/views/hive20/src/main/resources/ui/app/routes/databases/database/tables/table/edit.js new file mode 100644 index 0000000..47340ba --- /dev/null +++ b/contrib/views/hive20/src/main/resources/ui/app/routes/databases/database/tables/table/edit.js @@ -0,0 +1,86 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import TableMetaRouter from './table-meta-router'; +import tabs from '../../../../../configs/edit-table-tabs'; + +export default TableMetaRouter.extend({ + + tableOperations: Ember.inject.service(), + + activate() { + let tableController = this.controllerFor('databases.database.tables.table'); + this.set('existingTabs', tableController.get('tabs')); + tableController.set('tabs', []); + }, + + deactivate() { + let tableController = this.controllerFor('databases.database.tables.table'); + tableController.set('tabs', this.get('existingTabs')); + }, + + setupController(controller, model) { + this._super(controller, model); + controller.set('tabs', Ember.copy(tabs)); + }, + + actions: { + + cancel() { + this.transitionTo('databases.database.tables'); + }, + + edit(settings) { + this._modalStatus(true, 'Submitting request to edit table'); + this.get('tableOperations').editTable(settings).then((job) => { + this._modalStatus(true, 'Waiting for the table edit job to complete'); + return this.get('tableOperations').waitForJobToComplete(job.get('id'), 5 * 1000); + }).then((status) => { + this._modalStatus(true, 'Successfully edited the table'); + this._transitionToTables(); + }).catch((err) => { + this._modalStatus(true, 'Failed to edit table'); + this._alertMessage('Failed to edit table', err); + this._transitionToTables(); + }); + } + + }, + + _modalStatus(status, message) { + this.controller.set('showModal', status); + if(status) { + this.controller.set('modalMessage', message); + } + }, + + _transitionToTables() { + Ember.run.later(() => { + this._modalStatus(false); + this.send('refreshTableInfo'); + this.transitionTo('databases.database.tables.table'); + }, 2000); + }, + + _alertMessage(message, err) { + console.log(message, err); + // TODO: user alert message here + } + + +}); http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/services/table-operations.js ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/services/table-operations.js b/contrib/views/hive20/src/main/resources/ui/app/services/table-operations.js index d11816c..a5be574 100644 --- a/contrib/views/hive20/src/main/resources/ui/app/services/table-operations.js +++ b/contrib/views/hive20/src/main/resources/ui/app/services/table-operations.js @@ -45,6 +45,30 @@ export default Ember.Service.extend({ }); }, + editTable(settings) { + let detailedInfo = this._getDetailedInfo(settings); + let storageInfo = this._getStorageInfo(settings); + let columns = this._getColumns(settings); + let partitionColumns = this._getPartitionColumns(settings); + + let tableInfo = Ember.Object.create({ + database: settings.database, + table: settings.table, + columns: columns, + partitionInfo: { columns: partitionColumns }, + detailedInfo: detailedInfo, + storageInfo: storageInfo + }); + return new Promise((resolve, reject) => { + this.get('store').adapterFor('table').editTable(tableInfo).then((data) => { + this.get('store').pushPayload(data); + resolve(this.get('store').peekRecord('job', data.job.id)); + }, (err) => { + reject(err); + }); + }); + }, + deleteTable(database, table) { return new Ember.RSVP.Promise((resolve, reject) => { this.get('store').adapterFor('table').deleteTable(database, table).then((data) => { http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/styles/app.scss ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/styles/app.scss b/contrib/views/hive20/src/main/resources/ui/app/styles/app.scss index bce9f69..968d3b3 100644 --- a/contrib/views/hive20/src/main/resources/ui/app/styles/app.scss +++ b/contrib/views/hive20/src/main/resources/ui/app/styles/app.scss @@ -270,6 +270,9 @@ pre { .create-table-controls { padding-top: 15px; padding-bottom: 15px; + .warning { + margin-top: 15px; + } } .column-precision { http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/templates/components/column-item.hbs ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/templates/components/column-item.hbs b/contrib/views/hive20/src/main/resources/ui/app/templates/components/column-item.hbs index 73fac89..b649d5b 100644 --- a/contrib/views/hive20/src/main/resources/ui/app/templates/components/column-item.hbs +++ b/contrib/views/hive20/src/main/resources/ui/app/templates/components/column-item.hbs @@ -27,7 +27,7 @@ </td> <td> {{#power-select - disabled=notEditing + disabled=(not column.editing) selected=column.type options=datatypes searchField="label" @@ -114,10 +114,13 @@ </td> <td> <div class="text-center"> - {{#unless column.editing}} - <button class="btn btn-success" {{action "edit"}}>{{fa-icon "check"}} Edit</button> - {{/unless}} - <button class="btn btn-danger" {{action "delete"}}>{{fa-icon "times"}} Delete</button> + {{#if (or column.newColumn (not editMode)) }} + {{#unless column.editing}} + <button class="btn btn-success" {{action "edit"}}>{{fa-icon "check"}} Edit</button> + {{/unless}} + <button class="btn btn-danger" {{action "delete"}}>{{fa-icon "times"}} Delete</button> + {{/if}} + </div> http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/templates/components/edit-table.hbs ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/templates/components/edit-table.hbs b/contrib/views/hive20/src/main/resources/ui/app/templates/components/edit-table.hbs new file mode 100644 index 0000000..70e7824 --- /dev/null +++ b/contrib/views/hive20/src/main/resources/ui/app/templates/components/edit-table.hbs @@ -0,0 +1,65 @@ +{{! +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +}} + +<div class="col-md-12"> + {{#tabs-pane tabs=tabs inverse=true as |tab|}} + {{tabs-item tab=tab tabs=tabs}} + {{/tabs-pane}} + + <div class="create-table-inner"> + <div class="row"> + {{#each tabs as |tab|}} + {{#if tab.active}} + {{#if (eq tab.link "edit.table.columns")}} + {{#if hasEmptyColumnsError}} + <div class="alert alert-danger create-table-error"> + {{emptyColumnsErrorText}} + </div> + {{/if}} + {{table-columns columns=columns shouldAddBuckets=shouldAddBuckets editMode=true}} + {{/if}} + {{#if (eq tab.link "edit.table.properties")}} + {{table-properties properties=properties editMode=true}} + {{/if}} + {{#if (eq tab.link "edit.table.advanced")}} + {{table-advanced-settings settings=settings shouldAddBuckets=shouldAddBuckets + errors=settingErrors editMode=true}} + {{/if}} + {{/if}} + {{/each}} + </div> + + </div> + <div class="create-table-controls"> + <div class="row"> + <div class="col-md-2"> + <button class="btn btn-success" {{action "edit"}}>{{fa-icon "edit"}} Edit</button> + <button class="btn btn-warning" {{action "cancel"}}>{{fa-icon "times"}} Cancel</button> + </div> + </div> + <div class="row"> + <div class=" col-md-12 warning"> + <div class="alert alert-danger"> + <p>{{fa-icon "exclamation-circle" size="2"}} <strong>Warning:</strong> Operation is executed as multiple statements and they are not transactional in its entirety.</p> + </div> + </div> + </div> + </div> + + +</div> http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/templates/components/property-item.hbs ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/templates/components/property-item.hbs b/contrib/views/hive20/src/main/resources/ui/app/templates/components/property-item.hbs index e0ba696..26e5d9b 100644 --- a/contrib/views/hive20/src/main/resources/ui/app/templates/components/property-item.hbs +++ b/contrib/views/hive20/src/main/resources/ui/app/templates/components/property-item.hbs @@ -37,9 +37,11 @@ <td> <div class="text-center"> - {{#unless property.editing}} - <button class="btn btn-success" {{action "edit"}}>{{fa-icon "check"}} Edit</button> - {{/unless}} - <button class="btn btn-danger" {{action "delete"}}>{{fa-icon "times"}} Delete</button> + {{#if (or property.newProperty (not editMode)) }} + {{#unless property.editing}} + <button class="btn btn-success" {{action "edit"}}>{{fa-icon "check"}} Edit</button> + {{/unless}} + <button class="btn btn-danger" {{action "delete"}}>{{fa-icon "times"}} Delete</button> + {{/if}} </div> </td> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-advanced-settings.hbs ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-advanced-settings.hbs b/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-advanced-settings.hbs index 18f22f9..f7a92ce 100644 --- a/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-advanced-settings.hbs +++ b/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-advanced-settings.hbs @@ -29,7 +29,7 @@ <label class="col-md-2 control-label">Transactional</label> <div class="col-md-4"> <label> - {{input type="checkbox" checked=settings.transactional}} + {{input type="checkbox" checked=settings.transactional disabled=disableTransactionInput}} </label> </div> </div> @@ -39,7 +39,6 @@ <label class="col-md-2 control-label">Number of buckets</label> <div class="col-md-6"> <div class="{{if hasNumBucketError 'has-error'}}"> - {{input type="number" class="form-control" value=settings.numBuckets}} {{#if hasNumBucketError}} <span class="help-block">{{numBucketErrorText}}</span> @@ -53,174 +52,178 @@ </div> -<div class="panel panel-info"> - <div class="panel-heading"> - <div class="panel-title"> - <button class="btn btn-primary {{if showLocationInput 'active'}}" {{action "toggleLocation"}}> - {{fa-icon (if showLocationInput "minus" "plus")}} - </button> - Add Location +{{#if (not editMode)}} + <div class="panel panel-info"> + <div class="panel-heading"> + <div class="panel-title"> + <button class="btn btn-primary {{if showLocationInput 'active'}}" {{action "toggleLocation"}}> + {{fa-icon (if showLocationInput "minus" "plus")}} + </button> + Add Location + </div> </div> - </div> - {{#if showLocationInput}} - <div class="panel-body"> - <div class="row"> - <div class="col-md-6"> - {{input type="text" class="form-control" value=settings.location}} + {{#if showLocationInput}} + <div class="panel-body"> + <div class="row"> + <div class="col-md-6"> + {{input type="text" class="form-control" value=settings.location}} + </div> + <button class="btn btn-success" {{action "toggleDirectoryViewer"}}>Select HDFS Directory</button> </div> - <button class="btn btn-success" {{action "toggleDirectoryViewer"}}>Select HDFS Directory</button> + {{#if showDirectoryViewer}} + {{hdfs-viewer-modal + showSelectedPath=true + close="closeHdfsModal" + selected="hdfsPathSelected" + }} + {{/if}} </div> - {{#if showDirectoryViewer}} - {{hdfs-viewer-modal - showSelectedPath=true - close="closeHdfsModal" - selected="hdfsPathSelected" - }} - {{/if}} - </div> - {{/if}} -</div> + {{/if}} + </div> -<div class="panel panel-info"> - <div class="panel-heading"> - <div class="panel-title"> - <button class="btn btn-primary {{if showFileFormatInput 'active'}}" {{action "toggleFileFormat"}}> - {{fa-icon (if showFileFormatInput "minus" "plus")}} - </button> - Add File Format + <div class="panel panel-info"> + <div class="panel-heading"> + <div class="panel-title"> + <button class="btn btn-primary {{if showFileFormatInput 'active'}}" {{action "toggleFileFormat"}}> + {{fa-icon (if showFileFormatInput "minus" "plus")}} + </button> + Add File Format + </div> </div> - </div> - {{#if showFileFormatInput}} - <div class="panel-body"> - <div class="row"> - <div class="col-md-6"> - {{#power-select - selected=selectedFileFormat - options=fileFormats - searchField="name" - searchPlaceholder="Enter data type" - onchange=(action "fileFormatSelected") as |parameter|}} - {{parameter.name}} - {{/power-select}} + {{#if showFileFormatInput}} + <div class="panel-body"> + <div class="row"> + <div class="col-md-6"> + {{#power-select + selected=selectedFileFormat + options=fileFormats + searchField="name" + searchPlaceholder="Enter data type" + onchange=(action "fileFormatSelected") as |parameter|}} + {{parameter.name}} + {{/power-select}} + </div> </div> - </div> - {{#if customFileFormat}} - <div class="row fileformat-custom-row"> - <div class="col-md-6 form-horizontal"> - <div class="form-group"> - <label class="col-md-3 control-label">Input Format</label> - <div class="col-md-9"> - {{input type="text" class="form-control" value=settings.fileFormat.inputFormat - placeholder="Input format class"}} + {{#if customFileFormat}} + <div class="row fileformat-custom-row"> + <div class="col-md-6 form-horizontal"> + <div class="form-group"> + <label class="col-md-3 control-label">Input Format</label> + <div class="col-md-9"> + {{input type="text" class="form-control" value=settings.fileFormat.inputFormat + placeholder="Input format class"}} + </div> </div> - </div> - <div class="form-group"> - <label class="col-md-3 control-label">Output Format</label> - <div class="col-md-9"> - {{input type="text" class="form-control" value=settings.fileFormat.outputFormat - placeholder="Output format class"}} + <div class="form-group"> + <label class="col-md-3 control-label">Output Format</label> + <div class="col-md-9"> + {{input type="text" class="form-control" value=settings.fileFormat.outputFormat + placeholder="Output format class"}} + </div> </div> </div> </div> - </div> - {{/if}} + {{/if}} - </div> - {{/if}} -</div> + </div> + {{/if}} + </div> -<div class="panel panel-info"> - <div class="panel-heading"> - <div class="panel-title"> - <button class="btn btn-primary {{if showRowFormatInput 'active'}}" {{action "toggleRowFormat"}}> - {{fa-icon (if showRowFormatInput "minus" "plus")}} - </button> - Add Row Format + <div class="panel panel-info"> + <div class="panel-heading"> + <div class="panel-title"> + <button class="btn btn-primary {{if showRowFormatInput 'active'}}" {{action "toggleRowFormat"}}> + {{fa-icon (if showRowFormatInput "minus" "plus")}} + </button> + Add Row Format + </div> </div> - </div> - {{#if showRowFormatInput}} - <div class="panel-body rowformat-custom-row"> - <div class="row"> - <div class="col-md-6 form-horizontal"> - <div class="form-group"> - <label class="col-md-4 control-label">Fields Terminated By</label> - <div class="col-md-7"> - {{#power-select - selected=selectedFieldTerminator - options=terminationChars - searchField="name" - searchPlaceholder="Enter terminator character" - onchange=(action "fieldTerminatorSelected") as |parameter|}} - {{parameter.name}}{{#if parameter.description}} - {{parameter.description}}{{/if}} - {{/power-select}} - </div> - <div class="col-md-1"> - <a class="text-danger" {{action "clearFieldTerminator"}}>{{fa-icon "times" size="lg"}}</a> + {{#if showRowFormatInput}} + <div class="panel-body rowformat-custom-row"> + <div class="row"> + <div class="col-md-6 form-horizontal"> + <div class="form-group"> + <label class="col-md-4 control-label">Fields Terminated By</label> + <div class="col-md-7"> + {{#power-select + selected=selectedFieldTerminator + options=terminationChars + searchField="name" + searchPlaceholder="Enter terminator character" + onchange=(action "fieldTerminatorSelected") as |parameter|}} + {{parameter.name}}{{#if parameter.description}} - {{parameter.description}}{{/if}} + {{/power-select}} + </div> + <div class="col-md-1"> + <a class="text-danger" {{action "clearFieldTerminator"}}>{{fa-icon "times" size="lg"}}</a> + </div> </div> </div> </div> - </div> - <div class="row"> - <div class="col-md-6 form-horizontal"> - <div class="form-group"> - <label class="col-md-4 control-label">Lines Terminated By</label> - <div class="col-md-7"> - {{#power-select - selected=selectedLinesTerminator - options=terminationChars - searchField="name" - searchPlaceholder="Enter terminator character" - onchange=(action "linesTerminatorSelected") as |parameter|}} - {{parameter.name}}{{#if parameter.description}} - {{parameter.description}}{{/if}} - {{/power-select}} - </div> - <div class="col-md-1"> - <a class="text-danger" {{action "clearLinesTerminator"}}>{{fa-icon "times" size="lg"}}</a> + <div class="row"> + <div class="col-md-6 form-horizontal"> + <div class="form-group"> + <label class="col-md-4 control-label">Lines Terminated By</label> + <div class="col-md-7"> + {{#power-select + selected=selectedLinesTerminator + options=terminationChars + searchField="name" + searchPlaceholder="Enter terminator character" + onchange=(action "linesTerminatorSelected") as |parameter|}} + {{parameter.name}}{{#if parameter.description}} - {{parameter.description}}{{/if}} + {{/power-select}} + </div> + <div class="col-md-1"> + <a class="text-danger" {{action "clearLinesTerminator"}}>{{fa-icon "times" size="lg"}}</a> + </div> </div> </div> </div> - </div> - <div class="row"> - <div class="col-md-6 form-horizontal"> - <div class="form-group"> - <label class="col-md-4 control-label">Null Defined As</label> - <div class="col-md-7"> - {{#power-select - selected=selectedNullDefinition - options=terminationChars - searchField="name" - searchPlaceholder="Enter terminator character" - onchange=(action "nullDefinedAsSelected") as |parameter|}} - {{parameter.name}}{{#if parameter.description}} - {{parameter.description}}{{/if}} - {{/power-select}} - </div> - <div class="col-md-1"> - <a class="text-danger" {{action "clearNullDefinition"}}>{{fa-icon "times" size="lg"}}</a> + <div class="row"> + <div class="col-md-6 form-horizontal"> + <div class="form-group"> + <label class="col-md-4 control-label">Null Defined As</label> + <div class="col-md-7"> + {{#power-select + selected=selectedNullDefinition + options=terminationChars + searchField="name" + searchPlaceholder="Enter terminator character" + onchange=(action "nullDefinedAsSelected") as |parameter|}} + {{parameter.name}}{{#if parameter.description}} - {{parameter.description}}{{/if}} + {{/power-select}} + </div> + <div class="col-md-1"> + <a class="text-danger" {{action "clearNullDefinition"}}>{{fa-icon "times" size="lg"}}</a> + </div> </div> </div> </div> - </div> - <div class="row"> - <div class="col-md-6 form-horizontal"> - <div class="form-group"> - <label class="col-md-4 control-label">Escape Defined As</label> - <div class="col-md-7"> - {{#power-select - selected=selectedEscapeDefinition - options=terminationChars - searchField="name" - searchPlaceholder="Enter terminator chanracter" - onchange=(action "escapeDefinedAsSelected") as |parameter|}} - {{parameter.name}}{{#if parameter.description}} - {{parameter.description}}{{/if}} - {{/power-select}} - </div> - <div class="col-md-1"> - <a class="text-danger" {{action "clearEscapeDefinition"}}>{{fa-icon "times" size="lg"}}</a> + <div class="row"> + <div class="col-md-6 form-horizontal"> + <div class="form-group"> + <label class="col-md-4 control-label">Escape Defined As</label> + <div class="col-md-7"> + {{#power-select + selected=selectedEscapeDefinition + options=terminationChars + searchField="name" + searchPlaceholder="Enter terminator chanracter" + onchange=(action "escapeDefinedAsSelected") as |parameter|}} + {{parameter.name}}{{#if parameter.description}} - {{parameter.description}}{{/if}} + {{/power-select}} + </div> + <div class="col-md-1"> + <a class="text-danger" {{action "clearEscapeDefinition"}}>{{fa-icon "times" size="lg"}}</a> + </div> </div> </div> </div> </div> - </div> - {{/if}} -</div> + {{/if}} + </div> +{{/if}} + + http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-columns.hbs ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-columns.hbs b/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-columns.hbs index f5fc547..b942136 100644 --- a/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-columns.hbs +++ b/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-columns.hbs @@ -31,6 +31,7 @@ {{column-item column=column columnDeleted="columnDeleted" columnUpdated="columnUpdated" + editMode=editMode }} {{/each}} <tr class="new-settings text-center"> http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-properties.hbs ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-properties.hbs b/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-properties.hbs index 953ef84..802941e 100644 --- a/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-properties.hbs +++ b/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-properties.hbs @@ -29,6 +29,7 @@ {{property-item property=property propertyItemDeleted="itemDeleted" propertyItemUpdated="itemUpdated" + editMode=editMode }} {{/each}} <tr class="new-settings text-center"> http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-statistics.hbs ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-statistics.hbs b/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-statistics.hbs index 0ee3b13..5f62fca 100644 --- a/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-statistics.hbs +++ b/contrib/views/hive20/src/main/resources/ui/app/templates/components/table-statistics.hbs @@ -51,6 +51,10 @@ <td>{{tableStats.numFiles}}</td> </tr> <tr> + <td>Number of Rows</td> + <td>{{tableStats.numRows}}</td> + </tr> + <tr> <td>Raw Data Size</td> <td>{{tableStats.rawDataSize}}</td> </tr> http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/templates/databases/database/tables/table.hbs ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/templates/databases/database/tables/table.hbs b/contrib/views/hive20/src/main/resources/ui/app/templates/databases/database/tables/table.hbs index e3fe400..9a1306a 100644 --- a/contrib/views/hive20/src/main/resources/ui/app/templates/databases/database/tables/table.hbs +++ b/contrib/views/hive20/src/main/resources/ui/app/templates/databases/database/tables/table.hbs @@ -25,7 +25,7 @@ {{fa-icon "navicon"}} </button> <ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenu1"> - <li><a href="#" class="text-uppercase" {{action "editTable" model}}>{{fa-icon "edit"}} Edit</a></li> + <li>{{#link-to "databases.database.tables.table.edit" class="text-uppercase"}}{{fa-icon "edit"}} Edit{{/link-to}}</li> <li>{{#link-to "databases.database.tables.table.rename" class="text-uppercase"}}{{fa-icon "edit"}} Rename{{/link-to}}</li> <li><a href="#" class="text-uppercase" {{action "deleteTable" model}}>{{fa-icon "trash"}} Delete</a></li> </ul> http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/main/resources/ui/app/templates/databases/database/tables/table/edit.hbs ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/main/resources/ui/app/templates/databases/database/tables/table/edit.hbs b/contrib/views/hive20/src/main/resources/ui/app/templates/databases/database/tables/table/edit.hbs new file mode 100644 index 0000000..79f1701 --- /dev/null +++ b/contrib/views/hive20/src/main/resources/ui/app/templates/databases/database/tables/table/edit.hbs @@ -0,0 +1,45 @@ +{{! +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +}} + +<div class="row"> + <div class="alert alert-info"> + <p class="lead">{{fa-icon "pencil-square-o" size=1}} Edit table <strong class="text-uppercase">{{table.table}}</strong></p> + </div> +</div> + +<div class="row"> + {{edit-table tabs=tabs + table=table + cancel="cancel" + edit="edit"}} +</div> + +{{#if showModal}} + {{#modal-dialog + translucentOverlay=true + container-class="modal-dialog modal-sm"}} + <div class="modal-content"> + <div class="modal-header text-danger"> + <p class="modal-title">{{fa-icon "plus"}} Create Database</p> + </div> + <div class="modal-body text-center text-primary"> + <p>{{modalMessage}}</p> + </div> + </div><!-- /.modal-content --> + {{/modal-dialog}} +{{/if}} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/test/java/org/apache/ambari/view/hive20/internal/query/generators/AlterTableQueryGenerationSpecTest.groovy ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/test/java/org/apache/ambari/view/hive20/internal/query/generators/AlterTableQueryGenerationSpecTest.groovy b/contrib/views/hive20/src/test/java/org/apache/ambari/view/hive20/internal/query/generators/AlterTableQueryGenerationSpecTest.groovy deleted file mode 100644 index 874e268..0000000 --- a/contrib/views/hive20/src/test/java/org/apache/ambari/view/hive20/internal/query/generators/AlterTableQueryGenerationSpecTest.groovy +++ /dev/null @@ -1,59 +0,0 @@ -/* -* Licensed to the Apache Software Foundation (ASF) under one -* or more contributor license agreements. See the NOTICE file -* distributed with this work for additional information -* regarding copyright ownership. The ASF licenses this file -* to you under the Apache License, Version 2.0 (the -* "License"); you may not use this file except in compliance -* with the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ - - -package org.apache.ambari.view.hive20.internal.query.generators - -import spock.lang.Specification - -class AlterTableQueryGenerationSpecTest extends Specification { -// def "alter simple table"() { -// // blocks go here -// setup: -// def oldTableMeta = new TableMeta() -// def newTableMeta = new TableMeta() -// def oldCols = new ArrayList<>(); -// oldCols.add(new ColumnInfo()) -// oldTableMeta.setColumns() -// -// when: -// stack.push(elem) -// -// then: -// println "inside AlterTableQueryGenerationSpecTest" -// !stack.empty -// stack.size() == 1 -// stack.peek() == elem -// } -// -// def "pushing again an element on the stack"() { -// // blocks go here -// setup: -// def stack = new Stack() -// def elem = "push me" -// -// when: -// stack.push(elem) -// -// then: -// println "inside AlterTableQueryGenerationSpecTest" -// !stack.empty -// stack.size() == 1 -// stack.peek() == elem -// } -} http://git-wip-us.apache.org/repos/asf/ambari/blob/151d2f16/contrib/views/hive20/src/test/java/org/apache/ambari/view/hive20/internal/query/generators/AlterTableQueryGeneratorTest.java ---------------------------------------------------------------------- diff --git a/contrib/views/hive20/src/test/java/org/apache/ambari/view/hive20/internal/query/generators/AlterTableQueryGeneratorTest.java b/contrib/views/hive20/src/test/java/org/apache/ambari/view/hive20/internal/query/generators/AlterTableQueryGeneratorTest.java index 45f29da..35ea416 100644 --- a/contrib/views/hive20/src/test/java/org/apache/ambari/view/hive20/internal/query/generators/AlterTableQueryGeneratorTest.java +++ b/contrib/views/hive20/src/test/java/org/apache/ambari/view/hive20/internal/query/generators/AlterTableQueryGeneratorTest.java @@ -19,24 +19,19 @@ package org.apache.ambari.view.hive20.internal.query.generators; import com.google.common.base.Optional; +import com.google.gson.Gson; import org.apache.ambari.view.hive20.internal.dto.ColumnInfo; import org.apache.ambari.view.hive20.internal.dto.TableMeta; import org.junit.Assert; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Arrays; import java.util.List; public class AlterTableQueryGeneratorTest { - @Test - public void getQuery() throws Exception { - - } - - @Test - public void generateColumnQuery() throws Exception { - - } + private static final Logger LOG = LoggerFactory.getLogger(AlterTableQueryGeneratorTest.class); @Test public void createColumnQueriesForSuccessfulChangeColumn() throws Exception { @@ -86,9 +81,155 @@ public class AlterTableQueryGeneratorTest { List<String> queries = query.get(); Assert.assertEquals("Expected number of column update queries were different.", 4, queries.size()); - System.out.println(queries); String[] expectedQueries = new String[]{" CHANGE COLUMN `col1` `col4` VARCHAR(10) COMMENT \'COMMENT 4\'", " CHANGE COLUMN `col2` `col5` STRING COMMENT \'COMMENT 5\'", " CHANGE COLUMN `col3` `col6` INT"," ADD COLUMNS ( `col7` DATE, `col8` BOOLEAN COMMENT \'COMMENT 8\' )" }; Assert.assertArrayEquals("Column change queries were not equal ", expectedQueries, queries.toArray()); } + + @Test + public void createColumnQueriesForSuccessfulChangeSomeColumns() throws Exception { + + TableMeta oldMeta = new TableMeta(); + TableMeta newMeta = new TableMeta(); + + ColumnInfo colInfo1 = new ColumnInfo("col1", "CHAR(1)", "COMMENT 1"); // with comment + ColumnInfo colInfo2 = new ColumnInfo("col2", "DECIMAL(10,5)"); // no comment + ColumnInfo colInfo3 = new ColumnInfo("col3", "STRING", "COMMENT-3"); + ColumnInfo colInfo4 = new ColumnInfo("col4", "VARCHAR(10)", "COMMENT 4"); + ColumnInfo colInfo5 = new ColumnInfo("col5", "STRING", "COMMENT 5"); + ColumnInfo colInfo6 = new ColumnInfo("col6", "INT"); + ColumnInfo colInfo7 = new ColumnInfo("col7", "DATE"); + ColumnInfo colInfo8 = new ColumnInfo("col8", "BOOLEAN", "COMMENT 8"); + + List<ColumnInfo> oldColumns = Arrays.asList(colInfo1, colInfo2, colInfo3); + oldMeta.setColumns(oldColumns); + + List<ColumnInfo> newColumns = Arrays.asList(colInfo1, colInfo5, colInfo6); // all changed + oldMeta.setColumns(newColumns); + + Optional<List<String>> query = AlterTableQueryGenerator.createColumnQueries(oldColumns, newColumns, false); + + Assert.assertTrue(query.isPresent()); + List<String> queries = query.get(); + + Assert.assertEquals("Expected number of column update queries were different.", 2, queries.size()); + String[] expectedQueries = new String[]{" CHANGE COLUMN `col2` `col5` STRING COMMENT 'COMMENT 5'", " CHANGE COLUMN `col3` `col6` INT"}; + + Assert.assertArrayEquals("Column change queries were not equal ", expectedQueries, queries.toArray()); + } + + @Test + public void createColumnQueriesForSuccessfulAddColumns() throws Exception { + + TableMeta oldMeta = new TableMeta(); + TableMeta newMeta = new TableMeta(); + + ColumnInfo colInfo1 = new ColumnInfo("col1", "CHAR(1)", "COMMENT 1"); // with comment + ColumnInfo colInfo2 = new ColumnInfo("col2", "DECIMAL(10,5)"); // no comment + ColumnInfo colInfo3 = new ColumnInfo("col3", "STRING", "COMMENT-3"); + ColumnInfo colInfo4 = new ColumnInfo("col4", "VARCHAR(10)", "COMMENT 4"); + ColumnInfo colInfo5 = new ColumnInfo("col5", "STRING", "COMMENT 5"); + ColumnInfo colInfo6 = new ColumnInfo("col6", "INT"); + ColumnInfo colInfo7 = new ColumnInfo("col7", "DATE"); + ColumnInfo colInfo8 = new ColumnInfo("col8", "BOOLEAN", "COMMENT 8"); + + List<ColumnInfo> oldColumns = Arrays.asList(colInfo1, colInfo2, colInfo3); + oldMeta.setColumns(oldColumns); + + List<ColumnInfo> newColumns = Arrays.asList(colInfo1, colInfo2, colInfo3, colInfo5, colInfo6); // all changed + oldMeta.setColumns(newColumns); + + Optional<List<String>> query = AlterTableQueryGenerator.createColumnQueries(oldColumns, newColumns, false); + + Assert.assertTrue(query.isPresent()); + List<String> queries = query.get(); + + Assert.assertEquals("Expected number of column update queries were different.", 1, queries.size()); + String[] expectedQueries = new String[]{" ADD COLUMNS ( `col5` STRING COMMENT 'COMMENT 5', `col6` INT )"}; + + Assert.assertArrayEquals("Column change queries were not equal ", expectedQueries, queries.toArray()); + } + + @Test + public void getQueryWithAlterColumn(){ + String origMetaString = "{ " + + " \"database\": \"default\", " + + " \"table\": \"table2\", " + + " \"columns\": [{ " + + " \"name\": \"COL1\", " + + " \"type\": \"TINYINT\", " + + " \"comment\": \"\", " + + " \"precision\": null, " + + " \"scale\": null " + + " }, { " + + " \"name\": \"col2\", " + + " \"type\": \"VARCHAR\", " + + " \"comment\": \"\", " + + " \"precision\": \"333\", " + + " \"scale\": null " + + " }, { " + + " \"name\": \"col3\", " + + " \"type\": \"DECIMAL\", " + + " \"comment\": \"\", " + + " \"precision\": \"33\", " + + " \"scale\": \"3\" " + + " }], " + + " \"partitionInfo\": { " + + " \"columns\": [] " + + " }, " + + " \"detailedInfo\": { " + + " \"parameters\": {} " + + " }, " + + " \"storageInfo\": {} " + + " }"; + + String newMetaString = "{ " + + " \"database\": \"default\", " + + " \"table\": \"table2\", " + + " \"columns\": [{ " + + " \"name\": \"col1\", " + + " \"type\": \"TINYINT\", " + + " \"comment\": \"\", " + + " \"precision\": null, " + + " \"scale\": null " + + " }, { " + + " \"name\": \"col3\", " + + " \"type\": \"STRING\", " + + " \"comment\": \"\", " + + " \"precision\": \"333\", " + + " \"scale\": null " + + " }, { " + + " \"name\": \"col4\", " + + " \"type\": \"TINYINT\", " + + " \"comment\": \"\", " + + " \"precision\": null, " + + " \"scale\": null " + + " }], " + + " \"partitionInfo\": { " + + " \"columns\": [] " + + " }, " + + " \"detailedInfo\": { " + + " \"parameters\": {} " + + " }, " + + " \"storageInfo\": {} " + + " }"; + + Gson gson = new Gson(); + TableMeta origTableMeta = gson.fromJson(origMetaString, TableMeta.class); + TableMeta updatedTableMeta = gson.fromJson(newMetaString, TableMeta.class); + + LOG.info("origTableMeta : {},\n\nupdatedTableMeta : {}", origMetaString, updatedTableMeta); + + AlterTableQueryGenerator generator = new AlterTableQueryGenerator(origTableMeta, updatedTableMeta); + + Optional<String> query = generator.getQuery(); + Assert.assertTrue(query.isPresent()); + String hqlQuery = query.get(); + + LOG.info("hqlQuery : {}", hqlQuery); + + String expectedQuery = " ALTER TABLE `default.table2` CHANGE COLUMN `col2` `col3` STRING(333);\n" + + " ALTER TABLE `default.table2` CHANGE COLUMN `col3` `col4` TINYINT"; + Assert.assertEquals("Alter Edit table query did not match ", expectedQuery, hqlQuery); + } } \ No newline at end of file