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

kirs pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git


The following commit(s) were added to refs/heads/dev by this push:
     new 37ba1eb  Add E2E tests for some core features (#7025)
37ba1eb is described below

commit 37ba1eb5adb962f6f13825ee0375e82b4823f254
Author: kezhenxu94 <[email protected]>
AuthorDate: Tue Nov 30 13:07:55 2021 +0800

    Add E2E tests for some core features (#7025)
---
 .github/workflows/e2e.yml                          |  16 +-
 .../dolphinscheduler/e2e/cases/ProjectE2ETest.java |  68 +++
 .../dolphinscheduler/e2e/cases/TenantE2ETest.java  |  88 +++
 .../e2e/cases/WorkflowE2ETest.java                 | 200 +++++++
 .../e2e/cases/security/TenantE2ETest.java          |  98 ----
 .../dolphinscheduler/e2e/pages/LoginPage.java      |  25 +-
 .../e2e/pages/common/CodeEditor.java               |  45 ++
 .../e2e/pages/common/NavBarPage.java               |  62 +++
 .../e2e/pages/project/ProjectDetailPage.java       |  58 ++
 .../e2e/pages/project/ProjectPage.java             | 114 ++++
 .../project/workflow/WorkflowDefinitionTab.java    | 123 +++++
 .../e2e/pages/project/workflow/WorkflowForm.java   |  85 +++
 .../project/workflow/WorkflowInstanceTab.java      | 107 ++++
 .../pages/project/workflow/WorkflowRunDialog.java  |  46 ++
 .../pages/project/workflow/WorkflowSaveDialog.java | 115 ++++
 .../pages/project/workflow/task/ShellTaskForm.java |  42 ++
 .../project/workflow/task/SubWorkflowTaskForm.java |  31 ++
 .../pages/project/workflow/task/TaskNodeForm.java  |  93 ++++
 .../e2e/pages/security/SecurityPage.java           |  51 ++
 .../e2e/pages/{ => security}/TenantPage.java       |  55 +-
 .../docker/{tenant => basic}/docker-compose.yaml   |   1 +
 .../src/test/resources/dragAndDrop.js              |  55 ++
 .../e2e/core/DolphinSchedulerExtension.java        |  45 +-
 dolphinscheduler-e2e/pom.xml                       |  18 +-
 dolphinscheduler-ui/pom.xml                        |   2 +-
 .../conf/home/pages/dag/_source/canvas/taskbar.vue |   3 +-
 .../conf/home/pages/dag/_source/canvas/toolbar.vue |   1 +
 .../src/js/conf/home/pages/dag/_source/config.js   |  18 +-
 .../src/js/conf/home/pages/dag/_source/dag.vue     |   1 +
 .../home/pages/dag/_source/formModel/formModel.vue |   2 +
 .../formModel/tasks/_source/localParams.vue        |   2 +
 .../pages/dag/_source/udp/_source/selectTenant.vue |   2 +
 .../src/js/conf/home/pages/dag/_source/udp/udp.vue |   3 +-
 .../pages/definition/pages/list/_source/list.vue   |  14 +-
 .../pages/definition/pages/list/_source/start.vue  |   2 +-
 .../projects/pages/definition/pages/list/index.vue |   2 +-
 .../pages/instance/pages/list/_source/list.vue     |  12 +-
 .../projects/pages/list/_source/createProject.vue  |   3 +-
 .../pages/projects/pages/list/_source/list.vue     |   6 +-
 .../conf/home/pages/projects/pages/list/index.vue  |   2 +-
 .../pages/projects/pages/taskDefinition/index.vue  | 614 ++++++++++-----------
 .../src/js/module/components/nav/nav.vue           |   4 +-
 .../components/secondaryMenu/_source/menu.js       |   9 +-
 .../components/secondaryMenu/secondaryMenu.vue     |   4 +-
 44 files changed, 1861 insertions(+), 486 deletions(-)

diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 244b0e5..5ccf937 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -27,10 +27,6 @@ on:
     branches:
       - dev
 
-env:
-  TAG: ci
-  RECORDING_PATH: /tmp/recording
-
 name: E2E
 
 concurrency:
@@ -45,7 +41,13 @@ jobs:
       matrix:
         case:
           - name: Tenant
-            class: org.apache.dolphinscheduler.e2e.cases.security.TenantE2ETest
+            class: org.apache.dolphinscheduler.e2e.cases.TenantE2ETest
+          - name: Project
+            class: org.apache.dolphinscheduler.e2e.cases.ProjectE2ETest
+          - name: Workflow
+            class: org.apache.dolphinscheduler.e2e.cases.WorkflowE2ETest
+    env:
+      RECORDING_PATH: /tmp/recording-${{ matrix.case.name }}
     steps:
       - uses: actions/checkout@v2
         with:
@@ -62,13 +64,13 @@ jobs:
         run: TAG=ci sh ./docker/build/hooks/build
       - name: Run Test
         run: |
-          ./mvnw -f dolphinscheduler-e2e/pom.xml -am \
+          ./mvnw -B -f dolphinscheduler-e2e/pom.xml -am \
             -DfailIfNoTests=false \
             -Dtest=${{ matrix.case.class }} test
       - uses: actions/upload-artifact@v2
         if: always()
         name: Upload Recording
         with:
-          name: recording
+          name: recording-${{ matrix.case.name }}
           path: ${{ env.RECORDING_PATH }}
           retention-days: 1
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/ProjectE2ETest.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/ProjectE2ETest.java
new file mode 100644
index 0000000..9615075
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/ProjectE2ETest.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to 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. Apache Software Foundation (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.dolphinscheduler.e2e.cases;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+import org.apache.dolphinscheduler.e2e.core.DolphinScheduler;
+import org.apache.dolphinscheduler.e2e.pages.LoginPage;
+import org.apache.dolphinscheduler.e2e.pages.project.ProjectPage;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.remote.RemoteWebDriver;
+
+@DolphinScheduler(composeFiles = "docker/basic/docker-compose.yaml")
+class ProjectE2ETest {
+    private static final String project = "test-project-1";
+
+    private static RemoteWebDriver browser;
+
+    @BeforeAll
+    public static void setup() {
+        new LoginPage(browser)
+            .login("admin", "dolphinscheduler123")
+            .goToNav(ProjectPage.class);
+    }
+
+    @Test
+    @Order(1)
+    void testCreateProject() {
+        new ProjectPage(browser).create(project);
+    }
+
+    @Test
+    @Order(30)
+    void testDeleteProject() {
+        final var page = new ProjectPage(browser);
+        page.delete(project);
+
+        await().untilAsserted(() -> {
+            browser.navigate().refresh();
+            assertThat(
+                page.projectList()
+            ).noneMatch(
+                it -> it.getText().contains(project)
+            );
+        });
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/TenantE2ETest.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/TenantE2ETest.java
new file mode 100644
index 0000000..9de3edb
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/TenantE2ETest.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to 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. Apache Software Foundation (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.dolphinscheduler.e2e.cases;
+
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+import org.apache.dolphinscheduler.e2e.core.DolphinScheduler;
+import org.apache.dolphinscheduler.e2e.pages.LoginPage;
+import org.apache.dolphinscheduler.e2e.pages.security.SecurityPage;
+import org.apache.dolphinscheduler.e2e.pages.security.TenantPage;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.remote.RemoteWebDriver;
+
+@DolphinScheduler(composeFiles = "docker/basic/docker-compose.yaml")
+class TenantE2ETest {
+    private static final String tenant = System.getProperty("user.name");
+
+    private static RemoteWebDriver browser;
+
+    @BeforeAll
+    public static void setup() {
+        new LoginPage(browser)
+            .login("admin", "dolphinscheduler123")
+            .goToNav(SecurityPage.class)
+            .goToTab(TenantPage.class)
+        ;
+    }
+
+    @Test
+    @Order(10)
+    void testCreateTenant() {
+        new TenantPage(browser).create(tenant);
+    }
+
+    @Test
+    @Order(20)
+    void testCreateDuplicateTenant() {
+        final var page = new TenantPage(browser);
+
+        page.create(tenant);
+
+        await().untilAsserted(() ->
+            assertThat(browser.findElement(By.tagName("body")).getText())
+                .contains("already exists")
+        );
+
+        page.createTenantForm().buttonCancel().click();
+    }
+
+    @Test
+    @Order(30)
+    void testDeleteTenant() {
+        final var page = new TenantPage(browser);
+        page.delete(tenant);
+
+        await().untilAsserted(() -> {
+            browser.navigate().refresh();
+            assertThat(
+                page.tenantList()
+            ).noneMatch(
+                it -> it.getText().contains(tenant)
+            );
+        });
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/WorkflowE2ETest.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/WorkflowE2ETest.java
new file mode 100644
index 0000000..cc51932
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/WorkflowE2ETest.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to 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. Apache Software Foundation (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.dolphinscheduler.e2e.cases;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+import org.apache.dolphinscheduler.e2e.core.DolphinScheduler;
+import org.apache.dolphinscheduler.e2e.pages.LoginPage;
+import org.apache.dolphinscheduler.e2e.pages.common.NavBarPage;
+import org.apache.dolphinscheduler.e2e.pages.project.ProjectPage;
+import 
org.apache.dolphinscheduler.e2e.pages.project.workflow.WorkflowDefinitionTab;
+import 
org.apache.dolphinscheduler.e2e.pages.project.workflow.WorkflowForm.TaskType;
+import 
org.apache.dolphinscheduler.e2e.pages.project.workflow.WorkflowInstanceTab;
+import 
org.apache.dolphinscheduler.e2e.pages.project.workflow.WorkflowInstanceTab.Row;
+import 
org.apache.dolphinscheduler.e2e.pages.project.workflow.task.ShellTaskForm;
+import 
org.apache.dolphinscheduler.e2e.pages.project.workflow.task.SubWorkflowTaskForm;
+import org.apache.dolphinscheduler.e2e.pages.security.SecurityPage;
+import org.apache.dolphinscheduler.e2e.pages.security.TenantPage;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.remote.RemoteWebDriver;
+
+@DolphinScheduler(composeFiles = "docker/basic/docker-compose.yaml")
+class WorkflowE2ETest {
+    private static final String project = "test-workflow-1";
+    private static final String tenant = System.getProperty("user.name");
+
+    private static RemoteWebDriver browser;
+
+    @BeforeAll
+    public static void setup() {
+        new LoginPage(browser)
+            .login("admin", "dolphinscheduler123")
+            .goToNav(SecurityPage.class)
+            .goToTab(TenantPage.class)
+            .create(tenant)
+            .goToNav(ProjectPage.class)
+            .create(project)
+        ;
+    }
+
+    @AfterAll
+    public static void cleanup() {
+        new NavBarPage(browser)
+            .goToNav(ProjectPage.class)
+            .goTo(project)
+            .goToTab(WorkflowDefinitionTab.class)
+            .cancelPublishAll()
+            .deleteAll()
+        ;
+        new NavBarPage(browser)
+            .goToNav(ProjectPage.class)
+            .delete(project)
+            .goToNav(SecurityPage.class)
+            .goToTab(TenantPage.class)
+            .delete(tenant)
+        ;
+    }
+
+    @Test
+    @Order(1)
+    void testCreateWorkflow() {
+        final var workflow = "test-workflow-1";
+
+        final var workflowDefinitionPage =
+            new ProjectPage(browser)
+                .goTo(project)
+                .goToTab(WorkflowDefinitionTab.class);
+
+        workflowDefinitionPage
+            .createWorkflow()
+
+            .<ShellTaskForm> addTask(TaskType.SHELL)
+            .script("echo ${today}\necho ${global_param}\n")
+            .name("test-1")
+            .addParam("today", "${system.datetime}")
+            .submit()
+
+            .submit()
+            .name(workflow)
+            .tenant(tenant)
+            .addGlobalParam("global_param", "hello world")
+            .submit()
+        ;
+
+        await().untilAsserted(() -> assertThat(
+            workflowDefinitionPage.workflowList()
+        ).anyMatch(it -> it.getText().contains(workflow)));
+
+        workflowDefinitionPage.publish(workflow);
+    }
+
+    @Test
+    @Order(10)
+    void testCreateSubWorkflow() {
+        final var workflow = "test-sub-workflow-1";
+
+        final var workflowDefinitionPage =
+            new ProjectPage(browser)
+                .goToNav(ProjectPage.class)
+                .goTo(project)
+                .goToTab(WorkflowDefinitionTab.class);
+
+        workflowDefinitionPage
+            .createWorkflow()
+
+            .<SubWorkflowTaskForm> addTask(TaskType.SUB_PROCESS)
+            .submit()
+
+            .submit()
+            .name(workflow)
+            .tenant(tenant)
+            .addGlobalParam("global_param", "hello world")
+            .submit()
+        ;
+
+        await().untilAsserted(() -> assertThat(
+            workflowDefinitionPage.workflowList()
+        ).anyMatch(it -> it.getText().contains(workflow)));
+
+        workflowDefinitionPage.publish(workflow);
+    }
+
+    @Test
+    @Order(30)
+    void testRunWorkflow() {
+        final var workflow = "test-workflow-1";
+
+        final var projectPage =
+            new ProjectPage(browser)
+                .goToNav(ProjectPage.class)
+                .goTo(project);
+
+        projectPage
+            .goToTab(WorkflowInstanceTab.class)
+            .deleteAll();
+
+        projectPage
+            .goToTab(WorkflowDefinitionTab.class)
+            .run(workflow)
+            .submit();
+
+        await().untilAsserted(() -> {
+            browser.navigate().refresh();
+
+            final Row row = projectPage
+                .goToTab(WorkflowInstanceTab.class)
+                .instances()
+                .iterator()
+                .next();
+
+            assertThat(row.isSuccess()).isTrue();
+            assertThat(row.executionTime()).isEqualTo(1);
+        });
+
+        // Test rerun
+        projectPage
+            .goToTab(WorkflowInstanceTab.class)
+            .instances()
+            .stream()
+            .filter(it -> it.rerunButton().isDisplayed())
+            .iterator()
+            .next()
+            .rerun();
+
+        await().untilAsserted(() -> {
+            browser.navigate().refresh();
+
+            final Row row = projectPage
+                .goToTab(WorkflowInstanceTab.class)
+                .instances()
+                .iterator()
+                .next();
+
+            assertThat(row.isSuccess()).isTrue();
+            assertThat(row.executionTime()).isEqualTo(2);
+        });
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/security/TenantE2ETest.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/security/TenantE2ETest.java
deleted file mode 100644
index ab3159a..0000000
--- 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/security/TenantE2ETest.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Licensed to 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. Apache Software Foundation (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.dolphinscheduler.e2e.cases.security;
-
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.awaitility.Awaitility.await;
-
-import org.apache.dolphinscheduler.e2e.core.DolphinScheduler;
-import org.apache.dolphinscheduler.e2e.pages.LoginPage;
-import org.apache.dolphinscheduler.e2e.pages.TenantPage;
-
-import org.junit.jupiter.api.Order;
-import org.junit.jupiter.api.Test;
-import org.openqa.selenium.By;
-import org.openqa.selenium.WebElement;
-import org.openqa.selenium.remote.RemoteWebDriver;
-
-@DolphinScheduler(composeFiles = "docker/tenant/docker-compose.yaml")
-class TenantE2ETest {
-    private RemoteWebDriver browser;
-
-    @Test
-    @Order(1)
-    void testLogin() {
-        final LoginPage page = new LoginPage(browser);
-        page.inputUsername().sendKeys("admin");
-        page.inputPassword().sendKeys("dolphinscheduler123");
-        page.buttonLogin().click();
-    }
-
-    @Test
-    @Order(10)
-    void testCreateTenant() {
-        final TenantPage page = new TenantPage(browser);
-        final String tenant = System.getProperty("user.name");
-
-        page.buttonCreateTenant().click();
-        page.createTenantForm().inputTenantCode().sendKeys(tenant);
-        page.createTenantForm().inputDescription().sendKeys("Test");
-        page.createTenantForm().buttonSubmit().click();
-
-        await().untilAsserted(() -> assertThat(page.tenantList())
-                .as("Tenant list should contain newly-created tenant")
-                .extracting(WebElement::getText)
-                .anyMatch(it -> it.contains(tenant)));
-    }
-
-    @Test
-    @Order(20)
-    void testCreateDuplicateTenant() {
-        final String tenant = System.getProperty("user.name");
-        final TenantPage page = new TenantPage(browser);
-        page.buttonCreateTenant().click();
-        page.createTenantForm().inputTenantCode().sendKeys(tenant);
-        page.createTenantForm().inputDescription().sendKeys("Test");
-        page.createTenantForm().buttonSubmit().click();
-
-        await().untilAsserted(() -> 
assertThat(browser.findElementByTagName("body")
-                                                      
.getText().contains("already exists"))
-                .as("Should fail when creating a duplicate tenant")
-                .isTrue());
-
-        page.createTenantForm().buttonCancel().click();
-    }
-
-    @Test
-    @Order(30)
-    void testDeleteTenant() {
-        final String tenant = System.getProperty("user.name");
-        final TenantPage page = new TenantPage(browser);
-
-        page.tenantList()
-            .stream()
-            .filter(it -> it.getText().contains(tenant))
-            .findFirst()
-            .ifPresent(it -> it.findElement(By.className("delete")).click());
-
-        page.buttonConfirm().click();
-    }
-}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/LoginPage.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/LoginPage.java
index a772517..f2431d6 100644
--- 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/LoginPage.java
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/LoginPage.java
@@ -19,17 +19,20 @@
 
 package org.apache.dolphinscheduler.e2e.pages;
 
+import org.apache.dolphinscheduler.e2e.pages.common.NavBarPage;
+import org.apache.dolphinscheduler.e2e.pages.security.TenantPage;
+
 import org.openqa.selenium.WebElement;
 import org.openqa.selenium.remote.RemoteWebDriver;
 import org.openqa.selenium.support.FindBy;
-import org.openqa.selenium.support.PageFactory;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
 
 import lombok.Getter;
+import lombok.SneakyThrows;
 
 @Getter
-public final class LoginPage {
-    private final RemoteWebDriver driver;
-
+public final class LoginPage extends NavBarPage {
     @FindBy(id = "input-username")
     private WebElement inputUsername;
 
@@ -40,8 +43,18 @@ public final class LoginPage {
     private WebElement buttonLogin;
 
     public LoginPage(RemoteWebDriver driver) {
-        this.driver = driver;
+        super(driver);
+    }
+
+    @SneakyThrows
+    public TenantPage login(String username, String password) {
+        inputUsername().sendKeys(username);
+        inputPassword().sendKeys(password);
+        buttonLogin().click();
+
+        new WebDriverWait(driver(), 10)
+            .until(ExpectedConditions.urlContains("/#/security"));
 
-        PageFactory.initElements(driver, this);
+        return new TenantPage(driver);
     }
 }
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/common/CodeEditor.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/common/CodeEditor.java
new file mode 100644
index 0000000..5a8645d
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/common/CodeEditor.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.dolphinscheduler.e2e.pages.common;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.PageFactory;
+
+import lombok.Getter;
+
+@Getter
+public final class CodeEditor {
+    @FindBy(className = "CodeMirror")
+    private WebElement editor;
+
+    public CodeEditor(WebDriver driver) {
+        PageFactory.initElements(driver, this);
+    }
+
+    public CodeEditor content(String content) {
+        editor.findElement(By.className("CodeMirror-line")).click();
+        editor.findElement(By.tagName("textarea")).sendKeys(content);
+
+        return this;
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/common/NavBarPage.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/common/NavBarPage.java
new file mode 100644
index 0000000..546a714
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/common/NavBarPage.java
@@ -0,0 +1,62 @@
+/*
+ * 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.dolphinscheduler.e2e.pages.common;
+
+import org.apache.dolphinscheduler.e2e.pages.project.ProjectPage;
+import org.apache.dolphinscheduler.e2e.pages.security.SecurityPage;
+
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.PageFactory;
+
+import lombok.Getter;
+
+@Getter
+public class NavBarPage {
+    protected final RemoteWebDriver driver;
+
+    @FindBy(id = "project-tab")
+    private WebElement projectTab;
+    @FindBy(id = "security-tab")
+    private WebElement securityTab;
+
+    public NavBarPage(RemoteWebDriver driver) {
+        this.driver = driver;
+
+        PageFactory.initElements(driver, this);
+    }
+
+    public <T extends NavBarItem> T goToNav(Class<T> nav) {
+        if (nav == ProjectPage.class) {
+            projectTab().click();
+            return nav.cast(new ProjectPage(driver));
+        }
+        if (nav == SecurityPage.class) {
+            securityTab().click();
+            return nav.cast(new SecurityPage(driver));
+        }
+
+        throw new UnsupportedOperationException("Unknown nav bar");
+    }
+
+    public interface NavBarItem {
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/ProjectDetailPage.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/ProjectDetailPage.java
new file mode 100644
index 0000000..5dd7051
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/ProjectDetailPage.java
@@ -0,0 +1,58 @@
+/*
+ * 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.dolphinscheduler.e2e.pages.project;
+
+import org.apache.dolphinscheduler.e2e.pages.common.NavBarPage;
+import 
org.apache.dolphinscheduler.e2e.pages.project.workflow.WorkflowDefinitionTab;
+import 
org.apache.dolphinscheduler.e2e.pages.project.workflow.WorkflowInstanceTab;
+
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.support.FindBy;
+
+import lombok.Getter;
+
+@Getter
+public final class ProjectDetailPage extends NavBarPage {
+    @FindBy(className = "process-definition")
+    private WebElement menuProcessDefinition;
+    @FindBy(className = "process-instance")
+    private WebElement menuProcessInstances;
+
+    public ProjectDetailPage(RemoteWebDriver driver) {
+        super(driver);
+    }
+
+    public <T extends Tab> T goToTab(Class<T> tab) {
+        if (tab == WorkflowDefinitionTab.class) {
+            menuProcessDefinition().click();
+            return tab.cast(new WorkflowDefinitionTab(driver));
+        }
+        if (tab == WorkflowInstanceTab.class) {
+            menuProcessInstances().click();
+            return tab.cast(new WorkflowInstanceTab(driver));
+        }
+
+        throw new UnsupportedOperationException("Unknown tab: " + 
tab.getName());
+    }
+
+    public interface Tab {
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/ProjectPage.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/ProjectPage.java
new file mode 100644
index 0000000..86cdb93
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/ProjectPage.java
@@ -0,0 +1,114 @@
+/*
+ * 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.dolphinscheduler.e2e.pages.project;
+
+import org.apache.dolphinscheduler.e2e.pages.common.NavBarPage;
+import org.apache.dolphinscheduler.e2e.pages.common.NavBarPage.NavBarItem;
+
+import java.util.List;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.FindBys;
+import org.openqa.selenium.support.PageFactory;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+import lombok.Getter;
+
+@Getter
+public final class ProjectPage extends NavBarPage implements NavBarItem {
+    @FindBy(id = "button-create-project")
+    private WebElement buttonCreateProject;
+
+    @FindBy(className = "rows-project")
+    private List<WebElement> projectList;
+
+    @FindBys({
+        @FindBy(className = "el-popconfirm"),
+        @FindBy(className = "el-button--primary"),
+    })
+    private List<WebElement> buttonConfirm;
+
+    private final CreateProjectForm createProjectForm;
+
+    public ProjectPage(RemoteWebDriver driver) {
+        super(driver);
+
+        this.createProjectForm = new CreateProjectForm();
+
+        PageFactory.initElements(driver, this);
+    }
+
+    public ProjectPage create(String project) {
+        buttonCreateProject().click();
+        createProjectForm().inputProjectName().sendKeys(project);
+        createProjectForm().buttonSubmit().click();
+
+        new WebDriverWait(driver(), 10)
+            
.until(ExpectedConditions.textToBePresentInElementLocated(By.className("project-name"),
 project));
+
+        return this;
+    }
+
+    public ProjectPage delete(String project) {
+        projectList()
+            .stream()
+            .filter(it -> it.getText().contains(project))
+            .findFirst()
+            .orElseThrow(() -> new RuntimeException("Cannot find project: " + 
project))
+            .findElement(By.className("delete")).click();
+
+        buttonConfirm()
+            .stream()
+            .filter(WebElement::isDisplayed)
+            .findFirst()
+            .orElseThrow(() -> new RuntimeException("No confirm button is 
displayed"))
+            .click();
+
+        return this;
+    }
+
+    public ProjectDetailPage goTo(String project) {
+        projectList().stream()
+                     .filter(it -> it.getText().contains(project))
+                     .map(it -> it.findElement(By.className("project-name")))
+                     .findFirst()
+                     .orElseThrow(() -> new RuntimeException("Cannot click the 
project item"))
+                     .click();
+
+        return new ProjectDetailPage(driver);
+    }
+
+    @Getter
+    public class CreateProjectForm {
+        CreateProjectForm() {
+            PageFactory.initElements(driver, this);
+        }
+
+        @FindBy(id = "input-project-name")
+        private WebElement inputProjectName;
+
+        @FindBy(id = "button-submit")
+        private WebElement buttonSubmit;
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowDefinitionTab.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowDefinitionTab.java
new file mode 100644
index 0000000..56f0eb3
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowDefinitionTab.java
@@ -0,0 +1,123 @@
+/*
+ * 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.dolphinscheduler.e2e.pages.project.workflow;
+
+import org.apache.dolphinscheduler.e2e.pages.common.NavBarPage;
+import org.apache.dolphinscheduler.e2e.pages.project.ProjectDetailPage;
+
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.FindBys;
+
+import lombok.Getter;
+
+@Getter
+public final class WorkflowDefinitionTab extends NavBarPage implements 
ProjectDetailPage.Tab {
+    @FindBy(id = "button-create-process")
+    private WebElement buttonCreateProcess;
+    @FindBy(className = "select-all")
+    private WebElement checkBoxSelectAll;
+    @FindBy(className = "button-delete-all")
+    private WebElement buttonDeleteAll;
+    @FindBys({
+        @FindBy(className = "el-popconfirm"),
+        @FindBy(className = "el-button--primary"),
+    })
+    private List<WebElement> buttonConfirm;
+    @FindBy(className = "rows-workflow-definitions")
+    private List<WebElement> workflowList;
+
+    public WorkflowDefinitionTab(RemoteWebDriver driver) {
+        super(driver);
+    }
+
+    public WorkflowForm createWorkflow() {
+        buttonCreateProcess().click();
+
+        return new WorkflowForm(driver);
+    }
+
+    public WorkflowDefinitionTab publish(String workflow) {
+        workflowList()
+            .stream()
+            .filter(it -> 
it.findElement(By.className("name")).getAttribute("innerHTML").equals(workflow))
+            .flatMap(it -> 
it.findElements(By.className("button-publish")).stream())
+            .filter(WebElement::isDisplayed)
+            .findFirst()
+            .orElseThrow(() -> new RuntimeException("Cannot find publish 
button in workflow definition"))
+            .click();
+
+        return this;
+    }
+
+    public WorkflowRunDialog run(String workflow) {
+        workflowList()
+            .stream()
+            .filter(it -> 
it.findElement(By.className("name")).getAttribute("innerHTML").equals(workflow))
+            .flatMap(it -> 
it.findElements(By.className("button-run")).stream())
+            .filter(WebElement::isDisplayed)
+            .findFirst()
+            .orElseThrow(() -> new RuntimeException("Cannot find run button in 
workflow definition"))
+            .click();
+
+        return new WorkflowRunDialog(this);
+    }
+
+    public WorkflowDefinitionTab cancelPublishAll() {
+        final Supplier<List<WebElement>> cancelButtons = () ->
+            workflowList()
+                .stream()
+                .flatMap(it -> 
it.findElements(By.className("button-cancel-publish")).stream())
+                .filter(WebElement::isDisplayed)
+                .collect(Collectors.toList());
+
+        for (var buttons = cancelButtons.get();
+             !buttons.isEmpty();
+             buttons = cancelButtons.get()) {
+            buttons.forEach(WebElement::click);
+            driver().navigate().refresh();
+        }
+
+        return this;
+    }
+
+    public WorkflowDefinitionTab deleteAll() {
+        if (workflowList().isEmpty()) {
+            return this;
+        }
+
+        checkBoxSelectAll().click();
+        buttonDeleteAll().click();
+        buttonConfirm()
+            .stream()
+            .filter(WebElement::isDisplayed)
+            .findFirst()
+            .orElseThrow(() -> new RuntimeException("No confirm button is 
displayed"))
+            .click();
+
+        return this;
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowForm.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowForm.java
new file mode 100644
index 0000000..95c21b1
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowForm.java
@@ -0,0 +1,85 @@
+/*
+ * 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.dolphinscheduler.e2e.pages.project.workflow;
+
+import 
org.apache.dolphinscheduler.e2e.pages.project.workflow.task.ShellTaskForm;
+import 
org.apache.dolphinscheduler.e2e.pages.project.workflow.task.SubWorkflowTaskForm;
+
+import java.nio.charset.StandardCharsets;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.PageFactory;
+
+import com.google.common.io.Resources;
+
+import lombok.Getter;
+import lombok.SneakyThrows;
+
+@SuppressWarnings("UnstableApiUsage")
+@Getter
+public final class WorkflowForm {
+    private final WebDriver driver;
+    private final WorkflowSaveDialog saveForm;
+
+    @FindBy(id = "button-save")
+    private WebElement buttonSave;
+
+    public WorkflowForm(WebDriver driver) {
+        this.driver = driver;
+        this.saveForm = new WorkflowSaveDialog(this);
+
+        PageFactory.initElements(driver, this);
+    }
+
+    @SneakyThrows
+    @SuppressWarnings("unchecked")
+    public <T> T addTask(TaskType type) {
+        final var task = driver.findElement(By.className("task-item-" + 
type.name()));
+        final var canvas = driver.findElement(By.className("dag-container"));
+
+        final var js = (JavascriptExecutor) driver;
+        final var dragAndDrop = String.join("\n",
+            Resources.readLines(Resources.getResource("dragAndDrop.js"), 
StandardCharsets.UTF_8));
+        js.executeScript(dragAndDrop, task, canvas);
+
+        switch (type) {
+            case SHELL:
+                return (T) new ShellTaskForm(this);
+            case SUB_PROCESS:
+                return (T) new SubWorkflowTaskForm(this);
+        }
+        throw new UnsupportedOperationException("Unknown task type");
+    }
+
+    public WorkflowSaveDialog submit() {
+        buttonSave().click();
+
+        return new WorkflowSaveDialog(this);
+    }
+
+    public enum TaskType {
+        SHELL,
+        SUB_PROCESS,
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowInstanceTab.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowInstanceTab.java
new file mode 100644
index 0000000..97f29b4
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowInstanceTab.java
@@ -0,0 +1,107 @@
+/*
+ * 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.dolphinscheduler.e2e.pages.project.workflow;
+
+import org.apache.dolphinscheduler.e2e.pages.common.NavBarPage;
+import org.apache.dolphinscheduler.e2e.pages.project.ProjectDetailPage;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.FindBys;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+public final class WorkflowInstanceTab extends NavBarPage implements 
ProjectDetailPage.Tab {
+    @FindBy(className = "rows-workflow-instances")
+    private List<WebElement> instanceList;
+    @FindBy(className = "select-all")
+    private WebElement checkBoxSelectAll;
+    @FindBy(className = "button-delete-all")
+    private WebElement buttonDeleteAll;
+    @FindBys({
+        @FindBy(className = "el-popconfirm"),
+        @FindBy(className = "el-button--primary"),
+    })
+    private List<WebElement> buttonConfirm;
+
+    public WorkflowInstanceTab(RemoteWebDriver driver) {
+        super(driver);
+    }
+
+    public List<Row> instances() {
+        return instanceList()
+            .stream()
+            .filter(WebElement::isDisplayed)
+            .map(Row::new)
+            .collect(Collectors.toList());
+    }
+
+    public WorkflowInstanceTab deleteAll() {
+        if (instanceList().isEmpty()) {
+            return this;
+        }
+
+        checkBoxSelectAll().click();
+        buttonDeleteAll().click();
+        buttonConfirm()
+            .stream()
+            .filter(WebElement::isDisplayed)
+            .findFirst()
+            .orElseThrow(() -> new RuntimeException("No confirm button is 
displayed"))
+            .click();
+
+        return this;
+    }
+
+    @RequiredArgsConstructor
+    public static class Row {
+        private final WebElement row;
+
+        public WebElement rerunButton() {
+            return row.findElement(By.className("button-rerun"));
+        }
+
+        public boolean isSuccess() {
+            return !row.findElements(By.className("success")).isEmpty();
+        }
+
+        public int executionTime() {
+            return 
Integer.parseInt(row.findElement(By.className("execution-time")).getText());
+        }
+
+        public Row rerun() {
+            row.findElements(By.className("button-rerun"))
+               .stream()
+               .filter(WebElement::isDisplayed)
+               .findFirst()
+               .orElseThrow(() -> new RuntimeException("Cannot find rerun 
button"))
+               .click();
+
+            return this;
+        }
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowRunDialog.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowRunDialog.java
new file mode 100644
index 0000000..9801378
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowRunDialog.java
@@ -0,0 +1,46 @@
+/*
+ * 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.dolphinscheduler.e2e.pages.project.workflow;
+
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.PageFactory;
+
+import lombok.Getter;
+
+@Getter
+public final class WorkflowRunDialog {
+    private final WorkflowDefinitionTab parent;
+
+    @FindBy(id = "button-submit")
+    private WebElement buttonSubmit;
+
+    public WorkflowRunDialog(WorkflowDefinitionTab parent) {
+        this.parent = parent;
+
+        PageFactory.initElements(parent().driver(), this);
+    }
+
+    public WorkflowDefinitionTab submit() {
+        buttonSubmit().click();
+
+        return parent();
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowSaveDialog.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowSaveDialog.java
new file mode 100644
index 0000000..ce08926
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowSaveDialog.java
@@ -0,0 +1,115 @@
+/*
+ * 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.dolphinscheduler.e2e.pages.project.workflow;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.FindBys;
+import org.openqa.selenium.support.PageFactory;
+import org.openqa.selenium.support.pagefactory.ByChained;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+import lombok.Getter;
+
+@Getter
+public final class WorkflowSaveDialog {
+    private final WebDriver driver;
+    private final WorkflowForm parent;
+
+    @FindBy(id = "input-name")
+    private WebElement inputName;
+    @FindBy(id = "button-submit")
+    private WebElement buttonSubmit;
+    @FindBys({
+        @FindBy(className = "input-param-key"),
+        @FindBy(tagName = "input"),
+    })
+    private List<WebElement> inputParamKey;
+    @FindBys({
+        @FindBy(className = "input-param-val"),
+        @FindBy(tagName = "input"),
+    })
+    private List<WebElement> inputParamVal;
+    @FindBy(id = "select-tenant")
+    private WebElement selectTenant;
+
+    public WorkflowSaveDialog(WorkflowForm parent) {
+        this.parent = parent;
+        this.driver = parent.driver();
+
+        PageFactory.initElements(driver, this);
+    }
+
+    public WorkflowSaveDialog name(String name) {
+        inputName().sendKeys(name);
+
+        return this;
+    }
+
+    public WorkflowSaveDialog tenant(String tenant) {
+        selectTenant().click();
+
+        final var optionsLocator = By.className("option-tenants");
+
+        new WebDriverWait(driver, 10)
+            
.until(ExpectedConditions.visibilityOfElementLocated(optionsLocator));
+
+        driver().findElements(optionsLocator)
+                .stream()
+                .filter(it -> it.getText().contains(tenant))
+                .findFirst()
+                .orElseThrow(() -> new RuntimeException("No such tenant: " + 
tenant))
+                .click()
+        ;
+
+        return this;
+    }
+
+    public WorkflowSaveDialog addGlobalParam(String key, String val) {
+        assert inputParamKey().size() == inputParamVal().size();
+
+        final var len = inputParamKey().size();
+
+        final var driver = parent().driver();
+        Stream.concat(
+                  driver.findElements(new 
ByChained(By.className("user-def-params-model"), By.className("add"))).stream(),
+                  driver.findElements(new 
ByChained(By.className("user-def-params-model"), 
By.className("add-dp"))).stream())
+              .findFirst()
+              .orElseThrow(() -> new RuntimeException("Cannot find button to 
add param"))
+              .click();
+
+        inputParamKey().get(len).sendKeys(key);
+        inputParamVal().get(len).sendKeys(val);
+
+        return this;
+    }
+
+    public WorkflowForm submit() {
+        buttonSubmit().click();
+
+        return parent;
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/task/ShellTaskForm.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/task/ShellTaskForm.java
new file mode 100644
index 0000000..70b5f5d
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/task/ShellTaskForm.java
@@ -0,0 +1,42 @@
+/*
+ * 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.dolphinscheduler.e2e.pages.project.workflow.task;
+
+import org.apache.dolphinscheduler.e2e.pages.common.CodeEditor;
+import org.apache.dolphinscheduler.e2e.pages.project.workflow.WorkflowForm;
+
+import lombok.Getter;
+
+@Getter
+public final class ShellTaskForm extends TaskNodeForm {
+    private final CodeEditor codeEditor;
+
+    public ShellTaskForm(WorkflowForm parent) {
+        super(parent);
+
+        this.codeEditor = new CodeEditor(parent.driver());
+    }
+
+    public ShellTaskForm script(String script) {
+        codeEditor().content(script);
+
+        return this;
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/task/SubWorkflowTaskForm.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/task/SubWorkflowTaskForm.java
new file mode 100644
index 0000000..a94102f
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/task/SubWorkflowTaskForm.java
@@ -0,0 +1,31 @@
+/*
+ * 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.dolphinscheduler.e2e.pages.project.workflow.task;
+
+import org.apache.dolphinscheduler.e2e.pages.project.workflow.WorkflowForm;
+
+import lombok.Getter;
+
+@Getter
+public final class SubWorkflowTaskForm extends TaskNodeForm {
+    public SubWorkflowTaskForm(WorkflowForm parent) {
+        super(parent);
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/task/TaskNodeForm.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/task/TaskNodeForm.java
new file mode 100644
index 0000000..8689a21
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/task/TaskNodeForm.java
@@ -0,0 +1,93 @@
+/*
+ * 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.dolphinscheduler.e2e.pages.project.workflow.task;
+
+import org.apache.dolphinscheduler.e2e.pages.project.workflow.WorkflowForm;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.FindBys;
+import org.openqa.selenium.support.PageFactory;
+import org.openqa.selenium.support.pagefactory.ByChained;
+
+import lombok.Getter;
+
+@Getter
+public abstract class TaskNodeForm {
+    @FindBy(id = "input-node-name")
+    private WebElement inputNodeName;
+    @FindBy(id = "button-submit")
+    private WebElement buttonSubmit;
+    @FindBys({
+        @FindBy(className = "input-param-key"),
+        @FindBy(tagName = "input"),
+    })
+    private List<WebElement> inputParamKey;
+    @FindBys({
+        @FindBy(className = "input-param-val"),
+        @FindBy(tagName = "input"),
+    })
+    private List<WebElement> inputParamVal;
+
+    private final WorkflowForm parent;
+
+    TaskNodeForm(WorkflowForm parent) {
+        this.parent = parent;
+
+        final var driver = parent.driver();
+
+        PageFactory.initElements(driver, this);
+    }
+
+    public TaskNodeForm name(String name) {
+        inputNodeName().sendKeys(name);
+
+        return this;
+    }
+
+    public TaskNodeForm addParam(String key, String val) {
+        assert inputParamKey().size() == inputParamVal().size();
+
+        final var len = inputParamKey().size();
+
+        final var driver = parent().driver();
+        Stream.concat(
+                  driver.findElements(new 
ByChained(By.className("user-def-params-model"), By.className("add"))).stream(),
+                  driver.findElements(new 
ByChained(By.className("user-def-params-model"), 
By.className("add-dp"))).stream())
+              .findFirst()
+              .orElseThrow(() -> new RuntimeException("Cannot find button to 
add param"))
+              .click();
+
+        inputParamKey().get(len).sendKeys(key);
+        inputParamVal().get(len).sendKeys(val);
+
+        return this;
+    }
+
+    public WorkflowForm submit() {
+        buttonSubmit.click();
+
+        return parent();
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/security/SecurityPage.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/security/SecurityPage.java
new file mode 100644
index 0000000..8e4c7b9
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/security/SecurityPage.java
@@ -0,0 +1,51 @@
+/*
+ * 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.dolphinscheduler.e2e.pages.security;
+
+import org.apache.dolphinscheduler.e2e.pages.common.NavBarPage;
+import org.apache.dolphinscheduler.e2e.pages.common.NavBarPage.NavBarItem;
+
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.support.FindBy;
+
+import lombok.Getter;
+
+@Getter
+public class SecurityPage extends NavBarPage implements NavBarItem {
+    @FindBy(className = "tenant-manage")
+    private WebElement menuTenantManage;
+
+    public SecurityPage(RemoteWebDriver driver) {
+        super(driver);
+    }
+
+    public <T extends SecurityPage.Tab> T goToTab(Class<T> tab) {
+        if (tab == TenantPage.class) {
+            menuTenantManage().click();
+            return tab.cast(new TenantPage(driver));
+        }
+
+        throw new UnsupportedOperationException("Unknown tab: " + 
tab.getName());
+    }
+
+    public interface Tab {
+    }
+}
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/TenantPage.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/security/TenantPage.java
similarity index 55%
rename from 
dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/TenantPage.java
rename to 
dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/security/TenantPage.java
index da97349..6e9c548 100644
--- 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/TenantPage.java
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/security/TenantPage.java
@@ -17,12 +17,18 @@
  * under the License.
  */
 
-package org.apache.dolphinscheduler.e2e.pages;
+package org.apache.dolphinscheduler.e2e.pages.security;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+import org.apache.dolphinscheduler.e2e.pages.common.NavBarPage;
 
 import java.util.List;
 
-import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.By;
 import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.RemoteWebDriver;
 import org.openqa.selenium.support.FindBy;
 import org.openqa.selenium.support.FindBys;
 import org.openqa.selenium.support.PageFactory;
@@ -30,9 +36,7 @@ import org.openqa.selenium.support.PageFactory;
 import lombok.Getter;
 
 @Getter
-public final class TenantPage {
-    private final WebDriver driver;
-
+public final class TenantPage extends NavBarPage implements SecurityPage.Tab {
     @FindBy(id = "button-create-tenant")
     private WebElement buttonCreateTenant;
 
@@ -40,18 +44,47 @@ public final class TenantPage {
     private List<WebElement> tenantList;
 
     @FindBys({
-            @FindBy(className = "el-popconfirm"),
-            @FindBy(className = "el-button--primary"),
+        @FindBy(className = "el-popconfirm"),
+        @FindBy(className = "el-button--primary"),
     })
     private WebElement buttonConfirm;
 
     private final CreateTenantForm createTenantForm;
 
-    public TenantPage(WebDriver driver) {
-        this.driver = driver;
-        this.createTenantForm = new CreateTenantForm();
+    public TenantPage(RemoteWebDriver driver) {
+        super(driver);
+
+        createTenantForm = new CreateTenantForm();
+    }
+
+    public TenantPage create(String tenant) {
+        return create(tenant, "");
+    }
+
+    public TenantPage create(String tenant, String description) {
+        buttonCreateTenant().click();
+        createTenantForm().inputTenantCode().sendKeys(tenant);
+        createTenantForm().inputDescription().sendKeys(description);
+        createTenantForm().buttonSubmit().click();
+
+        await().untilAsserted(() -> assertThat(tenantList())
+            .as("Tenant list should contain newly-created tenant")
+            .extracting(WebElement::getText)
+            .anyMatch(it -> it.contains(tenant)));
+
+        return this;
+    }
+
+    public TenantPage delete(String tenant) {
+        tenantList()
+            .stream()
+            .filter(it -> it.getText().contains(tenant))
+            .findFirst()
+            .ifPresent(it -> it.findElement(By.className("delete")).click());
+
+        buttonConfirm().click();
 
-        PageFactory.initElements(driver, this);
+        return this;
     }
 
     @Getter
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/docker/tenant/docker-compose.yaml
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/docker/basic/docker-compose.yaml
similarity index 96%
rename from 
dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/docker/tenant/docker-compose.yaml
rename to 
dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/docker/basic/docker-compose.yaml
index ec761b2..22bd9ec 100644
--- 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/docker/tenant/docker-compose.yaml
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/docker/basic/docker-compose.yaml
@@ -23,6 +23,7 @@ services:
     command: [ standalone-server ]
     environment:
       DATABASE_TYPE: h2
+      WORKER_TENANT_AUTO_CREATE: 'true'
     expose:
       - 12345
     networks:
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/dragAndDrop.js
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/dragAndDrop.js
new file mode 100644
index 0000000..96011d9
--- /dev/null
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/dragAndDrop.js
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+function createEvent(typeOfEvent) {
+    const event = document.createEvent("CustomEvent");
+    event.initCustomEvent(typeOfEvent, true, true, null);
+    event.dataTransfer = {
+        data: {},
+        setData: function (key, value) {
+            this.data[key] = value;
+        },
+        getData: function (key) {
+            return this.data[key];
+        }
+    };
+    return event;
+}
+
+function dispatchEvent(element, event, transferData) {
+    if (transferData !== undefined) {
+        event.dataTransfer = transferData;
+    }
+    if (element.dispatchEvent) {
+        element.dispatchEvent(event);
+    } else if (element.fireEvent) {
+        element.fireEvent("on" + event.type, event);
+    }
+}
+
+function simulateHTML5DragAndDrop(element, destination) {
+    const dragStartEvent = createEvent('dragstart');
+    dispatchEvent(element, dragStartEvent);
+    const dropEvent = createEvent('drop');
+    dispatchEvent(destination, dropEvent, dragStartEvent.dataTransfer);
+    const dragEndEvent = createEvent('dragend');
+    dispatchEvent(element, dragEndEvent, dropEvent.dataTransfer);
+}
+
+const source = arguments[0];
+const destination = arguments[1];
+simulateHTML5DragAndDrop(source, destination);
diff --git 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinSchedulerExtension.java
 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinSchedulerExtension.java
index d7f3232..d6b1886 100644
--- 
a/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinSchedulerExtension.java
+++ 
b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinSchedulerExtension.java
@@ -24,6 +24,8 @@ import static 
org.testcontainers.containers.VncRecordingContainer.VncRecordingFo
 
 import java.io.File;
 import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
 import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -60,8 +62,8 @@ import lombok.extern.slf4j.Slf4j;
 
 @Slf4j
 final class DolphinSchedulerExtension
-        implements BeforeAllCallback, AfterAllCallback,
-        BeforeEachCallback {
+    implements BeforeAllCallback, AfterAllCallback,
+    BeforeEachCallback {
     private final boolean LOCAL_MODE = 
Objects.equals(System.getProperty("local"), "true");
 
     private RemoteWebDriver driver;
@@ -71,7 +73,7 @@ final class DolphinSchedulerExtension
     @Override
     @SuppressWarnings("UnstableApiUsage")
     public void beforeAll(ExtensionContext context) throws IOException {
-        Awaitility.setDefaultTimeout(Duration.ofSeconds(5));
+        Awaitility.setDefaultTimeout(Duration.ofSeconds(10));
         Awaitility.setDefaultPollInterval(Duration.ofSeconds(1));
 
         Network network = null;
@@ -115,8 +117,8 @@ final class DolphinSchedulerExtension
             record = Files.createTempDirectory("record-");
         }
         browser = new BrowserWebDriverContainer<>()
-                .withCapabilities(new ChromeOptions())
-                .withRecordingMode(RECORD_ALL, record.toFile(), MP4);
+            .withCapabilities(new ChromeOptions())
+            .withRecordingMode(RECORD_ALL, record.toFile(), MP4);
         if (network != null) {
             browser.withNetwork(network);
         }
@@ -127,6 +129,8 @@ final class DolphinSchedulerExtension
         driver.manage().timeouts()
               .implicitlyWait(5, TimeUnit.SECONDS)
               .pageLoadTimeout(5, TimeUnit.SECONDS);
+        driver.manage().window()
+              .maximize();
         if (address == null) {
             try {
                 address = 
HostAndPort.fromParts(browser.getTestHostIpAddress(), 8888);
@@ -142,6 +146,12 @@ final class DolphinSchedulerExtension
         driver.get(new URL("http", address.getHost(), address.getPort(), 
rootPath).toString());
 
         browser.beforeTest(new TestDescription(context));
+
+        final Class<?> clazz = context.getRequiredTestClass();
+        Stream.of(clazz.getDeclaredFields())
+              .filter(it -> Modifier.isStatic(it.getModifiers()))
+              .filter(f -> WebDriver.class.isAssignableFrom(f.getType()))
+              .forEach(it -> setDriver(clazz, it));
     }
 
     @Override
@@ -158,14 +168,16 @@ final class DolphinSchedulerExtension
         final Object instance = context.getRequiredTestInstance();
         Stream.of(instance.getClass().getDeclaredFields())
               .filter(f -> WebDriver.class.isAssignableFrom(f.getType()))
-              .forEach(it -> {
-                  try {
-                      it.setAccessible(true);
-                      it.set(instance, driver);
-                  } catch (IllegalAccessException e) {
-                      LOGGER.error("Failed to inject web driver to field: {}", 
it.getName(), e);
-                  }
-              });
+              .forEach(it -> setDriver(instance, it));
+    }
+
+    private void setDriver(Object object, Field field) {
+        try {
+            field.setAccessible(true);
+            field.set(object, driver);
+        } catch (IllegalAccessException e) {
+            LOGGER.error("Failed to inject web driver to field: {}", 
field.getName(), e);
+        }
     }
 
     private DockerComposeContainer<?> createDockerCompose(ExtensionContext 
context) {
@@ -178,9 +190,10 @@ final class DolphinSchedulerExtension
                                        .map(File::new)
                                        .collect(Collectors.toList());
         compose = new DockerComposeContainer<>(files)
-                .withPull(true)
-                .withTailChildContainers(true)
-                .waitingFor("dolphinscheduler_1", Wait.forHealthcheck());
+            .withPull(true)
+            .withTailChildContainers(true)
+            .withLogConsumer("dolphinscheduler_1", outputFrame -> 
LOGGER.info(outputFrame.getUtf8String()))
+            .waitingFor("dolphinscheduler_1", Wait.forHealthcheck());
 
         return compose;
     }
diff --git a/dolphinscheduler-e2e/pom.xml b/dolphinscheduler-e2e/pom.xml
index 1c7ce2e..22f913d 100644
--- a/dolphinscheduler-e2e/pom.xml
+++ b/dolphinscheduler-e2e/pom.xml
@@ -31,8 +31,8 @@
     </modules>
 
     <properties>
-        <maven.compiler.source>8</maven.compiler.source>
-        <maven.compiler.target>8</maven.compiler.target>
+        <maven.compiler.source>11</maven.compiler.source>
+        <maven.compiler.target>11</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 
         <junit.version>5.7.2</junit.version>
@@ -41,18 +41,21 @@
         <assertj-core.version>3.20.2</assertj-core.version>
         <awaitility.version>4.1.0</awaitility.version>
         <kotlin.version>1.5.30</kotlin.version>
+        <slf4j-api.version>1.7.32</slf4j-api.version>
+        <log4j-slf4j-impl.version>2.14.1</log4j-slf4j-impl.version>
+        <guava.version>31.0.1-jre</guava.version>
     </properties>
 
     <dependencies>
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
-            <version>1.7.32</version>
+            <version>${slf4j-api.version}</version>
         </dependency>
         <dependency>
             <groupId>org.apache.logging.log4j</groupId>
             <artifactId>log4j-slf4j-impl</artifactId>
-            <version>2.14.1</version>
+            <version>${log4j-slf4j-impl.version}</version>
         </dependency>
 
         <dependency>
@@ -110,6 +113,11 @@
     <dependencyManagement>
         <dependencies>
             <dependency>
+                <groupId>com.google.guava</groupId>
+                <artifactId>guava</artifactId>
+                <version>${guava.version}</version>
+            </dependency>
+            <dependency>
                 <groupId>org.junit</groupId>
                 <artifactId>junit-bom</artifactId>
                 <version>${junit.version}</version>
@@ -119,7 +127,7 @@
             <dependency>
                 <groupId>org.testcontainers</groupId>
                 <artifactId>testcontainers-bom</artifactId>
-                <version>1.16.0</version>
+                <version>1.16.1</version>
                 <scope>import</scope>
                 <type>pom</type>
             </dependency>
diff --git a/dolphinscheduler-ui/pom.xml b/dolphinscheduler-ui/pom.xml
index a50b82e..8ffb2d4 100644
--- a/dolphinscheduler-ui/pom.xml
+++ b/dolphinscheduler-ui/pom.xml
@@ -29,7 +29,7 @@
   <name>${project.artifactId}</name>
 
   <properties>
-    <node.version>v12.20.2</node.version>
+    <node.version>v14.15.1</node.version>
     <npm.version>6.14.11</npm.version>
     <sonar.sources>src</sonar.sources>
   </properties>
diff --git 
a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.vue 
b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.vue
index 7c5b665..5a46aca 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.vue
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.vue
@@ -25,7 +25,8 @@
           :key="taskType.name"
           @onDragstart="(e) => $emit('on-drag-start', e, taskType)"
           :class="{
-            disabled: isDetails
+            disabled: isDetails,
+            [`task-item-${taskType.name}`]: true
           }"
         >
           <div class="task-item">
diff --git 
a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/toolbar.vue 
b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/toolbar.vue
index d8f57ad..3b8f415 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/toolbar.vue
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/toolbar.vue
@@ -143,6 +143,7 @@
         type="primary"
         size="mini"
         @click="saveProcess"
+        id="button-save"
         >{{ $t("Save") }}</el-button
       >
       <el-button
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/config.js 
b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/config.js
index d4a35e5..b56e4e6 100755
--- a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/config.js
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/config.js
@@ -155,28 +155,32 @@ const tasksState = {
     desc: `${i18n.$t('Submitted successfully')}`,
     color: '#A9A9A9',
     icoUnicode: 'ri-record-circle-fill',
-    isSpin: false
+    isSpin: false,
+    classNames: 'submitted'
   },
   RUNNING_EXECUTION: {
     id: 1,
     desc: `${i18n.$t('Executing')}`,
     color: '#0097e0',
     icoUnicode: 'el-icon-s-tools',
-    isSpin: true
+    isSpin: true,
+    classNames: 'executing'
   },
   READY_PAUSE: {
     id: 2,
     desc: `${i18n.$t('Ready to pause')}`,
     color: '#07b1a3',
     icoUnicode: 'ri-settings-3-line',
-    isSpin: false
+    isSpin: false,
+    classNames: 'submitted'
   },
   PAUSE: {
     id: 3,
     desc: `${i18n.$t('Pause')}`,
     color: '#057c72',
     icoUnicode: 'el-icon-video-pause',
-    isSpin: false
+    isSpin: false,
+    classNames: 'pause'
   },
   READY_STOP: {
     id: 4,
@@ -197,14 +201,16 @@ const tasksState = {
     desc: `${i18n.$t('Failed')}`,
     color: '#000000',
     icoUnicode: 'el-icon-circle-close',
-    isSpin: false
+    isSpin: false,
+    classNames: 'failed'
   },
   SUCCESS: {
     id: 7,
     desc: `${i18n.$t('Success')}`,
     color: '#33cc00',
     icoUnicode: 'el-icon-circle-check',
-    isSpin: false
+    isSpin: false,
+    classNames: 'success'
   },
   NEED_FAULT_TOLERANCE: {
     id: 8,
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/dag.vue 
b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/dag.vue
index deda3f1..47d94a7 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/dag.vue
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/dag.vue
@@ -23,6 +23,7 @@
       size=""
       :with-header="false"
       :wrapperClosable="false"
+      class="task-drawer"
     >
       <!-- fix the bug that Element-ui(2.13.2) auto focus on the first input 
-->
       <div style="width: 0px; height: 0px; overflow: hidden">
diff --git 
a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/formModel/formModel.vue
 
b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/formModel/formModel.vue
index ac21d6a..e80f03f 100644
--- 
a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/formModel/formModel.vue
+++ 
b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/formModel/formModel.vue
@@ -60,6 +60,7 @@
               :disabled="isDetails"
               :placeholder="$t('Please enter name (required)')"
               maxlength="100"
+              id="input-node-name"
             >
             </el-input>
           </div>
@@ -437,6 +438,7 @@
           :loading="spinnerLoading"
           @click="ok()"
           :disabled="isDetails"
+          id="button-submit"
           >{{ spinnerLoading ? $t("Loading...") : $t("Confirm") }}
         </el-button>
       </div>
diff --git 
a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/formModel/tasks/_source/localParams.vue
 
b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/formModel/tasks/_source/localParams.vue
index ae9be4c..b468cf9 100644
--- 
a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/formModel/tasks/_source/localParams.vue
+++ 
b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/formModel/tasks/_source/localParams.vue
@@ -26,6 +26,7 @@
               size="small"
               v-model="localParamsList[$index].prop"
               :placeholder="$t('prop(required)')"
+              class="input-param-key"
               maxlength="256"
               @blur="_verifProp()"
               :style="inputStyle">
@@ -64,6 +65,7 @@
               size="small"
               v-model="localParamsList[$index].value"
               :placeholder="$t('value(optional)')"
+              class="input-param-val"
               maxlength="256"
               @blur="_handleValue()"
               :style="inputStyle">
diff --git 
a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/udp/_source/selectTenant.vue
 
b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/udp/_source/selectTenant.vue
index 0a491f9..50a789f 100644
--- 
a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/udp/_source/selectTenant.vue
+++ 
b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/udp/_source/selectTenant.vue
@@ -20,9 +20,11 @@
           @change="_onChange"
           v-model="selectedValue"
           size="small"
+          id="select-tenant"
           style="width: 180px">
     <el-option
             v-for="item in itemList"
+            class="option-tenants"
             :key="item.id"
             :value="item.id"
             :label="item.tenantCode">
diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/udp/udp.vue 
b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/udp/udp.vue
index 100f47f..d39ac79 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/udp/udp.vue
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/udp/udp.vue
@@ -22,6 +22,7 @@
                 type="text"
                 size="small"
                 v-model="name"
+                id="input-name"
                 :disabled="router.history.current.name === 
'projects-instance-details'"
                 :placeholder="$t('Please enter name (required)')">
         </el-input>
@@ -101,7 +102,7 @@
           </div>
         </template>
         <el-button type="text" size="small" @click="close()"> {{$t('Cancel')}} 
</el-button>
-        <el-button type="primary" size="small" round :disabled="isDetails" 
@click="ok()">{{$t('Add')}}</el-button>
+        <el-button type="primary" size="small" round :disabled="isDetails" 
@click="ok()" id="button-submit">{{$t('Add')}}</el-button>
       </div>
     </div>
   </div>
diff --git 
a/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/definition/pages/list/_source/list.vue
 
b/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/definition/pages/list/_source/list.vue
index 0af0b5a..b7d99bf 100644
--- 
a/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/definition/pages/list/_source/list.vue
+++ 
b/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/definition/pages/list/_source/list.vue
@@ -17,8 +17,8 @@
 <template>
   <div class="list-model" style="position: relative;">
     <div class="table-box">
-      <el-table :data="list" size="mini" style="width: 100%" 
@selection-change="_arrDelChange">
-        <el-table-column type="selection" width="50" 
:selectable="selectable"></el-table-column>
+      <el-table :data="list" size="mini" style="width: 100%" 
@selection-change="_arrDelChange" row-class-name="rows-workflow-definitions">
+        <el-table-column type="selection" width="50" :selectable="selectable" 
class-name="select-all"></el-table-column>
         <el-table-column prop="id" :label="$t('#')" 
width="50"></el-table-column>
         <el-table-column :label="$t('Process Name')" min-width="200">
           <template slot-scope="scope">
@@ -26,7 +26,7 @@
               <p>{{ scope.row.name }}</p>
               <div slot="reference" class="name-wrapper">
                 <router-link :to="{ path: 
`/projects/${projectCode}/definition/list/${scope.row.code}` }" tag="a" 
class="links">
-                  <span class="ellipsis">{{scope.row.name}}</span>
+                  <span class="ellipsis name">{{scope.row.name}}</span>
                 </router-link>
               </div>
             </el-popover>
@@ -66,16 +66,16 @@
               <span><el-button type="primary" size="mini" 
icon="el-icon-edit-outline" :disabled="scope.row.releaseState === 'ONLINE'" 
@click="_edit(scope.row)" circle></el-button></span>
             </el-tooltip>
             <el-tooltip :content="$t('Start')" placement="top" 
:enterable="false">
-              <span><el-button type="success" size="mini" 
:disabled="scope.row.releaseState !== 'ONLINE'"  icon="el-icon-video-play" 
@click="_start(scope.row)" circle></el-button></span>
+              <span><el-button type="success" size="mini" 
:disabled="scope.row.releaseState !== 'ONLINE'"  icon="el-icon-video-play" 
@click="_start(scope.row)" circle class="button-run"></el-button></span>
             </el-tooltip>
             <el-tooltip :content="$t('Timing')" placement="top" 
:enterable="false">
               <span><el-button type="primary" size="mini" icon="el-icon-time" 
:disabled="scope.row.releaseState !== 'ONLINE' || 
scope.row.scheduleReleaseState !== null" @click="_timing(scope.row)" 
circle></el-button></span>
             </el-tooltip>
             <el-tooltip :content="$t('online')" placement="top" 
:enterable="false">
-              <span><el-button type="warning" size="mini" 
v-if="scope.row.releaseState === 'OFFLINE'"  icon="el-icon-upload2" 
@click="_poponline(scope.row)" circle></el-button></span>
+              <span><el-button type="warning" size="mini" 
v-if="scope.row.releaseState === 'OFFLINE'"  icon="el-icon-upload2" 
@click="_poponline(scope.row)" circle class="button-publish"></el-button></span>
             </el-tooltip>
             <el-tooltip :content="$t('offline')" placement="top" 
:enterable="false">
-              <span><el-button type="danger" size="mini" 
icon="el-icon-download" v-if="scope.row.releaseState === 'ONLINE'" 
@click="_downline(scope.row)" circle></el-button></span>
+              <span><el-button type="danger" size="mini" 
icon="el-icon-download" v-if="scope.row.releaseState === 'ONLINE'" 
@click="_downline(scope.row)" circle 
class="button-cancel-publish"></el-button></span>
             </el-tooltip>
             <el-tooltip :content="$t('Copy Workflow')" placement="top" 
:enterable="false">
               <span><el-button type="primary" size="mini" 
:disabled="scope.row.releaseState === 'ONLINE'"  icon="el-icon-document-copy" 
@click="_copyProcess(scope.row)" circle></el-button></span>
@@ -115,7 +115,7 @@
         :title="$t('Delete?')"
         @onConfirm="_delete({},-1)"
       >
-        <el-button style="position: absolute; bottom: -48px; left: 19px;"  
type="primary" size="mini" :disabled="!strSelectCodes" 
slot="reference">{{$t('Delete')}}</el-button>
+        <el-button style="position: absolute; bottom: -48px; left: 19px;"  
type="primary" size="mini" :disabled="!strSelectCodes" slot="reference" 
class="button-delete-all">{{$t('Delete')}}</el-button>
       </el-popconfirm>
     </el-tooltip>
     <el-button type="primary" size="mini" :disabled="!strSelectCodes" 
style="position: absolute; bottom: -48px; left: 80px;" 
@click="_batchExport(item)" >{{$t('Export')}}</el-button>
diff --git 
a/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/definition/pages/list/_source/start.vue
 
b/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/definition/pages/list/_source/start.vue
index 740bbcd..8c99f8b 100644
--- 
a/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/definition/pages/list/_source/start.vue
+++ 
b/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/definition/pages/list/_source/start.vue
@@ -191,7 +191,7 @@
     </div>
     <div class="submit">
       <el-button type="text" size="small" @click="close()"> {{$t('Cancel')}} 
</el-button>
-      <el-button type="primary" size="small" round :loading="spinnerLoading" 
@click="ok()">{{spinnerLoading ? $t('Loading...') : $t('Start')}} </el-button>
+      <el-button type="primary" size="small" round :loading="spinnerLoading" 
@click="ok()" id="button-submit">{{spinnerLoading ? $t('Loading...') : 
$t('Start')}} </el-button>
     </div>
   </div>
 </template>
diff --git 
a/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/definition/pages/list/index.vue
 
b/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/definition/pages/list/index.vue
index e17f22b..9fca167 100644
--- 
a/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/definition/pages/list/index.vue
+++ 
b/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/definition/pages/list/index.vue
@@ -20,7 +20,7 @@
       <template slot="conditions">
         <m-conditions @on-conditions="_onConditions">
           <template slot="button-group">
-            <el-button size="mini"  @click="() => this.$router.push({name: 
'definition-create'})">{{$t('Create process')}}</el-button>
+            <el-button size="mini"  @click="() => this.$router.push({name: 
'definition-create'})" id="button-create-process">{{$t('Create 
process')}}</el-button>
             <el-button size="mini"  @click="_uploading">{{$t('Import 
process')}}</el-button>
           </template>
         </m-conditions>
diff --git 
a/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/instance/pages/list/_source/list.vue
 
b/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/instance/pages/list/_source/list.vue
index a4f4f9b..ba572b8 100644
--- 
a/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/instance/pages/list/_source/list.vue
+++ 
b/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/instance/pages/list/_source/list.vue
@@ -17,8 +17,8 @@
 <template>
   <div class="list-model" style="position: relative;">
     <div class="table-box">
-      <el-table class="fixed" :data="list" size="mini" style="width: 100%" 
@selection-change="_arrDelChange">
-        <el-table-column type="selection" width="50"></el-table-column>
+      <el-table class="fixed" :data="list" size="mini" style="width: 100%" 
@selection-change="_arrDelChange" row-class-name="rows-workflow-instances">
+        <el-table-column type="selection" width="50" 
class-name="select-all"></el-table-column>
         <el-table-column prop="id" :label="$t('#')" 
width="50"></el-table-column>
         <el-table-column :label="$t('Process Name')" min-width="200">
           <template slot-scope="scope">
@@ -61,7 +61,7 @@
             <span>{{scope.row.duration | filterNull}}</span>
           </template>
         </el-table-column>
-        <el-table-column prop="runTimes" :label="$t('Run 
Times')"></el-table-column>
+        <el-table-column prop="runTimes" :label="$t('Run Times')" 
class-name="execution-time"></el-table-column>
         <el-table-column prop="recovery" :label="$t('fault-tolerant 
sign')"></el-table-column>
         <el-table-column :label="$t('Dry-run flag')" width="100">
           <template slot-scope="scope">
@@ -80,7 +80,7 @@
                 </span>
               </el-tooltip>
               <el-tooltip :content="$t('Rerun')" placement="top" 
:enterable="false">
-                <span><el-button type="primary" size="mini" 
:disabled="scope.row.state !== 'SUCCESS' && scope.row.state !== 'PAUSE' && 
scope.row.state !== 'FAILURE' && scope.row.state !== 'STOP'"  
icon="el-icon-refresh" @click="_reRun(scope.row,scope.$index)" 
circle></el-button></span>
+                <span><el-button type="primary" size="mini" 
:disabled="scope.row.state !== 'SUCCESS' && scope.row.state !== 'PAUSE' && 
scope.row.state !== 'FAILURE' && scope.row.state !== 'STOP'"  
icon="el-icon-refresh" @click="_reRun(scope.row,scope.$index)" circle 
class="button-rerun"></el-button></span>
               </el-tooltip>
               <el-tooltip :content="$t('Recovery Failed')" placement="top" 
:enterable="false">
                 <span>
@@ -233,7 +233,7 @@
         :title="$t('Delete?')"
         @onConfirm="_delete({},-1)"
       >
-        <el-button style="position: absolute; bottom: -48px; left: 19px;"  
type="primary" size="mini" :disabled="!strDelete" 
slot="reference">{{$t('Delete')}}</el-button>
+        <el-button style="position: absolute; bottom: -48px; left: 19px;"  
type="primary" size="mini" :disabled="!strDelete" slot="reference" 
class="button-delete-all">{{$t('Delete')}}</el-button>
       </el-popconfirm>
     </el-tooltip>
   </div>
@@ -273,7 +273,7 @@
        */
       _rtState (code) {
         let o = tasksState[code]
-        return `<em class="fa ansfont ${o.icoUnicode} ${o.isSpin ? 'as 
as-spin' : ''}" style="color:${o.color}" data-toggle="tooltip" 
data-container="body" title="${o.desc}"></em>`
+        return `<em class="fa ansfont ${o.classNames} ${o.icoUnicode} 
${o.isSpin ? 'as as-spin' : ''}" style="color:${o.color}" data-toggle="tooltip" 
data-container="body" title="${o.desc}"></em>`
       },
       /**
        * delete
diff --git 
a/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/list/_source/createProject.vue
 
b/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/list/_source/createProject.vue
index a49be0e..443c219 100644
--- 
a/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/list/_source/createProject.vue
+++ 
b/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/list/_source/createProject.vue
@@ -16,13 +16,14 @@
  */
 <template>
   <m-popover ref="popover" :nameText="item ? $t('Edit') : $t('Create 
Project')" :ok-text="item ? $t('Edit') : $t('Submit')"
-           @close="_close" @ok="_ok">
+           @close="_close" @ok="_ok" ok-id="button-submit">
     <template slot="content">
       <div class="projects-create-model">
         <m-list-box-f>
           <template slot="name"><strong>*</strong>{{ $t('Project Name') 
}}</template>
           <template slot="content">
             <el-input
+              id="input-project-name"
               v-model="projectName"
               :placeholder="$t('Please enter name')"
               maxlength="60"
diff --git 
a/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/list/_source/list.vue
 
b/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/list/_source/list.vue
index 05fedbb..3b93932 100644
--- 
a/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/list/_source/list.vue
+++ 
b/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/list/_source/list.vue
@@ -17,14 +17,14 @@
 <template>
   <div class="list-model">
     <div class="table-box">
-      <el-table :data="list" size="mini" style="width: 100%">
+      <el-table :data="list" size="mini" style="width: 100%" 
row-class-name="rows-project">
         <el-table-column type="index" :label="$t('#')" 
width="50"></el-table-column>
         <el-table-column :label="$t('Project Name')">
           <template slot-scope="scope">
             <el-popover trigger="hover" placement="top">
               <p>{{ scope.row.name }}</p>
               <div slot="reference" class="name-wrapper">
-                <a href="javascript:" class="links" 
@click="_switchProjects(scope.row)">{{ scope.row.name }}</a>
+                <a href="javascript:" class="links project-name" 
@click="_switchProjects(scope.row)">{{ scope.row.name }}</a>
               </div>
             </el-popover>
           </template>
@@ -61,7 +61,7 @@
                 :title="$t('Delete?')"
                 @onConfirm="_delete(scope.row,scope.row.id)"
               >
-                <el-button type="danger" size="mini" icon="el-icon-delete" 
circle slot="reference"></el-button>
+                <el-button type="danger" size="mini" icon="el-icon-delete" 
circle slot="reference" class="delete"></el-button>
               </el-popconfirm>
             </el-tooltip>
           </template>
diff --git 
a/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/list/index.vue 
b/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/list/index.vue
index 57a9e7d..7457f3c 100644
--- a/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/list/index.vue
+++ b/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/list/index.vue
@@ -19,7 +19,7 @@
     <template slot="conditions">
       <m-conditions @on-conditions="_onConditions">
         <template slot="button-group">
-          <el-button size="mini" @click="_create('')">{{ $t('Create Project') 
}}</el-button>
+          <el-button size="mini" @click="_create('')" 
id="button-create-project">{{ $t('Create Project') }}</el-button>
           <el-dialog
             :title="item ? $t('Edit') : $t('Create Project')"
             v-if="createProjectDialog"
diff --git 
a/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/taskDefinition/index.vue
 
b/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/taskDefinition/index.vue
index 67aa2b3..40d83f3 100644
--- 
a/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/taskDefinition/index.vue
+++ 
b/dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/taskDefinition/index.vue
@@ -1,307 +1,307 @@
-/*
- * 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.
- */
-<template>
-  <div class="task-definition" v-if="!isLoading">
-    <m-list-construction :title="$t('Task Definition')">
-      <template slot="conditions">
-        <m-conditions @on-conditions="_onConditions" :taskTypeShow="true">
-          <template v-slot:button-group>
-            <el-button size="mini" @click="createTask">
-              {{ $t("Create task") }}
-            </el-button>
-          </template>
-        </m-conditions>
-      </template>
-      <template v-slot:content>
-        <template v-if="tasksList.length || total > 0">
-          <m-list
-            :tasksList="tasksList"
-            @on-update="_onUpdate"
-            @editTask="editTask"
-            @viewTaskDetail="viewTaskDetail"
-          ></m-list>
-          <div class="page-box">
-            <el-pagination
-              background
-              @current-change="_page"
-              @size-change="_pageSize"
-              :page-size="searchParams.pageSize"
-              :current-page.sync="searchParams.pageNo"
-              :page-sizes="[10, 30, 50]"
-              :total="total"
-              layout="sizes, prev, pager, next, jumper"
-            >
-            </el-pagination>
-          </div>
-        </template>
-        <template v-if="!tasksList.length">
-          <m-no-data></m-no-data>
-        </template>
-        <m-spin :is-spin="isLoading"></m-spin>
-      </template>
-    </m-list-construction>
-    <el-drawer
-      :visible.sync="taskDrawer"
-      size=""
-      :with-header="false"
-      @close="closeTaskDrawer"
-    >
-      <!-- fix the bug that Element-ui(2.13.2) auto focus on the first input 
-->
-      <div style="width: 0px; height: 0px; overflow: hidden">
-        <el-input type="text" />
-      </div>
-      <m-form-model
-        v-if="taskDrawer"
-        :nodeData="nodeData"
-        type="task-definition"
-        @changeTaskType="changeTaskType"
-        @close="closeTaskDrawer"
-        @addTaskInfo="saveTask"
-        :taskDefinition="editingTask"
-      >
-      </m-form-model>
-    </el-drawer>
-  </div>
-</template>
-<script>
-  import mListConstruction from 
'@/module/components/listConstruction/listConstruction'
-  import mConditions from '@/module/components/conditions/conditions'
-  import mList from './_source/list'
-  import mNoData from '@/module/components/noData/noData'
-  import mSpin from '@/module/components/spin/spin'
-  import { mapActions, mapMutations } from 'vuex'
-  import listUrlParamHandle from '@/module/mixin/listUrlParamHandle'
-  import mFormModel from 
'@/conf/home/pages/dag/_source/formModel/formModel.vue'
-  /**
-   * tasksType
-   */
-  import { tasksType } from '@/conf/home/pages/dag/_source/config.js'
-
-  const DEFAULT_NODE_DATA = {
-    id: -1,
-    taskType: 'SHELL',
-    instanceId: -1
-  }
-  export default {
-    name: 'task-definition-index',
-    data () {
-      // tasksType
-      const tasksTypeList = Object.keys(tasksType)
-      return {
-        total: null,
-        tasksList: [],
-        isLoading: true,
-        searchParams: {
-          pageSize: 10,
-          pageNo: 1,
-          searchVal: '',
-          taskType: '',
-          userId: ''
-        },
-        // whether the task config drawer is visible
-        taskDrawer: false,
-        // nodeData
-        nodeData: { ...DEFAULT_NODE_DATA },
-        // tasksType
-        tasksTypeList,
-        // editing task definition
-        editingTask: null
-      }
-    },
-    mixins: [listUrlParamHandle],
-    methods: {
-      ...mapActions('dag', [
-        'getTaskDefinitionsList',
-        'genTaskCodeList',
-        'saveTaskDefinition',
-        'updateTaskDefinition'
-      ]),
-      ...mapActions('dag', [
-        'getProcessList',
-        'getProjectList',
-        'getResourcesList',
-        'getResourcesListJar',
-        'getResourcesListJar'
-      ]),
-      ...mapMutations('dag', ['resetParams', 'setIsDetails']),
-      ...mapActions('security', [
-        'getTenantList',
-        'getWorkerGroupsAll',
-        'getAlarmGroupsAll'
-      ]),
-      /**
-       * Toggle task drawer
-       */
-      showTaskDrawer () {
-        this.taskDrawer = true
-      },
-      closeTaskDrawer () {
-        this.setIsDetails(false)
-        this.taskDrawer = false
-      },
-      saveTask ({ item }) {
-        const isEditing = !!this.editingTask
-        if (isEditing) {
-          this.updateTaskDefinition(item)
-            .then((res) => {
-              this.$message.success(res.msg)
-              this._onUpdate()
-              this.closeTaskDrawer()
-            })
-            .catch((e) => {
-              this.$message.error(e.msg || '')
-            })
-        } else {
-          this.genTaskCodeList({
-            genNum: 1
-          })
-            .then((res) => {
-              const [code] = res
-              return code
-            })
-            .then((code) => {
-              return this.saveTaskDefinition({
-                taskDefinitionJson: [
-                  {
-                    ...item,
-                    code
-                  }
-                ]
-              })
-            })
-            .then((res) => {
-              this.$message.success(res.msg)
-              this._onUpdate()
-              this.closeTaskDrawer()
-            })
-            .catch((e) => {
-              this.$message.error(e.msg || '')
-            })
-        }
-      },
-      createTask () {
-        this.editingTask = null
-        this.nodeData.taskType = DEFAULT_NODE_DATA.taskType
-        this.showTaskDrawer()
-      },
-      editTask (task) {
-        this.editingTask = task
-        this.nodeData.id = task.code
-        this.nodeData.taskType = task.taskType
-        this.showTaskDrawer()
-      },
-      viewTaskDetail (task) {
-        this.setIsDetails(true)
-        this.editTask(task)
-      },
-      /**
-       * pageNo
-       */
-      _page (val) {
-        this.searchParams.pageNo = val
-      },
-      _pageSize (val) {
-        this.searchParams.pageSize = val
-      },
-      /**
-       * conditions
-       */
-      _onConditions (o) {
-        this.searchParams.searchVal = o.searchVal
-        this.searchParams.taskType = o.taskType
-        this.searchParams.pageNo = 1
-      },
-      /**
-       * get task definition list
-       */
-      _getList (flag) {
-        this.isLoading = !flag
-        this.getTaskDefinitionsList(this.searchParams)
-          .then((res) => {
-            if (this.searchParams.pageNo > 1 && res.totalList.length === 0) {
-              this.searchParams.pageNo = this.searchParams.pageNo - 1
-            } else {
-              this.tasksList = []
-              this.tasksList = res.totalList
-              this.total = res.total
-              this.isLoading = false
-            }
-          })
-          .catch((e) => {
-            this.isLoading = false
-          })
-      },
-      /**
-       * update task dataList
-       */
-      _onUpdate () {
-        this._debounceGET('false')
-      },
-      /**
-       * change form modal task type
-       */
-      changeTaskType (value) {
-        this.nodeData.taskType = value
-      }
-    },
-    created () {
-      this.isLoading = true
-      // Initialization parameters
-      this.resetParams()
-      // Promise Get node needs data
-      Promise.all([
-        // get process definition
-        this.getProcessList(),
-        // get project
-        this.getProjectList(),
-        // get jar
-        this.getResourcesListJar(),
-        // get resource
-        this.getResourcesList(),
-        // get jar
-        this.getResourcesListJar(),
-        // get worker group list
-        this.getWorkerGroupsAll(),
-        // get alarm group list
-        this.getAlarmGroupsAll(),
-        this.getTenantList()
-      ])
-        .then((data) => {
-          this.isLoading = false
-        })
-        .catch(() => {
-          this.isLoading = false
-        })
-    },
-    mounted () {},
-    components: {
-      mListConstruction,
-      mConditions,
-      mList,
-      mNoData,
-      mSpin,
-      mFormModel
-    }
-  }
-</script>
-<style  lang="scss" scoped>
-.task-definition {
-  .taskGroupBtn {
-    width: 300px;
-  }
-}
-</style>
+/*
+ * 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.
+ */
+<template>
+  <div class="task-definition" v-if="!isLoading">
+    <m-list-construction :title="$t('Task Definition')">
+      <template slot="conditions">
+        <m-conditions @on-conditions="_onConditions" :taskTypeShow="true">
+          <template v-slot:button-group>
+            <el-button size="mini" @click="createTask">
+              {{ $t("Create task") }}
+            </el-button>
+          </template>
+        </m-conditions>
+      </template>
+      <template v-slot:content>
+        <template v-if="tasksList.length || total > 0">
+          <m-list
+            :tasksList="tasksList"
+            @on-update="_onUpdate"
+            @editTask="editTask"
+            @viewTaskDetail="viewTaskDetail"
+          ></m-list>
+          <div class="page-box">
+            <el-pagination
+              background
+              @current-change="_page"
+              @size-change="_pageSize"
+              :page-size="searchParams.pageSize"
+              :current-page.sync="searchParams.pageNo"
+              :page-sizes="[10, 30, 50]"
+              :total="total"
+              layout="sizes, prev, pager, next, jumper"
+            >
+            </el-pagination>
+          </div>
+        </template>
+        <template v-if="!tasksList.length">
+          <m-no-data></m-no-data>
+        </template>
+        <m-spin :is-spin="isLoading"></m-spin>
+      </template>
+    </m-list-construction>
+    <el-drawer
+      :visible.sync="taskDrawer"
+      size=""
+      :with-header="false"
+      @close="closeTaskDrawer"
+    >
+      <!-- fix the bug that Element-ui(2.13.2) auto focus on the first input 
-->
+      <div style="width: 0px; height: 0px; overflow: hidden">
+        <el-input type="text" />
+      </div>
+      <m-form-model
+        v-if="taskDrawer"
+        :nodeData="nodeData"
+        type="task-definition"
+        @changeTaskType="changeTaskType"
+        @close="closeTaskDrawer"
+        @addTaskInfo="saveTask"
+        :taskDefinition="editingTask"
+      >
+      </m-form-model>
+    </el-drawer>
+  </div>
+</template>
+<script>
+  import mListConstruction from 
'@/module/components/listConstruction/listConstruction'
+  import mConditions from '@/module/components/conditions/conditions'
+  import mList from './_source/list'
+  import mNoData from '@/module/components/noData/noData'
+  import mSpin from '@/module/components/spin/spin'
+  import { mapActions, mapMutations } from 'vuex'
+  import listUrlParamHandle from '@/module/mixin/listUrlParamHandle'
+  import mFormModel from 
'@/conf/home/pages/dag/_source/formModel/formModel.vue'
+  /**
+   * tasksType
+   */
+  import { tasksType } from '@/conf/home/pages/dag/_source/config.js'
+
+  const DEFAULT_NODE_DATA = {
+    id: -1,
+    taskType: 'SHELL',
+    instanceId: -1
+  }
+  export default {
+    name: 'task-definition-index',
+    data () {
+      // tasksType
+      const tasksTypeList = Object.keys(tasksType)
+      return {
+        total: null,
+        tasksList: [],
+        isLoading: true,
+        searchParams: {
+          pageSize: 10,
+          pageNo: 1,
+          searchVal: '',
+          taskType: '',
+          userId: ''
+        },
+        // whether the task config drawer is visible
+        taskDrawer: false,
+        // nodeData
+        nodeData: { ...DEFAULT_NODE_DATA },
+        // tasksType
+        tasksTypeList,
+        // editing task definition
+        editingTask: null
+      }
+    },
+    mixins: [listUrlParamHandle],
+    methods: {
+      ...mapActions('dag', [
+        'getTaskDefinitionsList',
+        'genTaskCodeList',
+        'saveTaskDefinition',
+        'updateTaskDefinition'
+      ]),
+      ...mapActions('dag', [
+        'getProcessList',
+        'getProjectList',
+        'getResourcesList',
+        'getResourcesListJar',
+        'getResourcesListJar'
+      ]),
+      ...mapMutations('dag', ['resetParams', 'setIsDetails']),
+      ...mapActions('security', [
+        'getTenantList',
+        'getWorkerGroupsAll',
+        'getAlarmGroupsAll'
+      ]),
+      /**
+       * Toggle task drawer
+       */
+      showTaskDrawer () {
+        this.taskDrawer = true
+      },
+      closeTaskDrawer () {
+        this.setIsDetails(false)
+        this.taskDrawer = false
+      },
+      saveTask ({ item }) {
+        const isEditing = !!this.editingTask
+        if (isEditing) {
+          this.updateTaskDefinition(item)
+            .then((res) => {
+              this.$message.success(res.msg)
+              this._onUpdate()
+              this.closeTaskDrawer()
+            })
+            .catch((e) => {
+              this.$message.error(e.msg || '')
+            })
+        } else {
+          this.genTaskCodeList({
+            genNum: 1
+          })
+            .then((res) => {
+              const [code] = res
+              return code
+            })
+            .then((code) => {
+              return this.saveTaskDefinition({
+                taskDefinitionJson: [
+                  {
+                    ...item,
+                    code
+                  }
+                ]
+              })
+            })
+            .then((res) => {
+              this.$message.success(res.msg)
+              this._onUpdate()
+              this.closeTaskDrawer()
+            })
+            .catch((e) => {
+              this.$message.error(e.msg || '')
+            })
+        }
+      },
+      createTask () {
+        this.editingTask = null
+        this.nodeData.taskType = DEFAULT_NODE_DATA.taskType
+        this.showTaskDrawer()
+      },
+      editTask (task) {
+        this.editingTask = task
+        this.nodeData.id = task.code
+        this.nodeData.taskType = task.taskType
+        this.showTaskDrawer()
+      },
+      viewTaskDetail (task) {
+        this.setIsDetails(true)
+        this.editTask(task)
+      },
+      /**
+       * pageNo
+       */
+      _page (val) {
+        this.searchParams.pageNo = val
+      },
+      _pageSize (val) {
+        this.searchParams.pageSize = val
+      },
+      /**
+       * conditions
+       */
+      _onConditions (o) {
+        this.searchParams.searchVal = o.searchVal
+        this.searchParams.taskType = o.taskType
+        this.searchParams.pageNo = 1
+      },
+      /**
+       * get task definition list
+       */
+      _getList (flag) {
+        this.isLoading = !flag
+        this.getTaskDefinitionsList(this.searchParams)
+          .then((res) => {
+            if (this.searchParams.pageNo > 1 && res.totalList.length === 0) {
+              this.searchParams.pageNo = this.searchParams.pageNo - 1
+            } else {
+              this.tasksList = []
+              this.tasksList = res.totalList
+              this.total = res.total
+              this.isLoading = false
+            }
+          })
+          .catch((e) => {
+            this.isLoading = false
+          })
+      },
+      /**
+       * update task dataList
+       */
+      _onUpdate () {
+        this._debounceGET('false')
+      },
+      /**
+       * change form modal task type
+       */
+      changeTaskType (value) {
+        this.nodeData.taskType = value
+      }
+    },
+    created () {
+      this.isLoading = true
+      // Initialization parameters
+      this.resetParams()
+      // Promise Get node needs data
+      Promise.all([
+        // get process definition
+        this.getProcessList(),
+        // get project
+        this.getProjectList(),
+        // get jar
+        this.getResourcesListJar(),
+        // get resource
+        this.getResourcesList(),
+        // get jar
+        this.getResourcesListJar(),
+        // get worker group list
+        this.getWorkerGroupsAll(),
+        // get alarm group list
+        this.getAlarmGroupsAll(),
+        this.getTenantList()
+      ])
+        .then((data) => {
+          this.isLoading = false
+        })
+        .catch(() => {
+          this.isLoading = false
+        })
+    },
+    mounted () {},
+    components: {
+      mListConstruction,
+      mConditions,
+      mList,
+      mNoData,
+      mSpin,
+      mFormModel
+    }
+  }
+</script>
+<style  lang="scss" scoped>
+.task-definition {
+  .taskGroupBtn {
+    width: 300px;
+  }
+}
+</style>
diff --git a/dolphinscheduler-ui/src/js/module/components/nav/nav.vue 
b/dolphinscheduler-ui/src/js/module/components/nav/nav.vue
index dbbafec..0c6fb23 100644
--- a/dolphinscheduler-ui/src/js/module/components/nav/nav.vue
+++ b/dolphinscheduler-ui/src/js/module/components/nav/nav.vue
@@ -29,7 +29,7 @@
       </div>
       <div class="clearfix list">
         <div class="nav-links">
-          <router-link :to="{ path: '/projects'}" tag="a" 
active-class="active">
+          <router-link :to="{ path: '/projects'}" tag="a" 
active-class="active" id="project-tab">
             <span><em class="ansiconfont el-icon-tickets"></em>{{$t('Project 
Manage')}}</span><strong></strong>
           </router-link>
         </div>
@@ -57,7 +57,7 @@
       </div>
       <div class="clearfix list" >
         <div class="nav-links">
-          <router-link :to="{ path: '/security'}" tag="a" 
active-class="active" v-ps="['ADMIN_USER']">
+          <router-link :to="{ path: '/security'}" tag="a" 
active-class="active" v-ps="['ADMIN_USER']" id="security-tab">
             <span><em class="ansfont 
ri-shield-check-line"></em>{{$t('Security')}}</span><strong></strong>
           </router-link>
         </div>
diff --git 
a/dolphinscheduler-ui/src/js/module/components/secondaryMenu/_source/menu.js 
b/dolphinscheduler-ui/src/js/module/components/secondaryMenu/_source/menu.js
index 1a0a432..9c02958 100644
--- a/dolphinscheduler-ui/src/js/module/components/secondaryMenu/_source/menu.js
+++ b/dolphinscheduler-ui/src/js/module/components/secondaryMenu/_source/menu.js
@@ -51,13 +51,15 @@ const menu = {
           name: `${i18n.$t('Process definition')}`,
           path: 'definition',
           id: 0,
-          enabled: true
+          enabled: true,
+          classNames: 'process-definition'
         },
         {
           name: `${i18n.$t('Process Instance')}`,
           path: 'instance',
           id: 1,
-          enabled: true
+          enabled: true,
+          classNames: 'process-instance'
         },
         {
           name: `${i18n.$t('Task Instance')}`,
@@ -95,7 +97,8 @@ const menu = {
       isOpen: true,
       enabled: true,
       icon: 'el-icon-user-solid',
-      children: []
+      children: [],
+      classNames: 'tenant-manage'
     },
     {
       name: `${i18n.$t('User Manage')}`,
diff --git 
a/dolphinscheduler-ui/src/js/module/components/secondaryMenu/secondaryMenu.vue 
b/dolphinscheduler-ui/src/js/module/components/secondaryMenu/secondaryMenu.vue
index eb135d0..0d0845b 100644
--- 
a/dolphinscheduler-ui/src/js/module/components/secondaryMenu/secondaryMenu.vue
+++ 
b/dolphinscheduler-ui/src/js/module/components/secondaryMenu/secondaryMenu.vue
@@ -23,7 +23,7 @@
     <div class="leven-1" v-for="(item,$index) in menuList" :key="$index">
       <div v-if="item.enabled">
         <template v-if="item.path">
-          <router-link :to="{ name: item.path}">
+          <router-link :to="{ name: item.path}" :class="item.classNames">
             <div class="name" @click="_toggleSubMenu(item)">
               <a href="javascript:">
                 <em class="fa icon" :class="item.icon"></em>
@@ -44,7 +44,7 @@
         </template>
         <ul v-if="item.isOpen && item.children.length">
           <template v-for="(el,index) in item.children">
-            <router-link :to="{ name: el.path}" tag="li" active-class="active" 
v-if="el.enabled" :key="index">
+            <router-link :to="{ name: el.path}" tag="li" active-class="active" 
v-if="el.enabled" :key="index" :class="el.classNames">
               <span>{{el.name}}</span>
             </router-link>
           </template>

Reply via email to