Merge branch 'main' into appbuilder/sprint-14

This commit is contained in:
johnsoncherian 2025-07-01 10:01:13 +05:30
commit ca09b8df9c
20 changed files with 1276 additions and 308 deletions

View file

@ -139,6 +139,7 @@ jobs:
context: .
build-args: |
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}
BRANCH_NAME=main
file: docker/ee/ee-production.Dockerfile
push: true
tags: tooljet/tooljet-ee:${{ github.event.release.tag_name }},tooljet/tooljet-ee:ee-lts-latest,tooljet/tooljet:ee-lts-latest,tooljet/tooljet:${{ github.event.release.tag_name }}
@ -155,6 +156,7 @@ jobs:
context: .
build-args: |
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}
BRANCH_NAME=lts
file: docker/ee/ee-production.Dockerfile
push: true
tags: tooljet/tooljet-ee:${{ github.event.release.tag_name }},tooljet/tooljet-ee:ee-lts-latest,tooljet/tooljet:ee-lts-latest,tooljet/tooljet:${{ github.event.release.tag_name }}

View file

@ -0,0 +1,47 @@
name: Manual Docker Build and Push
on:
workflow_dispatch:
inputs:
branch_name:
description: 'Git branch to build from'
required: true
default: 'main'
dockerfile_path:
description: 'Path to Dockerfile'
required: true
default: './Dockerfile'
docker_tag:
description: 'Docker tag suffix (e.g., pre-release-14)'
required: true
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch_name }}
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ${{ github.event.inputs.dockerfile_path }}
push: true
tags: tooljet/tj-osv:${{ github.event.inputs.docker_tag }}
platforms: linux/amd64
build-args: |
CUSTOM_GITHUB_TOKEN=${{ secrets.CUSTOM_GITHUB_TOKEN }}

View file

@ -98,8 +98,10 @@ module.exports = defineConfig({
configFile: environment.configFile,
specPattern: [
"cypress/e2e/happyPath/platform/firstUser/firstUserOnboarding.cy.js",
"cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js",
"cypress/e2e/happyPath/platform/ceTestcases/apps/!(*appSlug).cy.js",
"cypress/e2e/happyPath/platform/commonTestcases/userManagment/*.cy.js",
"cypress/e2e/happyPath/platform/eeTestcases/**/*.cy.js",
"cypress/e2e/happyPath/platform/eeTestcases/workspace/*.cy.js",
],
numTestsKeptInMemory: 1,
redirectionLimit: 15,

View file

@ -22,7 +22,7 @@ RUN git checkout ${BRANCH_NAME}
RUN git submodule update --init --recursive
# Checkout the same branch in submodules if it exists, otherwise stay on default branch
RUN git submodule foreach 'git checkout ${BRANCH_NAME} || true'
RUN git submodule foreach 'git checkout main'
# Scripts for building
COPY ./package.json ./package.json
@ -54,7 +54,7 @@ RUN npm install -g @nestjs/cli
RUN npm install -g copyfiles
RUN npm --prefix server run build
FROM node:22.15.1
FROM node:22.15.1-bullseye
RUN apt-get update -yq \
&& apt-get install curl wget gnupg zip -yq \

View file

@ -479,24 +479,22 @@ Cypress.Commands.add("apiMakeAppPublic", (appId = Cypress.env("appId")) => {
});
});
Cypress.Commands.add("apiDeleteGranularPermission", (groupName) => {
Cypress.Commands.add("apiDeleteGranularPermission", (groupName, typesToDelete = []) => {
cy.getAuthHeaders().then((headers) => {
// Fetch group permissions
// Step 1: Get the group by name
cy.request({
method: "GET",
url: `${Cypress.env("server_host")}/api/v2/group-permissions`,
headers: headers,
headers,
log: false,
}).then((response) => {
expect(response.status).to.equal(200);
const group = response.body.groupPermissions.find(
(g) => g.name === groupName
);
const group = response.body.groupPermissions.find((g) => g.name === groupName);
if (!group) throw new Error(`Group with name ${groupName} not found`);
const groupId = group.id;
// Fetch granular permissions for the specific group
// Step 2: Get all granular permissions for the group
cy.request({
method: "GET",
url: `${Cypress.env("server_host")}/api/v2/group-permissions/${groupId}/granular-permissions`,
@ -504,22 +502,31 @@ Cypress.Commands.add("apiDeleteGranularPermission", (groupName) => {
log: false,
}).then((granularResponse) => {
expect(granularResponse.status).to.equal(200);
const granularPermissionId = granularResponse.body[0].id;
const granularPermissions = granularResponse.body;
// Delete the granular permission
cy.request({
method: "DELETE",
url: `${Cypress.env("server_host")}/api/v2/group-permissions/granular-permissions/app/${granularPermissionId}`,
headers,
log: false,
}).then((deleteResponse) => {
expect(deleteResponse.status).to.equal(200);
// Step 3: Filter if typesToDelete is specified
const permissionsToDelete = typesToDelete.length
? granularPermissions.filter((perm) => typesToDelete.includes(perm.type))
: granularPermissions;
// Step 4: Delete each granular permission
permissionsToDelete.forEach((permission) => {
cy.request({
method: "DELETE",
url: `${Cypress.env("server_host")}/api/v2/group-permissions/granular-permissions/app/${permission.id}`,
headers,
log: false,
}).then((deleteResponse) => {
expect(deleteResponse.status).to.equal(200);
cy.log(`Deleted granular permission: ${permission.name}`);
});
});
});
});
});
});
Cypress.Commands.add(
"apiCreateGranularPermission",
(

View file

@ -34,7 +34,7 @@ describe("App Import Functionality", () => {
cy.apiLogin();
cy.apiCreateWorkspace(data.workspaceName, data.workspaceSlug);
cy.apiLogout();
cy.skipWalkthrough()
cy.skipWalkthrough();
});
it("should verify app import functionality", () => {
@ -151,23 +151,49 @@ describe("App Import Functionality", () => {
cy.visit(`${data.workspaceSlug}/data-sources`);
cy.get('[data-cy="postgresql-button"]').should("be.visible");
cy.apiUpdateDataSource("postgresql", "production", {
options: [
{
key: "password",
value: `${Cypress.env("pg_password")}`,
encrypted: true,
},
],
cy.ifEnv("Community", () => {
cy.apiUpdateDataSource("postgresql", "production", {
options: [
{
key: "password",
value: `${Cypress.env("pg_password")}`,
encrypted: true,
},
],
});
});
cy.ifEnv("Enterprise", () => {
cy.apiUpdateDataSource("postgresql", "development", {
options: [
{
key: "password",
value: `${Cypress.env("pg_password")}`,
encrypted: true,
},
],
});
});
cy.apiCreateWsConstant(
"pageHeader",
"Import and Export",
["Global"],
["production"]
);
cy.apiCreateWsConstant("db_name", "persons", ["Secret"], ["production"]);
cy.ifEnv("Community", () => {
cy.apiCreateWsConstant(
"pageHeader",
"Import and Export",
["Global"],
["production"]
);
cy.apiCreateWsConstant("db_name", "persons", ["Secret"], ["production"]);
});
cy.ifEnv("Enterprise", () => {
cy.apiCreateWsConstant(
"pageHeader",
"Import and Export",
["Global"],
["development"]
);
cy.apiCreateWsConstant("db_name", "persons", ["Secret"], ["development"]);
});
// Verify app after setup
cy.wait("@importApp").then((interception) => {

View file

@ -7,6 +7,7 @@ import {
verifyURLs,
resolveHost,
} from "Support/utils/apps";
import { appPromote } from "Support/utils/platform/multiEnv";
describe("App Slug", () => {
const data = {};
@ -153,6 +154,7 @@ describe("App Slug", () => {
cy.visit("/my-workspace");
cy.apiCreateApp(data.slug);
cy.openApp("my-workspace");
releaseApp();
cy.get(commonWidgetSelector.shareAppButton).click();
cy.clearAndType(commonWidgetSelector.appNameSlugInput, data.slug);

View file

@ -12,6 +12,8 @@ import {
verifyRestrictedAccess,
onboardUserFromAppLink,
} from "Support/utils/apps";
import { appPromote } from "Support/utils/platform/multiEnv";
import { InstanceSSO } from "Support/utils/platform/eeCommon";
describe(
"Private and Public apps",
@ -183,6 +185,9 @@ describe(
setupAppWithSlug(data.appName, data.slug);
cy.apiLogout();
cy.ifEnv("Enterprise", () => {
InstanceSSO(true, true, true);
});
userSignUp(data.firstName, data.email, data.workspaceName);
cy.wait(1000);
cy.visitSlug({
@ -312,7 +317,9 @@ describe(
cy.apiLogout();
cy.apiLogin();
cy.visit(`${data.workspaceSlug}`);
cy.apiDeleteGranularPermission("end-user");
cy.apiDeleteGranularPermission("end-user", ["app", "workflow"]);
setSignupStatus(true, data.workspaceName);
setupAppWithSlug(data.appName, data.slug);

View file

@ -1,7 +1,6 @@
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { fake } from "Fixtures/fake";
import { commonText } from "Texts/common";
import {
editVersionAndVerify,
deleteVersionAndVerify,
@ -13,25 +12,20 @@ import {
navigateToEditVersionModal,
switchVersionAndVerify,
} from "Support/utils/version";
import { appVersionSelectors } from "Selectors/exportImport";
import { editVersionSelectors } from "Selectors/version";
import { editVersionText } from "Texts/version";
import { createNewVersion } from "Support/utils/exportImport";
import { verifyModal, closeModal } from "Support/utils/common";
import {
verifyComponent,
verifyComponentinrightpannel,
deleteComponentAndVerify,
} from "Support/utils/basicComponents";
import { deleteVersionText, onlydeleteVersionText } from "Texts/version";
import { createRestAPIQuery } from "Support/utils/dataSource";
import { deleteQuery } from "Support/utils/queries";
import { selectEnv, appPromote } from "Support/utils/platform/multiEnv";
describe("App Version", () => {
let data;
@ -120,7 +114,15 @@ describe("App Version", () => {
// Preview and release verification
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.url().should("include", "/home?version=v2");
cy.ifEnv("Community", () => {
cy.url().should("include", "/home?version=v2");
});
cy.ifEnv("Enterprise", () => {
cy.url().should("include", "/home?env=development&version=v2");
});
cy.openApp(
"",
Cypress.env("workspaceId"),
@ -149,7 +151,11 @@ describe("App Version", () => {
createRestAPIQuery(data.query1, data.datasourceName, "", "", "/1", true);
// Version v2 creation and verification
cy.ifEnv("Enterprise", () => {
appPromote("development", "production");
});
// Version v2 creation and verification and v2 is created from v1 production environment
navigateToCreateNewVersionModal("v1");
createNewVersion(["v2"], "v1");
cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement(
@ -201,7 +207,8 @@ describe("App Version", () => {
versionChecks.forEach((check) => {
navigateToCreateNewVersionModal(check.create.from);
createNewVersion([check.create.version], check.create.from);
cy.waitForAutoSave();
cy.wait(1000);
if (check.verify.component.value) {
cy.get(
commonWidgetSelector.draggableWidget(check.verify.component.selector)
@ -224,6 +231,9 @@ describe("App Version", () => {
);
// Version switching and component verification
cy.ifEnv("Enterprise", () => {
selectEnv("development");
});
cy.get(appVersionSelectors.currentVersionField("v5")).click();
cy.contains(`[id*="react-select-"]`, "v4").click();
cy.get(appVersionSelectors.currentVersionField("v4")).should(
@ -238,7 +248,14 @@ describe("App Version", () => {
// Preview and version switching verification
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.url().should("include", "/home?version=v4");
cy.ifEnv("Community", () => {
cy.url().should("include", "/home?version=v4");
});
cy.ifEnv("Enterprise", () => {
cy.url().should("include", "/home?env=development&version=v4");
});
cy.get(commonWidgetSelector.draggableWidget("text1")).verifyVisibleElement(
"have.text",
"Leanne Graham"
@ -250,8 +267,74 @@ describe("App Version", () => {
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
//url validation should be added after bug fix
// cy.url().should("include", "/home?version=v5");
cy.ifEnv("Enterprise", () => {
cy.openApp(
"",
Cypress.env("workspaceId"),
Cypress.env("appId"),
commonWidgetSelector.draggableWidget("textInput")
);
navigateToCreateNewVersionModal("v5");
createNewVersion(["v6"], "v5");
cy.waitForAutoSave();
cy.wait(1000);
appPromote("development", "staging");
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
cy.get(`[data-cy="list-query-${data.query2}"]`).should("be.visible");
appPromote("staging", "production");
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
cy.get(`[data-cy="list-query-${data.query2}"]`).should("be.visible");
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
cy.url().should("include", "/home?env=production&version=v6");
cy.wait(1000);
cy.get('[data-cy="preview-settings"]').click();
switchVersionAndVerify("v6", "v1");
cy.get(
commonWidgetSelector.draggableWidget("text1")
).verifyVisibleElement("have.text", "Leanne Graham");
// url bug
// cy.url().should("include", "/home?env=production&version=v1");
cy.wait(1000);
cy.get('[data-cy="preview-settings"]').click();
switchVersionAndVerify("v1", "v6");
cy.wait(1000);
cy.get('[data-cy="preview-settings"]').click();
selectEnv("staging");
cy.get(
commonWidgetSelector.draggableWidget("textInput")
).verifyVisibleElement("have.value", "Ervin Howell");
// cy.url().should("include", "/home?env=staging&version=v6");
cy.wait(1000);
cy.get('[data-cy="preview-settings"]').click();
selectEnv("development");
cy.wait(1000);
cy.get('[data-cy="preview-settings"]').click();
switchVersionAndVerify("v6", "v1");
cy.get(
commonWidgetSelector.draggableWidget("text1")
).verifyVisibleElement("have.text", "Leanne Graham");
});
});
});

View file

@ -0,0 +1,662 @@
import { fake } from "Fixtures/fake";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { commonEeText, ssoEeText } from "Texts/eeCommon";
import { commonEeSelectors, multiEnvSelector } from "Selectors/eeCommon";
import {
verifyPromoteModalUI,
verifyTooltipDisabled,
} from "Support/utils/platform/eeCommon";
import { dataSourceSelector } from "Selectors/dataSource";
import {
navigateToAppEditor,
pinInspector,
verifyTooltip,
} from "Support/utils/common";
import { addQuery, selectDatasource } from "Support/utils/dataSource";
import {
appPromote,
createNewVersion,
selectVersion,
selectEnv,
} from "Support/utils/platform/multiEnv";
import { appVersionSelectors } from "Selectors/exportImport";
import { editAndVerifyWidgetName } from "Support/utils/commonWidget";
import { deleteVersionAndVerify } from "Support/utils/version";
import { deleteVersionText } from "Texts/version";
describe("Multi env", () => {
const data = {};
data.appName = `${fake.companyName} App`;
data.ds = fake.lastName.toLowerCase().replaceAll("[^A-Za-z]", "");
data.constName = fake.firstName.toLowerCase().replaceAll("[^A-Za-z]", "");
const slug = data.appName.toLowerCase().replace(/\s+/g, "-");
let currentVersion = "";
let newVersion = [];
let versionFrom = "";
beforeEach(() => {
cy.apiLogin();
cy.viewport(1800, 1800);
cy.skipWalkthrough();
});
it.only("Verify the datasource configuration and data on each env", () => {
cy.apiCreateGDS(
`${Cypress.env("server_host")}/api/data-sources`,
data.ds,
"restapi",
[
{ key: "url", value: "" },
{ key: "auth_type", value: "none" },
{ key: "grant_type", value: "authorization_code" },
{ key: "add_token_to", value: "header" },
{ key: "header_prefix", value: "Bearer " },
{ key: "access_token_url", value: "" },
{ key: "client_ide", value: "" },
{ key: "client_secret", value: "", encrypted: true },
{ key: "scopes", value: "read, write" },
{ key: "username", value: "", encrypted: false },
{ key: "password", value: "", encrypted: true },
{ key: "bearer_token", value: "", encrypted: true },
{ key: "auth_url", value: "" },
{ key: "client_auth", value: "header" },
{ key: "headers", value: [["", ""]] },
{ key: "custom_query_params", value: [["", ""]], encrypted: false },
{ key: "custom_auth_params", value: [["", ""]] },
{
key: "access_token_custom_headers",
value: [["", ""]],
encrypted: false,
},
{ key: "multiple_auth_enabled", value: false, encrypted: false },
{ key: "ssl_certificate", value: "none", encrypted: false },
]
);
cy.apiCreateApp(data.appName);
cy.visit("/my-workspace");
cy.get(commonSelectors.globalDataSourceIcon).click();
selectDatasource(data.ds);
cy.get('[data-cy="development-label"]').click();
cy.clearAndType(
'[data-cy="base-url-text-field"]',
"https://reqres.in/api/users?page=1"
);
cy.get(dataSourceSelector.buttonSave).click();
cy.wait(2000);
cy.get(commonSelectors.dashboardIcon).click();
cy.openApp();
// cy.waitForAppLoad();
cy.wait(2000);
cy.get(`[data-cy="${data.ds}-add-query-card"] > .text-truncate`).click();
cy.wait(1000);
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
cy.get('[data-cy="query-tab-settings"]').click();
cy.get(':nth-child(1) > .custom-toggle-switch > .switch > .slider').click();
cy.waitForAutoSave();
cy.dragAndDropWidget("Text Input", 550, 650);
editAndVerifyWidgetName(data.constName, []);
cy.waitForAutoSave();
cy.get(
'[data-cy="default-value-input-field"]'
).clearAndTypeOnCodeMirror(`{{queries.restapi1.data.data[0].email`);
cy.wait(1000);
cy.forceClickOnCanvas();
cy.waitForAutoSave();
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
cy.get(
commonWidgetSelector.draggableWidget(data.constName)
).verifyVisibleElement("have.value", "george.bluth@reqres.in");
pinInspector();
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(commonWidgetSelector.inspectorNodeComponents).click();
cy.get(commonWidgetSelector.nodeComponent(data.constName)).click();
cy.get('[data-cy="inspector-node-value"] > .mx-2').verifyVisibleElement(
"have.text",
`"george.bluth@reqres.in"`
);
cy.get('[style="height: 13px; width: 13px;"] > img').should("exist");
cy.get('[data-cy="inspector-node-globals"] > .node-key').click();
cy.get('[data-cy="inspector-node-environment"] > .node-key').click();
cy.get('[data-cy="inspector-node-name"] > .mx-2').verifyVisibleElement(
"have.text",
`"development"`
);
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.wait(4000);
cy.get(
commonWidgetSelector.draggableWidget(data.constName)
).verifyVisibleElement("have.value", "george.bluth@reqres.in");
cy.go("back");
cy.waitForAppLoad();
cy.wait(3000);
cy.get(commonEeSelectors.promoteButton).click();
cy.get(commonEeSelectors.promoteButton).eq(1).click();
cy.waitForAppLoad();
cy.wait(3000);
cy.get(dataSourceSelector.queryCreateAndRunButton, {
timeout: 20000,
}).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query could not be completed"
);
cy.backToApps();
cy.get(commonSelectors.globalDataSourceIcon).click();
selectDatasource(data.ds);
cy.get('[data-cy="staging-label"]').click();
cy.clearAndType(
'[data-cy="base-url-text-field"]',
"https://reqres.in/api/users?page=2"
);
cy.get(dataSourceSelector.buttonSave).click();
cy.wait(2000);
cy.get(commonSelectors.dashboardIcon).click();
navigateToAppEditor(data.appName);
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
cy.get(
commonWidgetSelector.draggableWidget(data.constName)
).verifyVisibleElement("have.value", "michael.lawson@reqres.in");
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(commonWidgetSelector.inspectorNodeComponents).click();
cy.get(commonWidgetSelector.nodeComponent(data.constName)).click();
cy.get('[data-cy="inspector-node-value"] > .mx-2').verifyVisibleElement(
"have.text",
`"michael.lawson@reqres.in"`
);
cy.get('[style="height: 13px; width: 13px;"] > img').should("not.exist");
cy.get('[data-cy="inspector-node-globals"] > .node-key').click();
cy.get('[data-cy="inspector-node-environment"] > .node-key').click();
cy.get('[data-cy="inspector-node-name"] > .mx-2').verifyVisibleElement(
"have.text",
`"staging"`
);
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.wait(4000);
cy.get(
commonWidgetSelector.draggableWidget(data.constName)
).verifyVisibleElement("have.value", "michael.lawson@reqres.in");
cy.go("back");
cy.waitForAppLoad();
cy.wait(3000);
cy.get(commonEeSelectors.promoteButton).click();
cy.get(commonEeSelectors.promoteButton).eq(1).click();
cy.waitForAppLoad();
cy.wait(3000);
cy.get(dataSourceSelector.queryCreateAndRunButton, {
timeout: 20000,
}).click();
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query could not be completed"
);
cy.backToApps();
cy.get(commonSelectors.globalDataSourceIcon).click();
selectDatasource(data.ds);
cy.get('[data-cy="production-label"]').click();
cy.clearAndType(
'[data-cy="base-url-text-field"]',
"https://reqres.in/api/users?page=1"
);
cy.get(dataSourceSelector.buttonSave).click();
cy.wait(2000);
cy.get(commonSelectors.dashboardIcon).click();
navigateToAppEditor(data.appName);
cy.get(dataSourceSelector.queryCreateAndRunButton).click();
cy.get(
commonWidgetSelector.draggableWidget(data.constName)
).verifyVisibleElement("have.value", "george.bluth@reqres.in");
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(commonWidgetSelector.inspectorNodeComponents).click();
cy.get(commonWidgetSelector.nodeComponent(data.constName)).click();
cy.get('[data-cy="inspector-node-value"] > .mx-2').verifyVisibleElement(
"have.text",
`"george.bluth@reqres.in"`
);
cy.get('[style="height: 13px; width: 13px;"] > img').should("not.exist");
cy.get('[data-cy="inspector-node-globals"] > .node-key').click();
cy.get('[data-cy="inspector-node-environment"] > .node-key').click();
cy.get('[data-cy="inspector-node-name"] > .mx-2').verifyVisibleElement(
"have.text",
`"production"`
);
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.wait(4000);
cy.get(
commonWidgetSelector.draggableWidget(data.constName)
).verifyVisibleElement("have.value", "george.bluth@reqres.in");
cy.go("back");
cy.waitForAppLoad();
cy.wait(3000);
cy.get(commonSelectors.releaseButton).click();
cy.get(commonSelectors.yesButton).click();
cy.verifyToastMessage(commonSelectors.toastMessage, "Version v1 released");
cy.wait(4000);
cy.get(commonWidgetSelector.shareAppButton).click();
cy.clearAndType(commonWidgetSelector.appNameSlugInput, `${slug}`);
cy.wait(2000);
cy.get(commonWidgetSelector.modalCloseButton).click();
cy.visit(`/applications/${slug}`);
cy.get(
commonWidgetSelector.draggableWidget(data.constName)
).verifyVisibleElement("have.value", "george.bluth@reqres.in");
});
it("should verify edit privilages of a promoted version", () => {
data.appName = `${fake.companyName} App`;
cy.apiCreateApp(data.appName);
cy.openApp();
cy.waitForAppLoad();
cy.dragAndDropWidget("Text", 550, 650);
appPromote("development", "production");
createNewVersion(
(currentVersion = "v1"),
(newVersion = ["v2"]),
(versionFrom = "v1")
);
appPromote("development", "release");
createNewVersion(
(currentVersion = "v2"),
(newVersion = ["v3"]),
(versionFrom = "v2")
);
appPromote("development", "staging");
selectVersion((currentVersion = "v3"), (newVersion = ["v1"]));
cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement(
"have.text",
"App cannot be edited after promotion. Please create a new version from Development to make any changes."
);
cy.forceClickOnCanvas();
cy.get(".datasource-picker").should("have.class", "disabled");
cy.get(commonEeSelectors.AddQueryButton).should("be.disabled");
cy.get(".components-container").should("have.class", "disabled");
cy.wait(1000);
selectEnv("development");
cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement(
"have.text",
"App cannot be edited after promotion. Please create a new version from Development to make any changes."
);
cy.get(".datasource-picker").should("have.class", "disabled");
cy.get(commonEeSelectors.AddQueryButton).should("be.disabled");
cy.get(".components-container").should("have.class", "disabled");
selectVersion((currentVersion = "v1"), (newVersion = ["v2"]));
cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement(
"have.text",
"This version of the app is released. Please create a new version in development to make any changes."
);
cy.get(".datasource-picker").should("have.class", "disabled");
cy.get(commonEeSelectors.AddQueryButton).should("be.disabled");
cy.get(".components-container").should("have.class", "disabled");
cy.wait(1000);
selectEnv("staging");
cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement(
"have.text",
"This version of the app is released. Please create a new version in development to make any changes."
);
cy.get(".datasource-picker").should("have.class", "disabled");
cy.get(commonEeSelectors.AddQueryButton).should("be.disabled");
cy.get(".components-container").should("have.class", "disabled");
cy.wait(1000);
selectEnv("production");
cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement(
"have.text",
"This version of the app is released. Please create a new version in development to make any changes."
);
cy.get(".datasource-picker").should("have.class", "disabled");
cy.get(commonEeSelectors.AddQueryButton).should("be.disabled");
cy.get(".components-container").should("have.class", "disabled");
cy.get(commonSelectors.releaseButton).should("be.disabled");
});
it("Should verify last exisiting version", () => {
data.appName = `${fake.companyName} App`;
cy.apiCreateApp(data.appName);
cy.openApp();
cy.waitForAppLoad();
cy.dragAndDropWidget("Text", 550, 650);
appPromote("development", "staging");
createNewVersion(
(currentVersion = "v1"),
(newVersion = ["v2"]),
(versionFrom = "v1")
);
selectVersion((currentVersion = "v2"), (newVersion = ["v1"]));
cy.wait(1000);
selectEnv("staging");
cy.get(appVersionSelectors.currentVersionField(newVersion[0]))
.should("be.visible")
.and("have.text", "v1");
appPromote("staging", "production");
cy.wait(3000)
deleteVersionAndVerify(
(currentVersion = "v1"),
deleteVersionText.deleteToastMessage((currentVersion = "v1"))
);
cy.wait(2000);
cy.get('[data-cy="list-current-env-name"]').click();
verifyTooltip(
'[data-cy="env-name-dropdown"]:eq(1)',
"There are no versions in this environment"
);
verifyTooltip(
'[data-cy="env-name-dropdown"]:eq(2)',
"There are no versions in this environment"
);
});
it("Should verify version deletion", () => {
data.appName = `${fake.companyName} App`;
cy.apiCreateApp(data.appName);
cy.openApp();
cy.waitForAppLoad();
cy.dragAndDropWidget("Text", 550, 650);
appPromote("development", "staging");
createNewVersion(
(currentVersion = "v1"),
(newVersion = ["v2"]),
(versionFrom = "v1")
);
appPromote("development", "staging");
createNewVersion(
(currentVersion = "v2"),
(newVersion = ["v3"]),
(versionFrom = "v2")
);
appPromote("development", "production");
selectEnv("staging");
selectVersion((currentVersion = "v3"), (newVersion = ["v2"]));
deleteVersionAndVerify(
(currentVersion = "v2"),
deleteVersionText.deleteToastMessage((currentVersion = "v2"))
);
cy.get('[data-cy="v3-current-version-text"]')
.should("be.visible")
.and("have.text", "v3");
cy.get('[data-cy="list-current-env-name"]').should(
"have.text",
"Staging"
);
})
it("Verify the multi env components UI", () => {
data.appName = `${fake.companyName} App`;
cy.apiCreateApp(data.appName);
cy.openApp();
cy.waitForAppLoad();
cy.dragAndDropWidget("Text", 550, 650);
cy.get(multiEnvSelector.envContainer).should("be.visible");
cy.get(multiEnvSelector.currentEnvName)
.verifyVisibleElement("have.text", "Development")
.click();
cy.get(multiEnvSelector.envArrow).should("be.visible");
cy.get(multiEnvSelector.selectedEnvName).verifyVisibleElement(
"have.text",
" Development"
);
cy.get(multiEnvSelector.envNameList)
.eq(0)
.verifyVisibleElement("have.text", "Development");
cy.get(multiEnvSelector.envNameList)
.eq(1)
.verifyVisibleElement("have.text", "Staging");
cy.get(multiEnvSelector.envNameList)
.eq(2)
.verifyVisibleElement("have.text", "Production");
verifyTooltip(
'[data-cy="env-name-dropdown"]:eq(1)',
"There are no versions in this environment"
);
verifyTooltip(
'[data-cy="env-name-dropdown"]:eq(2)',
"There are no versions in this environment"
);
cy.get(multiEnvSelector.appVersionLabel).should("be.visible");
cy.get('[data-cy="v1-current-version-text"]')
.verifyVisibleElement("have.text", "v1")
.click();
cy.get(multiEnvSelector.currentVersion).verifyVisibleElement(
"have.text",
"v1"
);
cy.get(".col-10 > .app-version-name").verifyVisibleElement(
"have.text",
"v1"
);
cy.get(multiEnvSelector.createNewVersionButton).verifyVisibleElement(
"have.text",
"Create new version"
);
verifyPromoteModalUI("v1", "Development", "Staging");
cy.get('[data-cy="env-change-info-text"]').verifyVisibleElement(
"have.text",
"You wont be able to edit this version after promotion. Are you sure you want to continue?"
);
cy.get(commonSelectors.closeButton).click();
cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement(
"have.text",
"Development"
);
cy.get(commonEeSelectors.promoteButton).click();
cy.get(commonSelectors.cancelButton).click();
cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement(
"have.text",
"Development"
);
cy.get(commonEeSelectors.promoteButton).click();
cy.get(commonEeSelectors.promoteButton).eq(1).click();
cy.waitForAppLoad();
cy.wait(3000);
cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement(
"have.text",
"App cannot be edited after promotion. Please create a new version from Development to make any changes."
);
cy.get(multiEnvSelector.envContainer).should("be.visible");
cy.get(multiEnvSelector.currentEnvName)
.verifyVisibleElement("have.text", "Staging")
.click();
cy.get(multiEnvSelector.envArrow).should("be.visible");
cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement(
"have.text",
"Staging"
);
cy.get(multiEnvSelector.envNameList)
.eq(0)
.verifyVisibleElement("have.text", "Development");
cy.get(multiEnvSelector.envNameList)
.eq(1)
.verifyVisibleElement("have.text", "Staging");
cy.get(multiEnvSelector.envNameList)
.eq(2)
.verifyVisibleElement("have.text", "Production");
cy.wait(2000)
verifyTooltip(
'[data-cy="env-name-dropdown"]:eq(2)',
"There are no versions in this environment"
);
cy.get(multiEnvSelector.appVersionLabel).should("be.visible");
cy.get('[data-cy="v1-current-version-text"]')
.verifyVisibleElement("have.text", "v1")
.click();
cy.get(multiEnvSelector.currentVersion).verifyVisibleElement(
"have.text",
"v1"
);
cy.get(".col-10 > .app-version-name").verifyVisibleElement(
"have.text",
"v1"
);
cy.get(multiEnvSelector.createNewVersionButton).verifyVisibleElement(
"have.text",
"Create new version"
);
verifyTooltip(
multiEnvSelector.createNewVersionButton,
"New versions can only be created in development"
);
cy.forceClickOnCanvas();
cy.get(".datasource-picker").should("have.class", "disabled");
cy.get(commonEeSelectors.AddQueryButton).should("be.disabled");
cy.get(".components-container").should("have.class", "disabled");
verifyPromoteModalUI("v1", "Staging", "Production");
cy.get(commonSelectors.closeButton).click();
cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement(
"have.text",
"Staging"
);
cy.get(commonEeSelectors.promoteButton).click();
cy.get(commonSelectors.cancelButton).click();
cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement(
"have.text",
"Staging"
);
cy.get(commonEeSelectors.promoteButton).click();
cy.get(commonEeSelectors.promoteButton).eq(1).click();
cy.waitForAppLoad();
cy.wait(3000);
cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement(
"have.text",
"App cannot be edited after promotion. Please create a new version from Development to make any changes."
);
cy.get(multiEnvSelector.envContainer).should("be.visible");
cy.get(multiEnvSelector.currentEnvName)
.verifyVisibleElement("have.text", "Production")
.click();
cy.get(multiEnvSelector.envArrow).should("be.visible");
cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement(
"have.text",
"Production"
);
cy.get(multiEnvSelector.envNameList)
.eq(0)
.verifyVisibleElement("have.text", "Development");
cy.get(multiEnvSelector.envNameList)
.eq(1)
.verifyVisibleElement("have.text", "Staging");
cy.get(multiEnvSelector.envNameList)
.eq(2)
.verifyVisibleElement("have.text", "Production");
cy.get(multiEnvSelector.appVersionLabel).should("be.visible");
cy.get('[data-cy="v1-current-version-text"]')
.verifyVisibleElement("have.text", "v1")
.click();
cy.get(multiEnvSelector.currentVersion).verifyVisibleElement(
"have.text",
"v1"
);
cy.get(".col-10 > .app-version-name").verifyVisibleElement(
"have.text",
"v1"
);
cy.get(multiEnvSelector.createNewVersionButton).verifyVisibleElement(
"have.text",
"Create new version"
);
cy.get(commonSelectors.releaseButton)
.verifyVisibleElement("have.text", "Release")
.click();
cy.get('[data-cy="modal-title"]').verifyVisibleElement(
"have.text",
"Release Version"
);
cy.get(commonSelectors.closeButton).should("be.visible");
cy.get('[data-cy="confirm-dialogue-box-text"]').verifyVisibleElement(
"have.text",
"Are you sure you want to release this version?"
);
cy.get(commonSelectors.cancelButton).verifyVisibleElement(
"have.text",
"Cancel"
);
cy.get(commonSelectors.yesButton).verifyVisibleElement("have.text", "Yes");
cy.get(commonSelectors.closeButton).click();
cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement(
"have.text",
"Production"
);
cy.get(commonSelectors.releaseButton).click();
cy.get(commonSelectors.cancelButton).click();
cy.get(multiEnvSelector.currentEnvName).verifyVisibleElement(
"have.text",
"Production"
);
cy.get(commonSelectors.releaseButton).click();
cy.get(commonSelectors.yesButton).click();
cy.verifyToastMessage(commonSelectors.toastMessage, "Version v1 released");
cy.wait(500);
cy.get(commonSelectors.warningText).eq(0).verifyVisibleElement(
"have.text",
"This version of the app is released. Please create a new version in development to make any changes."
);
cy.get('[data-cy="v1-current-version-text"]').click();
verifyTooltip(
multiEnvSelector.createNewVersionButton,
"New versions can only be created in development"
);
cy.get(".datasource-picker").should("have.class", "disabled");
cy.get(commonEeSelectors.AddQueryButton).should("be.disabled");
cy.get(".components-container").should("have.class", "disabled");
cy.get(commonSelectors.releaseButton).should("be.disabled");
});
});

View file

@ -1,112 +1,132 @@
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { appPromote } from "Support/utils/platform/multiEnv";
const slugValidations = [
{ input: "", error: "App slug can't be empty" },
{ input: "_2#", error: "Special characters are not accepted." },
{ input: "t ", error: "Cannot contain spaces" },
{ input: "T", error: "Only lowercase letters are accepted." },
{ input: "", error: "App slug can't be empty" },
{ input: "_2#", error: "Special characters are not accepted." },
{ input: "t ", error: "Cannot contain spaces" },
{ input: "T", error: "Only lowercase letters are accepted." },
];
export const verifySlugValidations = (inputSelector) => {
slugValidations.forEach(({ input, error }) => {
cy.get(inputSelector).clear();
if (input) cy.clearAndType(inputSelector, input);
cy.wait(500);
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
"have.text",
error
);
});
slugValidations.forEach(({ input, error }) => {
cy.get(inputSelector).clear();
if (input) cy.clearAndType(inputSelector, input);
cy.wait(500);
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
"have.text",
error
);
});
};
export const verifySuccessfulSlugUpdate = (workspaceId, slug) => {
const host = resolveHost();
cy.get('[data-cy="app-slug-accepted-label"]').verifyVisibleElement(
"have.text",
"Slug accepted!"
);
const host = resolveHost();
cy.get('[data-cy="app-slug-accepted-label"]').verifyVisibleElement(
"have.text",
"Slug accepted!"
);
cy.wait(500);
// cy.get(commonWidgetSelector.appLinkSucessLabel).should('be.visible');
cy.get(commonWidgetSelector.appLinkSucessLabel).should(
"have.text",
"Link updated successfully!"
);
cy.get(commonWidgetSelector.appLinkField).verifyVisibleElement(
"have.text",
`${host}/${workspaceId}/apps/${slug}`
);
cy.wait(500);
// cy.get(commonWidgetSelector.appLinkSucessLabel).should('be.visible');
cy.get(commonWidgetSelector.appLinkSucessLabel).should(
"have.text",
"Link updated successfully!"
);
cy.get(commonWidgetSelector.appLinkField).verifyVisibleElement(
"have.text",
`${host}/${workspaceId}/apps/${slug}`
);
};
export const verifyURLs = (workspaceId, slug, page) => {
const baseUrl = Cypress.config("baseUrl");
const baseUrl = Cypress.config("baseUrl");
cy.url().should(
"eq",
page
? `${baseUrl}/${workspaceId}/apps/${slug}/home`
: `${baseUrl}/${workspaceId}/apps/${slug}`
);
cy.url().should(
"eq",
page
? `${baseUrl}/${workspaceId}/apps/${slug}/home`
: `${baseUrl}/${workspaceId}/apps/${slug}`
);
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.openInCurrentTab(commonWidgetSelector.previewButton);
cy.ifEnv("Community", () => {
cy.url().should("eq", `${baseUrl}/applications/${slug}/home?version=v1`);
});
cy.ifEnv("Enterprise", () => {
cy.url().should(
"eq",
`${baseUrl}/applications/${slug}/home?env=production&version=v1`
);
});
cy.visit("/my-workspace");
cy.visitSlug({
actualUrl: `${baseUrl}/applications/${slug}`,
});
cy.url().should("eq", `${baseUrl}/applications/${slug}`);
cy.visit("/my-workspace");
cy.visitSlug({
actualUrl: `${baseUrl}/applications/${slug}`,
});
cy.url().should("eq", `${baseUrl}/applications/${slug}`);
};
export const setUpSlug = (slug) => {
cy.get(commonWidgetSelector.shareAppButton).click();
cy.clearAndType(commonWidgetSelector.appNameSlugInput, slug);
cy.get('[data-cy="app-slug-accepted-label"]')
.should("be.visible")
.and("have.text", "Slug accepted!");
cy.get(commonWidgetSelector.modalCloseButton).click();
cy.get(commonWidgetSelector.shareAppButton).click();
cy.clearAndType(commonWidgetSelector.appNameSlugInput, slug);
cy.get('[data-cy="app-slug-accepted-label"]')
.should("be.visible")
.and("have.text", "Slug accepted!");
cy.get(commonWidgetSelector.modalCloseButton).click();
};
export const setupAppWithSlug = (appName, slug) => {
cy.apiCreateApp(appName);
cy.apiAddComponentToApp(appName, "text1");
cy.apiReleaseApp(appName);
cy.apiAddAppSlug(appName, slug);
cy.apiCreateApp(appName);
cy.apiAddComponentToApp(appName, "text1");
cy.ifEnv("Enterprise", () => {
cy.openApp(
"",
Cypress.env("workspaceId"),
Cypress.env("appId"),
commonWidgetSelector.draggableWidget("text1")
);
appPromote("development", "production");
});
cy.apiReleaseApp(appName);
cy.apiAddAppSlug(appName, slug);
};
export const verifyRestrictedAccess = () => {
cy.get('[data-cy="modal-header"]').should("have.text", "Restricted access");
cy.get('[data-cy="modal-description"]')
.invoke("text")
.then((text) => {
const normalizedText = text.replace(//g, "'");
expect(normalizedText).to.equal(
"You don't have access to this app. Kindly contact admin to know more."
);
});
cy.get('[data-cy="back-to-home-button"]').verifyVisibleElement(
"have.text",
"Back to home page"
);
cy.get('[data-cy="modal-header"]').should("have.text", "Restricted access");
cy.get('[data-cy="modal-description"]')
.invoke("text")
.then((text) => {
const normalizedText = text.replace(//g, "'");
expect(normalizedText).to.equal(
"You don't have access to this app. Kindly contact admin to know more."
);
});
cy.get('[data-cy="back-to-home-button"]').verifyVisibleElement(
"have.text",
"Back to home page"
);
};
export const onboardUserFromAppLink = (
email,
slug,
workspaceName = "My workspace",
isNonExistingUser = true
email,
slug,
workspaceName = "My workspace",
isNonExistingUser = true
) => {
const dbConfig = Cypress.env("app_db");
const dbConfig = Cypress.env("app_db");
const query = isNonExistingUser
? `
const query = isNonExistingUser
? `
SELECT u.invitation_token, o.id AS workspace_id, ou.invitation_token AS organization_token
FROM users u
JOIN organization_users ou ON u.id = ou.user_id
JOIN organizations o ON ou.organization_id = o.id
WHERE u.email = '${email}' AND o.name = '${workspaceName}';
`
: `
: `
SELECT ou.invitation_token, o.id AS workspace_id
FROM users u
JOIN organization_users ou ON u.id = ou.user_id
@ -114,33 +134,33 @@ export const onboardUserFromAppLink = (
WHERE u.email = '${email}' AND o.name = '${workspaceName}';
`;
cy.task("dbConnection", { dbconfig: dbConfig, sql: query }).then((resp) => {
if (!resp.rows || resp.rows.length === 0) {
throw new Error(
`No records found for email: ${email} and workspace: ${workspaceName}`
);
}
cy.task("dbConnection", { dbconfig: dbConfig, sql: query }).then((resp) => {
if (!resp.rows || resp.rows.length === 0) {
throw new Error(
`No records found for email: ${email} and workspace: ${workspaceName}`
);
}
const { invitation_token, workspace_id, organization_token } = resp.rows[0];
const token = isNonExistingUser ? organization_token : invitation_token;
const url = isNonExistingUser
? `${Cypress.config("baseUrl")}/invitations/${invitation_token}/workspaces/${organization_token}?oid=${workspace_id}&redirectTo=%2Fapplications%2F${slug}`
: `${Cypress.config("baseUrl")}/organization-invitations/${token}?oid=${workspace_id}&redirectTo=%2Fapplications%2F${slug}`;
const { invitation_token, workspace_id, organization_token } = resp.rows[0];
const token = isNonExistingUser ? organization_token : invitation_token;
const url = isNonExistingUser
? `${Cypress.config("baseUrl")}/invitations/${invitation_token}/workspaces/${organization_token}?oid=${workspace_id}&redirectTo=%2Fapplications%2F${slug}`
: `${Cypress.config("baseUrl")}/organization-invitations/${token}?oid=${workspace_id}&redirectTo=%2Fapplications%2F${slug}`;
cy.visit(url);
});
cy.visit(url);
});
};
export const resolveHost = () => {
const baseUrl = Cypress.config("baseUrl");
const baseUrl = Cypress.config("baseUrl");
const urlMapping = {
"http://localhost:8082": "http://localhost:8082",
"http://localhost:3000": "http://localhost:3000",
"http://localhost:3000/apps": "http://localhost:3000/apps",
"http://localhost:4001": "http://localhost:3000",
"http://localhost:4001/apps": "http://localhost:3000/apps",
};
const urlMapping = {
"http://localhost:8082": "http://localhost:8082",
"http://localhost:3000": "http://localhost:3000",
"http://localhost:3000/apps": "http://localhost:3000/apps",
"http://localhost:4001": "http://localhost:3000",
"http://localhost:4001/apps": "http://localhost:3000/apps",
};
return urlMapping[baseUrl];
return urlMapping[baseUrl];
};

View file

@ -6,6 +6,7 @@ import moment from "moment";
import { dashboardSelector } from "Selectors/dashboard";
import { groupsSelector } from "Selectors/manageGroups";
import { groupsText } from "Texts/manageGroups";
import { appPromote } from "Support/utils/platform/multiEnv";
export const navigateToProfile = () => {
cy.get(commonSelectors.settingsIcon).click();
@ -48,7 +49,7 @@ export const randomDateOrTime = (format = "DD/MM/YYYY") => {
let startDate = new Date(2018, 0, 1);
startDate = new Date(
startDate.getTime() +
Math.random() * (endDate.getTime() - startDate.getTime())
Math.random() * (endDate.getTime() - startDate.getTime())
);
return moment(startDate).format(format);
};
@ -104,7 +105,7 @@ export const viewAppCardOptions = (appName) => {
cy.get(commonSelectors.appCard(appName))
.realHover()
.find(commonSelectors.appCardOptionsButton)
.realHover()
.realHover();
cy.contains("div", appName)
.parent()
.within(() => {
@ -230,6 +231,9 @@ export const navigateToworkspaceConstants = () => {
};
export const releaseApp = () => {
cy.ifEnv("Enterprise", () => {
appPromote("development", "production");
});
cy.get(commonSelectors.releaseButton).click();
cy.get(commonSelectors.yesButton).click();
cy.verifyToastMessage(commonSelectors.toastMessage, "Version v1 released");

View file

@ -92,7 +92,7 @@ export const userSignUp = (fullName, email, workspaceName = "test") => {
cy.visit(invitationLink);
cy.wait(2500);
});
if (Cypress.env("environment") !== "Community") {
if (Cypress.env("environment") == "Cloud") {
cy.clearAndType(
'[data-cy="onboarding-workspace-name-input"]',
workspaceName

View file

@ -0,0 +1,120 @@
import { multiEnvSelector, commonEeSelectors } from "Selectors/eeCommon";
import { commonSelectors, commonWidgetSelector } from "Selectors/common";
import { appVersionSelectors } from "Selectors/exportImport";
import { appVersionText } from "Texts/exportImport";
export const promoteApp = () => {
cy.get(commonEeSelectors.promoteButton).click();
cy.get(commonEeSelectors.promoteButton).eq(1).click();
cy.waitForAppLoad();
cy.wait(3000);
};
export const releaseApp = () => {
cy.get(commonSelectors.releaseButton).click();
cy.get(commonSelectors.yesButton).click();
cy.verifyToastMessage(commonSelectors.toastMessage, "Version v1 released");
cy.wait(500);
};
export const launchApp = () => {
cy.url().then((url) => {
const parts = url.split("/");
const value = parts[parts.length - 1];
cy.log(`Extracted value: ${value}`);
cy.visit(`/applications/${value}`);
cy.wait(3000);
});
};
export const appPromote = (fromEnv, toEnv) => {
const commonActions = () => {
cy.get(commonEeSelectors.promoteButton).click();
cy.get(commonEeSelectors.promoteButton).eq(1).click();
cy.waitForAppLoad();
cy.wait(2000);
};
const transitions = {
development: {
staging: commonActions,
production: () => {
commonActions();
appPromote("staging", "production");
},
release: () => {
commonActions();
commonActions();
cy.get(commonSelectors.releaseButton).click();
cy.get(commonSelectors.yesButton).click();
cy.wait(500);
},
},
staging: {
production: commonActions,
release: () => {
commonActions();
cy.get(commonSelectors.releaseButton).click();
cy.get(commonSelectors.yesButton).click();
cy.wait(500);
},
},
};
const transition = transitions[fromEnv]?.[toEnv];
transition();
};
export const createNewVersion = (value, newVersion = [], version) => {
cy.get('[data-cy="list-current-env-name"]').click();
cy.get(multiEnvSelector.envNameList).eq(0).click();
cy.get(appVersionSelectors.currentVersionField(value)).click();
cy.get(appVersionSelectors.createNewVersionButton).click();
cy.get(appVersionSelectors.createVersionInputField).click();
cy.contains(`[id*="react-select-"]`, version).click();
cy.get(appVersionSelectors.versionNameInputField).click().type(newVersion[0]);
cy.get(appVersionSelectors.createNewVersionButton).click();
cy.waitForAppLoad();
cy.verifyToastMessage(
commonSelectors.toastMessage,
appVersionText.createdToastMessage
);
cy.get(appVersionSelectors.currentVersionField(newVersion[0])).should(
"be.visible"
);
};
export const selectVersion = (value, newVersion = []) => {
cy.get(appVersionSelectors.currentVersionField(value)).click();
cy.get(".react-select__menu-list .app-version-name")
.contains(newVersion[0])
.click();
cy.waitForAppLoad();
};
export const selectEnv = (envName) => {
const envIndex = {
development: 0,
staging: 1,
production: 2,
}[envName];
const isValidEnvName = (envName) => {
return (
envName === "development" ||
envName === "staging" ||
envName === "production"
);
};
if (isValidEnvName(envName)) {
cy.get('[data-cy="list-current-env-name"]').click();
cy.wait(500)
const envSelector = `${multiEnvSelector.envNameList}:eq(${envIndex})`;
cy.get(envSelector).click();
cy.waitForAppLoad();
}
};

View file

@ -9,6 +9,7 @@ import {
} from "Selectors/version";
import { deleteVersionText, releasedVersionText } from "Texts/version";
import { verifyComponent } from "Support/utils/basicComponents";
import { appPromote } from "./platform/multiEnv";
export const navigateToCreateNewVersionModal = (value) => {
cy.get(appVersionSelectors.appVersionLabel).click();
@ -121,6 +122,9 @@ export const verifyDuplicateVersion = (newVersion = [], version) => {
};
export const releasedVersionAndVerify = (currentVersion) => {
cy.ifEnv("Enterprise", () => {
appPromote("development", "production");
});
cy.contains("Release").click();
cy.get(confirmVersionModalSelectors.yesButton).click();

View file

@ -3,15 +3,14 @@ FROM node:22.15.1 AS builder
# Fix for JS heap limit allocation issue
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN npm i -g npm@10.9.2
RUN mkdir -p /app
RUN npm cache clean --force
RUN npm i -g npm@10.9.2 && npm cache clean --force
RUN mkdir -p /app
WORKDIR /app
# Set GitHub token and branch as build arguments
ARG CUSTOM_GITHUB_TOKEN
ARG BRANCH_NAME=main
ARG BRANCH_NAME
# Clone and checkout the frontend repository
RUN git config --global url."https://x-access-token:${CUSTOM_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
@ -21,7 +20,7 @@ RUN git config --global http.postBuffer 524288000
RUN git clone https://github.com/ToolJet/ToolJet.git .
# The branch name needs to be changed the branch with modularisation in CE repo
RUN git checkout main
RUN git checkout ${BRANCH_NAME}
RUN git submodule update --init --recursive
@ -33,85 +32,61 @@ COPY ./package.json ./package.json
# Build plugins
COPY ./plugins/package.json ./plugins/package-lock.json ./plugins/
RUN npm --prefix plugins install
RUN npm --prefix plugins ci --omit=dev
COPY ./plugins/ ./plugins/
RUN NODE_ENV=production npm --prefix plugins run build
RUN npm --prefix plugins prune --production
ENV TOOLJET_EDITION=ee
RUN npm --prefix plugins prune --omit=dev
# Build frontend
COPY ./frontend/package.json ./frontend/package-lock.json ./frontend/
RUN npm --prefix frontend install
COPY ./frontend/ ./frontend/
RUN npm --prefix frontend run build --production
RUN npm --prefix frontend prune --production
RUN npm --prefix frontend run build --production && npm --prefix frontend prune --production
ENV NODE_ENV=production
ENV TOOLJET_EDITION=ee
# Build server
COPY ./server/package.json ./server/package-lock.json ./server/
RUN npm --prefix server install
RUN npm --prefix server ci --omit=dev
COPY ./server/ ./server/
RUN npm install -g @nestjs/cli
RUN npm install -g @nestjs/cli
RUN npm install -g copyfiles
RUN npm --prefix server run build
RUN npm prune --production --prefix server
FROM debian:12
RUN apt-get update -yq \
&& apt-get install curl wget gnupg zip -yq \
&& apt-get install -yq build-essential \
&& apt -y install redis \
&& apt-get clean -y
# Install required dependencies for downloading and extracting files
# Install dependencies for PostgREST, curl, unzip, etc.
RUN apt-get update && apt-get install -y \
curl tar xz-utils postgresql postgresql-contrib postgresql-client && \
apt-get clean && rm -rf /var/lib/apt/lists/*
curl ca-certificates unzip tar \
&& rm -rf /var/lib/apt/lists/*
# Install PostgREST from official Docker image
COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin
ENV POSTGREST_VERSION=v12.2.0
RUN apt-get update && apt-get install -y supervisor
RUN curl -Lo postgrest.tar.xz https://github.com/PostgREST/postgrest/releases/download/${POSTGREST_VERSION}/postgrest-v12.2.0-linux-static-x64.tar.xz && \
tar -xf postgrest.tar.xz && \
mv postgrest /postgrest && \
rm postgrest.tar.xz && \
chmod +x /postgrest
# Create supervisord configuration file
RUN echo "[supervisord]\n" \
"nodaemon=true\n" \
"\n" \
"[program:postgrest]\n" \
"command=/bin/postgrest\n" \
"autostart=true\n" \
"autorestart=true\n" \
"stdout_logfile=/dev/stdout\n" \
"stderr_logfile=/dev/stderr\n" \
"stdout_logfile_maxbytes=0\n" \
"stderr_logfile_maxbytes=0\n" \
"\n" \
"[program:neo4j]\n" \
"command=neo4j console\n" \
"autostart=true\n" \
"autorestart=unexpected\n" \
"startsecs=30\n" \
"startretries=999\n" \
"priority=90\n" \
"exitcodes=0,1,2\n" \
"stopsignal=SIGTERM\n" \
"stopasgroup=true\n" \
"killasgroup=true\n" \
"redirect_stderr=true\n" \
"stdout_logfile=/var/log/neo4j/neo4j.log\n" \
"stdout_logfile_backups=10\n" \
"stderr_capture_maxbytes=20MB\n" \
"\n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf
FROM debian:12-slim
# Create a wrapper for PostgREST to prefix its logs
RUN mv /bin/postgrest /bin/postgrest-original && \
echo '#!/bin/bash\n\
exec /bin/postgrest-original "$@" 2>&1 | sed "s/^/[PostgREST] /"\n\
' > /bin/postgrest && \
chmod +x /bin/postgrest
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
wget \
gnupg \
unzip \
ca-certificates \
xz-utils \
tar \
zip \
postgresql-client \
redis \
libaio1 \
git \
freetds-dev \
&& apt-get upgrade -y -o Dpkg::Options::="--force-confold" \
&& apt-get autoremove -y \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
RUN curl -O https://nodejs.org/dist/v22.15.1/node-v22.15.1-linux-x64.tar.xz \
@ -125,53 +100,18 @@ ENV PATH=/usr/local/lib/nodejs/bin:$PATH
ENV NODE_ENV=production
ENV TOOLJET_EDITION=ee
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN apt-get update && \
apt-get install -y postgresql-client freetds-dev libaio1 wget && \
apt-get -o Dpkg::Options::="--force-confold" upgrade -q -y --force-yes && \
apt-get -y autoremove && \
apt-get -y autoclean
# Install Neo4j
# Install Neo4j + APOC
RUN wget -O - https://debian.neo4j.com/neotechnology.gpg.key | apt-key add - && \
echo "deb https://debian.neo4j.com stable 5" > /etc/apt/sources.list.d/neo4j.list && \
apt-get update && \
apt-get install -y neo4j=1:5.26.6 && \
apt-mark hold neo4j && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Set the necessary Neo4j environment variables
ENV NEO4J_HOME=/opt/neo4j
ENV NEO4J_CONF=/etc/neo4j
ENV NEO4J_DATA=/var/lib/neo4j/data
ENV NEO4J_LOG=/var/log/neo4j
ENV NEO4J_PLUGIN=/var/lib/neo4j/plugins
ENV NEO4J_IMPORT=/var/lib/neo4j/import
# Create the necessary directories for Neo4j
RUN mkdir -p /data/db /data/logs /data/plugins
RUN mkdir -p /opt/neo4j/plugins
# Configure APOC plugin for Neo4j
ENV NEO4J_dbms_active_plugins=apoc
# Download and install APOC plugin for Neo4j 5.x (BEFORE creating user)
RUN mkdir -p /var/lib/neo4j/plugins && \
apt-get update && apt-get install -y neo4j=1:5.26.6 && apt-mark hold neo4j && \
mkdir -p /var/lib/neo4j/plugins && \
wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-core.jar && \
# Try to download extended version
(wget -P /var/lib/neo4j/plugins https://github.com/neo4j/apoc/releases/download/5.26.6/apoc-5.26.6-extended.jar || \
wget -P /var/lib/neo4j/plugins https://neo4j-contrib.github.io/neo4j-apoc-procedures/5.26.6/apoc-5.26.6-extended.jar || \
echo "Extended JAR not available, continuing with core only")
# Configure Neo4j with APOC
RUN echo "dbms.security.procedures.unrestricted=apoc.*" >> /etc/neo4j/neo4j.conf && \
echo "dbms.security.procedures.unrestricted=apoc.*" >> /etc/neo4j/neo4j.conf && \
echo "dbms.security.procedures.allowlist=apoc.*,algo.*,gds.*" >> /etc/neo4j/neo4j.conf && \
echo "dbms.directories.plugins=/var/lib/neo4j/plugins" >> /etc/neo4j/neo4j.conf
# Configure Neo4j to use authentication
RUN if [ -f "/etc/neo4j/neo4j.conf" ]; then \
sed -i '/dbms.security.auth_enabled/d' /etc/neo4j/neo4j.conf && \
echo "dbms.security.auth_enabled=true" >> /etc/neo4j/neo4j.conf; \
fi
echo "dbms.directories.plugins=/var/lib/neo4j/plugins" >> /etc/neo4j/neo4j.conf && \
echo "dbms.security.auth_enabled=true" >> /etc/neo4j/neo4j.conf && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Install Instantclient Basic Light Oracle and Dependencies
WORKDIR /opt/oracle
@ -186,40 +126,39 @@ RUN wget https://tooljet-plugins-production.s3.us-east-2.amazonaws.com/marketpla
# Set the Instant Client library paths
ENV LD_LIBRARY_PATH="/opt/oracle/instantclient_11_2:/opt/oracle/instantclient_21_10:${LD_LIBRARY_PATH}"
RUN rm -f *.zip *.key && apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /
RUN mkdir -p /app
# copy npm scripts
COPY --from=builder /app/package.json ./app/package.json
# copy plugins dependencies
COPY --from=builder /app/plugins/dist ./app/plugins/dist
COPY --from=builder /app/plugins/client.js ./app/plugins/client.js
COPY --from=builder /app/plugins/node_modules ./app/plugins/node_modules
COPY --from=builder /app/plugins/packages/common ./app/plugins/packages/common
COPY --from=builder /app/plugins/package.json ./app/plugins/package.json
# copy frontend build
COPY --from=builder /app/frontend/build ./app/frontend/build
# copy server build
COPY --from=builder /app/server/package.json ./app/server/package.json
COPY --from=builder /app/server/.version ./app/server/.version
COPY --from=builder /app/server/ee/keys ./app/server/ee/keys
COPY --from=builder /app/server/node_modules ./app/server/node_modules
COPY --from=builder /app/server/templates ./app/server/templates
COPY --from=builder /app/server/scripts ./app/server/scripts
COPY --from=builder /app/server/dist ./app/server/dist
COPY --from=builder /app/server/src/assets ./app/server/src/assets
COPY ./docker/ee/ee-entrypoint.sh ./app/server/ee-entrypoint.sh
RUN useradd --create-home --home-dir /home/appuser appuser
# Define non-sudo user
RUN useradd --create-home --home-dir /home/appuser appuser \
&& chown -R appuser:0 /app \
&& chown -R appuser:0 /home \
&& chmod u+x /app \
&& chmod u+x /home \
&& chmod -R g=u /app \
&& chmod -R g=u /home
# Use the PostgREST binary from the builder stage
COPY --from=builder --chown=appuser:0 /postgrest /usr/local/bin/postgrest
RUN mv /usr/local/bin/postgrest /usr/local/bin/postgrest-original && \
echo '#!/bin/bash\nexec /usr/local/bin/postgrest-original "$@" 2>&1 | sed "s/^/[PostgREST] /"' > /usr/local/bin/postgrest && \
chmod +x /usr/local/bin/postgrest
# Copy application with ownership set directly to avoid chown -R
COPY --from=builder --chown=appuser:0 /app/package.json ./app/package.json
COPY --from=builder --chown=appuser:0 /app/plugins/dist ./app/plugins/dist
COPY --from=builder --chown=appuser:0 /app/plugins/client.js ./app/plugins/client.js
COPY --from=builder --chown=appuser:0 /app/plugins/node_modules ./app/plugins/node_modules
COPY --from=builder --chown=appuser:0 /app/plugins/packages/common ./app/plugins/packages/common
COPY --from=builder --chown=appuser:0 /app/plugins/package.json ./app/plugins/package.json
COPY --from=builder --chown=appuser:0 /app/frontend/build ./app/frontend/build
COPY --from=builder --chown=appuser:0 /app/server/package.json ./app/server/package.json
COPY --from=builder --chown=appuser:0 /app/server/.version ./app/server/.version
COPY --from=builder --chown=appuser:0 /app/server/ee/keys ./app/server/ee/keys
COPY --from=builder --chown=appuser:0 /app/server/node_modules ./app/server/node_modules
COPY --from=builder --chown=appuser:0 /app/server/templates ./app/server/templates
COPY --from=builder --chown=appuser:0 /app/server/scripts ./app/server/scripts
COPY --from=builder --chown=appuser:0 /app/server/dist ./app/server/dist
COPY --from=builder --chown=appuser:0 /app/server/src/assets ./app/server/src/assets
COPY ./docker/ee/ee-entrypoint.sh ./app/server/ee-entrypoint.sh
RUN mkdir -p /var/lib/neo4j/data/databases /var/lib/neo4j/data/transactions /var/log/neo4j /opt/neo4j/run && \
chown -R appuser:0 /var/lib/neo4j /var/log/neo4j /etc/neo4j /opt/neo4j/run && \
@ -258,31 +197,11 @@ RUN mkdir -p /var/lib/redis /var/log/redis /etc/redis \
&& chmod g+s /var/lib/redis /var/log/redis /etc/redis \
&& chmod -R g=u /var/lib/redis /var/log/redis /etc/redis
# Set permissions for PostgREST binary
RUN chown appuser:0 /bin/postgrest && chmod u+x /bin/postgrest && chmod g=u /bin/postgrest
RUN touch /tmp/postgrest.conf \
&& chown appuser:0 /tmp/postgrest.conf \
&& chmod 640 /tmp/postgrest.conf
# Create PostgREST data, log, and configuration directories
RUN mkdir -p /var/lib/postgrest /var/log/postgrest /etc/postgrest \
&& chown -R appuser:0 /var/lib/postgrest /var/log/postgrest /etc/postgrest \
&& chmod g+s /var/lib/postgrest /var/log/postgrest /etc/postgrest \
&& chmod -R g=u /var/lib/postgrest /var/log/postgrest /etc/postgrest
ENV HOME=/home/appuser
# Installing git for simple git commands
RUN apt-get update && apt-get install -y git && apt-get clean
# Switch back to appuser
USER appuser
WORKDIR /app
# Dependencies for scripts outside nestjs
RUN npm install dotenv@10.0.0 joi@17.4.1
RUN npm cache clean --force
RUN npm install --prefix server --no-save dotenv@10.0.0 joi@17.4.1 && npm cache clean --force
ENTRYPOINT ["./server/ee-entrypoint.sh"]

View file

@ -1,15 +1,47 @@
#!/bin/bash
set -e
# Install grpcurl if not already installed
if ! command -v grpcurl &> /dev/null; then
echo "grpcurl not found, installing..."
apt update && apt install -y curl \
&& curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.8.0/grpcurl_1.8.0_linux_x86_64.tar.gz | tar -xzv -C /usr/local/bin grpcurl
fi
# Start Redis
# service redis-server start
# redis-server /etc/redis/redis.conf
service redis-server start
# Start Postgres
service postgresql start
# Start Temporal Server (SQLite configuration)
echo "Starting Temporal Server..."
/usr/bin/temporal-server -r / -c /etc/temporal/ -e temporal-server start &
# Export the PORT variable to be used by the application
export PORT=${PORT:-80}
# Start Supervisor
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf &
# Wait for Temporal Server to be ready
echo "Waiting for Temporal Server to be ready..."
sleep 10
# Check if namespace already exists
echo "Checking if Temporal namespace exists..."
if grpcurl -plaintext localhost:7233 temporal.api.workflowservice.v1.WorkflowService/ListNamespaces | grep -q '"name": "default"'; then
echo "Namespace 'default' already exists."
else
# Register the namespace if it doesn't exist
echo "Registering Temporal namespace..."
grpcurl -plaintext -d '{
"namespace": "default",
"description": "Default namespace",
"workflowExecutionRetentionPeriod": "259200s"
}' localhost:7233 temporal.api.workflowservice.v1.WorkflowService/RegisterNamespace
fi
# Run the worker process (last step)
echo "Starting worker process..."
npm run worker:prod

View file

@ -1,20 +1,20 @@
FROM tooljet/tooljet:ee-lts-latest
# Copy PostgREST executable
# Copy postgrest executable
COPY --from=postgrest/postgrest:v12.2.0 /bin/postgrest /bin
# Install PostgreSQL
# Install Postgres
USER root
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ bullseye-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN echo "deb http://deb.debian.org/debian"
RUN apt update && apt -y install postgresql-13 postgresql-client-13 supervisor
USER postgres
RUN service postgresql start && \
psql -c "create role tooljet with login superuser password 'postgres';"
USER root
# Install Redis
RUN apt update && apt -y install redis
# Create appuser home & ensure permission for supervisord and services
@ -22,6 +22,28 @@ RUN mkdir -p /var/log/supervisor /var/run/postgresql /var/lib/postgresql /var/li
chown -R appuser:appuser /etc/supervisor /var/log/supervisor /var/lib/redis && \
chown -R postgres:postgres /var/run/postgresql /var/lib/postgresql
# Install Temporal Server Binaries
RUN curl -OL https://github.com/temporalio/temporal/releases/download/v1.24.2/temporal_1.24.2_linux_amd64.tar.gz && \
tar -xzf temporal_1.24.2_linux_amd64.tar.gz && \
mv temporal-server /usr/bin/temporal-server && \
chmod +x /usr/bin/temporal-server && \
rm temporal_1.24.2_linux_amd64.tar.gz
# Install Temporal UI Server Binaries
RUN curl -OL https://github.com/temporalio/ui-server/releases/download/v2.28.0/ui-server_2.28.0_linux_amd64.tar.gz && \
tar -xzf ui-server_2.28.0_linux_amd64.tar.gz && \
mv ui-server /usr/bin/temporal-ui-server && \
chmod +x /usr/bin/temporal-ui-server && \
rm ui-server_2.28.0_linux_amd64.tar.gz
# Copy Temporal configuration files
COPY ./docker/ee/temporal-server.yaml /etc/temporal/temporal-server.yaml
COPY ./docker/ee/temporal-ui-server.yaml /etc/temporal/temporal-ui-server.yaml
# Install grpcurl
RUN apt update && apt install -y curl \
&& curl -sSL https://github.com/fullstorydev/grpcurl/releases/download/v1.8.0/grpcurl_1.8.0_linux_x86_64.tar.gz | tar -xzv -C /usr/local/bin grpcurl
# Configure Supervisor to manage PostgREST, ToolJet, and Redis
RUN echo "[supervisord] \n" \
"nodaemon=true \n" \
@ -52,8 +74,10 @@ RUN echo "[supervisord] \n" \
"stdout_logfile=/dev/stdout \n" \
"stdout_logfile_maxbytes=0 \n" | sed 's/ //' > /etc/supervisor/conf.d/supervisord.conf
# ENV defaults
ENV TOOLJET_HOST=http://localhost \
TOOLJET_SERVER_URL=http://localhost \
PORT=80 \
NODE_ENV=production \
LOCKBOX_MASTER_KEY=replace_with_lockbox_master_key \
@ -78,7 +102,14 @@ ENV TOOLJET_HOST=http://localhost \
REDIS_PORT=6379 \
REDIS_USER=default \
REDIS_PASS= \
TERM=xterm
ENABLE_MARKETPLACE_FEATURE=true \
TERM=xterm \
ENABLE_WORKFLOW_SCHEDULING=true \
TEMPORAL_SERVER_ADDRESS=localhost:7233 \
TEMPORAL_TASK_QUEUE_NAME_FOR_WORKFLOWS=tooljet-workflows \
TOOLJET_WORKFLOWS_TEMPORAL_NAMESPACE=default \
TEMPORAL_ADDRESS=localhost:7233 \
TEMPORAL_CORS_ORIGINS=http://localhost:8080
# Set the entrypoint
COPY ./docker/ee/ee-try-entrypoint-lts.sh /ee-try-entrypoint-lts.sh