Browse Source

Add E2E tests for some core features (#7025)

3.0.0/version-upgrade
kezhenxu94 3 years ago committed by GitHub
parent
commit
37ba1eb5ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      .github/workflows/e2e.yml
  2. 68
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/ProjectE2ETest.java
  3. 88
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/TenantE2ETest.java
  4. 200
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/WorkflowE2ETest.java
  5. 98
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/security/TenantE2ETest.java
  6. 25
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/LoginPage.java
  7. 45
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/common/CodeEditor.java
  8. 62
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/common/NavBarPage.java
  9. 58
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/ProjectDetailPage.java
  10. 114
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/ProjectPage.java
  11. 123
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowDefinitionTab.java
  12. 85
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowForm.java
  13. 107
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowInstanceTab.java
  14. 46
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowRunDialog.java
  15. 115
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/WorkflowSaveDialog.java
  16. 42
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/task/ShellTaskForm.java
  17. 31
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/task/SubWorkflowTaskForm.java
  18. 93
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/project/workflow/task/TaskNodeForm.java
  19. 51
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/security/SecurityPage.java
  20. 55
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/security/TenantPage.java
  21. 1
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/docker/basic/docker-compose.yaml
  22. 55
      dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/dragAndDrop.js
  23. 45
      dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinSchedulerExtension.java
  24. 18
      dolphinscheduler-e2e/pom.xml
  25. 2
      dolphinscheduler-ui/pom.xml
  26. 3
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/taskbar.vue
  27. 1
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/canvas/toolbar.vue
  28. 18
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/config.js
  29. 1
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/dag.vue
  30. 2
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/formModel/formModel.vue
  31. 2
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/formModel/tasks/_source/localParams.vue
  32. 2
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/udp/_source/selectTenant.vue
  33. 3
      dolphinscheduler-ui/src/js/conf/home/pages/dag/_source/udp/udp.vue
  34. 14
      dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/definition/pages/list/_source/list.vue
  35. 2
      dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/definition/pages/list/_source/start.vue
  36. 2
      dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/definition/pages/list/index.vue
  37. 12
      dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/instance/pages/list/_source/list.vue
  38. 3
      dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/list/_source/createProject.vue
  39. 6
      dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/list/_source/list.vue
  40. 2
      dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/list/index.vue
  41. 614
      dolphinscheduler-ui/src/js/conf/home/pages/projects/pages/taskDefinition/index.vue
  42. 4
      dolphinscheduler-ui/src/js/module/components/nav/nav.vue
  43. 9
      dolphinscheduler-ui/src/js/module/components/secondaryMenu/_source/menu.js
  44. 4
      dolphinscheduler-ui/src/js/module/components/secondaryMenu/secondaryMenu.vue

16
.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

68
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)
);
});
}
}

88
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)
);
});
}
}

200
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);
});
}
}

98
dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/security/TenantE2ETest.java

@ -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();
}
}

25
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);
}
}

45
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;
}
}

62
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 {
}
}

58
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 {
}
}

114
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;
}
}

123
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;
}
}

85
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,
}
}

107
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;
}
}
}

46
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();
}
}

115
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;
}
}

42
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;
}
}

31
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);
}
}

93
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();
}
}

51
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 {
}
}

55
dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/TenantPage.java → 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

1
dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/docker/tenant/docker-compose.yaml → 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:

55
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);

45
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;
}

18
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>
@ -109,6 +112,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>
@ -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>

2
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>

3
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">

1
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

18
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,

1
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">

2
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>

2
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">

2
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">

3
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>

14
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>

2
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>

2
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>

12
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

3
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"

6
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>

2
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"

614
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>

4
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>

9
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')}`,

4
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>

Loading…
Cancel
Save