Browse Source

HUE-9412. [ui] Add tests for ERD component (sree)

sreenaths 5 years ago
parent
commit
0502e4caab

+ 52 - 0
desktop/core/src/desktop/js/components/er-diagram/comps/literal-entity.test.ts

@@ -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.
+ */
+
+import LiteralEntity from './literal-entity.vue';
+import { shallowMount } from '@vue/test-utils';
+import { Literal } from '../lib/entities';
+
+describe('LiteralEntity UTs', () => {
+  test('Empty instance created', () => {
+    const wrapper = shallowMount(LiteralEntity, {
+      propsData: {
+        entity: {}
+      }
+    });
+
+    expect(wrapper.exists()).toBeTruthy();
+
+    expect(wrapper.props('entity')).toBeTruthy();
+
+    expect(wrapper.find('.literal-entity').exists()).toBeTruthy();
+    expect(wrapper.find('.literal-value').exists()).toBeTruthy();
+
+    expect(wrapper.find('.left-point').exists()).toBeTruthy();
+    expect(wrapper.find('.right-point').exists()).toBeTruthy();
+  });
+
+  test('Value check', () => {
+    const testValue = 'TEST_VALUE';
+    const wrapper = shallowMount(LiteralEntity, {
+      propsData: {
+        entity: new Literal(testValue)
+      }
+    });
+
+    expect(wrapper.text()).toBe(testValue);
+  });
+});

+ 194 - 0
desktop/core/src/desktop/js/components/er-diagram/comps/table-entity.test.ts

@@ -0,0 +1,194 @@
+/**
+ * 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 TableEntity from './table-entity.vue';
+import { shallowMount } from '@vue/test-utils';
+import { Table, Column } from '../lib/entities';
+import { sleep } from '../test/utils';
+
+const dbName = 'DB_NAME';
+const tableName = 'TABLE_NAME';
+const tableId: string = Table.buildId(dbName, tableName);
+const columnNames: Array<string> = ['id', 'name'];
+const defaultMaxColumns = 10;
+
+describe('TableEntity UTs', () => {
+  test('Empty instance created', () => {
+    const wrapper = shallowMount(TableEntity, {
+      propsData: {
+        entity: {
+          columns: []
+        }
+      }
+    });
+
+    expect(wrapper.exists()).toBeTruthy();
+
+    expect(wrapper.props('entity')).toBeTruthy();
+    expect(wrapper.props('maxColumns')).toBe(defaultMaxColumns);
+
+    expect(wrapper.find('.table-entity').exists()).toBeTruthy();
+    expect(wrapper.find('.db-name').exists()).toBeTruthy();
+    expect(wrapper.find('.table-name').exists()).toBeTruthy();
+
+    expect(wrapper.find('.columns-container').exists()).toBeTruthy();
+
+    expect(wrapper.find('.column-entity').exists()).toBeFalsy();
+    expect(wrapper.find('.grouped-columns').exists()).toBeFalsy();
+  });
+
+  test('Simple table with 2 columns', () => {
+    const wrapper = shallowMount(TableEntity, {
+      propsData: {
+        entity: new Table(dbName, tableName, [
+          new Column(tableId, columnNames[0]),
+          new Column(tableId, columnNames[1])
+        ])
+      }
+    });
+
+    expect(wrapper.exists()).toBeTruthy();
+
+    expect(wrapper.props('entity')).toBeTruthy();
+    expect(wrapper.props('maxColumns')).toBe(defaultMaxColumns);
+
+    expect(wrapper.find('.db-name').text()).toBe(dbName);
+    expect(wrapper.find('.table-name').text()).toBe(tableName);
+
+    expect(wrapper.findAll('.columns-container .column-entity')).toHaveLength(2);
+    expect(wrapper.findAll('.columns-container .column-entity .left-point')).toHaveLength(2);
+    expect(wrapper.findAll('.columns-container .column-entity .right-point')).toHaveLength(2);
+
+    expect(wrapper.findAll('.columns-container .column-entity').at(0).text()).toBe(columnNames[0]);
+    expect(
+      wrapper.findAll('.columns-container .column-entity').at(0).attributes('data-entity-id')
+    ).toBe(`${tableId}.${columnNames[0]}`);
+
+    expect(wrapper.findAll('.columns-container .column-entity').at(1).text()).toBe(columnNames[1]);
+    expect(
+      wrapper.findAll('.columns-container .column-entity').at(1).attributes('data-entity-id')
+    ).toBe(`${tableId}.${columnNames[1]}`);
+
+    expect(wrapper.find('.grouped-columns').exists()).toBeFalsy();
+  });
+
+  test('With number of columns more than maxColumns', () => {
+    const columns: Array<Column> = [];
+    const columnCount = 20;
+
+    for (let i = 0; i < columnCount; i++) {
+      columns.push(new Column(tableId, `column_${i}`));
+    }
+
+    const wrapper = shallowMount(TableEntity, {
+      propsData: {
+        entity: new Table(dbName, tableName, columns)
+      }
+    });
+
+    expect(wrapper.exists()).toBeTruthy();
+    expect(wrapper.props('entity')).toBeTruthy();
+    expect(wrapper.props('maxColumns')).toBe(defaultMaxColumns);
+
+    expect(wrapper.find('.db-name').text()).toBe(dbName);
+    expect(wrapper.find('.table-name').text()).toBe(tableName);
+
+    const container = wrapper.find('.columns-container');
+    expect(container.findAll('.column-entity')).toHaveLength(defaultMaxColumns);
+    expect(container.findAll('.column-entity .left-point')).toHaveLength(defaultMaxColumns);
+    expect(container.findAll('.column-entity .right-point')).toHaveLength(defaultMaxColumns);
+
+    const groupedEntitiesIds: string = <string>(
+      wrapper.find('.grouped-columns').attributes('data-entity-id')
+    );
+
+    const columnElements = wrapper.findAll('.columns-container .column-entity');
+    columns.forEach((column: Column, index: number) => {
+      if (index < defaultMaxColumns) {
+        expect(columnElements.at(index).text()).toBe(column.name);
+        expect(columnElements.at(index).attributes('data-entity-id')).toBe(column.id);
+      } else {
+        expect(groupedEntitiesIds.includes(column.id)).toBeTruthy();
+      }
+    });
+
+    expect(wrapper.find('.grouped-columns').exists()).toBeTruthy();
+    expect(groupedEntitiesIds.split(' ')).toHaveLength(columnCount - defaultMaxColumns);
+    expect(wrapper.find('.grouped-columns').text()).toBe(
+      `+${columnCount - defaultMaxColumns} columns`
+    );
+  });
+
+  test('With custom maxColumns', () => {
+    const columns: Array<Column> = [];
+    const columnCount = 10;
+    const maxColumns = 5;
+
+    for (let i = 0; i < columnCount; i++) {
+      columns.push(new Column(tableId, `column_${i}`));
+    }
+
+    const wrapper = shallowMount(TableEntity, {
+      propsData: {
+        entity: new Table(dbName, tableName, columns),
+        maxColumns
+      }
+    });
+
+    expect(wrapper.exists()).toBeTruthy();
+    expect(wrapper.props('maxColumns')).toBe(maxColumns);
+
+    const container = wrapper.find('.columns-container');
+    expect(container.findAll('.column-entity')).toHaveLength(maxColumns);
+
+    expect(wrapper.find('.grouped-columns').exists()).toBeTruthy();
+    expect(wrapper.find('.grouped-columns').text()).toBe(`+${columnCount - maxColumns} columns`);
+  });
+
+  test('Column expansion test', async () => {
+    const columns: Array<Column> = [];
+    const columnCount = 10;
+    const maxColumns = 5;
+
+    for (let i = 0; i < columnCount; i++) {
+      columns.push(new Column(tableId, `column_${i}`));
+    }
+
+    const wrapper = shallowMount(TableEntity, {
+      propsData: {
+        entity: new Table(dbName, tableName, columns),
+        maxColumns
+      }
+    });
+
+    expect(wrapper.exists()).toBeTruthy();
+    expect(wrapper.props('maxColumns')).toBe(maxColumns);
+
+    expect(wrapper.findAll('.columns-container .column-entity')).toHaveLength(maxColumns);
+
+    expect(wrapper.find('.grouped-columns').exists()).toBeTruthy();
+    expect(wrapper.find('.grouped-columns').text()).toBe(`+${columnCount - maxColumns} columns`);
+
+    wrapper.find('.grouped-columns').trigger('click');
+
+    await sleep(500);
+
+    expect(wrapper.findAll('.columns-container .column-entity')).toHaveLength(columnCount);
+    expect(wrapper.find('.grouped-columns').exists()).toBeFalsy();
+  });
+});

+ 18 - 7
desktop/core/src/desktop/js/components/er-diagram/comps/table-entity.vue

@@ -26,7 +26,7 @@
     </div>
     <div class="columns-container">
       <div
-        v-for="column in entity.columns.slice(0, maxColumns)"
+        v-for="column in entity.columns.slice(0, maxCols)"
         :key="column.id"
         :data-entity-id="column.id"
         :title="column.name"
@@ -37,14 +37,19 @@
         {{ column.name }}
       </div>
       <div
-        v-if="entity.columns.length > maxColumns"
-        :data-entity-id="entity.columns.map(column => column.id).join(' ')"
+        v-if="entity.columns.length > maxCols"
+        :data-entity-id="
+          entity.columns
+            .slice(maxCols)
+            .map(column => column.id)
+            .join(' ')
+        "
         class="grouped-columns"
         @click.stop="expandColumns()"
       >
         <div class="left-point" />
         <div class="right-point" />
-        +{{ entity.columns.length - maxColumns }} columns
+        +{{ entity.columns.length - maxCols }} columns
       </div>
     </div>
   </div>
@@ -61,12 +66,18 @@
     @Prop() entity: Table;
     @Prop({ default: 10 }) maxColumns: number;
 
-    updated(): void {
-      this.$emit('updated');
+    maxCols: number = 0;
+
+    created(): void {
+      this.maxCols = this.maxColumns;
     }
 
     expandColumns(): void {
-      this.maxColumns = Number.MAX_SAFE_INTEGER;
+      this.maxCols = Number.MAX_SAFE_INTEGER;
+    }
+
+    updated(): void {
+      this.$emit('updated');
     }
   }
 </script>

+ 146 - 9
desktop/core/src/desktop/js/components/er-diagram/er-diagram.test.ts

@@ -17,9 +17,13 @@
  */
 
 import ERDiagram from './index.vue';
-import { shallowMount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
+import { Table } from './lib/entities';
+import { createTables } from './test/utils';
 
-describe('ERDiagram', () => {
+import ERDData from './test/data.json';
+
+describe('ERDiagram UTs', () => {
   test('Empty instance created', () => {
     const wrapper = shallowMount(ERDiagram, {
       propsData: {
@@ -28,14 +32,147 @@ describe('ERDiagram', () => {
       }
     });
 
-    expect(wrapper.exists()).toBe(true);
-    expect(wrapper.find('[title="Toggle Fullscreen"]').exists()).toBe(true);
-    expect(wrapper.find('.erd-scroll-panel').exists()).toBe(true);
-    expect(wrapper.find('svg.erd-relations').exists()).toBe(true);
+    expect(wrapper.props('entities')).toHaveLength(0);
+    expect(wrapper.props('relations')).toHaveLength(0);
+
+    expect(wrapper.exists()).toBeTruthy();
+    expect(wrapper.find('[title="Toggle Fullscreen"]').exists()).toBeTruthy();
+    expect(wrapper.find('.erd-scroll-panel').exists()).toBeTruthy();
+    expect(wrapper.find('svg.erd-relations').exists()).toBeTruthy();
+
+    expect(wrapper.classes('er-diagram')).toBeTruthy();
+
+    expect(wrapper.findAll('.entity-container')).toHaveLength(0);
+    expect(wrapper.findAll('.relation-path')).toHaveLength(0);
+  });
+
+  test('Single entity', () => {
+    const wrapper = shallowMount(ERDiagram, {
+      propsData: {
+        entities: [new Table('db-name', 'table-name', [])],
+        relations: []
+      }
+    });
+
+    expect(wrapper.props('entities')).toHaveLength(1);
+    expect(wrapper.props('relations')).toHaveLength(0);
+
+    expect(wrapper.findAll('.entity-container')).toHaveLength(1);
+    expect(wrapper.findAll('.relation-path')).toHaveLength(0);
+  });
+
+  test('Multiple unrelated entities (t0, t1, t2, t3, t4)', () => {
+    const tableCount = 5;
+
+    const wrapper = shallowMount(ERDiagram, {
+      propsData: {
+        entities: createTables(tableCount, 0),
+        relations: []
+      }
+    });
+
+    expect(wrapper.props('entities')).toHaveLength(tableCount);
+    expect(wrapper.props('relations')).toHaveLength(0);
+
+    expect(wrapper.findAll('.entity-container')).toHaveLength(tableCount);
+    expect(wrapper.findAll('.relation-path')).toHaveLength(0);
+  });
+
+  test('Related entities - 3 levels (t0-t1, t0-t2, t0-t3, t3-t4, t3-t5)', () => {
+    const tableCount = 6;
+
+    const tables: Array<Table> = createTables(tableCount, 4);
+    const wrapper = shallowMount(ERDiagram, {
+      propsData: {
+        entities: tables,
+        relations: [
+          {
+            desc: '',
+            left: tables[0].columns[1],
+            right: tables[1].columns[0]
+          },
+          {
+            desc: '',
+            left: tables[0].columns[2],
+            right: tables[2].columns[0]
+          },
+          {
+            desc: '',
+            left: tables[0].columns[3],
+            right: tables[3].columns[0]
+          },
+          {
+            desc: '',
+            left: tables[3].columns[3],
+            right: tables[4].columns[0]
+          },
+          {
+            desc: '',
+            left: tables[3].columns[3],
+            right: tables[5].columns[0]
+          }
+        ]
+      }
+    });
+
+    expect(wrapper.props('entities')).toHaveLength(tableCount);
+    expect(wrapper.props('relations')).toHaveLength(5);
+
+    expect(wrapper.findAll('.entity-container')).toHaveLength(tableCount);
+    expect(wrapper.findAll('.relation-path')).toHaveLength(5);
+
+    expect(wrapper.findAll('.entity-group')).toHaveLength(3);
+  });
+});
+
+describe('ERDiagram integration tests', () => {
+  test('Check plotting of relation paths (t0-t1, t0-t2)', () => {
+    const tableCount = 3;
+
+    const tables: Array<Table> = createTables(tableCount, 3);
+    const wrapper = mount(ERDiagram, {
+      propsData: {
+        entities: tables,
+        relations: [
+          {
+            desc: '',
+            left: tables[0].columns[1],
+            right: tables[1].columns[0]
+          },
+          {
+            desc: '',
+            left: tables[0].columns[2],
+            right: tables[2].columns[0]
+          }
+        ]
+      }
+    });
+
+    expect(wrapper.props('entities')).toHaveLength(tableCount);
+    expect(wrapper.props('relations')).toHaveLength(2);
+
+    expect(wrapper.findAll('.entity-container')).toHaveLength(tableCount);
+    expect(wrapper.findAll('.relation-path')).toHaveLength(2);
+
+    expect(wrapper.findAll('.relation-path').at(0).attributes('d')).toBe('M 0,1 C 40,1 -40,1 0,1');
+    expect(wrapper.findAll('.relation-path').at(1).attributes('d')).toBe('M 0,1 C 40,1 -40,1 0,1');
+  });
+
+  test('Real data test', () => {
+    const wrapper = mount(ERDiagram, {
+      propsData: ERDData
+    });
+
+    expect(wrapper.props('entities')).toHaveLength(3);
+    expect(wrapper.props('relations')).toHaveLength(3);
+
+    expect(wrapper.findAll('.entity-container')).toHaveLength(3);
+    expect(wrapper.findAll('.relation-path')).toHaveLength(3);
 
-    expect(wrapper.classes('er-diagram')).toBe(true);
+    expect(wrapper.findAll('.entity-group')).toHaveLength(2);
 
-    expect(wrapper.props('entities').length).toBe(0);
-    expect(wrapper.props('relations').length).toBe(0);
+    expect(wrapper.findAll('.relation-path').at(0).attributes('d')).toBe('M 0,1 C 40,1 -40,1 0,1');
+    expect(wrapper.findAll('.relation-path').at(1).attributes('d')).toBe('M 0,1 C 40,1 -40,1 0,1');
+    expect(wrapper.findAll('.relation-path').at(2).attributes('d')).toBe('M 0,1 C 40,1 -40,1 0,1');
   });
 });

+ 3 - 2
desktop/core/src/desktop/js/components/er-diagram/index.vue

@@ -122,8 +122,9 @@
         );
 
         if (leftPos && rightPos) {
-          const path: string = `M ${leftPos.x},${leftPos.y} C ${leftPos.x + CURVATURE},${leftPos.y}
-              ${rightPos.x - CURVATURE},${rightPos.y} ${rightPos.x},${rightPos.y}`;
+          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);
 
           element.style.visibility = 'visible';

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

@@ -0,0 +1,223 @@
+/**
+ * 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 { groupEntities } from './processor';
+import { createTables } from '../test/utils';
+import { Table } from './entities';
+import { IEntity } from './interfaces';
+
+describe('processor UTs', () => {
+  test('Multiple unrelated entities (t0, t1, t2, t3, t4)', () => {
+    const tableCount = 5;
+
+    const entityGroups: Array<Array<IEntity>> = groupEntities(createTables(tableCount, 0), []);
+
+    expect(entityGroups).toHaveLength(5);
+    expect(entityGroups.flat()).toHaveLength(tableCount);
+  });
+
+  test('Related entities - Linked list (t0-t1-t2-t3-t4)', () => {
+    const tableCount = 5;
+
+    const tables: Array<Table> = createTables(tableCount, 2);
+    const entityGroups: Array<Array<IEntity>> = groupEntities(tables, [
+      {
+        desc: '',
+        left: tables[0].columns[1],
+        right: tables[1].columns[0]
+      },
+      {
+        desc: '',
+        left: tables[1].columns[1],
+        right: tables[2].columns[0]
+      },
+      {
+        desc: '',
+        left: tables[2].columns[1],
+        right: tables[3].columns[0]
+      },
+      {
+        desc: '',
+        left: tables[3].columns[1],
+        right: tables[4].columns[0]
+      }
+    ]);
+
+    expect(entityGroups).toHaveLength(5);
+    expect(entityGroups.flat()).toHaveLength(tableCount);
+  });
+
+  test('Related entities - Binary tree (t0-t1, t0-t2, t2-t3, t2-t4)', () => {
+    const tableCount = 5;
+
+    const tables: Array<Table> = createTables(tableCount, 3);
+    const entityGroups: Array<Array<IEntity>> = groupEntities(tables, [
+      {
+        desc: '',
+        left: tables[0].columns[1],
+        right: tables[1].columns[0]
+      },
+      {
+        desc: '',
+        left: tables[0].columns[2],
+        right: tables[2].columns[0]
+      },
+      {
+        desc: '',
+        left: tables[2].columns[1],
+        right: tables[3].columns[0]
+      },
+      {
+        desc: '',
+        left: tables[2].columns[2],
+        right: tables[4].columns[0]
+      }
+    ]);
+
+    expect(entityGroups).toHaveLength(3);
+    expect(entityGroups.flat()).toHaveLength(tableCount);
+  });
+
+  test('Related entities - Graph (t0-t1, t0-t2, t2-t3, t2-t4, t3-t0, t4-t0)', () => {
+    // Adding back links in the above binary tree to simulate graph
+    const tableCount = 5;
+
+    const tables: Array<Table> = createTables(tableCount, 3);
+    const entityGroups: Array<Array<IEntity>> = groupEntities(tables, [
+      {
+        desc: '',
+        left: tables[0].columns[1],
+        right: tables[1].columns[0]
+      },
+      {
+        desc: '',
+        left: tables[0].columns[2],
+        right: tables[2].columns[0]
+      },
+      {
+        desc: '',
+        left: tables[2].columns[1],
+        right: tables[3].columns[0]
+      },
+      {
+        // Back link - 1
+        desc: '',
+        left: tables[3].columns[1],
+        right: tables[0].columns[0]
+      },
+      {
+        desc: '',
+        left: tables[2].columns[2],
+        right: tables[4].columns[0]
+      },
+      {
+        // Back link - 2
+        desc: '',
+        left: tables[4].columns[2],
+        right: tables[0].columns[0]
+      }
+    ]);
+
+    expect(entityGroups).toHaveLength(3);
+    expect(entityGroups.flat()).toHaveLength(tableCount);
+  });
+
+  test('Related entities - Self relation (t0-t0, t1)', () => {
+    const tableCount = 2;
+
+    const tables: Array<Table> = createTables(tableCount, 2);
+    const entityGroups: Array<Array<IEntity>> = groupEntities(tables, [
+      {
+        desc: '',
+        left: tables[0].columns[1],
+        right: tables[0].columns[0]
+      }
+    ]);
+
+    expect(entityGroups).toHaveLength(2);
+    expect(entityGroups.flat()).toHaveLength(tableCount);
+  });
+
+  test('Related entities - Self relation + external reference (t0-t0, t0-t1)', () => {
+    const tableCount = 2;
+
+    const tables: Array<Table> = createTables(tableCount, 3);
+    const entityGroups: Array<Array<IEntity>> = groupEntities(tables, [
+      {
+        desc: '',
+        left: tables[0].columns[1],
+        right: tables[0].columns[0]
+      },
+      {
+        desc: '',
+        left: tables[0].columns[2],
+        right: tables[1].columns[0]
+      }
+    ]);
+
+    expect(entityGroups).toHaveLength(2);
+    expect(entityGroups.flat()).toHaveLength(tableCount);
+  });
+
+  test('Related entities - Cyclic relation (t0-t1, t1-t0)', () => {
+    const tableCount = 2;
+
+    const tables: Array<Table> = createTables(tableCount, 3);
+    const entityGroups: Array<Array<IEntity>> = groupEntities(tables, [
+      {
+        desc: '',
+        left: tables[0].columns[1],
+        right: tables[1].columns[0]
+      },
+      {
+        desc: '',
+        left: tables[1].columns[1],
+        right: tables[0].columns[0]
+      }
+    ]);
+
+    expect(entityGroups).toHaveLength(2);
+    expect(entityGroups.flat()).toHaveLength(tableCount);
+  });
+
+  test('Unrelated entity groups (t0-t1, t2-t3, t2-t4)', () => {
+    const tableCount = 5;
+
+    const tables: Array<Table> = createTables(tableCount, 2);
+    const entityGroups: Array<Array<IEntity>> = groupEntities(tables, [
+      {
+        desc: '',
+        left: tables[0].columns[1],
+        right: tables[1].columns[0]
+      },
+      {
+        desc: '',
+        left: tables[2].columns[1],
+        right: tables[3].columns[0]
+      },
+      {
+        desc: '',
+        left: tables[2].columns[1],
+        right: tables[4].columns[0]
+      }
+    ]);
+
+    expect(entityGroups).toHaveLength(4);
+    expect(entityGroups.flat()).toHaveLength(tableCount);
+  });
+});

+ 14 - 11
desktop/core/src/desktop/js/components/er-diagram/lib/processor.ts

@@ -28,6 +28,7 @@ class EntityNode {
   constructor(entity: IEntity) {
     this.entity = entity;
     this.relations = new Array<EntityNode>();
+    this.level = 0;
   }
 }
 
@@ -45,8 +46,8 @@ export function groupEntities(
   let level = 0;
   nodesMap.forEach((node: EntityNode) => {
     // To ensure levels are set even for disjoint graphs
-    if (node.level === undefined) {
-      level = setLevels(node, level);
+    if (node.level === 0) {
+      level = setLevels(node, level + 1);
     }
   });
 
@@ -76,8 +77,8 @@ function generateGraph(
   });
 
   relations.forEach((relation: IRelation) => {
-    const leftEntity: EntityNode = nodesMap.get(getNodeMapId(relation.left));
-    const rightEntity: EntityNode = nodesMap.get(getNodeMapId(relation.right));
+    const leftEntity: EntityNode | undefined = nodesMap.get(getNodeMapId(relation.left));
+    const rightEntity: EntityNode | undefined = nodesMap.get(getNodeMapId(relation.right));
     if (leftEntity && rightEntity) {
       leftEntity.relations.push(rightEntity);
     }
@@ -86,9 +87,9 @@ function generateGraph(
   return nodesMap;
 }
 
-function setLevels(entityNode: EntityNode, level: number) {
-  let maxLevel: number;
-  if (entityNode.level === undefined) {
+function setLevels(entityNode: EntityNode, level: number): number {
+  let maxLevel = 0;
+  if (entityNode.level === 0) {
     entityNode.level = level;
     maxLevel = level++;
     entityNode.relations.forEach((childNode: EntityNode) => {
@@ -102,11 +103,13 @@ function breadthFirstTraverse(nodesMap: Map<string, EntityNode>): Array<Array<IE
   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>();
+    const level = entityNode.level - 1;
+    if (level >= 0) {
+      if (entities[level] === undefined) {
+        entities[level] = new Array<IEntity>();
+      }
+      entities[level].push(entityNode.entity);
     }
-    entities[level].push(entityNode.entity);
   });
 
   return entities;

+ 183 - 0
desktop/core/src/desktop/js/components/er-diagram/test/data.json

@@ -0,0 +1,183 @@
+{
+  "entities": [
+    {
+      "id": "hue6.desktop_document2",
+      "type": "TABLE",
+      "database": "hue6",
+      "name": "desktop_document2",
+      "columns": [
+        {
+          "id": "hue6.desktop_document2.id",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_document2",
+          "name": "id"
+        },
+        {
+          "id": "hue6.desktop_document2.owner_id",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_document2",
+          "name": "owner_id"
+        },
+        {
+          "id": "hue6.desktop_document2.parent_directory_id",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_document2",
+          "name": "parent_directory_id"
+        },
+        {
+          "id": "hue6.desktop_document2.connector_id",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_document2",
+          "name": "connector_id"
+        },
+        {
+          "id": "hue6.desktop_document2.name",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_document2",
+          "name": "name"
+        },
+        {
+          "id": "hue6.desktop_document2.description",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_document2",
+          "name": "description"
+        },
+        {
+          "id": "hue6.desktop_document2.uuid",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_document2",
+          "name": "uuid"
+        },
+        {
+          "id": "hue6.desktop_document2.type",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_document2",
+          "name": "type"
+        },
+        {
+          "id": "hue6.desktop_document2.data",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_document2",
+          "name": "data"
+        },
+        {
+          "id": "hue6.desktop_document2.extra",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_document2",
+          "name": "extra"
+        },
+        {
+          "id": "hue6.desktop_document2.search",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_document2",
+          "name": "search"
+        },
+        {
+          "id": "hue6.desktop_document2.last_modified",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_document2",
+          "name": "last_modified"
+        },
+        {
+          "id": "hue6.desktop_document2.version",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_document2",
+          "name": "version"
+        },
+        {
+          "id": "hue6.desktop_document2.is_history",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_document2",
+          "name": "is_history"
+        },
+        {
+          "id": "hue6.desktop_document2.is_managed",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_document2",
+          "name": "is_managed"
+        },
+        {
+          "id": "hue6.desktop_document2.is_trashed",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_document2",
+          "name": "is_trashed"
+        }
+      ]
+    },
+    {
+      "id": "hue6.useradmin_organizationuser",
+      "type": "TABLE",
+      "database": "hue6",
+      "name": "useradmin_organizationuser",
+      "columns": [
+        {
+          "id": "hue6.useradmin_organizationuser.id",
+          "type": "COLUMN",
+          "tableId": "hue6.useradmin_organizationuser",
+          "name": "id"
+        }
+      ]
+    },
+    {
+      "id": "hue6.desktop_connector",
+      "type": "TABLE",
+      "database": "hue6",
+      "name": "desktop_connector",
+      "columns": [
+        {
+          "id": "hue6.desktop_connector.id",
+          "type": "COLUMN",
+          "tableId": "hue6.desktop_connector",
+          "name": "id"
+        }
+      ]
+    }
+  ],
+  "relations": [
+    {
+      "desc": "Foreign Key",
+      "left": {
+        "id": "hue6.desktop_document2.parent_directory_id",
+        "type": "COLUMN",
+        "tableId": "hue6.desktop_document2",
+        "name": "parent_directory_id"
+      },
+      "right": {
+        "id": "hue6.desktop_document2.id",
+        "type": "COLUMN",
+        "tableId": "hue6.desktop_document2",
+        "name": "id"
+      }
+    },
+    {
+      "desc": "Foreign Key",
+      "left": {
+        "id": "hue6.desktop_document2.owner_id",
+        "type": "COLUMN",
+        "tableId": "hue6.desktop_document2",
+        "name": "owner_id"
+      },
+      "right": {
+        "id": "hue6.useradmin_organizationuser.id",
+        "type": "COLUMN",
+        "tableId": "hue6.useradmin_organizationuser",
+        "name": "id"
+      }
+    },
+    {
+      "desc": "Foreign Key",
+      "left": {
+        "id": "hue6.desktop_document2.connector_id",
+        "type": "COLUMN",
+        "tableId": "hue6.desktop_document2",
+        "name": "connector_id"
+      },
+      "right": {
+        "id": "hue6.desktop_connector.id",
+        "type": "COLUMN",
+        "tableId": "hue6.desktop_connector",
+        "name": "id"
+      }
+    }
+  ]
+}

+ 43 - 0
desktop/core/src/desktop/js/components/er-diagram/test/utils.ts

@@ -0,0 +1,43 @@
+/**
+ * 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 '../lib/entities';
+
+export const dbName = 'db-name';
+export const tableNamePrefix = 'table-name';
+export const columnNamePrefix = 'column-name';
+
+export function createTables(tableCount: number, columnCount: number): Array<Table> {
+  const entities: Array<Table> = [];
+  for (let t = 0; t < tableCount; t++) {
+    const tableName = `${tableNamePrefix}-${t}`;
+    const tableId: string = Table.buildId(dbName, tableName);
+
+    const columns: Array<Column> = [];
+
+    for (let c = 0; c < columnCount; c++) {
+      columns.push(new Column(tableId, `${columnNamePrefix}-${c}`));
+    }
+    entities.push(new Table(dbName, tableName, columns));
+  }
+  return entities;
+}
+
+export function sleep(duration: number): Promise<unknown> {
+  return new Promise(r => setTimeout(r, duration));
+}

+ 13 - 2
tsconfig.json

@@ -10,12 +10,23 @@
     "isolatedModules": true,
     "esModuleInterop": true,
     "baseUrl": "desktop/core/src/desktop/js",
-    "typeRoots": [ "desktop/core/src/desktop/js/types", "./node_modules/@types"]
+    "typeRoots": [ "desktop/core/src/desktop/js/types", "./node_modules/@types"],
+    "lib": ["es2019"]
   },
   "include": [
     "."
   ],
   "exclude": [
-    "node_modules"
+    "apps",
+    "build",
+    "dist",
+    "docs",
+    "ext",
+    "logs",
+    "maven",
+    "node_modules",
+    ".github",
+    ".git",
+    ".circleci"
   ]
 }