This is an automated email from the ASF dual-hosted git repository.

liuxun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/submarine.git


The following commit(s) were added to refs/heads/master by this push:
     new f9b410d  SUBMARINE-398. Implement frontend of data-dict page in 
workbench with Angular
f9b410d is described below

commit f9b410dad4b4fed8de76f54f10b56859bfbd1dd0
Author: kevin85421 <[email protected]>
AuthorDate: Mon Mar 9 16:54:20 2020 +0800

    SUBMARINE-398. Implement frontend of data-dict page in workbench with 
Angular
    
    ### What is this PR for?
    Implement frontend of the data-dict page in workbench with Angular
    
    ### What type of PR is it?
    [Feature]
    
    ### Todos
    * [ ] - Task
    
    ### What is the Jira issue?
    https://issues.apache.org/jira/browse/SUBMARINE-398
    
    ### How should this be tested?
    
https://travis-ci.org/kevin85421/hadoop-submarine/builds/660648669?utm_medium=notification&utm_source=github_status
    <img width="898" alt="截圖 2020-03-10 下午10 12 26" 
src="https://user-images.githubusercontent.com/20109646/76327282-c72fea00-6324-11ea-98da-2274ef5b7790.png";>
    
    ### Screenshots (if appropriate)
    ![截圖 2020-03-09 下午11 22 
40](https://user-images.githubusercontent.com/20109646/76229345-0266e600-625d-11ea-8411-2bac2f9dbaf9.png)
    ![截圖 2020-03-09 下午11 22 
47](https://user-images.githubusercontent.com/20109646/76229353-0561d680-625d-11ea-9dc1-5b5161268cc2.png)
    ![截圖 2020-03-09 下午11 22 
58](https://user-images.githubusercontent.com/20109646/76229364-08f55d80-625d-11ea-8e39-782daacf1b71.png)
    ![截圖 2020-03-09 下午11 23 
16](https://user-images.githubusercontent.com/20109646/76229372-0b57b780-625d-11ea-9420-cf5555f0eed9.png)
    
    
![480p-1](https://user-images.githubusercontent.com/20109646/76228737-14945480-625c-11ea-9825-bfc71e258a04.gif)
    
![480p-2](https://user-images.githubusercontent.com/20109646/76228685-02b2b180-625c-11ea-8fcf-9f5f906624a4.gif)
    
![480p-3](https://user-images.githubusercontent.com/20109646/76228698-080ffc00-625c-11ea-8b39-7f3541d13e55.gif)
    
    ### Questions:
    * Does the licenses files need an update? No
    * Are there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Author: kevin85421 <[email protected]>
    
    Closes #215 from kevin85421/SUBMARINE-398 and squashes the following 
commits:
    
    af4bb2b [kevin85421] SUBMARINE-398. [WEB] Fix E2E test cases
    10e1c2d [kevin85421] SUBMARINE-398. [WEB] Fix datadictIT.java
    f726a14 [kevin85421] SUBMARINE-398. [WEB] Fix trailing whitespace
    8e6093b [kevin85421] SUBMARINE-398. [WEB] Implement frontend of data-dict 
page in workbench with Angular
---
 .../apache/submarine/integration/datadictIT.java   | 157 +++++++++++
 .../apache/submarine/integration/sidebarIT.java    |   6 +-
 .../page-layout/page-layout.component.html         |   3 +-
 .../page-layout/page-layout.component.ts           |   7 +-
 .../data-dict-config-modal.component.html          | 146 ++++++++++
 .../data-dict-config-modal.component.scss}         |   3 +
 .../data-dict-config-modal.component.ts            | 294 +++++++++++++++++++++
 .../data-dict-modal/data-dict-modal.component.html |  48 ++++
 .../data-dict-modal.component.scss}                |   0
 .../data-dict-modal/data-dict-modal.component.ts   |  70 +++++
 .../manager/data-dict/data-dict.component.html     | 105 +++++++-
 .../manager/data-dict/data-dict.component.scss     |  11 +
 .../manager/data-dict/data-dict.component.ts       | 141 +++++++++-
 .../workbench/manager/manager-routing.module.ts    |   2 +-
 .../pages/workbench/manager/manager.component.ts   |  31 ++-
 .../app/pages/workbench/manager/manager.module.ts  |   4 +-
 .../src/app/pages/workbench/workbench.component.ts |   2 +-
 17 files changed, 1018 insertions(+), 12 deletions(-)

diff --git 
a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/datadictIT.java
 
b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/datadictIT.java
new file mode 100644
index 0000000..329ff24
--- /dev/null
+++ 
b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/datadictIT.java
@@ -0,0 +1,157 @@
+/*
+ * 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.submarine.integration;
+
+import org.apache.submarine.AbstractSubmarineIT;
+import org.apache.submarine.WebDriverManager;
+import org.apache.submarine.SubmarineITUtils;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.interactions.Actions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+import org.testng.Assert;
+import java.util.*;
+
+public class datadictIT extends AbstractSubmarineIT {
+
+  public final static Logger LOG = LoggerFactory.getLogger(datadictIT.class);
+
+  @BeforeClass
+  public static void startUp(){
+    LOG.info("[Testcase]: datadictIT");
+    driver =  WebDriverManager.getWebDriver();
+  }
+
+  @AfterClass
+  public static void tearDown(){
+    driver.quit();
+  }
+
+  // @Test TODO(kevin85421): Due to the undeterministic behavior of travis, I 
decide to comment it.
+  public void dataDictTest() throws Exception {
+    // Login
+    LOG.info("Login");
+    pollingWait(By.cssSelector("input[ng-reflect-name='userName']"), 
MAX_BROWSER_TIMEOUT_SEC).sendKeys("admin");
+    pollingWait(By.cssSelector("input[ng-reflect-name='password']"), 
MAX_BROWSER_TIMEOUT_SEC).sendKeys("admin");
+    clickAndWait(By.cssSelector("button[class='login-form-button ant-btn 
ant-btn-primary']"));
+    pollingWait(By.cssSelector("a[routerlink='/workbench/dashboard']"), 
MAX_BROWSER_TIMEOUT_SEC);
+
+    // Start Routing & Navigation in data-dict 
+    LOG.info("Start Routing & Navigation in data-dict");
+    pollingWait(By.xpath("//span[contains(text(), \"Manager\")]"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    WebDriverWait wait = new WebDriverWait( driver, 60);
+    pollingWait(By.xpath("//a[@href='/workbench/manager/dataDict']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//span[@class='ant-breadcrumb-link
 ng-star-inserted']")));
+    Assert.assertEquals(driver.getCurrentUrl(), 
"http://localhost:8080/workbench/manager/dataDict";);
+
+    // Add button
+    LOG.info("[TEST] Add button");
+    // Add --> Ok --> required feedback
+    pollingWait(By.cssSelector("form > nz-form-item:nth-child(3) > 
nz-form-control > div > span > button.ant-btn.ant-btn-default"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    Assert.assertEquals( driver.findElements(By.xpath("//div[contains(text(), 
\"Add\")]")).size(), 1);
+    pollingWait(By.cssSelector("button[class='ant-btn ng-star-inserted 
ant-btn-primary']"), MAX_BROWSER_TIMEOUT_SEC).click();
+    Assert.assertEquals( driver.findElements(By.xpath("//div[contains(text(), 
\"Add\")]")).size(), 1);
+    // Add --> Close
+    pollingWait(By.cssSelector("button[class='ant-btn ng-star-inserted 
ant-btn-default']"), MAX_BROWSER_TIMEOUT_SEC).click();
+    Assert.assertEquals( driver.findElements(By.xpath("//div[contains(text(), 
\"Add\")]")).size(), 0);
+    // Add --> set input --> close
+    pollingWait(By.cssSelector("form > nz-form-item:nth-child(3) > 
nz-form-control > div > span > button.ant-btn.ant-btn-default"), 
MAX_BROWSER_TIMEOUT_SEC).click(); 
+    pollingWait(By.xpath("//input[@id='inputNewDictCode']"), 
MAX_BROWSER_TIMEOUT_SEC).sendKeys("test new dict code");
+    pollingWait(By.xpath("//input[@id='inputNewDictName']"), 
MAX_BROWSER_TIMEOUT_SEC).sendKeys("test new dict name");
+    pollingWait(By.xpath("//input[@id='inputNewDictDescription']"), 
MAX_BROWSER_TIMEOUT_SEC).sendKeys("test new dict description");
+    pollingWait(By.cssSelector("button[class='ant-btn ng-star-inserted 
ant-btn-default']"), MAX_BROWSER_TIMEOUT_SEC).click();
+    Assert.assertEquals( 
driver.findElements(By.xpath("//td[@id='dataDictCodetest new dict 
code']")).size(), 0);
+    // Add --> set input --> ok --> new dict 
+    pollingWait(By.cssSelector("form > nz-form-item:nth-child(3) > 
nz-form-control > div > span > button.ant-btn.ant-btn-default"), 
MAX_BROWSER_TIMEOUT_SEC).click(); 
+    pollingWait(By.xpath("//input[@id='inputNewDictCode']"), 
MAX_BROWSER_TIMEOUT_SEC).sendKeys("test new dict code");
+    pollingWait(By.xpath("//input[@id='inputNewDictName']"), 
MAX_BROWSER_TIMEOUT_SEC).sendKeys("test new dict name");
+    pollingWait(By.xpath("//input[@id='inputNewDictDescription']"), 
MAX_BROWSER_TIMEOUT_SEC).sendKeys("test new dict description");
+    pollingWait(By.cssSelector("button[class='ant-btn ng-star-inserted 
ant-btn-primary']"), MAX_BROWSER_TIMEOUT_SEC).click();
+    Assert.assertEquals( 
driver.findElements(By.xpath("//td[@id='dataDictCodetest new dict 
code']")).size(), 1);
+
+    // Configuration button
+    LOG.info("[TEST] PROJECT_TYPE Configuration button");
+    // old dict --> More --> Configuration --> Add --> set input --> OK
+    
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//a[@id='dataDictMorePROJECT_TYPE']")));
+    pollingWait(By.xpath("//a[@id='dataDictMorePROJECT_TYPE']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    pollingWait(By.xpath("//li[@id='dataDictConfigurationPROJECT_TYPE']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    pollingWait(By.xpath("//button[@id='dataDictItemAddPROJECT_TYPE']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    pollingWait(By.xpath("//span[@class='ant-cascader-picker-label']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    pollingWait(By.xpath("//li[@title='unavailable']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    pollingWait(By.xpath("//input[@id='newItemCodePROJECT_TYPE']"), 
MAX_BROWSER_TIMEOUT_SEC).sendKeys("qqq");
+    pollingWait(By.xpath("//input[@id='newItemNamePROJECT_TYPE']"), 
MAX_BROWSER_TIMEOUT_SEC).sendKeys("www");
+    pollingWait(By.xpath("//button[@class='ant-btn ng-star-inserted 
ant-btn-default ant-btn-sm']"), MAX_BROWSER_TIMEOUT_SEC).click();
+    Assert.assertEquals( driver.findElements(By.xpath("//td[contains(text(), 
\"qqq\")]")).size(), 1);
+    pollingWait(By.xpath("//button[@class='ant-btn ng-star-inserted 
ant-btn-primary']"), MAX_BROWSER_TIMEOUT_SEC).click();
+    // Check old dict
+    pollingWait(By.xpath("//a[@id='dataDictMorePROJECT_TYPE']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    pollingWait(By.xpath("//li[@id='dataDictConfigurationPROJECT_TYPE']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    Assert.assertEquals( driver.findElements(By.xpath("//td[contains(text(), 
\"qqq\")]")).size(), 1);
+    pollingWait(By.xpath("//button[@class='ant-btn ng-star-inserted 
ant-btn-primary']"), MAX_BROWSER_TIMEOUT_SEC).click();
+  
+    // Edit button
+    LOG.info("[TEST] Edit button");
+    // Edit dict --> Update --> OK --> More --> Configuration
+    
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//a[@id='dataDictEditPROJECT_VISIBILITY']")));
+    pollingWait(By.xpath("//a[@id='dataDictEditPROJECT_VISIBILITY']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    pollingWait(By.xpath("//input[@id='inputNewDictCode']"), 
MAX_BROWSER_TIMEOUT_SEC).sendKeys("123");
+    pollingWait(By.cssSelector("button[class='ant-btn ng-star-inserted 
ant-btn-primary']"), MAX_BROWSER_TIMEOUT_SEC).click();
+    Assert.assertEquals( 
driver.findElements(By.xpath("//td[@id='dataDictCodePROJECT_VISIBILITY123']")).size(),
 1);
+    pollingWait(By.xpath("//a[@id='dataDictMorePROJECT_VISIBILITY123']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    
pollingWait(By.xpath("//li[@id='dataDictConfigurationPROJECT_VISIBILITY123']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    Assert.assertEquals( driver.findElements(By.xpath("//td[contains(text(), 
\"PROJECT_VISIBILITY_PRIVATE\")]")).size(), 1);
+    Assert.assertEquals( driver.findElements(By.xpath("//td[contains(text(), 
\"PROJECT_VISIBILITY_TEAM\")]")).size(), 1);
+    Assert.assertEquals( driver.findElements(By.xpath("//td[contains(text(), 
\"PROJECT_VISIBILITY_PUBLIC\")]")).size(), 1);
+    pollingWait(By.xpath("//button[@class='ant-btn ng-star-inserted 
ant-btn-primary']"), MAX_BROWSER_TIMEOUT_SEC).click();
+
+    LOG.info("[TEST] test new dict code Configuration button");
+    // new dict --> More --> Configuration --> Add --> set input --> OK
+    
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//a[@id='dataDictMoretest
 new dict code']")));
+    pollingWait(By.xpath("//a[@id='dataDictMoretest new dict code']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    pollingWait(By.xpath("//li[@id='dataDictConfigurationtest new dict 
code']"), MAX_BROWSER_TIMEOUT_SEC).click();
+    pollingWait(By.xpath("//button[@id='dataDictItemAddtest new dict code']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    pollingWait(By.xpath("//span[@class='ant-cascader-picker-label']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    pollingWait(By.xpath("//li[@title='available']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    pollingWait(By.xpath("//input[@id='newItemCodetest new dict code']"), 
MAX_BROWSER_TIMEOUT_SEC).sendKeys("aaa");
+    pollingWait(By.xpath("//input[@id='newItemNametest new dict code']"), 
MAX_BROWSER_TIMEOUT_SEC).sendKeys("bbb");
+    pollingWait(By.xpath("//button[@class='ant-btn ng-star-inserted 
ant-btn-default ant-btn-sm']"), MAX_BROWSER_TIMEOUT_SEC).click();
+    Assert.assertEquals( driver.findElements(By.xpath("//td[contains(text(), 
\"aaa\")]")).size(), 1);
+    pollingWait(By.xpath("//button[@class='ant-btn ng-star-inserted 
ant-btn-primary']"), MAX_BROWSER_TIMEOUT_SEC).click();
+    // Check new dict
+    pollingWait(By.xpath("//a[@id='dataDictMoretest new dict code']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    pollingWait(By.xpath("//li[@id='dataDictConfigurationtest new dict 
code']"), MAX_BROWSER_TIMEOUT_SEC).click();
+    Assert.assertEquals( driver.findElements(By.xpath("//td[contains(text(), 
\"aaa\")]")).size(), 1);
+    pollingWait(By.xpath("//button[@class='ant-btn ng-star-inserted 
ant-btn-primary']"), MAX_BROWSER_TIMEOUT_SEC).click();
+
+    // Delete button
+    LOG.info("[TEST] Delete button");
+    // More --> Delete
+    Assert.assertEquals( 
driver.findElements(By.xpath("//td[@id='dataDictCodeSYS_USER_SEX']")).size(), 
1);
+    
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//a[@id='dataDictMoreSYS_USER_SEX']")));
+    pollingWait(By.xpath("//a[@id='dataDictMoreSYS_USER_SEX']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    pollingWait(By.xpath("//li[@id='dataDictDeleteSYS_USER_SEX']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    
pollingWait(By.xpath("//span[text()='Ok']/ancestor::button[@ng-reflect-nz-type='primary']"),
 MAX_BROWSER_TIMEOUT_SEC).click();
+    Assert.assertEquals( 
driver.findElements(By.xpath("//td[@id='dataDictCodeSYS_USER_SEX']")).size(), 
0);
+  }
+}
diff --git 
a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/sidebarIT.java
 
b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/sidebarIT.java
index 03afd7d..cfee7a2 100644
--- 
a/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/sidebarIT.java
+++ 
b/submarine-test/test-e2e/src/test/java/org/apache/submarine/integration/sidebarIT.java
@@ -54,7 +54,7 @@ public class sidebarIT extends AbstractSubmarineIT {
     clickAndWait(By.cssSelector("button[class='login-form-button ant-btn 
ant-btn-primary']"));
     pollingWait(By.cssSelector("a[routerlink='/workbench/dashboard']"), 
MAX_BROWSER_TIMEOUT_SEC);
 
-    // Start Routing & Navigation in sidebar 
+    // Start Routing & Navigation in sidebar
     LOG.info("Start Routing & Navigation in sidebar");
     pollingWait(By.xpath("//span[contains(text(), \"Workspace\")]"), 
MAX_BROWSER_TIMEOUT_SEC).click();
     Assert.assertEquals(driver.getCurrentUrl(), 
"http://localhost:8080/workbench/workspace";);
@@ -75,8 +75,8 @@ public class sidebarIT extends AbstractSubmarineIT {
     
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//span[@class='ant-breadcrumb-link
 ng-star-inserted']")));
     Assert.assertEquals(driver.getCurrentUrl(), 
"http://localhost:8080/workbench/manager/user";);
 
-    pollingWait(By.xpath("//a[@href='/workbench/manager/data-dict']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
-    Assert.assertEquals(driver.getCurrentUrl(), 
"http://localhost:8080/workbench/manager/data-dict";);
+    pollingWait(By.xpath("//a[@href='/workbench/manager/dataDict']"), 
MAX_BROWSER_TIMEOUT_SEC).click();
+    Assert.assertEquals(driver.getCurrentUrl(), 
"http://localhost:8080/workbench/manager/dataDict";);
     pollingWait(By.xpath("//span[contains(text(), \"Home\")]"), 
MAX_BROWSER_TIMEOUT_SEC).click();
     Assert.assertEquals(driver.getCurrentUrl(), 
"http://localhost:8080/workbench/home";); 
   }
diff --git 
a/submarine-workbench/workbench-web-ng/src/app/components/page-layout/page-layout.component.html
 
b/submarine-workbench/workbench-web-ng/src/app/components/page-layout/page-layout.component.html
index bf9287d..354e2ec 100644
--- 
a/submarine-workbench/workbench-web-ng/src/app/components/page-layout/page-layout.component.html
+++ 
b/submarine-workbench/workbench-web-ng/src/app/components/page-layout/page-layout.component.html
@@ -21,7 +21,8 @@
   <nz-page-header [nzTitle]="title|titlecase" *ngIf="breadCrumb">
     <nz-breadcrumb nz-page-header-breadcrumb>
       <nz-breadcrumb-item *ngFor="let item of breadCrumb">
-        {{item|titlecase}}
+        <a *ngIf="item.routerLink" 
[routerLink]="item.routerLink">{{item.title|titlecase}}</a>
+        <span *ngIf="!item.routerLink">{{item.title|titlecase}}</span>
       </nz-breadcrumb-item>
     </nz-breadcrumb>
     <nz-page-header-content *ngIf="description">
diff --git 
a/submarine-workbench/workbench-web-ng/src/app/components/page-layout/page-layout.component.ts
 
b/submarine-workbench/workbench-web-ng/src/app/components/page-layout/page-layout.component.ts
index 77a8a28..66e7eb8 100644
--- 
a/submarine-workbench/workbench-web-ng/src/app/components/page-layout/page-layout.component.ts
+++ 
b/submarine-workbench/workbench-web-ng/src/app/components/page-layout/page-layout.component.ts
@@ -19,6 +19,11 @@
 
 import { Component, Input, OnInit } from '@angular/core';
 
+interface BreadCrumbItem {
+  title: string;
+  routerLink?: string;
+}
+
 @Component({
   selector: 'submarine-page-layout',
   templateUrl: './page-layout.component.html',
@@ -27,7 +32,7 @@ import { Component, Input, OnInit } from '@angular/core';
 export class PageLayoutComponent implements OnInit {
   @Input() title: string;
   @Input() description: string;
-  @Input() breadCrumb: string[];
+  @Input() breadCrumb: BreadCrumbItem[];
 
   constructor() {
   }
diff --git 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-config-modal/data-dict-config-modal.component.html
 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-config-modal/data-dict-config-modal.component.html
new file mode 100644
index 0000000..62739f2
--- /dev/null
+++ 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-config-modal/data-dict-config-modal.component.html
@@ -0,0 +1,146 @@
+<!--
+  ~ 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.
+  -->
+
+<nz-modal [nzVisible]="visible" [nzTitle]="modalTitle" nzCancelText="Close" 
nzOkText="Ok" (nzOnCancel)="hideModal()"
+          (nzOnOk)="hideModal()" [nzWidth]="modalWidth">
+  <div class="dictItem-table-operate">
+    <form nz-form [nzLayout]="'inline'" [formGroup]="dictItemListForm">
+      <nz-form-item>
+        <nz-form-label>Name</nz-form-label>
+        <nz-form-control>
+          <input nz-input formControlName="dictItemName" placeholder="Enter 
Item Name"/>
+        </nz-form-control>
+      </nz-form-item>
+      <nz-form-item>
+        <nz-form-label>Code</nz-form-label>
+        <nz-form-control>
+          <input nz-input formControlName="dictItemCode" placeholder="Enter 
Item Code"/>
+        </nz-form-control>
+      </nz-form-item>
+      <nz-form-item>
+        <nz-form-control>
+          <button nz-button nzType="primary" (click)="queryDictItem()">
+            <i nz-icon nzType="search"></i>
+            Query
+          </button>
+          <button id="{{ 'dataDictItemAdd' + dictCode }}" nz-button 
style="margin-left: 8px" (click)="addDictItem()">
+            <i nz-icon nzType="plus"></i>
+            Add
+          </button>
+        </nz-form-control>
+      </nz-form-item>
+    </form>
+  </div>
+  <div>
+    <form nz-form [nzLayout]="'inline'" [formGroup]="newItemForm" 
(ngSubmit)="newItemSubmit()">
+      <nz-table #table [nzData]="selectedDictItemList" [nzScroll]="{ x: 
'900px' }" nzNoResult="No result" nzBordered>
+        <thead>
+          <tr nz-col>
+            <th nzWidth="400px">Code</th>
+            <th nzWidth="200px">Name</th>
+            <th nzWidth="200px">Status</th>
+            <th nzWidth="200px">Action</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr *ngIf="showNewRow">
+            <td>
+              <input id="{{ 'newItemCode' + dictCode }}" type="text" nz-input 
nzSize="small" formControlName="newItemCode"/>
+            </td>
+            <td>
+              <input id="{{ 'newItemName' + dictCode }}" type="text" nz-input 
nzSize="small" formControlName="newItemName"/>
+            </td>
+            <td>
+              <nz-cascader id="{{ 'newItemStatus' + dictCode }}" 
nzSize="small" [nzOptions]="statusOptions" 
formControlName="newItemStatus"></nz-cascader>
+            </td>
+            <td>
+              <button *ngIf="newItemForm.get('newItemCode').valid &&
+                             newItemForm.get('newItemName').valid &&
+                             newItemForm.get('newItemStatus').valid; else 
disabledAddButtonTpl"
+                             nzSize="small" nz-button nzType="default" 
(click)="addNewItemToList()">
+                <i nz-icon nzType="file-add"></i>
+                Add
+              </button>
+              <ng-template #disabledAddButtonTpl>
+                <button nzSize="small" nz-button nzType="default" 
(click)="addNewItemToList()" disabled>
+                  <i nz-icon nzType="file-add"></i>
+                  Add
+                </button>
+              </ng-template>
+              <button nzSize="small" nz-button nzType="default" 
style="margin-left: 8px" (click)="cancelAddDictItem()">
+                <i nz-icon nzType="undo"></i>
+                Cancel
+              </button>
+            </td>
+          </tr>
+          <tr *ngFor="let dictItem of table.data; index as dictItemIndex">
+            <td>
+              <ng-container *ngIf="!dictItem.edit; else codeInputTpl">
+                {{  dictItem.code }}
+              </ng-container>
+              <ng-template #codeInputTpl>
+                <input type="text" nz-input nzSize="small" 
formControlName="selectedDictItemCode"/>
+              </ng-template>
+            </td>
+            <td>
+              <ng-container *ngIf="!dictItem.edit; else nameInputTpl">
+                {{  dictItem.name }}
+              </ng-container>
+              <ng-template #nameInputTpl>
+                <input type="text" nz-input nzSize="small" 
formControlName="selectedDictItemName"/>
+              </ng-template>
+            </td>
+            <td>
+              <nz-tag *ngIf="!dictItem.edit && (dictItem.status == 
'available')" [nzColor]="'blue'">{{ dictItem.status }}</nz-tag>
+              <nz-tag *ngIf="!dictItem.edit && (dictItem.status == 
'unavailable')" [nzColor]="'red'">{{ dictItem.status }}</nz-tag>
+              <nz-cascader *ngIf="dictItem.edit" nzSize="small" 
[nzOptions]="statusOptions" 
formControlName="selectedDictItemStatus"></nz-cascader>
+            </td>
+            <td>
+              <ng-container *ngIf="!dictItem.edit; else saveTpl">
+                <a (click)="startEdit(dictItemIndex, dictItem.code, 
dictItem.name, dictItem.status)">
+                  <i nz-icon nzType="edit" nzTheme="outline"></i>
+                  Edit
+                </a>
+              </ng-container>
+              <ng-template #saveTpl>
+                <button *ngIf="newItemForm.get('selectedDictItemCode').valid 
&& 
+                               newItemForm.get('selectedDictItemName').valid &&
+                               
newItemForm.get('selectedDictItemStatus').valid; else disabledSaveButtonTpl" 
+                        nzSize="small" nz-button nzType="default" 
(click)="saveEdit(dictItemIndex)">
+                  <i nz-icon nzType="save" nzTheme="outline"></i>
+                  Save
+                </button>
+                <ng-template #disabledSaveButtonTpl>
+                  <button nzSize="small" nz-button nzType="default" 
(click)="saveEdit(dictItemIndex)" disabled>
+                    <i nz-icon nzType="save" nzTheme="outline"></i>
+                    Save
+                  </button>
+                </ng-template>
+                <button nzSize="small" nz-button nzType="default" 
style="margin-left: 8px" (click)="cancelEdit(dictItemIndex)">
+                  <i nz-icon nzType="undo" nzTheme="outline"></i>
+                  Cancel
+                </button>
+              </ng-template>
+            </td>
+          </tr>
+        </tbody>
+      </nz-table>
+    </form>
+  </div>
+</nz-modal>
diff --git 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.scss
 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-config-modal/data-dict-config-modal.component.scss
similarity index 93%
copy from 
submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.scss
copy to 
submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-config-modal/data-dict-config-modal.component.scss
index 510f082..8aa93a1 100644
--- 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.scss
+++ 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-config-modal/data-dict-config-modal.component.scss
@@ -17,3 +17,6 @@
  * under the License.
  */
 
+.dictItem-table-operate {
+    margin-bottom: 16px;
+}
diff --git 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-config-modal/data-dict-config-modal.component.ts
 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-config-modal/data-dict-config-modal.component.ts
new file mode 100644
index 0000000..1711a17
--- /dev/null
+++ 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-config-modal/data-dict-config-modal.component.ts
@@ -0,0 +1,294 @@
+/*
+ * 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 { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } 
from '@angular/core';
+import { FormBuilder, FormControl, FormGroup, Validators } from 
'@angular/forms';
+import { CascaderOption } from 'ng-zorro-antd/cascader';
+
+interface DictItemInfo {
+  code: string;
+  name: string;
+  status: string;
+  edit: boolean;
+}
+
+@Component({
+  selector: 'submarine-data-dict-config-modal',
+  templateUrl: './data-dict-config-modal.component.html',
+  styleUrls: ['./data-dict-config-modal.component.scss']
+})
+export class DataDictConfigModalComponent implements OnChanges {
+
+  constructor(private fb: FormBuilder) {
+    this.dictItemListForm = this.fb.group({
+      dictItemCode: ['', [Validators.required]],
+      dictItemName: ['', [Validators.required]]
+    });
+
+    this.newItemForm = this.fb.group({
+      newItemCode: ['', [Validators.required]],
+      newItemName: ['', [Validators.required]],
+      newItemStatus: ['', [Validators.required]],
+      selectedDictItemCode: ['', [Validators.required]],
+      selectedDictItemName: ['', [Validators.required]],
+      selectedDictItemStatus: ['', [Validators.required]]
+    });
+  }
+  @Input() modalTitle: string; // Add | Edit
+  @Input() dictCode: string;
+  @Input() dictName: string;
+  @Input() description: string;
+  @Input() visible: boolean;
+  @Input() modalWidth: number;
+  @Input() lastDictCode: string;
+  @Input() dictCodeChanged: boolean;
+  @Input() newDictCode: string;
+  @Output() readonly close: EventEmitter<any> = new EventEmitter();
+  @Output() readonly ok: EventEmitter<any> = new EventEmitter();
+  dictItemListForm: FormGroup;
+  newItemForm: FormGroup;
+  selectedItemIndex: number = 0;
+
+  // TODO(kevin85421): mock data
+  dictItemList: { [id: string]: DictItemInfo[] } = {
+    "PROJECT_TYPE": [
+      {
+        code: 'PROJECT_TYPE_NOTEBOOK',
+        name: 'notebook',
+        status: 'available',
+        edit: false
+      },
+      {
+        code: 'PROJECT_TYPE_PYTHON',
+        name: 'python',
+        status: 'available',
+        edit: false
+      },
+      {
+        code: 'PROJECT_TYPE_R',
+        name: 'R',
+        status: 'available',
+        edit: false
+      },
+      {
+        code: 'PROJECT_TYPE_SCALA',
+        name: 'scala',
+        status: 'available',
+        edit: false
+      },
+      {
+        code: 'PROJECT_TYPE_TENSORFLOW',
+        name: 'tensorflow',
+        status: 'available',
+        edit: false
+      },
+      {
+        code: 'PROJECT_TYPE_PYTORCH',
+        name: 'pytorch',
+        status: 'available',
+        edit: false
+      }
+    ],
+    "PROJECT_VISIBILITY": [
+      {
+        code: 'PROJECT_VISIBILITY_PRIVATE',
+        name: 'private',
+        status: 'available',
+        edit: false
+      },
+      {
+        code: 'PROJECT_VISIBILITY_TEAM',
+        name: 'team',
+        status: 'available',
+        edit: false
+      },
+      {
+        code: 'PROJECT_VISIBILITY_PUBLIC',
+        name: 'public',
+        status: 'available',
+        edit: false
+      }
+    ],
+    "PROJECT_PERMISSION": [
+      {
+        code: 'PROJECT_PERMISSION_VIEW',
+        name: 'can view',
+        status: 'available',
+        edit: false
+      },
+      {
+        code: 'PROJECT_PERMISSION_EDIT',
+        name: 'can edit',
+        status: 'available',
+        edit: false
+      },
+      {
+        code: 'PROJECT_PERMISSION_EXECUTE',
+        name: 'can execute',
+        status: 'available',
+        edit: false
+      }
+    ],
+    "SYS_USER_SEX": [
+      {
+        code: 'SYS_USER_SEX_MALE',
+        name: 'Male',
+        status: 'available',
+        edit: false
+      },
+      {
+        code: 'SYS_USER_SEX_FEMALE',
+        name: 'Female',
+        status: 'available',
+        edit: false
+      }
+    ],
+    "SYS_USER_STATUS": [
+      {
+        code: 'SYS_USER_STATUS_AVAILABLE',
+        name: 'Available',
+        status: 'available',
+        edit: false
+      },
+      {
+        code: 'SYS_USER_STATUS_LOCKED',
+        name: 'Locked',
+        status: 'available',
+        edit: false
+      },
+      {
+        code: 'SYS_USER_STATUS_REGISTERED',
+        name: 'New Registered',
+        status: 'available',
+        edit: false
+      }
+    ]
+  };
+
+  // Selected Item List
+  selectedDictItemList: DictItemInfo[];
+
+  // Add Item
+  showNewRow: boolean = false;
+
+  statusOptions: CascaderOption[] = [
+    {
+      value: 'available',
+      label: 'available',
+      isLeaf: true
+    },
+    {
+      value: 'unavailable',
+      label: 'unavailable',
+      isLeaf: true
+    }
+  ];
+  ngOnChanges(changes: SimpleChanges) {
+    if (this.dictCodeChanged) {
+      this.dictItemList[this.dictCode] = this.dictItemList[this.lastDictCode];
+      this.dictCodeChanged = false;
+      delete this.dictItemList[this.lastDictCode];
+    }
+
+    if (this.newDictCode !== '') {
+      this.dictItemList[this.newDictCode] = [];
+      this.newDictCode = '';
+    }
+    this.selectedDictItemList = this.dictItemList[this.dictCode];
+  }
+
+  hideModal() {
+    this.cancelEdit(this.selectedItemIndex);
+    this.cancelAddDictItem();
+    this.close.emit();
+  }
+
+  // TODO(kevin85421)
+  queryDictItem() {
+    // Get query data
+    for (const key in this.dictItemListForm.controls) {
+      console.log(key);
+      console.log(this.dictItemListForm.controls[key].value);
+    }
+  }
+
+  startEdit(dictItemIndex: number, dictItemCode: string, dictItemName: string, 
dictItemStatus: string) {
+    this.cancelAddDictItem();
+    if (this.selectedItemIndex !== dictItemIndex) {
+      this.cancelEdit(this.selectedItemIndex);
+    }
+    this.selectedDictItemList[dictItemIndex].edit = true;
+    this.selectedItemIndex = dictItemIndex;
+    this.newItemForm.setValue({ newItemCode: '', newItemName: '', 
newItemStatus: '',
+                               selectedDictItemCode: dictItemCode, 
selectedDictItemName: dictItemName,
+                               selectedDictItemStatus: dictItemStatus});
+  }
+
+  saveEdit(dictItemIndex: number) {
+    this.selectedDictItemList[dictItemIndex].code = 
this.newItemForm.value.selectedDictItemCode;
+    this.selectedDictItemList[dictItemIndex].name = 
this.newItemForm.value.selectedDictItemName;
+    this.selectedDictItemList[dictItemIndex].status = 
this.newItemForm.value.selectedDictItemStatus;
+    this.selectedDictItemList[dictItemIndex].edit = false;
+  }
+
+  cancelEdit(dictItemIndex: number) {
+    this.selectedItemIndex = 0;
+    this.newItemForm.setValue({ newItemCode: '', newItemName: '', 
newItemStatus: '',
+                                selectedDictItemCode: '', 
selectedDictItemName: '',
+                                selectedDictItemStatus: ''});
+    if (this.selectedDictItemList.length !== 0) {
+      this.selectedDictItemList[dictItemIndex].edit = false;
+    }
+  }
+
+  // Add Item
+  addDictItem() {
+    this.cancelEdit(this.selectedItemIndex);
+    this.showNewRow = true;
+  }
+
+  cancelAddDictItem() {
+    this.showNewRow = false;
+  }
+
+  addNewItemToList() {
+    this.selectedDictItemList = [
+      {
+        code: this.newItemForm.value.newItemCode,
+        name: this.newItemForm.value.newItemName,
+        status: this.newItemForm.value.newItemStatus,
+        edit: false
+      },
+      ...this.selectedDictItemList
+    ];
+    this.dictItemList[this.dictCode] = [
+      {
+        code: this.newItemForm.value.newItemCode,
+        name: this.newItemForm.value.newItemName,
+        status: this.newItemForm.value.newItemStatus,
+        edit: false
+      },
+      ...this.dictItemList[this.dictCode]
+    ]
+    this.newItemForm.setValue({ newItemCode: '', newItemName: '', 
newItemStatus: '',
+                                selectedDictItemCode: '', 
selectedDictItemName: '',
+                                selectedDictItemStatus: ''});
+    this.cancelAddDictItem();
+  }
+}
diff --git 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-modal/data-dict-modal.component.html
 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-modal/data-dict-modal.component.html
new file mode 100644
index 0000000..7d2cd8d
--- /dev/null
+++ 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-modal/data-dict-modal.component.html
@@ -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.
+  -->
+
+<nz-modal [nzVisible]="visible" [nzTitle]="modalTitle" nzCancelText="Close" 
nzOkText="Ok" (nzOnCancel)="hideModal()"
+          (nzOnOk)="submitForm()">
+  <form nz-form nzLayout="horizontal" [formGroup]="form">
+    <nz-form-item>
+      <nz-form-label nzRequired>
+        Dictionary Code
+      </nz-form-label>
+      <nz-form-control>
+        <input id="inputNewDictCode" type="text" nz-input 
formControlName="dictCode"/>
+      </nz-form-control>
+    </nz-form-item>
+    <nz-form-item>
+      <nz-form-label nzRequired>
+        Dictionary Name
+      </nz-form-label>
+      <nz-form-control>
+        <input id="inputNewDictName" nz-input formControlName="dictName"/>
+      </nz-form-control>
+    </nz-form-item>
+    <nz-form-item>
+      <nz-form-label>
+        Description
+      </nz-form-label>
+      <nz-form-control>
+        <input id="inputNewDictDescription" nz-input 
formControlName="description"/>
+      </nz-form-control>
+    </nz-form-item>
+  </form>
+</nz-modal>
diff --git 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.scss
 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-modal/data-dict-modal.component.scss
similarity index 100%
copy from 
submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.scss
copy to 
submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-modal/data-dict-modal.component.scss
diff --git 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-modal/data-dict-modal.component.ts
 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-modal/data-dict-modal.component.ts
new file mode 100644
index 0000000..52fce4e
--- /dev/null
+++ 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict-modal/data-dict-modal.component.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } 
from '@angular/core';
+import { FormBuilder, FormControl, FormGroup, Validators } from 
'@angular/forms';
+
+@Component({
+  selector: 'submarine-data-dict-modal',
+  templateUrl: './data-dict-modal.component.html',
+  styleUrls: ['./data-dict-modal.component.scss']
+})
+export class DataDictModalComponent implements OnChanges {
+  @Input() modalTitle: string; // Add | Edit
+  @Input() dictCode: string;
+  @Input() dictName: string;
+  @Input() description: string;
+  @Input() visible: boolean;
+  @Output() readonly close: EventEmitter<any> = new EventEmitter();
+  @Output() readonly ok: EventEmitter<any> = new EventEmitter();
+  form: FormGroup;
+
+  constructor(private fb: FormBuilder) {
+    this.form = this.fb.group({
+      dictCode: ['', Validators.required],
+      dictName: ['', Validators.required],
+      description: ['']
+    });
+  }
+
+  ngOnChanges(changes: SimpleChanges) {
+    this.form.reset({
+      dictCode: this.dictCode,
+      dictName: this.dictName,
+      description: this.description
+    });
+  }
+
+  hideModal() {
+    this.close.emit();
+  }
+
+  submitForm() {
+    for (const key in this.form.controls) {
+      this.form.controls[key].markAsDirty();
+      this.form.controls[key].updateValueAndValidity();
+    }
+
+    if (!this.form.valid) {
+      return;
+    }
+
+    this.ok.emit(this.form.value);
+  }
+}
diff --git 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.html
 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.html
index 5f8f81f..3375b79 100644
--- 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.html
+++ 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.html
@@ -17,4 +17,107 @@
   ~ under the License.
   -->
 
-<p>data-dict works!</p>
+<nz-card>
+  <div class="data-dict-table-operate">
+    <form nz-form [nzLayout]="'inline'" [formGroup]="dataDictForm">
+      <nz-form-item>
+        <nz-form-label>Dictionary Name</nz-form-label>
+        <nz-form-control>
+          <input nz-input formControlName="dictName" placeholder="Enter 
Dictionary Name"/>
+        </nz-form-control>
+      </nz-form-item>
+      <nz-form-item>
+        <nz-form-label>Dictionary Code</nz-form-label>
+        <nz-form-control>
+          <input nz-input formControlName="dictCode" placeholder="Enter 
Dictionary Code"/>
+        </nz-form-control>
+      </nz-form-item>
+      <nz-form-item>
+        <nz-form-control>
+          <button nz-button nzType="primary" (click)="queryDataDict()">
+            <i nz-icon nzType="search"></i>
+            Query
+          </button>
+          <button nz-button style="margin-left: 8px" 
(click)="onShowAddDataDictModal()">
+            <i nz-icon nzType="plus"></i>
+            Add
+          </button>
+        </nz-form-control>
+      </nz-form-item>
+    </form>
+  </div>
+
+  <nz-table #table [nzData]="sysDictList" [nzScroll]="{ x: '1100px' }" 
nzNoResult="No result" nzBordered>
+    <thead>
+      <tr>
+        <th nzLeft="0px">#</th>
+        <th>Dictionary Code</th>
+        <th>Dictionary Name</th>
+        <th>Description</th>
+        <th>Status</th>
+        <th nzRight="0px">Action</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr *ngFor="let data of table.data; index as dictItemIndex">
+        <td nzLeft="0px">{{ dictItemIndex+1 }}</td>
+        <td id="{{ 'dataDictCode' + data.dictCode }}">{{ data.dictCode }}</td>
+        <td>{{ data.dictName }}</td>
+        <td>{{ data.description }}</td>
+        <td>
+          <nz-tag [nzColor]="'blue'">{{ data.status }}</nz-tag>
+        </td>
+        <td class="td-action" nzRight="0px">
+          <a id="{{ 'dataDictEdit' + data.dictCode }}" 
(click)="onShowEditDataDictModal(data, dictItemIndex)">
+            <i nz-icon nzType="edit" nzTheme="outline"></i>
+            Edit
+          </a>
+          <a id="{{ 'dataDictMore' + data.dictCode }}" nz-dropdown 
[nzDropdownMenu]="more">
+            More
+            <i nz-icon nzType="down"></i>
+          </a>
+          <nz-dropdown-menu #more="nzDropdownMenu">
+            <ul nz-menu nzSelectable>
+              <li nz-menu-item (click)="onShowConfigModal(data, 
dictItemIndex)" id="{{ 'dataDictConfiguration' + data.dictCode 
}}">Configuration</li>
+              <li
+                id="{{ 'dataDictDelete' + data.dictCode }}"
+                nz-menu-item
+                nz-popconfirm
+                nzTitle="Are you certain you want to delete?"
+                nzOkText="Ok"
+                nzCancelText="Cancel"
+                (nzOnConfirm)="onDeleteDataDictItem(data)"
+              >
+                Delete
+              </li>
+            </ul>
+          </nz-dropdown-menu>
+        </td>
+      </tr>
+    </tbody>
+  </nz-table>
+</nz-card>
+
+<submarine-data-dict-modal
+  [modalTitle]="modalTitle"
+  [visible]="dataDictModalVisible"
+  [dictCode]="selectedDictCode"
+  [dictName]="selectedDictName"
+  [description]="selectedDescription" 
+  (close)="onHideDataDictModal()"
+  (ok)="updateDataDict($event)"
+></submarine-data-dict-modal>
+
+<submarine-data-dict-config-modal
+  [modalTitle]="configModalTitle"
+  [visible]="configModalVisible"
+  [dictCode]="selectedDictCode"
+  [lastDictCode]="lastDictCode"
+  [newDictCode]="newDictItemCode"
+  [dictCodeChanged]="editDictCodeChanged"
+  [dictName]="selectedDictName"
+  [description]="selectedDescription"
+  [modalWidth]="configModalWidth"
+  (close)="onHideConfigModal()"
+  (ok)="updateDataDict($event)"
+></submarine-data-dict-config-modal>
diff --git 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.scss
 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.scss
index 510f082..9133cd6 100644
--- 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.scss
+++ 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.scss
@@ -17,3 +17,14 @@
  * under the License.
  */
 
+.td-action a + a {
+    margin-left: 8px;
+}
+
+.data-dict-table-operate {
+    margin-bottom: 16px;
+}
+
+nz-table td {
+    word-break: break-all;
+}
diff --git 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.ts
 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.ts
index 3dde811..6fa38d3 100644
--- 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.ts
+++ 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/data-dict/data-dict.component.ts
@@ -18,16 +18,155 @@
  */
 
 import { Component, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup } from '@angular/forms';
+import { SysDictItem } from '@submarine/interfaces/sys-dict-item';
 
 @Component({
   selector: 'submarine-data-dict',
   templateUrl: './data-dict.component.html',
   styleUrls: ['./data-dict.component.scss']
 })
+
 export class DataDictComponent implements OnInit {
-  constructor() {
+  constructor(private fb: FormBuilder) {
   }
+  dataDictForm: FormGroup;
+  // TODO(kevin85421): (mock data) Replace it with sys-dict-item.ts
+  sysDictList = [
+    {
+      dictCode: 'PROJECT_TYPE',
+      dictName: 'Project machine learning type',
+      description: 'submarine system dict, Do not modify.',
+      status: 'available'
+    },
+    {
+      dictCode: 'PROJECT_VISIBILITY',
+      dictName: 'Project visibility type',
+      description: 'submarine system dict, Do not modify.',
+      status: 'available'
+    },
+    {
+      dictCode: 'PROJECT_PERMISSION',
+      dictName: 'Project permission type',
+      description: 'submarine system dict, Do not modify.',
+      status: 'available'
+    },
+    {
+      dictCode: 'SYS_USER_SEX',
+      dictName: 'Sys user sex',
+      description: 'submarine system dict, Do not modify.',
+      status: 'available'
+    },
+    {
+      dictCode: 'SYS_USER_STATUS',
+      dictName: 'Sys user status',
+      description: 'submarine system dict, Do not modify.',
+      status: 'available'
+    }
+  ];
+  // modal
+  modalTitle: string = '';
+  // edit modal
+  dataDictModalVisible: boolean = false;
+  selectedDictCode: string = '';
+  selectedDictName: string = '';
+  selectedDescription: string = '';
+  selectedDictID: number = 0;
+  editDictCodeChanged: boolean = false;
+  lastDictCode: string = '';
+
+  // Configuration Modal
+  configModalTitle: string = 'Dictionary Item List';
+  configModalWidth: number = 1000;
+  configModalVisible: boolean = false;
+
+  // New Dict Item List
+  newDictItemCode: string = '';
 
   ngOnInit() {
+    this.dataDictForm = this.fb.group({
+      dictName: [''],
+      dictCode: ['']
+    });
+  }
+
+  // TODO(kevin85421)
+  queryDataDict() {}
+  // TODO(kevin85421)
+  onShowAddDataDictModal() {
+    this.modalTitle = "Add";
+    this.selectedDictCode = '';
+    this.dataDictModalVisible = true;
+  }
+
+  // Edit Data Dictionary Modal
+  onShowEditDataDictModal(data, dictItemIndex: number) {
+    // set selected dict variables
+    this.modalTitle = "Edit";
+    this.selectedDictCode = data.dictCode;
+    this.selectedDictName = data.dictName;
+    this.selectedDescription = data.description;
+    this.selectedDictID = dictItemIndex;
+    this.lastDictCode = data.dictCode;
+    this.editDictCodeChanged = false;
+    // show edit modal
+    this.dataDictModalVisible = true;
+  }
+
+  onHideDataDictModal() {
+    // reset selected dict variables
+    this.selectedDictName = '';
+    this.selectedDescription = '';
+    this.selectedDictID = 0;
+    this.modalTitle = "";
+    // hide edit modal
+    this.dataDictModalVisible = false;
+  }
+
+  updateDataDict(dataDictItem: {dictCode: string, dictName: string, 
description: string}) {
+    if (this.modalTitle === 'Edit') {
+      if (this.sysDictList[this.selectedDictID].dictCode !== 
dataDictItem.dictCode) {
+        this.editDictCodeChanged = true;
+        this.sysDictList[this.selectedDictID].dictCode = dataDictItem.dictCode;
+        this.selectedDictCode = dataDictItem.dictCode;
+      }
+      this.sysDictList[this.selectedDictID].dictName = dataDictItem.dictName;
+      this.sysDictList[this.selectedDictID].description = 
dataDictItem.description;
+    } else if (this.modalTitle === 'Add') {
+      this.newDictItemCode = dataDictItem.dictCode;
+      this.sysDictList = [
+        ...this.sysDictList,
+        {
+          dictCode: dataDictItem.dictCode,
+          dictName: dataDictItem.dictName,
+          description: dataDictItem.description,
+          status: 'available'
+        }
+      ]
+    }
+
+    this.onHideDataDictModal();
+  }
+
+  // Configuration
+  onHideConfigModal() {
+    this.selectedDictCode = '';
+    this.selectedDictName = '';
+    this.selectedDescription = '';
+    this.selectedDictID = 0;
+    this.configModalVisible = false;
+  }
+
+  onShowConfigModal(data, dictItemIndex: number) {
+    this.selectedDictCode = data.dictCode;
+    this.selectedDictName = data.dictName;
+    this.selectedDescription = data.description;
+    this.selectedDictID = dictItemIndex;
+    this.configModalVisible = true;
+  }
+
+  // delete dataDictItem
+  onDeleteDataDictItem(data) {
+    this.sysDictList = this.sysDictList.filter(d => d.dictCode !== 
data.dictCode);
   }
 }
diff --git 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/manager-routing.module.ts
 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/manager-routing.module.ts
index 52757dc..e8128cd 100644
--- 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/manager-routing.module.ts
+++ 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/manager-routing.module.ts
@@ -38,7 +38,7 @@ const routes: Routes = [
         component: UserComponent
       },
       {
-        path: 'data-dict',
+        path: 'dataDict',
         component: DataDictComponent
       }
     ]
diff --git 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/manager.component.ts
 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/manager.component.ts
index 15f42c5..440c3f3 100644
--- 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/manager.component.ts
+++ 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/manager.component.ts
@@ -25,7 +25,7 @@ import _ from 'lodash';
 interface HeaderInfo {
   title: string;
   description: string;
-  breadCrumb: string[];
+  breadCrumb: Array<{ title: string, routerLink?: string }>;
 }
 
 @Component({
@@ -39,7 +39,34 @@ export class ManagerComponent implements OnInit {
     user: {
       title: 'user',
       description: 'You can check the user, delete the user, lock and unlock 
the user, etc.',
-      breadCrumb: ['manager', 'user']
+      breadCrumb: [
+        {
+          title: 'Home',
+          routerLink: '/workbench/home'
+        },
+        {
+          title: 'manager'
+        },
+        {
+          title: 'user'
+        }
+      ]
+    },
+    dataDict: {
+      title: 'Data Dict',
+      description: 'System Dict Manager',
+      breadCrumb: [
+        {
+          title: 'Home',
+          routerLink: '/workbench/home'
+        },
+        {
+          title: 'manager'
+        },
+        {
+          title: 'Data Dict'
+        }
+      ]
     }
   };
   currentHeaderInfo: HeaderInfo;
diff --git 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/manager.module.ts
 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/manager.module.ts
index 94048f5..4e1224e 100644
--- 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/manager.module.ts
+++ 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/manager/manager.module.ts
@@ -23,6 +23,8 @@ import { FormsModule, ReactiveFormsModule } from 
'@angular/forms';
 import { ComponentsModule } from '@submarine/components/components.module';
 import { NgZorroAntdModule } from 'ng-zorro-antd';
 
+import { DataDictConfigModalComponent } from 
'./data-dict-config-modal/data-dict-config-modal.component';
+import { DataDictModalComponent } from 
'./data-dict-modal/data-dict-modal.component';
 import { DataDictComponent } from './data-dict/data-dict.component';
 import { ManagerRoutingModule } from './manager-routing.module';
 import { ManagerComponent } from './manager.component';
@@ -31,7 +33,7 @@ import { UserPasswordModalComponent } from 
'./user-password-modal/user-password-
 import { UserComponent } from './user/user.component';
 
 @NgModule({
-  declarations: [UserComponent, ManagerComponent, DataDictComponent, 
UserPasswordModalComponent, UserDrawerComponent],
+  declarations: [UserComponent, ManagerComponent, DataDictComponent, 
UserPasswordModalComponent, UserDrawerComponent, DataDictModalComponent, 
DataDictConfigModalComponent],
   imports: [CommonModule, ManagerRoutingModule, NgZorroAntdModule, 
ComponentsModule, FormsModule, ReactiveFormsModule]
 })
 export class ManagerModule {}
diff --git 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/workbench.component.ts
 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/workbench.component.ts
index cd82b6e..24bf571 100644
--- 
a/submarine-workbench/workbench-web-ng/src/app/pages/workbench/workbench.component.ts
+++ 
b/submarine-workbench/workbench-web-ng/src/app/pages/workbench/workbench.component.ts
@@ -84,7 +84,7 @@ export class WorkbenchComponent implements OnInit {
         },
         {
           title: 'Data dict',
-          routerLink: '/workbench/manager/data-dict'
+          routerLink: '/workbench/manager/dataDict'
         }
       ]
     }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to