mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 08:58:26 +00:00
Merge branch 'release/v0.8.0' into main
This commit is contained in:
commit
310656afb8
141 changed files with 19964 additions and 1226 deletions
|
|
@ -23,7 +23,7 @@ DEFAULT_FROM_EMAIL=hello@tooljet.io
|
|||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_DOMAIN=
|
||||
SMTP_ADDRESS=
|
||||
SMTP_PORT=
|
||||
|
||||
# DISABLE USER SIGNUPS (true or false). Default: true
|
||||
DISABLE_SIGNUPS=
|
||||
|
|
|
|||
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
v14.17.3
|
||||
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
0.7.4
|
||||
0.8.0
|
||||
|
|
@ -36,7 +36,7 @@ Pull requests are the best way to propose changes to the codebase (we use [Git-F
|
|||
In short, when you submit code changes, your submissions are understood to be under the same [AGPL v3 License](https://www.gnu.org/licenses/agpl-3.0.en.html) that covers the project.
|
||||
|
||||
## Report bugs using Github's [issues](https://github.com/ToolJet/ToolJet/issues)
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy!
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/ToolJet/ToolJet/issues/new/choose). It's that easy!
|
||||
|
||||
**Great Bug Reports** tend to have:
|
||||
|
||||
|
|
@ -52,4 +52,4 @@ We use GitHub issues to track public bugs. Report a bug by [opening a new issue]
|
|||
By contributing, you agree that your contributions will be licensed under its AGPL v3 License.
|
||||
|
||||
## Questions?
|
||||
Contact us on slack [Slack](https://join.slack.com/t/tooljet/shared_invite/zt-r2neyfcw-KD1COL6t2kgVTlTtAV5rtg) or mail us at [hello@tooljet.io](hello@tooljet)
|
||||
Contact us on slack [Slack](https://join.slack.com/t/tooljet/shared_invite/zt-r2neyfcw-KD1COL6t2kgVTlTtAV5rtg) or mail us at [hello@tooljet.io](mailto:hello@tooljet.io.).
|
||||
|
|
|
|||
35
README.md
35
README.md
|
|
@ -4,7 +4,7 @@
|
|||
Build and deploy internal tools.
|
||||
</p>
|
||||
|
||||
ToolJet is an **open-source no-code framework** to build and deploy internal tools quickly without much effort from the engineering teams. You can connect to your data sources such as databases ( like PostgreSQL, MongoDB, Elasticsearch, etc ), API endpoints ( ToolJet supports importing OpenAPI spec & OAuth2 authorization) and external services ( like Stripe, Slack, Google Sheets, Airtable ) and use our pre-built UI widgets to build internal tools.
|
||||
ToolJet is an **open-source no-code framework** to build and deploy internal tools quickly without much effort from the engineering teams. You can connect to your data sources such as databases ( like PostgreSQL, MongoDB, Elasticsearch, etc ), API endpoints ( ToolJet supports importing OpenAPI spec & OAuth2 authorization), and external services ( like Stripe, Slack, Google Sheets, Airtable ) and use our pre-built UI widgets to build internal tools.
|
||||
|
||||

|
||||

|
||||
|
|
@ -27,23 +27,23 @@ ToolJet is an **open-source no-code framework** to build and deploy internal too
|
|||
|
||||
## Features
|
||||
|
||||
- Visual app builder with widgets such as tables, charts, modals, buttons, dropdowns and more
|
||||
- Visual app builder with widgets such as tables, charts, modals, buttons, dropdowns, and more.
|
||||
- Mobile 📱 & desktop layouts 🖥
|
||||
- Dark mode 🌛
|
||||
- Connect to databases, APIs and external services
|
||||
- Deploy on-premise ( supports docker, kubernetes, heroku and more )
|
||||
- Granular access control on organization level and app level
|
||||
- Write JS code almost anywhere in the builder
|
||||
- Query editors for all supported data sources
|
||||
- Transform query results using JS code
|
||||
- Connect to databases, APIs, and external services.
|
||||
- Deploy on-premise (supports docker, kubernetes, heroku, and more)
|
||||
- Granular access control on organization-level and app-level.
|
||||
- Write JS code almost anywhere in the builder.
|
||||
- Query editors for all supported data sources.
|
||||
- Transform query results using JS code.
|
||||
- All the credentials are securely encrypted using `aes-256-gcm`.
|
||||
- ToolJet acts only as a proxy and doesn't store any data.
|
||||
- Support for OAuth
|
||||
- Support for OAuth.
|
||||
|
||||
<hr>
|
||||
|
||||
## Quickstart
|
||||
The easiest way to get started with ToolJet is by creating a [ToolJet Cloud](https://tooljet.io) account. ToolJet Cloud offers a hosted solution of ToolJet. If you want to self-host ToolJet, please proceed to [deployment documentation](https://docs.tooljet.io/docs/deployment/architecture).
|
||||
The easiest way to get started with ToolJet is by creating a [ToolJet Cloud](https://tooljet.io) account. ToolJet Cloud offers a hosted solution of ToolJet. If you want to self-host ToolJet, kindly proceed to [deployment documentation](https://docs.tooljet.io/docs/deployment/architecture).
|
||||
|
||||
You can deploy ToolJet on Heroku for free using the one-click-deployment button below.
|
||||
<p align="center">
|
||||
|
|
@ -55,19 +55,18 @@ You can deploy ToolJet on Heroku for free using the one-click-deployment button
|
|||
[Building a Github contributor leaderboard using ToolJet](https://blog.tooljet.io/building-a-github-contributor-leaderboard-using-tooljet/)<br>
|
||||
|
||||
## Documentation
|
||||
The documentation is available at https://docs.tooljet.io
|
||||
Documentation is available at https://docs.tooljet.io.
|
||||
|
||||
[Getting Started](https://docs.tooljet.io)<br>
|
||||
[Deploying](https://docs.tooljet.io)<br>
|
||||
[Datasource Reference](https://docs.tooljet.io)<br>
|
||||
[Widget Reference](https://docs.tooljet.io)
|
||||
- [Getting Started](https://docs.tooljet.io)<br>
|
||||
- [Deploying](https://docs.tooljet.io/docs/deployment/architecture)<br>
|
||||
- [Datasource Reference](https://docs.tooljet.io/docs/data-sources/airtable/)<br>
|
||||
- [Widget Reference](https://docs.tooljet.io/docs/widgets/button)
|
||||
|
||||
## Branching model
|
||||
We use the git-flow branching model. The base branch is develop. If you are looking for a stable version, please use the main branch or tags labelled as v1.x.x.
|
||||
We use git-flow branching model. The base branch is `develop`. If you are looking for a stable version, please use the main branch or tags labeled as v1.x.x.
|
||||
|
||||
## Contributing
|
||||
Read our contributing guide (CONTRIBUTING.md) to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to ToolJet. <br>
|
||||
[Contributing Guide](https://docs.tooljet.io/docs/contributing-guide/setup/docker)
|
||||
Kindly read our [Contributing Guide](CONTRIBUTING.md) to learn and understand about our development process, how to propose bugfixes and improvements, and how to build and test your changes to ToolJet. <br>
|
||||
|
||||
## Contributors
|
||||
<a href="https://github.com/tooljet/tooljet/graphs/contributors">
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ sudo openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \
|
|||
# Setup nginx config
|
||||
export SERVER_HOST="${SERVER_HOST:=localhost}"
|
||||
export SERVER_USER="${SERVER_USER:=www-data}"
|
||||
VARS_TO_SUBSTITUTE='$SERVER_HOST:$SERVER_USER'
|
||||
VARS_TO_SUBSTITUTE="$SERVER_HOST:$SERVER_USER"
|
||||
envsubst "${VARS_TO_SUBSTITUTE}" < /tmp/nginx.conf > /tmp/nginx-substituted.conf
|
||||
sudo cp /tmp/nginx-substituted.conf /usr/local/openresty/nginx/conf/nginx.conf
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,15 @@ sidebar_position: 1
|
|||
# Mac OS
|
||||
Follow these steps to setup and run ToolJet on Mac OS for development purposes. Open terminal and run the commands below. We recommend reading our guide on [architecture](/docs/deployment/architecture) of ToolJet before proceeding.
|
||||
|
||||
1. ## Setting up the environment
|
||||
### Install Homebrew
|
||||
## Setting up
|
||||
|
||||
1. Set up the environment
|
||||
|
||||
1.1 Install Homebrew
|
||||
```bash
|
||||
/bin/bash -c "(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
|
||||
/bin/bash -c "(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
|
||||
```
|
||||
### Install Node.js ( version: v14.17.3 )
|
||||
1.2 Install Node.js ( version: v14.17.3 )
|
||||
```bash
|
||||
brew install nvm
|
||||
export NVM_DIR=~/.nvm
|
||||
|
|
@ -19,7 +22,7 @@ Follow these steps to setup and run ToolJet on Mac OS for development purposes.
|
|||
nvm use 14.17.3
|
||||
```
|
||||
|
||||
### Install Postgres
|
||||
1.3 Install Postgres
|
||||
:::tip
|
||||
ToolJet uses a postgres database as the persistent storage for storing data related to users and apps. We do not plan to support other databases such as MySQL.
|
||||
:::
|
||||
|
|
@ -27,13 +30,15 @@ Follow these steps to setup and run ToolJet on Mac OS for development purposes.
|
|||
```bash
|
||||
brew install postgresql
|
||||
```
|
||||
2. ## Setup environment variables
|
||||
Create a `.env` file by copying `.env.example`. More information on the variables that can be set is given here: env variable reference
|
||||
|
||||
2. Set up environment variables
|
||||
|
||||
Create a `.env` file by copying `.env.example`. More information on the variables that can be set is given in the [environment variables reference](/docs/deployment/env-vars)
|
||||
```bash
|
||||
cp .env.example .env
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
3. ## Populate the keys in the env file.
|
||||
3. Populate the keys in the env file
|
||||
:::info
|
||||
`SECRET_KEY_BASE` requires a 64 byte key. (If you have `openssl` installed, run `openssl rand -hex 64` to create a 64 byte secure random key)
|
||||
|
||||
|
|
@ -56,50 +61,50 @@ Follow these steps to setup and run ToolJet on Mac OS for development purposes.
|
|||
ORM_LOGGING=all
|
||||
```
|
||||
|
||||
4. ## Install dependencies
|
||||
4. Install dependencies
|
||||
```bash
|
||||
npm install --prefix server
|
||||
npm install --prefix frontend
|
||||
```
|
||||
5. ## Setup database
|
||||
|
||||
5. Set up database
|
||||
```bash
|
||||
npm run --prefix server db:reset
|
||||
```
|
||||
6. ## Install webpack & nest-cli
|
||||
|
||||
6. Install webpack & nest-cli
|
||||
```bash
|
||||
npm install -g webpack
|
||||
npm install -g webpack-cli
|
||||
npm install -g @nestjs/cli
|
||||
```
|
||||
|
||||
7. ## Running the server
|
||||
7. Run the server
|
||||
```bash
|
||||
cd ./server && npm run start:dev
|
||||
```
|
||||
|
||||
8. ## Running the client
|
||||
8. Run the client
|
||||
```bash
|
||||
cd ./frontend && npm start
|
||||
cd ./frontend && npm start
|
||||
```
|
||||
|
||||
The client will start on the port 8082, you can access the client by visiting: [https://localhost:8082](https://localhost:8082)
|
||||
|
||||
9. ## Creating login credentials
|
||||
9. Create login credentials
|
||||
|
||||
Visiting [https://localhost:8082](https://localhost:8082) should redirect you to the login page, click on the signup link and enter your email. The emails sent by the server in development environment are captured and are opened in your default browser. Click the invitation link in the email preview to setup the account.
|
||||
|
||||
|
||||
10. ## Running tests
|
||||
## Running tests
|
||||
|
||||
Test config requires the presence of `.env.test` file at the root of the project.
|
||||
|
||||
To run the unit tests
|
||||
|
||||
```bash
|
||||
$ npm run --prefix server test
|
||||
npm run --prefix server test
|
||||
```
|
||||
|
||||
To run e2e tests
|
||||
|
||||
```bash
|
||||
npm run --prefix server test:e2e
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ sidebar_position: 1
|
|||
---
|
||||
|
||||
# Docker
|
||||
|
||||
Docker compose is the easiest way to setup ToolJet server and client locally.
|
||||
|
||||
## Prerequisites
|
||||
|
|
@ -10,9 +11,11 @@ Docker compose is the easiest way to setup ToolJet server and client locally.
|
|||
Make sure you have the latest version of `docker` and `docker-compose` installed.
|
||||
|
||||
[Official docker installation guide](https://docs.docker.com/desktop/)
|
||||
|
||||
[Official docker-compose installation guide](https://docs.docker.com/compose/install/)
|
||||
|
||||
We recommend:
|
||||
|
||||
```bash
|
||||
docker --version
|
||||
Docker version 19.03.12, build 48a66213fe
|
||||
|
|
@ -23,25 +26,26 @@ docker-compose version 1.26.2, build eefe0d31
|
|||
|
||||
## Setting up
|
||||
|
||||
1. Close the repository
|
||||
1. Clone the repository
|
||||
```bash
|
||||
git clone https://github.com/tooljet/tooljet.git
|
||||
git clone https://github.com/tooljet/tooljet.git
|
||||
```
|
||||
|
||||
2. Create a `.env` file by copying `.env.example`. More information on the variables that can be set is given here: env variable reference
|
||||
2. Create a `.env` file by copying `.env.example`. More information on the variables that can be set is given in the [environment variables reference](/docs/deployment/env-vars)
|
||||
```bash
|
||||
cp .env.example .env
|
||||
cp .env.example .env.test
|
||||
cp .env.example .env
|
||||
cp .env.example .env.test
|
||||
```
|
||||
|
||||
3. Populate the keys in the `.env` and `.env.test` file.
|
||||
3. Populate the keys in the `.env` and `.env.test` file
|
||||
:::info
|
||||
`SECRET_KEY_BASE` requires a 64 byte key. (If you have `openssl` installed, run `openssl rand -hex 64` to create a 64 byte secure random key)
|
||||
`SECRET_KEY_BASE` requires a 64 byte key. (If you have `openssl` installed, run `openssl rand -hex 64` to create a 64 byte secure random key)
|
||||
|
||||
`LOCKBOX_MASTER_KEY` requires a 32 byte key. (Run `openssl rand -hex 32` to create a 32 byte secure random key)
|
||||
:::
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
cat .env
|
||||
TOOLJET_HOST=http://localhost:8082
|
||||
|
|
@ -73,31 +77,33 @@ docker-compose version 1.26.2, build eefe0d31
|
|||
```
|
||||
|
||||
4. Build docker images
|
||||
|
||||
```bash
|
||||
docker-compose build
|
||||
docker-compose build
|
||||
```
|
||||
|
||||
5. Run ToolJet
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
6. ToolJet server is built using NestJS and the data such as application definitions are persisted on a postgres database. You can run the below command to seed the database.
|
||||
|
||||
```bash
|
||||
docker-compose run --rm server npm run db:seed
|
||||
```
|
||||
|
||||
7. ToolJet should now be served locally at `http://localhost:8082`. You can login using the default user created.
|
||||
```
|
||||
email: dev@tooljet.io
|
||||
password: password
|
||||
```
|
||||
```
|
||||
email: dev@tooljet.io
|
||||
password: password
|
||||
```
|
||||
|
||||
|
||||
8. To shut down the containers,
|
||||
```bash
|
||||
docker-compose stop
|
||||
```
|
||||
8. To shut down the containers,
|
||||
```bash
|
||||
docker-compose stop
|
||||
```
|
||||
|
||||
## Making changes to the codebase
|
||||
|
||||
|
|
@ -107,10 +113,11 @@ Caveat:
|
|||
|
||||
1. If the changes include database migrations or new npm package additions in the package.json, you would need to restart the ToolJet server container by running `docker-compose restart server`.
|
||||
|
||||
2. If you need to add a new binary or system libary to the container itself, you would need to add those dependencies in `docker/server.Dockerfile.dev` and then rebuild the ToolJet server image. You can do that by running `docker-compose build server`. Once that completes you can start everything normally with `docker-compose up`.
|
||||
2. If you need to add a new binary or system library to the container itself, you would need to add those dependencies in `docker/server.Dockerfile.dev` and then rebuild the ToolJet server image. You can do that by running `docker-compose build server`. Once that completes you can start everything normally with `docker-compose up`.
|
||||
|
||||
Example:
|
||||
Let's say you need to install the `imagemagick` binary in your ToolJet server's container. You'd then need to make sure that `apt` installs `imagemagick` while building the image. The Dockerfile at `docker/server.Dockerfile.dev` for the server would then look something like this:
|
||||
|
||||
```
|
||||
FROM node:14.17.0-buster
|
||||
|
||||
|
|
@ -137,32 +144,32 @@ COPY ./.env ../.env
|
|||
RUN ["chmod", "755", "entrypoint.sh"]
|
||||
|
||||
```
|
||||
Once you've updated the Dockerfile, rebuild the image by running `docker-compose build server`. After building the new image, start the services by running `docker-compose up`.
|
||||
|
||||
Once you've updated the Dockerfile, rebuild the image by running `docker-compose build server`. After building the new image, start the services by running `docker-compose up`.
|
||||
|
||||
## Running tests
|
||||
|
||||
Test config picks up config from `.env.test` file at the root of the project.
|
||||
|
||||
Run the following command to create and migrate data for test db
|
||||
|
||||
```bash
|
||||
docker-compose run --rm -e NODE_ENV=test server npm run db:create
|
||||
docker-compose run --rm -e NODE_ENV=test server npm run db:migrate
|
||||
```
|
||||
|
||||
To run the unit tests
|
||||
|
||||
```bash
|
||||
$ docker-compose --rm run server npm run test
|
||||
docker-compose --rm run server npm run test
|
||||
```
|
||||
|
||||
To run e2e tests
|
||||
|
||||
```bash
|
||||
docker-compose run --rm server npm run test:e2e
|
||||
```
|
||||
|
||||
To run a specific unit test
|
||||
|
||||
```bash
|
||||
docker-compose run --rm server npm run test <path-to-file>
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,27 +5,30 @@ sidebar_position: 1
|
|||
# Ubuntu
|
||||
Follow these steps to setup and run ToolJet on Ubuntu. Open terminal and run the commands below.
|
||||
|
||||
1. ## Setting up the environment
|
||||
### Install Node.js
|
||||
## Setting up
|
||||
|
||||
1. Set up the environment
|
||||
|
||||
1.1 Install Node.js
|
||||
```bash
|
||||
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
```
|
||||
|
||||
### Install Postgres
|
||||
1.2 Install Postgres
|
||||
```bash
|
||||
sudo apt install postgresql postgresql-contrib
|
||||
sudo apt-get install libpq-dev
|
||||
sudo apt install postgresql postgresql-contrib
|
||||
sudo apt-get install libpq-dev
|
||||
```
|
||||
|
||||
2. ## Setup environment variables
|
||||
Create a `.env` file by copying `.env.example`. More information on the variables that can be set is given here: env variable reference
|
||||
2. Set up environment variables
|
||||
|
||||
Create a `.env` file by copying `.env.example`. More information on the variables that can be set is given in the [environment variables reference](/docs/deployment/env-vars)
|
||||
```bash
|
||||
cp .env.example .env
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
|
||||
3. ## Populate the keys in the env file.
|
||||
3. Populate the keys in the env file
|
||||
:::info
|
||||
`SECRET_KEY_BASE` requires a 64 byte key. (If you have `openssl` installed, run `openssl rand -hex 64` to create a 64 byte secure random key)
|
||||
|
||||
|
|
@ -34,49 +37,50 @@ Follow these steps to setup and run ToolJet on Ubuntu. Open terminal and run the
|
|||
|
||||
Example:
|
||||
```bash
|
||||
cat .env
|
||||
cat .env
|
||||
TOOLJET_HOST=http://localhost:8082
|
||||
LOCKBOX_MASTER_KEY=1d291a926ddfd221205a23adb4cc1db66cb9fcaf28d97c8c1950e3538e3b9281
|
||||
SECRET_KEY_BASE=4229d5774cfe7f60e75d6b3bf3a1dbb054a696b6d21b6d5de7b73291899797a222265e12c0a8e8d844f83ebacdf9a67ec42584edf1c2b23e1e7813f8a3339041
|
||||
```
|
||||
|
||||
4. ## Install dependencies
|
||||
4. Install dependencies
|
||||
```bash
|
||||
npm install --prefix server
|
||||
npm install --prefix frontend
|
||||
```
|
||||
5. ## Setup database
|
||||
|
||||
5. Set up database
|
||||
```bash
|
||||
npm run --prefix server db:reset
|
||||
```
|
||||
6. ## Running the server
|
||||
|
||||
6. Run the server
|
||||
```bash
|
||||
cd ./server && npm run start:dev
|
||||
```
|
||||
|
||||
7. ## Running the client
|
||||
7. Run the client
|
||||
```bash
|
||||
cd ./frontend && npm start
|
||||
```
|
||||
|
||||
The client will start running on the port 8082, you can access the client by visiting: [https://localhost:8082](https://localhost:8082)
|
||||
|
||||
8. ## Creating login credentials
|
||||
Visiting https://localhost:8082 should redirect you to the login page, click on the signup link and enter your email. The emails sent by the server in development environment are captured and are opened in your default browser. Click the invitation link in the email preview to setup the account.
|
||||
8. Create login credentials
|
||||
|
||||
Visiting https://localhost:8082 should redirect you to the login page, click on the signup link and enter your email. The emails sent by the server in development environment are captured and are opened in your default browser. Click the invitation link in the email preview to setup the account.
|
||||
|
||||
|
||||
9. ## Running tests
|
||||
## Running tests
|
||||
|
||||
Test config requires the presence of `.env.test` file at the root of the project.
|
||||
|
||||
To run the unit tests
|
||||
|
||||
```bash
|
||||
$ npm run --prefix server test
|
||||
npm run --prefix server test
|
||||
```
|
||||
|
||||
To run e2e tests
|
||||
|
||||
```bash
|
||||
npm run --prefix server test:e2e
|
||||
```
|
||||
|
|
|
|||
38
docs/docs/data-sources/mongodb.md
Normal file
38
docs/docs/data-sources/mongodb.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
sidebar_position: 11
|
||||
---
|
||||
|
||||
# MongoDB
|
||||
|
||||
ToolJet can connect to MongoDB to read and write data.
|
||||
|
||||
## Connection
|
||||
|
||||
Please make sure the host/ip of the database is accessible from your VPC if you have self-hosted ToolJet. If you are using ToolJet cloud, please whitelist our IP.
|
||||
|
||||
To add a new MongoDB, click on the `+` button on data sources panel at the left-bottom corner of the app editor. Select MongoDB from the modal that pops up.
|
||||
|
||||
ToolJet requires the following to connect to your MongoDB.
|
||||
|
||||
- **Host**
|
||||
- **Port**
|
||||
- **Username**
|
||||
- **Password**
|
||||
|
||||
It is recommended to create a new MongoDB user so that you can control the access levels of ToolJet.
|
||||
|
||||
<img src="/img/datasource-reference/mo-connect.png" alt="ToolJet - Mongo connection" height="250"/>
|
||||
|
||||
Click on 'Test connection' button to verify if the credentials are correct and that the database is accessible to ToolJet server. Click on 'Save' button to save the datasource.
|
||||
|
||||
## Querying MongoDB
|
||||
|
||||
Click on `+` button of the query manager at the bottom panel of the editor and select the database added in the previous step as the datasource. Select the operation that you want to perform and click 'Save' to save the query.
|
||||
|
||||
<img src="/img/datasource-reference/mo-query.png" alt="ToolJet - Mongo query" height="250"/>
|
||||
|
||||
Click on the 'run' button to run the query. NOTE: Query should be saved before running.
|
||||
|
||||
:::tip
|
||||
Query results can be transformed using transformations. Read our transformations documentation to see how: [link](/docs/tutorial/transformations)
|
||||
:::
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
sidebar_position: 6
|
||||
sidebar_position: 7
|
||||
---
|
||||
|
||||
# Deploying ToolJet client
|
||||
|
|
@ -15,14 +15,14 @@ For example: `TOOLJET_SERVER_URL=https://server.tooljet.io npm run build && fire
|
|||
:::
|
||||
|
||||
1. Initialize firebase project
|
||||
```bash
|
||||
firebase init
|
||||
```
|
||||
Select Firebase Hosting and set build as the static file directory
|
||||
```bash
|
||||
firebase init
|
||||
```
|
||||
Select Firebase Hosting and set build as the static file directory
|
||||
2. Deploy client to Firebase
|
||||
```bash
|
||||
firebase deploy
|
||||
```
|
||||
```bash
|
||||
firebase deploy
|
||||
```
|
||||
|
||||
:::tip
|
||||
If you want to run ToolJet on your local machine, please checkout the setup section of the contributing guide: [link](/docs/contributing-guide/setup/docker)
|
||||
|
|
|
|||
|
|
@ -43,18 +43,18 @@ For example, if the server is an AWS EC2 instance and the installation should re
|
|||
|
||||
`TOOLJET_HOST` environment variable can either be the public ipv4 address of your server or a custom domain that you want to use.
|
||||
|
||||
:::info
|
||||
We use a [lets encrypt](https://letsencrypt.org/) plugin on top of nginx to create TLS certificates on the fly.
|
||||
:::
|
||||
:::info
|
||||
We use a [lets encrypt](https://letsencrypt.org/) plugin on top of nginx to create TLS certificates on the fly.
|
||||
:::
|
||||
|
||||
Examples:
|
||||
`TOOLJET_HOST=http://12.34.56.78` or
|
||||
`TOOLJET_HOST=https://yourdomain.com` or
|
||||
`TOOLJET_HOST=https://tooljet.yourdomain.com`
|
||||
|
||||
:::info
|
||||
Please make sure that `TOOLJET_HOST` starts with either `http://` or `https://`
|
||||
:::
|
||||
:::info
|
||||
Please make sure that `TOOLJET_HOST` starts with either `http://` or `https://`
|
||||
:::
|
||||
|
||||
6. Once you've populated the `.env` file, run
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ For example, if the server is an AWS EC2 instance and the installation should re
|
|||
If you're running on a linux server, `docker` might need sudo permissions. In that case you can either run:
|
||||
`sudo docker-compose up -d`
|
||||
OR
|
||||
Setup docker to run without root privilages by following the instructions written here https://docs.docker.com/engine/install/linux-postinstall/
|
||||
Setup docker to run without root privileges by following the instructions written here https://docs.docker.com/engine/install/linux-postinstall/
|
||||
:::
|
||||
|
||||
7. If you've set a custom domain for `TOOLJET_HOST`, add a `A record` entry in your DNS settings to point to the IP address of the server.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
sidebar_position: 7
|
||||
sidebar_position: 8
|
||||
---
|
||||
|
||||
# Environment variables
|
||||
|
|
@ -10,30 +10,28 @@ Both the ToolJet server and client requires some environment variables to start
|
|||
|
||||
#### ToolJet host ( required )
|
||||
|
||||
| variable | description |
|
||||
| ----------- | ----------- |
|
||||
| variable | description |
|
||||
| ------------ | --------------------------------------------------------------- |
|
||||
| TOOLJET_HOST | the public URL of ToolJet client ( eg: https://app.tooljet.io ) |
|
||||
|
||||
|
||||
|
||||
#### Database configuration ( required )
|
||||
|
||||
ToolJet server uses PostgreSQL as the database.
|
||||
|
||||
| variable | description |
|
||||
| ----------- | ----------- |
|
||||
| PG_HOST | postgres database host |
|
||||
| PG_DB | name of the database |
|
||||
| PG_USER | username |
|
||||
| PG_PASS | password |
|
||||
| variable | description |
|
||||
| -------- | ---------------------- |
|
||||
| PG_HOST | postgres database host |
|
||||
| PG_DB | name of the database |
|
||||
| PG_USER | username |
|
||||
| PG_PASS | password |
|
||||
|
||||
#### Lockbox configuration ( required )
|
||||
|
||||
ToolJet server uses lockbox to encrypt datasource credentials. You should set the environment variable `LOCKBOX_MASTER_KEY` with a 32 byte hexadecimal string.
|
||||
|
||||
|
||||
#### Application Secret ( required )
|
||||
ToolJet server uses a secure 64 byte hexadecimal string to encrypt session cookies. You should set the environment variable `SECRET_KEY_BASE`.
|
||||
|
||||
ToolJet server uses a secure 64 byte hexadecimal string to encrypt session cookies. You should set the environment variable `SECRET_KEY_BASE`.
|
||||
|
||||
:::tip
|
||||
If you have `openssl` installed, you can run the following commands to generate the the value for `LOCKBOX_MASTER_KEY` and `SECRET_KEY_BASE`.
|
||||
|
|
@ -42,7 +40,6 @@ For `LOCKBOX_MASTER_KEY` use `openssl rand -hex 32`
|
|||
For `SECRET_KEY_BASE` use `openssl rand -hex 64`
|
||||
:::
|
||||
|
||||
|
||||
#### Disabling signups ( optional )
|
||||
|
||||
If want to restrict the signups and allow new users only by invitations, set the environment variable `DISABLE_SIGNUPS` to `true`.
|
||||
|
|
@ -53,7 +50,7 @@ You will still be able to see the signup page but won't be able to successfully
|
|||
|
||||
#### Serve client as a server end-point ( optional )
|
||||
|
||||
By default, the `SERVE_CLIENT` variable will be set to `false` and the server won't serve the client at its `/` end-point.
|
||||
By default, the `SERVE_CLIENT` variable will be set to `false` and the server won't serve the client at its `/` end-point.
|
||||
You can set `SERVE_CLIENT` to `true` and the server will attempt to serve the client at its root end-point (`/`).
|
||||
|
||||
#### SMTP configuration ( optional )
|
||||
|
|
@ -61,7 +58,7 @@ You can set `SERVE_CLIENT` to `true` and the server will attempt to serve the cl
|
|||
ToolJet uses SMTP services to send emails ( Eg: invitation email when you add new users to your organization ).
|
||||
|
||||
| variable | description |
|
||||
|--------------------|-------------------------------------------|
|
||||
| ------------------ | ----------------------------------------- |
|
||||
| DEFAULT_FROM_EMAIL | from email for the email fired by ToolJet |
|
||||
| SMTP_USERNAME | username |
|
||||
| SMTP_PASSWORD | password |
|
||||
|
|
@ -72,34 +69,34 @@ ToolJet uses SMTP services to send emails ( Eg: invitation email when you add ne
|
|||
|
||||
If your ToolJet installation requires Slack as a datasource, you need to create a Slack app and set the following environment variables:
|
||||
|
||||
| variable | description |
|
||||
| ----------- | ----------- |
|
||||
| SLACK_CLIENT_ID | client id of the slack app |
|
||||
| SLACK_CLIENT_SECRET | client secret of the slack app |
|
||||
| variable | description |
|
||||
| ------------------- | ------------------------------ |
|
||||
| SLACK_CLIENT_ID | client id of the slack app |
|
||||
| SLACK_CLIENT_SECRET | client secret of the slack app |
|
||||
|
||||
#### Google OAuth ( optional )
|
||||
|
||||
If your ToolJet installation needs access to datasources such as Google sheets, you need to create OAuth credentials from Google Cloud Console.
|
||||
|
||||
| variable | description |
|
||||
| ----------- | ----------- |
|
||||
| GOOGLE_CLIENT_ID | client id |
|
||||
| GOOGLE_CLIENT_SECRET | client secret |
|
||||
| variable | description |
|
||||
| -------------------- | ------------- |
|
||||
| GOOGLE_CLIENT_ID | client id |
|
||||
| GOOGLE_CLIENT_SECRET | client secret |
|
||||
|
||||
#### Google maps configuration ( optional )
|
||||
|
||||
If your ToolJet installation requires `Maps` widget, you need to create an API key for Google Maps API.
|
||||
|
||||
| variable | description |
|
||||
| ----------- | ----------- |
|
||||
| variable | description |
|
||||
| ------------------- | ------------------- |
|
||||
| GOOGLE_MAPS_API_KEY | Google maps API key |
|
||||
|
||||
#### APM VENDOR ( optional )
|
||||
|
||||
Specify application monitoring vendor. Currently supported values - `sentry`.
|
||||
|
||||
| variable | description |
|
||||
| ----------- | ----------- |
|
||||
| variable | description |
|
||||
| ---------- | ----------------------------------------- |
|
||||
| APM VENDOR | Application performance monitoring vendor |
|
||||
|
||||
#### SENTRY DNS ( optional )
|
||||
|
|
@ -112,21 +109,23 @@ Prints logs for sentry. Supported values: `true` | `false`
|
|||
Default value is `false`
|
||||
|
||||
#### Server URL ( optional)
|
||||
|
||||
This is used to set up for CSP headers and put trace info to be used with APM vendors.
|
||||
|
||||
| variable | description |
|
||||
| ----------- | ----------- |
|
||||
| variable | description |
|
||||
| ------------------ | ----------------------------------------------------------- |
|
||||
| TOOLJET_SERVER_URL | the URL of ToolJet server ( eg: https://server.tooljet.io ) |
|
||||
|
||||
|
||||
#### RELEASE VERSION ( optional)
|
||||
|
||||
Once set any APM provider that supports segregation with releases will track it.
|
||||
|
||||
## ToolJet client
|
||||
|
||||
#### Server URL ( optionally required )
|
||||
|
||||
This is required when client is built separately.
|
||||
|
||||
| variable | description |
|
||||
| ----------- | ----------- |
|
||||
| variable | description |
|
||||
| ------------------ | ----------------------------------------------------------- |
|
||||
| TOOLJET_SERVER_URL | the URL of ToolJet server ( eg: https://server.tooljet.io ) |
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
sidebar_position: 2
|
||||
sidebar_position: 3
|
||||
sidebar_label: Heroku
|
||||
---
|
||||
|
||||
|
|
@ -8,14 +8,15 @@ sidebar_label: Heroku
|
|||
Follow the steps below to deploy ToolJet on Heroku:
|
||||
|
||||
1. Click the button below to start one click deployment.
|
||||
[](https://heroku.com/deploy?template=https://github.com/tooljet/tooljet/tree/main)
|
||||
[](https://heroku.com/deploy?template=https://github.com/tooljet/tooljet/tree/main)
|
||||
|
||||
2. Navigate to Heroku dashboard and go to resources tab to verify that the dyno is turned on.
|
||||
3. Go to settings tab on Heroku dashboard and select `reveal config vars` to configure additional environment variables that your installation might need.
|
||||
3. Go to settings tab on Heroku dashboard and select `reveal config vars` to configure additional environment variables that your installation might need.
|
||||
|
||||
Read [environment variables reference](/docs/deployment/env-vars)
|
||||
|
||||
Read [environment variables reference](/docs/deployment/env-vars)
|
||||
4. Open the app.
|
||||
5. The default username of the admin is dev@tooljet.io and password is `password`.
|
||||
5. The default username of the admin is `dev@tooljet.io` and the password is `password`.
|
||||
|
||||
:::tip
|
||||
The one click deployment will create a free dyno and a free postgresql database.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
sidebar_position: 4
|
||||
sidebar_position: 6
|
||||
sidebar_label: Kubernetes (GKE)
|
||||
---
|
||||
|
||||
|
|
@ -11,52 +11,57 @@ You should setup a PostgreSQL database manually to be used by ToolJet. We recomm
|
|||
|
||||
Follow the steps below to deploy ToolJet on a GKE Kubernetes cluster.
|
||||
|
||||
1. Create an SSL certificate.
|
||||
```bash
|
||||
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/GKE/certificate.yaml
|
||||
```
|
||||
1. Create an SSL certificate.
|
||||
|
||||
```bash
|
||||
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/GKE/certificate.yaml
|
||||
```
|
||||
|
||||
Change the domain name to the domain/subdomain that you wish to use for ToolJet installation.
|
||||
|
||||
2. Reserve a static IP address using `gcloud` cli
|
||||
```bash
|
||||
gcloud compute addresses create tj-static-ip --global
|
||||
```
|
||||
2. Reserve a static IP address using `gcloud` cli
|
||||
|
||||
```bash
|
||||
gcloud compute addresses create tj-static-ip --global
|
||||
```
|
||||
|
||||
3. Create k8s deployment
|
||||
```bash
|
||||
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/GKE/deployment.yaml
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/GKE/deployment.yaml
|
||||
```
|
||||
|
||||
Make sure to edit the environment variables in the `deployment.yaml`. You can check out the available options [here](https://docs.tooljet.io/docs/deployment/env-vars).
|
||||
|
||||
4. Create k8s service
|
||||
```bash
|
||||
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/GKE/service.yaml
|
||||
```
|
||||
4. Create k8s service
|
||||
|
||||
```bash
|
||||
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/GKE/service.yaml
|
||||
```
|
||||
|
||||
5. Create k8s ingress
|
||||
```bash
|
||||
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/GKE/ingress.yaml
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/GKE/ingress.yaml
|
||||
```
|
||||
|
||||
Change the domain name to the domain/subdomain that you wish to use for ToolJet installation.
|
||||
|
||||
6. Apply YAML configs
|
||||
|
||||
```bash
|
||||
kubectl apply -f certificate.yaml, deployment.yaml, service.yaml, ingress.yaml
|
||||
```
|
||||
```bash
|
||||
kubectl apply -f certificate.yaml, deployment.yaml, service.yaml, ingress.yaml
|
||||
```
|
||||
|
||||
:::info
|
||||
It might take a few minutes to provision the managed certificates. [Managed certificates documentation](https://cloud.google.com/kubernetes-engine/docs/how-to/managed-certs).
|
||||
:::
|
||||
|
||||
You will be able to access your ToolJet installation once the pods, service and the ingress is running.
|
||||
You will be able to access your ToolJet installation once the pods, service and the ingress is running.
|
||||
|
||||
If you want to seed the database with a sample user, please SSH into a pod and run:
|
||||
`npm run db:seed --prefix server`.
|
||||
This seeds the database with a default user with the following credentials:
|
||||
|
||||
email: `dev@tooljet.io`
|
||||
password: `password`
|
||||
If you want to seed the database with a sample user, please SSH into a pod and run:
|
||||
`npm run db:seed --prefix server`.
|
||||
This seeds the database with a default user with the following credentials:
|
||||
|
||||
email: `dev@tooljet.io`
|
||||
password: `password`
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
sidebar_position: 4
|
||||
sidebar_position: 5
|
||||
sidebar_label: Kubernetes
|
||||
---
|
||||
|
||||
|
|
@ -12,17 +12,17 @@ You should setup a PostgreSQL database manually to be used by ToolJet.
|
|||
Follow the steps below to deploy ToolJet on a Kubernetes cluster.
|
||||
|
||||
1. Setup a PostgreSQL database
|
||||
ToolJet uses a postgres database as the persistent storage for storing data related to users and apps. We do not have plans to support other databases such as MySQL.
|
||||
ToolJet uses a postgres database as the persistent storage for storing data related to users and apps. We do not have plans to support other databases such as MySQL.
|
||||
|
||||
2. Create a Kubernetes secret with name `server`. For the minimal setup, ToolJet requires `pg_host`, `pg_db`, `pg_user`, `pg_password`, `secret_key_base` & `lockbox_key` keys in the secret.
|
||||
|
||||
Read [environment variables reference](/docs/deployment/env-vars)
|
||||
Read [environment variables reference](/docs/deployment/env-vars)
|
||||
|
||||
3. Create a Kubernetes deployment
|
||||
|
||||
```bash
|
||||
kubectl apply -f https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/deployment.yaml
|
||||
```
|
||||
```bash
|
||||
kubectl apply -f https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/deployment.yaml
|
||||
```
|
||||
|
||||
:::info
|
||||
The file given above is just a template and might not suit production environments. You should download the file and configure parameters such as the replica count and environment variables according to your needs.
|
||||
|
|
@ -30,14 +30,14 @@ The file given above is just a template and might not suit production environmen
|
|||
|
||||
4. Verify if ToolJet is running
|
||||
|
||||
```bash
|
||||
kubectl get pods
|
||||
```
|
||||
```bash
|
||||
kubectl get pods
|
||||
```
|
||||
|
||||
5. Create a Kubernetes services to publish the Kubernetes deployment that you've created. This step varies with cloud providers. We have a [template](https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/service.yaml) for exposing the ToolJet server as a service using an AWS loadbalancer.
|
||||
Examples:
|
||||
Application load balancing on Amazon EKS: https://docs.aws.amazon.com/eks/latest/userguide/alb-ingress.html
|
||||
GKE Ingress for HTTP(S) Load Balancing: https://cloud.google.com/kubernetes-engine/docs/concepts/ingress
|
||||
Examples:
|
||||
Application load balancing on Amazon EKS: https://docs.aws.amazon.com/eks/latest/userguide/alb-ingress.html
|
||||
GKE Ingress for HTTP(S) Load Balancing: https://cloud.google.com/kubernetes-engine/docs/concepts/ingress
|
||||
|
||||
:::tip
|
||||
If you want to serve ToolJet client from services such as Firebase or Netlify, please read the client deployment documentation [here](/docs/deployment/client).
|
||||
|
|
|
|||
|
|
@ -47,5 +47,10 @@ If you're setting this environment variable, please make sure that the value doe
|
|||
simply be `yourdomain.com`.
|
||||
:::
|
||||
|
||||
:::info
|
||||
### Restrict signup via SSO
|
||||
Set the environment variable `SSO_DISABLE_SIGNUP` to `true` to ensure that users can only log in and not sign up via SSO. If this variable is set to `true`, only those users who have already signed up, or the ones that are invited, can access ToolJet via SSO.
|
||||
:::
|
||||
|
||||
<br />
|
||||
The Google sign-in button will now be available in your ToolJet login screen.
|
||||
|
|
@ -30,7 +30,7 @@ Now we will connect the `data` object of the `fetch customers` query with the ta
|
|||
|
||||
Let's select `data` object of the 'postgresql' query.
|
||||
|
||||
Since we have already run the query in previous step, the data will be immedietly displayed in the table.
|
||||
Since we have already run the query in previous step, the data will be immediately displayed in the table.
|
||||
|
||||
<img class="screenshot-full" src="/img/tutorial/adding-widget/table-data.gif" alt="ToolJet - Table with data" height="420"/>
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,6 @@ You will redirected to the visual app editor once the app has been created. The
|
|||
|
||||
The main components of an app:
|
||||
|
||||
- **Widgets** - UI components such as tables, buttons, dropdowns.
|
||||
- **Data sources** - ToolJet can connect to databases, APIs and external services to fetch and modify data.
|
||||
- **Queries** - Queries are used to access the connected datasources.
|
||||
- **[Widgets](https://docs.tooljet.io/docs/tutorial/adding-widget)** - UI components such as tables, buttons, dropdowns.
|
||||
- **[Data sources](https://docs.tooljet.io/docs/tutorial/adding-a-datasource)** - ToolJet can connect to databases, APIs and external services to fetch and modify data.
|
||||
- **[Queries](https://docs.tooljet.io/docs/tutorial/building-queries)** - Queries are used to access the connected datasources.
|
||||
|
|
@ -4,7 +4,7 @@ sidebar_position: 8
|
|||
|
||||
# Mobile layout
|
||||
|
||||
Mobile layout is activated when the width of window is less than 600px.
|
||||
Mobile layout is activated when the width of the window is less than 600px.
|
||||
|
||||
:::tip
|
||||
Widgets can be shown on desktop, mobile, or both.
|
||||
|
|
@ -20,4 +20,4 @@ Switch the layout to mobile by clicking the button on the top navigation bar. Dr
|
|||
|
||||
:::tip
|
||||
Width of the widgets will be automatically adjusted to fit the screen while viewing the application in app viewer.
|
||||
:::
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ Scan QR codes using device camera and hold the data they carry.
|
|||
<img class="screenshot-full" src="/img/widgets/qr-scanner/qr-scanner.jpeg" alt="ToolJet - QR Scanner" height="420"/>
|
||||
|
||||
#### Known issue:
|
||||
In IOS, you might have to stick to the Safari browser as camera access had been restricted for third party browsers.
|
||||
You might have to stick to the Safari browser in IOS as camera access is restricted for third-party browsers.
|
||||
|
||||
## Exposed variables
|
||||
#### lastDetectedValue
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ module.exports = {
|
|||
],
|
||||
},
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} ToolJet.`,
|
||||
copyright: `Copyright © ${new Date().getFullYear()} ToolJet Solutions, Inc.`,
|
||||
},
|
||||
},
|
||||
presets: [
|
||||
|
|
|
|||
13997
docs/package-lock.json
generated
Normal file
13997
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
BIN
docs/static/img/datasource-reference/mo-connect.png
vendored
Normal file
BIN
docs/static/img/datasource-reference/mo-connect.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
BIN
docs/static/img/datasource-reference/mo-query.png
vendored
Normal file
BIN
docs/static/img/datasource-reference/mo-query.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
6
frontend/assets/images/icons/widgets/divider.svg
Normal file
6
frontend/assets/images/icons/widgets/divider.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-separator" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<line x1="3" y1="12" x2="3" y2="12.01"></line>
|
||||
<line x1="7" y1="12" x2="17" y2="12"></line>
|
||||
<line x1="21" y1="12" x2="21" y2="12.01"></line>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 446 B |
|
|
@ -3,7 +3,7 @@ set -eu
|
|||
|
||||
export SERVER_HOST="${SERVER_HOST:=server}"
|
||||
export SERVER_USER="${SERVER_USER:=root}"
|
||||
VARS_TO_SUBSTITUTE='$SERVER_HOST:$SERVER_USER'
|
||||
VARS_TO_SUBSTITUTE="$SERVER_HOST:$SERVER_USER"
|
||||
|
||||
envsubst "${VARS_TO_SUBSTITUTE}" < /etc/openresty/nginx.conf.template > /etc/openresty/nginx.conf
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import { Editor, Viewer } from '@/Editor';
|
|||
import '@/_styles/theme.scss';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { ManageGroupPermissions } from '@/ManageGroupPermissions';
|
||||
import { ManageOrgUsers } from '@/ManageOrgUsers';
|
||||
import { ManageGroupPermissionResources } from '@/ManageGroupPermissionResources';
|
||||
import { SettingsPage } from '../SettingsPage/SettingsPage';
|
||||
import { OnboardingModal } from '@/Onboarding/OnboardingModal';
|
||||
import { ForgotPassword } from '@/ForgotPassword';
|
||||
|
|
@ -134,6 +136,20 @@ class App extends React.Component {
|
|||
switchDarkMode={this.switchDarkMode}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
<PrivateRoute
|
||||
exact
|
||||
path="/groups"
|
||||
component={ManageGroupPermissions}
|
||||
switchDarkMode={this.switchDarkMode}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
<PrivateRoute
|
||||
exact
|
||||
path="/groups/:id"
|
||||
component={ManageGroupPermissionResources}
|
||||
switchDarkMode={this.switchDarkMode}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
<PrivateRoute
|
||||
exact
|
||||
path="/settings"
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { QrScanner } from './Components/QrScanner/QrScanner';
|
|||
import { ToggleSwitch } from './Components/Toggle';
|
||||
import { RadioButton } from './Components/RadioButton';
|
||||
import { StarRating } from './Components/StarRating';
|
||||
import { Divider } from './Components/Divider';
|
||||
import { renderTooltip } from '../_helpers/appUtils';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import '@/_styles/custom.scss';
|
||||
|
|
@ -46,6 +47,7 @@ const AllComponents = {
|
|||
ToggleSwitch,
|
||||
RadioButton,
|
||||
StarRating,
|
||||
Divider,
|
||||
};
|
||||
|
||||
export const Box = function Box({
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export function makeOverlay(style) {
|
|||
var ch;
|
||||
if (stream.match('{{')) {
|
||||
while ((ch = stream.next()) != null)
|
||||
if (ch == '}' && stream.next() == '}') {
|
||||
if (ch === '}' && stream.next() === '}') {
|
||||
stream.eat('}');
|
||||
return style;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
|||
var tinycolor = require('tinycolor2');
|
||||
|
||||
export const Button = function Button({ id, width, height, component, onComponentClick, currentState }) {
|
||||
console.log('currentState', currentState);
|
||||
|
||||
const [loadingState, setLoadingState] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -51,7 +49,7 @@ export const Button = function Button({ id, width, height, component, onComponen
|
|||
onComponentClick(id, component);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
{resolveReferences(text, currentState)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,9 +11,12 @@ export const Checkbox = function Checkbox({
|
|||
onComponentOptionChanged,
|
||||
onEvent,
|
||||
}) {
|
||||
const [checked, setChecked] = React.useState(false);
|
||||
const label = component.definition.properties.label.value;
|
||||
const textColorProperty = component.definition.styles.textColor;
|
||||
const textColor = textColorProperty ? textColorProperty.value : '#000';
|
||||
const checkboxColorProperty = component.definition.styles.checkboxColor;
|
||||
const checkboxColor = checkboxColorProperty ? checkboxColorProperty.value : '#3c92dc';
|
||||
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
|
||||
const disabledState = component.definition.styles?.disabledState?.value ?? false;
|
||||
|
||||
|
|
@ -29,9 +32,10 @@ export const Checkbox = function Checkbox({
|
|||
}
|
||||
|
||||
function toggleValue(e) {
|
||||
const checked = e.target.checked;
|
||||
onComponentOptionChanged(component, 'value', checked);
|
||||
if (checked) {
|
||||
const isChecked = e.target.checked;
|
||||
setChecked(isChecked);
|
||||
onComponentOptionChanged(component, 'value', isChecked);
|
||||
if (isChecked) {
|
||||
onEvent('onCheck', { component });
|
||||
} else {
|
||||
onEvent('onUnCheck', { component });
|
||||
|
|
@ -48,18 +52,21 @@ export const Checkbox = function Checkbox({
|
|||
onComponentClick(id, component);
|
||||
}}
|
||||
>
|
||||
<label className="my-auto mx-2 form-check form-check-inline">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
onClick={(e) => {
|
||||
toggleValue(e);
|
||||
}}
|
||||
/>
|
||||
<span className="form-check-label" style={{ color: textColor }}>
|
||||
{label}
|
||||
</span>
|
||||
</label>
|
||||
<div className="col px-1 py-0 mt-0">
|
||||
<label className="mx-1 form-check form-check-inline">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
onClick={(e) => {
|
||||
toggleValue(e);
|
||||
}}
|
||||
style={{ backgroundColor: checked ? `${checkboxColor}` : 'white', marginTop: '1px' }}
|
||||
/>
|
||||
<span className="form-check-label" style={{ color: textColor }}>
|
||||
{label}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
23
frontend/src/Editor/Components/Divider.jsx
Normal file
23
frontend/src/Editor/Components/Divider.jsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import { resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
|
||||
export const Divider = function Divider({ id, component, onComponentClick, currentState }) {
|
||||
const dividerColorProperty = component.definition.styles.dividerColor;
|
||||
const color = dividerColorProperty ? dividerColorProperty.value : '#E7E8EA';
|
||||
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
|
||||
|
||||
let parsedWidgetVisibility = widgetVisibility;
|
||||
|
||||
parsedWidgetVisibility = resolveWidgetFieldValue(parsedWidgetVisibility, currentState);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="hr mt-1"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onComponentClick(id, component);
|
||||
}}
|
||||
style={{ display: parsedWidgetVisibility ? '' : 'none', color: color, opacity: '1' }}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable react/no-string-refs */
|
||||
import React from 'react';
|
||||
import { Editor, EditorState, RichUtils, getDefaultKeyBinding } from 'draft-js';
|
||||
import { Editor, EditorState, RichUtils, getDefaultKeyBinding, ContentState } from 'draft-js';
|
||||
import 'draft-js/dist/Draft.css';
|
||||
import { stateToHTML } from 'draft-js-export-html';
|
||||
|
||||
|
|
@ -127,7 +127,7 @@ const InlineStyleControls = (props) => {
|
|||
class DraftEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { editorState: EditorState.createEmpty() };
|
||||
this.state = { editorState: EditorState.createWithContent(ContentState.createFromText(this.props.defaultValue)) };
|
||||
|
||||
this.focus = () => this.refs.editor.focus();
|
||||
this.onChange = (editorState) => {
|
||||
|
|
@ -142,6 +142,18 @@ class DraftEditor extends React.Component {
|
|||
this.toggleInlineStyle = this._toggleInlineStyle.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.defaultValue !== this.props.defaultValue) {
|
||||
const newContentState = ContentState.createFromText(this.props.defaultValue);
|
||||
const newEditorState = EditorState.createWithContent(newContentState);
|
||||
const newEditorStateWithFocus = EditorState.moveFocusToEnd(newEditorState);
|
||||
const html = stateToHTML(newContentState);
|
||||
|
||||
this.props.handleChange(html);
|
||||
this.setState({ editorState: newEditorStateWithFocus });
|
||||
}
|
||||
}
|
||||
|
||||
_handleKeyCommand(command, editorState) {
|
||||
const newState = RichUtils.handleKeyCommand(editorState, command);
|
||||
if (newState) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import Button from 'react-bootstrap/Button';
|
|||
import { SubCustomDragLayer } from '../SubCustomDragLayer';
|
||||
import { SubContainer } from '../SubContainer';
|
||||
import { ConfigHandle } from '../ConfigHandle';
|
||||
import { resolveWidgetFieldValue } from '../../_helpers/utils';
|
||||
import { resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
|
||||
export const Modal = function Modal({ id, component, height, containerProps, currentState, darkMode }) {
|
||||
const [show, showModal] = useState(false);
|
||||
|
|
@ -50,7 +50,7 @@ export const Modal = function Modal({ id, component, height, containerProps, cur
|
|||
<ConfigHandle id={id} component={component} configHandleClicked={containerProps.onComponentClick} />
|
||||
)}
|
||||
<BootstrapModal.Header>
|
||||
<BootstrapModal.Title>{title}</BootstrapModal.Title>
|
||||
<BootstrapModal.Title>{resolveWidgetFieldValue(title, currentState)}</BootstrapModal.Title>
|
||||
<div>
|
||||
<Button variant={darkMode ? 'secondary' : 'light'} size="sm" onClick={hideModal}>
|
||||
x
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
|||
export const QrScanner = function QrScanner({ component, onEvent, onComponentOptionChanged, currentState }) {
|
||||
const handleError = async (errorMessage) => {
|
||||
console.log(errorMessage);
|
||||
setErrorOccured(true);
|
||||
await setErrorOccured(true);
|
||||
};
|
||||
|
||||
const handleScan = async (data) => {
|
||||
if (data != null) {
|
||||
onEvent('onDetect', { component, data: data });
|
||||
onComponentOptionChanged(component, 'lastDetectedValue', data);
|
||||
if (data !== null || data !== undefined) {
|
||||
await onEvent('onDetect', { component, data: data });
|
||||
await onComponentOptionChanged(component, 'lastDetectedValue', data);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -85,20 +85,21 @@ export const RadioButton = function RadioButton({
|
|||
return (
|
||||
<div
|
||||
data-disabled={parsedDisabledState}
|
||||
className="row"
|
||||
className="row py-1"
|
||||
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onComponentClick(id, component);
|
||||
}}
|
||||
>
|
||||
<span className="form-check-label col-auto py-1" style={{ color: textColor }}>
|
||||
<span className="form-check-label col-auto py-0" style={{ color: textColor }}>
|
||||
{label}
|
||||
</span>
|
||||
<div className="col py-1">
|
||||
<div className="col px-1 py-0 mt-0">
|
||||
{selectOptions.map((option, index) => (
|
||||
<label key={index} className="form-check form-check-inline">
|
||||
<input
|
||||
style={{ marginTop: '1px' }}
|
||||
className="form-check-input"
|
||||
checked={value === option.value}
|
||||
type="radio"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export const RichTextEditor = function RichTextEditor({
|
|||
const placeholder = component.definition.properties.placeholder.value;
|
||||
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
|
||||
const disabledState = component.definition.styles?.disabledState?.value ?? false;
|
||||
const defaultValue = component.definition.properties?.defaultValue?.value ?? '';
|
||||
|
||||
const parsedDisabledState =
|
||||
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
|
||||
|
|
@ -40,7 +41,13 @@ export const RichTextEditor = function RichTextEditor({
|
|||
onComponentClick(id, component);
|
||||
}}
|
||||
>
|
||||
<DraftEditor handleChange={handleChange} height={height} width={width} placeholder={placeholder}></DraftEditor>
|
||||
<DraftEditor
|
||||
handleChange={handleChange}
|
||||
height={height}
|
||||
width={width}
|
||||
placeholder={placeholder}
|
||||
defaultValue={defaultValue}
|
||||
></DraftEditor>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -104,25 +104,27 @@ export const StarRating = function StarRating({
|
|||
<span className="label form-check-label form-check-label col-auto" style={{ color: labelColor }}>
|
||||
{label}
|
||||
</span>
|
||||
{animatedStars.map((props, index) => (
|
||||
<Star
|
||||
tooltip={getTooltip(index)}
|
||||
active={getActive(index)}
|
||||
isHalfStar={isHalfStar(index)}
|
||||
maxRating={maxRating}
|
||||
onClick={(e, idx) => {
|
||||
e.stopPropagation();
|
||||
setRatingIndex(idx);
|
||||
handleClick(idx);
|
||||
}}
|
||||
allowHalfStar={allowHalfStar}
|
||||
key={index}
|
||||
index={index}
|
||||
color={color}
|
||||
style={{ ...props }}
|
||||
setHoverIndex={setHoverIndex}
|
||||
/>
|
||||
))}
|
||||
<div className="col px-1 py-0 mt-0">
|
||||
{animatedStars.map((props, index) => (
|
||||
<Star
|
||||
tooltip={getTooltip(index)}
|
||||
active={getActive(index)}
|
||||
isHalfStar={isHalfStar(index)}
|
||||
maxRating={maxRating}
|
||||
onClick={(e, idx) => {
|
||||
e.stopPropagation();
|
||||
setRatingIndex(idx);
|
||||
handleClick(idx);
|
||||
}}
|
||||
allowHalfStar={allowHalfStar}
|
||||
key={index}
|
||||
index={index}
|
||||
color={color}
|
||||
style={{ ...props }}
|
||||
setHoverIndex={setHoverIndex}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const Star = ({
|
|||
}
|
||||
|
||||
function roundValueToPrecision(value, precision) {
|
||||
if (value == null) {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -630,6 +630,7 @@ export function Table({
|
|||
marginTop: 8,
|
||||
marginLeft: 10,
|
||||
}}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
{...rest}
|
||||
/>
|
||||
</>
|
||||
|
|
@ -820,7 +821,7 @@ export function Table({
|
|||
<tr
|
||||
key={index}
|
||||
className={`table-row ${
|
||||
highlightSelectedRow && row.id == componentState.selectedRowId ? 'selected' : ''
|
||||
highlightSelectedRow && row.id === componentState.selectedRowId ? 'selected' : ''
|
||||
}`}
|
||||
{...row.getRowProps()}
|
||||
onClick={(e) => {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ class Switch extends React.Component {
|
|||
return (
|
||||
<label className="form-switch form-check-inline">
|
||||
<input
|
||||
style={{ backgroundColor: on ? `${color}` : 'white' }}
|
||||
style={{ backgroundColor: on ? `${color}` : 'white', marginTop: '0px' }}
|
||||
disabled={disabledState}
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
|
|
@ -61,7 +61,7 @@ export const ToggleSwitch = ({
|
|||
|
||||
return (
|
||||
<div
|
||||
className="row"
|
||||
className="row py-1"
|
||||
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
|
@ -71,7 +71,7 @@ export const ToggleSwitch = ({
|
|||
<span className="form-check-label form-check-label col-auto my-auto" style={{ color: textColor }}>
|
||||
{label}
|
||||
</span>
|
||||
<div className="col px-1 py-0 my-auto">
|
||||
<div className="col px-1 py-0 mt-0">
|
||||
<Switch
|
||||
disabledState={parsedDisabledState}
|
||||
on={on}
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ export const componentTypes = [
|
|||
showOnMobile: { value: false },
|
||||
},
|
||||
properties: {
|
||||
value: { value: '' },
|
||||
value: { value: '99' },
|
||||
placeholder: { value: '0' },
|
||||
},
|
||||
events: [],
|
||||
|
|
@ -385,7 +385,7 @@ export const componentTypes = [
|
|||
customRule: { value: null },
|
||||
},
|
||||
properties: {
|
||||
defaultValue: { value: '' },
|
||||
defaultValue: { value: '01/04/2021' },
|
||||
format: { value: 'DD/MM/YYYY' },
|
||||
enableTime: { value: '{{false}}' },
|
||||
enableDate: { value: '{{true}}' },
|
||||
|
|
@ -419,6 +419,7 @@ export const componentTypes = [
|
|||
},
|
||||
styles: {
|
||||
textColor: { type: 'color', displayName: 'Text Color' },
|
||||
checkboxColor: { type: 'color', displayName: 'Checkbox Color' },
|
||||
visibility: { type: 'code', displayName: 'Visibility' },
|
||||
disabledState: { type: 'code', displayName: 'Disable' },
|
||||
},
|
||||
|
|
@ -434,6 +435,7 @@ export const componentTypes = [
|
|||
events: [],
|
||||
styles: {
|
||||
textColor: { value: '#000' },
|
||||
checkboxColor: { value: '#3c92dc' },
|
||||
visibility: { value: '{{true}}' },
|
||||
disabledState: { value: '{{false}}' },
|
||||
},
|
||||
|
|
@ -561,7 +563,10 @@ export const componentTypes = [
|
|||
showOnMobile: { value: false },
|
||||
},
|
||||
properties: {
|
||||
value: { value: '' },
|
||||
value: {
|
||||
value:
|
||||
'ToolJet is an open-source low-code platform for building and deploying internal tools with minimal engineering efforts 🚀',
|
||||
},
|
||||
placeholder: { value: 'Placeholder text' },
|
||||
},
|
||||
events: [],
|
||||
|
|
@ -643,7 +648,7 @@ export const componentTypes = [
|
|||
properties: {
|
||||
text: { value: 'Text goes here !' },
|
||||
visible: { value: true },
|
||||
loadingState: { value: false },
|
||||
loadingState: { value: `{{false}}` },
|
||||
},
|
||||
events: [],
|
||||
styles: {
|
||||
|
|
@ -847,6 +852,7 @@ export const componentTypes = [
|
|||
},
|
||||
properties: {
|
||||
placeholder: { type: 'code', displayName: 'Placeholder' },
|
||||
defaultValue: { type: 'code', displayName: 'Default Value' },
|
||||
},
|
||||
events: {},
|
||||
styles: {
|
||||
|
|
@ -854,7 +860,7 @@ export const componentTypes = [
|
|||
disabledState: { type: 'code', displayName: 'Disable' },
|
||||
},
|
||||
exposedVariables: {
|
||||
value: {},
|
||||
value: '',
|
||||
},
|
||||
definition: {
|
||||
others: {
|
||||
|
|
@ -863,6 +869,7 @@ export const componentTypes = [
|
|||
},
|
||||
properties: {
|
||||
placeholder: { value: 'Placeholder text' },
|
||||
defaultValue: { value: '' },
|
||||
},
|
||||
events: [],
|
||||
styles: {
|
||||
|
|
@ -1031,4 +1038,39 @@ export const componentTypes = [
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Divider',
|
||||
displayName: 'Divider',
|
||||
description: 'Separator between components',
|
||||
component: 'Divider',
|
||||
defaultSize: {
|
||||
width: 200,
|
||||
height: 10,
|
||||
},
|
||||
others: {
|
||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
||||
},
|
||||
properties: {},
|
||||
events: {},
|
||||
styles: {
|
||||
dividerColor: { type: 'color', displayName: 'Divider Color' },
|
||||
visibility: { type: 'code', displayName: 'Visibility' },
|
||||
},
|
||||
exposedVariables: {
|
||||
value: {},
|
||||
},
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: true },
|
||||
showOnMobile: { value: false },
|
||||
},
|
||||
properties: {},
|
||||
events: [],
|
||||
styles: {
|
||||
dividerColor: { value: '#E7E8EA' },
|
||||
visibility: { value: '{{true}}' },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -116,7 +116,6 @@ export const Container = ({
|
|||
deltaY = delta.y;
|
||||
}
|
||||
|
||||
componentData = item.component;
|
||||
left = Math.round(currentLayoutOptions.left + deltaX);
|
||||
top = Math.round(currentLayoutOptions.top + deltaY);
|
||||
|
||||
|
|
@ -311,7 +310,12 @@ export const Container = ({
|
|||
{Object.keys(boxes).length === 0 && !appLoading && !isDragging && (
|
||||
<div className="mx-auto w-50 p-5 bg-light no-components-box" style={{ marginTop: '10%' }}>
|
||||
<center className="text-muted">
|
||||
You haven't added any components yet. Drag components from the right sidebar and drop here.
|
||||
You haven't added any components yet. Drag components from the right sidebar and drop here. Check out
|
||||
our{' '}
|
||||
<a href="https://docs.tooljet.io/docs/tutorial/adding-widget" target="_blank" rel="noreferrer">
|
||||
guide
|
||||
</a>{' '}
|
||||
on adding widgets.
|
||||
</center>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ function getItemStyles(delta, item, initialOffset, currentOffset, currentLayout)
|
|||
display: 'none',
|
||||
};
|
||||
}
|
||||
let { x, y } = currentOffset;
|
||||
let x, y;
|
||||
|
||||
let id = item.id;
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class DataSourceManager extends React.Component {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.selectedDataSource != this.props.selectedDataSource) {
|
||||
if (prevProps.selectedDataSource !== this.props.selectedDataSource) {
|
||||
this.setState({
|
||||
selectedDataSource: this.props.selectedDataSource,
|
||||
options: this.props.selectedDataSource?.options,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export const TestConnection = ({ kind, options, onConnectionTestFailed }) => {
|
|||
useEffect(() => {
|
||||
if (isTesting) {
|
||||
setButtonText('Testing connection...');
|
||||
} else if (!isTesting && connectionStatus === 'success') {
|
||||
} else if (connectionStatus === 'success') {
|
||||
setButtonText('Connection verified');
|
||||
} else {
|
||||
setButtonText('Test Connection');
|
||||
|
|
@ -54,7 +54,7 @@ export const TestConnection = ({ kind, options, onConnectionTestFailed }) => {
|
|||
<Button
|
||||
className="m-2"
|
||||
variant="success"
|
||||
disabled={isTesting || (!isTesting && !(connectionStatus !== 'success'))}
|
||||
disabled={isTesting || connectionStatus === 'success'}
|
||||
onClick={testDataSource}
|
||||
>
|
||||
{buttonText}
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ class Editor extends React.Component {
|
|||
};
|
||||
|
||||
switchSidebarTab = (tabIndex) => {
|
||||
if (tabIndex == 2) {
|
||||
if (tabIndex === 2) {
|
||||
this.setState({ selectedComponent: null });
|
||||
}
|
||||
this.setState({
|
||||
|
|
@ -575,7 +575,7 @@ class Editor extends React.Component {
|
|||
value={this.state.app.name}
|
||||
/>
|
||||
)}
|
||||
<small>{this.state.editingVersion && `Editing version: ${this.state.editingVersion.name}`}</small>
|
||||
<small>{this.state.editingVersion && `App version: ${this.state.editingVersion.name}`}</small>
|
||||
<div className="editor-buttons">
|
||||
<span
|
||||
className={`btn btn-light mx-2`}
|
||||
|
|
@ -864,6 +864,7 @@ class Editor extends React.Component {
|
|||
componentTypes={componentTypes}
|
||||
zoomLevel={zoomLevel}
|
||||
currentLayout={currentLayout}
|
||||
darkMode={this.props.darkMode}
|
||||
></WidgetManager>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,29 @@ export const EventManager = ({
|
|||
return { name: action.name, value: action.id };
|
||||
});
|
||||
|
||||
let alertTypes = [
|
||||
{
|
||||
name: 'Info',
|
||||
id: 'info',
|
||||
},
|
||||
{
|
||||
name: 'Success',
|
||||
id: 'success',
|
||||
},
|
||||
{
|
||||
name: 'Warning',
|
||||
id: 'warning',
|
||||
},
|
||||
{
|
||||
name: 'Danger',
|
||||
id: 'error',
|
||||
},
|
||||
];
|
||||
|
||||
let alertOptions = alertTypes.map((alert) => {
|
||||
return { name: alert.name, value: alert.id };
|
||||
});
|
||||
|
||||
excludeEvents = excludeEvents || [];
|
||||
|
||||
/* Filter events based on excludesEvents ( a list of event ids to exclude ) */
|
||||
|
|
@ -53,8 +76,8 @@ export const EventManager = ({
|
|||
function getAllApps() {
|
||||
let appsOptionsList = [];
|
||||
apps
|
||||
.filter((item) => item.slug != undefined)
|
||||
.map((item) => {
|
||||
.filter((item) => item.slug !== undefined)
|
||||
.forEach((item) => {
|
||||
appsOptionsList.push({
|
||||
name: item.name,
|
||||
value: item.slug,
|
||||
|
|
@ -128,16 +151,31 @@ export const EventManager = ({
|
|||
<div className="hr-text">Action options</div>
|
||||
<div>
|
||||
{event.actionId === 'show-alert' && (
|
||||
<div className="row">
|
||||
<div className="col-3 p-2">Message</div>
|
||||
<div className="col-9">
|
||||
<CodeHinter
|
||||
currentState={currentState}
|
||||
initialValue={event.message}
|
||||
onChange={(value) => handlerChanged(index, 'message', value)}
|
||||
/>
|
||||
<>
|
||||
<div className="row">
|
||||
<div className="col-3 p-2">Message</div>
|
||||
<div className="col-9">
|
||||
<CodeHinter
|
||||
currentState={currentState}
|
||||
initialValue={event.message}
|
||||
onChange={(value) => handlerChanged(index, 'message', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row mt-3">
|
||||
<div className="col-3 p-2">Alert Type</div>
|
||||
<div className="col-9">
|
||||
<SelectSearch
|
||||
options={alertOptions}
|
||||
value={event.alertType}
|
||||
search={false}
|
||||
onChange={(value) => handlerChanged(index, 'alertType', value)}
|
||||
filterOptions={fuzzySearch}
|
||||
placeholder="Select.."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{event.actionId === 'open-webpage' && (
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const LeftSidebarDebugger = ({ darkMode, errors }) => {
|
|||
])(errors);
|
||||
|
||||
const errorData = [];
|
||||
Object.entries(newError).map(([key, value]) => {
|
||||
Object.entries(newError).forEach(([key, value]) => {
|
||||
const variableNames = {
|
||||
options: '',
|
||||
response: '',
|
||||
|
|
@ -46,6 +46,10 @@ export const LeftSidebarDebugger = ({ darkMode, errors }) => {
|
|||
variableNames.options = 'substitutedVariables';
|
||||
variableNames.response = 'response';
|
||||
break;
|
||||
|
||||
case 'transformations':
|
||||
variableNames.response = 'data';
|
||||
break;
|
||||
default:
|
||||
'options';
|
||||
}
|
||||
|
|
@ -168,27 +172,28 @@ function ErrorLogsComponent({ errorProps, idx, darkMode }) {
|
|||
height="16"
|
||||
/>
|
||||
[{_.capitalize(errorProps.type)} {errorProps.key}]
|
||||
<span className="text-red">
|
||||
{`Query Failed: ${errorProps.description}`} {errorProps.message}.
|
||||
</span>
|
||||
<span className="text-red">{`${_.startCase(errorProps.type)} Failed: ${errorProps.message}`} .</span>
|
||||
<br />
|
||||
<small className="text-muted px-1">{moment(errorProps.timestamp).fromNow()}</small>
|
||||
</p>
|
||||
|
||||
<div className={` queryData ${open ? 'open' : 'close'} py-0`}>
|
||||
<span>
|
||||
<ReactJson
|
||||
src={errorProps.options.data}
|
||||
theme={darkMode ? 'shapeshifter' : 'rjv-default'}
|
||||
name={errorProps.options.name}
|
||||
style={{ fontSize: '0.7rem', paddingLeft: '0.17rem' }}
|
||||
enableClipboard={false}
|
||||
displayDataTypes={false}
|
||||
collapsed={true}
|
||||
displayObjectSize={false}
|
||||
quotesOnKeys={false}
|
||||
sortKeys={false}
|
||||
/>
|
||||
</span>
|
||||
{errorProps.type === 'query' && (
|
||||
<span>
|
||||
<ReactJson
|
||||
src={errorProps.options.data}
|
||||
theme={darkMode ? 'shapeshifter' : 'rjv-default'}
|
||||
name={errorProps.options.name}
|
||||
style={{ fontSize: '0.7rem', paddingLeft: '0.17rem' }}
|
||||
enableClipboard={false}
|
||||
displayDataTypes={false}
|
||||
collapsed={true}
|
||||
displayObjectSize={false}
|
||||
quotesOnKeys={false}
|
||||
sortKeys={false}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
<ReactJson
|
||||
src={errorProps.response.data}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export const SidebarPinnedButton = ({ state, component, updateState, darkMode })
|
|||
return (
|
||||
<SidebarPinnedButton.OverlayContainer tip={tooltipMsg}>
|
||||
<div
|
||||
className={`btn btn-sm m-1 ${darkMode ? 'btn-outline-secondary' : 'btn-default'} ${state && 'active'} ${
|
||||
className={`btn btn-sm btn-light m-1 ${darkMode && 'btn-outline-secondary'} ${state && 'active'} ${
|
||||
component === 'Inspector' && 'position-absolute end-0'
|
||||
}`}
|
||||
onClick={updateState}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import '@/_styles/left-sidebar.scss';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { LeftSidebarItem } from './sidebar-item';
|
||||
import { LeftSidebarInspector } from './sidebar-inspector';
|
||||
|
|
@ -9,6 +9,7 @@ import { LeftSidebarZoom } from './sidebar-zoom';
|
|||
import { DarkModeToggle } from '../../_components/DarkModeToggle';
|
||||
import useRouter from '../../_hooks/use-router';
|
||||
import { LeftSidebarDebugger } from './SidebarDebugger';
|
||||
import { ConfirmDialog } from '@/_components';
|
||||
|
||||
export const LeftSidebar = ({
|
||||
appId,
|
||||
|
|
@ -23,6 +24,7 @@ export const LeftSidebar = ({
|
|||
errorLogs,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const [showLeaveDialog, setShowLeaveDialog] = useState(false);
|
||||
return (
|
||||
<div className="left-sidebar">
|
||||
<LeftSidebarInspector darkMode={darkMode} globals={globals} components={components} queries={queries} />
|
||||
|
|
@ -34,11 +36,17 @@ export const LeftSidebar = ({
|
|||
/>
|
||||
<LeftSidebarDebugger darkMode={darkMode} components={components} errors={errorLogs} />
|
||||
<LeftSidebarItem
|
||||
onClick={() => router.push('/')}
|
||||
onClick={() => setShowLeaveDialog(true)}
|
||||
tip="Back to home"
|
||||
icon="back"
|
||||
className="left-sidebar-item no-border"
|
||||
/>
|
||||
<ConfirmDialog
|
||||
show={showLeaveDialog}
|
||||
message={'The unsaved changes will be lost if you leave the editor, do you want to leave?'}
|
||||
onConfirm={() => router.push('/')}
|
||||
onCancel={() => setShowLeaveDialog(false)}
|
||||
/>
|
||||
<div className="left-sidebar-stack-bottom">
|
||||
<LeftSidebarZoom onZoomChanged={onZoomChanged} />
|
||||
<div className="left-sidebar-item no-border">
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ class ManageAppUsers extends React.Component {
|
|||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<a href="/users" target="_blank">
|
||||
<a href="/users" target="_blank" className="btn btn-outline-azure mt-3">
|
||||
Manage Organization Users
|
||||
</a>
|
||||
</Modal.Footer>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
export const BaseUrl = ({ dataSourceURL, theme }) => {
|
||||
return (
|
||||
<span
|
||||
htmlFor=""
|
||||
style={{
|
||||
padding: '7px',
|
||||
border: theme === 'default' ? '1px solid rgb(217 220 222)' : '1px solid #2c3a4c',
|
||||
background: theme === 'default' ? 'rgb(246 247 251)' : '#20211e',
|
||||
color: theme === 'default' ? '#9ca1a6' : '#9e9e9e',
|
||||
marginRight: '-3px',
|
||||
borderTopLeftRadius: '3px',
|
||||
borderBottomLeftRadius: '3px',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{dataSourceURL}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
@ -7,6 +7,7 @@ import Tabs from './Tabs';
|
|||
|
||||
import { changeOption } from '../utils';
|
||||
import { CodeHinter } from '../../../CodeBuilder/CodeHinter';
|
||||
import { BaseUrl } from './BaseUrl';
|
||||
|
||||
class Restapi extends React.Component {
|
||||
constructor(props) {
|
||||
|
|
@ -100,21 +101,7 @@ class Restapi extends React.Component {
|
|||
|
||||
<div className="col" style={{ display: 'flex' }}>
|
||||
{dataSourceURL && (
|
||||
<span
|
||||
htmlFor=""
|
||||
style={{
|
||||
padding: '7px',
|
||||
border: '1px solid rgb(217 220 222)',
|
||||
background: 'rgb(246 247 251)',
|
||||
color: '#9ca1a6',
|
||||
marginRight: '-3px',
|
||||
borderTopLeftRadius: '3px',
|
||||
borderBottomLeftRadius: '3px',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{dataSourceURL}
|
||||
</span>
|
||||
<BaseUrl theme={this.props.darkMode ? 'monokai' : 'default'} dataSourceURL={dataSourceURL} />
|
||||
)}
|
||||
<CodeHinter
|
||||
currentState={this.props.currentState}
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ class SaveAndPreview extends React.Component {
|
|||
</div>
|
||||
) : (
|
||||
<div className="table-responsive">
|
||||
{!versions?.length && !showVersionForm && !isLoading && <div>No versions yet.</div>}
|
||||
<table className="table table-vcenter">
|
||||
<tbody>
|
||||
{versions.map((version) => (
|
||||
|
|
@ -177,7 +178,7 @@ class SaveAndPreview extends React.Component {
|
|||
<button
|
||||
className="btn btn btn-sm mx-2 text-muted"
|
||||
onClick={() => this.props.setAppDefinitionFromVersion(version)}
|
||||
disabled={this.props.editingVersionId == version.id}
|
||||
disabled={this.props.editingVersionId === version.id}
|
||||
>
|
||||
edit
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -305,7 +305,13 @@ export const SubContainer = ({
|
|||
|
||||
{Object.keys(boxes).length === 0 && !appLoading && !isDragging && (
|
||||
<div className="mx-auto mt-5 w-50 p-5 bg-light no-components-box">
|
||||
<center className="text-muted">Drag components from the right sidebar and drop here.</center>
|
||||
<center className="text-muted">
|
||||
Drag components from the right sidebar and drop here. Check out our{' '}
|
||||
<a href="https://docs.tooljet.io/docs/tutorial/adding-widget" target="_blank" rel="noreferrer">
|
||||
guide
|
||||
</a>{' '}
|
||||
on adding widgets.
|
||||
</center>
|
||||
</div>
|
||||
)}
|
||||
{appLoading && (
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ function getItemStyles(delta, item, initialOffset, currentOffset, parentRef, par
|
|||
};
|
||||
}
|
||||
|
||||
let { x, y } = currentOffset;
|
||||
let x, y;
|
||||
|
||||
let id = item.id;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import { DraggableBox } from './DraggableBox';
|
|||
import Fuse from 'fuse.js';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
export const WidgetManager = function WidgetManager({ componentTypes, zoomLevel, currentLayout }) {
|
||||
export const WidgetManager = function WidgetManager({ componentTypes, zoomLevel, currentLayout, darkMode }) {
|
||||
const [filteredComponents, setFilteredComponents] = useState(componentTypes);
|
||||
|
||||
function filterComponents(value) {
|
||||
if (value != '') {
|
||||
if (value !== '') {
|
||||
const fuse = new Fuse(componentTypes, { keys: ['component'] });
|
||||
const results = fuse.search(value);
|
||||
setFilteredComponents(results.map((result) => result.item));
|
||||
|
|
@ -46,7 +46,7 @@ export const WidgetManager = function WidgetManager({ componentTypes, zoomLevel,
|
|||
<img src="./static/illustrations/undraw_printing_invoices_5r4r.svg" height="128" alt="" />
|
||||
</div> */}
|
||||
<p className="empty-title">No results found</p>
|
||||
<p className="empty-subtitle text-muted">
|
||||
<p className={`empty-subtitle ${darkMode ? 'text-white-50' : 'text-secondary'}`}>
|
||||
Try adjusting your search or filter to find what you're looking for.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -71,7 +71,7 @@ export const WidgetManager = function WidgetManager({ componentTypes, zoomLevel,
|
|||
];
|
||||
const integrationItems = ['Map'];
|
||||
|
||||
filteredComponents.map((f) => {
|
||||
filteredComponents.forEach((f) => {
|
||||
if (commonItems.includes(f.name)) commonSection.items.push(f);
|
||||
if (formItems.includes(f.name)) formSection.items.push(f);
|
||||
else if (integrationItems.includes(f.name)) integrationSection.items.push(f);
|
||||
|
|
|
|||
|
|
@ -17,13 +17,12 @@ class ForgotPassword extends React.Component {
|
|||
|
||||
handleChange = (event) => {
|
||||
this.setState({ [event.target.name]: event.target.value });
|
||||
if (event.target.value == '') {
|
||||
if (event.target.value === '') {
|
||||
this.setState({ isEmailFound: false, buttonClicked: false });
|
||||
}
|
||||
};
|
||||
|
||||
handleClick = (event) => {
|
||||
this.setState({ buttonClicked: true });
|
||||
event.preventDefault();
|
||||
|
||||
fetch(`${config.apiUrl}/forgot_password`, {
|
||||
|
|
@ -38,6 +37,7 @@ class ForgotPassword extends React.Component {
|
|||
this.setState({ isEmailFound: true });
|
||||
return res.json();
|
||||
} else {
|
||||
this.setState({ buttonClicked: true });
|
||||
this.setState({ isEmailFound: false });
|
||||
}
|
||||
})
|
||||
|
|
@ -58,7 +58,7 @@ class ForgotPassword extends React.Component {
|
|||
<div className="page page-center">
|
||||
<div className="container-tight py-2">
|
||||
<div className="text-center mb-4">
|
||||
<a href=".">
|
||||
<a href="." className="navbar-brand-autodark">
|
||||
<img src="/assets/images/logo-text.svg" height="30" alt="" />
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -86,7 +86,7 @@ class ForgotPassword extends React.Component {
|
|||
onClick={this.handleClick}
|
||||
disabled={!this.state.email}
|
||||
>
|
||||
Submit
|
||||
Reset Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import Fuse from 'fuse.js';
|
|||
import { folderService } from '@/_services';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
export const AppMenu = function AppMenu({ app, folders, foldersChanged, deleteApp, cloneApp }) {
|
||||
export const AppMenu = function AppMenu({ app, folders, foldersChanged, deleteApp, cloneApp, exportApp }) {
|
||||
const [addToFolder, setAddToFolder] = useState(false);
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ export const AppMenu = function AppMenu({ app, folders, foldersChanged, deleteAp
|
|||
<div>
|
||||
<div className="field mb-2">
|
||||
<span role="button" onClick={() => setAddToFolder(true)}>
|
||||
Add to folder{' '}
|
||||
Add to folder
|
||||
</span>
|
||||
</div>
|
||||
<div className="field mb-2">
|
||||
|
|
@ -78,9 +78,14 @@ export const AppMenu = function AppMenu({ app, folders, foldersChanged, deleteAp
|
|||
Clone app
|
||||
</span>
|
||||
</div>
|
||||
<div className="field mb-2">
|
||||
<span className="field mb-2" role="button" onClick={() => exportApp()}>
|
||||
Export app
|
||||
</span>
|
||||
</div>
|
||||
<div className="field mb-2">
|
||||
<span className="my-3 text-danger" role="button" onClick={() => deleteApp()}>
|
||||
Delete app{' '}
|
||||
Delete app
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -105,6 +110,7 @@ export const AppMenu = function AppMenu({ app, folders, foldersChanged, deleteAp
|
|||
onChange={(newVal) => {
|
||||
addAppToFolder(app.id, newVal);
|
||||
}}
|
||||
emptyMessage={folders.length === 0 ? 'No folders present' : 'Not found'}
|
||||
filterOptions={customFuzzySearch}
|
||||
placeholder="Select folder"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ export const Folders = function Folders({
|
|||
const [activeFolder, setActiveFolder] = useState(currentFolder || {});
|
||||
|
||||
function saveFolder() {
|
||||
if (!newFolderName || !newFolderName.trim()) {
|
||||
toast.warn("folder name can't be empty.", {
|
||||
hideProgressBar: true,
|
||||
position: 'top-left',
|
||||
});
|
||||
return;
|
||||
}
|
||||
setCreationStatus(true);
|
||||
folderService.create(newFolderName).then(() => {
|
||||
toast.info('folder created.', {
|
||||
|
|
@ -90,7 +97,7 @@ export const Folders = function Folders({
|
|||
))}
|
||||
<hr />
|
||||
{!showForm && (
|
||||
<a className="mx-3" onClick={() => setShowForm(true)}>
|
||||
<a className="mx-3 fw-500" onClick={() => setShowForm(true)}>
|
||||
+ Folder
|
||||
</a>
|
||||
)}
|
||||
|
|
@ -98,8 +105,6 @@ export const Folders = function Folders({
|
|||
<div className="p-2 row">
|
||||
<div className="col">
|
||||
<input
|
||||
// eslint-disable-next-line no-undef
|
||||
onClick={() => onComponentClick(id, component)} //onComponentClick, id and compoenent is not defined
|
||||
type="text"
|
||||
onChange={(e) => setNewFolderName(e.target.value)}
|
||||
className="form-control"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class HomePage extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.fileInput = React.createRef();
|
||||
this.state = {
|
||||
currentUser: authenticationService.currentUserValue,
|
||||
users: null,
|
||||
|
|
@ -20,7 +21,10 @@ class HomePage extends React.Component {
|
|||
creatingApp: false,
|
||||
isDeletingApp: false,
|
||||
isCloningApp: false,
|
||||
isExportingApp: false,
|
||||
isImportingApp: false,
|
||||
currentFolder: {},
|
||||
currentPage: 1,
|
||||
showAppDeletionConfirmation: false,
|
||||
apps: [],
|
||||
folders: [],
|
||||
|
|
@ -81,10 +85,15 @@ class HomePage extends React.Component {
|
|||
createApp = () => {
|
||||
let _self = this;
|
||||
_self.setState({ creatingApp: true });
|
||||
appService.createApp().then((data) => {
|
||||
console.log(data);
|
||||
_self.props.history.push(`/apps/${data.id}`);
|
||||
});
|
||||
appService
|
||||
.createApp()
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
_self.props.history.push(`/apps/${data.id}`);
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error(error, { hideProgressBar: true, position: 'top-center' });
|
||||
});
|
||||
};
|
||||
|
||||
deleteApp = (app) => {
|
||||
|
|
@ -104,12 +113,95 @@ class HomePage extends React.Component {
|
|||
this.props.history.push(`/apps/${data.id}`);
|
||||
})
|
||||
.catch(({ _error }) => {
|
||||
toast.error('Could not clone the app.', { hideProgressBar: true, position: 'top-center' });
|
||||
toast.error('Could not clone the app.', {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
this.setState({ isCloningApp: false });
|
||||
console.log(_error);
|
||||
});
|
||||
};
|
||||
|
||||
exportApp = (app) => {
|
||||
this.setState({ isExportingApp: true });
|
||||
appService
|
||||
.exportApp(app.id)
|
||||
.then((data) => {
|
||||
const appName = app.name.replace(/\s+/g, '-').toLowerCase();
|
||||
const fileName = `${appName}-export-${new Date().getTime()}`;
|
||||
// simulate link click download
|
||||
const json = JSON.stringify(data);
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const href = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = href;
|
||||
link.download = fileName + '.json';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
this.fileInput.value = '';
|
||||
this.setState({ isExportingApp: false });
|
||||
})
|
||||
.catch(({ _error }) => {
|
||||
toast.error('Could not export the app.', {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
this.fileInput.value = '';
|
||||
this.setState({ isExportingApp: false });
|
||||
console.log(_error);
|
||||
});
|
||||
};
|
||||
|
||||
handleImportApp = (event) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.readAsText(event.target.files[0], 'UTF-8');
|
||||
fileReader.onload = (event) => {
|
||||
const fileContent = event.target.result;
|
||||
this.setState({ isImportingApp: true });
|
||||
try {
|
||||
const requestBody = JSON.parse(fileContent);
|
||||
appService
|
||||
.importApp(requestBody)
|
||||
.then(() => {
|
||||
toast.info('App imported successfully.', {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
this.setState({
|
||||
isImportingApp: false,
|
||||
});
|
||||
this.fetchApps(this.state.currentPage, this.state.currentFolder.id);
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error(`Could not import the app: ${error}`, {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
this.setState({
|
||||
isImportingApp: false,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error(`Could not import the app: ${error}`, {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
this.setState({
|
||||
isImportingApp: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
isAppEditable = (app) => {
|
||||
return app.app_group_permissions.some((p) => p.update);
|
||||
};
|
||||
|
||||
isAppDeletable = (app) => {
|
||||
return app.app_group_permissions.some((p) => p.delete);
|
||||
};
|
||||
|
||||
executeAppDeletion = () => {
|
||||
this.setState({ isDeletingApp: true });
|
||||
appService
|
||||
|
|
@ -129,7 +221,10 @@ class HomePage extends React.Component {
|
|||
this.fetchFolders();
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error('Could not delete the app.', { hideProgressBar: true, position: 'top-center' });
|
||||
toast.error('Could not delete the app.', {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
this.setState({
|
||||
isDeletingApp: false,
|
||||
appToBeDeleted: null,
|
||||
|
|
@ -146,13 +241,13 @@ class HomePage extends React.Component {
|
|||
render() {
|
||||
const {
|
||||
apps,
|
||||
currentUser,
|
||||
isLoading,
|
||||
creatingApp,
|
||||
meta,
|
||||
currentFolder,
|
||||
showAppDeletionConfirmation,
|
||||
isDeletingApp,
|
||||
isImportingApp,
|
||||
} = this.state;
|
||||
return (
|
||||
<div className="wrapper home-page">
|
||||
|
|
@ -192,18 +287,34 @@ class HomePage extends React.Component {
|
|||
</h2>
|
||||
</div>
|
||||
<div className="col-auto ms-auto d-print-none">
|
||||
<button
|
||||
className={`btn btn-primary d-none d-lg-inline ${creatingApp ? 'btn-loading' : ''}`}
|
||||
onClick={this.createApp}
|
||||
>
|
||||
Create new application
|
||||
</button>
|
||||
<div className="w-100 ">
|
||||
<button
|
||||
className={`btn btn-default d-none d-lg-inline mb-3 ${isImportingApp ? 'btn-loading' : ''}`}
|
||||
onChange={this.handleImportApp}
|
||||
>
|
||||
<label>
|
||||
Import
|
||||
<input type="file" ref={this.fileInput} style={{ display: 'none' }} />
|
||||
</label>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-auto ms-auto d-print-none">
|
||||
<div className="w-100 ">
|
||||
<button
|
||||
className={`btn btn-primary d-none d-lg-inline mb-3 ${creatingApp ? 'btn-loading' : ''}`}
|
||||
onClick={this.createApp}
|
||||
>
|
||||
Create new application
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
currentFolder.count == 0
|
||||
currentFolder.count === 0
|
||||
? 'table-responsive w-100 apps-table mt-3 d-flex align-items-center'
|
||||
: 'table-responsive w-100 apps-table mt-3'
|
||||
}
|
||||
|
|
@ -241,16 +352,21 @@ class HomePage extends React.Component {
|
|||
<td className="col p-3">
|
||||
<span className="app-title mb-3">{app.name}</span> <br />
|
||||
<small className="pt-2 app-description">
|
||||
created {moment(app.created_at).fromNow()} ago by {app.user?.first_name}{' '}
|
||||
created {moment(app.created_at).fromNow(true)} ago by {app.user?.first_name}{' '}
|
||||
{app.user?.last_name}{' '}
|
||||
</small>
|
||||
</td>
|
||||
<td className="text-muted col-auto pt-4">
|
||||
{currentUser.role !== 'viewer' && (
|
||||
{!isLoading && this.isAppEditable(app) && (
|
||||
<Link to={`/apps/${app.id}`} className="d-none d-lg-inline">
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
overlay={(props) => renderTooltip({ props, text: 'Open in app builder' })}
|
||||
overlay={(props) =>
|
||||
renderTooltip({
|
||||
props,
|
||||
text: 'Open in app builder',
|
||||
})
|
||||
}
|
||||
>
|
||||
<span className="badge bg-green-lt">Edit</span>
|
||||
</OverlayTrigger>
|
||||
|
|
@ -267,7 +383,7 @@ class HomePage extends React.Component {
|
|||
renderTooltip({
|
||||
props,
|
||||
text:
|
||||
app?.current_version_id == null
|
||||
app?.current_version_id === null
|
||||
? 'App does not have a deployed version'
|
||||
: 'Open in app viewer',
|
||||
})
|
||||
|
|
@ -293,7 +409,7 @@ class HomePage extends React.Component {
|
|||
renderTooltip({
|
||||
props,
|
||||
text:
|
||||
app?.current_version_id == null
|
||||
app?.current_version_id === null
|
||||
? 'App does not have a deployed version'
|
||||
: 'Open in app viewer',
|
||||
})
|
||||
|
|
@ -302,14 +418,14 @@ class HomePage extends React.Component {
|
|||
{
|
||||
<span
|
||||
className={`${
|
||||
app?.current_version_id == null
|
||||
app?.current_version_id === null
|
||||
? 'badge mx-2 '
|
||||
: 'badge bg-azure-lt mx-2'
|
||||
}`}
|
||||
style={{
|
||||
filter:
|
||||
app?.current_version_id == null
|
||||
? 'brightness(0.8)'
|
||||
app?.current_version_id === null
|
||||
? 'brightness(0.3)'
|
||||
: 'brightness(1) invert(1)',
|
||||
}}
|
||||
>
|
||||
|
|
@ -320,19 +436,22 @@ class HomePage extends React.Component {
|
|||
)}
|
||||
</Link>
|
||||
|
||||
<AppMenu
|
||||
app={app}
|
||||
folders={this.state.folders}
|
||||
foldersChanged={this.foldersChanged}
|
||||
deleteApp={() => this.deleteApp(app)}
|
||||
cloneApp={() => this.cloneApp(app)}
|
||||
/>
|
||||
{this.isAppDeletable(app) && (
|
||||
<AppMenu
|
||||
app={app}
|
||||
folders={this.state.folders}
|
||||
foldersChanged={this.foldersChanged}
|
||||
deleteApp={() => this.deleteApp(app)}
|
||||
cloneApp={() => this.cloneApp(app)}
|
||||
exportApp={() => this.exportApp(app)}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{currentFolder.count == 0 && (
|
||||
{currentFolder.count === 0 && (
|
||||
<div>
|
||||
<img
|
||||
className="mx-auto d-block"
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ class LoginPage extends React.Component {
|
|||
name="email"
|
||||
type="email"
|
||||
className="form-control"
|
||||
placeholder="Enter email"
|
||||
placeholder="Email"
|
||||
data-testid="emailField"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,522 @@
|
|||
import React from 'react';
|
||||
import SelectSearch, { fuzzySearch } from 'react-select-search';
|
||||
import { groupPermissionService } from '../_services/groupPermission.service';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { Header } from '@/_components';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
class ManageGroupPermissionResources extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isLoadingGroup: true,
|
||||
isLoadingApps: true,
|
||||
isAddingApps: false,
|
||||
isLoadingUsers: true,
|
||||
isAddingUsers: false,
|
||||
groupPermission: null,
|
||||
usersInGroup: [],
|
||||
appsInGroup: [],
|
||||
usersNotInGroup: [],
|
||||
appsNotInGroup: [],
|
||||
selectedAppIds: [],
|
||||
selectedUserIds: [],
|
||||
removeAppIds: [],
|
||||
currentTab: 'apps',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const groupPermissionId = this.props.match.params.id;
|
||||
|
||||
this.fetchGroupAndResources(groupPermissionId);
|
||||
}
|
||||
|
||||
humanizeIfDefaultGroupName = (groupName) => {
|
||||
switch (groupName) {
|
||||
case 'all_users':
|
||||
return 'All Users';
|
||||
case 'admin':
|
||||
return 'Admin';
|
||||
default:
|
||||
return groupName;
|
||||
}
|
||||
};
|
||||
|
||||
fetchGroupAndResources = (groupPermissionId) => {
|
||||
groupPermissionService.getGroup(groupPermissionId).then((data) => {
|
||||
this.setState({
|
||||
groupPermission: data,
|
||||
isLoadingGroup: false,
|
||||
});
|
||||
|
||||
this.fetchUsersNotInGroup(groupPermissionId);
|
||||
this.fetchUsersInGroup(groupPermissionId);
|
||||
|
||||
this.fetchAppsNotInGroup(groupPermissionId);
|
||||
this.fetchAppsInGroup(groupPermissionId);
|
||||
});
|
||||
};
|
||||
|
||||
fetchUsersNotInGroup = (groupPermissionId) => {
|
||||
groupPermissionService.getUsersNotInGroup(groupPermissionId).then((data) => {
|
||||
this.setState({
|
||||
usersNotInGroup: data.users,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
fetchUsersInGroup = (groupPermissionId) => {
|
||||
groupPermissionService.getUsersInGroup(groupPermissionId).then((data) => {
|
||||
this.setState({
|
||||
usersInGroup: data.users,
|
||||
isLoadingUsers: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
fetchAppsNotInGroup = (groupPermissionId) => {
|
||||
groupPermissionService.getAppsNotInGroup(groupPermissionId).then((data) => {
|
||||
this.setState({
|
||||
appsNotInGroup: data.apps,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
fetchAppsInGroup = (groupPermissionId) => {
|
||||
groupPermissionService.getAppsInGroup(groupPermissionId).then((data) => {
|
||||
this.setState({
|
||||
appsInGroup: data.apps,
|
||||
isLoadingApps: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
updateAppGroupPermission = (app, groupPermissionId, action) => {
|
||||
const appGroupPermission = app.app_group_permissions.find(
|
||||
(permission) => permission.group_permission_id === groupPermissionId
|
||||
);
|
||||
|
||||
let actionParams = { read: true, update: action === 'edit' };
|
||||
|
||||
groupPermissionService
|
||||
.updateAppGroupPermission(groupPermissionId, appGroupPermission.id, actionParams)
|
||||
.then(() => {
|
||||
toast.success('App permissions updated', {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
|
||||
this.fetchAppsInGroup(groupPermissionId);
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error(error, { hideProgressBar: true, position: 'top-center' });
|
||||
});
|
||||
};
|
||||
|
||||
canAppGroupPermission = (app, groupPermissionId, action) => {
|
||||
let appGroupPermission;
|
||||
switch (action) {
|
||||
case 'edit':
|
||||
appGroupPermission = this.findAppGroupPermission(app, groupPermissionId);
|
||||
return appGroupPermission['read'] && appGroupPermission['update'];
|
||||
case 'view':
|
||||
appGroupPermission = this.findAppGroupPermission(app, groupPermissionId);
|
||||
|
||||
return appGroupPermission['read'] && !appGroupPermission['update'];
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
findAppGroupPermission = (app, groupPermissionId) => {
|
||||
const appGroupPermission = app.app_group_permissions.find(
|
||||
(permission) => permission.group_permission_id === groupPermissionId
|
||||
);
|
||||
|
||||
return appGroupPermission;
|
||||
};
|
||||
|
||||
setSelectedUsers = (value) => {
|
||||
this.setState({
|
||||
selectedUserIds: value,
|
||||
});
|
||||
};
|
||||
|
||||
setSelectedApps = (value) => {
|
||||
this.setState({
|
||||
selectedAppIds: value,
|
||||
});
|
||||
};
|
||||
|
||||
addSelectedAppsToGroup = (groupPermissionId, selectedAppIds) => {
|
||||
this.setState({ isAddingApps: true });
|
||||
const updateParams = {
|
||||
selectedAppIds,
|
||||
};
|
||||
groupPermissionService
|
||||
.update(groupPermissionId, updateParams)
|
||||
.then(() => {
|
||||
this.setState({
|
||||
selectedAppIds: [],
|
||||
isLoadingApps: true,
|
||||
isAddingApps: false,
|
||||
});
|
||||
this.fetchAppsNotInGroup(groupPermissionId);
|
||||
this.fetchAppsInGroup(groupPermissionId);
|
||||
})
|
||||
.then(() => {
|
||||
toast.success('Apps added to the group', {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error(error, { hideProgressBar: true, position: 'top-center' });
|
||||
});
|
||||
};
|
||||
|
||||
removeAppFromGroup = (groupPermissionId, appId) => {
|
||||
const updateParams = {
|
||||
removeAppIds: [appId],
|
||||
};
|
||||
groupPermissionService
|
||||
.update(groupPermissionId, updateParams)
|
||||
.then(() => {
|
||||
this.setState({ removeAppIds: [], isLoadingApps: true });
|
||||
this.fetchAppsNotInGroup(groupPermissionId);
|
||||
this.fetchAppsInGroup(groupPermissionId);
|
||||
})
|
||||
.then(() => {
|
||||
toast.success('Apps removed from the group', {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error(error, { hideProgressBar: true, position: 'top-center' });
|
||||
});
|
||||
};
|
||||
|
||||
addSelectedUsersToGroup = (groupPermissionId, selectedUserIds) => {
|
||||
this.setState({ isAddingUsers: true });
|
||||
const updateParams = {
|
||||
selectedUserIds,
|
||||
};
|
||||
groupPermissionService
|
||||
.update(groupPermissionId, updateParams)
|
||||
.then(() => {
|
||||
this.setState({
|
||||
selectedUserIds: [],
|
||||
isLoadingUsers: true,
|
||||
isAddingUsers: false,
|
||||
});
|
||||
this.fetchUsersNotInGroup(groupPermissionId);
|
||||
this.fetchUsersInGroup(groupPermissionId);
|
||||
})
|
||||
.then(() => {
|
||||
toast.success('Users added to the group', {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error(error, { hideProgressBar: true, position: 'top-center' });
|
||||
});
|
||||
};
|
||||
|
||||
removeUserFromGroup = (groupPermissionId, userId) => {
|
||||
const updateParams = {
|
||||
removeUserIds: [userId],
|
||||
};
|
||||
groupPermissionService
|
||||
.update(groupPermissionId, updateParams)
|
||||
.then(() => {
|
||||
this.setState({ removeUserIds: [], isLoadingUsers: true });
|
||||
this.fetchUsersNotInGroup(groupPermissionId);
|
||||
this.fetchUsersInGroup(groupPermissionId);
|
||||
})
|
||||
.then(() => {
|
||||
toast.success('Users removed from the group', {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error(error, { hideProgressBar: true, position: 'top-center' });
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
isLoadingGroup,
|
||||
isLoadingApps,
|
||||
isAddingApps,
|
||||
isLoadingUsers,
|
||||
isAddingUsers,
|
||||
appsInGroup,
|
||||
appsNotInGroup,
|
||||
usersInGroup,
|
||||
usersNotInGroup,
|
||||
groupPermission,
|
||||
currentTab,
|
||||
selectedAppIds,
|
||||
selectedUserIds,
|
||||
} = this.state;
|
||||
|
||||
const appSelectOptions = appsNotInGroup.map((app) => {
|
||||
return { name: app.name, value: app.id };
|
||||
});
|
||||
const userSelectOptions = usersNotInGroup.map((user) => {
|
||||
return { name: `${user.first_name} ${user.last_name}`, value: user.id };
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="wrapper org-users-page">
|
||||
<Header switchDarkMode={this.props.switchDarkMode} darkMode={this.props.darkMode} />
|
||||
|
||||
<div className="page-wrapper">
|
||||
<div className="container-xl">
|
||||
<div className="page-header d-print-none">
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<div className="page-pretitle"></div>
|
||||
{isLoadingGroup ? (
|
||||
<ol className="breadcrumb" aria-label="breadcrumbs">
|
||||
<li className="breadcrumb-item">
|
||||
<a href="#">User groups</a>
|
||||
</li>
|
||||
</ol>
|
||||
) : (
|
||||
<ol className="breadcrumb" aria-label="breadcrumbs">
|
||||
<li className="breadcrumb-item">
|
||||
<Link to="/groups">User groups</Link>
|
||||
</li>
|
||||
<li className="breadcrumb-item">
|
||||
<a href="#">{this.humanizeIfDefaultGroupName(groupPermission.group)}</a>
|
||||
</li>
|
||||
</ol>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-body">
|
||||
<div className="container-xl">
|
||||
<div className="card">
|
||||
<ul className="nav nav-tabs">
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className={`nav-link ${currentTab === 'apps' ? 'active' : ''}`}
|
||||
onClick={() => this.setState({ currentTab: 'apps' })}
|
||||
>
|
||||
Apps
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className={`nav-link ${currentTab === 'users' ? 'active' : ''}`}
|
||||
onClick={() => this.setState({ currentTab: 'users' })}
|
||||
>
|
||||
Users
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="card-body">
|
||||
<div className="tab-content">
|
||||
<div className={`tab-pane ${currentTab === 'apps' ? 'active show' : ''}`}>
|
||||
<div className="row">
|
||||
<div className="col-5">
|
||||
<SelectSearch
|
||||
options={appSelectOptions}
|
||||
closeOnSelect={false}
|
||||
multiple
|
||||
search={true}
|
||||
value={selectedAppIds}
|
||||
filterOptions={fuzzySearch}
|
||||
onChange={(value) => this.setSelectedApps(value)}
|
||||
printOptions="on-focus"
|
||||
placeholder="Select apps to add to the group"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<div
|
||||
className={`btn btn-primary w-100 ${isAddingApps ? 'btn-loading' : ''} ${
|
||||
selectedAppIds.length === 0 ? 'disabled' : ''
|
||||
}`}
|
||||
onClick={() => this.addSelectedAppsToGroup(groupPermission.id, selectedAppIds)}
|
||||
>
|
||||
Add
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<div className="table-responsive">
|
||||
<table className="table table-vcenter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Permissions</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoadingApps ? (
|
||||
<tr>
|
||||
<td className="col-auto">
|
||||
<div className="row">
|
||||
<div className="skeleton-line w-10 col mx-3"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="col-auto">
|
||||
<div className="skeleton-line w-10"></div>
|
||||
</td>
|
||||
<td className="col-auto">
|
||||
<div className="skeleton-line w-10"></div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
appsInGroup.map((app) => (
|
||||
<tr key={app.id}>
|
||||
<td>{app.name}</td>
|
||||
<td className="text-muted">
|
||||
<div>
|
||||
<label className="form-check form-check-inline">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="radio"
|
||||
onChange={() => {
|
||||
this.updateAppGroupPermission(app, groupPermission.id, 'view');
|
||||
}}
|
||||
disabled={groupPermission.group === 'admin'}
|
||||
checked={this.canAppGroupPermission(app, groupPermission.id, 'view')}
|
||||
/>
|
||||
<span className="form-check-label">View</span>
|
||||
</label>
|
||||
<label className="form-check form-check-inline">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="radio"
|
||||
onChange={() => {
|
||||
this.updateAppGroupPermission(app, groupPermission.id, 'edit');
|
||||
}}
|
||||
disabled={groupPermission.group === 'admin'}
|
||||
checked={this.canAppGroupPermission(app, groupPermission.id, 'edit')}
|
||||
/>
|
||||
<span className="form-check-label">Edit</span>
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{groupPermission.group !== 'admin' && (
|
||||
<Link
|
||||
to="#"
|
||||
onClick={() => {
|
||||
this.removeAppFromGroup(groupPermission.id, app.id);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Link>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`tab-pane ${currentTab === 'users' ? 'active show' : ''}`}>
|
||||
<div className="row">
|
||||
<div className="col-5">
|
||||
<SelectSearch
|
||||
options={userSelectOptions}
|
||||
closeOnSelect={false}
|
||||
multiple
|
||||
search={true}
|
||||
filterOptions={fuzzySearch}
|
||||
value={selectedUserIds}
|
||||
onChange={(value) => this.setSelectedUsers(value)}
|
||||
printOptions="on-focus"
|
||||
placeholder="Select users to add to the group"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<div
|
||||
className={`btn btn-primary w-100 ${isAddingUsers ? 'btn-loading' : ''} ${
|
||||
selectedUserIds.length === 0 ? 'disabled' : ''
|
||||
}`}
|
||||
onClick={() => this.addSelectedUsersToGroup(groupPermission.id, selectedUserIds)}
|
||||
>
|
||||
Add
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<div className="table-responsive">
|
||||
<table className="table table-vcenter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoadingUsers ? (
|
||||
<tr>
|
||||
<td className="col-auto">
|
||||
<div className="row">
|
||||
<div className="skeleton-line w-10 col mx-3"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="col-auto">
|
||||
<div className="skeleton-line w-10"></div>
|
||||
</td>
|
||||
<td className="col-auto">
|
||||
<div className="skeleton-line w-10"></div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
usersInGroup.map((user) => (
|
||||
<tr key={user.id}>
|
||||
<td>{`${user.first_name} ${user.last_name}`}</td>
|
||||
<td>{user.email}</td>
|
||||
<td className="text-muted">
|
||||
{groupPermission.group !== 'all_users' && (
|
||||
<Link
|
||||
to="#"
|
||||
onClick={() => {
|
||||
this.removeUserFromGroup(groupPermission.id, user.id);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Link>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { ManageGroupPermissionResources };
|
||||
1
frontend/src/ManageGroupPermissionResources/index.js
Normal file
1
frontend/src/ManageGroupPermissionResources/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './ManageGroupPermissionResources';
|
||||
235
frontend/src/ManageGroupPermissions/ManageGroupPermissions.jsx
Normal file
235
frontend/src/ManageGroupPermissions/ManageGroupPermissions.jsx
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
import React from 'react';
|
||||
import { authenticationService } from '@/_services';
|
||||
import { groupPermissionService } from '../_services/groupPermission.service';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { Header } from '@/_components';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
class ManageGroupPermissions extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
currentUser: authenticationService.currentUserValue,
|
||||
isLoading: true,
|
||||
groups: [],
|
||||
creatingGroup: false,
|
||||
showNewGroupForm: false,
|
||||
newGroupName: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchGroups();
|
||||
}
|
||||
|
||||
fetchGroups = () => {
|
||||
this.setState({
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
groupPermissionService.getGroups().then((data) => {
|
||||
this.setState({
|
||||
groups: data.group_permissions,
|
||||
isLoading: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
changeNewGroupName = (value) => {
|
||||
this.setState({
|
||||
newGroupName: value,
|
||||
});
|
||||
};
|
||||
|
||||
humanizeifDefaultGroupName = (groupName) => {
|
||||
switch (groupName) {
|
||||
case 'all_users':
|
||||
return 'All Users';
|
||||
case 'admin':
|
||||
return 'Admin';
|
||||
default:
|
||||
return groupName;
|
||||
}
|
||||
};
|
||||
|
||||
createGroup = () => {
|
||||
this.setState({ creatingGroup: true });
|
||||
groupPermissionService
|
||||
.create(this.state.newGroupName)
|
||||
.then(() => {
|
||||
this.setState({
|
||||
creatingGroup: false,
|
||||
showNewGroupForm: false,
|
||||
newGroup: null,
|
||||
});
|
||||
toast.success('Group has been created', {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
this.fetchGroups();
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error(error, { hideProgressBar: true, position: 'top-center' });
|
||||
this.setState({
|
||||
creatingGroup: false,
|
||||
showNewGroupForm: true,
|
||||
newGroup: {},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
deleteGroup = (groupPermissionId) => {
|
||||
groupPermissionService
|
||||
.del(groupPermissionId)
|
||||
.then(() => {
|
||||
toast.success('Group has been deleted', {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
});
|
||||
this.fetchGroups();
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error(error, { hideProgressBar: true, position: 'top-center' });
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isLoading, showNewGroupForm, creatingGroup, groups } = this.state;
|
||||
return (
|
||||
<div className="wrapper org-users-page">
|
||||
<Header switchDarkMode={this.props.switchDarkMode} darkMode={this.props.darkMode} />
|
||||
|
||||
<div className="page-wrapper">
|
||||
<div className="container-xl">
|
||||
<div className="page-header d-print-none">
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<div className="page-pretitle"></div>
|
||||
<h2 className="page-title">User Groups</h2>
|
||||
</div>
|
||||
<div className="col-auto ms-auto d-print-none">
|
||||
{!showNewGroupForm && (
|
||||
<div className="btn btn-primary" onClick={() => this.setState({ showNewGroupForm: true })}>
|
||||
Create new group
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-body">
|
||||
{showNewGroupForm && (
|
||||
<div className="container-xl">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">Add new group</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<form>
|
||||
<div className="form-group mb-3 ">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Enter Name"
|
||||
onChange={(e) => {
|
||||
this.changeNewGroupName(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-footer">
|
||||
<button
|
||||
className="btn btn-light mr-2"
|
||||
onClick={() =>
|
||||
this.setState({
|
||||
showNewGroupForm: false,
|
||||
newGroup: null,
|
||||
})
|
||||
}
|
||||
disabled={creatingGroup}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className={`btn mx-2 btn-primary ${creatingGroup ? 'btn-loading' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.createGroup();
|
||||
}}
|
||||
disabled={creatingGroup}
|
||||
>
|
||||
Create Group
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!showNewGroupForm && (
|
||||
<div className="container-xl">
|
||||
<div className="card">
|
||||
<div className="card-table table-responsive table-bordered">
|
||||
<table data-testid="usersTable" className="table table-vcenter" disabled={true}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th className="w-1"></th>
|
||||
<th className="w-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
{isLoading ? (
|
||||
<tbody className="w-100" style={{ minHeight: '300px' }}>
|
||||
{Array.from(Array(2)).map((index) => (
|
||||
<tr key={index}>
|
||||
<td className="col-auto">
|
||||
<div className="row">
|
||||
<div className="skeleton-line w-10 col mx-3"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="col-auto">
|
||||
<div className="skeleton-line w-10"></div>
|
||||
</td>
|
||||
<td className="col-auto">
|
||||
<div className="skeleton-line w-10"></div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
) : (
|
||||
<tbody>
|
||||
{groups.map((permissionGroup) => (
|
||||
<tr key={permissionGroup.id}>
|
||||
<td>
|
||||
<Link to={`/groups/${permissionGroup.id}`}>
|
||||
{this.humanizeifDefaultGroupName(permissionGroup.group)}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
{permissionGroup.group !== 'admin' && permissionGroup.group !== 'all_users' && (
|
||||
<Link onClick={() => this.deleteGroup(permissionGroup.id)}>Delete</Link>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
)}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { ManageGroupPermissions };
|
||||
1
frontend/src/ManageGroupPermissions/index.js
Normal file
1
frontend/src/ManageGroupPermissions/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './ManageGroupPermissions';
|
||||
|
|
@ -2,10 +2,10 @@ import React from 'react';
|
|||
import { authenticationService, organizationService, organizationUserService } from '@/_services';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { Header } from '@/_components';
|
||||
import SelectSearch, { fuzzySearch } from 'react-select-search';
|
||||
import { toast } from 'react-toastify';
|
||||
import { history } from '@/_helpers';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
|
||||
class ManageOrgUsers extends React.Component {
|
||||
constructor(props) {
|
||||
|
|
@ -17,8 +17,6 @@ class ManageOrgUsers extends React.Component {
|
|||
showNewUserForm: false,
|
||||
creatingUser: false,
|
||||
newUser: {},
|
||||
role: '',
|
||||
idChangingRole: null,
|
||||
archivingUser: null,
|
||||
fields: {},
|
||||
errors: {},
|
||||
|
|
@ -27,9 +25,10 @@ class ManageOrgUsers extends React.Component {
|
|||
|
||||
validateEmail(email) {
|
||||
console.log(email);
|
||||
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
const re =
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return re.test(String(email).toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
handleValidation() {
|
||||
let fields = this.state.fields;
|
||||
|
|
@ -38,14 +37,14 @@ class ManageOrgUsers extends React.Component {
|
|||
if (!fields['firstName']) {
|
||||
errors['firstName'] = 'This field is required';
|
||||
} else if (typeof fields['firstName'] !== 'undefined') {
|
||||
if (!fields['firstName'].match(/^[a-zA-Z]+$/)) {
|
||||
if (!/^[a-zA-Z]+$/.test(fields['firstName'])) {
|
||||
errors['firstName'] = 'Only letters are allowed';
|
||||
}
|
||||
}
|
||||
if (!fields['lastName']) {
|
||||
errors['lastName'] = 'This field is required';
|
||||
} else if (typeof fields['lastName'] !== 'undefined') {
|
||||
if (!fields['lastName'].match(/^[a-zA-Z]+$/)) {
|
||||
if (!/^[a-zA-Z]+$/.test(fields['lastName'])) {
|
||||
errors['lastName'] = 'Only letters are allowed';
|
||||
}
|
||||
}
|
||||
|
|
@ -53,14 +52,11 @@ class ManageOrgUsers extends React.Component {
|
|||
if (!fields['email']) {
|
||||
errors['email'] = 'This field is required';
|
||||
} else if (!this.validateEmail(fields['email'])) {
|
||||
errors['email'] = 'Email is not valid';
|
||||
errors['email'] = 'Email is not valid';
|
||||
}
|
||||
if (!fields['role']) {
|
||||
errors['role'] = 'This field is required';
|
||||
}
|
||||
|
||||
|
||||
this.setState({ errors: errors });
|
||||
return Object.keys(errors).length === 0;
|
||||
return Object.keys(errors).length === 0;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
|
@ -89,20 +85,6 @@ class ManageOrgUsers extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
changeNewUserRole = (id, role) => {
|
||||
this.setState({ idChangingRole: id });
|
||||
organizationUserService
|
||||
.changeRole(id, role)
|
||||
.then(() => {
|
||||
toast.success('User role has been updated', { hideProgressBar: true, position: 'top-center' });
|
||||
this.setState({ idChangingRole: null });
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error(error, { hideProgressBar: true, position: 'top-center' });
|
||||
this.setState({ idChangingRole: null });
|
||||
});
|
||||
};
|
||||
|
||||
archiveOrgUser = (id) => {
|
||||
this.setState({ archivingUser: id });
|
||||
|
||||
|
|
@ -124,7 +106,9 @@ class ManageOrgUsers extends React.Component {
|
|||
|
||||
if (this.handleValidation()) {
|
||||
let fields = {};
|
||||
Object.keys(fields).map(key => { fields[key] = '' })
|
||||
Object.keys(fields).forEach((key) => {
|
||||
fields[key] = '';
|
||||
});
|
||||
|
||||
this.setState({
|
||||
creatingUser: true,
|
||||
|
|
@ -151,12 +135,6 @@ class ManageOrgUsers extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
dropdownVal = (role) => {
|
||||
this.setState({
|
||||
fields: { ...this.state.fields, role },
|
||||
});
|
||||
};
|
||||
|
||||
logout = () => {
|
||||
authenticationService.logout();
|
||||
history.push('/login');
|
||||
|
|
@ -169,19 +147,11 @@ class ManageOrgUsers extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
isLoading,
|
||||
role,
|
||||
showNewUserForm,
|
||||
creatingUser,
|
||||
users,
|
||||
errors,
|
||||
idChangingRole,
|
||||
archivingUser,
|
||||
} = this.state;
|
||||
const { isLoading, showNewUserForm, creatingUser, users, archivingUser } = this.state;
|
||||
return (
|
||||
<div className="wrapper org-users-page">
|
||||
<Header switchDarkMode={this.props.switchDarkMode} darkMode={this.props.darkMode} />
|
||||
<ReactTooltip type="dark" effect="solid" delayShow={250} />
|
||||
|
||||
<div className="page-wrapper">
|
||||
<div className="container-xl">
|
||||
|
|
@ -210,7 +180,7 @@ class ManageOrgUsers extends React.Component {
|
|||
<h3 className="card-title">Add new user</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<form>
|
||||
<form onSubmit={this.createUser} noValidate>
|
||||
<div className="form-group mb-3 ">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
|
|
@ -241,7 +211,7 @@ class ManageOrgUsers extends React.Component {
|
|||
<label className="form-label">Email address</label>
|
||||
<div>
|
||||
<input
|
||||
type="email"
|
||||
type="text"
|
||||
className="form-control"
|
||||
aria-describedby="emailHelp"
|
||||
placeholder="Enter email"
|
||||
|
|
@ -252,33 +222,18 @@ class ManageOrgUsers extends React.Component {
|
|||
<span className="text-danger">{this.state.errors['email']}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group mb-3 ">
|
||||
<label className="form-label">Role</label>
|
||||
<div>
|
||||
<SelectSearch
|
||||
options={['Admin', 'Developer', 'Viewer'].map((role) => {
|
||||
return { name: role, value: role.toLowerCase() };
|
||||
})}
|
||||
search={true}
|
||||
value={role}
|
||||
name="role"
|
||||
onChange={this.dropdownVal}
|
||||
filterOptions={fuzzySearch}
|
||||
placeholder="Select.."
|
||||
/>
|
||||
<span className="text-danger">{errors.role}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-light mr-2"
|
||||
onClick={() => this.setState({ showNewUserForm: false, newUser: {} })}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className={`btn mx-2 btn-primary ${creatingUser ? 'btn-loading' : ''}`}
|
||||
onClick={(e) => this.createUser(e)}
|
||||
disabled={creatingUser}
|
||||
>
|
||||
Create User
|
||||
</button>
|
||||
|
|
@ -298,9 +253,6 @@ class ManageOrgUsers extends React.Component {
|
|||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>
|
||||
<center>Role</center>
|
||||
</th>
|
||||
<th>Status</th>
|
||||
<th className="w-1"></th>
|
||||
</tr>
|
||||
|
|
@ -351,27 +303,9 @@ class ManageOrgUsers extends React.Component {
|
|||
{user.email}
|
||||
</a>
|
||||
</td>
|
||||
<td className="text-muted" style={{ width: '280px' }}>
|
||||
<center className="mx-5 select-search-role">
|
||||
<SelectSearch
|
||||
options={['Admin', 'Developer', 'Viewer'].map((role) => {
|
||||
return { name: role, value: role.toLowerCase() };
|
||||
})}
|
||||
value={user.role}
|
||||
search={false}
|
||||
disabled={idChangingRole === user.id}
|
||||
onChange={(value) => {
|
||||
this.changeNewUserRole(user.id, value);
|
||||
}}
|
||||
filterOptions={fuzzySearch}
|
||||
placeholder="Select.."
|
||||
/>
|
||||
{idChangingRole === user.id && <small>Updating role...</small>}
|
||||
</center>
|
||||
</td>
|
||||
<td className="text-muted">
|
||||
<span
|
||||
className={`badge bg-${user.status === 'invited' ? 'warning' : 'success'} me-1 m-1`}
|
||||
className={`badge bg-${user.status === 'invited' ? 'warning' : user.status === 'archived' ? 'danger' : 'success'} me-1 m-1`}
|
||||
></span>
|
||||
<small className="user-status">{user.status}</small>
|
||||
{user.status === 'invited' && 'invitation_token' in user ? (
|
||||
|
|
@ -380,10 +314,14 @@ class ManageOrgUsers extends React.Component {
|
|||
onCopy={this.invitationLinkCopyHandler}
|
||||
>
|
||||
<img
|
||||
data-tip="Copy invitation link"
|
||||
className="svg-icon"
|
||||
src="/assets/images/icons/copy.svg"
|
||||
width="15"
|
||||
height="15"
|
||||
style={{
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
></img>
|
||||
</CopyToClipboard>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class ResetPassword extends React.Component {
|
|||
<div className="page page-center">
|
||||
<div className="container-tight py-2">
|
||||
<div className="text-center mb-4">
|
||||
<a href=".">
|
||||
<a href="." className="navbar-brand-autodark">
|
||||
<img src="/assets/images/logo-text.svg" height="30" alt="" />
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ function SettingsPage(props) {
|
|||
|
||||
const changePassword = async () => {
|
||||
setPasswordChangeInProgress(true);
|
||||
const response = userService.changePassword(currentpassword, newPassword);
|
||||
const response = await userService.changePassword(currentpassword, newPassword);
|
||||
response
|
||||
.then(() => {
|
||||
toast.success('Password updated successfully', { hideProgressBar: true, autoClose: 3000 });
|
||||
|
|
@ -40,7 +40,7 @@ function SettingsPage(props) {
|
|||
|
||||
const newPasswordKeyPressHandler = async (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
changePassword();
|
||||
await changePassword();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ function SettingsPage(props) {
|
|||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<div className="page-pretitle"></div>
|
||||
<h2 className="page-title">Settings</h2>
|
||||
<h2 className="page-title">Profile Settings</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class SignupPage extends React.Component {
|
|||
<div className="page page-center">
|
||||
<div className="container-tight py-2">
|
||||
<div className="text-center mb-4">
|
||||
<a href=".">
|
||||
<a href="." className="navbar-brand-autodark">
|
||||
<img src="/assets/images/logo-text.svg" height="30" alt="" />
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ const DynamicForm = ({ schema, optionchanged, createDataSource, options, isSavin
|
|||
case 'toggle':
|
||||
return {
|
||||
defaultChecked: options[$key],
|
||||
onChange: () => optionchanged($key, !options[$key]),
|
||||
onChange: () => optionchanged($key, !options[$key].value),
|
||||
};
|
||||
case 'dropdown':
|
||||
case 'dropdown-component-flip':
|
||||
|
|
|
|||
|
|
@ -1,18 +1,10 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import cx from 'classnames';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { authenticationService } from '@/_services';
|
||||
import { history } from '@/_helpers';
|
||||
import { DarkModeToggle } from './DarkModeToggle';
|
||||
|
||||
export const Header = function Header({ switchDarkMode, darkMode }) {
|
||||
const [pathName, setPathName] = useState(document.location.pathname);
|
||||
|
||||
useEffect(() => {
|
||||
setPathName(document.location.pathname);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [document.location.pathname]);
|
||||
|
||||
function logout() {
|
||||
authenticationService.logout();
|
||||
history.push('/login');
|
||||
|
|
@ -22,7 +14,7 @@ export const Header = function Header({ switchDarkMode, darkMode }) {
|
|||
history.push('/settings');
|
||||
}
|
||||
|
||||
const { first_name, last_name } = authenticationService.currentUserValue;
|
||||
const { first_name, last_name, admin } = authenticationService.currentUserValue;
|
||||
|
||||
return (
|
||||
<header className="navbar navbar-expand-md navbar-light d-print-none">
|
||||
|
|
@ -36,25 +28,6 @@ export const Header = function Header({ switchDarkMode, darkMode }) {
|
|||
</Link>
|
||||
</h1>
|
||||
|
||||
<ul className="navbar-nav d-none d-lg-flex">
|
||||
<li className={cx(`nav-item mx-3`, { active: pathName === '/' })}>
|
||||
<Link to={'/'} className="nav-link">
|
||||
<span className="nav-link-icon d-md-none d-lg-inline-block">
|
||||
<img className="svg-icon" src="/assets/images/icons/apps.svg" width="15" height="15" />
|
||||
</span>
|
||||
<span className="nav-link-title">Apps</span>
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
<li className={cx(`nav-item`, { active: pathName === '/users' })}>
|
||||
<Link to={'/users'} className="nav-link">
|
||||
<span className="nav-link-icon d-md-none d-lg-inline-block">
|
||||
<img className="svg-icon" src="/assets/images/icons/users.svg" width="15" height="15" />
|
||||
</span>
|
||||
<span className="nav-link-title">Users</span>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="navbar-nav flex-row order-md-last">
|
||||
<div className="p-1 m-1 d-flex align-items-center">
|
||||
<DarkModeToggle switchDarkMode={switchDarkMode} darkMode={darkMode} />
|
||||
|
|
@ -75,12 +48,22 @@ export const Header = function Header({ switchDarkMode, darkMode }) {
|
|||
</div>
|
||||
</a>
|
||||
<div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
||||
<a data-testId="settingsBtn" onClick={openSettings} className="dropdown-item">
|
||||
Settings
|
||||
</a>
|
||||
<a data-testId="logoutBtn" onClick={logout} className="dropdown-item">
|
||||
{admin && (
|
||||
<Link data-testid="settingsBtn" to="/users" className="dropdown-item">
|
||||
Manage Users
|
||||
</Link>
|
||||
)}
|
||||
{admin && (
|
||||
<Link data-tesid="settingsBtn" to="/groups" className="dropdown-item">
|
||||
Manage Groups
|
||||
</Link>
|
||||
)}
|
||||
<Link data-testid="settingsBtn" to="#" onClick={openSettings} className="dropdown-item">
|
||||
Profile
|
||||
</Link>
|
||||
<Link data-testid="logoutBtn" to="#" onClick={logout} className="dropdown-item">
|
||||
Logout
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export function runTransformation(_ref, rawData, transformation, query) {
|
|||
result = evalFunction(data, moment, _, currentState.components, currentState.queries, currentState.globals);
|
||||
} catch (err) {
|
||||
console.log('Transformation failed for query: ', query.name, err);
|
||||
toast.error(err.message, { hideProgressBar: true });
|
||||
result = { message: err.stack.split('\n')[0], status: 'failed', data: data };
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -67,7 +67,7 @@ export async function executeActionsForEventId(_ref, eventId, component, mode) {
|
|||
const filteredEvents = events.filter((event) => event.eventId === eventId);
|
||||
|
||||
for (const event of filteredEvents) {
|
||||
await executeAction(_ref, event, mode);
|
||||
await executeAction(_ref, event, mode); // skipcq: JS-0032
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,6 +98,11 @@ async function copyToClipboard(text) {
|
|||
}
|
||||
|
||||
function showModal(_ref, modalId, show) {
|
||||
if (_.isEmpty(modalId)) {
|
||||
console.log('No modal is associated with this event.');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const modalMeta = _ref.state.appDefinition.components[modalId];
|
||||
|
||||
const newState = {
|
||||
|
|
@ -115,9 +120,7 @@ function showModal(_ref, modalId, show) {
|
|||
|
||||
_ref.setState(newState);
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
resolve();
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function executeAction(_ref, event, mode) {
|
||||
|
|
@ -125,10 +128,8 @@ function executeAction(_ref, event, mode) {
|
|||
switch (event.actionId) {
|
||||
case 'show-alert': {
|
||||
const message = resolveReferences(event.message, _ref.state.currentState);
|
||||
toast(message, { hideProgressBar: true });
|
||||
return new Promise(function (resolve, reject) {
|
||||
resolve();
|
||||
});
|
||||
toast(message, { hideProgressBar: true, type: event.alertType });
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
case 'run-query': {
|
||||
|
|
@ -139,9 +140,7 @@ function executeAction(_ref, event, mode) {
|
|||
case 'open-webpage': {
|
||||
const url = resolveReferences(event.url, _ref.state.currentState);
|
||||
window.open(url, '_blank');
|
||||
return new Promise(function (resolve, reject) {
|
||||
resolve();
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
case 'go-to-app': {
|
||||
|
|
@ -174,9 +173,7 @@ function executeAction(_ref, event, mode) {
|
|||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
return new Promise(function (resolve, reject) {
|
||||
resolve();
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
case 'show-modal':
|
||||
|
|
@ -189,9 +186,7 @@ function executeAction(_ref, event, mode) {
|
|||
const contentToCopy = resolveReferences(event.contentToCopy, _ref.state.currentState);
|
||||
copyToClipboard(contentToCopy);
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
resolve();
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -451,6 +446,34 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined) {
|
|||
|
||||
if (dataQuery.options.enableTransformation) {
|
||||
finalData = runTransformation(_self, rawData, dataQuery.options.transformation, dataQuery);
|
||||
if (finalData.status === 'failed') {
|
||||
return _self.setState(
|
||||
{
|
||||
currentState: {
|
||||
..._self.state.currentState,
|
||||
queries: {
|
||||
..._self.state.currentState.queries,
|
||||
[queryName]: {
|
||||
..._self.state.currentState.queries[queryName],
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
..._self.state.currentState.errors,
|
||||
[queryName]: {
|
||||
type: 'transformations',
|
||||
data: finalData,
|
||||
options: options,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {
|
||||
resolve();
|
||||
onEvent(_self, 'onDataQueryFailure', { definition: { events: dataQuery.options.events } });
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (dataQuery.options.showSuccessNotification) {
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ export const serializeNestedObjectToQueryParams = function (obj, prefix) {
|
|||
var str = [],
|
||||
p;
|
||||
for (p in obj) {
|
||||
if (obj.hasOwnProperty(p)) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, p)) {
|
||||
var k = prefix ? prefix + '[' + p + ']' : p,
|
||||
v = obj[p];
|
||||
str.push(
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const useEscapeHandler = (handler = noop, dependencies = []) => {
|
|||
document === null || document === void 0 ? void 0 : document.removeEventListener('keyup', escapeHandler);
|
||||
}, dependencies);
|
||||
};
|
||||
const useClickOutside = (handler = noop, dependencies) => {
|
||||
const useClickOutside = (dependencies, handler = noop) => {
|
||||
const callbackRef = useRef(handler);
|
||||
const ref = useRef(null);
|
||||
const outsideClickHandler = (e) => {
|
||||
|
|
@ -43,7 +43,7 @@ const usePopover = (defaultOpen = false) => {
|
|||
const toggle = useCallback(() => setOpen(!open), []);
|
||||
const close = useCallback(() => setOpen(false), []);
|
||||
useEscapeHandler(close, []);
|
||||
const contentRef = useClickOutside(open ? close : undefined, []);
|
||||
const contentRef = useClickOutside([], open ? close : undefined);
|
||||
const trigger = {
|
||||
ref: triggerRef,
|
||||
onClick: toggle,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ export const appService = {
|
|||
getAll,
|
||||
createApp,
|
||||
cloneApp,
|
||||
exportApp,
|
||||
importApp,
|
||||
deleteApp,
|
||||
getApp,
|
||||
getAppBySlug,
|
||||
|
|
@ -40,6 +42,16 @@ function cloneApp(id) {
|
|||
return fetch(`${config.apiUrl}/apps/${id}/clone`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function exportApp(id) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader() };
|
||||
return fetch(`${config.apiUrl}/apps/${id}/export`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function importApp(body) {
|
||||
const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
|
||||
return fetch(`${config.apiUrl}/apps/import`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getApp(id) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader() };
|
||||
return fetch(`${config.apiUrl}/apps/${id}`, requestOptions).then(handleResponse);
|
||||
|
|
|
|||
120
frontend/src/_services/groupPermission.service.js
Normal file
120
frontend/src/_services/groupPermission.service.js
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import config from 'config';
|
||||
import { authHeader, handleResponse } from '@/_helpers';
|
||||
|
||||
export const groupPermissionService = {
|
||||
create,
|
||||
update,
|
||||
del,
|
||||
getGroup,
|
||||
getGroups,
|
||||
getAppsInGroup,
|
||||
getAppsNotInGroup,
|
||||
getUsersInGroup,
|
||||
getUsersNotInGroup,
|
||||
updateAppGroupPermission,
|
||||
};
|
||||
|
||||
function create(group) {
|
||||
const body = {
|
||||
group,
|
||||
};
|
||||
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/group_permissions`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function update(groupPermissionId, params) {
|
||||
const body = {
|
||||
add_apps: params.selectedAppIds,
|
||||
remove_apps: params.removeAppIds,
|
||||
add_users: params.selectedUserIds,
|
||||
remove_users: params.removeUserIds,
|
||||
};
|
||||
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: authHeader(),
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/group_permissions/${groupPermissionId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function del(groupPermissionId) {
|
||||
const requestOptions = {
|
||||
method: 'DELETE',
|
||||
headers: authHeader(),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/group_permissions/${groupPermissionId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getGroup(groupPermissionId) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: authHeader(),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/group_permissions/${groupPermissionId}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getGroups() {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: authHeader(),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/group_permissions`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getAppsInGroup(groupPermissionId) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: authHeader(),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/group_permissions/${groupPermissionId}/apps`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getAppsNotInGroup(groupPermissionId) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: authHeader(),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/group_permissions/${groupPermissionId}/addable_apps`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
}
|
||||
|
||||
function getUsersInGroup(groupPermissionId) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: authHeader(),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/group_permissions/${groupPermissionId}/users`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getUsersNotInGroup(groupPermissionId) {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: authHeader(),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/group_permissions/${groupPermissionId}/addable_users`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
}
|
||||
|
||||
function updateAppGroupPermission(groupPermissionId, appGroupPermissionId, actions) {
|
||||
const body = {
|
||||
actions,
|
||||
};
|
||||
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: authHeader(),
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
return fetch(
|
||||
`${config.apiUrl}/group_permissions/${groupPermissionId}/app_group_permissions/${appGroupPermissionId}`,
|
||||
requestOptions
|
||||
).then(handleResponse);
|
||||
}
|
||||
|
|
@ -7,18 +7,18 @@ export const organizationUserService = {
|
|||
changeRole,
|
||||
};
|
||||
|
||||
function create(first_name, last_name, email, role) {
|
||||
function create(first_name, last_name, email) {
|
||||
const body = {
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
role,
|
||||
};
|
||||
|
||||
const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
|
||||
return fetch(`${config.apiUrl}/organization_users`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
function changeRole(id, role) {
|
||||
const body = {
|
||||
role,
|
||||
|
|
|
|||
|
|
@ -388,7 +388,6 @@ body {
|
|||
z-index: 3;
|
||||
width: 59.3%;
|
||||
margin-top: 0px;
|
||||
padding: 0.5px;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
|
|
@ -893,7 +892,6 @@ body {
|
|||
|
||||
.jet-data-table::-webkit-scrollbar {
|
||||
background: transparent;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.jet-data-table:hover {
|
||||
|
|
@ -952,7 +950,7 @@ tr:focus {
|
|||
}
|
||||
|
||||
.jet-container {
|
||||
border-radius: 8px;
|
||||
|
||||
}
|
||||
|
||||
.select-search__option {
|
||||
|
|
@ -1797,6 +1795,9 @@ input:focus-visible {
|
|||
.card {
|
||||
background-color: #324156 !important;
|
||||
}
|
||||
.card .table tbody td a{
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.DateInput {
|
||||
background: #1f2936;
|
||||
|
|
@ -2013,8 +2014,13 @@ input:focus-visible {
|
|||
.editor .editor-sidebar .inspector .header {
|
||||
border: solid rgba(255, 255, 255, 0.09) !important;
|
||||
border-width: 0px 0px 1px 0px !important;
|
||||
.input-icon .input-icon-addon img {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
.editor .editor-sidebar .inspector .hr-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.skeleton-line::after {
|
||||
// background-image: linear-gradient(to right, #232e3c 0, #4c5b79 40%, #4c5b79 80%);
|
||||
background-image: linear-gradient(to right, #566177 0, #5a6170 40%, #4c5b79 80%);
|
||||
|
|
@ -2238,3 +2244,7 @@ input[type='text'] {
|
|||
height: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.fw-500 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,4 +7,7 @@
|
|||
.label {
|
||||
flex: 1;
|
||||
}
|
||||
.star {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
export default ({ defaultChecked, onChange }) => {
|
||||
return (
|
||||
<label className="form-switch mt-3">
|
||||
<label className="form-switch">
|
||||
<input className="form-check-input" type="checkbox" defaultChecked={defaultChecked} onChange={onChange} />
|
||||
</label>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ appService
|
|||
.then((config) => {
|
||||
window.public_config = config;
|
||||
|
||||
if (window.public_config.APM_VENDOR == 'sentry') {
|
||||
if (window.public_config.APM_VENDOR === 'sentry') {
|
||||
const history = createBrowserHistory();
|
||||
const tooljetServerUrl = window.public_config.TOOLJET_SERVER_URL;
|
||||
const tracingOrigins = ['localhost', /^\//];
|
||||
|
|
|
|||
427
package-lock.json
generated
427
package-lock.json
generated
|
|
@ -18,6 +18,7 @@
|
|||
"babel-loader": "^8.0.5",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"husky": "^7.0.2",
|
||||
"lint-staged": "^11.2.3",
|
||||
"path": "^0.12.7",
|
||||
"webpack": "^5.55.1",
|
||||
"webpack-cli": "^4.8.0"
|
||||
|
|
@ -1312,7 +1313,6 @@
|
|||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"clean-stack": "^2.0.0",
|
||||
"indent-string": "^4.0.0"
|
||||
|
|
@ -1868,7 +1868,6 @@
|
|||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
|
|
@ -1954,7 +1953,6 @@
|
|||
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
|
||||
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"slice-ansi": "^3.0.0",
|
||||
"string-width": "^4.2.0"
|
||||
|
|
@ -2013,9 +2011,9 @@
|
|||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/colorette": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
|
||||
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
|
||||
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="
|
||||
},
|
||||
"node_modules/colors": {
|
||||
"version": "1.4.0",
|
||||
|
|
@ -2265,24 +2263,6 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/cypress/node_modules/debug": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cypress/node_modules/fs-extra": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||
|
|
@ -2349,9 +2329,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
||||
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
|
|
@ -3207,6 +3187,12 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-own-enumerable-property-symbols": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
|
||||
"integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
|
|
@ -3568,7 +3554,6 @@
|
|||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
|
|
@ -3717,6 +3702,15 @@
|
|||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-obj": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
|
||||
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-path-inside": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
|
||||
|
|
@ -3739,6 +3733,15 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-regexp": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
|
||||
"integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
|
|
@ -3966,15 +3969,126 @@
|
|||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
|
||||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="
|
||||
},
|
||||
"node_modules/listr2": {
|
||||
"version": "3.11.1",
|
||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.11.1.tgz",
|
||||
"integrity": "sha512-ZXQvQfmH9iWLlb4n3hh31yicXDxlzB0pE7MM1zu6kgbVL4ivEsO4H8IPh4E682sC8RjnYO9anose+zT52rrpyg==",
|
||||
"node_modules/lint-staged": {
|
||||
"version": "11.2.3",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-11.2.3.tgz",
|
||||
"integrity": "sha512-Tfmhk8O2XFMD25EswHPv+OYhUjsijy5D7liTdxeXvhG2rsadmOLFtyj8lmlfoFFXY8oXWAIOKpoI+lJe1DB1mw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cli-truncate": "2.1.0",
|
||||
"colorette": "^1.4.0",
|
||||
"commander": "^8.2.0",
|
||||
"cosmiconfig": "^7.0.1",
|
||||
"debug": "^4.3.2",
|
||||
"enquirer": "^2.3.6",
|
||||
"execa": "^5.1.1",
|
||||
"listr2": "^3.12.2",
|
||||
"micromatch": "^4.0.4",
|
||||
"normalize-path": "^3.0.0",
|
||||
"please-upgrade-node": "^3.2.0",
|
||||
"string-argv": "0.3.1",
|
||||
"stringify-object": "3.3.0",
|
||||
"supports-color": "8.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"lint-staged": "bin/lint-staged.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/lint-staged"
|
||||
}
|
||||
},
|
||||
"node_modules/lint-staged/node_modules/commander": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.2.0.tgz",
|
||||
"integrity": "sha512-LLKxDvHeL91/8MIyTAD5BFMNtoIwztGPMiM/7Bl8rIPmHCZXRxmSWr91h57dpOpnQ6jIUqEWdXE/uBYMfiVZDA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/lint-staged/node_modules/cosmiconfig": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
|
||||
"integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/parse-json": "^4.0.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"parse-json": "^5.0.0",
|
||||
"path-type": "^4.0.0",
|
||||
"yaml": "^1.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/lint-staged/node_modules/execa": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.3",
|
||||
"get-stream": "^6.0.0",
|
||||
"human-signals": "^2.1.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"merge-stream": "^2.0.0",
|
||||
"npm-run-path": "^4.0.1",
|
||||
"onetime": "^5.1.2",
|
||||
"signal-exit": "^3.0.3",
|
||||
"strip-final-newline": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/lint-staged/node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lint-staged/node_modules/human-signals": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lint-staged/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/listr2": {
|
||||
"version": "3.12.2",
|
||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.12.2.tgz",
|
||||
"integrity": "sha512-64xC2CJ/As/xgVI3wbhlPWVPx0wfTqbUAkpb7bjDi0thSWMqrf07UFhrfsGoo8YSXmF049Rp9C0cjLC8rZxK9A==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cli-truncate": "^2.1.0",
|
||||
"colorette": "^1.2.2",
|
||||
"colorette": "^1.4.0",
|
||||
"log-update": "^4.0.0",
|
||||
"p-map": "^4.0.0",
|
||||
"rxjs": "^6.6.7",
|
||||
|
|
@ -4074,7 +4188,6 @@
|
|||
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
|
||||
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-escapes": "^4.3.0",
|
||||
"cli-cursor": "^3.1.0",
|
||||
|
|
@ -4093,7 +4206,6 @@
|
|||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
|
||||
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"astral-regex": "^2.0.0",
|
||||
|
|
@ -4111,7 +4223,6 @@
|
|||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
|
|
@ -4187,6 +4298,19 @@
|
|||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
|
||||
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.1",
|
||||
"picomatch": "^2.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.49.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz",
|
||||
|
|
@ -4454,7 +4578,6 @@
|
|||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
|
||||
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"aggregate-error": "^3.0.0"
|
||||
},
|
||||
|
|
@ -4635,6 +4758,15 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/please-upgrade-node": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
|
||||
"integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"semver-compare": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pluralize": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
||||
|
|
@ -4988,6 +5120,12 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/semver-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||
"integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
|
||||
|
|
@ -5053,7 +5191,6 @@
|
|||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
|
||||
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"astral-regex": "^2.0.0",
|
||||
|
|
@ -5129,6 +5266,15 @@
|
|||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-argv": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz",
|
||||
"integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.6.19"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
||||
|
|
@ -5142,6 +5288,20 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/stringify-object": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
|
||||
"integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"get-own-enumerable-property-symbols": "^3.0.0",
|
||||
"is-obj": "^1.0.1",
|
||||
"is-regexp": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
||||
|
|
@ -5936,7 +6096,6 @@
|
|||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
|
|
@ -7089,7 +7248,6 @@
|
|||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"clean-stack": "^2.0.0",
|
||||
"indent-string": "^4.0.0"
|
||||
|
|
@ -7486,8 +7644,7 @@
|
|||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
|
|
@ -7546,7 +7703,6 @@
|
|||
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
|
||||
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"slice-ansi": "^3.0.0",
|
||||
"string-width": "^4.2.0"
|
||||
|
|
@ -7587,9 +7743,9 @@
|
|||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"colorette": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
|
||||
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
|
||||
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="
|
||||
},
|
||||
"colors": {
|
||||
"version": "1.4.0",
|
||||
|
|
@ -7794,16 +7950,6 @@
|
|||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"fs-extra": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||
|
|
@ -7857,9 +8003,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
||||
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
|
|
@ -8516,6 +8662,12 @@
|
|||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||
"dev": true
|
||||
},
|
||||
"get-own-enumerable-property-symbols": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
|
||||
"integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
|
||||
"dev": true
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
|
|
@ -8754,8 +8906,7 @@
|
|||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"dev": true
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
|
|
@ -8865,6 +9016,12 @@
|
|||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
||||
},
|
||||
"is-obj": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
|
||||
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
|
||||
"dev": true
|
||||
},
|
||||
"is-path-inside": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
|
||||
|
|
@ -8881,6 +9038,12 @@
|
|||
"isobject": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"is-regexp": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
|
||||
"integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=",
|
||||
"dev": true
|
||||
},
|
||||
"is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
|
|
@ -9060,15 +9223,95 @@
|
|||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
|
||||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="
|
||||
},
|
||||
"listr2": {
|
||||
"version": "3.11.1",
|
||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.11.1.tgz",
|
||||
"integrity": "sha512-ZXQvQfmH9iWLlb4n3hh31yicXDxlzB0pE7MM1zu6kgbVL4ivEsO4H8IPh4E682sC8RjnYO9anose+zT52rrpyg==",
|
||||
"lint-staged": {
|
||||
"version": "11.2.3",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-11.2.3.tgz",
|
||||
"integrity": "sha512-Tfmhk8O2XFMD25EswHPv+OYhUjsijy5D7liTdxeXvhG2rsadmOLFtyj8lmlfoFFXY8oXWAIOKpoI+lJe1DB1mw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cli-truncate": "2.1.0",
|
||||
"colorette": "^1.4.0",
|
||||
"commander": "^8.2.0",
|
||||
"cosmiconfig": "^7.0.1",
|
||||
"debug": "^4.3.2",
|
||||
"enquirer": "^2.3.6",
|
||||
"execa": "^5.1.1",
|
||||
"listr2": "^3.12.2",
|
||||
"micromatch": "^4.0.4",
|
||||
"normalize-path": "^3.0.0",
|
||||
"please-upgrade-node": "^3.2.0",
|
||||
"string-argv": "0.3.1",
|
||||
"stringify-object": "3.3.0",
|
||||
"supports-color": "8.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.2.0.tgz",
|
||||
"integrity": "sha512-LLKxDvHeL91/8MIyTAD5BFMNtoIwztGPMiM/7Bl8rIPmHCZXRxmSWr91h57dpOpnQ6jIUqEWdXE/uBYMfiVZDA==",
|
||||
"dev": true
|
||||
},
|
||||
"cosmiconfig": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
|
||||
"integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/parse-json": "^4.0.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"parse-json": "^5.0.0",
|
||||
"path-type": "^4.0.0",
|
||||
"yaml": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"execa": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cross-spawn": "^7.0.3",
|
||||
"get-stream": "^6.0.0",
|
||||
"human-signals": "^2.1.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"merge-stream": "^2.0.0",
|
||||
"npm-run-path": "^4.0.1",
|
||||
"onetime": "^5.1.2",
|
||||
"signal-exit": "^3.0.3",
|
||||
"strip-final-newline": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||
"dev": true
|
||||
},
|
||||
"human-signals": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"listr2": {
|
||||
"version": "3.12.2",
|
||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.12.2.tgz",
|
||||
"integrity": "sha512-64xC2CJ/As/xgVI3wbhlPWVPx0wfTqbUAkpb7bjDi0thSWMqrf07UFhrfsGoo8YSXmF049Rp9C0cjLC8rZxK9A==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"cli-truncate": "^2.1.0",
|
||||
"colorette": "^1.2.2",
|
||||
"colorette": "^1.4.0",
|
||||
"log-update": "^4.0.0",
|
||||
"p-map": "^4.0.0",
|
||||
"rxjs": "^6.6.7",
|
||||
|
|
@ -9147,7 +9390,6 @@
|
|||
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
|
||||
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"ansi-escapes": "^4.3.0",
|
||||
"cli-cursor": "^3.1.0",
|
||||
|
|
@ -9160,7 +9402,6 @@
|
|||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
|
||||
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"astral-regex": "^2.0.0",
|
||||
|
|
@ -9172,7 +9413,6 @@
|
|||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
|
|
@ -9237,6 +9477,16 @@
|
|||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
|
||||
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"braces": "^3.0.1",
|
||||
"picomatch": "^2.2.3"
|
||||
}
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.49.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz",
|
||||
|
|
@ -9445,7 +9695,6 @@
|
|||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
|
||||
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"aggregate-error": "^3.0.0"
|
||||
}
|
||||
|
|
@ -9593,6 +9842,15 @@
|
|||
"find-up": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"please-upgrade-node": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
|
||||
"integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"semver-compare": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"pluralize": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
||||
|
|
@ -9844,6 +10102,12 @@
|
|||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"semver-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||
"integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
|
||||
"dev": true
|
||||
},
|
||||
"serialize-javascript": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
|
||||
|
|
@ -9894,7 +10158,6 @@
|
|||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
|
||||
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"astral-regex": "^2.0.0",
|
||||
|
|
@ -9956,6 +10219,12 @@
|
|||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"string-argv": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz",
|
||||
"integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==",
|
||||
"dev": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
||||
|
|
@ -9966,6 +10235,17 @@
|
|||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"stringify-object": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
|
||||
"integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"get-own-enumerable-property-symbols": "^3.0.0",
|
||||
"is-obj": "^1.0.1",
|
||||
"is-regexp": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
||||
|
|
@ -10527,7 +10807,6 @@
|
|||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
|
|
|
|||
|
|
@ -11,11 +11,6 @@
|
|||
"eslint --fix"
|
||||
]
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@4tw/cypress-drag-drop": "^1.8.0",
|
||||
"@babel/core": "^7.4.3",
|
||||
|
|
@ -25,6 +20,7 @@
|
|||
"babel-loader": "^8.0.5",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"husky": "^7.0.2",
|
||||
"lint-staged": "^11.2.3",
|
||||
"path": "^0.12.7",
|
||||
"webpack": "^5.55.1",
|
||||
"webpack-cli": "^4.8.0"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
0.7.4
|
||||
0.8.0
|
||||
70
server/migrations/1632382322381-CreateGroupPermissions.ts
Normal file
70
server/migrations/1632382322381-CreateGroupPermissions.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import {
|
||||
MigrationInterface,
|
||||
QueryRunner,
|
||||
Table,
|
||||
TableForeignKey,
|
||||
TableUnique,
|
||||
} from "typeorm";
|
||||
|
||||
export class CreateGroupPermissions1632382322381 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: "group_permissions",
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
type: "uuid",
|
||||
isGenerated: true,
|
||||
default: "gen_random_uuid()",
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
name: "organization_id",
|
||||
type: "uuid",
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: "group",
|
||||
type: "varchar",
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: "created_at",
|
||||
type: "timestamp",
|
||||
isNullable: false,
|
||||
default: "now()",
|
||||
},
|
||||
{
|
||||
name: "updated_at",
|
||||
type: "timestamp",
|
||||
isNullable: false,
|
||||
default: "now()",
|
||||
},
|
||||
],
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
"group_permissions",
|
||||
new TableForeignKey({
|
||||
columnNames: ["organization_id"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "organizations",
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
);
|
||||
|
||||
await queryRunner.createUniqueConstraint(
|
||||
"group_permissions",
|
||||
new TableUnique({
|
||||
columnNames: ["organization_id", "group"],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable("group_permissions");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import {
|
||||
MigrationInterface,
|
||||
QueryRunner,
|
||||
Table,
|
||||
TableForeignKey,
|
||||
} from "typeorm";
|
||||
|
||||
export class CreateUserGroupPermissions1632383798339
|
||||
implements MigrationInterface
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: "user_group_permissions",
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
type: "uuid",
|
||||
isGenerated: true,
|
||||
default: "gen_random_uuid()",
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
name: "user_id",
|
||||
type: "uuid",
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: "group_permission_id",
|
||||
type: "uuid",
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: "created_at",
|
||||
type: "timestamp",
|
||||
isNullable: false,
|
||||
default: "now()",
|
||||
},
|
||||
{
|
||||
name: "updated_at",
|
||||
type: "timestamp",
|
||||
isNullable: false,
|
||||
default: "now()",
|
||||
},
|
||||
],
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
"user_group_permissions",
|
||||
new TableForeignKey({
|
||||
columnNames: ["user_id"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "users",
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
"user_group_permissions",
|
||||
new TableForeignKey({
|
||||
columnNames: ["group_permission_id"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "group_permissions",
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable("user_group_permissions");
|
||||
}
|
||||
}
|
||||
92
server/migrations/1632384954344-CreateAppGroupPermissions.ts
Normal file
92
server/migrations/1632384954344-CreateAppGroupPermissions.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import {
|
||||
MigrationInterface,
|
||||
QueryRunner,
|
||||
Table,
|
||||
TableForeignKey,
|
||||
} from "typeorm";
|
||||
|
||||
export class CreateAppGroupPermissions1632384954344
|
||||
implements MigrationInterface
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: "app_group_permissions",
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
type: "uuid",
|
||||
isGenerated: true,
|
||||
default: "gen_random_uuid()",
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
name: "app_id",
|
||||
type: "uuid",
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: "group_permission_id",
|
||||
type: "uuid",
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: "read",
|
||||
type: "boolean",
|
||||
default: false,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: "update",
|
||||
type: "boolean",
|
||||
default: false,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: "delete",
|
||||
type: "boolean",
|
||||
default: false,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
name: "created_at",
|
||||
type: "timestamp",
|
||||
isNullable: false,
|
||||
default: "now()",
|
||||
},
|
||||
{
|
||||
name: "updated_at",
|
||||
type: "timestamp",
|
||||
isNullable: false,
|
||||
default: "now()",
|
||||
},
|
||||
],
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
"app_group_permissions",
|
||||
new TableForeignKey({
|
||||
columnNames: ["app_id"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "apps",
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
"app_group_permissions",
|
||||
new TableForeignKey({
|
||||
columnNames: ["group_permission_id"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "group_permissions",
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable("app_group_permissions");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import { EntityManager, In, MigrationInterface, QueryRunner } from "typeorm";
|
||||
import { Organization } from "../src/entities/organization.entity";
|
||||
import { GroupPermission } from "../src/entities/group_permission.entity";
|
||||
import { AppGroupPermission } from "../src/entities/app_group_permission.entity";
|
||||
import { UserGroupPermission } from "../src/entities/user_group_permission.entity";
|
||||
import { App } from "../src/entities/app.entity";
|
||||
|
||||
export class PopulateUserGroupsFromOrganizationRoles1632468258787
|
||||
implements MigrationInterface
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const entityManager = queryRunner.manager;
|
||||
const OrganizationRepository = entityManager.getRepository(Organization);
|
||||
|
||||
const organizations = await OrganizationRepository.find({
|
||||
relations: ["users"],
|
||||
});
|
||||
|
||||
for (let organization of organizations) {
|
||||
const groupPermissions = await setupInitialGroupPermissions(
|
||||
entityManager,
|
||||
organization
|
||||
);
|
||||
await setupUserAndAppGroupPermissions(
|
||||
entityManager,
|
||||
organization,
|
||||
groupPermissions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const entityManager = queryRunner.manager;
|
||||
|
||||
entityManager
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(GroupPermission)
|
||||
.execute();
|
||||
|
||||
entityManager
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(AppGroupPermission)
|
||||
.execute();
|
||||
|
||||
entityManager
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from(UserGroupPermission)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
async function setupInitialGroupPermissions(
|
||||
entityManager: EntityManager,
|
||||
organization: Organization
|
||||
): Promise<Array<GroupPermission>> {
|
||||
const existingRoles = ["admin", "developer", "viewer"];
|
||||
const groupsToCreate = ["all_users", ...existingRoles];
|
||||
const createdGroupPermissions = [];
|
||||
|
||||
const groupPermissionRepository =
|
||||
entityManager.getRepository(GroupPermission);
|
||||
|
||||
for (const group of groupsToCreate) {
|
||||
const groupPermission = groupPermissionRepository.create({
|
||||
organizationId: organization.id,
|
||||
group: group,
|
||||
});
|
||||
await groupPermissionRepository.save(groupPermission);
|
||||
createdGroupPermissions.push(groupPermission);
|
||||
}
|
||||
|
||||
return createdGroupPermissions;
|
||||
}
|
||||
|
||||
async function setupUserAndAppGroupPermissions(
|
||||
entityManager: EntityManager,
|
||||
organization: Organization,
|
||||
createdGroupPermissions: Array<GroupPermission>
|
||||
): Promise<void> {
|
||||
const userGroupPermissionRepository =
|
||||
entityManager.getRepository(UserGroupPermission);
|
||||
|
||||
const appGroupPermissionRepository =
|
||||
entityManager.getRepository(AppGroupPermission);
|
||||
|
||||
const appRepository = entityManager.getRepository(App);
|
||||
|
||||
const organizationApps = await appRepository.find({
|
||||
organizationId: organization.id,
|
||||
});
|
||||
|
||||
for (const groupPermission of createdGroupPermissions) {
|
||||
const usersForGroup = organization.users.filter(
|
||||
(u) =>
|
||||
u.organizationUsers[0].role == groupPermission.group || groupPermission.group == "all_users"
|
||||
);
|
||||
|
||||
for (const user of usersForGroup) {
|
||||
const userGroupPermission = userGroupPermissionRepository.create({
|
||||
groupPermissionId: groupPermission.id,
|
||||
userId: user.id,
|
||||
});
|
||||
await userGroupPermissionRepository.save(userGroupPermission);
|
||||
}
|
||||
|
||||
const permissions = determinePermissionsForGroup(groupPermission.group);
|
||||
|
||||
for (const app of organizationApps) {
|
||||
const appGroupPermission = appGroupPermissionRepository.create({
|
||||
groupPermissionId: groupPermission.id,
|
||||
appId: app.id,
|
||||
...permissions,
|
||||
});
|
||||
await appGroupPermissionRepository.save(appGroupPermission);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function determinePermissionsForGroup(group: string): {
|
||||
read: boolean;
|
||||
update: boolean;
|
||||
delete: boolean;
|
||||
} {
|
||||
switch (group) {
|
||||
case "all_users":
|
||||
return { read: true, update: false, delete: false };
|
||||
case "admin":
|
||||
return { read: true, update: true, delete: true };
|
||||
case "developer":
|
||||
return { read: true, update: true, delete: true };
|
||||
case "viewer":
|
||||
return { read: true, update: false, delete: false };
|
||||
}
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ export default class RestapiQueryService implements QueryService {
|
|||
let result = {};
|
||||
|
||||
/* Prefixing the base url of datasouce if datasource exists */
|
||||
const url = hasDataSource ? `${sourceOptions.url}${queryOptions.url}` : queryOptions.url;
|
||||
const url = hasDataSource ? `${sourceOptions.url}${queryOptions.url || ''}` : queryOptions.url;
|
||||
|
||||
const method = queryOptions['method'];
|
||||
const json = method !== 'get' ? this.body(sourceOptions, queryOptions, hasDataSource) : undefined;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import { CaslModule } from './modules/casl/casl.module';
|
|||
import { EmailService } from '@services/email.service';
|
||||
import { MetaModule } from './modules/meta/meta.module';
|
||||
import { AppController } from './controllers/app.controller';
|
||||
import { AppService } from './services/app.service';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { UsersModule } from './modules/users/users.module';
|
||||
import { AppConfigModule } from './modules/app_config/app_config.module';
|
||||
|
|
@ -27,6 +26,7 @@ import { DataQueriesModule } from './modules/data_queries/data_queries.module';
|
|||
import { DataSourcesModule } from './modules/data_sources/data_sources.module';
|
||||
import { OrganizationsModule } from './modules/organizations/organizations.module';
|
||||
import { join } from 'path';
|
||||
import { GroupPermissionsModule } from './modules/group_permissions/group_permissions.module';
|
||||
|
||||
const imports = [
|
||||
ConfigModule.forRoot({
|
||||
|
|
@ -63,6 +63,7 @@ const imports = [
|
|||
OrganizationsModule,
|
||||
CaslModule,
|
||||
MetaModule,
|
||||
GroupPermissionsModule,
|
||||
];
|
||||
|
||||
if (process.env.SERVE_CLIENT !== 'false') {
|
||||
|
|
@ -86,7 +87,7 @@ if (process.env.APM_VENDOR == 'sentry') {
|
|||
@Module({
|
||||
imports,
|
||||
controllers: [AppController],
|
||||
providers: [AppService, EmailService, SeedsService],
|
||||
providers: [EmailService, SeedsService],
|
||||
})
|
||||
export class AppModule implements OnModuleInit, OnApplicationBootstrap {
|
||||
constructor(private connection: Connection) {}
|
||||
|
|
@ -98,11 +99,11 @@ export class AppModule implements OnModuleInit, OnApplicationBootstrap {
|
|||
});
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
onModuleInit(): void {
|
||||
console.log(`Initializing ToolJet server modules 📡 `);
|
||||
}
|
||||
|
||||
onApplicationBootstrap() {
|
||||
onApplicationBootstrap(): void {
|
||||
console.log(`Initialized ToolJet server, waiting for requests 🚀`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export class AppUsersController {
|
|||
private appsAbilityFactory: AppsAbilityFactory
|
||||
) {}
|
||||
|
||||
// TODO: remove deprecated
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post()
|
||||
async create(@Request() req) {
|
||||
|
|
@ -22,7 +23,7 @@ export class AppUsersController {
|
|||
const { role } = params;
|
||||
|
||||
const app = await this.appsService.find(appId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, { id: appId });
|
||||
|
||||
if (!ability.can('createUsers', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
|
|||
|
|
@ -16,11 +16,14 @@ import { decamelizeKeys } from 'humps';
|
|||
import { AppsAbilityFactory } from 'src/modules/casl/abilities/apps-ability.factory';
|
||||
import { AppAuthGuard } from 'src/modules/auth/app-auth.guard';
|
||||
import { FoldersService } from '@services/folders.service';
|
||||
import { App } from 'src/entities/app.entity';
|
||||
import { AppImportExportService } from '@services/app_import_export.service';
|
||||
|
||||
@Controller('apps')
|
||||
export class AppsController {
|
||||
constructor(
|
||||
private appsService: AppsService,
|
||||
private appImportExportService: AppImportExportService,
|
||||
private foldersService: FoldersService,
|
||||
private appsAbilityFactory: AppsAbilityFactory
|
||||
) {}
|
||||
|
|
@ -28,12 +31,12 @@ export class AppsController {
|
|||
@UseGuards(JwtAuthGuard)
|
||||
@Post()
|
||||
async create(@Request() req) {
|
||||
const app = await this.appsService.create(req.user);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
|
||||
if (!ability.can('createApp', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
if (!ability.can('createApp', App)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
const app = await this.appsService.create(req.user);
|
||||
|
||||
await this.appsService.update(req.user, app.id, {
|
||||
slug: app.id,
|
||||
|
|
@ -46,6 +49,11 @@ export class AppsController {
|
|||
@Get(':id')
|
||||
async show(@Request() req, @Param() params) {
|
||||
const app = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
|
||||
if (!ability.can('viewApp', app)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
const response = decamelizeKeys(app);
|
||||
|
||||
const seralizedQueries = [];
|
||||
|
|
@ -68,10 +76,12 @@ export class AppsController {
|
|||
async appFromSlug(@Request() req, @Param() params) {
|
||||
if (req.user) {
|
||||
const app = await this.appsService.findBySlug(params.slug);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: app.id,
|
||||
});
|
||||
|
||||
if (!ability.can('viewApp', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,10 +102,10 @@ export class AppsController {
|
|||
@Put(':id')
|
||||
async update(@Request() req, @Param() params) {
|
||||
const app = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
|
||||
if (!ability.can('updateParams', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const result = await this.appsService.update(req.user, params.id, req.body.app);
|
||||
|
|
@ -108,10 +118,10 @@ export class AppsController {
|
|||
@Post(':id/clone')
|
||||
async clone(@Request() req, @Param() params) {
|
||||
const existingApp = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
|
||||
if (!ability.can('cloneApp', existingApp)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const result = await this.appsService.clone(existingApp, req.user);
|
||||
|
|
@ -120,11 +130,38 @@ export class AppsController {
|
|||
return response;
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get(':id/export')
|
||||
async export(@Request() req, @Param() params) {
|
||||
const appToExport = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
|
||||
if (!ability.can('viewApp', appToExport)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const app = await this.appImportExportService.export(req.user, params.id);
|
||||
return app;
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('/import')
|
||||
async import(@Request() req) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
|
||||
if (!ability.can('createApp', App)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
await this.appImportExportService.import(req.user, req.body);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete(':id')
|
||||
async delete(@Request() req, @Param() params) {
|
||||
const app = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
|
||||
if (!ability.can('deleteApp', app)) {
|
||||
throw new ForbiddenException('Only administrators are allowed to delete apps.');
|
||||
|
|
@ -172,14 +209,15 @@ export class AppsController {
|
|||
return decamelizeKeys(response);
|
||||
}
|
||||
|
||||
// deprecated
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get(':id/users')
|
||||
async fetchUsers(@Request() req, @Param() params) {
|
||||
const app = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
|
||||
if (!ability.can('fetchUsers', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const result = await this.appsService.fetchUsers(req.user, params.id);
|
||||
|
|
@ -190,10 +228,10 @@ export class AppsController {
|
|||
@Get(':id/versions')
|
||||
async fetchVersions(@Request() req, @Param() params) {
|
||||
const app = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
|
||||
if (!ability.can('fetchVersions', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const result = await this.appsService.fetchVersions(req.user, params.id);
|
||||
|
|
@ -206,10 +244,10 @@ export class AppsController {
|
|||
const versionName = req.body['versionName'];
|
||||
|
||||
const app = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
|
||||
if (!ability.can('createVersions', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const appUser = await this.appsService.createVersion(req.user, app, versionName);
|
||||
|
|
@ -220,10 +258,10 @@ export class AppsController {
|
|||
@Get(':id/versions/:versionId')
|
||||
async version(@Request() req, @Param() params) {
|
||||
const app = await this.appsService.find(params.id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
|
||||
if (!ability.can('fetchVersions', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const appVersion = await this.appsService.findVersion(params.versionId);
|
||||
|
|
@ -237,10 +275,10 @@ export class AppsController {
|
|||
const definition = req.body['definition'];
|
||||
|
||||
const version = await this.appsService.findVersion(params.versionId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, params);
|
||||
|
||||
if (!ability.can('updateVersions', version.app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const appUser = await this.appsService.updateVersion(req.user, version, definition);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ export class DataQueriesController {
|
|||
@Get()
|
||||
async index(@Request() req, @Query() query) {
|
||||
const app = await this.appsService.find(query.app_id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: query.app_id,
|
||||
});
|
||||
|
||||
if (!ability.can('getQueries', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
@ -61,7 +63,9 @@ export class DataQueriesController {
|
|||
const appId = req.body.app_id;
|
||||
|
||||
const app = await this.appsService.find(appId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: appId,
|
||||
});
|
||||
|
||||
if (!ability.can('createQuery', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
@ -88,7 +92,9 @@ export class DataQueriesController {
|
|||
const dataQueryId = params.id;
|
||||
|
||||
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: dataQuery.appId,
|
||||
});
|
||||
|
||||
if (!ability.can('updateQuery', dataQuery.app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
@ -104,7 +110,9 @@ export class DataQueriesController {
|
|||
const dataQueryId = params.id;
|
||||
|
||||
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: dataQuery.appId,
|
||||
});
|
||||
|
||||
if (!ability.can('deleteQuery', dataQuery.app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
@ -123,7 +131,9 @@ export class DataQueriesController {
|
|||
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
|
||||
|
||||
if (req.user) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: dataQuery.appId,
|
||||
});
|
||||
|
||||
if (!ability.can('runQuery', dataQuery.app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
@ -166,7 +176,9 @@ export class DataQueriesController {
|
|||
};
|
||||
|
||||
if (dataQueryEntity.dataSource) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: dataQueryEntity.dataSource.appId,
|
||||
});
|
||||
|
||||
if (!ability.can('previewQuery', dataQueryEntity.dataSource.app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ export class DataSourcesController {
|
|||
@Get()
|
||||
async index(@Request() req, @Query() query) {
|
||||
const app = await this.appsService.find(query.app_id);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: app.id,
|
||||
});
|
||||
|
||||
if (!ability.can('getDataSources', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
@ -38,7 +40,9 @@ export class DataSourcesController {
|
|||
const appId = req.body.app_id;
|
||||
|
||||
const app = await this.appsService.find(appId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: appId,
|
||||
});
|
||||
|
||||
if (!ability.can('createDataSource', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
@ -57,7 +61,9 @@ export class DataSourcesController {
|
|||
const dataSource = await this.dataSourcesService.findOne(dataSourceId);
|
||||
|
||||
const app = await this.appsService.find(dataSource.appId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: app.id,
|
||||
});
|
||||
|
||||
if (!ability.can('updateDataSource', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
@ -90,7 +96,9 @@ export class DataSourcesController {
|
|||
const dataSource = await this.dataSourcesService.findOne(dataSourceId);
|
||||
|
||||
const app = await this.appsService.find(dataSource.appId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {
|
||||
id: app.id,
|
||||
});
|
||||
|
||||
if (!ability.can('authorizeOauthForSource', app)) {
|
||||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
|
|
|
|||
109
server/src/controllers/group_permissions.controller.ts
Normal file
109
server/src/controllers/group_permissions.controller.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { Controller, Post, Get, Put, Delete, Request, UseGuards, Param } from '@nestjs/common';
|
||||
|
||||
import { decamelizeKeys } from 'humps';
|
||||
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
|
||||
import { GroupPermissionsService } from '../services/group_permissions.service';
|
||||
import { PoliciesGuard } from 'src/modules/casl/policies.guard';
|
||||
import { CheckPolicies } from 'src/modules/casl/check_policies.decorator';
|
||||
import { AppAbility } from 'src/modules/casl/casl-ability.factory';
|
||||
import { User } from 'src/entities/user.entity';
|
||||
|
||||
@Controller('group_permissions')
|
||||
export class GroupPermissionsController {
|
||||
constructor(private groupPermissionsService: GroupPermissionsService) {}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@Post()
|
||||
async create(@Request() req) {
|
||||
const groupPermission = await this.groupPermissionsService.create(req.user, req.body.group);
|
||||
|
||||
return decamelizeKeys(groupPermission);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@Get(':id')
|
||||
async show(@Request() req, @Param() params) {
|
||||
const groupPermission = await this.groupPermissionsService.findOne(req.user, params.id);
|
||||
|
||||
return decamelizeKeys(groupPermission);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@Put(':id/app_group_permissions/:appGroupPermissionId')
|
||||
async updateAppGroupPermission(@Request() req, @Param() params) {
|
||||
const groupPermission = await this.groupPermissionsService.updateAppGroupPermission(
|
||||
req.user,
|
||||
params.id,
|
||||
params.appGroupPermissionId,
|
||||
req.body.actions
|
||||
);
|
||||
|
||||
return decamelizeKeys(groupPermission);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@Put(':id')
|
||||
async update(@Request() req, @Param() params) {
|
||||
const groupPermission = await this.groupPermissionsService.update(req.user, params.id, req.body);
|
||||
|
||||
return decamelizeKeys(groupPermission);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@Get()
|
||||
async index(@Request() req) {
|
||||
const groupPermissions = await this.groupPermissionsService.findAll(req.user);
|
||||
|
||||
return decamelizeKeys({ groupPermissions });
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@Delete(':id')
|
||||
async destroy(@Request() req, @Param() params) {
|
||||
const groupPermission = await this.groupPermissionsService.destroy(req.user, params.id);
|
||||
|
||||
return decamelizeKeys(groupPermission);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@Get(':id/apps')
|
||||
async apps(@Request() req, @Param() params) {
|
||||
const apps = await this.groupPermissionsService.findApps(req.user, params.id);
|
||||
|
||||
return decamelizeKeys({ apps });
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@Get(':id/addable_apps')
|
||||
async addableApps(@Request() req, @Param() params) {
|
||||
const apps = await this.groupPermissionsService.findAddableApps(req.user, params.id);
|
||||
|
||||
return decamelizeKeys({ apps });
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@Get(':id/users')
|
||||
async users(@Request() req, @Param() params) {
|
||||
const users = await this.groupPermissionsService.findUsers(req.user, params.id);
|
||||
|
||||
return decamelizeKeys({ users });
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PoliciesGuard)
|
||||
@CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
|
||||
@Get(':id/addable_users')
|
||||
async addableUsers(@Request() req, @Param() params) {
|
||||
const users = await this.groupPermissionsService.findAddableUsers(req.user, params.id);
|
||||
|
||||
return decamelizeKeys({ users });
|
||||
}
|
||||
}
|
||||
|
|
@ -9,11 +9,17 @@ import {
|
|||
OneToMany,
|
||||
AfterLoad,
|
||||
BaseEntity,
|
||||
ManyToMany,
|
||||
JoinTable,
|
||||
AfterInsert,
|
||||
getRepository,
|
||||
} from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { AppVersion } from './app_version.entity';
|
||||
import { DataQuery } from './data_query.entity';
|
||||
import { DataSource } from './data_source.entity';
|
||||
import { GroupPermission } from './group_permission.entity';
|
||||
import { AppGroupPermission } from './app_group_permission.entity';
|
||||
|
||||
@Entity({ name: 'apps' })
|
||||
export class App extends BaseEntity {
|
||||
|
|
@ -45,17 +51,47 @@ export class App extends BaseEntity {
|
|||
@JoinColumn({ name: 'user_id' })
|
||||
user: User;
|
||||
|
||||
@OneToMany(() => AppVersion, (appVersion) => appVersion.app, { eager: true, onDelete: 'CASCADE' })
|
||||
@OneToMany(() => AppVersion, (appVersion) => appVersion.app, {
|
||||
eager: true,
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
appVersions: AppVersion[];
|
||||
|
||||
@OneToMany(() => DataQuery, (dataQuery) => dataQuery.app, { onDelete: 'CASCADE' })
|
||||
@OneToMany(() => DataQuery, (dataQuery) => dataQuery.app, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
dataQueries: DataQuery[];
|
||||
|
||||
@OneToMany(() => DataSource, (dataSource) => dataSource.app, { onDelete: 'CASCADE' })
|
||||
@OneToMany(() => DataSource, (dataSource) => dataSource.app, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
dataSources: DataSource[];
|
||||
|
||||
@ManyToMany(() => GroupPermission)
|
||||
@JoinTable({
|
||||
name: 'app_group_permissions',
|
||||
joinColumn: {
|
||||
name: 'app_id',
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: 'group_permission_id',
|
||||
},
|
||||
})
|
||||
groupPermissions: GroupPermission[];
|
||||
|
||||
@OneToMany(() => AppGroupPermission, (appGroupPermission) => appGroupPermission.app, { onDelete: 'CASCADE' })
|
||||
appGroupPermissions: AppGroupPermission[];
|
||||
|
||||
public editingVersion;
|
||||
|
||||
@AfterInsert()
|
||||
updateSlug(): void {
|
||||
if (!this.slug) {
|
||||
const appRepository = getRepository(App);
|
||||
appRepository.update(this.id, { slug: this.id });
|
||||
}
|
||||
}
|
||||
|
||||
@AfterLoad()
|
||||
async afterLoad(): Promise<void> {
|
||||
if (this.currentVersionId) {
|
||||
|
|
|
|||
47
server/src/entities/app_group_permission.entity.ts
Normal file
47
server/src/entities/app_group_permission.entity.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { GroupPermission } from './group_permission.entity';
|
||||
import { App } from './app.entity';
|
||||
|
||||
@Entity({ name: 'app_group_permissions' })
|
||||
export class AppGroupPermission extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'app_id' })
|
||||
appId: string;
|
||||
|
||||
@Column({ name: 'group_permission_id' })
|
||||
groupPermissionId: string;
|
||||
|
||||
@Column({ default: false })
|
||||
read: boolean;
|
||||
|
||||
@Column({ default: false })
|
||||
update: boolean;
|
||||
|
||||
@Column({ default: false })
|
||||
delete: boolean;
|
||||
|
||||
@CreateDateColumn({ default: () => 'now()', name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ default: () => 'now()', name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
@ManyToOne(() => App, (app) => app.id)
|
||||
@JoinColumn({ name: 'app_id' })
|
||||
app: App;
|
||||
|
||||
@ManyToOne(() => GroupPermission, (groupPermission) => groupPermission.id)
|
||||
@JoinColumn({ name: 'group_permission_id' })
|
||||
groupPermission: GroupPermission;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue