ソースを参照

HUE-9381. [frontend] Table ERD (sree)

sreenaths 5 年 前
コミット
85e179cf99
27 ファイル変更902 行追加25 行削除
  1. 13 1
      .eslintrc.js
  2. 7 0
      apps/metastore/src/metastore/conf.py
  3. 0 0
      apps/metastore/src/metastore/static/metastore/css/metastore.css
  4. 17 0
      apps/metastore/src/metastore/static/metastore/less/metastore.less
  5. 24 0
      apps/metastore/src/metastore/templates/metastore.mako
  6. 2 0
      desktop/conf.dist/hue.ini
  7. 2 0
      desktop/conf/pseudo-distributed.ini.tmpl
  8. 2 0
      desktop/core/src/desktop/js/apps/tableBrowser/app.js
  9. 3 3
      desktop/core/src/desktop/js/apps/tableBrowser/metastoreDatabase.js
  10. 3 0
      desktop/core/src/desktop/js/apps/tableBrowser/metastoreNamespace.js
  11. 17 0
      desktop/core/src/desktop/js/apps/tableBrowser/metastoreViewModel.js
  12. 76 0
      desktop/core/src/desktop/js/apps/tableBrowser/propsMappers.ts
  13. 32 0
      desktop/core/src/desktop/js/components/er-diagram/comps/literal-entity.scss
  14. 39 0
      desktop/core/src/desktop/js/components/er-diagram/comps/literal-entity.vue
  15. 56 0
      desktop/core/src/desktop/js/components/er-diagram/comps/table-entity.scss
  16. 52 0
      desktop/core/src/desktop/js/components/er-diagram/comps/table-entity.vue
  17. 113 0
      desktop/core/src/desktop/js/components/er-diagram/er-diagram.scss
  18. 142 0
      desktop/core/src/desktop/js/components/er-diagram/index.vue
  19. 70 0
      desktop/core/src/desktop/js/components/er-diagram/lib/entities.ts
  20. 23 0
      desktop/core/src/desktop/js/components/er-diagram/lib/enums.ts
  21. 30 0
      desktop/core/src/desktop/js/components/er-diagram/lib/interfaces.ts
  22. 113 0
      desktop/core/src/desktop/js/components/er-diagram/lib/processor.ts
  23. 23 0
      desktop/core/src/desktop/js/components/er-diagram/webcomp.ts
  24. 34 21
      desktop/core/src/desktop/js/utils/hueUtils.js
  25. 5 0
      package-lock.json
  26. 1 0
      package.json
  27. 3 0
      tsconfig.json

+ 13 - 1
.eslintrc.js

@@ -74,7 +74,19 @@ module.exports = {
         'plugin:@typescript-eslint/recommended'
       ],
       parser: 'vue-eslint-parser',
-      plugins: ['vue', '@typescript-eslint']
+      plugins: ['vue', '@typescript-eslint'],
+      rules: {
+        'vue/max-attributes-per-line': [
+          'error',
+          {
+            singleline: 10,
+            multiline: {
+              max: 1,
+              allowFirstLine: false
+            }
+          }
+        ]
+      }
     },
     {
       files: ['*.ts'],

+ 7 - 0
apps/metastore/src/metastore/conf.py

@@ -33,3 +33,10 @@ FORCE_HS2_METADATA = Config(
   type=bool,
   default=False
 )
+
+SHOW_TABLE_ERD = Config(
+  key="show_table_erd",
+  default=False,
+  type=bool,
+  help=_('Choose whether to show the table ERD component.')
+)

ファイルの差分が大きいため隠しています
+ 0 - 0
apps/metastore/src/metastore/static/metastore/css/metastore.css


+ 17 - 0
apps/metastore/src/metastore/static/metastore/less/metastore.less

@@ -19,6 +19,23 @@
 @import (reference) "hue-mixins.less";
 @import (reference) "hue-helpers.less";
 
+@keyframes erd-slide-in {
+  from {
+    margin-left: 100px;
+    opacity: 0;
+  }
+  to {
+    margin-left: 0px;
+    opacity: 1;
+  }
+}
+
+.erd-animated {
+  .erd-container {
+    animation: erd-slide-in 0.25s;
+  }
+}
+
 #metastoreComponents {
   .divider {
     padding-left: 12px;

+ 24 - 0
apps/metastore/src/metastore/templates/metastore.mako

@@ -21,6 +21,7 @@ from desktop import conf
 from desktop.conf import USE_NEW_EDITOR
 from desktop.lib.i18n import smart_unicode
 from desktop.views import commonheader, commonfooter, _ko
+from metastore.conf import SHOW_TABLE_ERD
 from beeswax.conf import LIST_PARTITIONS_LIMIT
 from webpack_loader.templatetags.webpack_loader import render_bundle
 
@@ -61,6 +62,7 @@ ${ commonheader(_("Metastore"), app_name, user, request) | n,unicode }
 <link rel="stylesheet" href="${ static('desktop/ext/css/bootstrap-editable.css') }">
 <link rel="stylesheet" href="${ static('notebook/css/notebook.css') }">
 
+${ render_bundle('vendors~jobBrowser~tableBrowser') | n,unicode }
 ${ render_bundle('tableBrowser') | n,unicode }
 
 <span class="notebook">
@@ -822,6 +824,11 @@ ${ components.menubar(is_embeddable) }
       <a href="javascript: void(0);" data-bind="click: function() { $root.currentTab('privileges'); }">${ _('Privileges') }</a>
     </li>
     <!-- /ko -->
+    % if SHOW_TABLE_ERD.get():
+    <li data-bind="css: { 'active' : $root.currentTab() === 'erd' || $root.currentTab() === 'erd-animated' }">
+      <a href="javascript: void(0);" data-bind="click: function() { $root.currentTab('erd'); }">${ _('Table Relations') }</a>
+    </li>
+    % endif
   </ul>
 
   <div class="tab-content margin-top-10" style="border: none; overflow: hidden">
@@ -857,6 +864,23 @@ ${ components.menubar(is_embeddable) }
       <!-- ko if: $root.currentTab() === 'privileges' -->
         <div data-bind="component: { name: 'hue-sentry-privileges', params: { isSentryAdmin: false, readOnly: true, server: 'server1', path: catalogEntry.path.join('.')}}"></div>
       <!-- /ko -->
+
+      <!-- ko if: $root.currentTab() === 'erd' || $root.currentTab() === 'erd-animated' -->
+        <!-- ko hueSpinner: { spin: loading, center: true, size: 'xlarge' } --><!-- /ko -->
+        <!-- ko ifnot: loading -->
+          <er-diagram data-bind="
+            vueProps: $root.propsMappers.tableERD(database.table().catalogEntry),
+            vueEvents: {
+              'entity-clicked': function (event){
+                const table = event.detail[0];
+                $root.setDbAndTableByName(table.database, table.name, () => $root.currentTab('erd-animated'));
+              }
+            },
+            class: $root.currentTab()
+          "></er-diagram>
+        <!-- /ko -->
+      <!-- /ko -->
+
     </div>
   </div>
 </script>

+ 2 - 0
desktop/conf.dist/hue.ini

@@ -1314,6 +1314,8 @@
   # Flag to force all metadata calls (e.g. list tables, table or column details...) to happen via HiveServer2 if available instead of Impala.
   ## force_hs2_metadata=false
 
+  # Choose whether to show the table ERD component. Default false
+  ## show_table_erd=false
 
 ###########################################################################
 # Settings to configure Impala

+ 2 - 0
desktop/conf/pseudo-distributed.ini.tmpl

@@ -1299,6 +1299,8 @@
   # Flag to force all metadata calls (e.g. list tables, table or column details...) to happen via HiveServer2 if available instead of Impala.
   ## force_hs2_metadata=false
 
+  # Choose whether to show the table ERD component. Default false
+  ## show_table_erd=false
 
 ###########################################################################
 # Settings to configure Impala

+ 2 - 0
desktop/core/src/desktop/js/apps/tableBrowser/app.js

@@ -24,6 +24,8 @@ import hueUtils from 'utils/hueUtils';
 import I18n from 'utils/i18n';
 import { GET_KNOWN_CONFIG_EVENT, CONFIG_REFRESHED_EVENT } from 'utils/hueConfig';
 
+import 'components/er-diagram/webcomp';
+
 const HUE_PUB_SUB_EDITOR_ID = 'metastore';
 
 if (ko.options) {

+ 3 - 3
desktop/core/src/desktop/js/apps/tableBrowser/metastoreDatabase.js

@@ -182,14 +182,14 @@ class MetastoreDatabase {
     apiHelper.setInTotalStorage('metastore', 'last.selected.database', this.name);
   }
 
-  setTableByName(tableName) {
+  setTableByName(tableName, callback) {
     if (this.table() && this.table().catalogEntry.name === tableName) {
       return;
     }
 
     this.tables().some(metastoreTable => {
       if (metastoreTable.catalogEntry.name === tableName) {
-        this.setTable(metastoreTable);
+        this.setTable(metastoreTable, callback);
         return true;
       }
     });
@@ -201,10 +201,10 @@ class MetastoreDatabase {
     if (!metastoreTable.loaded()) {
       metastoreTable.load();
     }
+    this.metastoreViewModel.currentTab('overview');
     if (callback) {
       callback();
     }
-    this.metastoreViewModel.currentTab('overview');
   }
 
   showContextPopover(entry, event, orientation) {

+ 3 - 0
desktop/core/src/desktop/js/apps/tableBrowser/metastoreNamespace.js

@@ -131,6 +131,9 @@ class MetastoreNamespace {
         this.setDatabase(database, () => {
           huePubSub.publish('metastore.url.change');
         });
+        if (this.database()) {
+          this.database().table(null);
+        }
         return true;
       }
     });

+ 17 - 0
desktop/core/src/desktop/js/apps/tableBrowser/metastoreViewModel.js

@@ -22,6 +22,7 @@ import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
 import MetastoreSource from 'apps/tableBrowser/metastoreSource';
 import dataCatalog from 'catalog/dataCatalog';
+import * as propsMappers from './propsMappers';
 import { findEditorConnector, GET_KNOWN_CONFIG_EVENT } from 'utils/hueConfig';
 
 class MetastoreViewModel {
@@ -45,6 +46,8 @@ class MetastoreViewModel {
     this.navigatorEnabled = ko.observable(options.navigatorEnabled || false);
     this.appConfig = ko.observable();
 
+    this.propsMappers = propsMappers;
+
     this.source = ko.observable();
     this.sources = ko.observableArray();
 
@@ -66,6 +69,20 @@ class MetastoreViewModel {
       huePubSub.publish('metastore.url.change');
     };
 
+    this.setDbAndTableByName = (dbName, tableName, callback) => {
+      this.source()
+        .namespace()
+        .setDatabaseByName(dbName, () => {
+          this.source()
+            .namespace()
+            .database()
+            .setTableByName(tableName, () => {
+              this.sourceChanged();
+              callback();
+            });
+        });
+    };
+
     this.loading = ko.pureComputed(() => !this.source() || this.source().loading());
 
     // TODO: Support dynamic config changes

+ 76 - 0
desktop/core/src/desktop/js/apps/tableBrowser/propsMappers.ts

@@ -0,0 +1,76 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  Cloudera, Inc. 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 { Table, Column } from '../../components/er-diagram/lib/entities';
+
+function createTable(database: string, name: string, columns: Array<string>) {
+  return new Table(database, name, columns);
+}
+
+function getTables(
+  fromDB: string,
+  fromTableName: string,
+  columns: Array<string>,
+  foreignKeys: Array<unknown>
+) {
+  const fromTable = createTable(fromDB, fromTableName, columns);
+  const tables = [fromTable];
+  const createdTableIdSet = new Set([fromTable.id]);
+
+  foreignKeys.forEach((key: unknown) => {
+    const toPath = key.to.split('.');
+    const tableId = Table.buildId(toPath[0], toPath[1]);
+    if (!createdTableIdSet.has(tableId)) {
+      createdTableIdSet.add(tableId);
+      tables.push(createTable(toPath[0], toPath[1], [toPath[2]]));
+    }
+  });
+
+  return tables;
+}
+
+function getForeignKeyRelations(
+  fromDB: string,
+  fromTableName: string,
+  foreignKeys: Array<unknown>
+) {
+  const fromTableId = Table.buildId(fromDB, fromTableName);
+  return foreignKeys.map(key => {
+    const toPath = key.to.split('.');
+    return {
+      desc: 'Foreign Key',
+      left: new Column(fromTableId, key.name),
+      right: new Column(Table.buildId(toPath[0], toPath[1]), toPath[2])
+    };
+  });
+}
+
+export function tableERD(catalogEntry: unknown): unknown {
+  const fromDB: string = catalogEntry.path[0];
+  const fromTable: string = catalogEntry.path[1];
+
+  return {
+    entities: getTables(
+      fromDB,
+      fromTable,
+      catalogEntry.sourceMeta.columns,
+      catalogEntry.sourceMeta.foreign_keys
+    ),
+    relations: getForeignKeyRelations(fromDB, fromTable, catalogEntry.sourceMeta.foreign_keys)
+  };
+}

+ 32 - 0
desktop/core/src/desktop/js/components/er-diagram/comps/literal-entity.scss

@@ -0,0 +1,32 @@
+/*
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+*/
+
+.literal-entity {
+  border: 1px solid #d6d8db;
+  border-radius: 5px;
+  padding: 5px;
+  background-color: #FFF;
+
+  position: relative;
+
+  box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.05) inset, 0px 0px 8px rgba(0, 0, 0, 0.15);
+
+  .literal-value {
+    text-align: center;
+  }
+}

+ 39 - 0
desktop/core/src/desktop/js/components/er-diagram/comps/literal-entity.vue

@@ -0,0 +1,39 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+-->
+
+<template>
+  <div class="literal-entity">
+    <div class="left-point" />
+    <div class="right-point" />
+    <div class="literal-value">
+      {{ entity.value }}
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { Component, Prop, Vue } from 'vue-property-decorator';
+  import { Literal } from '../lib/entities';
+
+  import './literal-entity.scss';
+
+  @Component
+  export default class LiteralEntity extends Vue {
+    @Prop() entity: Literal;
+  }
+</script>

+ 56 - 0
desktop/core/src/desktop/js/components/er-diagram/comps/table-entity.scss

@@ -0,0 +1,56 @@
+/*
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+*/
+
+.table-entity {
+  border: 1px solid #d6d8db;
+  border-radius: 5px;
+  padding: 5px;
+
+  cursor: pointer;
+
+  box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.05) inset, 0px 0px 8px rgba(0, 0, 0, 0.15);
+
+  background-color: #EFEFEF;
+
+  .db-name {
+    color: rgb(5, 5, 5);
+    font-size: 0.8em;
+    text-align: center;
+  }
+  .table-name {
+    color: #0a78a3;
+    text-align: center;
+    margin-top: -5px;
+  }
+
+  .columns-container {
+    padding: 0 10px;
+
+    .column-entity {
+      padding: 5px;
+      text-align: center;
+      background-color: #FFF;
+
+      border: 1px solid #d6d8db;
+      border-radius: 5px;
+      margin: 5px 0;
+
+      position: relative;
+    }
+  }
+}

+ 52 - 0
desktop/core/src/desktop/js/components/er-diagram/comps/table-entity.vue

@@ -0,0 +1,52 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+-->
+
+<template>
+  <div class="table-entity" @click="$emit('click')">
+    <div class="db-name">
+      {{ entity.database }}
+    </div>
+    <div class="table-name">
+      {{ entity.name }}
+    </div>
+    <div class="columns-container">
+      <div
+        v-for="column in entity.columns"
+        :key="column"
+        class="column-entity"
+        :data-erd-id="entity.id + '.' + column"
+      >
+        <div class="left-point" />
+        <div class="right-point" />
+        {{ column }}
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { Component, Prop, Vue } from 'vue-property-decorator';
+  import { Table } from '../lib/entities';
+
+  import './table-entity.scss';
+
+  @Component
+  export default class TableEntity extends Vue {
+    @Prop() entity: Table;
+  }
+</script>

+ 113 - 0
desktop/core/src/desktop/js/components/er-diagram/er-diagram.scss

@@ -0,0 +1,113 @@
+/*
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+*/
+
+// Colors
+$bg-color: #FFF;
+$dot-color: #BBB;
+
+// Dimensions
+$dot-size: 1px;
+$dot-space: 10px;
+
+.er-diagram {
+  color: #0a78a3;
+  border: 1px solid #d6d8db;
+  border-radius: 5px;
+
+  position: relative;
+
+  user-select: none;
+
+  background:
+    linear-gradient(90deg, $bg-color ($dot-space - $dot-size), transparent 1%) center,
+    linear-gradient($bg-color ($dot-space - $dot-size), transparent 1%) center, $dot-color;
+  background-size: $dot-space $dot-space;
+
+  .buttons-panel {
+    position: absolute;
+    top: 5px;
+    right: 5px;
+    z-index: 100;
+  }
+
+  .erd-scroll-panel {
+    overflow-x: scroll;
+    position: relative;
+  }
+
+  &:fullscreen {
+    .erd-scroll-panel {
+      overflow-y: scroll;
+      height: 100%;
+    }
+  }
+
+  .erd-container {
+    white-space: nowrap;
+    min-height: 500px;
+
+    position: relative;
+
+    .entity-group {
+      display: inline-block;
+      width: 200px;
+      padding: 15px 30px;
+
+      white-space: normal;
+
+      vertical-align: top;
+
+      .entity-container {
+        padding: 10px 0;
+
+        .left-point, .right-point {
+          position: absolute;
+          width: 0px;
+          height: 0px;
+          top: 1em;
+        }
+
+        .left-point {
+          left: -1px;
+        }
+        .right-point {
+          right: -1px;
+        }
+
+      }
+    }
+  }
+
+  .erd-relations {
+    position: absolute;
+    overflow: visible;
+
+    top: 0;
+    left: 0;
+
+    opacity: 0.5;
+
+    pointer-events: none;
+
+    .relation-path {
+      stroke: #0a78a3;
+      stroke-width: 3;
+      fill: none;
+    }
+  }
+}

+ 142 - 0
desktop/core/src/desktop/js/components/er-diagram/index.vue

@@ -0,0 +1,142 @@
+<!--
+  Licensed to Cloudera, Inc. under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  Cloudera, Inc. 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.
+-->
+
+<template>
+  <div class="er-diagram">
+    <div class="buttons-panel">
+      <button class="btn btn-default btn-sm" title="Toggle Fullscreen" @click="toggleFS">
+        <em class="fa fa-expand" />
+      </button>
+    </div>
+
+    <div class="erd-scroll-panel">
+      <div class="erd-container">
+        <div v-for="(group, index) in groups" :key="index" class="entity-group">
+          <div v-for="entity in group" :key="entity.id" class="entity-container">
+            <TableEntity
+              v-if="entity.type === EntityTypes.Table"
+              :entity="entity"
+              @click="entityClicked(entity)"
+            />
+            <LiteralEntity
+              v-else-if="entity.type === EntityTypes.Literal"
+              :entity="entity"
+              :data-erd-id="entity.id"
+            />
+          </div>
+        </div>
+
+        <!-- Relations -->
+        <svg class="erd-relations">
+          <path
+            v-for="(relation, index) in relations"
+            :key="index"
+            :data-erd-left="relation.left.id"
+            :data-erd-right="relation.right.id"
+            class="relation-path"
+          />
+        </svg>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { Component, Prop, Vue } from 'vue-property-decorator';
+
+  import { groupEntities } from './lib/processor';
+  import { IEntity, IRelation } from './lib/interfaces';
+  import { EntityTypes } from './lib/enums';
+
+  import { toggleFullScreen } from 'utils/hueUtils';
+
+  import TableEntity from './comps/table-entity.vue';
+  import LiteralEntity from './comps/literal-entity.vue';
+
+  import './er-diagram.scss';
+
+  const CURVATURE: number = 40;
+
+  @Component({
+    components: {
+      TableEntity,
+      LiteralEntity
+    }
+  })
+  export default class ERDiagram extends Vue {
+    @Prop() entities: Array<IEntity>;
+    @Prop() relations: Array<IRelation>;
+
+    EntityTypes = EntityTypes;
+
+    get groups(): Object {
+      if (this.entities && this.relations) {
+        return groupEntities(this.entities, this.relations);
+      }
+    }
+
+    getSelectorPosition(selector: string, offset: any): any {
+      const element = this.$el.querySelector(selector);
+      const rect = element.getBoundingClientRect();
+      let x = rect.left;
+      let y = rect.top;
+
+      if (offset) {
+        x = x - offset.x;
+        y = y - offset.y + 1;
+      }
+
+      return { x, y };
+    }
+
+    plotRelations(): void {
+      const relationPaths: Array<any> = this.$el.querySelectorAll('.relation-path');
+      const offset = this.getSelectorPosition('.erd-relations');
+
+      relationPaths.forEach((element: any) => {
+        const leftPos = this.getSelectorPosition(
+          `[data-erd-id="${element.dataset.erdLeft}"] .right-point`,
+          offset
+        );
+        const rightPos = this.getSelectorPosition(
+          `[data-erd-id="${element.dataset.erdRight}"] .left-point`,
+          offset
+        );
+
+        const path: string = `M ${leftPos.x},${leftPos.y} C ${leftPos.x + CURVATURE},${leftPos.y}
+            ${rightPos.x - CURVATURE},${rightPos.y} ${rightPos.x},${rightPos.y}`;
+
+        element.setAttribute('d', path);
+      });
+    }
+    updated(): void {
+      this.plotRelations();
+    }
+    mounted(): void {
+      this.plotRelations();
+    }
+
+    toggleFS(): void {
+      toggleFullScreen(this.$el);
+    }
+
+    entityClicked(entity: IEntity): void {
+      this.$emit('entity-clicked', entity);
+    }
+  }
+</script>

+ 70 - 0
desktop/core/src/desktop/js/components/er-diagram/lib/entities.ts

@@ -0,0 +1,70 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  Cloudera, Inc. 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 { EntityTypes } from './enums';
+import { IEntity } from './interfaces';
+
+export class Table implements IEntity {
+  readonly id: string;
+  readonly type: EntityTypes = EntityTypes.Table;
+
+  database: string;
+  name: string;
+  columns: Array<string>;
+
+  constructor(database: string, name: string, columns: Array<string>) {
+    this.id = Table.buildId(database, name);
+    this.database = database;
+    this.name = name;
+    this.columns = columns;
+  }
+
+  static buildId(db: string, name: string): string {
+    return `${db}.${name}`;
+  }
+}
+
+export class Column implements IEntity {
+  readonly id: string;
+  readonly type: EntityTypes = EntityTypes.Column;
+
+  tableId: string;
+  name: string;
+
+  constructor(tableId: string, name: string) {
+    this.id = Column.buildId(tableId, name);
+    this.tableId = tableId;
+    this.name = name;
+  }
+
+  static buildId(tableId: string, name: string): string {
+    return `${tableId}.${name}`;
+  }
+}
+
+export class Literal implements IEntity {
+  readonly id: string;
+  readonly type: EntityTypes = EntityTypes.Literal;
+
+  value: string;
+
+  constructor(value: string) {
+    this.id = value;
+    this.value = value;
+  }
+}

+ 23 - 0
desktop/core/src/desktop/js/components/er-diagram/lib/enums.ts

@@ -0,0 +1,23 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  Cloudera, Inc. 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.
+ */
+
+export enum EntityTypes {
+  Table = 'TABLE',
+  Column = 'COLUMN',
+  Literal = 'LITERAL'
+}

+ 30 - 0
desktop/core/src/desktop/js/components/er-diagram/lib/interfaces.ts

@@ -0,0 +1,30 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  Cloudera, Inc. 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 { EntityTypes } from './enums';
+
+export interface IEntity {
+  readonly id: string;
+  readonly type: EntityTypes;
+}
+
+export interface IRelation {
+  desc: string;
+  left: IEntity;
+  right: IEntity;
+}

+ 113 - 0
desktop/core/src/desktop/js/components/er-diagram/lib/processor.ts

@@ -0,0 +1,113 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  Cloudera, Inc. 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 { IEntity, IRelation } from './interfaces';
+import { EntityTypes } from './enums';
+import { Column } from './entities';
+
+class EntityNode {
+  entity: IEntity;
+  level: number;
+  relations: Array<EntityNode>;
+
+  constructor(entity: IEntity) {
+    this.entity = entity;
+    this.relations = new Array<EntityNode>();
+  }
+}
+
+/**
+ * To visualize the ERD in a more human-readable form - Entities will be grouped based on
+ * it's level (Distance from root entity) in the relationship graph. entities[0] will be
+ * taken as the root entity.
+ */
+export function groupEntities(
+  entities: Array<IEntity>,
+  relations: Array<IRelation>
+): Array<Array<IEntity>> {
+  const nodesMap: Map<string, EntityNode> = generateGraph(entities, relations);
+
+  let level = 0;
+  nodesMap.forEach((node: EntityNode) => {
+    // To ensure levels are set even for disjoint graphs
+    if (node.level === undefined) {
+      level = setLevels(node, level);
+    }
+  });
+
+  return breadthFirstTraverse(nodesMap);
+}
+
+function getNodeMapId(entity: IEntity) {
+  if (entity.type === EntityTypes.Column) {
+    return (entity as Column).tableId;
+  }
+  return entity.id;
+}
+
+function generateGraph(
+  entities: Array<IEntity>,
+  relations: Array<IRelation>
+): Map<string, EntityNode> {
+  const nodesMap = new Map<string, EntityNode>();
+
+  entities.forEach((entity: IEntity) => {
+    switch (entity.type) {
+      // We now support only Table & Literal nodes, column will be shown inside Table
+      case EntityTypes.Table:
+      case EntityTypes.Literal:
+        nodesMap.set(entity.id, new EntityNode(entity));
+    }
+  });
+
+  relations.forEach((relation: IRelation) => {
+    const leftEntity: EntityNode = nodesMap.get(getNodeMapId(relation.left));
+    const rightEntity: EntityNode = nodesMap.get(getNodeMapId(relation.right));
+    if (leftEntity && rightEntity) {
+      leftEntity.relations.push(rightEntity);
+    }
+  });
+
+  return nodesMap;
+}
+
+function setLevels(entityNode: EntityNode, level: number) {
+  let maxLevel: number;
+  if (entityNode.level === undefined) {
+    entityNode.level = level;
+    maxLevel = level++;
+    entityNode.relations.forEach((childNode: EntityNode) => {
+      maxLevel = Math.max(maxLevel, setLevels(childNode, level));
+    });
+  }
+  return maxLevel;
+}
+
+function breadthFirstTraverse(nodesMap: Map<string, EntityNode>): Array<Array<IEntity>> {
+  const entities: Array<Array<IEntity>> = new Array<Array<IEntity>>();
+
+  nodesMap.forEach((entityNode: EntityNode) => {
+    const level = entityNode.level;
+    if (entities[level] === undefined) {
+      entities[level] = new Array<IEntity>();
+    }
+    entities[level].push(entityNode.entity);
+  });
+
+  return entities;
+}

+ 23 - 0
desktop/core/src/desktop/js/components/er-diagram/webcomp.ts

@@ -0,0 +1,23 @@
+/**
+ * Licensed to Cloudera, Inc. under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  Cloudera, Inc. 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 ERDiagram from './index.vue';
+import { wrap } from 'vue/webComponentWrapper';
+
+export const COMPONENT_NAME = 'er-diagram';
+wrap(COMPONENT_NAME, ERDiagram);

+ 34 - 21
desktop/core/src/desktop/js/utils/hueUtils.js

@@ -94,32 +94,35 @@ export const html2text = value => {
     .replace(/\u00A0/g, ' ');
 };
 
-export const goFullScreen = () => {
-  if (
-    !document.fullscreenElement &&
-    !document.mozFullScreenElement &&
-    !document.webkitFullscreenElement &&
-    !document.msFullscreenElement
-  ) {
-    if (document.documentElement.requestFullscreen) {
-      document.documentElement.requestFullscreen();
-    } else if (document.documentElement.msRequestFullscreen) {
-      document.documentElement.msRequestFullscreen();
-    } else if (document.documentElement.mozRequestFullScreen) {
-      document.documentElement.mozRequestFullScreen();
-    } else if (document.documentElement.webkitRequestFullscreen) {
-      document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
+export const isFullScreen = () => {
+  return (
+    document.fullscreenElement ||
+    document.mozFullScreenElement ||
+    document.webkitFullscreenElement ||
+    document.msFullscreenElement
+  );
+};
+
+export const goFullScreen = element => {
+  if (!element) {
+    element = document.documentElement;
+  }
+
+  if (!isFullScreen()) {
+    if (element.requestFullscreen) {
+      element.requestFullscreen();
+    } else if (element.msRequestFullscreen) {
+      element.msRequestFullscreen();
+    } else if (element.mozRequestFullScreen) {
+      element.mozRequestFullScreen();
+    } else if (element.webkitRequestFullscreen) {
+      element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
     }
   }
 };
 
 export const exitFullScreen = () => {
-  if (
-    document.fullscreenElement ||
-    document.mozFullScreenElement ||
-    document.webkitFullscreenElement ||
-    document.msFullscreenElement
-  ) {
+  if (isFullScreen()) {
     if (document.exitFullscreen) {
       document.exitFullscreen();
     } else if (document.msExitFullscreen) {
@@ -132,6 +135,14 @@ export const exitFullScreen = () => {
   }
 };
 
+export const toggleFullScreen = element => {
+  if (isFullScreen()) {
+    exitFullScreen();
+  } else {
+    goFullScreen(element);
+  }
+};
+
 export const changeURL = (newURL, params) => {
   let extraSearch = '';
   if (params) {
@@ -584,8 +595,10 @@ export default {
   text2Url: text2Url,
   htmlEncode: htmlEncode,
   html2text: html2text,
+  isFullScreen: isFullScreen,
   goFullScreen: goFullScreen,
   exitFullScreen: exitFullScreen,
+  toggleFullScreen: toggleFullScreen,
   changeURL: changeURL,
   replaceURL: replaceURL,
   changeURLParameter: changeURLParameter,

+ 5 - 0
package-lock.json

@@ -21061,6 +21061,11 @@
         "vue-style-loader": "^4.1.0"
       }
     },
+    "vue-property-decorator": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/vue-property-decorator/-/vue-property-decorator-9.0.0.tgz",
+      "integrity": "sha512-oegTNPItuHOkW0AP1MnbdNwkmyhfsUIIXvIRHpgC18tVoEo21/i6kItyeekjMs8JgZJeuHzsaTc/DZaJFH4IWQ=="
+    },
     "vue-style-loader": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz",

+ 1 - 0
package.json

@@ -60,6 +60,7 @@
     "vue": "2.6.11",
     "vue-class-component": "7.2.3",
     "vue-custom-element": "3.2.14",
+    "vue-property-decorator": "9.0.0",
     "vue-template-compiler": "2.6.11"
   },
   "devDependencies": {

+ 3 - 0
tsconfig.json

@@ -14,5 +14,8 @@
   },
   "include": [
     "."
+  ],
+  "exclude": [
+    "node_modules"
   ]
 }

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません