diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index b5f3acd0d5..545be444d7 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -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 }} diff --git a/.github/workflows/manual-docker-build.yml b/.github/workflows/manual-docker-build.yml new file mode 100644 index 0000000000..952b5f40dd --- /dev/null +++ b/.github/workflows/manual-docker-build.yml @@ -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 }} diff --git a/cypress-tests/cypress-ee-platform.config.js b/cypress-tests/cypress-ee-platform.config.js index 25aa7f6f15..fb4f45faeb 100644 --- a/cypress-tests/cypress-ee-platform.config.js +++ b/cypress-tests/cypress-ee-platform.config.js @@ -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, diff --git a/cypress-tests/cypress.Dockerfile b/cypress-tests/cypress.Dockerfile index 32607825e7..705ae6bea6 100644 --- a/cypress-tests/cypress.Dockerfile +++ b/cypress-tests/cypress.Dockerfile @@ -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 \ diff --git a/cypress-tests/cypress/commands/apiCommands.js b/cypress-tests/cypress/commands/apiCommands.js index c8ca26fd2f..60e747a48c 100644 --- a/cypress-tests/cypress/commands/apiCommands.js +++ b/cypress-tests/cypress/commands/apiCommands.js @@ -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", ( diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appImport.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appImport.cy.js index 2bd1ccf51e..5d6353ae25 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appImport.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appImport.cy.js @@ -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) => { diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js index 796ac009b3..c4a2c674ff 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/appSlug.cy.js @@ -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); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js index 19b87c6efe..f763211bea 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/privateAndpublicApps.cy.js @@ -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); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/version.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/version.cy.js index 3432744fc3..a0241f1191 100644 --- a/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/version.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/platform/ceTestcases/apps/version.cy.js @@ -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"); + }); }); }); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/multi-env/multiEnv.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/multi-env/multiEnv.cy.js new file mode 100644 index 0000000000..4197213244 --- /dev/null +++ b/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/multi-env/multiEnv.cy.js @@ -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 won’t 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"); + }); +}); diff --git a/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/workspace/ldapOnboarding.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/workspace/ldapOnboarding.cy.js similarity index 100% rename from cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/workspace/ldapOnboarding.cy.js rename to cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/workspace/ldapOnboarding.cy.js diff --git a/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/workspace/openId.cy.js b/cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/workspace/openId.cy.js similarity index 100% rename from cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/externalApi/workspace/openId.cy.js rename to cypress-tests/cypress/e2e/happyPath/platform/eeTestcases/workspace/openId.cy.js diff --git a/cypress-tests/cypress/support/utils/apps.js b/cypress-tests/cypress/support/utils/apps.js index ff791d6d6c..4041c3ce46 100644 --- a/cypress-tests/cypress/support/utils/apps.js +++ b/cypress-tests/cypress/support/utils/apps.js @@ -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]; }; diff --git a/cypress-tests/cypress/support/utils/common.js b/cypress-tests/cypress/support/utils/common.js index 7a8c55ffcf..63f188db61 100644 --- a/cypress-tests/cypress/support/utils/common.js +++ b/cypress-tests/cypress/support/utils/common.js @@ -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"); diff --git a/cypress-tests/cypress/support/utils/onboarding.js b/cypress-tests/cypress/support/utils/onboarding.js index 182e890acd..74d5c77d3d 100644 --- a/cypress-tests/cypress/support/utils/onboarding.js +++ b/cypress-tests/cypress/support/utils/onboarding.js @@ -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 diff --git a/cypress-tests/cypress/support/utils/platform/multiEnv.js b/cypress-tests/cypress/support/utils/platform/multiEnv.js new file mode 100644 index 0000000000..be72c3ae53 --- /dev/null +++ b/cypress-tests/cypress/support/utils/platform/multiEnv.js @@ -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(); + } +}; + + diff --git a/cypress-tests/cypress/support/utils/version.js b/cypress-tests/cypress/support/utils/version.js index b15d84f45b..c6d79853fb 100644 --- a/cypress-tests/cypress/support/utils/version.js +++ b/cypress-tests/cypress/support/utils/version.js @@ -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(); diff --git a/docker/ee/ee-production.Dockerfile b/docker/ee/ee-production.Dockerfile index 337bafb476..af0de72e7d 100644 --- a/docker/ee/ee-production.Dockerfile +++ b/docker/ee/ee-production.Dockerfile @@ -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"] diff --git a/docker/ee/ee-try-entrypoint-lts.sh b/docker/ee/ee-try-entrypoint-lts.sh index 27590534d0..5143e10e75 100755 --- a/docker/ee/ee-try-entrypoint-lts.sh +++ b/docker/ee/ee-try-entrypoint-lts.sh @@ -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 diff --git a/docker/ee/ee-try-tooljet-lts.Dockerfile b/docker/ee/ee-try-tooljet-lts.Dockerfile index 5eb10b938a..1aedd58253 100644 --- a/docker/ee/ee-try-tooljet-lts.Dockerfile +++ b/docker/ee/ee-try-tooljet-lts.Dockerfile @@ -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