diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index 1be855e3fa..0000000000 --- a/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,76 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at community@budibase.com. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 120000 index 0000000000..d28e66efdc --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1 @@ +../docs/CODE_OF_CONDUCT.md \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index bd21123709..0000000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,208 +0,0 @@ -# Contributing - -From opening a bug report to creating a pull request: every contribution is appreciated and welcome. If you're planning to implement a new feature or change the api please create an issue first. This way we can ensure that your precious work is not in vain. - -## Not Sure Where to Start? - -Budibase is a low-code web application builder that creates svelte based web applications. - -Budibase is a monorepo managed by [lerna](https://github.com/lerna/lerna). Lerna manages the building and publishing of the budibase packages. At a high level, here are the packages that make up budibase. - -- **packages/builder** - contains code for the budibase builder client side svelte application. - -- **packages/client** - A module that runs in the browser responsible for reading JSON definition and creating living, breathing web apps from it. - -- **packages/server** - The budibase server. This [Koa](https://koajs.com/) app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system. - -- **packages/worker** - This [Koa](https://koajs.com/) app is responsible for providing global apis for managing your budibase installation. Authentication, Users, Email, Org and Auth configs are all provided by the worker. - -## Contributor License Agreement (CLA) - -In order to accept your pull request, we need you to submit a CLA. You only need to do this once. If you are submitting a pull request for the first time, just submit a Pull Request and our CLA Bot will give you instructions on how to sign the CLA before merging your Pull Request. - -All contributors must sign an [Individual Contributor License Agreement](https://github.com/budibase/budibase/blob/next/.github/cla/individual-cla.md). - -If contributing on behalf of your company, your company must sign a [Corporate Contributor License Agreement](https://github.com/budibase/budibase/blob/next/.github/cla/corporate-cla.md). If so, please contact us via community@budibase.com. - -## Glossary of Terms - -To understand the budibase API, it can be helpful to understand the top level entities that make up Budibase. - -### Client - -A client represents a single budibase customer. Each budibase client will have 1 or more budibase servers. Every client is assigned a unique ID. - -### App - -A client can have one or more budibase applications. Budibase applications would be things like "Developer Inventory Management" or "Goat Herder CRM". Think of a budibase application as a tree. - -### Database - -An App can have one or more databases. Keeping with our [dendrology](https://en.wikipedia.org/wiki/Dendrology) analogy - think of an database as a branch on the tree. Databases are used to keep data separate for different instances of your app. For example, if you had a CRM app, you may create a database for your US office, and a database for your Australian office. Databases allow us to support [multitenancy](https://www.gartner.com/en/information-technology/glossary/multitenancy) in budibase applications. - -### Table - -Tables in budibase are almost akin to tables in relational databases. A table may be a "Car" or an "Employee". They are the main building blocks for the creation and management of backend data in budibase. - -### View - -A View is an advanced feature in budibase that allows you to write a custom query using [MapReduce](https://pouchdb.com/guides/queries.html) queries. Views enable powerful query functionality and calculations, allowing you to do more with your data. - -### Page - -A page in budibase is actually a single, self contained svelte web app. There are only 2 pages in budibase. The **login** page and the **main** page. - -### Screen - -A screen is a component within a single page. Generally, screens represent client side routes, and can be switched without refreshing the page. - -### Component - -A component is the basic frontend building block of a budibase app. - -### Component Library - -Component libraries are collections of components as well as the definition of their props contained in a file called `components.json`. - -## Contributing to Budibase - -* Please maintain the existing code style. - -* Please try to keep your commits small and focused. - -* Please write tests. - -* If the project diverges from your branch, please rebase instead of merging. This makes the commit graph easier to read. - -* Once your work is completed, please raise a PR against the `develop` branch with some information about what has changed and why. - -### Getting Started For Contributors -#### 1. Prerequisites - -NodeJS Version `14.x.x` - -*yarn -* `npm install -g yarn` - -*jest* - `npm install -g jest` - -#### 2. Clone this repository - -`git clone https://github.com/Budibase/budibase.git` - -then `cd ` into your local copy. - -#### 3. Install and Build - -| **NOTE**: On Windows, all yarn commands must be executed on a bash shell (e.g. git bash) - -To develop the Budibase platform you'll need [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) installed. - -##### Quick method - -`yarn setup` will check that all necessary components are installed and setup the repo for usage. - -##### Manual method - -The following commands can be executed to manually get Budibase up and running (assuming Docker/Docker Compose has been installed). - -`yarn` to install project dependencies - -`yarn bootstrap` will install all budibase modules and symlink them together using lerna. - -`yarn build` will build all budibase packages. - -#### 4. Running - -To run the budibase server and builder in dev mode (i.e. with live reloading): - -1. Open a new console -2. `yarn dev` (from root) -3. Access the builder on http://localhost:10000/builder - -This will enable watch mode for both the builder app, server, client library and any component libraries. - -#### 5. Debugging using VS Code - -To debug the budibase server and worker a VS Code launch configuration has been provided. - -Visit the debug window and select `Budibase Server` or `Budibase Worker` to debug the respective component. -Alternatively to start both components simultaneously select `Start Budibase`. - -In addition to the above, the remaining budibase components may be ran in dev mode using: `yarn dev:noserver`. - -#### 6. Cleanup - -If you wish to delete all the apps created in development and reset the environment then run the following: - -1. `yarn nuke:docker` will wipe all the Budibase services -2. `yarn dev` will restart all the services - -### Backend - -For the backend we run [Redis](https://redis.io/), [CouchDB](https://couchdb.apache.org/), [MinIO](https://min.io/) and [NGINX](https://www.nginx.com/) in Docker compose. This means that to develop Budibase you will need Docker and Docker compose installed. The backend services are then ran separately as Node services with nodemon so that they can be debugged outside of Docker. - -### Data Storage - -When you are running locally, budibase stores data on disk using docker volumes. The volumes and the types of data associated with each are: - -- `redis_data` - - Sessions, email tokens -- `couchdb3_data` - - Global and app databases -- `minio_data` - - App manifest, budibase client, static assets - -### Devlopment Modes - -A combination of environment variables controls the mode that budibase runs in. -Yarn commands can be used to mimic the different modes that budibase can be ran in - -#### Self Hosted -The default mode. A single tenant installation with no usage restrictions. - -To enable this mode, use: -``` -yarn mode:self -``` - -#### Cloud -The cloud mode, with account portal turned off. - -To enable this mode, use: -``` -yarn mode:cloud -``` -#### Cloud & Account -The cloud mode, with account portal turned on. This is a replica of the mode that runs at https://budibase.app - - -To enable this mode, use: -``` -yarn mode:account -``` -### CI - An overview of the CI pipelines can be found [here](./workflows/README.md) -### Troubleshooting - -Sometimes, things go wrong. This can be due to incompatible updates on the budibase platform. To clear down your development environment and start again follow **Step 6. Cleanup**, then proceed from **Step 3. Install and Build** in the setup guide above. You should have a fresh Budibase installation. -### Running tests - -#### End-to-end Tests - -Budibase uses Cypress to run a number of E2E tests. To run the tests execute the following command in the root folder: - -``` -yarn test:e2e -``` - -Or if you are in the builder you can run `yarn cy:test`. - - -### Other Useful Information - -* The contributors are listed in [AUTHORS.md](https://github.com/Budibase/budibase/blob/master/.github/AUTHORS.md) (add yourself). - -* This project uses a modified version of the MPLv2 license, see [LICENSE](https://github.com/budibase/server/blob/master/LICENSE). - -* We use the [C4 (Collective Code Construction Contract)](https://rfc.zeromq.org/spec:42/C4/) process for contributions. - Please read this if you are unfamiliar with it. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 120000 index 0000000000..c81d5e88a2 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1 @@ +../docs/CONTRIBUTING.md \ No newline at end of file diff --git a/.github/workflows/README.md b/.github/workflows/README.md index d2fcd16bb0..c33665c964 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -6,7 +6,7 @@ Welcome to the budibase CI pipelines directory. This document details what each ## All CI Pipelines ### Note -- When running workflow dispatch jobs, ensure you always run them off the `master` branch. It defaults to `develop`, so double check before running any jobs. +- When running workflow dispatch jobs, ensure you always run them off the `master` branch. It defaults to `develop`, so double check before running any jobs. The exception to this case is the `deploy-release` job which requires the develop branch. ### Standard CI Build Job (budibase_ci.yml) Triggers: @@ -24,14 +24,14 @@ The standard CI Build job is what runs when you raise a PR to develop or master. Triggers: - Push to develop -The job responsible for building, tagging and pushing docker images out to the test and staging environments. +The job responsible for building, tagging and pushing docker images out to the test and release environments. - Installs all dependencies - builds the project - run the unit tests - publish the budibase JS packages under a prerelease tag to NPM - build, tag and push docker images under the `develop` tag to docker hub -These images will then be pulled by the test and staging environments, updating the latest automatically. Discord notifications are sent to the #infra channel when this occurs. +These images will then be pulled by the test and release environments, updating the latest automatically. Discord notifications are sent to the #infra channel when this occurs. ### Release Job (release.yml) Triggers: @@ -57,8 +57,33 @@ This job relies on the release job to have run first, so the latest image is pus - Build and release the budibase helm chart for kubernetes users - Perform a github release with the latest version. You can see previous releases here (https://github.com/Budibase/budibase/releases) +### Deploy Release (deploy-release.yml) +Triggers: +- Manual Workflow Dispatch Trigger -### Cloud Deploy (deploy-cloud.yml) +This job is responsible for deploying to our release, cloud kubernetes environment. You must run the release job first, to ensure that the latest images have been built and pushed to docker hub. After kicking off this job, the following will occur: + +- Checks out the release branch +- Pulls the latest `values.yaml` from budibase infra, a private repo containing budibases infrastructure configuration +- Gets the latest budibase version from `lerna.json`, if it hasn't been specified in the workflow when you kicked it off +- Configures AWS Credentials +- Deploys the helm chart in the budibase repo to our preproduction EKS cluster, injecting the `values.yaml` we pulled from budibase-infra +- Fires off a discord webhook in the #infra channel to show that the deployment completely successfully. + +### Deploy Preprod (deploy-preprod.yml) +Triggers: +- Manual Workflow Dispatch Trigger + +This job is responsible for deploying to our preprod, cloud kubernetes environment. You must run the release job first, to ensure that the latest images have been built and pushed to docker hub. After kicking off this job, the following will occur: + +- Checks out the master branch +- Pulls the latest `values.yaml` from budibase infra, a private repo containing budibases infrastructure configuration +- Gets the latest budibase version from `lerna.json`, if it hasn't been specified in the workflow when you kicked it off +- Configures AWS Credentials +- Deploys the helm chart in the budibase repo to our preprod EKS cluster, injecting the `values.yaml` we pulled from budibase-infra +- Fires off a discord webhook in the #infra channel to show that the deployment completely successfully. + +### Deploy Production (deploy-cloud.yml) Triggers: - Manual Workflow Dispatch Trigger @@ -90,4 +115,75 @@ This job is responsible for deploying to our production, cloud kubernetes enviro ### Rollback A Bad Cloud Deployment - Kick off cloud deploy job - Ensure you are running off master -- Enter the version number of the last known good version of budibase. For example `1.0.0` \ No newline at end of file +- Enter the version number of the last known good version of budibase. For example `1.0.0` + +## Pro + +### Installing Pro + +The pro package is always installed from source in our CI jobs. + +This is done to prevent pro needing to be published prior to CI runs in budiabse. This is required for two reasons: +- To reduce developer need to manually bump versions, i.e: + - release pro, bump pro dep in budibase, now ci can run successfully +- The cyclic dependency on backend-core, i.e: + - pro depends on backend-core + - server depends on pro + - backend-core lives in the monorepo, so it can't be released independently to be used in pro + - therefore the only option is to pull pro from source and release it as a part of the monorepo release, as if it were a mono package + +The install is performed using the same steps as local development, via the `yarn bootstrap` command, see the [Contributing Guide#Pro](../CONTRIBUTING.md#pro) + +The branch to install pro from can vary depending on ref of the commit that triggered the budibase CI job. This is done to enable branches which have changes in both the monorepo and the pro repo to have their CI pass successfully. + +This is done using the [pro/install.sh](../../scripts/pro/install.sh) script. The script will: +- Clone pro to it's default branch (`develop`) +- Check if the clone worked, on forked versions of budibase this will fail due to no access + - This is fine as the `yarn` command will install the version from NPM + - Community PRs should never touch pro so this will always work +- Checkout the `BRANCH` argument, if this fails fallback to `BASE_BRANCH` + - This enables the more complex case of a feature branch being merged to another feature branch, e.g. + - I am working on a branch `epic/stonks` which exists on budibase and pro. + - I want to merge a change to this branch in budibase from `feature/stonks-ui`, which only exists in budibase + - The base branch ensures that `epic/stonks` in pro will still be checked out for the CI run, rather than falling back to `develop` +- Run `yarn setup` to build and install dependencies + - `yarn` + - `yarn bootstrap` + - `yarn build` + - The will build .ts files, and also update the `main` and `types` of `package.json` to point to `dist` rather than src + - The build command will only ever work in CI, it is prevented in local dev + +#### `BRANCH` and `BASE_BRANCH` arguments +These arguments are supplied by the various budibase build and release pipelines +- `budibase_ci` + - `BRANCH: ${{ github.event.pull_request.head.ref }}` -> The branch being merged + - `BASE_BRANCH: ${{ github.event.pull_request.base.ref}}` -> The base branch +- `release-develop` + - `BRANCH: develop` -> always use the `develop` branch in pro +- `release` + - `BRANCH: master` -> always use the `master` branch in pro + + +### Releasing Pro +After budibase dependencies have been released we will release the new version of pro to match the release version of budibase dependencies. This is to ensure that we are always keeping the version of `backend-core` in sync in the pro package and in budibase packages. Without this we could run into scenarios where different versions are being used when installed via `yarn` inside the docker images, creating very difficult to debug cases. + +Pro is released using the [pro/release.sh](../../scripts/pro/release.sh) script. The script will: +- Inspect the `VERSION` from the `lerna.json` file in budibase +- Determine whether to use the `latest` or `develop` tag based on the command argument +- Go to pro directory + - install npm creds + - update the version of `backend-core` to be `VERSION`, the version just released by lerna + - publish to npm. Uses a `lerna publish` command, pro itself is a mono repo. + - force the version to be the same as `VERSION` to keep pro and budibase in sync + - reverts the changes to `main` and `types` in `package.json` that were made by the build step, to point back to source + - commit & push: `Prep next development iteration` +- Go to budibase + - Update to the new version of pro in `server` and `worker` so the latest pro version is used in the docker builds + - commit & push: `Update pro version to $VERSION` + + +#### `COMMAND` argument +This argument is supplied by the existing `release` and `release:develop` budibase commands, which invoke the pro release +- `release` will supply no command and default to use `latest` +- `release:develop` will supply `develop` + diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 1303d5921a..6e886f3011 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -71,7 +71,7 @@ jobs: - name: Upload to S3 if: github.ref == 'refs/heads/new-design-ui' run: | - tar -czvf new_ui.tar.gz packages/server/assets packages/server/index.html + tar -czvf new_ui.tar.gz packages/server/builder/assets packages/server/builder/index.html aws s3 cp new_ui.tar.gz s3://prod-budi-app-assets/beta:design_ui/ aws s3 cp packages/client/dist/budibase-client.js s3://prod-budi-app-assets/beta:design_ui/budibase-client.js aws cloudfront create-invalidation --distribution-id E3ELKP4RCEHVLW --paths "/beta:design_ui/*" diff --git a/.github/workflows/deploy-cloud.yaml b/.github/workflows/deploy-cloud.yaml index a05f97f097..869a88a5b3 100644 --- a/.github/workflows/deploy-cloud.yaml +++ b/.github/workflows/deploy-cloud.yaml @@ -1,4 +1,4 @@ -name: Budibase Cloud Deploy +name: Budibase Deploy Production on: workflow_dispatch: diff --git a/.github/workflows/deploy-preprod.yml b/.github/workflows/deploy-preprod.yml index ab941100a0..c3f690f568 100644 --- a/.github/workflows/deploy-preprod.yml +++ b/.github/workflows/deploy-preprod.yml @@ -1,4 +1,4 @@ -name: Budibase Release Preprod +name: Budibase Deploy Preprod on: workflow_dispatch: diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml new file mode 100644 index 0000000000..0fb8a5fea0 --- /dev/null +++ b/.github/workflows/deploy-release.yml @@ -0,0 +1,77 @@ +name: Budibase Deploy Release + +on: + workflow_dispatch: + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: eu-west-1 + + - name: Fail if branch is not develop + if: github.ref != 'refs/heads/develop' + run: | + echo "Ref is not develop, you must run this job from develop." + exit 1 + + - name: Get the latest budibase release version + id: version + run: | + release_version=$(cat lerna.json | jq -r '.version') + echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV + + - name: Tag and release Proxy service docker image + run: | + docker login -u $DOCKER_USER -p $DOCKER_PASSWORD + yarn build:docker:proxy:release + docker tag proxy-service budibase/proxy:$RELEASE_TAG + docker push budibase/proxy:$RELEASE_TAG + env: + DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} + RELEASE_TAG: k8s-release + + - name: Pull values.yaml from budibase-infra + run: | + curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \ + -H 'Accept: application/vnd.github.v3.raw' \ + -o values.release.yaml \ + -L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-release/values.yaml + wc -l values.release.yaml + + - name: Deploy to Release Environment + uses: glopezep/helm@v1.7.1 + with: + release: budibase-release + namespace: budibase + chart: charts/budibase + token: ${{ github.token }} + helm: helm3 + values: | + globals: + appVersion: develop + ingress: + enabled: true + nginx: true + value-files: >- + [ + "values.release.yaml" + ] + env: + KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}' + + - name: Discord Webhook Action + uses: tsickert/discord-webhook@v4.0.0 + with: + webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }} + content: "Release Env Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Release Env." + embed-title: ${{ env.RELEASE_VERSION }} diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index 7330a7154f..631308d945 100644 --- a/.github/workflows/release-develop.yml +++ b/.github/workflows/release-develop.yml @@ -1,5 +1,5 @@ -name: Budibase Release Staging -concurrency: release-develop +name: Budibase Prerelease +concurrency: release-prerelease on: push: @@ -22,6 +22,7 @@ env: POSTHOG_TOKEN: phc_uDYOfnFt6wAbBAXkC6STjcrTpAFiWIhqgFcsC1UVO5F INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }} PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + FEATURE_PREVIEW_URL: https://budirelease.live jobs: release: @@ -124,4 +125,4 @@ jobs: with: webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }} content: "Release Env Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Release Env." - embed-title: ${{ env.RELEASE_VERSION }} \ No newline at end of file + embed-title: ${{ env.RELEASE_VERSION }} diff --git a/.vscode/settings.json b/.vscode/settings.json index d471924fe0..4838a4fd89 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,17 @@ "editor.codeActionsOnSave": { "source.fixAll": true }, - "editor.defaultFormatter": "svelte.svelte-vscode" + "editor.defaultFormatter": "svelte.svelte-vscode", + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "debug.javascript.terminalOptions": { + "skipFiles": [ + "${workspaceFolder}/packages/backend-core/node_modules/**", + "/**" + ] + }, } diff --git a/README.md b/README.md index 17a3ab1ef2..e8c6475d90 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,7 @@ Budibase is dedicated to providing a welcoming, diverse, and harrassment-free ex ## 🙌 Contributing to Budibase From opening a bug report to creating a pull request: every contribution is appreciated and welcomed. If you're planning to implement a new feature or change the API please create an issue first. This way we can ensure your work is not in vain. +Environment setup instructions are available for [Debian](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-DEBIAN.md) and [MacOSX](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-MACOSX.md) ### Not Sure Where to Start? A good place to start contributing, is the [First time issues project](https://github.com/Budibase/budibase/projects/22). @@ -187,7 +188,7 @@ Budibase is a monorepo managed by lerna. Lerna manages the building and publishi - [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - The budibase server. This Koa app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system. -For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md) +For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/docs/CONTRIBUTING.md)

@@ -202,7 +203,7 @@ Budibase is open-source, licensed as [GPL v3](https://www.gnu.org/licenses/gpl-3 [![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase) -If you are having issues between updates of the builder, please use the guide [here](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting) to clear down your environment. +If you are having issues between updates of the builder, please use the guide [here](https://github.com/Budibase/budibase/blob/HEAD/docs/CONTRIBUTING.md#troubleshooting) to clear down your environment.

diff --git a/charts/budibase/templates/worker-service-deployment.yaml b/charts/budibase/templates/worker-service-deployment.yaml index 73bb26dad9..a7f05f3137 100644 --- a/charts/budibase/templates/worker-service-deployment.yaml +++ b/charts/budibase/templates/worker-service-deployment.yaml @@ -123,6 +123,8 @@ spec: value: {{ .Values.globals.google.clientId | quote }} - name: GOOGLE_CLIENT_SECRET value: {{ .Values.globals.google.secret | quote }} + - name: TENANT_FEATURE_FLAGS + value: {{ .Values.globals.tenantFeatureFlags | quote }} image: budibase/worker:{{ .Values.globals.appVersion }} imagePullPolicy: Always livenessProbe: diff --git a/charts/budibase/values.yaml b/charts/budibase/values.yaml index 455d3251a8..2734202fff 100644 --- a/charts/budibase/values.yaml +++ b/charts/budibase/values.yaml @@ -103,7 +103,7 @@ globals: google: clientId: "" secret: "" - automationMaxIterations: "500" + automationMaxIterations: "200" createSecrets: true # creates an internal API key, JWT secrets and redis password for you diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..1be855e3fa --- /dev/null +++ b/docs/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at community@budibase.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000000..531ed05749 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,231 @@ +# Contributing + +From opening a bug report to creating a pull request: every contribution is appreciated and welcome. If you're planning to implement a new feature or change the api please [create an issue](https://github.com/Budibase/budibase/issues/new/choose) first. This way we can ensure that your precious work is not in vain. + +## Table of contents + +- [Quick start](#quick-start) +- [Status](#status) +- [What's included](#whats-included) +- [Bugs and feature requests](#bugs-and-feature-requests) + + +## Not Sure Where to Start? + +Budibase is a low-code web application builder that creates svelte-based web applications. + +Budibase is a monorepo managed by [lerna](https://github.com/lerna/lerna). Lerna manages the building and publishing of the budibase packages. At a high level, here are the packages that make up budibase. + +- **packages/builder** - contains code for the budibase builder client side svelte application. + +- **packages/client** - A module that runs in the browser responsible for reading JSON definition and creating living, breathing web apps from it. + +- **packages/server** - The budibase server. This [Koa](https://koajs.com/) app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system. + +- **packages/worker** - This [Koa](https://koajs.com/) app is responsible for providing global apis for managing your budibase installation. Authentication, Users, Email, Org and Auth configs are all provided by the worker. + +## Contributor License Agreement (CLA) + +In order to accept your pull request, we need you to submit a CLA. You only need to do this once. If you are submitting a pull request for the first time, just submit a Pull Request and our CLA Bot will give you instructions on how to sign the CLA before merging your Pull Request. + +All contributors must sign an [Individual Contributor License Agreement](https://github.com/budibase/budibase/blob/next/.github/cla/individual-cla.md). + +If contributing on behalf of your company, your company must sign a [Corporate Contributor License Agreement](https://github.com/budibase/budibase/blob/next/.github/cla/corporate-cla.md). If so, please contact us via community@budibase.com. + +## Glossary of Terms + +To understand the budibase API, it can be helpful to understand the top level entities that make up Budibase. + +### Client + +A client represents a single budibase customer. Each budibase client will have 1 or more budibase servers. Every client is assigned a unique ID. + +### App + +A client can have one or more budibase applications. Budibase applications would be things like "Developer Inventory Management" or "Goat Herder CRM". Think of a budibase application as a tree. + +### Database + +An App can have one or more databases. Keeping with our [dendrology](https://en.wikipedia.org/wiki/Dendrology) analogy - think of an database as a branch on the tree. Databases are used to keep data separate for different instances of your app. For example, if you had a CRM app, you may create a database for your US office, and a database for your Australian office. Databases allow us to support [multitenancy](https://www.gartner.com/en/information-technology/glossary/multitenancy) in budibase applications. + +### Table + +Tables in budibase are almost akin to tables in relational databases. A table may be a "Car" or an "Employee". They are the main building blocks for the creation and management of backend data in budibase. + +### View + +A View is an advanced feature in budibase that allows you to write a custom query using [MapReduce](https://pouchdb.com/guides/queries.html) queries. Views enable powerful query functionality and calculations, allowing you to do more with your data. + +### Page + +A page in budibase is actually a single, self contained svelte web app. There are only 2 pages in budibase. The **login** page and the **main** page. + +### Screen + +A screen is a component within a single page. Generally, screens represent client side routes, and can be switched without refreshing the page. + +### Component + +A component is the basic frontend building block of a budibase app. + +### Component Library + +Component libraries are collections of components as well as the definition of their props contained in a file called `components.json`. + +## Contributing to Budibase + +* Please maintain the existing code style. + +* Please try to keep your commits small and focused. + +* Please write tests. + +* If the project diverges from your branch, please rebase instead of merging. This makes the commit graph easier to read. + +* Once your work is completed, please raise a PR against the `develop` branch with some information about what has changed and why. + +### Getting Started For Contributors +#### 1. Prerequisites + +NodeJS Version `14.x.x` + +*yarn -* `npm install -g yarn` + +*jest* - `npm install -g jest` + +#### 2. Clone this repository + +`git clone https://github.com/Budibase/budibase.git` + +then `cd ` into your local copy. + +#### 3. Install and Build + +| **NOTE**: On Windows, all yarn commands must be executed on a bash shell (e.g. git bash) + +To develop the Budibase platform you'll need [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) installed. + +##### Quick method + +`yarn setup` will check that all necessary components are installed and setup the repo for usage. + +##### Manual method + +The following commands can be executed to manually get Budibase up and running (assuming Docker/Docker Compose has been installed). + +`yarn` to install project dependencies + +`yarn bootstrap` will install all budibase modules and symlink them together using lerna. + +`yarn build` will build all budibase packages. + +#### 4. Running + +To run the budibase server and builder in dev mode (i.e. with live reloading): + +1. Open a new console +2. `yarn dev` (from root) +3. Access the builder on http://localhost:10000/builder + +This will enable watch mode for both the builder app, server, client library and any component libraries. + +#### 5. Debugging using VS Code + +To debug the budibase server and worker a VS Code launch configuration has been provided. + +Visit the debug window and select `Budibase Server` or `Budibase Worker` to debug the respective component. +Alternatively to start both components simultaneously select `Start Budibase`. + +In addition to the above, the remaining budibase components may be run in dev mode using: `yarn dev:noserver`. + +#### 6. Cleanup + +If you wish to delete all the apps created in development and reset the environment then run the following: + +1. `yarn nuke:docker` will wipe all the Budibase services +2. `yarn dev` will restart all the services + +### Backend + +For the backend we run [Redis](https://redis.io/), [CouchDB](https://couchdb.apache.org/), [MinIO](https://min.io/) and [NGINX](https://www.nginx.com/) in Docker compose. This means that to develop Budibase you will need Docker and Docker compose installed. The backend services are then run separately as Node services with nodemon so that they can be debugged outside of Docker. + +### Data Storage + +When you are running locally, budibase stores data on disk using docker volumes. The volumes and the types of data associated with each are: + +- `redis_data` + - Sessions, email tokens +- `couchdb3_data` + - Global and app databases +- `minio_data` + - App manifest, budibase client, static assets + +### Development Modes + +A combination of environment variables controls the mode budibase runs in. +Yarn commands can be used to mimic the different modes as described in the sections below: + +#### Self Hosted +The default mode. A single tenant installation with no usage restrictions. + +To enable this mode, use: +``` +yarn mode:self +``` + +#### Cloud +The cloud mode, with account portal turned off. + +To enable this mode, use: +``` +yarn mode:cloud +``` +#### Cloud & Account +The cloud mode, with account portal turned on. This is a replica of the mode that runs at https://budibase.app + + +To enable this mode, use: +``` +yarn mode:account +``` +### CI + An overview of the CI pipelines can be found [here](./workflows/README.md) + +### Pro + +@budibase/pro is the closed source package that supports licensed features in budibase. By default the package will be pulled from NPM and will not normally need to be touched in local development. If you require to update code inside the pro package it can be cloned to the same root level as budibase, e.g. + +``` +. +|_ budibase +|_ budibase-pro +``` + +Note that only budibase maintainers will be able to access the pro repo. + +The `yarn bootstrap` command can be used to replace the NPM supplied dependency with the local source aware version. This is achieved using the `yarn link` command. To see specifically how dependencies are linked see [scripts/link-dependencies.sh](../scripts/link-dependencies.sh). The same link script is used to link dependencies to account-portal in local dev. + +### Troubleshooting + +Sometimes, things go wrong. This can be due to incompatible updates on the budibase platform. To clear down your development environment and start again follow **Step 6. Cleanup**, then proceed from **Step 3. Install and Build** in the setup guide above to create a fresh Budibase installation. +### Running tests + +#### End-to-end Tests + +Budibase uses Cypress to run a number of E2E tests. To run the tests execute the following command in the root folder: + +``` +yarn test:e2e +``` + +Or if you are in the builder you can run `yarn cy:test`. + + +### Other Useful Information + +* The contributors are listed in [AUTHORS.md](https://github.com/Budibase/budibase/blob/master/.github/AUTHORS.md) (add yourself). + +* This project uses a modified version of the MPLv2 license, see [LICENSE](https://github.com/budibase/server/blob/master/LICENSE). + +* We use the [C4 (Collective Code Construction Contract)](https://rfc.zeromq.org/spec:42/C4/) process for contributions. + Please read this if you are unfamiliar with it. diff --git a/docs/DEV-SETUP-DEBIAN.md b/docs/DEV-SETUP-DEBIAN.md new file mode 100644 index 0000000000..88a124708c --- /dev/null +++ b/docs/DEV-SETUP-DEBIAN.md @@ -0,0 +1,52 @@ +## Dev Environment on Debian 11 + +### Install Node + +Budibase requires a recent version of node (14+): +``` +curl -sL https://deb.nodesource.com/setup_16.x | sudo bash - +apt -y install nodejs +node -v +``` + +### Install npm requirements + +``` +npm install -g yarn jest lerna +``` +### Install Docker and Docker Compose + +``` +apt install docker.io +pip3 install docker-compose +``` +### Clone the repo +``` +git clone https://github.com/Budibase/budibase.git +``` + +### Check Versions + +This setup process was tested on Debian 11 (bullseye) with version numbers show below. Your mileage may vary using anything else. + +- Docker: 20.10.5 +- Docker-Compose: 1.29.2 +- Node: v16.15.1 +- Yarn: 1.22.19 +- Lerna: 5.1.4 + +### Build + +``` +cd budibase +yarn setup +``` +The yarn setup command runs several build steps i.e. +``` +node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev +``` +So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose. + +The dev version will be available on port 10000 i.e. + +http://127.0.0.1:10000/builder/admin \ No newline at end of file diff --git a/docs/DEV-SETUP-MACOSX.md b/docs/DEV-SETUP-MACOSX.md new file mode 100644 index 0000000000..5606fd0d10 --- /dev/null +++ b/docs/DEV-SETUP-MACOSX.md @@ -0,0 +1,54 @@ +## Dev Environment on MAC OSX 12 (Monterey) + +### Install Homebrew + +Install instructions [here](https://brew.sh/) + +### Install Node + +Budibase requires a recent version of node (14+): +``` +brew install node npm +node -v +``` + +### Install npm requirements + +``` +npm install -g yarn jest lerna +``` +### Install Docker and Docker Compose + +``` +brew install docker docker-compose +``` +### Clone the repo +``` +git clone https://github.com/Budibase/budibase.git +``` + +### Check Versions + +This setup process was tested on Mac OSX 12 (Monterey) with version numbers shown below. Your mileage may vary using anything else. + +- Docker: 20.10.14 +- Docker-Compose: 2.6.0 +- Node: 18.3.0 +- Yarn: 1.22.19 +- Lerna: 5.1.4 + +### Build + +``` +cd budibase +yarn setup +``` +The yarn setup command runs several build steps i.e. +``` +node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev +``` +So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose. + +The dev version will be available on port 10000 i.e. + +http://127.0.0.1:10000/builder/admin \ No newline at end of file diff --git a/hosting/letsencrypt/certificate-renew.sh b/hosting/letsencrypt/certificate-renew.sh new file mode 100644 index 0000000000..df88b44322 --- /dev/null +++ b/hosting/letsencrypt/certificate-renew.sh @@ -0,0 +1,13 @@ +#!/bin/bash +CUSTOM_DOMAIN="$1" + +if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then + certbot certonly --webroot --webroot-path="/var/www/html" \ + --register-unsafely-without-email \ + --domains $CUSTOM_DOMAIN \ + --rsa-key-size 4096 \ + --agree-tos \ + --force-renewal + + nginx -s reload +fi diff --git a/hosting/letsencrypt/certificate-request.sh b/hosting/letsencrypt/certificate-request.sh new file mode 100644 index 0000000000..83f314fc88 --- /dev/null +++ b/hosting/letsencrypt/certificate-request.sh @@ -0,0 +1,23 @@ +#!/bin/bash +CUSTOM_DOMAIN="$1" +# Request from Lets Encrypt +certbot certonly --webroot --webroot-path="/var/www/html" \ + --register-unsafely-without-email \ + --domains $CUSTOM_DOMAIN \ + --rsa-key-size 4096 \ + --agree-tos \ + --force-renewal + +if (($? != 0)); then + echo "ERROR: certbot request failed for $CUSTOM_DOMAIN use http on port 80 - exiting" + exit 1 +else + cp /app/letsencrypt/options-ssl-nginx.conf /etc/letsencrypt/options-ssl-nginx.conf + cp /app/letsencrypt/ssl-dhparams.pem /etc/letsencrypt/ssl-dhparams.pem + cp /app/letsencrypt/nginx-ssl.conf /etc/nginx/sites-available/nginx-ssl.conf + sed -i "s/CUSTOM_DOMAIN/$CUSTOM_DOMAIN/g" /etc/nginx/sites-available/nginx-ssl.conf + ln -s /etc/nginx/sites-available/nginx-ssl.conf /etc/nginx/sites-enabled/nginx-ssl.conf + + echo "INFO: restart nginx after certbot request" + /etc/init.d/nginx restart +fi diff --git a/hosting/letsencrypt/nginx-ssl.conf b/hosting/letsencrypt/nginx-ssl.conf new file mode 100644 index 0000000000..50c5e0198a --- /dev/null +++ b/hosting/letsencrypt/nginx-ssl.conf @@ -0,0 +1,96 @@ +server { + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + server_name _; + ssl_certificate /etc/letsencrypt/live/CUSTOM_DOMAIN/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/CUSTOM_DOMAIN/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + client_max_body_size 1000m; + ignore_invalid_headers off; + proxy_buffering off; + # port_in_redirect off; + + location ^~ /.well-known/acme-challenge/ { + default_type "text/plain"; + root /var/www/html; + break; + } + location = /.well-known/acme-challenge/ { + return 404; + } + + location /app { + proxy_pass http://127.0.0.1:4001; + } + + location = / { + proxy_pass http://127.0.0.1:4001; + } + + location ~ ^/(builder|app_) { + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://127.0.0.1:4001; + } + + location ~ ^/api/(system|admin|global)/ { + proxy_pass http://127.0.0.1:4002; + } + + location /worker/ { + proxy_pass http://127.0.0.1:4002; + rewrite ^/worker/(.*)$ /$1 break; + } + + location /api/ { + # calls to the API are rate limited with bursting + limit_req zone=ratelimit burst=20 nodelay; + + # 120s timeout on API requests + proxy_read_timeout 120s; + proxy_connect_timeout 120s; + proxy_send_timeout 120s; + + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_pass http://127.0.0.1:4001; + } + + location /db/ { + proxy_pass http://127.0.0.1:5984; + rewrite ^/db/(.*)$ /$1 break; + } + + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 300; + proxy_http_version 1.1; + proxy_set_header Connection ""; + chunked_transfer_encoding off; + proxy_pass http://127.0.0.1:9000; + } + + client_header_timeout 60; + client_body_timeout 60; + keepalive_timeout 60; + + # gzip + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; + +} diff --git a/hosting/letsencrypt/options-ssl-nginx.conf b/hosting/letsencrypt/options-ssl-nginx.conf new file mode 100644 index 0000000000..52fdfde245 --- /dev/null +++ b/hosting/letsencrypt/options-ssl-nginx.conf @@ -0,0 +1,13 @@ +# This file contains important security parameters. If you modify this file +# manually, Certbot will be unable to automatically provide future security +# updates. Instead, Certbot will print and log an error message with a path to +# the up-to-date file that you will need to refer to when manually updating +# this file. + +ssl_session_cache shared:le_nginx_SSL:10m; +ssl_session_timeout 1440m; + +ssl_protocols TLSv1.2 TLSv1.3; +ssl_prefer_server_ciphers off; + +ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; diff --git a/hosting/letsencrypt/ssl-dhparams.pem b/hosting/letsencrypt/ssl-dhparams.pem new file mode 100644 index 0000000000..088f9673dc --- /dev/null +++ b/hosting/letsencrypt/ssl-dhparams.pem @@ -0,0 +1,8 @@ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz ++8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a +87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7 +YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi +7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD +ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg== +-----END DH PARAMETERS----- \ No newline at end of file diff --git a/hosting/scripts/build-target-paths.sh b/hosting/scripts/build-target-paths.sh new file mode 100644 index 0000000000..d1c9b5cd05 --- /dev/null +++ b/hosting/scripts/build-target-paths.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +echo ${TARGETBUILD} > /buildtarget.txt +if [[ "${TARGETBUILD}" = "aas" ]]; then + # Azure AppService uses /home for persisent data & SSH on port 2222 + mkdir -p /home/budibase/{minio,couchdb} + mkdir -p /home/budibase/couchdb/data + chown -R couchdb:couchdb /home/budibase/couchdb/ + apt update + apt-get install -y openssh-server + sed -i 's#dir=/opt/couchdb/data/search#dir=/home/budibase/couchdb/data/search#' /opt/clouseau/clouseau.ini + sed -i 's#/minio/minio server /minio &#/minio/minio server /home/budibase/minio &#' /runner.sh + sed -i 's#database_dir = ./data#database_dir = /home/budibase/couchdb/data#' /opt/couchdb/etc/default.ini + sed -i 's#view_index_dir = ./data#view_index_dir = /home/budibase/couchdb/data#' /opt/couchdb/etc/default.ini + sed -i "s/#Port 22/Port 2222/" /etc/ssh/sshd_config + /etc/init.d/ssh restart +fi diff --git a/hosting/scripts/healthcheck.sh b/hosting/scripts/healthcheck.sh new file mode 100644 index 0000000000..80f2ece0b6 --- /dev/null +++ b/hosting/scripts/healthcheck.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +healthy=true + +if [[ $(curl -Lfk -s -w "%{http_code}\n" http://localhost/ -o /dev/null) -ne 200 ]]; then + echo 'ERROR: Budibase is not running'; + healthy=false +fi + +if [[ $(curl -s -w "%{http_code}\n" http://localhost:4001/health -o /dev/null) -ne 200 ]]; then + echo 'ERROR: Budibase backend is not running'; + healthy=false +fi + +if [[ $(curl -s -w "%{http_code}\n" http://localhost:4002/health -o /dev/null) -ne 200 ]]; then + echo 'ERROR: Budibase worker is not running'; + healthy=false +fi + +if [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/ -o /dev/null) -ne 200 ]]; then + echo 'ERROR: CouchDB is not running'; + healthy=false +fi +if [[ $(redis-cli -a $REDIS_PASSWORD --no-auth-warning ping) != 'PONG' ]]; then + echo 'ERROR: Redis is down'; + healthy=false +fi +# mino, clouseau, +nginx -t -q +NGINX_STATUS=$? + +if [[ $NGINX_STATUS -gt 0 ]]; then + echo 'ERROR: Nginx config problem'; + healthy=false +fi + +if [ $healthy == true ]; then + exit 0 +else + exit 1 +fi diff --git a/hosting/single/Dockerfile b/hosting/single/Dockerfile index 79d84f70c6..5e1b0b1374 100644 --- a/hosting/single/Dockerfile +++ b/hosting/single/Dockerfile @@ -1,79 +1,93 @@ -FROM couchdb +FROM node:14-slim as build -ENV DEPLOYMENT_ENVIRONMENT=docker -ENV POSTHOG_TOKEN=phc_fg5I3nDOf6oJVMHSaycEhpPdlgS8rzXG2r6F2IpxCHS -ENV COUCHDB_PASSWORD=budibase -ENV COUCHDB_USER=budibase -ENV COUCH_DB_URL=http://budibase:budibase@localhost:5984 -ENV BUDIBASE_ENVIRONMENT=PRODUCTION -ENV MINIO_URL=http://localhost:9000 -ENV REDIS_URL=localhost:6379 -ENV WORKER_URL=http://localhost:4002 -ENV INTERNAL_API_KEY=budibase -ENV JWT_SECRET=testsecret -ENV MINIO_ACCESS_KEY=budibase -ENV MINIO_SECRET_KEY=budibase -ENV SELF_HOSTED=1 -ENV CLUSTER_PORT=10000 -ENV REDIS_PASSWORD=budibase -ENV ARCHITECTURE=amd -ENV APP_PORT=4001 -ENV WORKER_PORT=4002 +# install node-gyp dependencies +RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python -RUN apt-get update -RUN apt-get install software-properties-common wget nginx -y -RUN apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main' -RUN apt-get update +# add pin script +WORKDIR / +ADD scripts/pinVersions.js scripts/cleanup.sh ./ +RUN chmod +x /cleanup.sh + +# build server +WORKDIR /app +ADD packages/server . +RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh + +# build worker +WORKDIR /worker +ADD packages/worker . +RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh + +FROM couchdb:3.2.1 +# TARGETARCH can be amd64 or arm e.g. docker build --build-arg TARGETARCH=amd64 +ARG TARGETARCH amd64 +#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service) +# e.g. docker build --build-arg TARGETBUILD=aas .... +ARG TARGETBUILD single +ENV TARGETBUILD $TARGETBUILD + +COPY --from=build /app /app +COPY --from=build /worker /worker + +ENV \ + APP_PORT=4001 \ + ARCHITECTURE=amd \ + BUDIBASE_ENVIRONMENT=PRODUCTION \ + CLUSTER_PORT=80 \ + COUCHDB_PASSWORD=budibase \ + COUCHDB_USER=budibase \ + COUCH_DB_URL=http://budibase:budibase@localhost:5984 \ + # CUSTOM_DOMAIN=budi001.custom.com \ + DEPLOYMENT_ENVIRONMENT=docker \ + INTERNAL_API_KEY=budibase \ + JWT_SECRET=testsecret \ + MINIO_ACCESS_KEY=budibase \ + MINIO_SECRET_KEY=budibase \ + MINIO_URL=http://localhost:9000 \ + POSTHOG_TOKEN=phc_fg5I3nDOf6oJVMHSaycEhpPdlgS8rzXG2r6F2IpxCHS \ + REDIS_PASSWORD=budibase \ + REDIS_URL=localhost:6379 \ + SELF_HOSTED=1 \ + TARGETBUILD=$TARGETBUILD \ + WORKER_PORT=4002 \ + WORKER_URL=http://localhost:4002 + +# install base dependencies +RUN apt-get update && \ + apt-get install -y software-properties-common wget nginx && \ + apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main' && \ + apt-get update + +# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx +WORKDIR /nodejs +RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh && \ + bash /tmp/nodesource_setup.sh && \ + apt-get install -y libaio1 nodejs nginx openjdk-8-jdk redis-server unzip && \ + npm install --global yarn pm2 # setup nginx ADD hosting/single/nginx.conf /etc/nginx -RUN mkdir /etc/nginx/logs -RUN useradd www -RUN touch /etc/nginx/logs/error.log -RUN touch /etc/nginx/logs/nginx.pid +ADD hosting/single/nginx-default-site.conf /etc/nginx/sites-enabled/default +RUN mkdir -p /var/log/nginx && \ + touch /var/log/nginx/error.log && \ + touch /var/run/nginx.pid -# install java -RUN apt-get install openjdk-8-jdk -y - -# setup nodejs -WORKDIR /nodejs -RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh -RUN bash /tmp/nodesource_setup.sh -RUN apt-get install nodejs -RUN npm install --global yarn -RUN npm install --global pm2 - -# setup redis -RUN apt install redis-server -y - -# setup server -WORKDIR /app -ADD packages/server . -RUN ls -al -RUN yarn -RUN yarn build -# Install client for oracle datasource -RUN apt-get install unzip libaio1 -RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh - -# setup worker -WORKDIR /worker -ADD packages/worker . -RUN yarn -RUN yarn build +WORKDIR / +RUN mkdir -p scripts/integrations/oracle +ADD packages/server/scripts/integrations/oracle scripts/integrations/oracle +RUN /bin/bash -e ./scripts/integrations/oracle/instantclient/linux/install.sh # setup clouseau WORKDIR / -RUN wget https://github.com/cloudant-labs/clouseau/releases/download/2.21.0/clouseau-2.21.0-dist.zip -RUN unzip clouseau-2.21.0-dist.zip -RUN mv clouseau-2.21.0 /opt/clouseau -RUN rm clouseau-2.21.0-dist.zip +RUN wget https://github.com/cloudant-labs/clouseau/releases/download/2.21.0/clouseau-2.21.0-dist.zip && \ + unzip clouseau-2.21.0-dist.zip && \ + mv clouseau-2.21.0 /opt/clouseau && \ + rm clouseau-2.21.0-dist.zip WORKDIR /opt/clouseau RUN mkdir ./bin ADD hosting/single/clouseau ./bin/ -ADD hosting/single/log4j.properties . -ADD hosting/single/clouseau.ini . +ADD hosting/single/log4j.properties hosting/single/clouseau.ini ./ RUN chmod +x ./bin/clouseau # setup CouchDB @@ -82,18 +96,49 @@ ADD hosting/single/vm.args ./etc/ # setup minio WORKDIR /minio -RUN wget https://dl.min.io/server/minio/release/linux-${ARCHITECTURE}64/minio -RUN chmod +x minio +ADD scripts/install-minio.sh ./install.sh +RUN chmod +x install.sh && ./install.sh # setup runner file WORKDIR / ADD hosting/single/runner.sh . RUN chmod +x ./runner.sh +ADD hosting/scripts/healthcheck.sh . +RUN chmod +x ./healthcheck.sh -EXPOSE 10000 +ADD hosting/scripts/build-target-paths.sh . +RUN chmod +x ./build-target-paths.sh + +# For Azure App Service install SSH & point data locations to /home +RUN /build-target-paths.sh + +# cleanup cache +RUN yarn cache clean -f + +EXPOSE 80 +EXPOSE 443 VOLUME /opt/couchdb/data VOLUME /minio +# setup letsencrypt certificate +RUN apt-get install -y certbot python3-certbot-nginx +ADD hosting/letsencrypt /app/letsencrypt +RUN chmod +x /app/letsencrypt/certificate-request.sh /app/letsencrypt/certificate-renew.sh +# Remove cached files +RUN rm -rf \ + /root/.cache \ + /root/.npm \ + /root/.pip \ + /usr/local/share/doc \ + /usr/share/doc \ + /usr/share/man \ + /var/lib/apt/lists/* \ + /tmp/* + +HEALTHCHECK --interval=15s --timeout=15s --start-period=45s CMD "/healthcheck.sh" + # must set this just before running ENV NODE_ENV=production +WORKDIR / + CMD ["./runner.sh"] diff --git a/hosting/single/README.md b/hosting/single/README.md new file mode 100644 index 0000000000..1147d55c89 --- /dev/null +++ b/hosting/single/README.md @@ -0,0 +1,112 @@ +# Docker Single Image for Budibase + +## Overview +As an alternative to running several docker containers via docker-compose, the files under ./hosting/single can be used to build a docker image containing all of the Budibase components (minio, couch, clouseau etc). +We call this the 'single image' container as the Dockerfile adds all the components to a single docker image. + +## Usage + +- Amend Environment Variables +- Build Requirements +- Build the Image +- Run the Container + +### Amend Environment Variables + +Edit the Dockerfile in this directory amending the environment variables to suit your usage. Pay particular attention to changing passwords. +The CUSTOM_DOMAIN variable will be used to request a certificate from LetsEncrypt and if successful you can point traffic to port 443. If you choose to use the CUSTOM_DOMAIN variable ensure that the DNS for your custom domain points to the public IP address where you are running Budibase - otherwise the certificate issuance will fail. +If you have other arrangements for a proxy in front of the single image container you can omit the CUSTOM_DOMAIN environment variable and the request to LetsEncrypt will be skipped. You can then point traffic to port 80. + +### Build Requirements +We would suggest building the image with 6GB of RAM and 20GB of free disk space for build artifacts. The resulting image size will use approx 2GB of disk space. + +### Build the Image +The guidance below is based on building the Budibase single image on Debian 11 and AlmaLinux 8. If you use another distro or OS you will need to amend the commands to suit. +#### Install Node +Budibase requires a more recent version of node (14+) than is available in the base Debian repos so: + +``` +curl -sL https://deb.nodesource.com/setup_16.x | sudo bash - +apt install -y nodejs +node -v +``` +Install yarn and lerna: +``` +npm install -g yarn jest lerna +``` +#### Install Docker + +``` +apt install -y docker.io +``` + +Check the versions of each installed version. This process was tested with the version numbers below so YMMV using anything else: + +- Docker: 20.10.5 +- node: 16.15.1 +- yarn: 1.22.19 +- lerna: 5.1.4 + +#### Get the Code +Clone the Budibase repo +``` +git clone https://github.com/Budibase/budibase.git +cd budibase +``` +#### Setup Node +Node setup: +``` +node ./hosting/scripts/setup.js +yarn +yarn bootstrap +yarn build +``` +#### Build Image +The following yarn command does some prep and then runs the docker build command: +``` +yarn build:docker:single +``` +If the docker build step fails try running that step again manually with: +``` +docker build --build-arg TARGETARCH=amd --no-cache -t budibase:latest -f ./hosting/single/Dockerfile . +``` + +#### Azure App Services +Azure have some specific requirements for running a container in their App Service. Specifically, installation of SSH to port 2222 and data storage under /home. If you would like to build a budibase container for Azure App Service add the build argument shown below setting it to 'aas'. You can remove the CUSTOM_DOMAIN env variable from the Dockerfile too as Azure terminate SSL before requests reach the container. +``` +docker build --build-arg TARGETARCH=amd --build-arg TARGETBUILD=aas -t budibase:latest -f ./hosting/single/Dockerfile . +``` + +### Run the Container +``` +docker run -d -p 80:80 -p 443:443 --name budibase budibase:latest +``` +Where: +- -d runs the container in detached mode +- -p forwards ports from your host to the ports inside the container. If you are already using port 80 on your host for something else you can try running with an alternative port e.g. `-p 8080:80` +- --name is the name for the container as shown in `docker ps` and can be used with other docker commands e.g. `docker restart budibase` + +When the container runs you should be able to access the container over http at your host address e.g. http://1.2.3.4/ or using your custom domain e.g. https://my.custom.domain/ + +When the Budibase UI appears you will be prompted to create an account to get started. + +### Podman +The single image container builds fine when using podman in place of docker. You may be prompted for the registry to use for the CouchDB image and the HEALTHCHECK parameter is not OCI compliant so is ignored. + +### Check +There are many things that could go wrong so if your container is not building or running as expected please check the following before opening a support issue. +Verify the healthcheck status of the container: +``` +docker ps +``` +Check the container logs: +``` +docker logs budibase +``` + +### Support +This single image build is still a work-in-progress so if you open an issue please provide the following information: +- The OS and OS version you are building on +- The versions you are using of docker, docker-compose, yarn, node, lerna +- For build errors please provide zipped output +- For container errors please provide zipped container logs diff --git a/hosting/single/nginx-default-site.conf b/hosting/single/nginx-default-site.conf new file mode 100644 index 0000000000..964313fa73 --- /dev/null +++ b/hosting/single/nginx-default-site.conf @@ -0,0 +1,94 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + client_max_body_size 1000m; + ignore_invalid_headers off; + proxy_buffering off; + # port_in_redirect off; + + location ^~ /.well-known/acme-challenge/ { + default_type "text/plain"; + root /var/www/html; + break; + } + location = /.well-known/acme-challenge/ { + return 404; + } + + location /app { + proxy_pass http://127.0.0.1:4001; + } + + location = / { + proxy_pass http://127.0.0.1:4001; + } + + location ~ ^/(builder|app_) { + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://127.0.0.1:4001; + } + + location ~ ^/api/(system|admin|global)/ { + proxy_pass http://127.0.0.1:4002; + } + + location /worker/ { + proxy_pass http://127.0.0.1:4002; + rewrite ^/worker/(.*)$ /$1 break; + } + + location /api/ { + # calls to the API are rate limited with bursting + limit_req zone=ratelimit burst=20 nodelay; + + # 120s timeout on API requests + proxy_read_timeout 120s; + proxy_connect_timeout 120s; + proxy_send_timeout 120s; + + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_pass http://127.0.0.1:4001; + } + + location /db/ { + proxy_pass http://127.0.0.1:5984; + rewrite ^/db/(.*)$ /$1 break; + } + + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 300; + proxy_http_version 1.1; + proxy_set_header Connection ""; + chunked_transfer_encoding off; + proxy_pass http://127.0.0.1:9000; + } + + client_header_timeout 60; + client_body_timeout 60; + keepalive_timeout 60; + + # gzip + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; + + + +} diff --git a/hosting/single/nginx.conf b/hosting/single/nginx.conf index 86938ced4e..1e5d1c20d2 100644 --- a/hosting/single/nginx.conf +++ b/hosting/single/nginx.conf @@ -1,6 +1,6 @@ -user www www; -error_log /etc/nginx/logs/error.log; -pid /etc/nginx/logs/nginx.pid; +user www-data www-data; +error_log /var/log/nginx/error.log; +pid /var/run/nginx.pid; worker_processes auto; worker_rlimit_nofile 8192; @@ -32,85 +32,6 @@ http { default "upgrade"; } - server { - listen 10000 default_server; - listen [::]:10000 default_server; - server_name _; - client_max_body_size 1000m; - ignore_invalid_headers off; - proxy_buffering off; - # port_in_redirect off; + include /etc/nginx/sites-enabled/*; - location /app { - proxy_pass http://127.0.0.1:4001; - } - - location = / { - proxy_pass http://127.0.0.1:4001; - } - - location ~ ^/(builder|app_) { - proxy_http_version 1.1; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_pass http://127.0.0.1:4001; - } - - location ~ ^/api/(system|admin|global)/ { - proxy_pass http://127.0.0.1:4002; - } - - location /worker/ { - proxy_pass http://127.0.0.1:4002; - rewrite ^/worker/(.*)$ /$1 break; - } - - location /api/ { - # calls to the API are rate limited with bursting - limit_req zone=ratelimit burst=20 nodelay; - - # 120s timeout on API requests - proxy_read_timeout 120s; - proxy_connect_timeout 120s; - proxy_send_timeout 120s; - - proxy_http_version 1.1; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_pass http://127.0.0.1:4001; - } - - location /db/ { - proxy_pass http://127.0.0.1:5984; - rewrite ^/db/(.*)$ /$1 break; - } - - location / { - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_connect_timeout 300; - proxy_http_version 1.1; - proxy_set_header Connection ""; - chunked_transfer_encoding off; - proxy_pass http://127.0.0.1:9000; - } - - client_header_timeout 60; - client_body_timeout 60; - keepalive_timeout 60; - - # gzip - gzip on; - gzip_vary on; - gzip_proxied any; - gzip_comp_level 6; - gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; - } } diff --git a/hosting/single/runner.sh b/hosting/single/runner.sh index fab8431796..6f3d247842 100644 --- a/hosting/single/runner.sh +++ b/hosting/single/runner.sh @@ -2,6 +2,15 @@ redis-server --requirepass $REDIS_PASSWORD & /opt/clouseau/bin/clouseau & /minio/minio server /minio & /docker-entrypoint.sh /opt/couchdb/bin/couchdb & +/etc/init.d/nginx restart +if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then + # Add monthly cron job to renew certbot certificate + echo -n "* * 2 * * root exec /app/letsencrypt/certificate-renew.sh ${CUSTOM_DOMAIN}" >> /etc/cron.d/certificate-renew + chmod +x /etc/cron.d/certificate-renew + # Request the certbot certificate + /app/letsencrypt/certificate-request.sh ${CUSTOM_DOMAIN} +fi + /etc/init.d/nginx restart pushd app pm2 start --name app "yarn run:docker" @@ -10,7 +19,6 @@ pushd worker pm2 start --name worker "yarn run:docker" popd sleep 10 -URL=http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984 -curl -X PUT ${URL}/_users -curl -X PUT ${URL}/_replicator -sleep infinity \ No newline at end of file +curl -X PUT ${COUCH_DB_URL}/_users +curl -X PUT ${COUCH_DB_URL}/_replicator +sleep infinity diff --git a/hosting/single/test.sh b/hosting/single/test.sh new file mode 100755 index 0000000000..c7ef53f994 --- /dev/null +++ b/hosting/single/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash +id=$(docker run -t -d -p 80:80 budibase:latest) +docker exec -it $id bash +docker kill $id diff --git a/package.json b/package.json index 014cafcfc1..9c35af497f 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -", "build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild", "build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -", + "build:docker:single:multiarch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/single/Dockerfile -t budibase:latest .", "build:docker:single:image": "docker build -f hosting/single/Dockerfile -t budibase:latest .", "build:docker:single": "lerna run build && lerna run predocker && npm run build:docker:single:image", "build:docs": "lerna run build:docs", diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 0fb0350367..915ba63eaa 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -20,6 +20,7 @@ "test:watch": "jest --watchAll" }, "dependencies": { + "@budibase/types": "^1.0.218", "@techpass/passport-openidconnect": "0.3.2", "aws-sdk": "2.1030.0", "bcrypt": "5.0.1", @@ -35,6 +36,7 @@ "passport-google-oauth": "2.0.0", "passport-jwt": "4.0.0", "passport-local": "1.0.0", + "passport-oauth2-refresh": "^2.1.0", "posthog-node": "1.3.0", "pouchdb": "7.3.0", "pouchdb-find": "7.2.2", @@ -57,7 +59,6 @@ ] }, "devDependencies": { - "@budibase/types": "^1.0.218", "@shopify/jest-koa-mocks": "3.1.5", "@types/jest": "27.5.1", "@types/koa": "2.0.52", diff --git a/packages/backend-core/src/auth.js b/packages/backend-core/src/auth.js index b13cd932c6..b6d6a2027f 100644 --- a/packages/backend-core/src/auth.js +++ b/packages/backend-core/src/auth.js @@ -2,6 +2,9 @@ const passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy const { getGlobalDB } = require("./tenancy") +const refresh = require("passport-oauth2-refresh") +const { Configs } = require("./constants") +const { getScopedConfig } = require("./db/utils") const { jwt, local, @@ -12,6 +15,7 @@ const { tenancy, appTenancy, authError, + ssoCallbackUrl, csrf, internalApi, } = require("./middleware") @@ -34,6 +38,122 @@ passport.deserializeUser(async (user, done) => { } }) +async function refreshOIDCAccessToken(db, chosenConfig, refreshToken) { + const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig) + let enrichedConfig + let strategy + + try { + enrichedConfig = await oidc.fetchStrategyConfig(chosenConfig, callbackUrl) + if (!enrichedConfig) { + throw new Error("OIDC Config contents invalid") + } + strategy = await oidc.strategyFactory(enrichedConfig) + } catch (err) { + console.error(err) + throw new Error("Could not refresh OAuth Token") + } + + refresh.use(strategy, { + setRefreshOAuth2() { + return strategy._getOAuth2Client(enrichedConfig) + }, + }) + + return new Promise(resolve => { + refresh.requestNewAccessToken( + Configs.OIDC, + refreshToken, + (err, accessToken, refreshToken, params) => { + resolve({ err, accessToken, refreshToken, params }) + } + ) + }) +} + +async function refreshGoogleAccessToken(db, config, refreshToken) { + let callbackUrl = await google.getCallbackUrl(db, config) + + let strategy + try { + strategy = await google.strategyFactory(config, callbackUrl) + } catch (err) { + console.error(err) + throw new Error("Error constructing OIDC refresh strategy", err) + } + + refresh.use(strategy) + + return new Promise(resolve => { + refresh.requestNewAccessToken( + Configs.GOOGLE, + refreshToken, + (err, accessToken, refreshToken, params) => { + resolve({ err, accessToken, refreshToken, params }) + } + ) + }) +} + +async function refreshOAuthToken(refreshToken, configType, configId) { + const db = getGlobalDB() + + const config = await getScopedConfig(db, { + type: configType, + group: {}, + }) + + let chosenConfig = {} + let refreshResponse + if (configType === Configs.OIDC) { + // configId - retrieved from cookie. + chosenConfig = config.configs.filter(c => c.uuid === configId)[0] + if (!chosenConfig) { + throw new Error("Invalid OIDC configuration") + } + refreshResponse = await refreshOIDCAccessToken( + db, + chosenConfig, + refreshToken + ) + } else { + chosenConfig = config + refreshResponse = await refreshGoogleAccessToken( + db, + chosenConfig, + refreshToken + ) + } + + return refreshResponse +} + +async function updateUserOAuth(userId, oAuthConfig) { + const details = { + accessToken: oAuthConfig.accessToken, + refreshToken: oAuthConfig.refreshToken, + } + + try { + const db = getGlobalDB() + const dbUser = await db.get(userId) + + //Do not overwrite the refresh token if a valid one is not provided. + if (typeof details.refreshToken !== "string") { + delete details.refreshToken + } + + dbUser.oauth2 = { + ...dbUser.oauth2, + ...details, + } + + await db.put(dbUser) + } catch (e) { + console.error("Could not update OAuth details for current user", e) + } +} + module.exports = { buildAuthMiddleware: authenticated, passport, @@ -46,4 +166,7 @@ module.exports = { authError, buildCsrfMiddleware: csrf, internalApi, + refreshOAuthToken, + updateUserOAuth, + ssoCallbackUrl, } diff --git a/packages/backend-core/src/context/index.js b/packages/backend-core/src/context/index.js index a69a381f0f..bd4d857ef2 100644 --- a/packages/backend-core/src/context/index.js +++ b/packages/backend-core/src/context/index.js @@ -314,6 +314,7 @@ function getContextDB(key, opts) { toUseAppId = getDevelopmentAppID(appId) break } + db = dangerousGetDB(toUseAppId, opts) try { cls.setOnContext(key, db) diff --git a/packages/backend-core/src/db/constants.js b/packages/backend-core/src/db/constants.js deleted file mode 100644 index 10c6e174d7..0000000000 --- a/packages/backend-core/src/db/constants.js +++ /dev/null @@ -1,41 +0,0 @@ -exports.SEPARATOR = "_" - -const PRE_APP = "app" -const PRE_DEV = "dev" - -exports.DocumentTypes = { - USER: "us", - WORKSPACE: "workspace", - CONFIG: "config", - TEMPLATE: "template", - APP: PRE_APP, - DEV: PRE_DEV, - APP_DEV: `${PRE_APP}${exports.SEPARATOR}${PRE_DEV}`, - APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`, - ROLE: "role", - MIGRATIONS: "migrations", - DEV_INFO: "devinfo", -} - -exports.StaticDatabases = { - GLOBAL: { - name: "global-db", - docs: { - apiKeys: "apikeys", - usageQuota: "usage_quota", - licenseInfo: "license_info", - }, - }, - // contains information about tenancy and so on - PLATFORM_INFO: { - name: "global-info", - docs: { - tenants: "tenants", - install: "install", - }, - }, -} - -exports.APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR -exports.APP_DEV = exports.APP_DEV_PREFIX = - exports.DocumentTypes.APP_DEV + exports.SEPARATOR diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/db/constants.ts new file mode 100644 index 0000000000..be0e824e61 --- /dev/null +++ b/packages/backend-core/src/db/constants.ts @@ -0,0 +1,58 @@ +export const SEPARATOR = "_" +export const UNICODE_MAX = "\ufff0" + +/** + * Can be used to create a few different forms of querying a view. + */ +export enum AutomationViewModes { + ALL = "all", + AUTOMATION = "automation", + STATUS = "status", +} + +export enum ViewNames { + USER_BY_EMAIL = "by_email", + BY_API_KEY = "by_api_key", + USER_BY_BUILDERS = "by_builders", + LINK = "by_link", + ROUTING = "screen_routes", + AUTOMATION_LOGS = "automation_logs", +} + +export enum DocumentTypes { + USER = "us", + WORKSPACE = "workspace", + CONFIG = "config", + TEMPLATE = "template", + APP = "app", + DEV = "dev", + APP_DEV = "app_dev", + APP_METADATA = "app_metadata", + ROLE = "role", + MIGRATIONS = "migrations", + DEV_INFO = "devinfo", + AUTOMATION_LOG = "log_au", +} + +export const StaticDatabases = { + GLOBAL: { + name: "global-db", + docs: { + apiKeys: "apikeys", + usageQuota: "usage_quota", + licenseInfo: "license_info", + }, + }, + // contains information about tenancy and so on + PLATFORM_INFO: { + name: "global-info", + docs: { + tenants: "tenants", + install: "install", + }, + }, +} + +export const APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR +export const APP_DEV = exports.DocumentTypes.APP_DEV + exports.SEPARATOR +export const APP_DEV_PREFIX = APP_DEV diff --git a/packages/backend-core/src/db/pouch.js b/packages/backend-core/src/db/pouch.js index 76390ac644..59b7ff8ae7 100644 --- a/packages/backend-core/src/db/pouch.js +++ b/packages/backend-core/src/db/pouch.js @@ -1,21 +1,42 @@ const PouchDB = require("pouchdb") const env = require("../environment") -function getUrlInfo() { - let url = env.COUCH_DB_URL - let username, password, host - const [protocol, rest] = url.split("://") - if (url.includes("@")) { - const hostParts = rest.split("@") - host = hostParts[1] - const authParts = hostParts[0].split(":") - username = authParts[0] - password = authParts[1] - } else { - host = rest +exports.getUrlInfo = (url = env.COUCH_DB_URL) => { + let cleanUrl, username, password, host + if (url) { + // Ensure the URL starts with a protocol + const protoRegex = /^https?:\/\//i + if (!protoRegex.test(url)) { + url = `http://${url}` + } + + // Split into protocol and remainder + const split = url.split("://") + const protocol = split[0] + const rest = split.slice(1).join("://") + + // Extract auth if specified + if (url.includes("@")) { + // Split into host and remainder + let parts = rest.split("@") + host = parts[parts.length - 1] + let auth = parts.slice(0, -1).join("@") + + // Split auth into username and password + if (auth.includes(":")) { + const authParts = auth.split(":") + username = authParts[0] + password = authParts.slice(1).join(":") + } else { + username = auth + } + } else { + host = rest + } + cleanUrl = `${protocol}://${host}` } return { - url: `${protocol}://${host}`, + url: cleanUrl, auth: { username, password, @@ -24,7 +45,7 @@ function getUrlInfo() { } exports.getCouchInfo = () => { - const urlInfo = getUrlInfo() + const urlInfo = exports.getUrlInfo() let username let password if (env.COUCH_DB_USERNAME) { diff --git a/packages/backend-core/src/db/tests/pouch.spec.js b/packages/backend-core/src/db/tests/pouch.spec.js new file mode 100644 index 0000000000..30cdd0f5ec --- /dev/null +++ b/packages/backend-core/src/db/tests/pouch.spec.js @@ -0,0 +1,62 @@ +require("../../../tests/utilities/TestConfiguration") +const getUrlInfo = require("../pouch").getUrlInfo + +describe("pouch", () => { + describe("Couch DB URL parsing", () => { + it("should handle a null Couch DB URL", () => { + const info = getUrlInfo(null) + expect(info.url).toBeUndefined() + expect(info.auth.username).toBeUndefined() + }) + it("should be able to parse a basic Couch DB URL", () => { + const info = getUrlInfo("http://host.com") + expect(info.url).toBe("http://host.com") + expect(info.auth.username).toBeUndefined() + }) + it("should be able to parse a Couch DB basic URL with HTTPS", () => { + const info = getUrlInfo("https://host.com") + expect(info.url).toBe("https://host.com") + expect(info.auth.username).toBeUndefined() + }) + it("should be able to parse a basic Couch DB URL with a custom port", () => { + const info = getUrlInfo("https://host.com:1234") + expect(info.url).toBe("https://host.com:1234") + expect(info.auth.username).toBeUndefined() + }) + it("should be able to parse a Couch DB URL with auth", () => { + const info = getUrlInfo("https://user:pass@host.com:1234") + expect(info.url).toBe("https://host.com:1234") + expect(info.auth.username).toBe("user") + expect(info.auth.password).toBe("pass") + }) + it("should be able to parse a Couch DB URL with auth and special chars", () => { + const info = getUrlInfo("https://user:s:p@s://@://:d@;][~s@host.com:1234") + expect(info.url).toBe("https://host.com:1234") + expect(info.auth.username).toBe("user") + expect(info.auth.password).toBe("s:p@s://@://:d@;][~s") + }) + it("should be able to parse a Couch DB URL without a protocol", () => { + const info = getUrlInfo("host.com:1234") + expect(info.url).toBe("http://host.com:1234") + expect(info.auth.username).toBeUndefined() + }) + it("should be able to parse a Couch DB URL with auth and without a protocol", () => { + const info = getUrlInfo("user:s:p@s://@://:d@;][~s@host.com:1234") + expect(info.url).toBe("http://host.com:1234") + expect(info.auth.username).toBe("user") + expect(info.auth.password).toBe("s:p@s://@://:d@;][~s") + }) + it("should be able to parse a Couch DB URL with only username auth", () => { + const info = getUrlInfo("https://user@host.com:1234") + expect(info.url).toBe("https://host.com:1234") + expect(info.auth.username).toBe("user") + expect(info.auth.password).toBeUndefined() + }) + it("should be able to parse a Couch DB URL with only username auth and without a protocol", () => { + const info = getUrlInfo("user@host.com:1234") + expect(info.url).toBe("http://host.com:1234") + expect(info.auth.username).toBe("user") + expect(info.auth.password).toBeUndefined() + }) + }) +}) diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index dc7a0454c3..ba3f1dd3e9 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -1,7 +1,7 @@ import { newid } from "../hashing" import { DEFAULT_TENANT_ID, Configs } from "../constants" import env from "../environment" -import { SEPARATOR, DocumentTypes } from "./constants" +import { SEPARATOR, DocumentTypes, UNICODE_MAX, ViewNames } from "./constants" import { getTenantId, getGlobalDBName, getGlobalDB } from "../tenancy" import fetch from "node-fetch" import { doWithDB, allDbs } from "./index" @@ -12,14 +12,6 @@ import { isDevApp, isDevAppID } from "./conversions" import { APP_PREFIX } from "./constants" import * as events from "../events" -const UNICODE_MAX = "\ufff0" - -export const ViewNames = { - USER_BY_EMAIL: "by_email", - BY_API_KEY: "by_api_key", - USER_BY_BUILDERS: "by_builders", -} - export * from "./constants" export * from "./conversions" export { default as Replication } from "./Replication" @@ -63,6 +55,13 @@ export function getDocParams( } } +/** + * Retrieve the correct index for a view based on default design DB. + */ +export function getQueryIndex(viewName: ViewNames) { + return `database/${viewName}` +} + /** * Generates a new workspace ID. * @returns {string} The new workspace ID which the workspace doc can be stored under. @@ -93,13 +92,17 @@ export function generateGlobalUserID(id?: any) { /** * Gets parameters for retrieving users. */ -export function getGlobalUserParams(globalId: any, otherProps = {}) { +export function getGlobalUserParams(globalId: any, otherProps: any = {}) { if (!globalId) { globalId = "" } + const startkey = otherProps?.startkey return { ...otherProps, - startkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}`, + // need to include this incase pagination + startkey: startkey + ? startkey + : `${DocumentTypes.USER}${SEPARATOR}${globalId}`, endkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`, } } @@ -384,7 +387,9 @@ export const getScopedFullConfig = async function ( if (type === Configs.SETTINGS) { if (scopedConfig && scopedConfig.doc) { // overrides affected by environment variables - scopedConfig.doc.config.platformUrl = await getPlatformUrl() + scopedConfig.doc.config.platformUrl = await getPlatformUrl({ + tenantAware: true, + }) scopedConfig.doc.config.analyticsEnabled = await events.analytics.enabled() } else { @@ -393,7 +398,7 @@ export const getScopedFullConfig = async function ( doc: { _id: generateConfigID({ type, user, workspace }), config: { - platformUrl: await getPlatformUrl(), + platformUrl: await getPlatformUrl({ tenantAware: true }), analyticsEnabled: await events.analytics.enabled(), }, }, @@ -434,6 +439,26 @@ export const getPlatformUrl = async (opts = { tenantAware: true }) => { return platformUrl } +export function pagination( + data: any[], + pageSize: number, + { paginate, property } = { paginate: true, property: "_id" } +) { + if (!paginate) { + return { data, hasNextPage: false } + } + const hasNextPage = data.length > pageSize + let nextPage = undefined + if (hasNextPage) { + nextPage = property ? data[pageSize]?.[property] : data[pageSize]?._id + } + return { + data: data.slice(0, pageSize), + hasNextPage, + nextPage, + } +} + export async function getScopedConfig(db: any, params: any) { const configDoc = await getScopedFullConfig(db, params) return configDoc && configDoc.config ? configDoc.config : configDoc diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 6eb6b14bc4..ab89eed3b2 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -13,6 +13,7 @@ import deprovisioning from "./context/deprovision" import auth from "./auth" import constants from "./constants" import * as dbConstants from "./db/constants" +import logging from "./logging" // mimic the outer package exports import * as db from "./pkg/db" @@ -49,6 +50,7 @@ const core = { deprovisioning, installation, errors, + logging, ...errorClasses, } diff --git a/packages/backend-core/src/middleware/authenticated.js b/packages/backend-core/src/middleware/authenticated.js index 9c35336dda..4e6e0b7ba2 100644 --- a/packages/backend-core/src/middleware/authenticated.js +++ b/packages/backend-core/src/middleware/authenticated.js @@ -94,7 +94,6 @@ module.exports = ( user = await getUser(userId, session.tenantId) } user.csrfToken = session.csrfToken - delete user.password authenticated = true } catch (err) { error = err @@ -128,6 +127,8 @@ module.exports = ( } if (!user && tenantId) { user = { tenantId } + } else { + delete user.password } // be explicit if (authenticated !== true) { diff --git a/packages/backend-core/src/middleware/index.js b/packages/backend-core/src/middleware/index.js index 6c4c0d8883..1721d56a3c 100644 --- a/packages/backend-core/src/middleware/index.js +++ b/packages/backend-core/src/middleware/index.js @@ -2,7 +2,7 @@ const jwt = require("./passport/jwt") const local = require("./passport/local") const google = require("./passport/google") const oidc = require("./passport/oidc") -const { authError } = require("./passport/utils") +const { authError, ssoCallbackUrl } = require("./passport/utils") const authenticated = require("./authenticated") const auditLog = require("./auditLog") const tenancy = require("./tenancy") @@ -20,6 +20,7 @@ module.exports = { tenancy, authError, internalApi, + ssoCallbackUrl, datasource: { google: datasourceGoogle, }, diff --git a/packages/backend-core/src/middleware/passport/google.js b/packages/backend-core/src/middleware/passport/google.js index 858029ca80..7419974cd7 100644 --- a/packages/backend-core/src/middleware/passport/google.js +++ b/packages/backend-core/src/middleware/passport/google.js @@ -1,6 +1,7 @@ const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy - +const { ssoCallbackUrl } = require("./utils") const { authenticateThirdParty } = require("./third-party-common") +const { Configs } = require("../../../constants") const buildVerifyFn = saveUserFn => { return (accessToken, refreshToken, profile, done) => { @@ -57,5 +58,10 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { ) } } + +exports.getCallbackUrl = async function (db, config) { + return ssoCallbackUrl(db, config, Configs.GOOGLE) +} + // expose for testing exports.buildVerifyFn = buildVerifyFn diff --git a/packages/backend-core/src/middleware/passport/local.js b/packages/backend-core/src/middleware/passport/local.js index 445893b1df..b955d29102 100644 --- a/packages/backend-core/src/middleware/passport/local.js +++ b/packages/backend-core/src/middleware/passport/local.js @@ -55,6 +55,7 @@ exports.authenticate = async function (ctx, email, password, done) { if (await compare(password, dbUser.password)) { const sessionId = newid() const tenantId = getTenantId() + await createASession(dbUser._id, { sessionId, tenantId }) dbUser.token = jwt.sign( diff --git a/packages/backend-core/src/middleware/passport/oidc.js b/packages/backend-core/src/middleware/passport/oidc.js index 1e93e20b1c..20dbd4669b 100644 --- a/packages/backend-core/src/middleware/passport/oidc.js +++ b/packages/backend-core/src/middleware/passport/oidc.js @@ -1,6 +1,8 @@ const fetch = require("node-fetch") const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy const { authenticateThirdParty } = require("./third-party-common") +const { ssoCallbackUrl } = require("./utils") +const { Configs } = require("../../../constants") const buildVerifyFn = saveUserFn => { /** @@ -89,11 +91,24 @@ function validEmail(value) { * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport. * @returns Dynamically configured Passport OIDC Strategy */ -exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { +exports.strategyFactory = async function (config, saveUserFn) { try { - const { clientID, clientSecret, configUrl } = config + const verify = buildVerifyFn(saveUserFn) + const strategy = new OIDCStrategy(config, verify) + strategy.name = "oidc" + return strategy + } catch (err) { + console.error(err) + throw new Error("Error constructing OIDC authentication strategy", err) + } +} + +exports.fetchStrategyConfig = async function (enrichedConfig, callbackUrl) { + try { + const { clientID, clientSecret, configUrl } = enrichedConfig if (!clientID || !clientSecret || !callbackUrl || !configUrl) { + //check for remote config and all required elements throw new Error( "Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configUrl" ) @@ -109,24 +124,24 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { const body = await response.json() - const verify = buildVerifyFn(saveUserFn) - return new OIDCStrategy( - { - issuer: body.issuer, - authorizationURL: body.authorization_endpoint, - tokenURL: body.token_endpoint, - userInfoURL: body.userinfo_endpoint, - clientID: clientID, - clientSecret: clientSecret, - callbackURL: callbackUrl, - }, - verify - ) + return { + issuer: body.issuer, + authorizationURL: body.authorization_endpoint, + tokenURL: body.token_endpoint, + userInfoURL: body.userinfo_endpoint, + clientID: clientID, + clientSecret: clientSecret, + callbackURL: callbackUrl, + } } catch (err) { console.error(err) - throw new Error("Error constructing OIDC authentication strategy", err) + throw new Error("Error constructing OIDC authentication configuration", err) } } +exports.getCallbackUrl = async function (db, config) { + return ssoCallbackUrl(db, config, Configs.OIDC) +} + // expose for testing exports.buildVerifyFn = buildVerifyFn diff --git a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js index c5e9fe0034..c00ab2ea7d 100644 --- a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js +++ b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js @@ -48,8 +48,8 @@ describe("oidc", () => { it("should create successfully create an oidc strategy", async () => { const oidc = require("../oidc") - - await oidc.strategyFactory(oidcConfig, callbackUrl) + const enrichedConfig = await oidc.fetchStrategyConfig(oidcConfig, callbackUrl) + await oidc.strategyFactory(enrichedConfig, callbackUrl) expect(mockFetch).toHaveBeenCalledWith(oidcConfig.configUrl) diff --git a/packages/backend-core/src/middleware/passport/utils.js b/packages/backend-core/src/middleware/passport/utils.js index cbb93bfa3b..217130cd6d 100644 --- a/packages/backend-core/src/middleware/passport/utils.js +++ b/packages/backend-core/src/middleware/passport/utils.js @@ -1,3 +1,7 @@ +const { isMultiTenant, getTenantId } = require("../../tenancy") +const { getScopedConfig } = require("../../db/utils") +const { Configs } = require("../../constants") + /** * Utility to handle authentication errors. * @@ -5,6 +9,7 @@ * @param {*} message Message that will be returned in the response body * @param {*} err (Optional) error that will be logged */ + exports.authError = function (done, message, err = null) { return done( err, @@ -12,3 +17,21 @@ exports.authError = function (done, message, err = null) { { message: message } ) } + +exports.ssoCallbackUrl = async (db, config, type) => { + // incase there is a callback URL from before + if (config && config.callbackURL) { + return config.callbackURL + } + const publicConfig = await getScopedConfig(db, { + type: Configs.SETTINGS, + }) + + let callbackUrl = `/api/global/auth` + if (isMultiTenant()) { + callbackUrl += `/${getTenantId()}` + } + callbackUrl += `/${type}/callback` + + return `${publicConfig.platformUrl}${callbackUrl}` +} diff --git a/packages/backend-core/src/users.js b/packages/backend-core/src/users.js index 4acccda2a0..0c1350a674 100644 --- a/packages/backend-core/src/users.js +++ b/packages/backend-core/src/users.js @@ -1,5 +1,6 @@ const { ViewNames } = require("./db/utils") const { queryGlobalView } = require("./db/views") +const { UNICODE_MAX } = require("./db/constants") /** * Given an email address this will use a view to search through @@ -19,3 +20,24 @@ exports.getGlobalUserByEmail = async email => { return response } + +/** + * Performs a starts with search on the global email view. + */ +exports.searchGlobalUsersByEmail = async (email, opts) => { + if (typeof email !== "string") { + throw new Error("Must provide a string to search by") + } + const lcEmail = email.toLowerCase() + // handle if passing up startkey for pagination + const startkey = opts && opts.startkey ? opts.startkey : lcEmail + let response = await queryGlobalView(ViewNames.USER_BY_EMAIL, { + ...opts, + startkey, + endkey: `${lcEmail}${UNICODE_MAX}`, + }) + if (!response) { + response = [] + } + return Array.isArray(response) ? response : [response] +} diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock index e40cddc468..77dbc61425 100644 --- a/packages/backend-core/yarn.lock +++ b/packages/backend-core/yarn.lock @@ -4123,6 +4123,11 @@ passport-oauth1@1.x.x: passport-strategy "1.x.x" utils-merge "1.x.x" +passport-oauth2-refresh@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/passport-oauth2-refresh/-/passport-oauth2-refresh-2.1.0.tgz#c31cd133826383f5539d16ad8ab4f35ca73ce4a4" + integrity sha512-4ML7ooCESCqiTgdDBzNUFTBcPR8zQq9iM6eppEUGMMvLdsjqRL93jKwWm4Az3OJcI+Q2eIVyI8sVRcPFvxcF/A== + passport-oauth2@1.x.x: version "1.6.1" resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.6.1.tgz#c5aee8f849ce8bd436c7f81d904a3cd1666f181b" diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte index b518ac3d92..2d23120046 100644 --- a/packages/bbui/src/ActionButton/ActionButton.svelte +++ b/packages/bbui/src/ActionButton/ActionButton.svelte @@ -13,6 +13,7 @@ export let size = "M" export let active = false export let fullWidth = false + export let noPadding = false function longPress(element) { if (!longPressable) return @@ -41,6 +42,7 @@ class:spectrum-ActionButton--quiet={quiet} class:spectrum-ActionButton--emphasized={emphasized} class:is-selected={selected} + class:noPadding class:fullWidth class="spectrum-ActionButton spectrum-ActionButton--size{size}" class:active @@ -80,4 +82,8 @@ .active svg { color: var(--spectrum-global-color-blue-600); } + .noPadding { + padding: 0; + min-width: 0; + } diff --git a/packages/bbui/src/Button/Button.svelte b/packages/bbui/src/Button/Button.svelte index e8f6b4500e..36abcbf4da 100644 --- a/packages/bbui/src/Button/Button.svelte +++ b/packages/bbui/src/Button/Button.svelte @@ -14,6 +14,7 @@ export let active = false export let tooltip = undefined export let dataCy + export let newStyles = false let showTooltip = false @@ -25,6 +26,7 @@ class:spectrum-Button--warning={warning} class:spectrum-Button--overBackground={overBackground} class:spectrum-Button--quiet={quiet} + class:new-styles={newStyles} class:active class="spectrum-Button spectrum-Button--size{size.toUpperCase()}" {disabled} @@ -93,4 +95,20 @@ padding-left: var(--spacing-m); line-height: 0; } + .spectrum-Button--primary.new-styles { + background: var(--spectrum-global-color-gray-800); + border-color: transparent; + color: var(--spectrum-global-color-gray-50); + } + .spectrum-Button--primary.new-styles:hover { + background: var(--spectrum-global-color-gray-900); + } + .spectrum-Button--secondary.new-styles { + background: var(--spectrum-global-color-gray-200); + border-color: transparent; + color: var(--spectrum-global-color-gray-900); + } + .spectrum-Button--secondary.new-styles:hover { + background: var(--spectrum-global-color-gray-300); + } diff --git a/packages/bbui/src/Form/Combobox.svelte b/packages/bbui/src/Form/Combobox.svelte index 83927b05db..343af559cb 100644 --- a/packages/bbui/src/Form/Combobox.svelte +++ b/packages/bbui/src/Form/Combobox.svelte @@ -40,5 +40,6 @@ on:change={onChange} on:pick on:type + on:blur /> diff --git a/packages/bbui/src/Form/Core/Combobox.svelte b/packages/bbui/src/Form/Core/Combobox.svelte index 6a6423cccf..2835b3cd40 100644 --- a/packages/bbui/src/Form/Core/Combobox.svelte +++ b/packages/bbui/src/Form/Core/Combobox.svelte @@ -18,23 +18,6 @@ const dispatch = createEventDispatcher() let open = false let focus = false - $: fieldText = getFieldText(value, options, placeholder) - - const getFieldText = (value, options, placeholder) => { - // Always use placeholder if no value - if (value == null || value === "") { - return placeholder || "Choose an option or type" - } - - // Wait for options to load if there is a value but no options - if (!options?.length) { - return "" - } - - // Render the label if the selected option is found, otherwise raw value - const selected = options.find(option => getOptionValue(option) === value) - return selected ? getOptionLabel(selected) : value - } const selectOption = value => { dispatch("change", value) @@ -69,7 +52,10 @@ {id} type="text" on:focus={() => (focus = true)} - on:blur={() => (focus = false)} + on:blur={() => { + focus = false + dispatch("blur") + }} on:change={onType} value={value || ""} placeholder={placeholder || ""} diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index 2585f11939..cfd4b096b4 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -199,6 +199,7 @@ } .spectrum-Picker { width: 100%; + box-shadow: none; } .spectrum-Picker-label:not(.auto-width) { overflow: hidden; diff --git a/packages/bbui/src/Form/Core/TextField.svelte b/packages/bbui/src/Form/Core/TextField.svelte index 6a64876a2c..0a723c140a 100644 --- a/packages/bbui/src/Form/Core/TextField.svelte +++ b/packages/bbui/src/Form/Core/TextField.svelte @@ -112,4 +112,8 @@ .spectrum-Textfield { width: 100%; } + input:disabled { + color: var(--spectrum-global-color-gray-600) !important; + -webkit-text-fill-color: var(--spectrum-global-color-gray-600) !important; + } diff --git a/packages/bbui/src/Notification/Notification.svelte b/packages/bbui/src/Notification/Notification.svelte index 1d21131553..53ab062701 100644 --- a/packages/bbui/src/Notification/Notification.svelte +++ b/packages/bbui/src/Notification/Notification.svelte @@ -1,15 +1,20 @@ -
+
{#if icon} {/if} -
+
{message || ""}
+ {#if action} + +
{actionMessage}
+
+ {/if}
{#if dismissable}
@@ -46,4 +56,15 @@ .spectrum-Toast { pointer-events: all; } + + .wide { + width: 100%; + } + + .actionBody { + justify-content: space-between; + display: flex; + width: 100%; + align-items: center; + } diff --git a/packages/bbui/src/Notification/NotificationDisplay.svelte b/packages/bbui/src/Notification/NotificationDisplay.svelte index eb778f3aa0..0b846f06ce 100644 --- a/packages/bbui/src/Notification/NotificationDisplay.svelte +++ b/packages/bbui/src/Notification/NotificationDisplay.svelte @@ -8,13 +8,15 @@
- {#each $notifications as { type, icon, message, id, dismissable } (id)} + {#each $notifications as { type, icon, message, id, dismissable, action, wide } (id)}
notifications.dismiss(id)} />
diff --git a/packages/bbui/src/Stores/notifications.js b/packages/bbui/src/Stores/notifications.js index 74eed8628a..449d282f24 100644 --- a/packages/bbui/src/Stores/notifications.js +++ b/packages/bbui/src/Stores/notifications.js @@ -20,7 +20,16 @@ export const createNotificationStore = () => { setTimeout(() => (block = false), timeout) } - const send = (message, type = "default", icon = "", autoDismiss = true) => { + const send = ( + message, + { + type = "default", + icon = "", + autoDismiss = true, + action = null, + wide = false, + } + ) => { if (block) { return } @@ -28,7 +37,15 @@ export const createNotificationStore = () => { _notifications.update(state => { return [ ...state, - { id: _id, type, message, icon, dismissable: !autoDismiss }, + { + id: _id, + type, + message, + icon, + dismissable: !autoDismiss, + action, + wide, + }, ] }) if (autoDismiss) { @@ -50,10 +67,11 @@ export const createNotificationStore = () => { return { subscribe, send, - info: msg => send(msg, "info", "Info"), - error: msg => send(msg, "error", "Alert", false), - warning: msg => send(msg, "warning", "Alert"), - success: msg => send(msg, "success", "CheckmarkCircle"), + info: msg => send(msg, { type: "info", icon: "Info" }), + error: msg => + send(msg, { type: "error", icon: "Alert", autoDismiss: false }), + warning: msg => send(msg, { type: "warning", icon: "Alert" }), + success: msg => send(msg, { type: "success", icon: "CheckmarkCircle" }), blockNotifications, dismiss: dismissNotification, } diff --git a/packages/bbui/src/Table/CellRenderer.svelte b/packages/bbui/src/Table/CellRenderer.svelte index 4dda31240a..246323244a 100644 --- a/packages/bbui/src/Table/CellRenderer.svelte +++ b/packages/bbui/src/Table/CellRenderer.svelte @@ -26,12 +26,20 @@ array: ArrayRenderer, internal: InternalRenderer, } - $: type = schema?.type ?? "string" + $: type = getType(schema) $: customRenderer = customRenderers?.find(x => x.column === schema?.name) $: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer $: width = schema?.width || "150px" $: cellValue = getCellValue(value, schema.template) + const getType = schema => { + // Use a string renderer for dates if we use a custom template + if (schema?.type === "datetime" && schema?.template) { + return "string" + } + return schema?.type || "string" + } + const getCellValue = (value, template) => { if (!template) { return value diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte index baa84c91e0..e01d84e123 100644 --- a/packages/bbui/src/Table/Table.svelte +++ b/packages/bbui/src/Table/Table.svelte @@ -37,6 +37,7 @@ export let autoSortColumns = true export let compact = false export let customPlaceholder = false + export let placeholderText = "No rows found" const dispatch = createEventDispatcher() @@ -405,7 +406,7 @@ > -
No rows found
+
{placeholderText}
{/if}
diff --git a/packages/bbui/src/Typography/Detail.svelte b/packages/bbui/src/Typography/Detail.svelte index bb5c78c11e..76437ffb3c 100644 --- a/packages/bbui/src/Typography/Detail.svelte +++ b/packages/bbui/src/Typography/Detail.svelte @@ -1,9 +1,7 @@ diff --git a/packages/builder/cypress.json b/packages/builder/cypress.json index 06bf558946..46f85a52c8 100644 --- a/packages/builder/cypress.json +++ b/packages/builder/cypress.json @@ -13,7 +13,7 @@ "HOST_IP": "" }, "retries": { - "runMode": 2, + "runMode": 1, "openMode": 0 } } diff --git a/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js b/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js index 38ae881db8..32f62efe1f 100644 --- a/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js +++ b/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js @@ -16,18 +16,15 @@ filterTests(['all'], () => { it("should add form with multi select picker, containing 5 options", () => { cy.navigateToFrontend() - cy.wait(500) // Add data provider - cy.get(interact.CATEGORY_DATA).click() + cy.get(interact.CATEGORY_DATA, { timeout: 500 }).click() cy.get(interact.COMPONENT_DATA_PROVIDER).click() cy.get(interact.DATASOURCE_PROP_CONTROL).click() cy.get(interact.DROPDOWN).contains("Multi Data").click() - cy.wait(500) // Add Form with schema to match table cy.addComponent("Form", "Form") cy.get(interact.DATASOURCE_PROP_CONTROL).click() cy.get(interact.DROPDOWN).contains("Multi Data").click() - cy.wait(500) // Add multi-select picker to form cy.addComponent("Form", "Multi-select Picker").then(componentId => { cy.get(interact.DATASOURCE_FIELD_CONTROL).type("Test Data").type("{enter}") diff --git a/packages/builder/cypress/integration/adminAndManagement/accountPortals.spec.js b/packages/builder/cypress/integration/adminAndManagement/accountPortals.spec.js new file mode 100644 index 0000000000..c615b2b4e6 --- /dev/null +++ b/packages/builder/cypress/integration/adminAndManagement/accountPortals.spec.js @@ -0,0 +1,131 @@ +import filterTests from "../../support/filterTests" +const interact = require('../../support/interact') + +filterTests(["smoke", "all"], () => { + context("Account Portals", () => { + + const bbUserEmail = "bbuser@test.com" + + before(() => { + cy.login() + cy.deleteApp("Cypress Tests") + cy.createApp("Cypress Tests") + + // Create new user + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 1000}) + cy.createUser(bbUserEmail) + cy.contains("bbuser").click() + cy.wait(500) + + // Reset password + cy.get(".spectrum-ActionButton-label", { timeout: 2000 }).contains("Force password reset").click({ force: true }) + + cy.get(".spectrum-Dialog-grid") + .find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd') + + cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true }) + + // Login as new user and set password + cy.logOut() + cy.get('@pwd').then((pwd) => { + cy.login(bbUserEmail, pwd) + }) + + for (let i = 0; i < 2; i++) { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test") + } + cy.get(interact.SPECTRUM_BUTTON).contains("Reset your password").click({ force: true }) + cy.logoutNoAppGrid() + }) + + it("should verify Admin Portal", () => { + cy.login() + cy.contains("Users").click() + cy.contains("bbuser").click() + + // Enable Development & Administration access + cy.wait(500) + for (let i = 4; i < 6; i++) { + cy.get(interact.FIELD).eq(i).within(() => { + cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true }) + cy.get(interact.SPECTRUM_SWITCH_INPUT).should('be.enabled') + }) + } + bbUserLogin() + + // Verify available options for Admin portal + cy.get(".spectrum-SideNav") + .should('contain', 'Apps') + //.and('contain', 'Usage') + .and('contain', 'Users') + .and('contain', 'Auth') + .and('contain', 'Email') + .and('contain', 'Organisation') + .and('contain', 'Theming') + .and('contain', 'Update') + //.and('contain', 'Upgrade') + + cy.logOut() + }) + + it("should verify Development Portal", () => { + // Only Development access should be enabled + cy.login() + cy.contains("Users").click() + cy.contains("bbuser").click() + cy.wait(500) + cy.get(interact.FIELD).eq(5).within(() => { + cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true }) + }) + + bbUserLogin() + + // Verify available options for Admin portal + cy.get(interact.SPECTRUM_SIDENAV) + .should('contain', 'Apps') + //.and('contain', 'Usage') + .and('not.contain', 'Users') + .and('not.contain', 'Auth') + .and('not.contain', 'Email') + .and('not.contain', 'Organisation') + .and('contain', 'Theming') + .and('not.contain', 'Update') + .and('not.contain', 'Upgrade') + + cy.logOut() + }) + + it("should verify Standard Portal", () => { + // Development access should be disabled (Admin access is already disabled) + cy.login() + cy.contains("Users").click() + cy.contains("bbuser").click() + cy.wait(500) + cy.get(interact.FIELD).eq(4).within(() => { + cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true }) + }) + + bbUserLogin() + + // Verify Standard Portal + cy.get(interact.SPECTRUM_SIDENAV).should('not.exist') // No config sections + cy.get(interact.CREATE_APP_BUTTON).should('not.exist') // No create app button + cy.get(".app").should('not.exist') // No apps -> no roles assigned to user + cy.get(interact.CONTAINER).should('contain', bbUserEmail) // Message containing users email + + cy.logoutNoAppGrid() + }) + + const bbUserLogin = () => { + // Login as bbuser + cy.logOut() + cy.login(bbUserEmail, "test") + } + + after(() => { + cy.login() + // Delete BB user + cy.deleteUser(bbUserEmail) + }) + }) +}) diff --git a/packages/builder/cypress/integration/adminAndManagement/userManagement.spec.js b/packages/builder/cypress/integration/adminAndManagement/userManagement.spec.js new file mode 100644 index 0000000000..3c23086136 --- /dev/null +++ b/packages/builder/cypress/integration/adminAndManagement/userManagement.spec.js @@ -0,0 +1,234 @@ +import filterTests from "../../support/filterTests" +const interact = require('../../support/interact') + +filterTests(["smoke", "all"], () => { + context("User Management", () => { + before(() => { + cy.login() + cy.deleteApp("Cypress Tests") + cy.createApp("Cypress Tests") + }) + + it("should create a user via basic onboarding", () => { + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 1000}) + cy.createUser("bbuser@test.com") + cy.get(interact.SPECTRUM_TABLE).should("contain", "bbuser") + }) + + it("should confirm basic permission for a New User", () => { + // Basic permission = development & administraton disabled + cy.contains("bbuser").click() + // Confirm development and admin access are disabled + for (let i = 4; i < 6; i++) { + cy.wait(500) + cy.get(interact.FIELD).eq(i).within(() => { + //cy.get(interact.SPECTRUM_SWITCH_INPUT).should('be.disabled') + cy.get(".spectrum-Switch-switch").should('not.be.checked') + }) + } + // Existing apps appear within the No Access table + cy.get(interact.SPECTRUM_TABLE, { timeout: 500 }).eq(1).should("not.contain", "No rows found") + // Configure roles table should not contain apps + cy.get(interact.SPECTRUM_TABLE).eq(0).contains("No rows found") + }) + + if (Cypress.env("TEST_ENV")) { + it("should assign role types", () => { + // 3 apps minimum required - to assign an app to each role type + cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) + .its("body") + .then(val => { + if (val.length < 3) { + for (let i = 1; i < 3; i++) { + const uuid = () => Cypress._.random(0, 1e6) + const name = uuid() + if(i < 1){ + cy.createApp(name) + } else { + cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.get(interact.CREATE_APP_BUTTON, { timeout: 1000 }).click({ force: true }) + cy.createAppFromScratch(name) + } + } + } + }) + // Navigate back to the user + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 500}) + cy.get(interact.SPECTRUM_SIDENAV).contains("Users").click() + cy.get(interact.SPECTRUM_TABLE, { timeout: 500 }).contains("bbuser").click() + for (let i = 0; i < 3; i++) { + cy.get(interact.SPECTRUM_TABLE, { timeout: 3000}) + .eq(1) + .find(interact.SPECTRUM_TABLE_ROW) + .eq(0) + .find(interact.SPECTRUM_TABLE_CELL) + .eq(0) + .click() + cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 500 }) + .contains("Choose an option") + .click() + .then(() => { + if (i == 0) { + cy.get(interact.SPECTRUM_MENU, { timeout: 1000 }).contains("Admin").click({ force: true }) + } + else if (i == 1) { + cy.get(interact.SPECTRUM_MENU, { timeout: 1000 }).contains("Power").click({ force: true }) + } + else if (i == 2) { + cy.get(interact.SPECTRUM_MENU, { timeout: 1000 }).contains("Basic").click({ force: true }) + } + cy.get(interact.SPECTRUM_BUTTON, { timeout: 1000 }) + .contains("Update role") + .click({ force: true }) + }) + cy.reload() + cy.wait(1000) + } + // Confirm roles exist within Configure roles table + cy.get(interact.SPECTRUM_TABLE, { timeout: 2000 }) + .eq(0) + .within(assginedRoles => { + expect(assginedRoles).to.contain("Admin") + expect(assginedRoles).to.contain("Power") + expect(assginedRoles).to.contain("Basic") + }) + }) + + it("should unassign role types", () => { + // Set each app within Configure roles table to 'No Access' + cy.get(interact.SPECTRUM_TABLE) + .eq(0) + .find(interact.SPECTRUM_TABLE_ROW) + .its("length") + .then(len => { + for (let i = 0; i < len; i++) { + cy.get(interact.SPECTRUM_TABLE) + .eq(0) + .find(interact.SPECTRUM_TABLE_ROW) + .eq(0) + .find(interact.SPECTRUM_TABLE_CELL) + .eq(0) + .click() + .then(() => { + cy.get(interact.SPECTRUM_PICKER).eq(1).click({ force: true }) + cy.get(interact.SPECTRUM_POPOVER, { timeout: 500 }).contains("No Access").click() + }) + cy.get(interact.SPECTRUM_BUTTON) + .contains("Update role") + .click({ force: true }) + } + }) + // Confirm Configure roles table no longer has any apps in it + cy.get(interact.SPECTRUM_TABLE, { timeout: 1000 }).eq(0).contains("No rows found") + }) + } + + it("should enable Developer access and verify application access", () => { + // Enable Developer access + cy.get(interact.FIELD) + .eq(4) + .within(() => { + cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true }) + }) + // No Access table should now be empty + cy.get(interact.CONTAINER) + .contains("No Access") + .parent() + .within(() => { + cy.get(interact.SPECTRUM_TABLE).contains("No rows found") + }) + + // Each app within Configure roles should have Admin access + cy.get(interact.SPECTRUM_TABLE) + .eq(0) + .find(interact.SPECTRUM_TABLE_ROW) + .its("length") + .then(len => { + for (let i = 0; i < len; i++) { + cy.get(interact.SPECTRUM_TABLE) + .eq(0) + .find(interact.SPECTRUM_TABLE_ROW) + .eq(i) + .contains("Admin") + cy.wait(500) + } + }) + }) + + it("should disable Developer access and verify application access", () => { + // Disable Developer access + cy.get(interact.FIELD) + .eq(4) + .within(() => { + cy.get(".spectrum-Switch-input").click({ force: true }) + }) + // Configure roles table should now be empty + cy.get(interact.CONTAINER) + .contains("Configure roles") + .parent() + .within(() => { + cy.get(interact.SPECTRUM_TABLE).contains("No rows found") + }) + }) + + it("Should edit user details within user details page", () => { + // Add First name + cy.get(interact.FIELD, { timeout: 500 }).eq(2).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 500 }).type("bb") + }) + // Add Last name + cy.get(interact.FIELD).eq(3).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).type("test") + }) + cy.get(interact.FIELD).eq(0).click() + // Reload page + cy.reload() + + // Confirm details have been saved + cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb") + }) + cy.get(interact.FIELD).eq(3).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 500 }).should('have.value', "test") + }) + }) + + it("should reset the users password", () => { + cy.get(interact.REGENERATE, { timeout: 500 }).contains("Force password reset").click({ force: true }) + + // Reset password modal + cy.get(interact.SPECTRUM_DIALOG_GRID) + .find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd') + cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true }) + + // Logout, then login with new password + cy.logOut() + cy.get('@pwd').then((pwd) => { + cy.login("bbuser@test.com", pwd) + }) + + // Reset password screen + for (let i = 0; i < 2; i++) { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test") + } + cy.get(interact.SPECTRUM_BUTTON).contains("Reset your password").click({ force: true }) + + // Confirm user logged in afer password change + cy.get(".avatar > .icon").click({ force: true }) + + cy.get(".spectrum-Menu-item").contains("Update user information").click({ force: true }) + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT) + .eq(0) + .invoke('val').should('eq', 'bbuser@test.com') + + // Logout and login as previous user + cy.logoutNoAppGrid() + cy.login() + }) + + it("should delete a user", () => { + cy.deleteUser("bbuser@test.com") + cy.get(interact.SPECTRUM_TABLE, { timeout: 4000 }).should("not.have.text", "bbuser") + }) + }) +}) diff --git a/packages/builder/cypress/integration/adminAndManagement/userSettings.spec.js b/packages/builder/cypress/integration/adminAndManagement/userSettings.spec.js new file mode 100644 index 0000000000..7827275620 --- /dev/null +++ b/packages/builder/cypress/integration/adminAndManagement/userSettings.spec.js @@ -0,0 +1,108 @@ +import filterTests from "../../support/filterTests" +const interact = require('../../support/interact') + +filterTests(["smoke", "all"], () => { + context("User Settings Menu", () => { + + before(() => { + cy.login() + }) + + it("should update user information via user settings menu", () => { + const fname = "test" + const lname = "user" + + cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.updateUserInformation(fname, lname) + + // Go to user info and confirm name update + cy.contains("Users").click() + cy.contains("test@test.com").click() + + cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', fname) + }) + cy.get(interact.FIELD).eq(3).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', lname) + }) + }) + + it("should allow copying of the users API key", () => { + cy.get(".user-dropdown .avatar > .icon", { timeout: 2000 }).click({ force: true }) + cy.get(interact.SPECTRUM_MENU_ITEM).contains("View API key").click({ force: true }) + cy.get(interact.SPECTRUM_DIALOG_CONTENT).within(() => { + cy.get(interact.SPECTRUM_ICON).click({force: true}) + }) + // There may be timing issues with this on the smoke build + cy.wait(500) + cy.get(".spectrum-Toast-content") + .contains("URL copied to clipboard") + .should("be.visible") + }) + + it("should allow API key regeneration", () => { + // Get initial API key value + cy.get(interact.SPECTRUM_DIALOG_CONTENT) + .find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('keyOne') + + // Click re-generate key button + cy.get("button").contains("Re-generate key").click({ force: true }) + + // Verify API key was changed + cy.get(interact.SPECTRUM_DIALOG_CONTENT).within(() => { + cy.get('@keyOne').then((keyOne) => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').should('not.eq', keyOne) + }) + }) + cy.closeModal() + }) + + it("should update password", () => { + // Access Update password modal + cy.get(".user-dropdown .avatar > .icon", { timeout: 2000 }).click({ force: true }) + cy.get(interact.SPECTRUM_MENU_ITEM).contains("Update password").click({ force: true }) + + // Enter new password and update + cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => { + for (let i = 0; i < 2; i++) { + // password set to 'newpwd' + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("newpwd") + } + cy.get("button").contains("Update password").click({ force: true }) + }) + + // Logout & in with new password + cy.logOut() + cy.login("test@test.com", "newpwd") + }) + + it("should open and close developer mode", () => { + cy.get(".user-dropdown .avatar > .icon", { timeout: 2000 }).click({ force: true }) + + // Close developer mode & verify + cy.get(interact.SPECTRUM_MENU_ITEM).contains("Close developer mode").click({ force: true }) + cy.get(interact.SPECTRUM_SIDENAV).should('not.exist') // No config sections + cy.get(interact.CREATE_APP_BUTTON).should('not.exist') // No create app button + cy.get(".app").should('not.exist') // At least one app should be available + + // Open developer mode & verify + cy.get(".avatar > .icon").click({ force: true }) + cy.get(interact.SPECTRUM_MENU_ITEM).contains("Open developer mode").click({ force: true }) + cy.get(interact.SPECTRUM_SIDENAV).should('exist') // config sections available + cy.get(interact.CREATE_APP_BUTTON).should('exist') // create app button available + cy.get(interact.APP_TABLE).should('exist') // App table available + }) + + after(() => { + // Change password back to original value + cy.get(".user-dropdown .avatar > .icon", { timeout: 2000 }).click({ force: true }) + cy.get(interact.SPECTRUM_MENU_ITEM).contains("Update password").click({ force: true }) + cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => { + for (let i = 0; i < 2; i++) { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test") + } + cy.get("button").contains("Update password").click({ force: true }) + }) + }) + }) +}) diff --git a/packages/builder/cypress/integration/appOverview.spec.js b/packages/builder/cypress/integration/appOverview.spec.js index db093344b4..d718f95b9f 100644 --- a/packages/builder/cypress/integration/appOverview.spec.js +++ b/packages/builder/cypress/integration/appOverview.spec.js @@ -1,7 +1,7 @@ import filterTests from "../support/filterTests" import clientPackage from "@budibase/client/package.json" -filterTests(['all'], () => { +filterTests(["all"], () => { context("Application Overview screen", () => { before(() => { cy.login() @@ -10,31 +10,19 @@ filterTests(['all'], () => { it("Should be accessible from the applications list", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - - cy.get(".appTable .title").eq(0) - .invoke('attr', 'data-cy') - .then(($dataCy) => { - const dataCy = $dataCy; - cy.get(".appTable .name").eq(0).click() + cy.get(".appTable .title") + .eq(0) + .invoke("attr", "data-cy") + .then($dataCy => { + const dataCy = $dataCy + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .click({ force: true }) - cy.location().should((loc) => { - expect(loc.pathname).to.eq('/builder/portal/overview/' + dataCy) + cy.location().should(loc => { + expect(loc.pathname).to.eq("/builder/portal/overview/" + dataCy) + }) }) - }) - - cy.visit(`${Cypress.config().baseUrl}/builder`) - - cy.get(".appTable .title").eq(0) - .invoke('attr', 'data-cy') - .then(($dataCy) => { - const dataCy = $dataCy; - cy.get(".appTable .app-row-actions button").contains("View").click({force: true}) - - cy.location().should((loc) => { - expect(loc.pathname).to.eq('/builder/portal/overview/' + dataCy) - }) - }) - }) // Find a more suitable place for this. @@ -43,24 +31,28 @@ filterTests(['all'], () => { cy.get(".appTable .lock-status").eq(0).contains("Locked by you").click() - cy.unlockApp({ owned : true }) + cy.unlockApp({ owned: true }) cy.get(".appTable").should("exist") - cy.get(".lock-status").should('not.be.visible') + cy.get(".lock-status").should("not.be.visible") }) - + it("Should allow unlocking in the app overview screen", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true}) + cy.get(".appTable .app-row-actions button") + .contains("Edit") + .eq(0) + .click({ force: true }) cy.wait(1000) cy.visit(`${Cypress.config().baseUrl}/builder`) - - cy.get(".appTable .name").eq(0).click() - + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) cy.get(".lock-status").eq(0).contains("Locked by you").click() - cy.unlockApp({ owned : true }) + cy.unlockApp({ owned: true }) cy.get(".lock-status").should("not.be.visible") }) @@ -68,93 +60,146 @@ filterTests(['all'], () => { it("Should reflect the deploy state of an app that hasn't been published.", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - - cy.get(".header-right button.spectrum-Button[data-cy='view-app']").should("be.disabled") + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) + cy.get(".header-right button.spectrum-Button[data-cy='view-app']").should( + "be.disabled" + ) cy.get(".spectrum-Tabs-item.is-selected").contains("Overview") cy.get(".overview-tab").should("be.visible") cy.get(".overview-tab [data-cy='app-status']").within(() => { cy.get(".status-display").contains("Unpublished") - cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist") + cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should( + "exist" + ) cy.get(".status-text").contains("-") }) }) it("Should reflect the app deployment state", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true}) - - cy.get(".toprightnav button.spectrum-Button").contains("Publish").click({ force : true }) - cy.get(".spectrum-Modal [data-cy='deploy-app-modal']").should("be.visible") - .within(() => { - cy.get(".spectrum-Button").contains("Publish").click({ force : true }) - cy.wait(1000) - }); + cy.get(".appTable .app-row-actions button") + .contains("Edit") + .eq(0) + .click({ force: true }) + + cy.get(".toprightnav button.spectrum-Button") + .contains("Publish") + .click({ force: true }) + cy.get(".spectrum-Modal [data-cy='deploy-app-modal']") + .should("be.visible") + .within(() => { + cy.get(".spectrum-Button").contains("Publish").click({ force: true }) + cy.wait(1000) + }) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - - cy.get(".header-right button.spectrum-Button[data-cy='view-app']").should("not.be.disabled") + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) + cy.get(".header-right button.spectrum-Button[data-cy='view-app']").should( + "not.be.disabled" + ) cy.get(".overview-tab [data-cy='app-status']").within(() => { cy.get(".status-display").contains("Published") - cy.get(".status-display .icon svg[aria-label='GlobeCheck']").should("exist") + cy.get(".status-display .icon svg[aria-label='GlobeCheck']").should( + "exist" + ) cy.get(".status-text").contains("Last published a few seconds ago") }) }) - it("Should reflect an application that has been unpublished", () => { + it("Should reflect an application that has been unpublished", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true}) + cy.get(".appTable .app-row-actions button") + .contains("Edit") + .eq(0) + .click({ force: true }) - cy.get(".deployment-top-nav svg[aria-label='Globe']") - .click({ force: true }) + cy.get(".deployment-top-nav svg[aria-label='Globe']").click({ + force: true, + }) cy.get("[data-cy='publish-popover-menu']").should("be.visible") - cy.get("[data-cy='publish-popover-menu'] [data-cy='publish-popover-action']") - .click({ force : true }) - - cy.get("[data-cy='unpublish-modal']").should("be.visible") - .within(() => { - cy.get(".confirm-wrap button").click({ force: true } - )}) - cy.wait(1000) + cy.get( + "[data-cy='publish-popover-menu'] [data-cy='publish-popover-action']" + ).click({ force: true }) + + cy.get("[data-cy='unpublish-modal']") + .should("be.visible") + .within(() => { + cy.get(".confirm-wrap button").click({ force: true }) + }) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) cy.get(".overview-tab [data-cy='app-status']").within(() => { cy.get(".status-display").contains("Unpublished") - cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist") + cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should( + "exist" + ) cy.get(".status-text").contains("Last published a few seconds ago") }) }) - it("Should allow the editing of the application icon", () => { + it("Should allow the editing of the application icon and colour", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - - cy.get(".appTable .name").eq(0).click() - - cy.get(".app-logo .edit-hover").should("exist").invoke("show").click() - - cy.customiseAppIcon() - - cy.get(".app-logo") - .within(() => { - cy.get('[aria-label]').eq(0).children() - .should('have.attr', 'xlink:href').and('not.contain', '#spectrum-icon-18-Apps') - cy.get(".app-icon") - .should('have.attr', 'style').and('contains', 'color') + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) + cy.get(".edit-hover", { timeout: 1000 }).eq(0).click({ force: true }) + // Select random icon + cy.wait(400) + cy.get(".grid").within(() => { + cy.get(".icon-item") + .eq(Math.floor(Math.random() * 23) + 1) + .click() + }) + // Select random colour + cy.get(".fill").click() + cy.get(".colors").within(() => { + cy.get(".color") + .eq(Math.floor(Math.random() * 33) + 1) + .click() + }) + cy.intercept("**/applications/**").as("iconChange") + cy.get(".spectrum-Button").contains("Save").click({ force: true }) + cy.wait("@iconChange") + cy.get("@iconChange").its("response.statusCode").should("eq", 200) + // Confirm icon has changed from default + // Confirm colour has been applied + cy.get(".spectrum-ActionButton-label").contains("Back").click({ force: true }) + cy.get(".appTable", { timeout: 2000 }).within(() => { + cy.get("[aria-label]") + .eq(0) + .children() + .should("have.attr", "xlink:href") + .and("not.contain", "#spectrum-icon-18-Apps") + cy.get(".title") + .children() + .children() + .should("have.attr", "style") + .and("contains", "color") }) }) it("Should reflect the last time the application was edited", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - - cy.get(".header-right button").contains("Edit").click({ force: true }); + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) + cy.get(".header-right button").contains("Edit").click({ force: true }) cy.navigateToFrontend() @@ -163,41 +208,51 @@ filterTests(['all'], () => { }) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) cy.get(".overview-tab [data-cy='edited-by']").within(() => { cy.get(".editor-name").contains("You") cy.get(".last-edit-text").contains("Last edited a few seconds ago") }) - }); + }) it("Should reflect application version is up-to-date", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) cy.get(".overview-tab [data-cy='app-version']").within(() => { cy.get(".version-status").contains("You're running the latest!") }) - }); + }) it("Should navigate to the settings tab when clicking the App Version card header", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) cy.get(".spectrum-Tabs-item.is-selected").contains("Overview") cy.get(".overview-tab").should("be.visible") - cy.get(".overview-tab [data-cy='app-version'] .dash-card-header").click({ force : true }) + cy.get(".overview-tab [data-cy='app-version'] .dash-card-header").click({ + force: true, + }) cy.get(".spectrum-Tabs-item.is-selected").contains("Settings") cy.get(".settings-tab").should("be.visible") cy.get(".overview-tab").should("not.exist") - - }); + }) it("Should allow the upgrading of an application, if available.", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) cy.wait(500) cy.location().then(loc => { @@ -205,123 +260,161 @@ filterTests(['all'], () => { const appId = params[params.length - 1] cy.log(appId) //Downgrade the app for the test - cy.alterAppVersion(appId, "0.0.1-alpha.0") - .then(()=>{ + cy.alterAppVersion(appId, "0.0.1-alpha.0").then(() => { cy.reload() - cy.wait(1000) cy.log("Current deployment version: " + clientPackage.version) - cy.get(".version-status a").contains("Update").click() + cy.get(".version-status a", { timeout: 1000 }).contains("Update").click() cy.get(".spectrum-Tabs-item.is-selected").contains("Settings") - cy.get(".version-section .page-action button").contains("Update").click({ force: true }) - - cy.intercept('POST', '**/applications/**/client/update').as('updateVersion') - cy.get(".spectrum-Modal.is-open button").contains("Update").click({ force: true }) + cy.get(".version-section .page-action button") + .contains("Update") + .click({ force: true }) + + cy.intercept("POST", "**/applications/**/client/update").as( + "updateVersion" + ) + cy.get(".spectrum-Modal.is-open button") + .contains("Update") + .click({ force: true }) cy.wait("@updateVersion") - .its('response.statusCode').should('eq', 200) - .then(() => { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - - cy.get(".spectrum-Tabs-item").contains("Overview").click({ force: true }) - cy.get(".overview-tab [data-cy='app-version']").within(() => { - cy.get(".spectrum-Heading").contains(clientPackage.version) - cy.get(".version-status").contains("You're running the latest!") + .its("response.statusCode") + .should("eq", 200) + .then(() => { + cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) + cy.get(".spectrum-Tabs-item") + .contains("Overview") + .click({ force: true }) + cy.get(".overview-tab [data-cy='app-version']").within(() => { + cy.get(".spectrum-Heading").contains(clientPackage.version) + cy.get(".version-status").contains("You're running the latest!") + }) }) - }) }) - }); - + }) }) it("Should allow editing of the app details.", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) cy.get(".spectrum-Tabs-item").contains("Settings").click() cy.get(".spectrum-Tabs-item.is-selected").contains("Settings") cy.get(".settings-tab").should("be.visible") - cy.get(".details-section .page-action button").contains("Edit").click({ force: true }) + cy.get(".details-section .page-action button") + .contains("Edit") + .click({ force: true }) cy.updateAppName("sample name") //publish and check its disabled cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true}) + cy.get(".appTable .app-row-actions button") + .contains("Edit") + .eq(0) + .click({ force: true }) - cy.get(".toprightnav button.spectrum-Button").contains("Publish").click({ force : true }) - cy.get(".spectrum-Modal [data-cy='deploy-app-modal']").should("be.visible") - .within(() => { - cy.get(".spectrum-Button").contains("Publish").click({ force : true }) - cy.wait(1000) - }); + cy.get(".toprightnav button.spectrum-Button") + .contains("Publish") + .click({ force: true }) + cy.get(".spectrum-Modal [data-cy='deploy-app-modal']") + .should("be.visible") + .within(() => { + cy.get(".spectrum-Button").contains("Publish").click({ force: true }) + cy.wait(1000) + }) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() + cy.get(".appTable .app-row-actions button", { timeout: 1000 }) + .contains("Manage") + .eq(0) + .click({ force: true }) cy.get(".spectrum-Tabs-item").contains("Settings").click() cy.get(".spectrum-Tabs-item.is-selected").contains("Settings") cy.get(".details-section .page-action .spectrum-Button").scrollIntoView() - cy.wait(1000) - cy.get(".details-section .page-action .spectrum-Button").should("be.disabled") - + cy.get(".details-section .page-action .spectrum-Button", { timeout: 1000 }).should( + "be.disabled" + ) }) - it("Should allow copying of the published application Id", () => { + xit("Should allow copying of the published application Id", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .app-row-actions").eq(0) - .within(() => { - cy.get(".spectrum-Button").contains("Edit").click({ force: true }) - }) + cy.get(".appTable .app-row-actions") + .eq(0) + .within(() => { + cy.get(".spectrum-Button").contains("Edit").click({ force: true }) + }) cy.publishApp("sample-name") cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) + cy.get(".app-overview-actions-icon > .icon").click({ force: true }) - cy.get(".app-overview-actions-icon > .icon").click({ force : true }) - - cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => { - cy.get(".spectrum-Menu-item").contains("Copy App ID").click({ force: true }) - }) - - cy.get(".spectrum-Toast-content").contains("App ID copied to clipboard.").should("be.visible") + cy.get("[data-cy='app-overview-menu-popover']") + .eq(0) + .within(() => { + cy.get(".spectrum-Menu-item") + .contains("Copy App ID") + .click({ force: true }) + }) + + cy.get(".spectrum-Toast-content") + .contains("App ID copied to clipboard.") + .should("be.visible") }) - it("Should allow unpublishing of the application", () => { + it("Should allow unpublishing of the application via the Unpublish link", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) - cy.get(".app-overview-actions-icon > .icon").click({ force : true }) + cy.get(`[data-cy="app-status"]`).within(() => { + cy.contains("Unpublish").click({ force: true }) + }) - cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => { - cy.get(".spectrum-Menu-item").contains("Unpublish").click({ force: true }) - cy.wait(500) - }) - - cy.get("[data-cy='unpublish-modal']").should("be.visible") - .within(() => { - cy.get(".confirm-wrap button").click({ force: true } - )}) + cy.get("[data-cy='unpublish-modal']") + .should("be.visible") + .within(() => { + cy.get(".confirm-wrap button").click({ force: true }) + }) cy.get(".overview-tab [data-cy='app-status']").within(() => { cy.get(".status-display").contains("Unpublished") - cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist") + cy.get(".status-display .icon svg[aria-label='GlobeStrike']") + .should("exist") }) }) it("Should allow deleting of the application", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) + cy.get(".app-overview-actions-icon > .icon").click({ force: true }) - cy.get(".app-overview-actions-icon > .icon").click({ force : true }) - - cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => { - cy.get(".spectrum-Menu-item").contains("Delete").click({ force: true }) - cy.wait(500) - }) + cy.get("[data-cy='app-overview-menu-popover']") + .eq(0) + .within(() => { + cy.get(".spectrum-Menu-item") + .contains("Delete") + .click({ force: true }) + cy.wait(500) + }) //The test application was renamed earlier in the spec cy.get(".spectrum-Dialog-grid").within(() => { @@ -329,18 +422,17 @@ filterTests(['all'], () => { cy.get(".spectrum-Button--warning").click() }) - cy.location().should((loc) => { - expect(loc.pathname).to.eq('/builder/portal/apps') + cy.location().should(loc => { + expect(loc.pathname).to.eq("/builder/portal/apps") }) cy.get(".appTable").should("not.exist") - + cy.get(".welcome .container h1").contains("Let's create your first app!") }) after(() => { cy.deleteAllApps() }) - }) }) diff --git a/packages/builder/cypress/integration/appPublishWorkflow.spec.js b/packages/builder/cypress/integration/appPublishWorkflow.spec.js index d05a97f691..e65c01c1b6 100644 --- a/packages/builder/cypress/integration/appPublishWorkflow.spec.js +++ b/packages/builder/cypress/integration/appPublishWorkflow.spec.js @@ -1,4 +1,5 @@ import filterTests from "../support/filterTests" +import { APP_TABLE_APP_NAME, DEPLOY_SUCCESS_MODAL } from "../support/interact"; const interact = require('../support/interact') filterTests(['all'], () => { @@ -10,9 +11,8 @@ filterTests(['all'], () => { it("Should reflect the unpublished status correctly", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) - cy.get(interact.APP_TABLE_STATUS).eq(0) + cy.get(interact.APP_TABLE_STATUS, { timeout: 3000 }).eq(0) .within(() => { cy.contains("Unpublished") cy.get(interact.GLOBESTRIKE).should("exist") @@ -20,10 +20,9 @@ filterTests(['all'], () => { cy.get(interact.APP_TABLE_ROW_ACTION).eq(0) .within(() => { - cy.get(interact.SPECTRUM_BUTTON_TEMPLATE).contains("Preview") cy.get(interact.SPECTRUM_BUTTON_TEMPLATE).contains("Edit").click({ force: true }) }) - + cy.get(interact.DEPLOYMENT_TOP_NAV_GLOBESTRIKE).should("exist") cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("not.exist") }) @@ -35,11 +34,10 @@ filterTests(['all'], () => { cy.get(interact.DEPLOY_APP_MODAL).should("be.visible") .within(() => { cy.get(interact.SPECTRUM_BUTTON).contains("Publish").click({ force : true }) - cy.wait(1000) }); //Verify that the app url is presented correctly to the user - cy.get(interact.DEPLOY_APP_MODAL) + cy.get(interact.DEPLOY_SUCCESS_MODAL, { timeout: 1000 }) .should("be.visible") .within(() => { let appUrl = Cypress.config().baseUrl + '/app/cypress-tests' @@ -48,9 +46,8 @@ filterTests(['all'], () => { }) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) - cy.get(interact.APP_TABLE_STATUS).eq(0) + cy.get(interact.APP_TABLE_STATUS, { timeout: 3000 }).eq(0) .within(() => { cy.contains("Published") cy.get(interact.GLOBE).should("exist") @@ -58,12 +55,12 @@ filterTests(['all'], () => { cy.get(interact.APP_TABLE_ROW_ACTION).eq(0) .within(() => { - cy.get(interact.SPECTRUM_BUTTON).contains("View") + cy.get(interact.SPECTRUM_BUTTON).contains("Manage") cy.get(interact.SPECTRUM_BUTTON).contains("Edit").click({ force: true }) }) cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("exist").click({ force: true }) - + cy.get(interact.PUBLISH_POPOVER_MENU).should("be.visible") .within(() => { cy.get(interact.PUBLISH_POPOVER_ACTION).should("exist") @@ -72,9 +69,9 @@ filterTests(['all'], () => { }) }) - it("Should unpublish an application from the top navigation and reflect the status change", () => { + it("Should unpublish an application using the link and reflect the status change", () => { //Assuming the previous test app exists and is published - + cy.visit(`${Cypress.config().baseUrl}/builder`) cy.get(interact.APP_TABLE_STATUS).eq(0) @@ -83,30 +80,25 @@ filterTests(['all'], () => { cy.get("svg[aria-label='Globe']").should("exist") }) - cy.get(interact.APP_TABLE_ROW_ACTION).eq(0) + cy.get(interact.APP_TABLE).eq(0) .within(() => { - cy.get(interact.SPECTRUM_BUTTON).contains("View app") - cy.get(interact.SPECTRUM_BUTTON).contains("Edit").click({ force: true }) + cy.get(interact.APP_TABLE_APP_NAME).click({ force: true }) }) - //The published status - cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("exist") - .click({ force: true }) - - cy.get(interact.PUBLISH_POPOVER_MENU).should("be.visible") - cy.get("[data-cy='publish-popover-menu'] [data-cy='publish-popover-action']") - .click({ force : true }) + cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("exist").click({ force: true }) + + cy.get("[data-cy='publish-popover-menu']") + .within(() => { + cy.get(interact.PUBLISH_POPOVER_ACTION).click({ force: true }) + }) cy.get(interact.UNPUBLISH_MODAL).should("be.visible") .within(() => { cy.get(interact.CONFIRM_WRAP_BUTTON).click({ force: true } )}) - cy.get(interact.DEPLOYMENT_TOP_NAV_GLOBESTRIKE).should("exist") - cy.visit(`${Cypress.config().baseUrl}/builder`) - - cy.get(interact.APP_TABLE_STATUS).eq(0).contains("Unpublished") + cy.get(interact.APP_TABLE_STATUS, { timeout: 1000 }).eq(0).contains("Unpublished") }) }) diff --git a/packages/builder/cypress/integration/autoScreensUI.spec.js b/packages/builder/cypress/integration/autoScreensUI.spec.js index eebeac3e71..ca997479ae 100644 --- a/packages/builder/cypress/integration/autoScreensUI.spec.js +++ b/packages/builder/cypress/integration/autoScreensUI.spec.js @@ -5,6 +5,7 @@ filterTests(['smoke', 'all'], () => { context("Auto Screens UI", () => { before(() => { cy.login() + cy.deleteAllApps() }) it("should disable the autogenerated screen options if no sources are available", () => { diff --git a/packages/builder/cypress/integration/changeAppIconAndColour.spec.js b/packages/builder/cypress/integration/changeAppIconAndColour.spec.js deleted file mode 100644 index 0f623ddb04..0000000000 --- a/packages/builder/cypress/integration/changeAppIconAndColour.spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import filterTests from "../support/filterTests" - -filterTests(['all'], () => { - context("Change Application Icon and Colour", () => { - before(() => { - cy.login() - }) - - it("should change the icon and colour for an application", () => { - // Search for test application - cy.applicationInAppTable("Cypress Tests") - cy.get(".appTable") - .within(() => { - cy.get(".app-row-actions-icon").eq(0).click() - }) - cy.get(".spectrum-Menu").contains("Edit icon").click() - // Select random icon - cy.get(".grid").within(() => { - cy.get(".icon-item").eq(Math.floor(Math.random() * 23) + 1).click() - }) - // Select random colour - cy.get(".fill").click() - cy.get(".colors").within(() => { - cy.get(".color").eq(Math.floor(Math.random() * 33) + 1).click() - }) - cy.intercept('**/applications/**').as('iconChange') - cy.get(".spectrum-Button").contains("Save").click({ force: true }) - cy.wait("@iconChange") - cy.get("@iconChange").its('response.statusCode') - .should('eq', 200) - cy.wait(1000) - // Confirm icon has changed from default - // Confirm colour has been applied - There is no default colour - cy.get(".appTable") - .within(() => { - cy.get('[aria-label]').eq(0).children() - .should('have.attr', 'xlink:href').and('not.contain', '#spectrum-icon-18-Apps') - cy.get(".title").children().children() - .should('have.attr', 'style').and('contains', 'color') - }) - cy.deleteAllApps() - }) - }) -}) diff --git a/packages/builder/cypress/integration/createApp.spec.js b/packages/builder/cypress/integration/createApp.spec.js index df617e3d9f..00c875e4fa 100644 --- a/packages/builder/cypress/integration/createApp.spec.js +++ b/packages/builder/cypress/integration/createApp.spec.js @@ -6,14 +6,14 @@ filterTests(['smoke', 'all'], () => { before(() => { cy.login() - cy.deleteApp("Cypress Tests") + cy.deleteAllApps() }) if (!(Cypress.env("TEST_ENV"))) { it("should show the new user UI/UX", () => { - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/create`) //added /portal/apps/create + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/create`, { timeout: 5000 }) //added /portal/apps/create + cy.wait(1000) cy.get(interact.CREATE_APP_BUTTON).contains('Start from scratch').should("exist") - cy.get(interact.CREATE_APP_BUTTON).should("exist") cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist") cy.get(interact.TEMPLATE_CATEGORY).should("exist") @@ -23,7 +23,7 @@ filterTests(['smoke', 'all'], () => { } it("should provide filterable templates", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.wait(500) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) @@ -48,18 +48,10 @@ filterTests(['smoke', 'all'], () => { }) it("should enforce a valid url before submission", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 }) // Start create app process. If apps already exist, click second button - cy.get(interact.CREATE_APP_BUTTON).click({ force: true }) - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(interact.CREATE_APP_BUTTON).click({ force: true }) - } - }) + cy.get(interact.CREATE_APP_BUTTON, { timeout: 1000 }).click({ force: true }) const appName = "Cypress Tests" cy.get(interact.SPECTRUM_MODAL).within(() => { @@ -92,21 +84,16 @@ filterTests(['smoke', 'all'], () => { it("should create the first application from scratch", () => { const appName = "Cypress Tests" - cy.createApp(appName) + cy.createApp(appName, false) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) cy.applicationInAppTable(appName) cy.deleteApp(appName) }) it("should create the first application from scratch with a default name", () => { - cy.createApp() - - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) - + cy.createApp("", false) cy.applicationInAppTable("My app") cy.deleteApp("My app") }) @@ -116,26 +103,22 @@ filterTests(['smoke', 'all'], () => { cy.updateUserInformation("Ted", "Userman") - cy.createApp() + cy.createApp("", false) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) cy.applicationInAppTable("Teds app") cy.deleteApp("Teds app") - cy.wait(2000) //Accomodate names that end in 'S' cy.updateUserInformation("Chris", "Userman") - cy.createApp() + cy.createApp("", false) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) cy.applicationInAppTable("Chris app") cy.deleteApp("Chris app") - cy.wait(2000) cy.updateUserInformation("", "") }) @@ -145,7 +128,7 @@ filterTests(['smoke', 'all'], () => { cy.importApp(exportedApp, "") - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 2000 }) cy.applicationInAppTable("My app") @@ -224,14 +207,12 @@ filterTests(['smoke', 'all'], () => { cy.createApp(appName) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) // Create second app const secondAppName = "Second App Demo" cy.createApp(secondAppName) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) //Both applications should exist and be searchable cy.searchForApplication(appName) diff --git a/packages/builder/cypress/integration/createAutomation.spec.js b/packages/builder/cypress/integration/createAutomation.spec.js index 69ef3f98a3..b5ff406297 100644 --- a/packages/builder/cypress/integration/createAutomation.spec.js +++ b/packages/builder/cypress/integration/createAutomation.spec.js @@ -1,4 +1,5 @@ import filterTests from "../support/filterTests" +const interact = require('../support/interact') filterTests(['smoke', 'all'], () => { context("Create a automation", () => { @@ -11,51 +12,47 @@ filterTests(['smoke', 'all'], () => { cy.createTestTableWithData() cy.wait(2000) cy.contains("Automate").click() - cy.get(".add-button .spectrum-Icon").click() - cy.get(".modal-inner-wrapper").within(() => { + cy.get(interact.ADD_BUTTON_SPECTRUM).click() + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { cy.get("input").type("Add Row") cy.contains("Row Created").click({ force: true }) - cy.wait(500) - cy.get(".spectrum-Button--cta").click() + cy.get(interact.SPECTRUM_BUTTON_CTA, { timeout: 500 }).click() }) // Setup trigger - cy.get(".spectrum-Picker-label").click() + cy.get(interact.SPECTRUM_PICKER_LABEL).click() cy.wait(500) cy.contains("dog").click() - cy.wait(2000) // Create action - cy.get('[aria-label="AddCircle"]').eq(1).click() - cy.get(".modal-inner-wrapper").within(() => { + cy.get('[aria-label="AddCircle"]', { timeout: 2000 }).eq(1).click() + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { cy.wait(1000) cy.contains("Create Row").trigger('mouseover').click().click() - cy.get(".spectrum-Button--cta").click() + cy.get(interact.SPECTRUM_BUTTON_CTA).click() }) - cy.get(".spectrum-Picker-label").eq(1).click() + cy.get(interact.SPECTRUM_PICKER_LABEL).eq(1).click() cy.contains("dog").click() - cy.get(".spectrum-Textfield-input") + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT) .first() .type("{{ trigger.row.name }}", { parseSpecialCharSequences: false }) - cy.get(".spectrum-Textfield-input") + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT) .eq(1) .type("11") cy.contains("Finish and test automation").click() - cy.get(".modal-inner-wrapper").within(() => { - cy.wait(1000) - cy.get(".spectrum-Picker-label").click() + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { + cy.get(interact.SPECTRUM_PICKER_LABEL, { timeout: 1000 }).click() cy.contains("dog").click() - cy.wait(1000) - cy.get(".spectrum-Textfield-input") + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }) .first() .type("automationGoodboy") - cy.get(".spectrum-Textfield-input") + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT) .eq(1) .type("11") - cy.get(".spectrum-Textfield-input") + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT) .eq(2) .type("123456") - cy.get(".spectrum-Textfield-input") + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT) .eq(3) .type("123456") cy.contains("Test").click() diff --git a/packages/builder/cypress/integration/createComponents.spec.js b/packages/builder/cypress/integration/createComponents.spec.js index 43a76c700e..eff7fc216f 100644 --- a/packages/builder/cypress/integration/createComponents.spec.js +++ b/packages/builder/cypress/integration/createComponents.spec.js @@ -1,10 +1,8 @@ -// TODO for now components are skipped, might not be good to keep doing this - import filterTests from "../support/filterTests" -const interact = require('../support/interact') +const interact = require("../support/interact") -filterTests(['all'], () => { - xcontext("Create Components", () => { +filterTests(["all"], () => { + context("Create Components", () => { let headlineId before(() => { @@ -13,12 +11,27 @@ filterTests(['all'], () => { cy.createTable("dog") cy.addColumn("dog", "name", "Text") cy.addColumn("dog", "age", "Number") - cy.addColumn("dog", "type", "Options") + cy.addColumn("dog", "breed", "Options") + cy.navigateToFrontend() + cy.wait(1000) //allow the iframe some wiggle room }) + //Use the tree to delete a selected component + const deleteSelectedComponent = () => { + cy.get( + ".nav-items-container .nav-item.selected .actions > div > .icon" + ).click({ + force: true, + }) + cy.get(".spectrum-Popover.is-open li").contains("Delete").click() + cy.get(".spectrum-Modal button").contains("Delete Component").click({ + force: true, + }) + } + it("should add a container", () => { - cy.addComponent(null, "Container").then(componentId => { + cy.addComponent("Layout", "Container").then(componentId => { cy.getComponent(componentId).should("exist") }) }) @@ -32,44 +45,41 @@ filterTests(['all'], () => { it("should change the text of the headline", () => { const text = "Lorem ipsum dolor sit amet." - cy.get(interact.SETTINGS).click() - cy.get(interact.SETTINGS_INPUT) - .type(text) - .blur() + cy.get("[data-cy=setting-text] input").type(text).blur() cy.getComponent(headlineId).should("have.text", text) }) it("should change the size of the headline", () => { - cy.get(interact.DESIGN).click() - cy.contains("Typography").click() - cy.get(interact.FONT_SIZE_PROP_CONTROL).click() - cy.contains("60px").click() - cy.getComponent(headlineId).should("have.css", "font-size", "60px") + cy.get("[data-cy=setting-size]").scrollIntoView().click() + cy.get("[data-cy=setting-size]").within(() => { + cy.get(".spectrum-Form-item li.spectrum-Menu-item") + .contains("3XL") + .click() + }) + + cy.getComponent(headlineId).within(() => { + cy.get(".spectrum-Heading").should("have.css", "font-size", "60px") + }) }) it("should create a form and reset to match schema", () => { cy.addComponent("Form", "Form").then(() => { - cy.get(interact.SETTINGS).click() - cy.get(interact.DATA_CY_DATASOURCE) - .contains("Choose option") - .click() - cy.get(interact.DROPDOWN) - .contains("dog") - .click() + cy.get("[data-cy=setting-dataSource]").contains("Custom").click() + cy.get(interact.DROPDOWN).contains("dog").click() + cy.wait(500) cy.addComponent("Form", "Field Group").then(fieldGroupId => { - cy.get(interact.SETTINGS).click() - cy.contains("Update Form Fields").click() - cy.get(".modal") - .get("button.primary") + cy.contains("Update form fields").click() + cy.get(".spectrum-Modal") + .get(".confirm-wrap .spectrum-Button") .click() + cy.wait(500) cy.getComponent(fieldGroupId).within(() => { cy.contains("name").should("exist") cy.contains("age").should("exist") - cy.contains("type").should("exist") + cy.contains("breed").should("exist") + // cy.contains("image").should("exist") }) - cy.getComponent(fieldGroupId) - .find("input") - .should("have.length", 2) + cy.getComponent(fieldGroupId).find("input").should("have.length", 2) cy.getComponent(fieldGroupId) .find(interact.SPECTRUM_PICKER) .should("have.length", 1) @@ -79,20 +89,191 @@ filterTests(['all'], () => { it("deletes a component", () => { cy.addComponent("Elements", "Paragraph").then(componentId => { - cy.get("[data-cy=setting-_instanceName] input") - .type(componentId) - .blur() - cy.get(".ui-nav ul .nav-item.selected .ri-more-line").click({ + cy.get("[data-cy=setting-_instanceName] input").type(componentId).blur() + cy.get( + ".nav-items-container .nav-item.selected .actions > div > .icon" + ).click({ + force: true, + }) + cy.get(".spectrum-Popover.is-open li").contains("Delete").click() + cy.get(".spectrum-Modal button").contains("Delete Component").click({ force: true, }) - cy.get(interact.DROPDOWN_CONTAINER) - .contains("Delete") - .click() - cy.get(".modal") - .contains("Delete Component") - .click() cy.getComponent(componentId).should("not.exist") }) }) + + it("should clear the iframe place holder when a form field has been set", () => { + cy.addComponent("Form", "Form").then(formId => { + //For deletion + cy.get("[data-cy=setting-_instanceName] input") + .clear() + .type(formId) + .blur() + cy.get("[data-cy=setting-dataSource]").contains("Custom").click() + cy.get(".dropdown").contains("dog").click() + + const fieldTypeToColumnName = { + "Text Field": "name", + "Number Field": "age", + "Options Picker": "breed", + } + + const componentTypeLabels = Object.keys(fieldTypeToColumnName) + + const testFieldFocusOnCreate = componentLabel => { + cy.log("Adding: " + componentLabel) + return cy.addComponent("Form", componentLabel).then(componentId => { + cy.getComponent(componentId) + .find(".component-placeholder") + .should("exist") + cy.get("[data-cy=setting-field] button.spectrum-Picker").click() + + //Click the first appropriate field. They are filtered by type + cy.get( + "[data-cy=setting-field] .spectrum-Popover.is-open li.spectrum-Menu-item" + ) + .contains(fieldTypeToColumnName[componentLabel]) + .click() + cy.wait(500) + cy.getComponent(componentId) + .find(".component-placeholder") + .should("not.exist") + }) + } + + cy.wait(500) + cy.wrap(componentTypeLabels) + .each(label => { + return testFieldFocusOnCreate(label) + }) + .then(() => { + cy.get(".nav-items-container .nav-item") + .contains(formId) + .click({ force: true }) + deleteSelectedComponent() + }) + }) + }) + + it("should populate the provider for charts with a data provider in its path", () => { + cy.addComponent("Data", "Data Provider").then(providerId => { + //For deletion + cy.get("[data-cy=setting-_instanceName] input") + .clear() + .type(providerId) + .blur() + cy.get("[data-cy=setting-dataSource]") + .contains("Choose an option") + .click() + cy.get(`[data-cy=dataSource-popover-${providerId}] ul li`) + .contains("dog") + .click() + + const chartTypeLabels = [ + "Bar Chart", + "Line Chart", + "Area Chart", + "Pie Chart", + "Donut Chart", + "Candlestick Chart", + ] + + const testFocusOnCreate = chartLabel => { + cy.log("Adding: " + chartLabel) + cy.addComponent("Chart", chartLabel).then(componentId => { + cy.get( + "[data-cy=dataProvider-prop-control] .spectrum-Picker" + ).should("not.have.class", "is-focused") + + // Pre populated. + cy.get("[data-cy=dataProvider-prop-control] .spectrum-Picker-label") + .contains(providerId) + .should("exist") + }) + } + cy.wait(1000) + cy.wrap(chartTypeLabels) + .each(label => { + return testFocusOnCreate(label) + }) + .then(() => { + cy.get(".nav-items-container .nav-item") + .contains(providerId) + .click({ force: true }) + deleteSelectedComponent() + }) + }) + }) + + it("should replace the placeholder when a url is set on an image", () => { + cy.addComponent("Elements", "Image").then(imageId => { + cy.get("[data-cy=setting-_instanceName] input") + .clear() + .type(imageId) + .blur() + //return $("New Data Provider.Rows")[0]["Attachment"][0]["url"] + //No minio, so just enter something local that will not reslove + cy.get("[data-cy=url-prop-control] input[type=text]") + .type("cypress/fixtures/ghost.png") + .blur() + cy.getComponent(imageId) + .find(".component-placeholder") + .should("not.exist") + cy.getComponent(imageId).find(`img[alt=${imageId}]`).should("exist") + cy.get(".nav-items-container .nav-item") + .contains(imageId) + .click({ force: true }) + deleteSelectedComponent() + }) + }) + + it("should add a markdown component.", () => { + cy.addComponent("Elements", "Markdown Viewer").then(markdownId => { + cy.get("[data-cy=setting-_instanceName] input") + .clear() + .type(markdownId) + .blur() + cy.get( + "[data-cy=value-prop-control] input[type=text].spectrum-Textfield-input" + ) + .type("# Hi") + .blur() + cy.getComponent(markdownId) + .find(".component-placeholder") + .should("not.exist") + cy.getComponent(markdownId) + .find(".editor-preview-full h1") + .contains("Hi") + cy.get(".nav-items-container .nav-item") + .contains(markdownId) + .click({ force: true }) + deleteSelectedComponent() + }) + }) + + it("should direct the user when adding an Icon component.", () => { + cy.addComponent("Elements", "Icon").then(iconId => { + cy.getComponent(iconId).find(".component-placeholder").should("exist") + cy.get("[data-cy=setting-_instanceName] input") + .clear() + .type(iconId) + .blur() + cy.get("[data-cy=icon-prop-control] .spectrum-ActionButton").click() + cy.get("[data-cy=icon-popover].spectrum-Popover.is-open").within(() => { + cy.get(".search-input input").type("save").blur() + cy.get(".search-input button").click({ force: true }) + cy.get(".icon-area .icon-container").eq(0).click({ force: true }) + }) + cy.getComponent(iconId) + .find(".component-placeholder") + .should("not.exist") + cy.getComponent(iconId).find("i.ri-save-fill").should("exist") + cy.get(".nav-items-container .nav-item") + .contains(iconId) + .click({ force: true }) + deleteSelectedComponent() + }) + }) }) }) diff --git a/packages/builder/cypress/integration/createScreen.js b/packages/builder/cypress/integration/createScreen.spec.js similarity index 100% rename from packages/builder/cypress/integration/createScreen.js rename to packages/builder/cypress/integration/createScreen.spec.js diff --git a/packages/builder/cypress/integration/createTable.spec.js b/packages/builder/cypress/integration/createTable.spec.js index 4600807cbc..da73c19fa6 100644 --- a/packages/builder/cypress/integration/createTable.spec.js +++ b/packages/builder/cypress/integration/createTable.spec.js @@ -1,4 +1,5 @@ import filterTests from "../support/filterTests" +const interact = require('../support/interact') filterTests(["smoke", "all"], () => { context("Create a Table", () => { @@ -9,9 +10,8 @@ filterTests(["smoke", "all"], () => { it("should create a new Table", () => { cy.createTable("dog") - cy.wait(1000) // Check if Table exists - cy.get(".table-title h1").should("have.text", "dog") + cy.get(interact.TABLE_TITLE_H1, { timeout: 1000 }).should("have.text", "dog") }) it("adds a new column to the table", () => { @@ -25,13 +25,13 @@ filterTests(["smoke", "all"], () => { }) it("updates a column on the table", () => { - cy.get(".title").click() - cy.get(".spectrum-Table-editIcon > use").click() - cy.get(".modal-inner-wrapper").within(() => { + cy.get(interact.TABLE_TITLE).click() + cy.get(interact.SPECTRUM_TABLE_EDIT).click() + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { cy.get("input").eq(0).type("updated", { force: true }) // Unset table display column - cy.get(".spectrum-Switch-input").eq(1).click() + cy.get(interact.SPECTRUM_SWITCH_INPUT).eq(1).click() cy.contains("Save Column").click() }) cy.contains("nameupdated ").should("contain", "nameupdated") @@ -39,17 +39,17 @@ filterTests(["smoke", "all"], () => { it("edits a row", () => { cy.contains("button", "Edit").click({ force: true }) - cy.wait(1000) - cy.get(".spectrum-Modal input").clear() - cy.get(".spectrum-Modal input").type("Updated") + cy.wait(500) + cy.get(interact.SPECTRUM_MODAL_INPUT).clear() + cy.get(interact.SPECTRUM_MODAL_INPUT).type("Updated") cy.contains("Save").click() cy.contains("Updated").should("have.text", "Updated") }) it("deletes a row", () => { - cy.get(".spectrum-Checkbox-input").check({ force: true }) + cy.get(interact.SPECTRUM_CHECKBOX_INPUT).check({ force: true }) cy.contains("Delete 1 row(s)").click() - cy.get(".spectrum-Modal").contains("Delete").click() + cy.get(interact.SPECTRUM_MODAL).contains("Delete").click() cy.contains("RoverUpdated").should("not.exist") }) @@ -62,51 +62,49 @@ filterTests(["smoke", "all"], () => { cy.addRow([i]) } cy.reload() - cy.wait(2000) - cy.get(".spectrum-Pagination").within(() => { - cy.get(".spectrum-ActionButton").eq(1).click() + cy.get(interact.SPECTRUM_PAGINATION, { timeout: 2000 }).within(() => { + cy.get(interact.SPECTRUM_ACTION_BUTTON).eq(1).click() }) - cy.get(".spectrum-Pagination").within(() => { - cy.get(".spectrum-Body--secondary").contains("Page 2") + cy.get(interact.SPECTRUM_PAGINATION).within(() => { + cy.get(interact.SPECTRUM_BODY_SECOND).contains("Page 2") }) }) xit("Deletes rows and checks pagination", () => { // Delete rows, removing second page from table - cy.get(".spectrum-Checkbox-input").check({ force: true }) - cy.get(".popovers").within(() => { - cy.get(".spectrum-Button").click({ force: true }) + cy.get(interact.SPECTRUM_CHECKBOX_INPUT).check({ force: true }) + cy.get(interact.POPOVERS).within(() => { + cy.get(interact.SPECTRUM_BUTTON).click({ force: true }) }) - cy.get(".spectrum-Dialog-grid").contains("Delete").click({ force: true }) - cy.wait(1000) + cy.get(interact.SPECTRUM_DIALOG_GRID).contains("Delete").click({ force: true }) // Confirm table only has one page - cy.get(".spectrum-Pagination").within(() => { - cy.get(".spectrum-ActionButton").eq(1).should("not.be.enabled") + cy.get(interact.SPECTRUM_PAGINATION, { timeout: 1000 }).within(() => { + cy.get(interact.SPECTRUM_ACTION_BUTTON).eq(1).should("not.be.enabled") }) }) } it("deletes a column", () => { const columnName = "nameupdated" - cy.get(".title").click() - cy.get(".spectrum-Table-editIcon > use").click() + cy.get(interact.TABLE_TITLE).click() + cy.get(interact.SPECTRUM_TABLE_EDIT).click() cy.contains("Delete").click() - cy.get('[data-cy="delete-column-confirm"]').type(columnName) + cy.get(interact.DELETE_COLUMN_CONFIRM).type(columnName) cy.contains("Delete Column").click() cy.contains("nameupdated").should("not.exist") }) it("deletes a table", () => { - cy.get(".nav-item") + cy.get(interact.NAV_ITEM) .contains("dog") - .parents(".nav-item") + .parents(interact.NAV_ITEM) .first() .within(() => { - cy.get(".actions .spectrum-Icon").click({ force: true }) + cy.get(interact.ACTION_SPECTRUM_ICON).click({ force: true }) }) - cy.get(".spectrum-Menu > :nth-child(2)").click() - cy.get('[data-cy="delete-table-confirm"]').type("dog") + cy.get(interact.SPECTRUM_MENU_CHILD2).click() + cy.get(interact.DELETE_TABLE_CONFIRM).type("dog") cy.contains("Delete Table").click() cy.contains("dog").should("not.exist") }) diff --git a/packages/builder/cypress/integration/createUserAndRoles.spec.js b/packages/builder/cypress/integration/createUserAndRoles.spec.js deleted file mode 100644 index ac7ec1b5fd..0000000000 --- a/packages/builder/cypress/integration/createUserAndRoles.spec.js +++ /dev/null @@ -1,194 +0,0 @@ -import filterTests from "../support/filterTests" - -filterTests(["smoke", "all"], () => { - context("Create a User and Assign Roles", () => { - before(() => { - cy.login() - cy.deleteApp("Cypress Tests") - cy.createApp("Cypress Tests") - }) - - it("should create a user", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) - cy.createUser("bbuser@test.com") - cy.get(".spectrum-Table").should("contain", "bbuser") - }) - - it("should confirm there is No Access for a New User", () => { - // Click into the user - cy.contains("bbuser").click() - cy.wait(500) - // Get No Access table - Confirm it has apps in it - cy.get(".spectrum-Table").eq(1).should("not.contain", "No rows found") - // Get Configure Roles table - Confirm it has no apps - cy.get(".spectrum-Table").eq(0).contains("No rows found") - }) - - if (Cypress.env("TEST_ENV")) { - it("should assign role types", () => { - // 3 apps minimum required - to assign an app to each role type - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length < 3) { - for (let i = 1; i < 3; i++) { - const uuid = () => Cypress._.random(0, 1e6) - const name = uuid() - if(i < 1){ - cy.createApp(name) - } else { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) - cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) - cy.createAppFromScratch(name) - } - } - } - }) - // Navigate back to the user - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) - cy.get(".spectrum-SideNav").contains("Users").click() - cy.wait(500) - cy.get(".spectrum-Table").contains("bbuser").click() - cy.wait(1000) - for (let i = 0; i < 3; i++) { - cy.get(".spectrum-Table", { timeout: 3000}) - .eq(1) - .find(".spectrum-Table-row") - .eq(0) - .find(".spectrum-Table-cell") - .eq(0) - .click() - cy.wait(500) - cy.get(".spectrum-Dialog-grid") - .contains("Choose an option") - .click() - .then(() => { - cy.wait(1000) - if (i == 0) { - cy.get(".spectrum-Menu").contains("Admin").click({ force: true }) - } - else if (i == 1) { - cy.get(".spectrum-Menu").contains("Power").click({ force: true }) - } - else if (i == 2) { - cy.get(".spectrum-Menu").contains("Basic").click({ force: true }) - } - cy.wait(1000) - cy.get(".spectrum-Button") - .contains("Update role") - .click({ force: true }) - }) - cy.reload() - } - // Confirm roles exist within Configure roles table - cy.wait(2000) - cy.get(".spectrum-Table") - .eq(0) - .within(assginedRoles => { - expect(assginedRoles).to.contain("Admin") - expect(assginedRoles).to.contain("Power") - expect(assginedRoles).to.contain("Basic") - }) - }) - - it("should unassign role types", () => { - // Set each app within Configure roles table to 'No Access' - cy.get(".spectrum-Table") - .eq(0) - .find(".spectrum-Table-row") - .its("length") - .then(len => { - for (let i = 0; i < len; i++) { - cy.get(".spectrum-Table") - .eq(0) - .find(".spectrum-Table-row") - .eq(0) - .find(".spectrum-Table-cell") - .eq(0) - .click() - .then(() => { - cy.get(".spectrum-Picker").eq(1).click({ force: true }) - cy.wait(500) - cy.get(".spectrum-Popover").contains("No Access").click() - }) - cy.get(".spectrum-Button") - .contains("Update role") - .click({ force: true }) - cy.wait(1000) - } - }) - // Confirm Configure roles table no longer has any apps in it - cy.get(".spectrum-Table").eq(0).contains("No rows found") - }) - } - - it("should enable Developer access", () => { - // Enable Developer access - cy.get(".field") - .eq(4) - .within(() => { - cy.get(".spectrum-Switch-input").click({ force: true }) - }) - // No Access table should now be empty - cy.get(".container") - .contains("No Access") - .parent() - .within(() => { - cy.get(".spectrum-Table").contains("No rows found") - }) - - // Each app within Configure roles should have Admin access - cy.get(".spectrum-Table") - .eq(0) - .find(".spectrum-Table-row") - .its("length") - .then(len => { - for (let i = 0; i < len; i++) { - cy.get(".spectrum-Table") - .eq(0) - .find(".spectrum-Table-row") - .eq(i) - .contains("Admin") - cy.wait(500) - } - }) - }) - - it("should disable Developer access", () => { - // Disable Developer access - cy.get(".field") - .eq(4) - .within(() => { - cy.get(".spectrum-Switch-input").click({ force: true }) - }) - // Configure roles table should now be empty - cy.get(".container") - .contains("Configure roles") - .parent() - .within(() => { - cy.get(".spectrum-Table").contains("No rows found") - }) - }) - - it("should delete a user", () => { - // Click Delete user button - cy.get(".spectrum-Button") - .contains("Delete user") - .click({ force: true }) - .then(() => { - // Confirm deletion within modal - cy.wait(500) - cy.get(".spectrum-Dialog-grid").within(() => { - cy.get(".spectrum-Button") - .contains("Delete user") - .click({ force: true }) - cy.wait(4000) - }) - }) - cy.get(".spectrum-Table").should("not.have.text", "bbuser") - }) - }) -}) diff --git a/packages/builder/cypress/integration/createView.spec.js b/packages/builder/cypress/integration/createView.spec.js index feaf1c3b5f..a2d09d97bf 100644 --- a/packages/builder/cypress/integration/createView.spec.js +++ b/packages/builder/cypress/integration/createView.spec.js @@ -1,4 +1,5 @@ import filterTests from "../support/filterTests" +const interact = require('../support/interact') filterTests(['smoke', 'all'], () => { context("Create a View", () => { @@ -22,12 +23,12 @@ filterTests(['smoke', 'all'], () => { it("creates a view", () => { cy.contains("Create view").click() - cy.get(".modal-inner-wrapper").within(() => { + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { cy.get("input").type("Test View") cy.get("button").contains("Create View").click({ force: true }) }) - cy.get(".table-title h1").contains("Test View") - cy.get(".title").then($headers => { + cy.get(interact.TABLE_TITLE_H1).contains("Test View") + cy.get(interact.TITLE).then($headers => { expect($headers).to.have.length(3) const headers = Array.from($headers).map(header => header.textContent.trim() @@ -40,18 +41,18 @@ filterTests(['smoke', 'all'], () => { cy.contains("Filter").click() cy.contains("Add Filter").click() - cy.get(".modal-inner-wrapper").within(() => { - cy.get(".spectrum-Picker-label").eq(0).click() + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { + cy.get(interact.SPECTRUM_PICKER_LABEL).eq(0).click() cy.contains("age").click({ force: true }) - cy.get(".spectrum-Picker-label").eq(1).click() + cy.get(interact.SPECTRUM_PICKER_LABEL).eq(1).click() cy.contains("More Than").click({ force: true }) cy.get("input").type(18) cy.contains("Save").click() }) - cy.get(".spectrum-Table-row").get($values => { + cy.get(interact.SPECTRUM_TABLE_ROW).get($values => { expect($values).to.have.length(5) }) }) @@ -59,18 +60,18 @@ filterTests(['smoke', 'all'], () => { it("creates a stats calculation view based on age", () => { cy.wait(1000) cy.contains("Calculate").click() - cy.get(".modal-inner-wrapper").within(() => { - cy.get(".spectrum-Picker-label").eq(0).click() + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { + cy.get(interact.SPECTRUM_PICKER_LABEL).eq(0).click() cy.contains("Statistics").click() - cy.get(".spectrum-Picker-label").eq(1).click() + cy.get(interact.SPECTRUM_PICKER_LABEL).eq(1).click() cy.contains("age").click({ force: true }) - cy.get(".spectrum-Button").contains("Save").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON).contains("Save").click({ force: true }) }) - cy.wait(1000) - cy.get(".title").then($headers => { + cy.wait(1000) + cy.get(interact.TITLE).then($headers => { expect($headers).to.have.length(7) const headers = Array.from($headers).map(header => header.textContent.trim() @@ -85,7 +86,7 @@ filterTests(['smoke', 'all'], () => { "avg", ]) }) - cy.get(".spectrum-Table-cell").then($values => { + cy.get(interact.SPECTRUM_TABLE_CELL).then($values => { let values = Array.from($values).map(header => header.textContent.trim()) expect(values).to.deep.eq(["age", "155", "20", "49", "5", "5347", "31"]) }) @@ -93,8 +94,8 @@ filterTests(['smoke', 'all'], () => { it("groups the view by group", () => { cy.contains("Group by").click() - cy.get(".modal-inner-wrapper").within(() => { - cy.get(".spectrum-Picker-label").eq(0).click() + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { + cy.get(interact.SPECTRUM_PICKER_LABEL).eq(0).click() cy.contains("group").click() cy.contains("Save").click() }) @@ -102,7 +103,7 @@ filterTests(['smoke', 'all'], () => { cy.contains("Students").should("be.visible") cy.contains("Teachers").should("be.visible") - cy.get(".spectrum-Table-cell").then($values => { + cy.get(interact.SPECTRUM_TABLE_CELL).then($values => { let values = Array.from($values).map(header => header.textContent.trim()) expect(values).to.deep.eq([ "Students", @@ -124,11 +125,11 @@ filterTests(['smoke', 'all'], () => { }) it("renames a view", () => { - cy.contains(".nav-item", "Test View") + cy.contains(interact.NAV_ITEM, "Test View") .find(".actions .icon.open-popover") .click({ force: true }) - cy.get(".spectrum-Menu-itemLabel").contains("Edit").click() - cy.get(".modal-inner-wrapper").within(() => { + cy.get(interact.SPECTRUM_MENU_ITEM_LABEL).contains("Edit").click() + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { cy.get("input").type(" Updated") cy.contains("Save").click() }) @@ -137,7 +138,7 @@ filterTests(['smoke', 'all'], () => { }) it("deletes a view", () => { - cy.contains(".nav-item", "Test View Updated") + cy.contains(interact.NAV_ITEM, "Test View Updated") .find(".actions .icon.open-popover") .click({ force: true }) cy.contains("Delete").click() diff --git a/packages/builder/cypress/integration/customThemingProperties.spec.js b/packages/builder/cypress/integration/customThemingProperties.spec.js index ed3478ca67..e9de0985d0 100644 --- a/packages/builder/cypress/integration/customThemingProperties.spec.js +++ b/packages/builder/cypress/integration/customThemingProperties.spec.js @@ -34,7 +34,6 @@ filterTests(['all'], () => { Large = 16px */ it("should test button roundness", () => { const buttonRoundnessValues = ["0", "4px", "8px", "16px"] - cy.wait(1000) // Add button, change roundness and confirm value cy.addComponent("Button", null).then((componentId) => { buttonRoundnessValues.forEach(function (item, index){ diff --git a/packages/builder/cypress/integration/datasources/datasourceWizard.spec.js b/packages/builder/cypress/integration/datasources/datasourceWizard.spec.js index 1bee7b5ec1..14653d8286 100644 --- a/packages/builder/cypress/integration/datasources/datasourceWizard.spec.js +++ b/packages/builder/cypress/integration/datasources/datasourceWizard.spec.js @@ -17,11 +17,10 @@ filterTests(['all'], () => { // Navigate back within datasource wizard cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Button").contains("Back").click({ force: true }) - cy.wait(1000) }) // Select PostgreSQL datasource again - cy.get(".item-list").contains(datasource).click() + cy.get(".item-list", { timeout: 1000 }).contains(datasource).click() cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Button").contains("Continue").click({ force: true }) }) diff --git a/packages/builder/cypress/integration/datasources/mySql.spec.js b/packages/builder/cypress/integration/datasources/mySql.spec.js index 98bb2f2acf..b79f5af9c6 100644 --- a/packages/builder/cypress/integration/datasources/mySql.spec.js +++ b/packages/builder/cypress/integration/datasources/mySql.spec.js @@ -111,10 +111,9 @@ filterTests(["all"], () => { // Save relationship & reload page cy.get(".spectrum-Button").contains("Save").click({ force: true }) cy.reload() - cy.wait(1000) }) // Confirm table length & relationship name - cy.get(".spectrum-Table") + cy.get(".spectrum-Table", { timeout: 1000 }) .eq(1) .find(".spectrum-Table-row") .its("length") @@ -136,15 +135,15 @@ filterTests(["all"], () => { cy.get(".spectrum-Table") .eq(1) .within(() => { - cy.get(".spectrum-Table-row").eq(0).click({ force: true }) - cy.wait(500) + cy.get(".spectrum-Table-cell").eq(0).click({ force: true }) }) - cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { cy.get(".spectrum-Button") .contains("Delete") .click({ force: true }) }) cy.reload() + cy.wait(500) } // Confirm relationships no longer exist cy.get(".spectrum-Body").should( @@ -217,9 +216,8 @@ filterTests(["all"], () => { cy.get(".spectrum-Button") .contains("Delete Query") .click({ force: true }) - cy.wait(1000) // Confirm deletion - cy.get(".nav-item").should("not.contain", queryName) + cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryName) }) } }) diff --git a/packages/builder/cypress/integration/datasources/oracle.spec.js b/packages/builder/cypress/integration/datasources/oracle.spec.js index 4c4d33d654..92a5737ff9 100644 --- a/packages/builder/cypress/integration/datasources/oracle.spec.js +++ b/packages/builder/cypress/integration/datasources/oracle.spec.js @@ -20,7 +20,7 @@ filterTests(["all"], () => { .click({ force: true }) cy.wait(500) // Confirm config contains localhost - cy.get(".spectrum-Textfield-input") + cy.get(".spectrum-Textfield-input", { timeout: 500 }) .eq(1) .should("have.value", "localhost") // Add another Oracle data source, configure & skip table fetch @@ -140,9 +140,8 @@ filterTests(["all"], () => { .eq(1) .within(() => { cy.get(".spectrum-Table-row").eq(0).click() - cy.wait(500) }) - cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { cy.get(".spectrum-Button") .contains("Delete") .click({ force: true }) @@ -221,10 +220,9 @@ filterTests(["all"], () => { cy.get(".spectrum-Button") .contains("Delete Query") .click({ force: true }) - cy.wait(1000) // Confirm deletion - cy.get(".nav-item").should("not.contain", queryName) + cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryName) }) } }) diff --git a/packages/builder/cypress/integration/datasources/postgreSql.spec.js b/packages/builder/cypress/integration/datasources/postgreSql.spec.js index 7448e6b27d..de959e203c 100644 --- a/packages/builder/cypress/integration/datasources/postgreSql.spec.js +++ b/packages/builder/cypress/integration/datasources/postgreSql.spec.js @@ -35,6 +35,7 @@ filterTests(["all"], () => { // Check response from datasource after adding configuration cy.wait("@datasource") cy.get("@datasource").its("response.statusCode").should("eq", 200) + cy.wait(2000) // Confirm fetch tables was successful cy.get(".spectrum-Table") .eq(0) @@ -113,13 +114,13 @@ filterTests(["all"], () => { cy.get(".spectrum-Table") .eq(1) .within(() => { - cy.get(".spectrum-Table-row").eq(0).click({ force: true }) - cy.wait(500) + cy.get(".spectrum-Table-cell").eq(0).click({ force: true }) }) - cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { cy.get(".spectrum-Button").contains("Delete").click({ force: true }) }) cy.reload() + cy.wait(500) // Confirm relationship was deleted cy.get(".spectrum-Table") .eq(1) @@ -159,7 +160,7 @@ filterTests(["all"], () => { switchSchema("randomText") // No tables displayed - cy.get(".spectrum-Body").eq(2).should("contain", "No tables found") + cy.get(".spectrum-Body", { timeout: 5000 }).eq(2).should("contain", "No tables found") // Previously created query should be visible cy.get(".spectrum-Table").should("contain", queryName) @@ -170,7 +171,7 @@ filterTests(["all"], () => { switchSchema("1") // Confirm tables exist - Check for specific one - cy.get(".spectrum-Table").eq(0).should("contain", "test") + cy.get(".spectrum-Table", { timeout: 5000 }).eq(0).should("contain", "test") cy.get(".spectrum-Table") .eq(0) .find(".spectrum-Table-row") @@ -184,7 +185,7 @@ filterTests(["all"], () => { switchSchema("public") // Confirm tables exist - again - cy.get(".spectrum-Table").eq(0).should("contain", "REGIONS") + cy.get(".spectrum-Table", { timeout: 5000 }).eq(0).should("contain", "REGIONS") cy.get(".spectrum-Table") .eq(0) .find(".spectrum-Table-row") @@ -230,7 +231,9 @@ filterTests(["all"], () => { // Run and Save query cy.get(".spectrum-Button").contains("Run Query").click({ force: true }) cy.wait(500) - cy.get(".spectrum-Button").contains("Save Query").click({ force: true }) + cy.get(".spectrum-Button", { timeout: 500 }).contains("Save Query").click({ force: true }) + //cy.reload() + //cy.wait(500) cy.get(".nav-item").should("contain", queryRename) }) @@ -247,9 +250,8 @@ filterTests(["all"], () => { cy.get(".spectrum-Button") .contains("Delete Query") .click({ force: true }) - cy.wait(1000) // Confirm deletion - cy.get(".nav-item").should("not.contain", queryName) + cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryName) }) const switchSchema = schema => { @@ -271,7 +273,7 @@ filterTests(["all"], () => { .click({ force: true }) }) cy.reload() - cy.wait(5000) + cy.wait(1000) } } }) diff --git a/packages/builder/cypress/integration/datasources/rest.spec.js b/packages/builder/cypress/integration/datasources/rest.spec.js index a15487c01b..488c30c0cf 100644 --- a/packages/builder/cypress/integration/datasources/rest.spec.js +++ b/packages/builder/cypress/integration/datasources/rest.spec.js @@ -14,8 +14,7 @@ filterTests(["smoke", "all"], () => { // Select REST data source cy.selectExternalDatasource(datasource) // Enter incorrect api & attempt to send query - cy.wait(500) - cy.get(".spectrum-Button").contains("Add query").click({ force: true }) + cy.get(".spectrum-Button", { timeout: 500 }).contains("Add query").click({ force: true }) cy.intercept("**/preview").as("queryError") cy.get("input").clear().type("random text") cy.get(".spectrum-Button").contains("Send").click({ force: true }) @@ -36,8 +35,7 @@ filterTests(["smoke", "all"], () => { // createRestQuery confirms query creation cy.createRestQuery("GET", restUrl, "/breweries") // Confirm status code response within REST datasource - cy.wait(1000) - cy.get(".stats").within(() => { + cy.get(".stats", { timeout: 1000 }).within(() => { cy.get(".spectrum-FieldLabel") .eq(0) .should("contain", 200) diff --git a/packages/builder/cypress/integration/queryLevelTransformers.spec.js b/packages/builder/cypress/integration/queryLevelTransformers.spec.js index bec0825e70..2b74e0c2e5 100644 --- a/packages/builder/cypress/integration/queryLevelTransformers.spec.js +++ b/packages/builder/cypress/integration/queryLevelTransformers.spec.js @@ -1,4 +1,5 @@ import filterTests from "../support/filterTests" +const interact = require('../support/interact') filterTests(["smoke", "all"], () => { context("Query Level Transformers", () => { @@ -13,11 +14,11 @@ filterTests(["smoke", "all"], () => { const restUrl = "https://api.openbrewerydb.org/breweries" cy.selectExternalDatasource(datasource) cy.createRestQuery("GET", restUrl, "/breweries") - cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click() + cy.get(interact.SPECTRUM_TABS_ITEM).contains("Transformer").click() // Get Transformer Function from file cy.readFile("cypress/support/queryLevelTransformerFunction.js").then( transformerFunction => { - cy.get(".CodeMirror textarea") + cy.get(interact.CODEMIRROR_TEXTAREA) // Highlight current text and overwrite with file contents .type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", { force: true, @@ -27,7 +28,7 @@ filterTests(["smoke", "all"], () => { ) // Send Query cy.intercept("**/queries/preview").as("query") - cy.get(".spectrum-Button").contains("Send").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON).contains("Send").click({ force: true }) cy.wait("@query") // Assert against Status Code, body, & body rows cy.get("@query").its("response.statusCode").should("eq", 200) @@ -41,13 +42,13 @@ filterTests(["smoke", "all"], () => { const restUrl = "https://api.openbrewerydb.org/breweries" cy.selectExternalDatasource(datasource) cy.createRestQuery("GET", restUrl, "/breweries") - cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click() + cy.get(interact.SPECTRUM_TABS_ITEM).contains("Transformer").click() // Get Transformer Function with Data from file cy.readFile( "cypress/support/queryLevelTransformerFunctionWithData.js" ).then(transformerFunction => { //console.log(transformerFunction[1]) - cy.get(".CodeMirror textarea") + cy.get(interact.CODEMIRROR_TEXTAREA) // Highlight current text and overwrite with file contents .type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", { force: true, @@ -56,7 +57,7 @@ filterTests(["smoke", "all"], () => { }) // Send Query cy.intercept("**/queries/preview").as("query") - cy.get(".spectrum-Button").contains("Send").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON).contains("Send").click({ force: true }) cy.wait("@query") // Assert against Status Code, body, & body rows cy.get("@query").its("response.statusCode").should("eq", 200) @@ -70,16 +71,16 @@ filterTests(["smoke", "all"], () => { const restUrl = "https://api.openbrewerydb.org/breweries" cy.selectExternalDatasource(datasource) cy.createRestQuery("GET", restUrl, "/breweries") - cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click() + cy.get(interact.SPECTRUM_TABS_ITEM).contains("Transformer").click() // Clear the code box and add "test" - cy.get(".CodeMirror textarea") + cy.get(interact.CODEMIRROR_TEXTAREA) .type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", { force: true, }) .type("test") // Run Query and intercept cy.intercept("**/preview").as("queryError") - cy.get(".spectrum-Button").contains("Send").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON).contains("Send").click({ force: true }) cy.wait("@queryError") cy.wait(500) // Assert against message and status for the query error diff --git a/packages/builder/cypress/integration/renameAnApplication.spec.js b/packages/builder/cypress/integration/renameAnApplication.spec.js index 7e611ac4ec..370efadff2 100644 --- a/packages/builder/cypress/integration/renameAnApplication.spec.js +++ b/packages/builder/cypress/integration/renameAnApplication.spec.js @@ -1,6 +1,7 @@ import filterTests from "../support/filterTests" +const interact = require("../support/interact") -filterTests(['all'], () => { +filterTests(["all"], () => { context("Rename an App", () => { beforeEach(() => { cy.login() @@ -12,16 +13,13 @@ filterTests(['all'], () => { const appRename = "Cypress Renamed" // Rename app, Search for app, Confirm name was changed cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) renameApp(appName, appRename) cy.reload() - cy.wait(1000) cy.searchForApplication(appRename) - cy.get(".appTable").find(".title").should("have.length", 1) + cy.get(interact.APP_TABLE).find(interact.TITLE).should("have.length", 1) cy.applicationInAppTable(appRename) // Set app name back to Cypress Tests cy.reload() - cy.wait(1000) renameApp(appRename, appName) }) @@ -30,31 +28,29 @@ filterTests(['all'], () => { const appName = "Cypress Tests" const appRename = "Cypress Renamed" // Publish the app - cy.get(".toprightnav") - cy.get(".spectrum-Button").contains("Publish").click({ force: true }) - cy.get(".spectrum-Dialog-grid") - .within(() => { - // Click publish again within the modal - cy.get(".spectrum-Button").contains("Publish").click({ force: true }) - }) + cy.get(interact.TOP_RIGHT_NAV) + cy.get(interact.SPECTRUM_BUTTON) + .contains("Publish") + .click({ force: true }) + cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => { + // Click publish again within the modal + cy.get(interact.SPECTRUM_BUTTON) + .contains("Publish") + .click({ force: true }) + }) // Rename app, Search for app, Confirm name was changed cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) renameApp(appName, appRename, true) - cy.get(".appTable").find(".wrapper").should("have.length", 1) + cy.get(interact.APP_TABLE).find(interact.WRAPPER).should("have.length", 1) cy.applicationInAppTable(appRename) }) it("Should try to rename an application to have no name", () => { const appName = "Cypress Tests" cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) renameApp(appName, " ", false, true) - cy.wait(500) // Close modal and confirm name has not been changed - cy.get(".spectrum-Dialog-grid").contains("Cancel").click() - cy.reload() - cy.wait(1000) + cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 1000 }).contains("Cancel").click() cy.applicationInAppTable(appName) }) @@ -62,15 +58,20 @@ filterTests(['all'], () => { // It is not possible to have applications with the same name const appName = "Cypress Tests" cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) - cy.get(".spectrum-Button").contains("Create app").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON), { timeout: 500 } + .contains("Create app") + .click({ force: true }) cy.contains(/Start from scratch/).click() - cy.get(".spectrum-Modal") - .within(() => { - cy.get("input").eq(0).type(appName) - cy.get(".spectrum-ButtonGroup").contains("Create app").click({ force: true }) - cy.get(".error").should("have.text", "Another app with the same name already exists") - }) + cy.get(interact.SPECTRUM_MODAL).within(() => { + cy.get("input").eq(0).type(appName) + cy.get(interact.SPECTRUM_BUTTON_GROUP) + .contains("Create app") + .click({ force: true }) + cy.get(interact.ERROR).should( + "have.text", + "Another app with the same name already exists" + ) + }) }) it("should validate application names", () => { @@ -80,41 +81,35 @@ filterTests(['all'], () => { const numberName = 12345 const specialCharName = "£$%^" cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) renameApp(appName, numberName) cy.reload() - cy.wait(1000) cy.applicationInAppTable(numberName) cy.reload() - cy.wait(1000) renameApp(numberName, specialCharName) - cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only") + cy.get(interact.ERROR).should( + "have.text", + "App name must be letters, numbers and spaces only" + ) // Set app name back to Cypress Tests cy.reload() - cy.wait(1000) renameApp(numberName, appName) }) const renameApp = (originalName, changedName, published, noName) => { cy.searchForApplication(originalName) - cy.get(".appTable") - .within(() => { - cy.get("[aria-label='More']").eq(0).click() - }) - // Check for when an app is published - if (published == true) { - // Should not have Edit as option, will unpublish app - cy.should("not.have.value", "Edit") - cy.get(".spectrum-Menu").contains("Unpublish").click() - cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click() - cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click() - } - cy.get("[data-cy='app-row-actions-menu-popover']").eq(0).within(() => { - cy.get(".spectrum-Menu-item").contains("Edit").click({ force: true }) + cy.get(interact.APP_TABLE, { timeout: 1000 }).within(() => { + cy.get(".app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) }) - + cy.get(".spectrum-Tabs-item").contains("Settings").click() + cy.get(".spectrum-Tabs-item.is-selected").contains("Settings") + cy.get(".settings-tab").should("be.visible") + cy.get(".details-section .page-action button") + .contains("Edit") + .click({ force: true }) cy.updateAppName(changedName, noName) - } }) }) diff --git a/packages/builder/cypress/integration/revertApp.spec.js b/packages/builder/cypress/integration/revertApp.spec.js index 01d5a04981..4c6f245b76 100644 --- a/packages/builder/cypress/integration/revertApp.spec.js +++ b/packages/builder/cypress/integration/revertApp.spec.js @@ -1,4 +1,5 @@ import filterTests from "../support/filterTests" +const interact = require('../support/interact') filterTests(['smoke', 'all'], () => { context("Revert apps", () => { @@ -9,15 +10,15 @@ filterTests(['smoke', 'all'], () => { it("should try to revert an unpublished app", () => { // Click revert icon - cy.get(".toprightnav").within(() => { - cy.get("[aria-label='Revert']").click({ force: true }) + cy.get(interact.TOP_RIGHT_NAV).within(() => { + cy.get(interact.AREA_LABEL_REVERT).click({ force: true }) }) - cy.get(".spectrum-Modal").within(() => { + cy.get(interact.SPECTRUM_MODAL).within(() => { // Enter app name before revert cy.get("input").type("Cypress Tests") cy.intercept('**/revert').as('revertApp') // Click Revert - cy.get(".spectrum-Button").contains("Revert").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON).contains("Revert").click({ force: true }) // Intercept Request after button click & apply assertions cy.wait("@revertApp") cy.get("@revertApp").its('response.body').should('have.property', 'message', "App has not yet been deployed") @@ -31,43 +32,42 @@ filterTests(['smoke', 'all'], () => { // Add initial component - Paragraph cy.addComponent("Elements", "Paragraph") // Publish app - cy.get(".spectrum-Button").contains("Publish").click({ force: true }) - cy.get(".spectrum-ButtonGroup").within(() => { - cy.get(".spectrum-Button").contains("Publish").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON).contains("Publish").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON_GROUP).within(() => { + cy.get(interact.SPECTRUM_BUTTON).contains("Publish").click({ force: true }) }) - cy.wait(1000) - cy.get(".spectrum-ButtonGroup").within(() => { - cy.get(".spectrum-Button").contains("Done").click({ force: true }) + cy.wait(1000) // Wait for next modal to finish loading + cy.get(interact.SPECTRUM_BUTTON_GROUP, { timeout: 1000 }).within(() => { + cy.get(interact.SPECTRUM_BUTTON).contains("Done").click({ force: true }) }) // Add second component - Button cy.addComponent("Elements", "Button") // Click Revert - cy.get(".toprightnav").within(() => { - cy.get("[aria-label='Revert']").click({ force: true }) + cy.get(interact.TOP_RIGHT_NAV).within(() => { + cy.get(interact.AREA_LABEL_REVERT).click({ force: true }) }) - cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => { // Click Revert - cy.get(".spectrum-Button").contains("Revert").click({ force: true }) - cy.wait(1000) + cy.get(interact.SPECTRUM_BUTTON).contains("Revert").click({ force: true }) + cy.wait(2000) // Wait for app to finish reverting }) // Confirm Paragraph component is still visible - cy.get(".root").contains("New Paragraph") + cy.get(interact.ROOT, { timeout: 1000 }).contains("New Paragraph") // Confirm Button component is not visible - cy.get(".root").should("not.have.text", "New Button") - cy.wait(500) + cy.get(interact.ROOT, { timeout: 1000 }).should("not.have.text", "New Button") }) it("should enter incorrect app name when reverting", () => { // Click Revert - cy.get(".toprightnav").within(() => { - cy.get("[aria-label='Revert']").click({ force: true }) + cy.get(interact.TOP_RIGHT_NAV, { timeout: 1000 }).within(() => { + cy.get(interact.AREA_LABEL_REVERT).click({ force: true }) }) // Enter incorrect app name - cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => { cy.get("input").type("Cypress Tests") // Revert button within modal should be disabled - cy.get(".spectrum-Button").eq(1).should('be.disabled') + cy.get(interact.SPECTRUM_BUTTON).eq(1).should('be.disabled') }) }) }) diff --git a/packages/builder/cypress/integration/templates/HR/hrTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/HR/hrTemplateDetails.spec.js deleted file mode 100644 index fbac463bfe..0000000000 --- a/packages/builder/cypress/integration/templates/HR/hrTemplateDetails.spec.js +++ /dev/null @@ -1,56 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify HR Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter HR Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="HR"]').click() - }) - }) - - it("should verify the details option for HR templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - - if (templateNameText == "Job Application Tracker") { - // Template name should include 'applicant-tracking-system' - cy.get('a') - .should('have.attr', 'href').and('contain', 'applicant-tracking-system') - } - else if (templateNameText == "Job Portal App") { - // Template name should include 'job-portal' - const templateNameSplit = templateNameParsed.split('-app')[0] - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameSplit) - } - else { - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameParsed) - } - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/HR/jobApplicationTracker.spec.js b/packages/builder/cypress/integration/templates/HR/jobApplicationTracker.spec.js deleted file mode 100644 index 045a85d8f6..0000000000 --- a/packages/builder/cypress/integration/templates/HR/jobApplicationTracker.spec.js +++ /dev/null @@ -1,222 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Job Application Tracker Template Functionality", () => { - const templateName = "Job Application Tracker" - const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-') - - before(() => { - cy.login() - cy.deleteApp(templateName) - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, { - onBeforeLoad(win) { - cy.stub(win, 'open') - } - }) - cy.wait(2000) - }) - - it("should create and publish app with Job Application Tracker template", () => { - // Select Job Application Tracker template - cy.get(".template-thumbnail-text") - .contains(templateName).parentsUntil(".template-grid").within(() => { - cy.get(".spectrum-Button").contains("Use template").click({ force: true }) - }) - - // Confirm URL matches template name - const appUrl = cy.get(".app-server") - appUrl.invoke('text').then(appUrlText => { - expect(appUrlText).to.equal(`${Cypress.config().baseUrl}/app/` + templateNameParsed) - }) - - // Create App - cy.get(".spectrum-Dialog-grid").within(() => { - cy.get(".spectrum-Button").contains("Create app").click({ force: true }) - }) - - // Publish App & Verify it opened - cy.wait(2000) // Wait for app to generate - cy.publishApp(true) - cy.window().its('open').should('be.calledOnce') - }) - - it("should add active/inactive vacancies", () => { - // Visit published app - cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) - - // loop for active/inactive vacancies - for (let i = 0; i < 2; i++) { - // Vacancies section - cy.get(".links").contains("Vacancies").click({ force: true }) - cy.get(".spectrum-Button").contains("Create New").click() - - // Add inactive vacancy - // Title - cy.get('[data-name="Title"]').within(() => { - cy.get(".spectrum-Textfield").type("Tester") - }) - - // Closing Date - cy.get('[data-name="Closing date"]').within(() => { - cy.get('[aria-label=Calendar]').click({ force: true }) - }) - cy.get("[aria-current=date]").click() - - // Department - cy.get('[data-name="Department"]').within(() => { - cy.get(".spectrum-Picker-label").click() - }) - cy.get(".spectrum-Menu").find('li').its('length').then(len => { - cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click() - }) - - // Employment Type - cy.get('[data-name="Employment type"]').within(() => { - cy.get(".spectrum-Picker-label").click() - }) - cy.get(".spectrum-Menu").find('li').its('length').then(len => { - cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click() - }) - - // Salary - cy.get('[data-name="Salary ($)"]').within(() => { - cy.get(".spectrum-Textfield").type(40000) - }) - - // Description - cy.get('[data-name="Description"]').within(() => { - cy.get(".spectrum-Textfield").type("description") - }) - - // Responsibilities - cy.get('[data-name="Responsibilities"]').within(() => { - cy.get(".spectrum-Textfield").type("Responsibilities") - }) - - // Requirements - cy.get('[data-name="Requirements"]').within(() => { - cy.get(".spectrum-Textfield").type("Requirements") - }) - - // Hiring manager - cy.get('[data-name="Hiring manager"]').within(() => { - cy.get(".spectrum-Picker-label").click() - }) - cy.get(".spectrum-Menu").find('li').its('length').then(len => { - cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click() - }) - - // Active - if (i == 0) { - cy.get('[data-name="Active"]').within(() => { - cy.get(".spectrum-Checkbox-box").click({ force: true }) - }) - } - - // Location - cy.get('[data-name="Location"]').within(() => { - cy.get(".spectrum-Picker-label").click() - }) - cy.get(".spectrum-Menu").find('li').its('length').then(len => { - cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click() - }) - - // Save vacancy - cy.get(".spectrum-Button").contains("Save").click({ force: true }) - cy.wait(1000) - - // Check table was updated - cy.get('[data-name="Vacancies Table"]').eq(i).should('contain', 'Tester') - } - }) - - xit("should filter applications by stage", () => { - // Visit published app - cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) - cy.wait(1000) - - // Applications section - cy.get(".links").contains("Applications").click({ force: true }) - cy.wait(1000) - - // Filter by stage - Confirm table updates - cy.get(".spectrum-Picker").contains("Filter by stage").click({ force: true }) - cy.get(".spectrum-Menu").find('li').its('length').then(len => { - for (let i = 1; i < len; i++) { - cy.get(".spectrum-Menu-item").eq(i).click() - const stage = cy.get(".spectrum-Picker-label") - stage.invoke('text').then(stageText => { - if (stageText == "1st interview") { - cy.get(".placeholder").should('contain', 'No rows found') - } - else { - cy.get(".spectrum-Table-row").should('contain', stageText) - } - cy.get(".spectrum-Picker").contains(stageText).click({ force: true }) - }) - } - }) - }) - - xit("should edit an application", () => { - // Switch application from not hired to hired - // Visit published app - cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) - cy.wait(1000) - - // Not Hired section - cy.get(".links").contains("Not hired").click({ force: true }) - cy.wait(500) - - // View application - cy.get(".spectrum-Table").within(() => { - cy.get(".spectrum-Button").contains("View").click({ force: true }) - cy.wait(500) - }) - - // Update value for 'Staged' - cy.get('[data-name="Stage"]').within(() => { - cy.get(".spectrum-Picker-label").click() - }) - cy.get(".spectrum-Menu").within(() => { - cy.get(".spectrum-Menu-item").contains("Hired").click() - }) - - // Save application - cy.get(".spectrum-Button").contains("Save").click({ force: true }) - cy.wait(500) - - // Hired section - cy.get(".links").contains("Hired").click({ force: true }) - cy.wait(500) - - // Verify Table size - Total rows = 2 - cy.get(".spectrum-Table").find(".spectrum-Table-row").its('length').then((len => { - expect(len).to.eq(2) - })) - }) - - xit("should delete an application", () => { - // Visit published app - cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) - cy.wait(1000) - - // Hired section - cy.get(".links").contains("Hired").click({ force: true }) - cy.wait(500) - - // View first application - cy.get(".spectrum-Table-row").eq(0).within(() => { - cy.get(".spectrum-Button").contains("View").click({ force: true }) - cy.wait(500) - }) - - // Delete application - cy.get(".spectrum-Button").contains("Delete").click({ force: true }) - cy.get(".spectrum-Dialog-grid").within(() => { - cy.get(".spectrum-Button").contains("Confirm").click() - }) - }) - }) -}) diff --git a/packages/builder/cypress/integration/templates/IT/ITTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/IT/ITTemplateDetails.spec.js deleted file mode 100644 index 84cbc5707e..0000000000 --- a/packages/builder/cypress/integration/templates/IT/ITTemplateDetails.spec.js +++ /dev/null @@ -1,60 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify IT Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter IT Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="IT"]').click() - }) - }) - - it("should verify the details option for IT templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - - if (templateNameText == "Hashicorp Scorecard Template") { - const templateNameSplit = templateNameParsed.split('-template')[0] - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameSplit) - } - else if (templateNameText == "IT Ticketing System") { - const templateNameSplit = templateNameParsed.split('it-')[1] - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameSplit) - } - else if (templateNameText == "IT Incident Report Form") { - const templateNameSplit = templateNameParsed.split('-form')[0] - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameSplit) - } - else { - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - } - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/IT/ITTicketingSystem.spec.js b/packages/builder/cypress/integration/templates/IT/ITTicketingSystem.spec.js deleted file mode 100644 index 15628ab131..0000000000 --- a/packages/builder/cypress/integration/templates/IT/ITTicketingSystem.spec.js +++ /dev/null @@ -1,72 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("IT Ticketing System Template Functionality", () => { - const templateName = "IT Ticketing System" - const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-') - - before(() => { - cy.login() - cy.deleteApp(templateName) - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, { - onBeforeLoad(win) { - cy.stub(win, 'open') - } - }) - cy.wait(2000) - }) - - it("should create and publish app with IT Ticketing System template", () => { - // Select IT Ticketing System template - cy.get(".template-thumbnail-text") - .contains(templateName).parentsUntil(".template-grid").within(() => { - cy.get(".spectrum-Button").contains("Use template").click({ force: true }) - }) - - // Confirm URL matches template name - const appUrl = cy.get(".app-server") - appUrl.invoke('text').then(appUrlText => { - expect(appUrlText).to.equal(`${Cypress.config().baseUrl}/app/` + templateNameParsed) - }) - - // Create App - cy.get(".spectrum-Dialog-grid").within(() => { - cy.get(".spectrum-Button").contains("Create app").click({ force: true }) - }) - - // Publish App & Verify it opened - cy.wait(2000) // Wait for app to generate - cy.publishApp(true) - cy.window().its('open').should('be.calledOnce') - }) - - xit("should filter tickets by status", () => { - // Visit published app - cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) - cy.wait(1000) - - // Tickets section - cy.get(".links").contains("Tickets").click({ force: true }) - cy.wait(1000) - - // Filter by stage - Confirm table updates - cy.get(".spectrum-Picker").contains("Filter by status").click({ force: true }) - cy.get(".spectrum-Menu").find('li').its('length').then(len => { - for (let i = 1; i < len; i++) { - cy.get(".spectrum-Menu-item").eq(i).click() - const stage = cy.get(".spectrum-Picker-label") - stage.invoke('text').then(stageText => { - if (stageText == "In progress" || stageText == "On hold" || stageText == "Triaged") { - cy.get(".placeholder").should('contain', 'No rows found') - } - else { - cy.get(".spectrum-Table-row").should('contain', stageText) - } - cy.get(".spectrum-Picker").contains(stageText).click({ force: true }) - }) - } - }) - }) - }) -}) diff --git a/packages/builder/cypress/integration/templates/adminPanels/adminPanelsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/adminPanels/adminPanelsTemplateDetails.spec.js deleted file mode 100644 index 2fa57b2c89..0000000000 --- a/packages/builder/cypress/integration/templates/adminPanels/adminPanelsTemplateDetails.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Admin Panel Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Admin Panels Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Admin Panels"]').click() - }) - }) - - it("should verify the details option for Admin Panels templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/approvalApps/approvalAppsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/approvalApps/approvalAppsTemplateDetails.spec.js deleted file mode 100644 index 322a17f6c2..0000000000 --- a/packages/builder/cypress/integration/templates/approvalApps/approvalAppsTemplateDetails.spec.js +++ /dev/null @@ -1,51 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Aproval Apps Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Approval Apps Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Approval Apps"]').click() - }) - }) - - it("should verify the details option for Approval Apps templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - - if (templateNameText == "Content Approval System") { - // Template name should include 'content-approval' - const templateNameSplit = templateNameParsed.split('-system')[0] - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameSplit) - } - else { - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - } - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/businessApps/businessAppsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/businessApps/businessAppsTemplateDetails.spec.js deleted file mode 100644 index 734fb9a968..0000000000 --- a/packages/builder/cypress/integration/templates/businessApps/businessAppsTemplateDetails.spec.js +++ /dev/null @@ -1,51 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Business Apps Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Business Apps Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Business Apps"]').click() - }) - }) - - it("should verify the details option for Business Apps templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - - if (templateNameText == "Employee Check-in/Check-Out Template") { - // Remove / from template name - const templateNameReplace = templateNameParsed.replace(/\//g, "-") - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameReplace) - } - else { - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - } - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/directories/directoriesTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/directories/directoriesTemplateDetails.spec.js deleted file mode 100644 index dc874fcbaf..0000000000 --- a/packages/builder/cypress/integration/templates/directories/directoriesTemplateDetails.spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Directories Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Directories Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Directories"]').click() - }) - }) - - it("should verify the details option for Directories templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - const templateNameSplit = templateNameParsed.split('-template')[0] - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameSplit) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/forms/formsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/forms/formsTemplateDetails.spec.js deleted file mode 100644 index 3206a71f6e..0000000000 --- a/packages/builder/cypress/integration/templates/forms/formsTemplateDetails.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Forms Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Forms Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Forms"]').click() - }) - }) - - it("should verify the details option for Forms templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/healthcare/healthcareTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/healthcare/healthcareTemplateDetails.spec.js deleted file mode 100644 index b46bb46274..0000000000 --- a/packages/builder/cypress/integration/templates/healthcare/healthcareTemplateDetails.spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Healthcare Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Healthcare Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Healthcare"]').click() - }) - }) - - it("should verify the details option for Healthcare templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/legal/legalTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/legal/legalTemplateDetails.spec.js deleted file mode 100644 index 57485aee40..0000000000 --- a/packages/builder/cypress/integration/templates/legal/legalTemplateDetails.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Legal Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Legal Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Legal"]').click() - }) - }) - - it("should verify the details option for Legal templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/logistics/logisticsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/logistics/logisticsTemplateDetails.spec.js deleted file mode 100644 index e5d5745e4e..0000000000 --- a/packages/builder/cypress/integration/templates/logistics/logisticsTemplateDetails.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Logistics Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Logistics Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Logistics"]').click() - }) - }) - - it("should verify the details option for Logistics templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/manufacturing/manufacturingTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/manufacturing/manufacturingTemplateDetails.spec.js deleted file mode 100644 index 30019c87fd..0000000000 --- a/packages/builder/cypress/integration/templates/manufacturing/manufacturingTemplateDetails.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Manufacturing Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Manufacturing Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Manufacturing"]').click() - }) - }) - - it("should verify the details option for Manufacturing templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/marketing/leadGenerationForm.spec.js b/packages/builder/cypress/integration/templates/marketing/leadGenerationForm.spec.js deleted file mode 100644 index 9f08b36d56..0000000000 --- a/packages/builder/cypress/integration/templates/marketing/leadGenerationForm.spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Lead Generation Form Template Functionality", () => { - const templateName = "Lead Generation Form" - const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-') - - before(() => { - cy.login() - cy.deleteApp(templateName) - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, { - onBeforeLoad(win) { - cy.stub(win, 'open') - } - }) - cy.wait(2000) - }) - - it("should create and publish app with Lead Generation Form template", () => { - // Select Lead Generation Form template - cy.get(".template-thumbnail-text") - .contains(templateName).parentsUntil(".template-grid").within(() => { - cy.get(".spectrum-Button").contains("Use template").click({ force: true }) - }) - - // Confirm URL matches template name - const appUrl = cy.get(".app-server") - appUrl.invoke('text').then(appUrlText => { - expect(appUrlText).to.equal(`${Cypress.config().baseUrl}/app/` + templateNameParsed) - }) - - // Create App - cy.get(".spectrum-Dialog-grid").within(() => { - cy.get(".spectrum-Button").contains("Create app").click({ force: true }) - }) - - // Publish App & Verify it opened - cy.wait(2000) // Wait for app to generate - cy.publishApp(true) - cy.window().its('open').should('be.calledOnce') - }) - }) -}) diff --git a/packages/builder/cypress/integration/templates/marketing/marketingTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/marketing/marketingTemplateDetails.spec.js deleted file mode 100644 index 66875e6939..0000000000 --- a/packages/builder/cypress/integration/templates/marketing/marketingTemplateDetails.spec.js +++ /dev/null @@ -1,51 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Marketing Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Marketing Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Marketing"]').click() - }) - }) - - it("should verify the details option for Marketing templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - - if (templateNameText == "Lead Generation Form") { - // Multi-step lead form - // Template name includes 'multi-step-lead-form' - cy.get('a') - .should('have.attr', 'href').and('contain', 'multi-step-lead-form') - } - else { - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - } - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/operations/operationsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/operations/operationsTemplateDetails.spec.js deleted file mode 100644 index 1a2ee1703a..0000000000 --- a/packages/builder/cypress/integration/templates/operations/operationsTemplateDetails.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Operations Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Operations Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Operations"]').click() - }) - }) - - it("should verify the details option for Operations templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/portals/portalsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/portals/portalsTemplateDetails.spec.js deleted file mode 100644 index e81e12318d..0000000000 --- a/packages/builder/cypress/integration/templates/portals/portalsTemplateDetails.spec.js +++ /dev/null @@ -1,71 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Portals Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Portal Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Portal"]').click() - }) - }) - - it("should verify the details option for Portal templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) - - it("should verify the details option for Portals templates", () => { - // Filter Portals Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Portals"]').click() - }) - - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/professionalServices/professionalServicesTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/professionalServices/professionalServicesTemplateDetails.spec.js deleted file mode 100644 index 1267d8bd5c..0000000000 --- a/packages/builder/cypress/integration/templates/professionalServices/professionalServicesTemplateDetails.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Professional Services Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Professional Services Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Professional Services"]').click() - }) - }) - - it("should verify the details option for Professional Services templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js index d50364fd54..9d77b89c57 100644 --- a/packages/builder/cypress/support/commands.js +++ b/packages/builder/cypress/support/commands.js @@ -1,15 +1,9 @@ -// *********************************************** -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// - Cypress.on("uncaught:exception", () => { return false }) -Cypress.Commands.add("login", () => { +// ACCOUNTS & USERS +Cypress.Commands.add("login", (email, password) => { cy.visit(`${Cypress.config().baseUrl}/builder`) cy.wait(2000) cy.url().then(url => { @@ -23,8 +17,13 @@ Cypress.Commands.add("login", () => { if (url.includes("builder/auth/login") || url.includes("builder/admin")) { // login cy.contains("Sign in to Budibase").then(() => { - cy.get("input").first().type("test@test.com") - cy.get('input[type="password"]').type("test") + if (email == null) { + cy.get("input").first().type("test@test.com") + cy.get('input[type="password"]').type("test") + } else { + cy.get("input").first().type(email) + cy.get('input[type="password"]').type(password) + } cy.get("button").first().click({ force: true }) cy.wait(1000) }) @@ -33,7 +32,7 @@ Cypress.Commands.add("login", () => { }) Cypress.Commands.add("logOut", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 2000 }) cy.get(".user-dropdown .avatar > .icon").click({ force: true }) cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => { cy.get("li[data-cy='user-logout']").click({ force: true }) @@ -41,47 +40,58 @@ Cypress.Commands.add("logOut", () => { cy.wait(2000) }) -Cypress.Commands.add("closeModal", () => { - cy.get(".spectrum-Modal").within(() => { - cy.get(".close-icon").click() - cy.wait(1000) // Wait for modal to close +Cypress.Commands.add("logoutNoAppGrid", () => { + // Logs user out when app grid is not present + cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.get(".avatar > .icon").click({ force: true }) + cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => { + cy.get(".spectrum-Menu-item").contains("Log out").click({ force: true }) + }) + cy.wait(2000) +}) + +Cypress.Commands.add("createUser", email => { + // quick hacky recorded way to create a user + cy.contains("Users").click() + cy.get(`[data-cy="add-user"]`).click() + cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".spectrum-Picker-label").click() + cy.get( + ".spectrum-Menu-item:nth-child(2) > .spectrum-Menu-itemLabel" + ).click() + + // Onboarding type selector + cy.get(".spectrum-Textfield-input") + .eq(0) + .first() + .type(email, { force: true }) + cy.get(".spectrum-Button--cta").click({ force: true }) }) }) -Cypress.Commands.add("importApp", (exportFilePath, name) => { - cy.visit(`${Cypress.config().baseUrl}/builder`) +Cypress.Commands.add("deleteUser", email => { + // Assumes user has access to Users section + cy.contains("Users", { timeout: 2000 }).click() + cy.contains(email).click() - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) - cy.wait(500) - } - cy.get(`[data-cy="import-app-btn"]`).click({ force: true }) + // Click Delete user button + cy.get(".spectrum-Button") + .contains("Delete user") + .click({ force: true }) + .then(() => { + // Confirm deletion within modal + cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { + cy.get(".spectrum-Button") + .contains("Delete user") + .click({ force: true }) + }) }) - - cy.get(".spectrum-Modal").within(() => { - cy.get("input").eq(1).should("have.focus") - - cy.get(".spectrum-Dropzone").selectFile(exportFilePath, { - action: "drag-drop", - }) - - cy.get(".gallery .filename").contains("exported-app.txt") - - if (name && name != "") { - cy.get("input").eq(0).type(name).should("have.value", name).blur() - } - cy.get(".confirm-wrap button") - .should("not.be.disabled") - .click({ force: true }) - cy.wait(5000) - }) }) Cypress.Commands.add("updateUserInformation", (firstName, lastName) => { - cy.get(".user-dropdown .avatar > .icon").click({ force: true }) + cy.get(".user-dropdown .avatar > .icon", { timeout: 2000 }).click({ + force: true, + }) cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => { cy.get("li[data-cy='user-info']").click({ force: true }) @@ -113,13 +123,19 @@ Cypress.Commands.add("updateUserInformation", (firstName, lastName) => { }) }) +// APPLICATIONS +Cypress.Commands.add("createTestApp", () => { + const appName = "Cypress Tests" + cy.deleteApp(appName) + cy.createApp(appName, "This app is used for Cypress testing.") +}) + Cypress.Commands.add("createApp", (name, addDefaultTable) => { const shouldCreateDefaultTable = typeof addDefaultTable != "boolean" ? true : addDefaultTable - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) - cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) + cy.get(`[data-cy="create-app-btn"]`, { timeout: 2000 }).click({ force: true }) // If apps already exist cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) @@ -139,7 +155,7 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => { cy.get(".spectrum-ButtonGroup") .contains("Create app") .click({ force: true }) - cy.wait(10000) + cy.wait(2000) }) if (shouldCreateDefaultTable) { cy.createTable("Cypress Tests", true) @@ -147,7 +163,7 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => { }) Cypress.Commands.add("deleteApp", name => { - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.wait(2000) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) .its("body") @@ -166,20 +182,28 @@ Cypress.Commands.add("deleteApp", name => { return } + // Go to app overview const appIdParsed = appId.split("_").pop() const actionEleId = `[data-cy=row_actions_${appIdParsed}]` cy.get(actionEleId).within(() => { - cy.get(".spectrum-Icon").eq(0).click({ force: true }) + cy.contains("Manage").click({ force: true }) }) - cy.get(".spectrum-Menu").then($menu => { - if ($menu.text().includes("Unpublish")) { - cy.get(".spectrum-Menu").contains("Unpublish").click() - cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click() + cy.wait(500) + + // Unpublish first if needed + cy.get(`[data-cy="app-status"]`).then($status => { + if ($status.text().includes("- Unpublish")) { + // Exact match for Unpublish + cy.contains("Unpublish").click({ force: true }) + cy.get(".spectrum-Modal").within(() => { + cy.contains("Unpublish app").click({ force: true }) + }) } }) - cy.get(actionEleId).within(() => { - cy.get(".spectrum-Icon").eq(0).click({ force: true }) + // Delete app + cy.get(".app-overview-actions-icon").within(() => { + cy.get(".spectrum-Icon").click({ force: true }) }) cy.get(".spectrum-Menu").contains("Delete").click() cy.get(".spectrum-Dialog-grid").within(() => { @@ -202,73 +226,12 @@ Cypress.Commands.add("deleteAllApps", () => { .its("body") .then(val => { for (let i = 0; i < val.length; i++) { - const appIdParsed = val[i].appId.split("_").pop() - const actionEleId = `[data-cy=row_actions_${appIdParsed}]` - cy.get(actionEleId).within(() => { - cy.get(".spectrum-Icon").eq(0).click({ force: true }) - }) - - cy.get(".spectrum-Menu").contains("Delete").click() - cy.get(".spectrum-Dialog-grid").within(() => { - cy.get("input").type(val[i].name) - cy.get(".spectrum-Button--warning").click() - }) + cy.deleteApp(val[i].name) cy.reload() } }) }) -Cypress.Commands.add("customiseAppIcon", () => { - // Select random icon - cy.get(".grid").within(() => { - cy.get(".icon-item") - .eq(Math.floor(Math.random() * 23) + 1) - .click() - }) - // Select random colour - cy.get(".fill").click() - cy.get(".colors").within(() => { - cy.get(".color") - .eq(Math.floor(Math.random() * 33) + 1) - .click() - }) - cy.intercept("**/applications/**").as("iconChange") - cy.get(".spectrum-Button").contains("Save").click({ force: true }) - cy.wait("@iconChange") - cy.get("@iconChange").its("response.statusCode").should("eq", 200) - cy.wait(1000) -}) - -Cypress.Commands.add("alterAppVersion", (appId, version) => { - return cy - .request("put", `${Cypress.config().baseUrl}/api/applications/${appId}`, { - version: version || "0.0.1-alpha.0", - }) - .then(resp => { - expect(resp.status).to.eq(200) - }) -}) - -Cypress.Commands.add("updateAppName", (changedName, noName) => { - cy.get(".spectrum-Modal").within(() => { - if (noName == true) { - cy.get("input").clear() - cy.get(".spectrum-Dialog-grid") - .click() - .contains("App name must be letters, numbers and spaces only") - return cy - } - cy.get("input").clear() - cy.get("input") - .eq(0) - .type(changedName) - .should("have.value", changedName) - .blur() - cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true }) - cy.wait(500) - }) -}) - Cypress.Commands.add("unlockApp", unlock_config => { let config = { ...unlock_config } @@ -298,6 +261,26 @@ Cypress.Commands.add("unlockApp", unlock_config => { }) }) +Cypress.Commands.add("updateAppName", (changedName, noName) => { + cy.get(".spectrum-Modal").within(() => { + if (noName == true) { + cy.get("input").clear() + cy.get(".spectrum-Dialog-grid") + .click() + .contains("App name must be letters, numbers and spaces only") + return cy + } + cy.get("input").clear() + cy.get("input") + .eq(0) + .type(changedName) + .should("have.value", changedName) + .blur() + cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true }) + cy.wait(500) + }) +}) + Cypress.Commands.add("publishApp", resolvedAppPath => { //Assumes you have navigated to an application first cy.get(".toprightnav button.spectrum-Button") @@ -321,54 +304,124 @@ Cypress.Commands.add("publishApp", resolvedAppPath => { }) }) -Cypress.Commands.add("createTestApp", () => { - const appName = "Cypress Tests" - cy.deleteApp(appName) - cy.createApp(appName, "This app is used for Cypress testing.") - //cy.createScreen("home") +Cypress.Commands.add("alterAppVersion", (appId, version) => { + return cy + .request("put", `${Cypress.config().baseUrl}/api/applications/${appId}`, { + version: version || "0.0.1-alpha.0", + }) + .then(resp => { + expect(resp.status).to.eq(200) + }) }) -Cypress.Commands.add("createTestTableWithData", () => { - cy.createTable("dog") - cy.addColumn("dog", "name", "Text") - cy.addColumn("dog", "age", "Number") -}) +Cypress.Commands.add("importApp", (exportFilePath, name) => { + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) -Cypress.Commands.add("publishApp", (viewApp = false) => { - cy.get(".toprightnav").contains("Publish").click({ force: true }) - cy.get(".spectrum-Dialog-grid").within(() => { - cy.get(".spectrum-Button").contains("Publish").click({ force: true }) - }) - cy.wait(2000) // Wait for App to publish and modal to appear - cy.get(".spectrum-Dialog-grid").within(() => { - if (viewApp) { - cy.get(".spectrum-Button").contains("View App").click({ force: true }) - } else { - cy.get(".spectrum-Button").contains("Done").click({ force: true }) + cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) + .its("body") + .then(val => { + if (val.length > 0) { + cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) + } + cy.wait(500) + cy.get(`[data-cy="import-app-btn"]`).click({ + force: true, + }) + }) + + cy.get(".spectrum-Modal").within(() => { + cy.get("input").eq(1).should("have.focus") + + cy.get(".spectrum-Dropzone").selectFile(exportFilePath, { + action: "drag-drop", + }) + + cy.get(".gallery .filename").contains("exported-app.txt") + + if (name && name != "") { + cy.get("input").eq(0).type(name).should("have.value", name).blur() } + cy.get(".confirm-wrap button") + .should("not.be.disabled") + .click({ force: true }) + cy.wait(3000) }) }) +// Filters visible with 1 or more +Cypress.Commands.add("searchForApplication", appName => { + cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.wait(2000) + + // No app filter functionality if only 1 app exists + cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) + .its("body") + .then(val => { + if (val.length < 2) { + return + } else { + // Searches for the app + cy.get(".filter").then(() => { + cy.get(".spectrum-Textfield").within(() => { + cy.get("input").eq(0).clear() + cy.get("input").eq(0).type(appName) + }) + }) + } + }) +}) + +// Assumes there are no others +Cypress.Commands.add("applicationInAppTable", appName => { + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 }) + cy.get(".appTable", { timeout: 2000 }).within(() => { + cy.get(".title").contains(appName).should("exist") + }) +}) + +Cypress.Commands.add("createAppFromScratch", appName => { + cy.get(`[data-cy="create-app-btn"]`) + .contains("Start from scratch") + .click({ force: true }) + cy.get(".spectrum-Modal").within(() => { + cy.get("input") + .eq(0) + .clear() + .type(appName) + .should("have.value", appName) + .blur() + cy.get(".spectrum-ButtonGroup").contains("Create app").click() + cy.wait(10000) + }) + cy.createTable("Cypress Tests", true) +}) + +// TABLES Cypress.Commands.add("createTable", (tableName, initialTable) => { if (!initialTable) { cy.navigateToDataSection() cy.get(`[data-cy="new-table"]`).click() } - cy.wait(5000) - cy.get(".spectrum-Dialog-grid") + cy.wait(2000) + cy.get(".item") .contains("Budibase DB") .click({ force: true }) .then(() => { cy.get(".spectrum-Button").contains("Continue").click({ force: true }) }) cy.get(".spectrum-Modal").within(() => { - cy.wait(1000) - cy.get("input").first().type(tableName).blur() + cy.get("input", { timeout: 1000 }).first().type(tableName).blur() cy.get(".spectrum-ButtonGroup").contains("Create").click() }) cy.contains(tableName).should("be.visible") }) +Cypress.Commands.add("createTestTableWithData", () => { + cy.createTable("dog") + cy.addColumn("dog", "name", "Text") + cy.addColumn("dog", "age", "Number") +}) + Cypress.Commands.add( "addColumn", (tableName, columnName, type, multiOptions = null) => { @@ -423,30 +476,44 @@ Cypress.Commands.add("addRowMultiValue", values => { }) }) -Cypress.Commands.add("createUser", email => { - // quick hacky recorded way to create a user - cy.contains("Users").click() - cy.get(`[data-cy="add-user"]`).click() - cy.get(".spectrum-Picker-label").click() - cy.get(".spectrum-Menu-item:nth-child(2) > .spectrum-Menu-itemLabel").click() - - //Onboarding type selector - cy.get( - ":nth-child(2) > .spectrum-Form-itemField > .spectrum-Textfield > .spectrum-Textfield-input" - ) - .first() - .type(email, { force: true }) - cy.get(".spectrum-Button--cta").click({ force: true }) +Cypress.Commands.add("selectTable", tableName => { + cy.expandBudibaseConnection() + cy.contains(".nav-item", tableName).click() }) +Cypress.Commands.add("addCustomSourceOptions", totalOptions => { + cy.get(".spectrum-ActionButton") + .contains("Define Options") + .click() + .then(() => { + for (let i = 0; i < totalOptions; i++) { + // Add radio button options + cy.get(".spectrum-Button") + .contains("Add Option") + .click({ force: true }) + .then(() => { + cy.get("[placeholder='Label']", { timeout: 500 }).eq(i).type(i) + cy.get("[placeholder='Value']").eq(i).type(i) + }) + } + // Save options + cy.get(".spectrum-Button").contains("Save").click({ force: true }) + }) +}) + +// DESIGN AREA Cypress.Commands.add("addComponent", (category, component) => { if (category) { - cy.get(`[data-cy="category-${category}"]`).click({ force: true }) + cy.get(`[data-cy="category-${category}"]`, { timeout: 1000 }).click({ + force: true, + }) } if (component) { - cy.get(`[data-cy="component-${component}"]`).click({ force: true }) + cy.get(`[data-cy="component-${component}"]`, { timeout: 1000 }).click({ + force: true, + }) } - cy.wait(2000) + cy.wait(1000) cy.location().then(loc => { const params = loc.pathname.split("/") const componentId = params[params.length - 1] @@ -461,41 +528,26 @@ Cypress.Commands.add("getComponent", componentId => { .its("0.contentDocument") .should("exist") .its("body") - .should("not.be.null") + .should("not.be.undefined") .then(cy.wrap) - .find(`[data-id=${componentId}]`) + .find(`[data-id='${componentId}']`) }) -Cypress.Commands.add("navigateToFrontend", () => { - // Clicks on Design tab and then the Home nav item - cy.wait(1000) - cy.contains("Design").click() - cy.get(".spectrum-Search").type("/") - cy.get(".nav-item").contains("home").click() -}) - -Cypress.Commands.add("navigateToDataSection", () => { - // Clicks on the Data tab - cy.wait(500) - cy.contains("Data").click() -}) - -//Blank Cypress.Commands.add("createScreen", (route, accessLevelLabel) => { + // Blank Screen cy.contains("Design").click() cy.get("[aria-label=AddCircle]").click() cy.get(".spectrum-Modal").within(() => { cy.get("[data-cy='blank-screen']").click() cy.get(".spectrum-Button").contains("Continue").click({ force: true }) - cy.wait(500) }) - cy.get(".spectrum-Dialog-grid").within(() => { + cy.wait(500) + cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { cy.get(".spectrum-Form-itemField").eq(0).type(route) - cy.get(".spectrum-Button").contains("Continue").click({ force: true }) - cy.wait(1000) + cy.get(".confirm-wrap").contains("Continue").click({ force: true }) }) - cy.get(".spectrum-Modal").within(() => { + cy.get(".spectrum-Modal", { timeout: 1000 }).within(() => { if (accessLevelLabel) { cy.get(".spectrum-Picker-label").click() cy.wait(500) @@ -513,10 +565,12 @@ Cypress.Commands.add( cy.get(".spectrum-Modal").within(() => { cy.get(".item").contains("Autogenerated screens").click() cy.get(".spectrum-Button").contains("Continue").click({ force: true }) - cy.wait(500) }) - cy.get(".spectrum-Modal [data-cy='data-source-modal']").within(() => { + cy.get(".spectrum-Modal [data-cy='data-source-modal']", { + timeout: 500, + }).within(() => { for (let i = 0; i < datasourceNames.length; i++) { + cy.wait(500) cy.get(".data-source-entry").contains(datasourceNames[i]).click() //Ensure the check mark is visible cy.get(".data-source-entry") @@ -541,17 +595,6 @@ Cypress.Commands.add( } ) -Cypress.Commands.add("navigateToAutogeneratedModal", () => { - // Screen name must already exist within data source - cy.contains("Design").click() - cy.get("[aria-label=AddCircle]").click() - cy.get(".spectrum-Modal").within(() => { - cy.get(".item").contains("Autogenerated screens").click() - cy.get(".spectrum-Button").contains("Continue").click({ force: true }) - cy.wait(500) - }) -}) - Cypress.Commands.add( "createAutogeneratedScreens", (screenNames, accessLevelLabel) => { @@ -573,96 +616,33 @@ Cypress.Commands.add( } ) -Cypress.Commands.add("addRow", values => { - cy.contains("Create row").click() +// NAVIGATION +Cypress.Commands.add("navigateToFrontend", () => { + // Clicks on Design tab and then the Home nav item + cy.wait(500) + cy.contains("Design").click() + cy.get(".spectrum-Search").type("/") + cy.get(".nav-item").contains("home").click() +}) + +Cypress.Commands.add("navigateToDataSection", () => { + // Clicks on the Data tab + cy.wait(500) + cy.contains("Data").click() +}) + +Cypress.Commands.add("navigateToAutogeneratedModal", () => { + // Screen name must already exist within data source + cy.contains("Design").click() + cy.get("[aria-label=AddCircle]").click() cy.get(".spectrum-Modal").within(() => { - for (let i = 0; i < values.length; i++) { - cy.get("input").eq(i).type(values[i]).blur() - } - cy.get(".spectrum-ButtonGroup").contains("Create").click() + cy.get(".item").contains("Autogenerated screens").click() + cy.get(".spectrum-Button").contains("Continue").click({ force: true }) + cy.wait(500) }) }) -Cypress.Commands.add("expandBudibaseConnection", () => { - if (Cypress.$(".nav-item > .content > .opened").length === 0) { - // expand the Budibase DB connection string - cy.get(".icon.arrow").eq(0).click() - } -}) - -Cypress.Commands.add("selectTable", tableName => { - cy.expandBudibaseConnection() - cy.contains(".nav-item", tableName).click() -}) - -Cypress.Commands.add("addCustomSourceOptions", totalOptions => { - cy.get(".spectrum-ActionButton") - .contains("Define Options") - .click() - .then(() => { - for (let i = 0; i < totalOptions; i++) { - // Add radio button options - cy.get(".spectrum-Button") - .contains("Add Option") - .click({ force: true }) - .then(() => { - cy.wait(500) - cy.get("[placeholder='Label']").eq(i).type(i) - cy.get("[placeholder='Value']").eq(i).type(i) - }) - } - // Save options - cy.get(".spectrum-Button").contains("Save").click({ force: true }) - }) -}) - -//Filters visible with 1 or more -Cypress.Commands.add("searchForApplication", appName => { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(2000) - - // No app filter functionality if only 1 app exists - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length < 2) { - return - } else { - // Searches for the app - cy.get(".filter").then(() => { - cy.get(".spectrum-Textfield").within(() => { - cy.get("input").eq(0).clear() - cy.get("input").eq(0).type(appName) - }) - }) - } - }) -}) - -//Assumes there are no others -Cypress.Commands.add("applicationInAppTable", appName => { - cy.get(".appTable").within(() => { - cy.get(".title").contains(appName).should("exist") - }) -}) - -Cypress.Commands.add("createAppFromScratch", appName => { - cy.get(`[data-cy="create-app-btn"]`) - .contains("Start from scratch") - .click({ force: true }) - cy.get(".spectrum-Modal").within(() => { - cy.get("input") - .eq(0) - .clear() - .type(appName) - .should("have.value", appName) - .blur() - cy.get(".spectrum-ButtonGroup").contains("Create app").click() - cy.wait(10000) - }) - cy.createTable("Cypress Tests", true) -}) - +// DATASOURCES Cypress.Commands.add("selectExternalDatasource", datasourceName => { // Navigates to Data Section cy.navigateToDataSection() @@ -671,11 +651,11 @@ Cypress.Commands.add("selectExternalDatasource", datasourceName => { cy.get(".add-button").click() }) // Clicks specified datasource & continue - cy.wait(1000) - cy.get(".item-list").contains(datasourceName).click() + cy.get(".item-list", { timeout: 1000 }).contains(datasourceName).click() cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Button").contains("Continue").click({ force: true }) }) + cy.wait(500) }) Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => { @@ -683,8 +663,7 @@ Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => { // Adds the config for specified datasource & fetches tables // Currently supports MySQL, PostgreSQL, Oracle // Host IP Address - cy.wait(500) - cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { cy.get(".form-row") .eq(0) .within(() => { @@ -784,17 +763,34 @@ Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => { Cypress.Commands.add("createRestQuery", (method, restUrl, queryPrettyName) => { // addExternalDatasource should be called prior to this // Configures REST datasource & sends query - cy.wait(1000) - cy.get(".spectrum-Button").contains("Add query").click({ force: true }) + cy.get(".spectrum-Button", { timeout: 1000 }) + .contains("Add query") + .click({ force: true }) // Select Method & add Rest URL cy.get(".spectrum-Picker-label").eq(1).click() cy.get(".spectrum-Menu").contains(method).click() cy.get("input").clear().type(restUrl) // Send query cy.get(".spectrum-Button").contains("Send").click({ force: true }) - cy.wait(500) - cy.get(".spectrum-Button").contains("Save").click({ force: true }) + cy.get(".spectrum-Button", { timeout: 500 }) + .contains("Save") + .click({ force: true }) cy.get(".hierarchy-items-container") .should("contain", method) .and("contain", queryPrettyName) }) + +// MISC +Cypress.Commands.add("closeModal", () => { + cy.get(".spectrum-Modal").within(() => { + cy.get(".close-icon").click() + cy.wait(1000) // Wait for modal to close + }) +}) + +Cypress.Commands.add("expandBudibaseConnection", () => { + if (Cypress.$(".nav-item > .content > .opened").length === 0) { + // expand the Budibase DB connection string + cy.get(".icon.arrow").eq(0).click() + } +}) diff --git a/packages/builder/cypress/support/interact.js b/packages/builder/cypress/support/interact.js index 22ab13a48b..0b31d8a8c5 100644 --- a/packages/builder/cypress/support/interact.js +++ b/packages/builder/cypress/support/interact.js @@ -26,6 +26,7 @@ export const SPECTRUM_POPOVER = ".spectrum-Popover" export const OPTION_SOURCE_PROP_CONROL = '[data-cy="optionsSource-prop-control' export const APP_TABLE_STATUS = ".appTable .app-status" export const APP_TABLE_ROW_ACTION = ".appTable .app-row-actions" +export const APP_TABLE_APP_NAME = '[data-cy="app-name-link"]' export const DEPLOYMENT_TOP_NAV_GLOBESTRIKE = ".deployment-top-nav svg[aria-label=GlobeStrike]" export const DEPLOYMENT_TOP_GLOBE = ".deployment-top-nav svg[aria-label=Globe]" @@ -33,13 +34,14 @@ export const PUBLISH_POPOVER_MENU = '[data-cy="publish-popover-menu"]' export const PUBLISH_POPOVER_ACTION = '[data-cy="publish-popover-action"]' export const PUBLISH_POPOVER_MESSAGE = ".publish-popover-message" export const SPECTRUM_BUTTON = ".spectrum-Button" +export const SPECTRUM_LINK = ".spectrum-Link" export const TOPRIGHTNAV_BUTTON_SPECTRUM = ".toprightnav button.spectrum-Button" //createComponents export const SETTINGS = "[data-cy=Settings]" export const SETTINGS_INPUT = "[data-cy=setting-text] input" export const DESIGN = "[data-cy=Design]" -export const FONT_SIZE_PROP_CONTRO = "[data-cy=font-size-prop-control]" +export const FONT_SIZE_PROP_CONTROL = "[data-cy=font-size-prop-control]" export const DATA_CY_DATASOURCE = "[data-cy=setting-dataSource]" export const DROPDOWN_CONTAINER = ".dropdown-container" export const SPECTRUM_PICKER = ".spectrum-Picker" @@ -53,8 +55,79 @@ export const NAV_ITEMS_CONTAINER = ".nav-items-container" //publishWorkFlow export const DEPLOY_APP_MODAL = ".spectrum-Modal [data-cy=deploy-app-modal]" +export const DEPLOY_SUCCESS_MODAL = + ".spectrum-Modal [data-cy=deploy-app-success-modal]" export const DEPLOY_APP_URL_INPUT = "[data-cy=deployed-app-url] input" export const GLOBESTRIKE = "svg[aria-label=GlobeStrike]" export const GLOBE = "svg[aria-label=Globe]" export const UNPUBLISH_MODAL = "[data-cy=unpublish-modal]" export const CONFIRM_WRAP_BUTTON = ".confirm-wrap button" +export const DEPLOYMENT_TOP_NAV = ".deployment-top-nav" + +//changeAppiconAndColour +export const APP_ROW_ACTION = ".app-row-actions-icon" +export const SPECTRUM_MENU = ".spectrum-Menu" +export const ICON_ITEM = ".icon-item" +export const FILL = ".fill" +export const COLOURSS = ".colors" +export const AREA_LABEL = "[aria-label]" +export const TITLE = ".title" +export const GRID = ".grid" +export const COLOUR = ".color" + +//createAutomation +export const ADD_BUTTON_SPECTRUM = ".add-button .spectrum-Icon" +export const MODAL_INNER_WRAPPER = ".modal-inner-wrapper" +export const SPECTRUM_BUTTON_CTA = ".spectrum-Button--cta" +export const SPECTRUM_TEXTFIELD_INPUT = ".spectrum-Textfield-input" + +//createTable +export const TABLE_TITLE_H1 = ".table-title h1" +export const TABLE_TITLE = ".title" +export const SPECTRUM_TABLE_EDIT = ".spectrum-Table-editIcon > use" +export const SPECTRUM_SWITCH_INPUT = ".spectrum-Switch-input" +export const SPECTRUM_CHECKBOX_INPUT = ".spectrum-Checkbox-input" +export const SPECTRUM_PAGINATION = ".spectrum-Pagination" +export const SPECTRUM_ACTION_BUTTON = ".spectrum-ActionButton" +export const SPECTRUM_BODY_SECOND = ".spectrum-Body--secondary" +export const POPOVERS = ".popovers" +export const SPECTRUM_DIALOG_GRID = ".spectrum-Dialog-grid" +export const DELETE_COLUMN_CONFIRM = '[data-cy="delete-column-confirm"]' +export const NAV_ITEM = ".nav-item" +export const ACTION_SPECTRUM_ICON = ".actions .spectrum-Icon" +export const SPECTRUM_MENU_CHILD2 = ".spectrum-Menu > :nth-child(2)" +export const DELETE_TABLE_CONFIRM = '[data-cy="delete-table-confirm"]' + +//adminAndManagement Folder +export const SPECTRUM_TABLE = ".spectrum-Table" +export const SPECTRUM_SIDENAV = ".spectrum-SideNav" +export const SPECTRUM_TABLE_ROW = ".spectrum-Table-row" +export const SPECTRUM_TABLE_CELL = ".spectrum-Table-cell" +export const FIELD = ".field" +export const CONTAINER = ".container" +export const REGENERATE = ".regenerate" +export const SPECTRUM_DIALOG_CONTENT = ".spectrum-Dialog-content" +export const SPECTRUM_ICON = ".spectrum-Icon" + +//createView +export const SPECTRUM_MENU_ITEM_LABEL = ".spectrum-Menu-itemLabel" + +//revertApp +export const TOP_RIGHT_NAV = ".toprightnav" +export const AREA_LABEL_REVERT = "[aria-label=Revert]" +export const ROOT = ".root" + +//queryLevelTransformers +export const SPECTRUM_TABS_ITEM = ".spectrum-Tabs-itemLabel" +export const CODEMIRROR_TEXTAREA = ".CodeMirror textarea" + +//renameApplication +export const WRAPPER = ".wrapper" +export const ERROR = ".error" +export const AREA_LABEL_MORE = "[aria-label=More]" +export const APP_ROW_ACTION_MENU_POPOVER = + '[data-cy="app-row-actions-menu-popover"]' +export const SPECTRUM_MENU_ITEM = ".spectrum-Menu-item" + +//commands +export const HOME_LOGO = ".home-logo" diff --git a/packages/builder/package.json b/packages/builder/package.json index c6a95f51e6..a1729e82dc 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -77,6 +77,7 @@ "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", "codemirror": "^5.59.0", + "dayjs": "^1.11.2", "downloadjs": "1.4.7", "lodash": "4.17.21", "posthog-js": "1.4.5", diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 8cbc629291..234f83d7cc 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -49,6 +49,95 @@ export const getBindableProperties = (asset, componentId) => { ] } +/** + * Gets all rest bindable data fields + */ +export const getRestBindings = () => { + const userBindings = getUserBindings() + return [...userBindings, ...getAuthBindings()] +} + +/** + * Gets all rest bindable auth fields + */ +export const getAuthBindings = () => { + let bindings = [] + const safeUser = makePropSafe("user") + const safeOAuth2 = makePropSafe("oauth2") + const safeAccessToken = makePropSafe("accessToken") + + const authBindings = [ + { + runtime: `${safeUser}.${safeOAuth2}.${safeAccessToken}`, + readable: `Current User.OAuthToken`, + key: "accessToken", + }, + ] + + bindings = Object.keys(authBindings).map(key => { + const fieldBinding = authBindings[key] + return { + type: "context", + runtimeBinding: fieldBinding.runtime, + readableBinding: fieldBinding.readable, + fieldSchema: { type: "string", name: fieldBinding.key }, + providerId: "user", + } + }) + return bindings +} + +/** + * Utility - convert a key/value map to an array of custom 'context' bindings + * @param {object} valueMap Key/value pairings + * @param {string} prefix A contextual string prefix/path for a user readable binding + * @return {object[]} An array containing readable/runtime binding objects + */ +export const toBindingsArray = (valueMap, prefix) => { + if (!valueMap) { + return [] + } + return Object.keys(valueMap).reduce((acc, binding) => { + if (!binding || !valueMap[binding]) { + return acc + } + acc.push({ + type: "context", + runtimeBinding: binding, + readableBinding: `${prefix}.${binding}`, + }) + return acc + }, []) +} + +/** + * Utility - coverting a map of readable bindings to runtime + */ +export const readableToRuntimeMap = (bindings, ctx) => { + if (!bindings || !ctx) { + return {} + } + return Object.keys(ctx).reduce((acc, key) => { + let parsedQuery = readableToRuntimeBinding(bindings, ctx[key]) + acc[key] = parsedQuery + return acc + }, {}) +} + +/** + * Utility - coverting a map of runtime bindings to readable + */ +export const runtimeToReadableMap = (bindings, ctx) => { + if (!bindings || !ctx) { + return {} + } + return Object.keys(ctx).reduce((acc, key) => { + let parsedQuery = runtimeToReadableBinding(bindings, ctx[key]) + acc[key] = parsedQuery + return acc + }, {}) +} + /** * Gets the bindable properties exposed by a certain component. */ @@ -298,7 +387,6 @@ const getUserBindings = () => { providerId: "user", }) }) - return bindings } diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index cf42492c05..dd09e3356a 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -5,6 +5,7 @@ import { cloneDeep } from "lodash/fp" const initialAutomationState = { automations: [], + showTestPanel: false, blockDefinitions: { TRIGGER: [], ACTION: [], @@ -19,6 +20,17 @@ export const getAutomationStore = () => { } const automationActions = store => ({ + definitions: async () => { + const response = await API.getAutomationDefinitions() + store.update(state => { + state.blockDefinitions = { + TRIGGER: response.trigger, + ACTION: response.action, + } + return state + }) + return response + }, fetch: async () => { const responses = await Promise.all([ API.getAutomations(), @@ -109,6 +121,20 @@ const automationActions = store => ({ return state }) }, + getLogs: async ({ automationId, startDate, status, page } = {}) => { + return await API.getAutomationLogs({ + automationId, + startDate, + status, + page, + }) + }, + clearLogErrors: async ({ automationId, appId } = {}) => { + return await API.clearAutomationLogErrors({ + automationId, + appId, + }) + }, addTestDataToAutomation: data => { store.update(state => { state.selectedAutomation.addTestData(data) @@ -117,11 +143,10 @@ const automationActions = store => ({ }, addBlockToAutomation: (block, blockIdx) => { store.update(state => { - const newBlock = state.selectedAutomation.addBlock( + state.selectedBlock = state.selectedAutomation.addBlock( cloneDeep(block), blockIdx ) - state.selectedBlock = newBlock return state }) }, diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index bc02e04db3..ec810e5c31 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -60,6 +60,7 @@ const INITIAL_FRONTEND_STATE = { theme: "", customTheme: {}, previewDevice: "desktop", + highlightedSettingKey: null, } export const getFrontendStore = () => { @@ -189,6 +190,7 @@ export const getFrontendStore = () => { // Build array of promises to speed up bulk deletions const promises = [] + let deleteUrls = [] screensToDelete.forEach(screen => { // Delete the screen promises.push( @@ -198,14 +200,10 @@ export const getFrontendStore = () => { }) ) // Remove links to this screen - promises.push( - store.actions.components.links.delete( - screen.routing.route, - screen.props._instanceName - ) - ) + deleteUrls.push(screen.routing.route) }) + promises.push(store.actions.links.delete(deleteUrls)) await Promise.all(promises) const deletedIds = screensToDelete.map(screen => screen._id) store.update(state => { @@ -577,89 +575,46 @@ export const getFrontendStore = () => { }) await store.actions.preview.saveSelected() }, - links: { - save: async (url, title) => { - const layout = get(mainLayout) - if (!layout) { - return - } + }, + links: { + save: async (url, title) => { + const layout = get(mainLayout) + if (!layout) { + return + } - // Add link setting to main layout - if (layout.props._component.endsWith("layout")) { - // If using a new SDK, add to the layout component settings - if (!layout.props.links) { - layout.props.links = [] - } - layout.props.links.push({ - text: title, - url, - }) - } else { - // If using an old SDK, add to the navigation component - // TODO: remove this when we can assume everyone has updated - const nav = findComponentType( - layout.props, - "@budibase/standard-components/navigation" - ) - if (!nav) { - return - } + // Add link setting to main layout + if (!layout.props.links) { + layout.props.links = [] + } + layout.props.links.push({ + text: title, + url, + }) - let newLink - if (nav._children && nav._children.length) { - // Clone an existing link if one exists - newLink = cloneDeep(nav._children[0]) + await store.actions.layouts.save(layout) + }, + delete: async urls => { + const layout = get(mainLayout) + if (!layout?.props.links?.length) { + return + } - // Set our new props - newLink._id = Helpers.uuid() - newLink._instanceName = `${title} Link` - newLink.url = url - newLink.text = title - } else { - // Otherwise create vanilla new link - newLink = { - ...store.actions.components.createInstance("link"), - url, - text: title, - _instanceName: `${title} Link`, - } - nav._children = [...nav._children, newLink] - } - } + // Filter out the URLs to delete + urls = Array.isArray(urls) ? urls : [urls] + layout.props.links = layout.props.links.filter( + link => !urls.includes(link.url) + ) - // Save layout - await store.actions.layouts.save(layout) - }, - delete: async (url, title) => { - const layout = get(mainLayout) - if (!layout) { - return - } - - // Add link setting to main layout - if (layout.props._component.endsWith("layout")) { - // If using a new SDK, add to the layout component settings - layout.props.links = layout.props.links.filter( - link => !(link.text === title && link.url === url) - ) - } else { - // If using an old SDK, add to the navigation component - // TODO: remove this when we can assume everyone has updated - const nav = findComponentType( - layout.props, - "@budibase/standard-components/navigation" - ) - if (!nav) { - return - } - - nav._children = nav._children.filter( - child => !(child.url === url && child.text === title) - ) - } - // Save layout - await store.actions.layouts.save(layout) - }, + await store.actions.layouts.save(layout) + }, + }, + settings: { + highlight: key => { + store.update(state => ({ + ...state, + highlightedSettingKey: key, + })) }, }, } diff --git a/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js b/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js index 2b9d2bc663..dd97c511e5 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/newRowScreen.js @@ -15,7 +15,7 @@ export default function (tables) { name: `${table.name} - New`, create: () => createScreen(table), id: NEW_ROW_TEMPLATE, - table: table.name, + table: table._id, } }) } diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js index 8ab4a2bea7..a1916769c9 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js @@ -17,7 +17,7 @@ export default function (tables) { name: `${table.name} - Detail`, create: () => createScreen(table), id: ROW_DETAIL_TEMPLATE, - table: table.name, + table: table._id, } }) } diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js index c369f99f68..39e88ae69e 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowListScreen.js @@ -10,7 +10,7 @@ export default function (tables) { name: `${table.name} - List`, create: () => createScreen(table), id: ROW_LIST_TEMPLATE, - table: table.name, + table: table._id, } }) } diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte index 3e58b25ff6..9c987c89d8 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte @@ -65,7 +65,7 @@ { - $automationStore.selectedAutomation.automation.showTestPanel = true + $automationStore.showTestPanel = true }} size="M">Test Details diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte index a4c41c6948..291575f3f2 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte @@ -1,6 +1,4 @@
@@ -60,16 +80,13 @@
- {#if showTestStatus && testResult && testResult[0]} + {#if showTestStatus && testResult}
{testResult[0].outputs?.success || isTrigger - ? "Success" - : "Error"}{status?.message}
{/if} diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte index fecd0fcc7e..b86cffb1f9 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte @@ -51,7 +51,7 @@ $automationStore.selectedAutomation?.automation, testData ) - $automationStore.selectedAutomation.automation.showTestPanel = true + $automationStore.showTestPanel = true } catch (error) { notifications.error("Error testing notification") } diff --git a/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte b/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte new file mode 100644 index 0000000000..a2eb904c94 --- /dev/null +++ b/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte @@ -0,0 +1,137 @@ + + +
+ {#each blocks as block, idx} +
+ {#if block.stepId !== "LOOP"} + + {#if showParameters && showParameters[block.id]} + + {#if filteredResults?.[idx]?.outputs.iterations} +
+ +
+ +
+
+ {/if} + +
+ + +
+