mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 01:18:23 +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_USERNAME=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
SMTP_DOMAIN=
|
SMTP_DOMAIN=
|
||||||
SMTP_ADDRESS=
|
SMTP_PORT=
|
||||||
|
|
||||||
# DISABLE USER SIGNUPS (true or false). Default: true
|
# DISABLE USER SIGNUPS (true or false). Default: true
|
||||||
DISABLE_SIGNUPS=
|
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.
|
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)
|
## 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:
|
**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.
|
By contributing, you agree that your contributions will be licensed under its AGPL v3 License.
|
||||||
|
|
||||||
## Questions?
|
## 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.
|
Build and deploy internal tools.
|
||||||
</p>
|
</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
|
## 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 🖥
|
- Mobile 📱 & desktop layouts 🖥
|
||||||
- Dark mode 🌛
|
- Dark mode 🌛
|
||||||
- Connect to databases, APIs and external services
|
- Connect to databases, APIs, and external services.
|
||||||
- Deploy on-premise ( supports docker, kubernetes, heroku and more )
|
- Deploy on-premise (supports docker, kubernetes, heroku, and more)
|
||||||
- Granular access control on organization level and app level
|
- Granular access control on organization-level and app-level.
|
||||||
- Write JS code almost anywhere in the builder
|
- Write JS code almost anywhere in the builder.
|
||||||
- Query editors for all supported data sources
|
- Query editors for all supported data sources.
|
||||||
- Transform query results using JS code
|
- Transform query results using JS code.
|
||||||
- All the credentials are securely encrypted using `aes-256-gcm`.
|
- All the credentials are securely encrypted using `aes-256-gcm`.
|
||||||
- ToolJet acts only as a proxy and doesn't store any data.
|
- ToolJet acts only as a proxy and doesn't store any data.
|
||||||
- Support for OAuth
|
- Support for OAuth.
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
## Quickstart
|
## 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.
|
You can deploy ToolJet on Heroku for free using the one-click-deployment button below.
|
||||||
<p align="center">
|
<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>
|
[Building a Github contributor leaderboard using ToolJet](https://blog.tooljet.io/building-a-github-contributor-leaderboard-using-tooljet/)<br>
|
||||||
|
|
||||||
## Documentation
|
## 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>
|
- [Getting Started](https://docs.tooljet.io)<br>
|
||||||
[Deploying](https://docs.tooljet.io)<br>
|
- [Deploying](https://docs.tooljet.io/docs/deployment/architecture)<br>
|
||||||
[Datasource Reference](https://docs.tooljet.io)<br>
|
- [Datasource Reference](https://docs.tooljet.io/docs/data-sources/airtable/)<br>
|
||||||
[Widget Reference](https://docs.tooljet.io)
|
- [Widget Reference](https://docs.tooljet.io/docs/widgets/button)
|
||||||
|
|
||||||
## Branching model
|
## 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
|
## 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>
|
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>
|
||||||
[Contributing Guide](https://docs.tooljet.io/docs/contributing-guide/setup/docker)
|
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
<a href="https://github.com/tooljet/tooljet/graphs/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
|
# Setup nginx config
|
||||||
export SERVER_HOST="${SERVER_HOST:=localhost}"
|
export SERVER_HOST="${SERVER_HOST:=localhost}"
|
||||||
export SERVER_USER="${SERVER_USER:=www-data}"
|
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
|
envsubst "${VARS_TO_SUBSTITUTE}" < /tmp/nginx.conf > /tmp/nginx-substituted.conf
|
||||||
sudo cp /tmp/nginx-substituted.conf /usr/local/openresty/nginx/conf/nginx.conf
|
sudo cp /tmp/nginx-substituted.conf /usr/local/openresty/nginx/conf/nginx.conf
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,15 @@ sidebar_position: 1
|
||||||
# Mac OS
|
# 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.
|
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
|
## Setting up
|
||||||
### Install Homebrew
|
|
||||||
|
1. Set up the environment
|
||||||
|
|
||||||
|
1.1 Install Homebrew
|
||||||
```bash
|
```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
|
```bash
|
||||||
brew install nvm
|
brew install nvm
|
||||||
export NVM_DIR=~/.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
|
nvm use 14.17.3
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Postgres
|
1.3 Install Postgres
|
||||||
:::tip
|
:::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.
|
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
|
```bash
|
||||||
brew install postgresql
|
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
|
```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
|
:::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)
|
||||||
|
|
||||||
|
|
@ -56,50 +61,50 @@ Follow these steps to setup and run ToolJet on Mac OS for development purposes.
|
||||||
ORM_LOGGING=all
|
ORM_LOGGING=all
|
||||||
```
|
```
|
||||||
|
|
||||||
4. ## Install dependencies
|
4. Install dependencies
|
||||||
```bash
|
```bash
|
||||||
npm install --prefix server
|
npm install --prefix server
|
||||||
npm install --prefix frontend
|
npm install --prefix frontend
|
||||||
```
|
```
|
||||||
5. ## Setup database
|
|
||||||
|
5. Set up database
|
||||||
```bash
|
```bash
|
||||||
npm run --prefix server db:reset
|
npm run --prefix server db:reset
|
||||||
```
|
```
|
||||||
6. ## Install webpack & nest-cli
|
|
||||||
|
6. Install webpack & nest-cli
|
||||||
```bash
|
```bash
|
||||||
npm install -g webpack
|
npm install -g webpack
|
||||||
npm install -g webpack-cli
|
npm install -g webpack-cli
|
||||||
npm install -g @nestjs/cli
|
npm install -g @nestjs/cli
|
||||||
```
|
```
|
||||||
|
|
||||||
7. ## Running the server
|
7. Run the server
|
||||||
```bash
|
```bash
|
||||||
cd ./server && npm run start:dev
|
cd ./server && npm run start:dev
|
||||||
```
|
```
|
||||||
|
|
||||||
8. ## Running the client
|
8. Run the client
|
||||||
```bash
|
```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)
|
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.
|
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.
|
||||||
|
|
||||||
|
## Running tests
|
||||||
10. ## Running tests
|
|
||||||
|
|
||||||
Test config requires the presence of `.env.test` file at the root of the project.
|
Test config requires the presence of `.env.test` file at the root of the project.
|
||||||
|
|
||||||
To run the unit tests
|
To run the unit tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ npm run --prefix server test
|
npm run --prefix server test
|
||||||
```
|
```
|
||||||
|
|
||||||
To run e2e tests
|
To run e2e tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run --prefix server test:e2e
|
npm run --prefix server test:e2e
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ sidebar_position: 1
|
||||||
---
|
---
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
|
|
||||||
Docker compose is the easiest way to setup ToolJet server and client locally.
|
Docker compose is the easiest way to setup ToolJet server and client locally.
|
||||||
|
|
||||||
## Prerequisites
|
## 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.
|
Make sure you have the latest version of `docker` and `docker-compose` installed.
|
||||||
|
|
||||||
[Official docker installation guide](https://docs.docker.com/desktop/)
|
[Official docker installation guide](https://docs.docker.com/desktop/)
|
||||||
|
|
||||||
[Official docker-compose installation guide](https://docs.docker.com/compose/install/)
|
[Official docker-compose installation guide](https://docs.docker.com/compose/install/)
|
||||||
|
|
||||||
We recommend:
|
We recommend:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker --version
|
docker --version
|
||||||
Docker version 19.03.12, build 48a66213fe
|
Docker version 19.03.12, build 48a66213fe
|
||||||
|
|
@ -23,25 +26,26 @@ docker-compose version 1.26.2, build eefe0d31
|
||||||
|
|
||||||
## Setting up
|
## Setting up
|
||||||
|
|
||||||
1. Close the repository
|
1. Clone the repository
|
||||||
```bash
|
```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
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
cp .env.example .env.test
|
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
|
:::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)
|
`LOCKBOX_MASTER_KEY` requires a 32 byte key. (Run `openssl rand -hex 32` to create a 32 byte secure random key)
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat .env
|
cat .env
|
||||||
TOOLJET_HOST=http://localhost:8082
|
TOOLJET_HOST=http://localhost:8082
|
||||||
|
|
@ -73,31 +77,33 @@ docker-compose version 1.26.2, build eefe0d31
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Build docker images
|
4. Build docker images
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose build
|
docker-compose build
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Run ToolJet
|
5. Run ToolJet
|
||||||
|
|
||||||
```bash
|
```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.
|
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
|
```bash
|
||||||
docker-compose run --rm server npm run db:seed
|
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.
|
7. ToolJet should now be served locally at `http://localhost:8082`. You can login using the default user created.
|
||||||
```
|
```
|
||||||
email: dev@tooljet.io
|
email: dev@tooljet.io
|
||||||
password: password
|
password: password
|
||||||
```
|
```
|
||||||
|
|
||||||
|
8. To shut down the containers,
|
||||||
8. To shut down the containers,
|
```bash
|
||||||
```bash
|
docker-compose stop
|
||||||
docker-compose stop
|
```
|
||||||
```
|
|
||||||
|
|
||||||
## Making changes to the codebase
|
## 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`.
|
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:
|
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:
|
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
|
FROM node:14.17.0-buster
|
||||||
|
|
||||||
|
|
@ -137,32 +144,32 @@ COPY ./.env ../.env
|
||||||
RUN ["chmod", "755", "entrypoint.sh"]
|
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
|
## Running tests
|
||||||
|
|
||||||
Test config picks up config from `.env.test` file at the root of the project.
|
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
|
Run the following command to create and migrate data for test db
|
||||||
|
|
||||||
```bash
|
```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:create
|
||||||
docker-compose run --rm -e NODE_ENV=test server npm run db:migrate
|
docker-compose run --rm -e NODE_ENV=test server npm run db:migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
To run the unit tests
|
To run the unit tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ docker-compose --rm run server npm run test
|
docker-compose --rm run server npm run test
|
||||||
```
|
```
|
||||||
|
|
||||||
To run e2e tests
|
To run e2e tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose run --rm server npm run test:e2e
|
docker-compose run --rm server npm run test:e2e
|
||||||
```
|
```
|
||||||
|
|
||||||
To run a specific unit test
|
To run a specific unit test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose run --rm server npm run test <path-to-file>
|
docker-compose run --rm server npm run test <path-to-file>
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -5,27 +5,30 @@ sidebar_position: 1
|
||||||
# Ubuntu
|
# Ubuntu
|
||||||
Follow these steps to setup and run ToolJet on Ubuntu. Open terminal and run the commands below.
|
Follow these steps to setup and run ToolJet on Ubuntu. Open terminal and run the commands below.
|
||||||
|
|
||||||
1. ## Setting up the environment
|
## Setting up
|
||||||
### Install Node.js
|
|
||||||
|
1. Set up the environment
|
||||||
|
|
||||||
|
1.1 Install Node.js
|
||||||
```bash
|
```bash
|
||||||
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
|
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
|
||||||
sudo apt-get install -y nodejs
|
sudo apt-get install -y nodejs
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Postgres
|
1.2 Install Postgres
|
||||||
```bash
|
```bash
|
||||||
sudo apt install postgresql postgresql-contrib
|
sudo apt install postgresql postgresql-contrib
|
||||||
sudo apt-get install libpq-dev
|
sudo apt-get install libpq-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
2. ## Setup environment variables
|
2. Set up 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
|
|
||||||
|
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
|
```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
|
:::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)
|
||||||
|
|
||||||
|
|
@ -34,49 +37,50 @@ Follow these steps to setup and run ToolJet on Ubuntu. Open terminal and run the
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```bash
|
```bash
|
||||||
cat .env
|
cat .env
|
||||||
TOOLJET_HOST=http://localhost:8082
|
TOOLJET_HOST=http://localhost:8082
|
||||||
LOCKBOX_MASTER_KEY=1d291a926ddfd221205a23adb4cc1db66cb9fcaf28d97c8c1950e3538e3b9281
|
LOCKBOX_MASTER_KEY=1d291a926ddfd221205a23adb4cc1db66cb9fcaf28d97c8c1950e3538e3b9281
|
||||||
SECRET_KEY_BASE=4229d5774cfe7f60e75d6b3bf3a1dbb054a696b6d21b6d5de7b73291899797a222265e12c0a8e8d844f83ebacdf9a67ec42584edf1c2b23e1e7813f8a3339041
|
SECRET_KEY_BASE=4229d5774cfe7f60e75d6b3bf3a1dbb054a696b6d21b6d5de7b73291899797a222265e12c0a8e8d844f83ebacdf9a67ec42584edf1c2b23e1e7813f8a3339041
|
||||||
```
|
```
|
||||||
|
|
||||||
4. ## Install dependencies
|
4. Install dependencies
|
||||||
```bash
|
```bash
|
||||||
npm install --prefix server
|
npm install --prefix server
|
||||||
npm install --prefix frontend
|
npm install --prefix frontend
|
||||||
```
|
```
|
||||||
5. ## Setup database
|
|
||||||
|
5. Set up database
|
||||||
```bash
|
```bash
|
||||||
npm run --prefix server db:reset
|
npm run --prefix server db:reset
|
||||||
```
|
```
|
||||||
6. ## Running the server
|
|
||||||
|
6. Run the server
|
||||||
```bash
|
```bash
|
||||||
cd ./server && npm run start:dev
|
cd ./server && npm run start:dev
|
||||||
```
|
```
|
||||||
|
|
||||||
7. ## Running the client
|
7. Run the client
|
||||||
```bash
|
```bash
|
||||||
cd ./frontend && npm start
|
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)
|
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
|
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.
|
|
||||||
|
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.
|
Test config requires the presence of `.env.test` file at the root of the project.
|
||||||
|
|
||||||
To run the unit tests
|
To run the unit tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ npm run --prefix server test
|
npm run --prefix server test
|
||||||
```
|
```
|
||||||
|
|
||||||
To run e2e tests
|
To run e2e tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run --prefix server test:e2e
|
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
|
# Deploying ToolJet client
|
||||||
|
|
@ -15,14 +15,14 @@ For example: `TOOLJET_SERVER_URL=https://server.tooljet.io npm run build && fire
|
||||||
:::
|
:::
|
||||||
|
|
||||||
1. Initialize firebase project
|
1. Initialize firebase project
|
||||||
```bash
|
```bash
|
||||||
firebase init
|
firebase init
|
||||||
```
|
```
|
||||||
Select Firebase Hosting and set build as the static file directory
|
Select Firebase Hosting and set build as the static file directory
|
||||||
2. Deploy client to Firebase
|
2. Deploy client to Firebase
|
||||||
```bash
|
```bash
|
||||||
firebase deploy
|
firebase deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
:::tip
|
:::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)
|
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.
|
`TOOLJET_HOST` environment variable can either be the public ipv4 address of your server or a custom domain that you want to use.
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
We use a [lets encrypt](https://letsencrypt.org/) plugin on top of nginx to create TLS certificates on the fly.
|
We use a [lets encrypt](https://letsencrypt.org/) plugin on top of nginx to create TLS certificates on the fly.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
`TOOLJET_HOST=http://12.34.56.78` or
|
`TOOLJET_HOST=http://12.34.56.78` or
|
||||||
`TOOLJET_HOST=https://yourdomain.com` or
|
`TOOLJET_HOST=https://yourdomain.com` or
|
||||||
`TOOLJET_HOST=https://tooljet.yourdomain.com`
|
`TOOLJET_HOST=https://tooljet.yourdomain.com`
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
Please make sure that `TOOLJET_HOST` starts with either `http://` or `https://`
|
Please make sure that `TOOLJET_HOST` starts with either `http://` or `https://`
|
||||||
:::
|
:::
|
||||||
|
|
||||||
6. Once you've populated the `.env` file, run
|
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:
|
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`
|
`sudo docker-compose up -d`
|
||||||
OR
|
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.
|
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
|
# Environment variables
|
||||||
|
|
@ -10,30 +10,28 @@ Both the ToolJet server and client requires some environment variables to start
|
||||||
|
|
||||||
#### ToolJet host ( required )
|
#### ToolJet host ( required )
|
||||||
|
|
||||||
| variable | description |
|
| variable | description |
|
||||||
| ----------- | ----------- |
|
| ------------ | --------------------------------------------------------------- |
|
||||||
| TOOLJET_HOST | the public URL of ToolJet client ( eg: https://app.tooljet.io ) |
|
| TOOLJET_HOST | the public URL of ToolJet client ( eg: https://app.tooljet.io ) |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Database configuration ( required )
|
#### Database configuration ( required )
|
||||||
|
|
||||||
ToolJet server uses PostgreSQL as the database.
|
ToolJet server uses PostgreSQL as the database.
|
||||||
|
|
||||||
| variable | description |
|
| variable | description |
|
||||||
| ----------- | ----------- |
|
| -------- | ---------------------- |
|
||||||
| PG_HOST | postgres database host |
|
| PG_HOST | postgres database host |
|
||||||
| PG_DB | name of the database |
|
| PG_DB | name of the database |
|
||||||
| PG_USER | username |
|
| PG_USER | username |
|
||||||
| PG_PASS | password |
|
| PG_PASS | password |
|
||||||
|
|
||||||
#### Lockbox configuration ( required )
|
#### 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.
|
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 )
|
#### 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
|
:::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`.
|
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`
|
For `SECRET_KEY_BASE` use `openssl rand -hex 64`
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|
||||||
#### Disabling signups ( optional )
|
#### Disabling signups ( optional )
|
||||||
|
|
||||||
If want to restrict the signups and allow new users only by invitations, set the environment variable `DISABLE_SIGNUPS` to `true`.
|
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 )
|
#### 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 (`/`).
|
You can set `SERVE_CLIENT` to `true` and the server will attempt to serve the client at its root end-point (`/`).
|
||||||
|
|
||||||
#### SMTP configuration ( optional )
|
#### 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 ).
|
ToolJet uses SMTP services to send emails ( Eg: invitation email when you add new users to your organization ).
|
||||||
|
|
||||||
| variable | description |
|
| variable | description |
|
||||||
|--------------------|-------------------------------------------|
|
| ------------------ | ----------------------------------------- |
|
||||||
| DEFAULT_FROM_EMAIL | from email for the email fired by ToolJet |
|
| DEFAULT_FROM_EMAIL | from email for the email fired by ToolJet |
|
||||||
| SMTP_USERNAME | username |
|
| SMTP_USERNAME | username |
|
||||||
| SMTP_PASSWORD | password |
|
| 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:
|
If your ToolJet installation requires Slack as a datasource, you need to create a Slack app and set the following environment variables:
|
||||||
|
|
||||||
| variable | description |
|
| variable | description |
|
||||||
| ----------- | ----------- |
|
| ------------------- | ------------------------------ |
|
||||||
| SLACK_CLIENT_ID | client id of the slack app |
|
| SLACK_CLIENT_ID | client id of the slack app |
|
||||||
| SLACK_CLIENT_SECRET | client secret of the slack app |
|
| SLACK_CLIENT_SECRET | client secret of the slack app |
|
||||||
|
|
||||||
#### Google OAuth ( optional )
|
#### 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.
|
If your ToolJet installation needs access to datasources such as Google sheets, you need to create OAuth credentials from Google Cloud Console.
|
||||||
|
|
||||||
| variable | description |
|
| variable | description |
|
||||||
| ----------- | ----------- |
|
| -------------------- | ------------- |
|
||||||
| GOOGLE_CLIENT_ID | client id |
|
| GOOGLE_CLIENT_ID | client id |
|
||||||
| GOOGLE_CLIENT_SECRET | client secret |
|
| GOOGLE_CLIENT_SECRET | client secret |
|
||||||
|
|
||||||
#### Google maps configuration ( optional )
|
#### Google maps configuration ( optional )
|
||||||
|
|
||||||
If your ToolJet installation requires `Maps` widget, you need to create an API key for Google Maps API.
|
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 |
|
| GOOGLE_MAPS_API_KEY | Google maps API key |
|
||||||
|
|
||||||
#### APM VENDOR ( optional )
|
#### APM VENDOR ( optional )
|
||||||
|
|
||||||
Specify application monitoring vendor. Currently supported values - `sentry`.
|
Specify application monitoring vendor. Currently supported values - `sentry`.
|
||||||
|
|
||||||
| variable | description |
|
| variable | description |
|
||||||
| ----------- | ----------- |
|
| ---------- | ----------------------------------------- |
|
||||||
| APM VENDOR | Application performance monitoring vendor |
|
| APM VENDOR | Application performance monitoring vendor |
|
||||||
|
|
||||||
#### SENTRY DNS ( optional )
|
#### SENTRY DNS ( optional )
|
||||||
|
|
@ -112,21 +109,23 @@ Prints logs for sentry. Supported values: `true` | `false`
|
||||||
Default value is `false`
|
Default value is `false`
|
||||||
|
|
||||||
#### Server URL ( optional)
|
#### Server URL ( optional)
|
||||||
|
|
||||||
This is used to set up for CSP headers and put trace info to be used with APM vendors.
|
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 ) |
|
| TOOLJET_SERVER_URL | the URL of ToolJet server ( eg: https://server.tooljet.io ) |
|
||||||
|
|
||||||
|
|
||||||
#### RELEASE VERSION ( optional)
|
#### RELEASE VERSION ( optional)
|
||||||
|
|
||||||
Once set any APM provider that supports segregation with releases will track it.
|
Once set any APM provider that supports segregation with releases will track it.
|
||||||
|
|
||||||
## ToolJet client
|
## ToolJet client
|
||||||
|
|
||||||
#### Server URL ( optionally required )
|
#### Server URL ( optionally required )
|
||||||
|
|
||||||
This is required when client is built separately.
|
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 ) |
|
| 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
|
sidebar_label: Heroku
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -8,14 +8,15 @@ sidebar_label: Heroku
|
||||||
Follow the steps below to deploy ToolJet on Heroku:
|
Follow the steps below to deploy ToolJet on Heroku:
|
||||||
|
|
||||||
1. Click the button below to start one click deployment.
|
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.
|
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.
|
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
|
:::tip
|
||||||
The one click deployment will create a free dyno and a free postgresql database.
|
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)
|
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.
|
Follow the steps below to deploy ToolJet on a GKE Kubernetes cluster.
|
||||||
|
|
||||||
1. Create an SSL certificate.
|
1. Create an SSL certificate.
|
||||||
```bash
|
|
||||||
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/GKE/certificate.yaml
|
```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.
|
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
|
2. Reserve a static IP address using `gcloud` cli
|
||||||
```bash
|
|
||||||
gcloud compute addresses create tj-static-ip --global
|
```bash
|
||||||
```
|
gcloud compute addresses create tj-static-ip --global
|
||||||
|
```
|
||||||
|
|
||||||
3. Create k8s deployment
|
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).
|
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
|
4. Create k8s service
|
||||||
```bash
|
|
||||||
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/GKE/service.yaml
|
```bash
|
||||||
```
|
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/GKE/service.yaml
|
||||||
|
```
|
||||||
|
|
||||||
5. Create k8s ingress
|
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.
|
Change the domain name to the domain/subdomain that you wish to use for ToolJet installation.
|
||||||
|
|
||||||
6. Apply YAML configs
|
6. Apply YAML configs
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
kubectl apply -f certificate.yaml, deployment.yaml, service.yaml, ingress.yaml
|
kubectl apply -f certificate.yaml, deployment.yaml, service.yaml, ingress.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
:::info
|
:::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).
|
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:
|
If you want to seed the database with a sample user, please SSH into a pod and run:
|
||||||
`npm run db:seed --prefix server`.
|
`npm run db:seed --prefix server`.
|
||||||
This seeds the database with a default user with the following credentials:
|
This seeds the database with a default user with the following credentials:
|
||||||
|
|
||||||
email: `dev@tooljet.io`
|
email: `dev@tooljet.io`
|
||||||
password: `password`
|
password: `password`
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
sidebar_position: 4
|
sidebar_position: 5
|
||||||
sidebar_label: Kubernetes
|
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.
|
Follow the steps below to deploy ToolJet on a Kubernetes cluster.
|
||||||
|
|
||||||
1. Setup a PostgreSQL database
|
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.
|
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
|
3. Create a Kubernetes deployment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
kubectl apply -f https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/deployment.yaml
|
kubectl apply -f https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/deployment.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
:::info
|
:::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.
|
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
|
4. Verify if ToolJet is running
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
kubectl get pods
|
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.
|
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:
|
Examples:
|
||||||
Application load balancing on Amazon EKS: https://docs.aws.amazon.com/eks/latest/userguide/alb-ingress.html
|
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
|
GKE Ingress for HTTP(S) Load Balancing: https://cloud.google.com/kubernetes-engine/docs/concepts/ingress
|
||||||
|
|
||||||
:::tip
|
:::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).
|
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`.
|
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 />
|
<br />
|
||||||
The Google sign-in button will now be available in your ToolJet login screen.
|
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.
|
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"/>
|
<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:
|
The main components of an app:
|
||||||
|
|
||||||
- **Widgets** - UI components such as tables, buttons, dropdowns.
|
- **[Widgets](https://docs.tooljet.io/docs/tutorial/adding-widget)** - UI components such as tables, buttons, dropdowns.
|
||||||
- **Data sources** - ToolJet can connect to databases, APIs and external services to fetch and modify data.
|
- **[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** - Queries are used to access the connected datasources.
|
- **[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
|
||||||
|
|
||||||
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
|
:::tip
|
||||||
Widgets can be shown on desktop, mobile, or both.
|
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
|
:::tip
|
||||||
Width of the widgets will be automatically adjusted to fit the screen while viewing the application in app viewer.
|
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"/>
|
<img class="screenshot-full" src="/img/widgets/qr-scanner/qr-scanner.jpeg" alt="ToolJet - QR Scanner" height="420"/>
|
||||||
|
|
||||||
#### Known issue:
|
#### 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
|
## Exposed variables
|
||||||
#### lastDetectedValue
|
#### lastDetectedValue
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ module.exports = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
copyright: `Copyright © ${new Date().getFullYear()} ToolJet.`,
|
copyright: `Copyright © ${new Date().getFullYear()} ToolJet Solutions, Inc.`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
presets: [
|
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_HOST="${SERVER_HOST:=server}"
|
||||||
export SERVER_USER="${SERVER_USER:=root}"
|
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
|
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 '@/_styles/theme.scss';
|
||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from 'react-toastify';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
import { ManageGroupPermissions } from '@/ManageGroupPermissions';
|
||||||
import { ManageOrgUsers } from '@/ManageOrgUsers';
|
import { ManageOrgUsers } from '@/ManageOrgUsers';
|
||||||
|
import { ManageGroupPermissionResources } from '@/ManageGroupPermissionResources';
|
||||||
import { SettingsPage } from '../SettingsPage/SettingsPage';
|
import { SettingsPage } from '../SettingsPage/SettingsPage';
|
||||||
import { OnboardingModal } from '@/Onboarding/OnboardingModal';
|
import { OnboardingModal } from '@/Onboarding/OnboardingModal';
|
||||||
import { ForgotPassword } from '@/ForgotPassword';
|
import { ForgotPassword } from '@/ForgotPassword';
|
||||||
|
|
@ -134,6 +136,20 @@ class App extends React.Component {
|
||||||
switchDarkMode={this.switchDarkMode}
|
switchDarkMode={this.switchDarkMode}
|
||||||
darkMode={darkMode}
|
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
|
<PrivateRoute
|
||||||
exact
|
exact
|
||||||
path="/settings"
|
path="/settings"
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { QrScanner } from './Components/QrScanner/QrScanner';
|
||||||
import { ToggleSwitch } from './Components/Toggle';
|
import { ToggleSwitch } from './Components/Toggle';
|
||||||
import { RadioButton } from './Components/RadioButton';
|
import { RadioButton } from './Components/RadioButton';
|
||||||
import { StarRating } from './Components/StarRating';
|
import { StarRating } from './Components/StarRating';
|
||||||
|
import { Divider } from './Components/Divider';
|
||||||
import { renderTooltip } from '../_helpers/appUtils';
|
import { renderTooltip } from '../_helpers/appUtils';
|
||||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||||
import '@/_styles/custom.scss';
|
import '@/_styles/custom.scss';
|
||||||
|
|
@ -46,6 +47,7 @@ const AllComponents = {
|
||||||
ToggleSwitch,
|
ToggleSwitch,
|
||||||
RadioButton,
|
RadioButton,
|
||||||
StarRating,
|
StarRating,
|
||||||
|
Divider,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Box = function Box({
|
export const Box = function Box({
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ export function makeOverlay(style) {
|
||||||
var ch;
|
var ch;
|
||||||
if (stream.match('{{')) {
|
if (stream.match('{{')) {
|
||||||
while ((ch = stream.next()) != null)
|
while ((ch = stream.next()) != null)
|
||||||
if (ch == '}' && stream.next() == '}') {
|
if (ch === '}' && stream.next() === '}') {
|
||||||
stream.eat('}');
|
stream.eat('}');
|
||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||||
var tinycolor = require('tinycolor2');
|
var tinycolor = require('tinycolor2');
|
||||||
|
|
||||||
export const Button = function Button({ id, width, height, component, onComponentClick, currentState }) {
|
export const Button = function Button({ id, width, height, component, onComponentClick, currentState }) {
|
||||||
console.log('currentState', currentState);
|
|
||||||
|
|
||||||
const [loadingState, setLoadingState] = useState(false);
|
const [loadingState, setLoadingState] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -51,7 +49,7 @@ export const Button = function Button({ id, width, height, component, onComponen
|
||||||
onComponentClick(id, component);
|
onComponentClick(id, component);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{text}
|
{resolveReferences(text, currentState)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,12 @@ export const Checkbox = function Checkbox({
|
||||||
onComponentOptionChanged,
|
onComponentOptionChanged,
|
||||||
onEvent,
|
onEvent,
|
||||||
}) {
|
}) {
|
||||||
|
const [checked, setChecked] = React.useState(false);
|
||||||
const label = component.definition.properties.label.value;
|
const label = component.definition.properties.label.value;
|
||||||
const textColorProperty = component.definition.styles.textColor;
|
const textColorProperty = component.definition.styles.textColor;
|
||||||
const textColor = textColorProperty ? textColorProperty.value : '#000';
|
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 widgetVisibility = component.definition.styles?.visibility?.value ?? true;
|
||||||
const disabledState = component.definition.styles?.disabledState?.value ?? false;
|
const disabledState = component.definition.styles?.disabledState?.value ?? false;
|
||||||
|
|
||||||
|
|
@ -29,9 +32,10 @@ export const Checkbox = function Checkbox({
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleValue(e) {
|
function toggleValue(e) {
|
||||||
const checked = e.target.checked;
|
const isChecked = e.target.checked;
|
||||||
onComponentOptionChanged(component, 'value', checked);
|
setChecked(isChecked);
|
||||||
if (checked) {
|
onComponentOptionChanged(component, 'value', isChecked);
|
||||||
|
if (isChecked) {
|
||||||
onEvent('onCheck', { component });
|
onEvent('onCheck', { component });
|
||||||
} else {
|
} else {
|
||||||
onEvent('onUnCheck', { component });
|
onEvent('onUnCheck', { component });
|
||||||
|
|
@ -48,18 +52,21 @@ export const Checkbox = function Checkbox({
|
||||||
onComponentClick(id, component);
|
onComponentClick(id, component);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<label className="my-auto mx-2 form-check form-check-inline">
|
<div className="col px-1 py-0 mt-0">
|
||||||
<input
|
<label className="mx-1 form-check form-check-inline">
|
||||||
className="form-check-input"
|
<input
|
||||||
type="checkbox"
|
className="form-check-input"
|
||||||
onClick={(e) => {
|
type="checkbox"
|
||||||
toggleValue(e);
|
onClick={(e) => {
|
||||||
}}
|
toggleValue(e);
|
||||||
/>
|
}}
|
||||||
<span className="form-check-label" style={{ color: textColor }}>
|
style={{ backgroundColor: checked ? `${checkboxColor}` : 'white', marginTop: '1px' }}
|
||||||
{label}
|
/>
|
||||||
</span>
|
<span className="form-check-label" style={{ color: textColor }}>
|
||||||
</label>
|
{label}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</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 */
|
/* eslint-disable react/no-string-refs */
|
||||||
import React from 'react';
|
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 'draft-js/dist/Draft.css';
|
||||||
import { stateToHTML } from 'draft-js-export-html';
|
import { stateToHTML } from 'draft-js-export-html';
|
||||||
|
|
||||||
|
|
@ -127,7 +127,7 @@ const InlineStyleControls = (props) => {
|
||||||
class DraftEditor extends React.Component {
|
class DraftEditor extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(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.focus = () => this.refs.editor.focus();
|
||||||
this.onChange = (editorState) => {
|
this.onChange = (editorState) => {
|
||||||
|
|
@ -142,6 +142,18 @@ class DraftEditor extends React.Component {
|
||||||
this.toggleInlineStyle = this._toggleInlineStyle.bind(this);
|
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) {
|
_handleKeyCommand(command, editorState) {
|
||||||
const newState = RichUtils.handleKeyCommand(editorState, command);
|
const newState = RichUtils.handleKeyCommand(editorState, command);
|
||||||
if (newState) {
|
if (newState) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import Button from 'react-bootstrap/Button';
|
||||||
import { SubCustomDragLayer } from '../SubCustomDragLayer';
|
import { SubCustomDragLayer } from '../SubCustomDragLayer';
|
||||||
import { SubContainer } from '../SubContainer';
|
import { SubContainer } from '../SubContainer';
|
||||||
import { ConfigHandle } from '../ConfigHandle';
|
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 }) {
|
export const Modal = function Modal({ id, component, height, containerProps, currentState, darkMode }) {
|
||||||
const [show, showModal] = useState(false);
|
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} />
|
<ConfigHandle id={id} component={component} configHandleClicked={containerProps.onComponentClick} />
|
||||||
)}
|
)}
|
||||||
<BootstrapModal.Header>
|
<BootstrapModal.Header>
|
||||||
<BootstrapModal.Title>{title}</BootstrapModal.Title>
|
<BootstrapModal.Title>{resolveWidgetFieldValue(title, currentState)}</BootstrapModal.Title>
|
||||||
<div>
|
<div>
|
||||||
<Button variant={darkMode ? 'secondary' : 'light'} size="sm" onClick={hideModal}>
|
<Button variant={darkMode ? 'secondary' : 'light'} size="sm" onClick={hideModal}>
|
||||||
x
|
x
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ import { resolveReferences, resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||||
export const QrScanner = function QrScanner({ component, onEvent, onComponentOptionChanged, currentState }) {
|
export const QrScanner = function QrScanner({ component, onEvent, onComponentOptionChanged, currentState }) {
|
||||||
const handleError = async (errorMessage) => {
|
const handleError = async (errorMessage) => {
|
||||||
console.log(errorMessage);
|
console.log(errorMessage);
|
||||||
setErrorOccured(true);
|
await setErrorOccured(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleScan = async (data) => {
|
const handleScan = async (data) => {
|
||||||
if (data != null) {
|
if (data !== null || data !== undefined) {
|
||||||
onEvent('onDetect', { component, data: data });
|
await onEvent('onDetect', { component, data: data });
|
||||||
onComponentOptionChanged(component, 'lastDetectedValue', data);
|
await onComponentOptionChanged(component, 'lastDetectedValue', data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,20 +85,21 @@ export const RadioButton = function RadioButton({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-disabled={parsedDisabledState}
|
data-disabled={parsedDisabledState}
|
||||||
className="row"
|
className="row py-1"
|
||||||
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
|
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onComponentClick(id, component);
|
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}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
<div className="col py-1">
|
<div className="col px-1 py-0 mt-0">
|
||||||
{selectOptions.map((option, index) => (
|
{selectOptions.map((option, index) => (
|
||||||
<label key={index} className="form-check form-check-inline">
|
<label key={index} className="form-check form-check-inline">
|
||||||
<input
|
<input
|
||||||
|
style={{ marginTop: '1px' }}
|
||||||
className="form-check-input"
|
className="form-check-input"
|
||||||
checked={value === option.value}
|
checked={value === option.value}
|
||||||
type="radio"
|
type="radio"
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export const RichTextEditor = function RichTextEditor({
|
||||||
const placeholder = component.definition.properties.placeholder.value;
|
const placeholder = component.definition.properties.placeholder.value;
|
||||||
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
|
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
|
||||||
const disabledState = component.definition.styles?.disabledState?.value ?? false;
|
const disabledState = component.definition.styles?.disabledState?.value ?? false;
|
||||||
|
const defaultValue = component.definition.properties?.defaultValue?.value ?? '';
|
||||||
|
|
||||||
const parsedDisabledState =
|
const parsedDisabledState =
|
||||||
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
|
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
|
||||||
|
|
@ -40,7 +41,13 @@ export const RichTextEditor = function RichTextEditor({
|
||||||
onComponentClick(id, component);
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -104,25 +104,27 @@ export const StarRating = function StarRating({
|
||||||
<span className="label form-check-label form-check-label col-auto" style={{ color: labelColor }}>
|
<span className="label form-check-label form-check-label col-auto" style={{ color: labelColor }}>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
{animatedStars.map((props, index) => (
|
<div className="col px-1 py-0 mt-0">
|
||||||
<Star
|
{animatedStars.map((props, index) => (
|
||||||
tooltip={getTooltip(index)}
|
<Star
|
||||||
active={getActive(index)}
|
tooltip={getTooltip(index)}
|
||||||
isHalfStar={isHalfStar(index)}
|
active={getActive(index)}
|
||||||
maxRating={maxRating}
|
isHalfStar={isHalfStar(index)}
|
||||||
onClick={(e, idx) => {
|
maxRating={maxRating}
|
||||||
e.stopPropagation();
|
onClick={(e, idx) => {
|
||||||
setRatingIndex(idx);
|
e.stopPropagation();
|
||||||
handleClick(idx);
|
setRatingIndex(idx);
|
||||||
}}
|
handleClick(idx);
|
||||||
allowHalfStar={allowHalfStar}
|
}}
|
||||||
key={index}
|
allowHalfStar={allowHalfStar}
|
||||||
index={index}
|
key={index}
|
||||||
color={color}
|
index={index}
|
||||||
style={{ ...props }}
|
color={color}
|
||||||
setHoverIndex={setHoverIndex}
|
style={{ ...props }}
|
||||||
/>
|
setHoverIndex={setHoverIndex}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ const Star = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
function roundValueToPrecision(value, precision) {
|
function roundValueToPrecision(value, precision) {
|
||||||
if (value == null) {
|
if (value === null || value === undefined) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -630,6 +630,7 @@ export function Table({
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
marginLeft: 10,
|
marginLeft: 10,
|
||||||
}}
|
}}
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
@ -820,7 +821,7 @@ export function Table({
|
||||||
<tr
|
<tr
|
||||||
key={index}
|
key={index}
|
||||||
className={`table-row ${
|
className={`table-row ${
|
||||||
highlightSelectedRow && row.id == componentState.selectedRowId ? 'selected' : ''
|
highlightSelectedRow && row.id === componentState.selectedRowId ? 'selected' : ''
|
||||||
}`}
|
}`}
|
||||||
{...row.getRowProps()}
|
{...row.getRowProps()}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ class Switch extends React.Component {
|
||||||
return (
|
return (
|
||||||
<label className="form-switch form-check-inline">
|
<label className="form-switch form-check-inline">
|
||||||
<input
|
<input
|
||||||
style={{ backgroundColor: on ? `${color}` : 'white' }}
|
style={{ backgroundColor: on ? `${color}` : 'white', marginTop: '0px' }}
|
||||||
disabled={disabledState}
|
disabled={disabledState}
|
||||||
className="form-check-input"
|
className="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
@ -61,7 +61,7 @@ export const ToggleSwitch = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="row"
|
className="row py-1"
|
||||||
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
|
style={{ width, height, display: parsedWidgetVisibility ? '' : 'none' }}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
@ -71,7 +71,7 @@ export const ToggleSwitch = ({
|
||||||
<span className="form-check-label form-check-label col-auto my-auto" style={{ color: textColor }}>
|
<span className="form-check-label form-check-label col-auto my-auto" style={{ color: textColor }}>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
<div className="col px-1 py-0 my-auto">
|
<div className="col px-1 py-0 mt-0">
|
||||||
<Switch
|
<Switch
|
||||||
disabledState={parsedDisabledState}
|
disabledState={parsedDisabledState}
|
||||||
on={on}
|
on={on}
|
||||||
|
|
|
||||||
|
|
@ -336,7 +336,7 @@ export const componentTypes = [
|
||||||
showOnMobile: { value: false },
|
showOnMobile: { value: false },
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
value: { value: '' },
|
value: { value: '99' },
|
||||||
placeholder: { value: '0' },
|
placeholder: { value: '0' },
|
||||||
},
|
},
|
||||||
events: [],
|
events: [],
|
||||||
|
|
@ -385,7 +385,7 @@ export const componentTypes = [
|
||||||
customRule: { value: null },
|
customRule: { value: null },
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
defaultValue: { value: '' },
|
defaultValue: { value: '01/04/2021' },
|
||||||
format: { value: 'DD/MM/YYYY' },
|
format: { value: 'DD/MM/YYYY' },
|
||||||
enableTime: { value: '{{false}}' },
|
enableTime: { value: '{{false}}' },
|
||||||
enableDate: { value: '{{true}}' },
|
enableDate: { value: '{{true}}' },
|
||||||
|
|
@ -419,6 +419,7 @@ export const componentTypes = [
|
||||||
},
|
},
|
||||||
styles: {
|
styles: {
|
||||||
textColor: { type: 'color', displayName: 'Text Color' },
|
textColor: { type: 'color', displayName: 'Text Color' },
|
||||||
|
checkboxColor: { type: 'color', displayName: 'Checkbox Color' },
|
||||||
visibility: { type: 'code', displayName: 'Visibility' },
|
visibility: { type: 'code', displayName: 'Visibility' },
|
||||||
disabledState: { type: 'code', displayName: 'Disable' },
|
disabledState: { type: 'code', displayName: 'Disable' },
|
||||||
},
|
},
|
||||||
|
|
@ -434,6 +435,7 @@ export const componentTypes = [
|
||||||
events: [],
|
events: [],
|
||||||
styles: {
|
styles: {
|
||||||
textColor: { value: '#000' },
|
textColor: { value: '#000' },
|
||||||
|
checkboxColor: { value: '#3c92dc' },
|
||||||
visibility: { value: '{{true}}' },
|
visibility: { value: '{{true}}' },
|
||||||
disabledState: { value: '{{false}}' },
|
disabledState: { value: '{{false}}' },
|
||||||
},
|
},
|
||||||
|
|
@ -561,7 +563,10 @@ export const componentTypes = [
|
||||||
showOnMobile: { value: false },
|
showOnMobile: { value: false },
|
||||||
},
|
},
|
||||||
properties: {
|
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' },
|
placeholder: { value: 'Placeholder text' },
|
||||||
},
|
},
|
||||||
events: [],
|
events: [],
|
||||||
|
|
@ -643,7 +648,7 @@ export const componentTypes = [
|
||||||
properties: {
|
properties: {
|
||||||
text: { value: 'Text goes here !' },
|
text: { value: 'Text goes here !' },
|
||||||
visible: { value: true },
|
visible: { value: true },
|
||||||
loadingState: { value: false },
|
loadingState: { value: `{{false}}` },
|
||||||
},
|
},
|
||||||
events: [],
|
events: [],
|
||||||
styles: {
|
styles: {
|
||||||
|
|
@ -847,6 +852,7 @@ export const componentTypes = [
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
placeholder: { type: 'code', displayName: 'Placeholder' },
|
placeholder: { type: 'code', displayName: 'Placeholder' },
|
||||||
|
defaultValue: { type: 'code', displayName: 'Default Value' },
|
||||||
},
|
},
|
||||||
events: {},
|
events: {},
|
||||||
styles: {
|
styles: {
|
||||||
|
|
@ -854,7 +860,7 @@ export const componentTypes = [
|
||||||
disabledState: { type: 'code', displayName: 'Disable' },
|
disabledState: { type: 'code', displayName: 'Disable' },
|
||||||
},
|
},
|
||||||
exposedVariables: {
|
exposedVariables: {
|
||||||
value: {},
|
value: '',
|
||||||
},
|
},
|
||||||
definition: {
|
definition: {
|
||||||
others: {
|
others: {
|
||||||
|
|
@ -863,6 +869,7 @@ export const componentTypes = [
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
placeholder: { value: 'Placeholder text' },
|
placeholder: { value: 'Placeholder text' },
|
||||||
|
defaultValue: { value: '' },
|
||||||
},
|
},
|
||||||
events: [],
|
events: [],
|
||||||
styles: {
|
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;
|
deltaY = delta.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentData = item.component;
|
|
||||||
left = Math.round(currentLayoutOptions.left + deltaX);
|
left = Math.round(currentLayoutOptions.left + deltaX);
|
||||||
top = Math.round(currentLayoutOptions.top + deltaY);
|
top = Math.round(currentLayoutOptions.top + deltaY);
|
||||||
|
|
||||||
|
|
@ -311,7 +310,12 @@ export const Container = ({
|
||||||
{Object.keys(boxes).length === 0 && !appLoading && !isDragging && (
|
{Object.keys(boxes).length === 0 && !appLoading && !isDragging && (
|
||||||
<div className="mx-auto w-50 p-5 bg-light no-components-box" style={{ marginTop: '10%' }}>
|
<div className="mx-auto w-50 p-5 bg-light no-components-box" style={{ marginTop: '10%' }}>
|
||||||
<center className="text-muted">
|
<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>
|
</center>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ function getItemStyles(delta, item, initialOffset, currentOffset, currentLayout)
|
||||||
display: 'none',
|
display: 'none',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let { x, y } = currentOffset;
|
let x, y;
|
||||||
|
|
||||||
let id = item.id;
|
let id = item.id;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ class DataSourceManager extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.selectedDataSource != this.props.selectedDataSource) {
|
if (prevProps.selectedDataSource !== this.props.selectedDataSource) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedDataSource: this.props.selectedDataSource,
|
selectedDataSource: this.props.selectedDataSource,
|
||||||
options: this.props.selectedDataSource?.options,
|
options: this.props.selectedDataSource?.options,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export const TestConnection = ({ kind, options, onConnectionTestFailed }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isTesting) {
|
if (isTesting) {
|
||||||
setButtonText('Testing connection...');
|
setButtonText('Testing connection...');
|
||||||
} else if (!isTesting && connectionStatus === 'success') {
|
} else if (connectionStatus === 'success') {
|
||||||
setButtonText('Connection verified');
|
setButtonText('Connection verified');
|
||||||
} else {
|
} else {
|
||||||
setButtonText('Test Connection');
|
setButtonText('Test Connection');
|
||||||
|
|
@ -54,7 +54,7 @@ export const TestConnection = ({ kind, options, onConnectionTestFailed }) => {
|
||||||
<Button
|
<Button
|
||||||
className="m-2"
|
className="m-2"
|
||||||
variant="success"
|
variant="success"
|
||||||
disabled={isTesting || (!isTesting && !(connectionStatus !== 'success'))}
|
disabled={isTesting || connectionStatus === 'success'}
|
||||||
onClick={testDataSource}
|
onClick={testDataSource}
|
||||||
>
|
>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ class Editor extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
switchSidebarTab = (tabIndex) => {
|
switchSidebarTab = (tabIndex) => {
|
||||||
if (tabIndex == 2) {
|
if (tabIndex === 2) {
|
||||||
this.setState({ selectedComponent: null });
|
this.setState({ selectedComponent: null });
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|
@ -575,7 +575,7 @@ class Editor extends React.Component {
|
||||||
value={this.state.app.name}
|
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">
|
<div className="editor-buttons">
|
||||||
<span
|
<span
|
||||||
className={`btn btn-light mx-2`}
|
className={`btn btn-light mx-2`}
|
||||||
|
|
@ -864,6 +864,7 @@ class Editor extends React.Component {
|
||||||
componentTypes={componentTypes}
|
componentTypes={componentTypes}
|
||||||
zoomLevel={zoomLevel}
|
zoomLevel={zoomLevel}
|
||||||
currentLayout={currentLayout}
|
currentLayout={currentLayout}
|
||||||
|
darkMode={this.props.darkMode}
|
||||||
></WidgetManager>
|
></WidgetManager>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,29 @@ export const EventManager = ({
|
||||||
return { name: action.name, value: action.id };
|
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 || [];
|
excludeEvents = excludeEvents || [];
|
||||||
|
|
||||||
/* Filter events based on excludesEvents ( a list of event ids to exclude ) */
|
/* Filter events based on excludesEvents ( a list of event ids to exclude ) */
|
||||||
|
|
@ -53,8 +76,8 @@ export const EventManager = ({
|
||||||
function getAllApps() {
|
function getAllApps() {
|
||||||
let appsOptionsList = [];
|
let appsOptionsList = [];
|
||||||
apps
|
apps
|
||||||
.filter((item) => item.slug != undefined)
|
.filter((item) => item.slug !== undefined)
|
||||||
.map((item) => {
|
.forEach((item) => {
|
||||||
appsOptionsList.push({
|
appsOptionsList.push({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
value: item.slug,
|
value: item.slug,
|
||||||
|
|
@ -128,16 +151,31 @@ export const EventManager = ({
|
||||||
<div className="hr-text">Action options</div>
|
<div className="hr-text">Action options</div>
|
||||||
<div>
|
<div>
|
||||||
{event.actionId === 'show-alert' && (
|
{event.actionId === 'show-alert' && (
|
||||||
<div className="row">
|
<>
|
||||||
<div className="col-3 p-2">Message</div>
|
<div className="row">
|
||||||
<div className="col-9">
|
<div className="col-3 p-2">Message</div>
|
||||||
<CodeHinter
|
<div className="col-9">
|
||||||
currentState={currentState}
|
<CodeHinter
|
||||||
initialValue={event.message}
|
currentState={currentState}
|
||||||
onChange={(value) => handlerChanged(index, 'message', value)}
|
initialValue={event.message}
|
||||||
/>
|
onChange={(value) => handlerChanged(index, 'message', value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</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' && (
|
{event.actionId === 'open-webpage' && (
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export const LeftSidebarDebugger = ({ darkMode, errors }) => {
|
||||||
])(errors);
|
])(errors);
|
||||||
|
|
||||||
const errorData = [];
|
const errorData = [];
|
||||||
Object.entries(newError).map(([key, value]) => {
|
Object.entries(newError).forEach(([key, value]) => {
|
||||||
const variableNames = {
|
const variableNames = {
|
||||||
options: '',
|
options: '',
|
||||||
response: '',
|
response: '',
|
||||||
|
|
@ -46,6 +46,10 @@ export const LeftSidebarDebugger = ({ darkMode, errors }) => {
|
||||||
variableNames.options = 'substitutedVariables';
|
variableNames.options = 'substitutedVariables';
|
||||||
variableNames.response = 'response';
|
variableNames.response = 'response';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'transformations':
|
||||||
|
variableNames.response = 'data';
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
'options';
|
'options';
|
||||||
}
|
}
|
||||||
|
|
@ -168,27 +172,28 @@ function ErrorLogsComponent({ errorProps, idx, darkMode }) {
|
||||||
height="16"
|
height="16"
|
||||||
/>
|
/>
|
||||||
[{_.capitalize(errorProps.type)} {errorProps.key}]
|
[{_.capitalize(errorProps.type)} {errorProps.key}]
|
||||||
<span className="text-red">
|
<span className="text-red">{`${_.startCase(errorProps.type)} Failed: ${errorProps.message}`} .</span>
|
||||||
{`Query Failed: ${errorProps.description}`} {errorProps.message}.
|
|
||||||
</span>
|
|
||||||
<br />
|
<br />
|
||||||
<small className="text-muted px-1">{moment(errorProps.timestamp).fromNow()}</small>
|
<small className="text-muted px-1">{moment(errorProps.timestamp).fromNow()}</small>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className={` queryData ${open ? 'open' : 'close'} py-0`}>
|
<div className={` queryData ${open ? 'open' : 'close'} py-0`}>
|
||||||
<span>
|
{errorProps.type === 'query' && (
|
||||||
<ReactJson
|
<span>
|
||||||
src={errorProps.options.data}
|
<ReactJson
|
||||||
theme={darkMode ? 'shapeshifter' : 'rjv-default'}
|
src={errorProps.options.data}
|
||||||
name={errorProps.options.name}
|
theme={darkMode ? 'shapeshifter' : 'rjv-default'}
|
||||||
style={{ fontSize: '0.7rem', paddingLeft: '0.17rem' }}
|
name={errorProps.options.name}
|
||||||
enableClipboard={false}
|
style={{ fontSize: '0.7rem', paddingLeft: '0.17rem' }}
|
||||||
displayDataTypes={false}
|
enableClipboard={false}
|
||||||
collapsed={true}
|
displayDataTypes={false}
|
||||||
displayObjectSize={false}
|
collapsed={true}
|
||||||
quotesOnKeys={false}
|
displayObjectSize={false}
|
||||||
sortKeys={false}
|
quotesOnKeys={false}
|
||||||
/>
|
sortKeys={false}
|
||||||
</span>
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span>
|
<span>
|
||||||
<ReactJson
|
<ReactJson
|
||||||
src={errorProps.response.data}
|
src={errorProps.response.data}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export const SidebarPinnedButton = ({ state, component, updateState, darkMode })
|
||||||
return (
|
return (
|
||||||
<SidebarPinnedButton.OverlayContainer tip={tooltipMsg}>
|
<SidebarPinnedButton.OverlayContainer tip={tooltipMsg}>
|
||||||
<div
|
<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'
|
component === 'Inspector' && 'position-absolute end-0'
|
||||||
}`}
|
}`}
|
||||||
onClick={updateState}
|
onClick={updateState}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import '@/_styles/left-sidebar.scss';
|
import '@/_styles/left-sidebar.scss';
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { LeftSidebarItem } from './sidebar-item';
|
import { LeftSidebarItem } from './sidebar-item';
|
||||||
import { LeftSidebarInspector } from './sidebar-inspector';
|
import { LeftSidebarInspector } from './sidebar-inspector';
|
||||||
|
|
@ -9,6 +9,7 @@ import { LeftSidebarZoom } from './sidebar-zoom';
|
||||||
import { DarkModeToggle } from '../../_components/DarkModeToggle';
|
import { DarkModeToggle } from '../../_components/DarkModeToggle';
|
||||||
import useRouter from '../../_hooks/use-router';
|
import useRouter from '../../_hooks/use-router';
|
||||||
import { LeftSidebarDebugger } from './SidebarDebugger';
|
import { LeftSidebarDebugger } from './SidebarDebugger';
|
||||||
|
import { ConfirmDialog } from '@/_components';
|
||||||
|
|
||||||
export const LeftSidebar = ({
|
export const LeftSidebar = ({
|
||||||
appId,
|
appId,
|
||||||
|
|
@ -23,6 +24,7 @@ export const LeftSidebar = ({
|
||||||
errorLogs,
|
errorLogs,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [showLeaveDialog, setShowLeaveDialog] = useState(false);
|
||||||
return (
|
return (
|
||||||
<div className="left-sidebar">
|
<div className="left-sidebar">
|
||||||
<LeftSidebarInspector darkMode={darkMode} globals={globals} components={components} queries={queries} />
|
<LeftSidebarInspector darkMode={darkMode} globals={globals} components={components} queries={queries} />
|
||||||
|
|
@ -34,11 +36,17 @@ export const LeftSidebar = ({
|
||||||
/>
|
/>
|
||||||
<LeftSidebarDebugger darkMode={darkMode} components={components} errors={errorLogs} />
|
<LeftSidebarDebugger darkMode={darkMode} components={components} errors={errorLogs} />
|
||||||
<LeftSidebarItem
|
<LeftSidebarItem
|
||||||
onClick={() => router.push('/')}
|
onClick={() => setShowLeaveDialog(true)}
|
||||||
tip="Back to home"
|
tip="Back to home"
|
||||||
icon="back"
|
icon="back"
|
||||||
className="left-sidebar-item no-border"
|
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">
|
<div className="left-sidebar-stack-bottom">
|
||||||
<LeftSidebarZoom onZoomChanged={onZoomChanged} />
|
<LeftSidebarZoom onZoomChanged={onZoomChanged} />
|
||||||
<div className="left-sidebar-item no-border">
|
<div className="left-sidebar-item no-border">
|
||||||
|
|
|
||||||
|
|
@ -297,7 +297,7 @@ class ManageAppUsers extends React.Component {
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
|
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<a href="/users" target="_blank">
|
<a href="/users" target="_blank" className="btn btn-outline-azure mt-3">
|
||||||
Manage Organization Users
|
Manage Organization Users
|
||||||
</a>
|
</a>
|
||||||
</Modal.Footer>
|
</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 { changeOption } from '../utils';
|
||||||
import { CodeHinter } from '../../../CodeBuilder/CodeHinter';
|
import { CodeHinter } from '../../../CodeBuilder/CodeHinter';
|
||||||
|
import { BaseUrl } from './BaseUrl';
|
||||||
|
|
||||||
class Restapi extends React.Component {
|
class Restapi extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
|
@ -100,21 +101,7 @@ class Restapi extends React.Component {
|
||||||
|
|
||||||
<div className="col" style={{ display: 'flex' }}>
|
<div className="col" style={{ display: 'flex' }}>
|
||||||
{dataSourceURL && (
|
{dataSourceURL && (
|
||||||
<span
|
<BaseUrl theme={this.props.darkMode ? 'monokai' : 'default'} dataSourceURL={dataSourceURL} />
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
<CodeHinter
|
<CodeHinter
|
||||||
currentState={this.props.currentState}
|
currentState={this.props.currentState}
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ class SaveAndPreview extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
|
{!versions?.length && !showVersionForm && !isLoading && <div>No versions yet.</div>}
|
||||||
<table className="table table-vcenter">
|
<table className="table table-vcenter">
|
||||||
<tbody>
|
<tbody>
|
||||||
{versions.map((version) => (
|
{versions.map((version) => (
|
||||||
|
|
@ -177,7 +178,7 @@ class SaveAndPreview extends React.Component {
|
||||||
<button
|
<button
|
||||||
className="btn btn btn-sm mx-2 text-muted"
|
className="btn btn btn-sm mx-2 text-muted"
|
||||||
onClick={() => this.props.setAppDefinitionFromVersion(version)}
|
onClick={() => this.props.setAppDefinitionFromVersion(version)}
|
||||||
disabled={this.props.editingVersionId == version.id}
|
disabled={this.props.editingVersionId === version.id}
|
||||||
>
|
>
|
||||||
edit
|
edit
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -305,7 +305,13 @@ export const SubContainer = ({
|
||||||
|
|
||||||
{Object.keys(boxes).length === 0 && !appLoading && !isDragging && (
|
{Object.keys(boxes).length === 0 && !appLoading && !isDragging && (
|
||||||
<div className="mx-auto mt-5 w-50 p-5 bg-light no-components-box">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{appLoading && (
|
{appLoading && (
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ function getItemStyles(delta, item, initialOffset, currentOffset, parentRef, par
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let { x, y } = currentOffset;
|
let x, y;
|
||||||
|
|
||||||
let id = item.id;
|
let id = item.id;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ import { DraggableBox } from './DraggableBox';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import { isEmpty } from 'lodash';
|
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);
|
const [filteredComponents, setFilteredComponents] = useState(componentTypes);
|
||||||
|
|
||||||
function filterComponents(value) {
|
function filterComponents(value) {
|
||||||
if (value != '') {
|
if (value !== '') {
|
||||||
const fuse = new Fuse(componentTypes, { keys: ['component'] });
|
const fuse = new Fuse(componentTypes, { keys: ['component'] });
|
||||||
const results = fuse.search(value);
|
const results = fuse.search(value);
|
||||||
setFilteredComponents(results.map((result) => result.item));
|
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="" />
|
<img src="./static/illustrations/undraw_printing_invoices_5r4r.svg" height="128" alt="" />
|
||||||
</div> */}
|
</div> */}
|
||||||
<p className="empty-title">No results found</p>
|
<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.
|
Try adjusting your search or filter to find what you're looking for.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -71,7 +71,7 @@ export const WidgetManager = function WidgetManager({ componentTypes, zoomLevel,
|
||||||
];
|
];
|
||||||
const integrationItems = ['Map'];
|
const integrationItems = ['Map'];
|
||||||
|
|
||||||
filteredComponents.map((f) => {
|
filteredComponents.forEach((f) => {
|
||||||
if (commonItems.includes(f.name)) commonSection.items.push(f);
|
if (commonItems.includes(f.name)) commonSection.items.push(f);
|
||||||
if (formItems.includes(f.name)) formSection.items.push(f);
|
if (formItems.includes(f.name)) formSection.items.push(f);
|
||||||
else if (integrationItems.includes(f.name)) integrationSection.items.push(f);
|
else if (integrationItems.includes(f.name)) integrationSection.items.push(f);
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,12 @@ class ForgotPassword extends React.Component {
|
||||||
|
|
||||||
handleChange = (event) => {
|
handleChange = (event) => {
|
||||||
this.setState({ [event.target.name]: event.target.value });
|
this.setState({ [event.target.name]: event.target.value });
|
||||||
if (event.target.value == '') {
|
if (event.target.value === '') {
|
||||||
this.setState({ isEmailFound: false, buttonClicked: false });
|
this.setState({ isEmailFound: false, buttonClicked: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleClick = (event) => {
|
handleClick = (event) => {
|
||||||
this.setState({ buttonClicked: true });
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
fetch(`${config.apiUrl}/forgot_password`, {
|
fetch(`${config.apiUrl}/forgot_password`, {
|
||||||
|
|
@ -38,6 +37,7 @@ class ForgotPassword extends React.Component {
|
||||||
this.setState({ isEmailFound: true });
|
this.setState({ isEmailFound: true });
|
||||||
return res.json();
|
return res.json();
|
||||||
} else {
|
} else {
|
||||||
|
this.setState({ buttonClicked: true });
|
||||||
this.setState({ isEmailFound: false });
|
this.setState({ isEmailFound: false });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -58,7 +58,7 @@ class ForgotPassword extends React.Component {
|
||||||
<div className="page page-center">
|
<div className="page page-center">
|
||||||
<div className="container-tight py-2">
|
<div className="container-tight py-2">
|
||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<a href=".">
|
<a href="." className="navbar-brand-autodark">
|
||||||
<img src="/assets/images/logo-text.svg" height="30" alt="" />
|
<img src="/assets/images/logo-text.svg" height="30" alt="" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -86,7 +86,7 @@ class ForgotPassword extends React.Component {
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
disabled={!this.state.email}
|
disabled={!this.state.email}
|
||||||
>
|
>
|
||||||
Submit
|
Reset Password
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import Fuse from 'fuse.js';
|
||||||
import { folderService } from '@/_services';
|
import { folderService } from '@/_services';
|
||||||
import { toast } from 'react-toastify';
|
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 [addToFolder, setAddToFolder] = useState(false);
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
|
|
||||||
|
|
@ -70,7 +70,7 @@ export const AppMenu = function AppMenu({ app, folders, foldersChanged, deleteAp
|
||||||
<div>
|
<div>
|
||||||
<div className="field mb-2">
|
<div className="field mb-2">
|
||||||
<span role="button" onClick={() => setAddToFolder(true)}>
|
<span role="button" onClick={() => setAddToFolder(true)}>
|
||||||
Add to folder{' '}
|
Add to folder
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="field mb-2">
|
<div className="field mb-2">
|
||||||
|
|
@ -78,9 +78,14 @@ export const AppMenu = function AppMenu({ app, folders, foldersChanged, deleteAp
|
||||||
Clone app
|
Clone app
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="field mb-2">
|
||||||
|
<span className="field mb-2" role="button" onClick={() => exportApp()}>
|
||||||
|
Export app
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="field mb-2">
|
<div className="field mb-2">
|
||||||
<span className="my-3 text-danger" role="button" onClick={() => deleteApp()}>
|
<span className="my-3 text-danger" role="button" onClick={() => deleteApp()}>
|
||||||
Delete app{' '}
|
Delete app
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -105,6 +110,7 @@ export const AppMenu = function AppMenu({ app, folders, foldersChanged, deleteAp
|
||||||
onChange={(newVal) => {
|
onChange={(newVal) => {
|
||||||
addAppToFolder(app.id, newVal);
|
addAppToFolder(app.id, newVal);
|
||||||
}}
|
}}
|
||||||
|
emptyMessage={folders.length === 0 ? 'No folders present' : 'Not found'}
|
||||||
filterOptions={customFuzzySearch}
|
filterOptions={customFuzzySearch}
|
||||||
placeholder="Select folder"
|
placeholder="Select folder"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,13 @@ export const Folders = function Folders({
|
||||||
const [activeFolder, setActiveFolder] = useState(currentFolder || {});
|
const [activeFolder, setActiveFolder] = useState(currentFolder || {});
|
||||||
|
|
||||||
function saveFolder() {
|
function saveFolder() {
|
||||||
|
if (!newFolderName || !newFolderName.trim()) {
|
||||||
|
toast.warn("folder name can't be empty.", {
|
||||||
|
hideProgressBar: true,
|
||||||
|
position: 'top-left',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
setCreationStatus(true);
|
setCreationStatus(true);
|
||||||
folderService.create(newFolderName).then(() => {
|
folderService.create(newFolderName).then(() => {
|
||||||
toast.info('folder created.', {
|
toast.info('folder created.', {
|
||||||
|
|
@ -90,7 +97,7 @@ export const Folders = function Folders({
|
||||||
))}
|
))}
|
||||||
<hr />
|
<hr />
|
||||||
{!showForm && (
|
{!showForm && (
|
||||||
<a className="mx-3" onClick={() => setShowForm(true)}>
|
<a className="mx-3 fw-500" onClick={() => setShowForm(true)}>
|
||||||
+ Folder
|
+ Folder
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
@ -98,8 +105,6 @@ export const Folders = function Folders({
|
||||||
<div className="p-2 row">
|
<div className="p-2 row">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<input
|
<input
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
onClick={() => onComponentClick(id, component)} //onComponentClick, id and compoenent is not defined
|
|
||||||
type="text"
|
type="text"
|
||||||
onChange={(e) => setNewFolderName(e.target.value)}
|
onChange={(e) => setNewFolderName(e.target.value)}
|
||||||
className="form-control"
|
className="form-control"
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ class HomePage extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.fileInput = React.createRef();
|
||||||
this.state = {
|
this.state = {
|
||||||
currentUser: authenticationService.currentUserValue,
|
currentUser: authenticationService.currentUserValue,
|
||||||
users: null,
|
users: null,
|
||||||
|
|
@ -20,7 +21,10 @@ class HomePage extends React.Component {
|
||||||
creatingApp: false,
|
creatingApp: false,
|
||||||
isDeletingApp: false,
|
isDeletingApp: false,
|
||||||
isCloningApp: false,
|
isCloningApp: false,
|
||||||
|
isExportingApp: false,
|
||||||
|
isImportingApp: false,
|
||||||
currentFolder: {},
|
currentFolder: {},
|
||||||
|
currentPage: 1,
|
||||||
showAppDeletionConfirmation: false,
|
showAppDeletionConfirmation: false,
|
||||||
apps: [],
|
apps: [],
|
||||||
folders: [],
|
folders: [],
|
||||||
|
|
@ -81,10 +85,15 @@ class HomePage extends React.Component {
|
||||||
createApp = () => {
|
createApp = () => {
|
||||||
let _self = this;
|
let _self = this;
|
||||||
_self.setState({ creatingApp: true });
|
_self.setState({ creatingApp: true });
|
||||||
appService.createApp().then((data) => {
|
appService
|
||||||
console.log(data);
|
.createApp()
|
||||||
_self.props.history.push(`/apps/${data.id}`);
|
.then((data) => {
|
||||||
});
|
console.log(data);
|
||||||
|
_self.props.history.push(`/apps/${data.id}`);
|
||||||
|
})
|
||||||
|
.catch(({ error }) => {
|
||||||
|
toast.error(error, { hideProgressBar: true, position: 'top-center' });
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteApp = (app) => {
|
deleteApp = (app) => {
|
||||||
|
|
@ -104,12 +113,95 @@ class HomePage extends React.Component {
|
||||||
this.props.history.push(`/apps/${data.id}`);
|
this.props.history.push(`/apps/${data.id}`);
|
||||||
})
|
})
|
||||||
.catch(({ _error }) => {
|
.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 });
|
this.setState({ isCloningApp: false });
|
||||||
console.log(_error);
|
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 = () => {
|
executeAppDeletion = () => {
|
||||||
this.setState({ isDeletingApp: true });
|
this.setState({ isDeletingApp: true });
|
||||||
appService
|
appService
|
||||||
|
|
@ -129,7 +221,10 @@ class HomePage extends React.Component {
|
||||||
this.fetchFolders();
|
this.fetchFolders();
|
||||||
})
|
})
|
||||||
.catch(({ error }) => {
|
.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({
|
this.setState({
|
||||||
isDeletingApp: false,
|
isDeletingApp: false,
|
||||||
appToBeDeleted: null,
|
appToBeDeleted: null,
|
||||||
|
|
@ -146,13 +241,13 @@ class HomePage extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
apps,
|
apps,
|
||||||
currentUser,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
creatingApp,
|
creatingApp,
|
||||||
meta,
|
meta,
|
||||||
currentFolder,
|
currentFolder,
|
||||||
showAppDeletionConfirmation,
|
showAppDeletionConfirmation,
|
||||||
isDeletingApp,
|
isDeletingApp,
|
||||||
|
isImportingApp,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
return (
|
return (
|
||||||
<div className="wrapper home-page">
|
<div className="wrapper home-page">
|
||||||
|
|
@ -192,18 +287,34 @@ class HomePage extends React.Component {
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-auto ms-auto d-print-none">
|
<div className="col-auto ms-auto d-print-none">
|
||||||
<button
|
<div className="w-100 ">
|
||||||
className={`btn btn-primary d-none d-lg-inline ${creatingApp ? 'btn-loading' : ''}`}
|
<button
|
||||||
onClick={this.createApp}
|
className={`btn btn-default d-none d-lg-inline mb-3 ${isImportingApp ? 'btn-loading' : ''}`}
|
||||||
>
|
onChange={this.handleImportApp}
|
||||||
Create new application
|
>
|
||||||
</button>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={
|
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 d-flex align-items-center'
|
||||||
: 'table-responsive w-100 apps-table mt-3'
|
: 'table-responsive w-100 apps-table mt-3'
|
||||||
}
|
}
|
||||||
|
|
@ -241,16 +352,21 @@ class HomePage extends React.Component {
|
||||||
<td className="col p-3">
|
<td className="col p-3">
|
||||||
<span className="app-title mb-3">{app.name}</span> <br />
|
<span className="app-title mb-3">{app.name}</span> <br />
|
||||||
<small className="pt-2 app-description">
|
<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}{' '}
|
{app.user?.last_name}{' '}
|
||||||
</small>
|
</small>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-muted col-auto pt-4">
|
<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">
|
<Link to={`/apps/${app.id}`} className="d-none d-lg-inline">
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
placement="top"
|
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>
|
<span className="badge bg-green-lt">Edit</span>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
|
|
@ -267,7 +383,7 @@ class HomePage extends React.Component {
|
||||||
renderTooltip({
|
renderTooltip({
|
||||||
props,
|
props,
|
||||||
text:
|
text:
|
||||||
app?.current_version_id == null
|
app?.current_version_id === null
|
||||||
? 'App does not have a deployed version'
|
? 'App does not have a deployed version'
|
||||||
: 'Open in app viewer',
|
: 'Open in app viewer',
|
||||||
})
|
})
|
||||||
|
|
@ -293,7 +409,7 @@ class HomePage extends React.Component {
|
||||||
renderTooltip({
|
renderTooltip({
|
||||||
props,
|
props,
|
||||||
text:
|
text:
|
||||||
app?.current_version_id == null
|
app?.current_version_id === null
|
||||||
? 'App does not have a deployed version'
|
? 'App does not have a deployed version'
|
||||||
: 'Open in app viewer',
|
: 'Open in app viewer',
|
||||||
})
|
})
|
||||||
|
|
@ -302,14 +418,14 @@ class HomePage extends React.Component {
|
||||||
{
|
{
|
||||||
<span
|
<span
|
||||||
className={`${
|
className={`${
|
||||||
app?.current_version_id == null
|
app?.current_version_id === null
|
||||||
? 'badge mx-2 '
|
? 'badge mx-2 '
|
||||||
: 'badge bg-azure-lt mx-2'
|
: 'badge bg-azure-lt mx-2'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
filter:
|
filter:
|
||||||
app?.current_version_id == null
|
app?.current_version_id === null
|
||||||
? 'brightness(0.8)'
|
? 'brightness(0.3)'
|
||||||
: 'brightness(1) invert(1)',
|
: 'brightness(1) invert(1)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -320,19 +436,22 @@ class HomePage extends React.Component {
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<AppMenu
|
{this.isAppDeletable(app) && (
|
||||||
app={app}
|
<AppMenu
|
||||||
folders={this.state.folders}
|
app={app}
|
||||||
foldersChanged={this.foldersChanged}
|
folders={this.state.folders}
|
||||||
deleteApp={() => this.deleteApp(app)}
|
foldersChanged={this.foldersChanged}
|
||||||
cloneApp={() => this.cloneApp(app)}
|
deleteApp={() => this.deleteApp(app)}
|
||||||
/>
|
cloneApp={() => this.cloneApp(app)}
|
||||||
|
exportApp={() => this.exportApp(app)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{currentFolder.count == 0 && (
|
{currentFolder.count === 0 && (
|
||||||
<div>
|
<div>
|
||||||
<img
|
<img
|
||||||
className="mx-auto d-block"
|
className="mx-auto d-block"
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ class LoginPage extends React.Component {
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder="Enter email"
|
placeholder="Email"
|
||||||
data-testid="emailField"
|
data-testid="emailField"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { authenticationService, organizationService, organizationUserService } from '@/_services';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
import { Header } from '@/_components';
|
import { Header } from '@/_components';
|
||||||
import SelectSearch, { fuzzySearch } from 'react-select-search';
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { history } from '@/_helpers';
|
import { history } from '@/_helpers';
|
||||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||||
|
import ReactTooltip from 'react-tooltip';
|
||||||
|
|
||||||
class ManageOrgUsers extends React.Component {
|
class ManageOrgUsers extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
|
@ -17,8 +17,6 @@ class ManageOrgUsers extends React.Component {
|
||||||
showNewUserForm: false,
|
showNewUserForm: false,
|
||||||
creatingUser: false,
|
creatingUser: false,
|
||||||
newUser: {},
|
newUser: {},
|
||||||
role: '',
|
|
||||||
idChangingRole: null,
|
|
||||||
archivingUser: null,
|
archivingUser: null,
|
||||||
fields: {},
|
fields: {},
|
||||||
errors: {},
|
errors: {},
|
||||||
|
|
@ -27,9 +25,10 @@ class ManageOrgUsers extends React.Component {
|
||||||
|
|
||||||
validateEmail(email) {
|
validateEmail(email) {
|
||||||
console.log(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());
|
return re.test(String(email).toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
handleValidation() {
|
handleValidation() {
|
||||||
let fields = this.state.fields;
|
let fields = this.state.fields;
|
||||||
|
|
@ -38,14 +37,14 @@ class ManageOrgUsers extends React.Component {
|
||||||
if (!fields['firstName']) {
|
if (!fields['firstName']) {
|
||||||
errors['firstName'] = 'This field is required';
|
errors['firstName'] = 'This field is required';
|
||||||
} else if (typeof fields['firstName'] !== 'undefined') {
|
} 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';
|
errors['firstName'] = 'Only letters are allowed';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!fields['lastName']) {
|
if (!fields['lastName']) {
|
||||||
errors['lastName'] = 'This field is required';
|
errors['lastName'] = 'This field is required';
|
||||||
} else if (typeof fields['lastName'] !== 'undefined') {
|
} 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';
|
errors['lastName'] = 'Only letters are allowed';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -53,14 +52,11 @@ class ManageOrgUsers extends React.Component {
|
||||||
if (!fields['email']) {
|
if (!fields['email']) {
|
||||||
errors['email'] = 'This field is required';
|
errors['email'] = 'This field is required';
|
||||||
} else if (!this.validateEmail(fields['email'])) {
|
} 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 });
|
this.setState({ errors: errors });
|
||||||
return Object.keys(errors).length === 0;
|
return Object.keys(errors).length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
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) => {
|
archiveOrgUser = (id) => {
|
||||||
this.setState({ archivingUser: id });
|
this.setState({ archivingUser: id });
|
||||||
|
|
||||||
|
|
@ -124,7 +106,9 @@ class ManageOrgUsers extends React.Component {
|
||||||
|
|
||||||
if (this.handleValidation()) {
|
if (this.handleValidation()) {
|
||||||
let fields = {};
|
let fields = {};
|
||||||
Object.keys(fields).map(key => { fields[key] = '' })
|
Object.keys(fields).forEach((key) => {
|
||||||
|
fields[key] = '';
|
||||||
|
});
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
creatingUser: true,
|
creatingUser: true,
|
||||||
|
|
@ -151,12 +135,6 @@ class ManageOrgUsers extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
dropdownVal = (role) => {
|
|
||||||
this.setState({
|
|
||||||
fields: { ...this.state.fields, role },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
logout = () => {
|
logout = () => {
|
||||||
authenticationService.logout();
|
authenticationService.logout();
|
||||||
history.push('/login');
|
history.push('/login');
|
||||||
|
|
@ -169,19 +147,11 @@ class ManageOrgUsers extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { isLoading, showNewUserForm, creatingUser, users, archivingUser } = this.state;
|
||||||
isLoading,
|
|
||||||
role,
|
|
||||||
showNewUserForm,
|
|
||||||
creatingUser,
|
|
||||||
users,
|
|
||||||
errors,
|
|
||||||
idChangingRole,
|
|
||||||
archivingUser,
|
|
||||||
} = this.state;
|
|
||||||
return (
|
return (
|
||||||
<div className="wrapper org-users-page">
|
<div className="wrapper org-users-page">
|
||||||
<Header switchDarkMode={this.props.switchDarkMode} darkMode={this.props.darkMode} />
|
<Header switchDarkMode={this.props.switchDarkMode} darkMode={this.props.darkMode} />
|
||||||
|
<ReactTooltip type="dark" effect="solid" delayShow={250} />
|
||||||
|
|
||||||
<div className="page-wrapper">
|
<div className="page-wrapper">
|
||||||
<div className="container-xl">
|
<div className="container-xl">
|
||||||
|
|
@ -210,7 +180,7 @@ class ManageOrgUsers extends React.Component {
|
||||||
<h3 className="card-title">Add new user</h3>
|
<h3 className="card-title">Add new user</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<form>
|
<form onSubmit={this.createUser} noValidate>
|
||||||
<div className="form-group mb-3 ">
|
<div className="form-group mb-3 ">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
|
|
@ -241,7 +211,7 @@ class ManageOrgUsers extends React.Component {
|
||||||
<label className="form-label">Email address</label>
|
<label className="form-label">Email address</label>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
aria-describedby="emailHelp"
|
aria-describedby="emailHelp"
|
||||||
placeholder="Enter email"
|
placeholder="Enter email"
|
||||||
|
|
@ -252,33 +222,18 @@ class ManageOrgUsers extends React.Component {
|
||||||
<span className="text-danger">{this.state.errors['email']}</span>
|
<span className="text-danger">{this.state.errors['email']}</span>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="form-footer">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="btn btn-light mr-2"
|
className="btn btn-light mr-2"
|
||||||
onClick={() => this.setState({ showNewUserForm: false, newUser: {} })}
|
onClick={() => this.setState({ showNewUserForm: false, newUser: {} })}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="submit"
|
||||||
className={`btn mx-2 btn-primary ${creatingUser ? 'btn-loading' : ''}`}
|
className={`btn mx-2 btn-primary ${creatingUser ? 'btn-loading' : ''}`}
|
||||||
onClick={(e) => this.createUser(e)}
|
disabled={creatingUser}
|
||||||
>
|
>
|
||||||
Create User
|
Create User
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -298,9 +253,6 @@ class ManageOrgUsers extends React.Component {
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>
|
|
||||||
<center>Role</center>
|
|
||||||
</th>
|
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th className="w-1"></th>
|
<th className="w-1"></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -351,27 +303,9 @@ class ManageOrgUsers extends React.Component {
|
||||||
{user.email}
|
{user.email}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</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">
|
<td className="text-muted">
|
||||||
<span
|
<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>
|
></span>
|
||||||
<small className="user-status">{user.status}</small>
|
<small className="user-status">{user.status}</small>
|
||||||
{user.status === 'invited' && 'invitation_token' in user ? (
|
{user.status === 'invited' && 'invitation_token' in user ? (
|
||||||
|
|
@ -380,10 +314,14 @@ class ManageOrgUsers extends React.Component {
|
||||||
onCopy={this.invitationLinkCopyHandler}
|
onCopy={this.invitationLinkCopyHandler}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
data-tip="Copy invitation link"
|
||||||
className="svg-icon"
|
className="svg-icon"
|
||||||
src="/assets/images/icons/copy.svg"
|
src="/assets/images/icons/copy.svg"
|
||||||
width="15"
|
width="15"
|
||||||
height="15"
|
height="15"
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
></img>
|
></img>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ class ResetPassword extends React.Component {
|
||||||
<div className="page page-center">
|
<div className="page page-center">
|
||||||
<div className="container-tight py-2">
|
<div className="container-tight py-2">
|
||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<a href=".">
|
<a href="." className="navbar-brand-autodark">
|
||||||
<img src="/assets/images/logo-text.svg" height="30" alt="" />
|
<img src="/assets/images/logo-text.svg" height="30" alt="" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ function SettingsPage(props) {
|
||||||
|
|
||||||
const changePassword = async () => {
|
const changePassword = async () => {
|
||||||
setPasswordChangeInProgress(true);
|
setPasswordChangeInProgress(true);
|
||||||
const response = userService.changePassword(currentpassword, newPassword);
|
const response = await userService.changePassword(currentpassword, newPassword);
|
||||||
response
|
response
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success('Password updated successfully', { hideProgressBar: true, autoClose: 3000 });
|
toast.success('Password updated successfully', { hideProgressBar: true, autoClose: 3000 });
|
||||||
|
|
@ -40,7 +40,7 @@ function SettingsPage(props) {
|
||||||
|
|
||||||
const newPasswordKeyPressHandler = async (event) => {
|
const newPasswordKeyPressHandler = async (event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
changePassword();
|
await changePassword();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ function SettingsPage(props) {
|
||||||
<div className="row align-items-center">
|
<div className="row align-items-center">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
<div className="page-pretitle"></div>
|
<div className="page-pretitle"></div>
|
||||||
<h2 className="page-title">Settings</h2>
|
<h2 className="page-title">Profile Settings</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ class SignupPage extends React.Component {
|
||||||
<div className="page page-center">
|
<div className="page page-center">
|
||||||
<div className="container-tight py-2">
|
<div className="container-tight py-2">
|
||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<a href=".">
|
<a href="." className="navbar-brand-autodark">
|
||||||
<img src="/assets/images/logo-text.svg" height="30" alt="" />
|
<img src="/assets/images/logo-text.svg" height="30" alt="" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ const DynamicForm = ({ schema, optionchanged, createDataSource, options, isSavin
|
||||||
case 'toggle':
|
case 'toggle':
|
||||||
return {
|
return {
|
||||||
defaultChecked: options[$key],
|
defaultChecked: options[$key],
|
||||||
onChange: () => optionchanged($key, !options[$key]),
|
onChange: () => optionchanged($key, !options[$key].value),
|
||||||
};
|
};
|
||||||
case 'dropdown':
|
case 'dropdown':
|
||||||
case 'dropdown-component-flip':
|
case 'dropdown-component-flip':
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,10 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React from 'react';
|
||||||
import cx from 'classnames';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { authenticationService } from '@/_services';
|
import { authenticationService } from '@/_services';
|
||||||
import { history } from '@/_helpers';
|
import { history } from '@/_helpers';
|
||||||
import { DarkModeToggle } from './DarkModeToggle';
|
import { DarkModeToggle } from './DarkModeToggle';
|
||||||
|
|
||||||
export const Header = function Header({ switchDarkMode, darkMode }) {
|
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() {
|
function logout() {
|
||||||
authenticationService.logout();
|
authenticationService.logout();
|
||||||
history.push('/login');
|
history.push('/login');
|
||||||
|
|
@ -22,7 +14,7 @@ export const Header = function Header({ switchDarkMode, darkMode }) {
|
||||||
history.push('/settings');
|
history.push('/settings');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { first_name, last_name } = authenticationService.currentUserValue;
|
const { first_name, last_name, admin } = authenticationService.currentUserValue;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="navbar navbar-expand-md navbar-light d-print-none">
|
<header className="navbar navbar-expand-md navbar-light d-print-none">
|
||||||
|
|
@ -36,25 +28,6 @@ export const Header = function Header({ switchDarkMode, darkMode }) {
|
||||||
</Link>
|
</Link>
|
||||||
</h1>
|
</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="navbar-nav flex-row order-md-last">
|
||||||
<div className="p-1 m-1 d-flex align-items-center">
|
<div className="p-1 m-1 d-flex align-items-center">
|
||||||
<DarkModeToggle switchDarkMode={switchDarkMode} darkMode={darkMode} />
|
<DarkModeToggle switchDarkMode={switchDarkMode} darkMode={darkMode} />
|
||||||
|
|
@ -75,12 +48,22 @@ export const Header = function Header({ switchDarkMode, darkMode }) {
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
<div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
||||||
<a data-testId="settingsBtn" onClick={openSettings} className="dropdown-item">
|
{admin && (
|
||||||
Settings
|
<Link data-testid="settingsBtn" to="/users" className="dropdown-item">
|
||||||
</a>
|
Manage Users
|
||||||
<a data-testId="logoutBtn" onClick={logout} className="dropdown-item">
|
</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
|
Logout
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ export function runTransformation(_ref, rawData, transformation, query) {
|
||||||
result = evalFunction(data, moment, _, currentState.components, currentState.queries, currentState.globals);
|
result = evalFunction(data, moment, _, currentState.components, currentState.queries, currentState.globals);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Transformation failed for query: ', query.name, 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;
|
return result;
|
||||||
|
|
@ -67,7 +67,7 @@ export async function executeActionsForEventId(_ref, eventId, component, mode) {
|
||||||
const filteredEvents = events.filter((event) => event.eventId === eventId);
|
const filteredEvents = events.filter((event) => event.eventId === eventId);
|
||||||
|
|
||||||
for (const event of filteredEvents) {
|
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) {
|
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 modalMeta = _ref.state.appDefinition.components[modalId];
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
|
|
@ -115,9 +120,7 @@ function showModal(_ref, modalId, show) {
|
||||||
|
|
||||||
_ref.setState(newState);
|
_ref.setState(newState);
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
return Promise.resolve();
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeAction(_ref, event, mode) {
|
function executeAction(_ref, event, mode) {
|
||||||
|
|
@ -125,10 +128,8 @@ function executeAction(_ref, event, mode) {
|
||||||
switch (event.actionId) {
|
switch (event.actionId) {
|
||||||
case 'show-alert': {
|
case 'show-alert': {
|
||||||
const message = resolveReferences(event.message, _ref.state.currentState);
|
const message = resolveReferences(event.message, _ref.state.currentState);
|
||||||
toast(message, { hideProgressBar: true });
|
toast(message, { hideProgressBar: true, type: event.alertType });
|
||||||
return new Promise(function (resolve, reject) {
|
return Promise.resolve();
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'run-query': {
|
case 'run-query': {
|
||||||
|
|
@ -139,9 +140,7 @@ function executeAction(_ref, event, mode) {
|
||||||
case 'open-webpage': {
|
case 'open-webpage': {
|
||||||
const url = resolveReferences(event.url, _ref.state.currentState);
|
const url = resolveReferences(event.url, _ref.state.currentState);
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
return new Promise(function (resolve, reject) {
|
return Promise.resolve();
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'go-to-app': {
|
case 'go-to-app': {
|
||||||
|
|
@ -174,9 +173,7 @@ function executeAction(_ref, event, mode) {
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new Promise(function (resolve, reject) {
|
return Promise.resolve();
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'show-modal':
|
case 'show-modal':
|
||||||
|
|
@ -189,9 +186,7 @@ function executeAction(_ref, event, mode) {
|
||||||
const contentToCopy = resolveReferences(event.contentToCopy, _ref.state.currentState);
|
const contentToCopy = resolveReferences(event.contentToCopy, _ref.state.currentState);
|
||||||
copyToClipboard(contentToCopy);
|
copyToClipboard(contentToCopy);
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
return Promise.resolve();
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -451,6 +446,34 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined) {
|
||||||
|
|
||||||
if (dataQuery.options.enableTransformation) {
|
if (dataQuery.options.enableTransformation) {
|
||||||
finalData = runTransformation(_self, rawData, dataQuery.options.transformation, dataQuery);
|
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) {
|
if (dataQuery.options.showSuccessNotification) {
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@ export const serializeNestedObjectToQueryParams = function (obj, prefix) {
|
||||||
var str = [],
|
var str = [],
|
||||||
p;
|
p;
|
||||||
for (p in obj) {
|
for (p in obj) {
|
||||||
if (obj.hasOwnProperty(p)) {
|
if (Object.prototype.hasOwnProperty.call(obj, p)) {
|
||||||
var k = prefix ? prefix + '[' + p + ']' : p,
|
var k = prefix ? prefix + '[' + p + ']' : p,
|
||||||
v = obj[p];
|
v = obj[p];
|
||||||
str.push(
|
str.push(
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ const useEscapeHandler = (handler = noop, dependencies = []) => {
|
||||||
document === null || document === void 0 ? void 0 : document.removeEventListener('keyup', escapeHandler);
|
document === null || document === void 0 ? void 0 : document.removeEventListener('keyup', escapeHandler);
|
||||||
}, dependencies);
|
}, dependencies);
|
||||||
};
|
};
|
||||||
const useClickOutside = (handler = noop, dependencies) => {
|
const useClickOutside = (dependencies, handler = noop) => {
|
||||||
const callbackRef = useRef(handler);
|
const callbackRef = useRef(handler);
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const outsideClickHandler = (e) => {
|
const outsideClickHandler = (e) => {
|
||||||
|
|
@ -43,7 +43,7 @@ const usePopover = (defaultOpen = false) => {
|
||||||
const toggle = useCallback(() => setOpen(!open), []);
|
const toggle = useCallback(() => setOpen(!open), []);
|
||||||
const close = useCallback(() => setOpen(false), []);
|
const close = useCallback(() => setOpen(false), []);
|
||||||
useEscapeHandler(close, []);
|
useEscapeHandler(close, []);
|
||||||
const contentRef = useClickOutside(open ? close : undefined, []);
|
const contentRef = useClickOutside([], open ? close : undefined);
|
||||||
const trigger = {
|
const trigger = {
|
||||||
ref: triggerRef,
|
ref: triggerRef,
|
||||||
onClick: toggle,
|
onClick: toggle,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ export const appService = {
|
||||||
getAll,
|
getAll,
|
||||||
createApp,
|
createApp,
|
||||||
cloneApp,
|
cloneApp,
|
||||||
|
exportApp,
|
||||||
|
importApp,
|
||||||
deleteApp,
|
deleteApp,
|
||||||
getApp,
|
getApp,
|
||||||
getAppBySlug,
|
getAppBySlug,
|
||||||
|
|
@ -40,6 +42,16 @@ function cloneApp(id) {
|
||||||
return fetch(`${config.apiUrl}/apps/${id}/clone`, requestOptions).then(handleResponse);
|
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) {
|
function getApp(id) {
|
||||||
const requestOptions = { method: 'GET', headers: authHeader() };
|
const requestOptions = { method: 'GET', headers: authHeader() };
|
||||||
return fetch(`${config.apiUrl}/apps/${id}`, requestOptions).then(handleResponse);
|
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,
|
changeRole,
|
||||||
};
|
};
|
||||||
|
|
||||||
function create(first_name, last_name, email, role) {
|
function create(first_name, last_name, email) {
|
||||||
const body = {
|
const body = {
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
email,
|
email,
|
||||||
role,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
|
const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
|
||||||
return fetch(`${config.apiUrl}/organization_users`, requestOptions).then(handleResponse);
|
return fetch(`${config.apiUrl}/organization_users`, requestOptions).then(handleResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated
|
||||||
function changeRole(id, role) {
|
function changeRole(id, role) {
|
||||||
const body = {
|
const body = {
|
||||||
role,
|
role,
|
||||||
|
|
|
||||||
|
|
@ -388,7 +388,6 @@ body {
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
width: 59.3%;
|
width: 59.3%;
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
padding: 0.5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-header {
|
.preview-header {
|
||||||
|
|
@ -893,7 +892,6 @@ body {
|
||||||
|
|
||||||
.jet-data-table::-webkit-scrollbar {
|
.jet-data-table::-webkit-scrollbar {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
height: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.jet-data-table:hover {
|
.jet-data-table:hover {
|
||||||
|
|
@ -952,7 +950,7 @@ tr:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.jet-container {
|
.jet-container {
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-search__option {
|
.select-search__option {
|
||||||
|
|
@ -1797,6 +1795,9 @@ input:focus-visible {
|
||||||
.card {
|
.card {
|
||||||
background-color: #324156 !important;
|
background-color: #324156 !important;
|
||||||
}
|
}
|
||||||
|
.card .table tbody td a{
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.DateInput {
|
.DateInput {
|
||||||
background: #1f2936;
|
background: #1f2936;
|
||||||
|
|
@ -2013,8 +2014,13 @@ input:focus-visible {
|
||||||
.editor .editor-sidebar .inspector .header {
|
.editor .editor-sidebar .inspector .header {
|
||||||
border: solid rgba(255, 255, 255, 0.09) !important;
|
border: solid rgba(255, 255, 255, 0.09) !important;
|
||||||
border-width: 0px 0px 1px 0px !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 {
|
.skeleton-line::after {
|
||||||
// background-image: linear-gradient(to right, #232e3c 0, #4c5b79 40%, #4c5b79 80%);
|
// background-image: linear-gradient(to right, #232e3c 0, #4c5b79 40%, #4c5b79 80%);
|
||||||
background-image: linear-gradient(to right, #566177 0, #5a6170 40%, #4c5b79 80%);
|
background-image: linear-gradient(to right, #566177 0, #5a6170 40%, #4c5b79 80%);
|
||||||
|
|
@ -2238,3 +2244,7 @@ input[type='text'] {
|
||||||
height: 17px;
|
height: 17px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fw-500 {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,7 @@
|
||||||
.label {
|
.label {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
.star {
|
||||||
|
margin-bottom: 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
export default ({ defaultChecked, onChange }) => {
|
export default ({ defaultChecked, onChange }) => {
|
||||||
return (
|
return (
|
||||||
<label className="form-switch mt-3">
|
<label className="form-switch">
|
||||||
<input className="form-check-input" type="checkbox" defaultChecked={defaultChecked} onChange={onChange} />
|
<input className="form-check-input" type="checkbox" defaultChecked={defaultChecked} onChange={onChange} />
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ appService
|
||||||
.then((config) => {
|
.then((config) => {
|
||||||
window.public_config = config;
|
window.public_config = config;
|
||||||
|
|
||||||
if (window.public_config.APM_VENDOR == 'sentry') {
|
if (window.public_config.APM_VENDOR === 'sentry') {
|
||||||
const history = createBrowserHistory();
|
const history = createBrowserHistory();
|
||||||
const tooljetServerUrl = window.public_config.TOOLJET_SERVER_URL;
|
const tooljetServerUrl = window.public_config.TOOLJET_SERVER_URL;
|
||||||
const tracingOrigins = ['localhost', /^\//];
|
const tracingOrigins = ['localhost', /^\//];
|
||||||
|
|
|
||||||
427
package-lock.json
generated
427
package-lock.json
generated
|
|
@ -18,6 +18,7 @@
|
||||||
"babel-loader": "^8.0.5",
|
"babel-loader": "^8.0.5",
|
||||||
"html-webpack-plugin": "^5.3.2",
|
"html-webpack-plugin": "^5.3.2",
|
||||||
"husky": "^7.0.2",
|
"husky": "^7.0.2",
|
||||||
|
"lint-staged": "^11.2.3",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"webpack": "^5.55.1",
|
"webpack": "^5.55.1",
|
||||||
"webpack-cli": "^4.8.0"
|
"webpack-cli": "^4.8.0"
|
||||||
|
|
@ -1312,7 +1313,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||||
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
|
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clean-stack": "^2.0.0",
|
"clean-stack": "^2.0.0",
|
||||||
"indent-string": "^4.0.0"
|
"indent-string": "^4.0.0"
|
||||||
|
|
@ -1868,7 +1868,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||||
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
|
|
@ -1954,7 +1953,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
|
||||||
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
|
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"slice-ansi": "^3.0.0",
|
"slice-ansi": "^3.0.0",
|
||||||
"string-width": "^4.2.0"
|
"string-width": "^4.2.0"
|
||||||
|
|
@ -2013,9 +2011,9 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
},
|
},
|
||||||
"node_modules/colorette": {
|
"node_modules/colorette": {
|
||||||
"version": "1.2.2",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
|
||||||
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
|
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="
|
||||||
},
|
},
|
||||||
"node_modules/colors": {
|
"node_modules/colors": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
|
|
@ -2265,24 +2263,6 @@
|
||||||
"node": ">= 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": {
|
"node_modules/cypress/node_modules/fs-extra": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||||
|
|
@ -2349,9 +2329,9 @@
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||||
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
|
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "2.1.2"
|
"ms": "2.1.2"
|
||||||
},
|
},
|
||||||
|
|
@ -3207,6 +3187,12 @@
|
||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/get-stream": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
|
|
@ -3717,6 +3702,15 @@
|
||||||
"node": ">=0.12.0"
|
"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": {
|
"node_modules/is-path-inside": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
|
||||||
|
|
@ -3739,6 +3733,15 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/is-stream": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
|
||||||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="
|
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="
|
||||||
},
|
},
|
||||||
"node_modules/listr2": {
|
"node_modules/lint-staged": {
|
||||||
"version": "3.11.1",
|
"version": "11.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-11.2.3.tgz",
|
||||||
"integrity": "sha512-ZXQvQfmH9iWLlb4n3hh31yicXDxlzB0pE7MM1zu6kgbVL4ivEsO4H8IPh4E682sC8RjnYO9anose+zT52rrpyg==",
|
"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,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cli-truncate": "^2.1.0",
|
"cli-truncate": "^2.1.0",
|
||||||
"colorette": "^1.2.2",
|
"colorette": "^1.4.0",
|
||||||
"log-update": "^4.0.0",
|
"log-update": "^4.0.0",
|
||||||
"p-map": "^4.0.0",
|
"p-map": "^4.0.0",
|
||||||
"rxjs": "^6.6.7",
|
"rxjs": "^6.6.7",
|
||||||
|
|
@ -4074,7 +4188,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
|
||||||
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
|
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-escapes": "^4.3.0",
|
"ansi-escapes": "^4.3.0",
|
||||||
"cli-cursor": "^3.1.0",
|
"cli-cursor": "^3.1.0",
|
||||||
|
|
@ -4093,7 +4206,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
|
||||||
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
|
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^4.0.0",
|
"ansi-styles": "^4.0.0",
|
||||||
"astral-regex": "^2.0.0",
|
"astral-regex": "^2.0.0",
|
||||||
|
|
@ -4111,7 +4223,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^4.0.0",
|
"ansi-styles": "^4.0.0",
|
||||||
"string-width": "^4.1.0",
|
"string-width": "^4.1.0",
|
||||||
|
|
@ -4187,6 +4298,19 @@
|
||||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
|
"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": {
|
"node_modules/mime-db": {
|
||||||
"version": "1.49.0",
|
"version": "1.49.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
|
||||||
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
|
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aggregate-error": "^3.0.0"
|
"aggregate-error": "^3.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -4635,6 +4758,15 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/pluralize": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
||||||
|
|
@ -4988,6 +5120,12 @@
|
||||||
"node": ">=10"
|
"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": {
|
"node_modules/serialize-javascript": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
|
||||||
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
|
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^4.0.0",
|
"ansi-styles": "^4.0.0",
|
||||||
"astral-regex": "^2.0.0",
|
"astral-regex": "^2.0.0",
|
||||||
|
|
@ -5129,6 +5266,15 @@
|
||||||
"safe-buffer": "~5.2.0"
|
"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": {
|
"node_modules/string-width": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
||||||
|
|
@ -5142,6 +5288,20 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/strip-ansi": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^4.0.0",
|
"ansi-styles": "^4.0.0",
|
||||||
"string-width": "^4.1.0",
|
"string-width": "^4.1.0",
|
||||||
|
|
@ -7089,7 +7248,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
|
||||||
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
|
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"clean-stack": "^2.0.0",
|
"clean-stack": "^2.0.0",
|
||||||
"indent-string": "^4.0.0"
|
"indent-string": "^4.0.0"
|
||||||
|
|
@ -7486,8 +7644,7 @@
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||||
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"cli-cursor": {
|
"cli-cursor": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
|
|
@ -7546,7 +7703,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
|
||||||
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
|
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"slice-ansi": "^3.0.0",
|
"slice-ansi": "^3.0.0",
|
||||||
"string-width": "^4.2.0"
|
"string-width": "^4.2.0"
|
||||||
|
|
@ -7587,9 +7743,9 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
},
|
},
|
||||||
"colorette": {
|
"colorette": {
|
||||||
"version": "1.2.2",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
|
||||||
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
|
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
|
|
@ -7794,16 +7950,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": 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": {
|
"fs-extra": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||||
|
|
@ -7857,9 +8003,9 @@
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||||
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
|
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"ms": "2.1.2"
|
"ms": "2.1.2"
|
||||||
}
|
}
|
||||||
|
|
@ -8516,6 +8662,12 @@
|
||||||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||||
"dev": true
|
"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": {
|
"get-stream": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||||
|
|
@ -8754,8 +8906,7 @@
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"inflight": {
|
"inflight": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
|
|
@ -8865,6 +9016,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
"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": {
|
"is-path-inside": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
|
||||||
|
|
@ -8881,6 +9038,12 @@
|
||||||
"isobject": "^3.0.1"
|
"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": {
|
"is-stream": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
|
||||||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="
|
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="
|
||||||
},
|
},
|
||||||
"listr2": {
|
"lint-staged": {
|
||||||
"version": "3.11.1",
|
"version": "11.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-3.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-11.2.3.tgz",
|
||||||
"integrity": "sha512-ZXQvQfmH9iWLlb4n3hh31yicXDxlzB0pE7MM1zu6kgbVL4ivEsO4H8IPh4E682sC8RjnYO9anose+zT52rrpyg==",
|
"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,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"cli-truncate": "^2.1.0",
|
"cli-truncate": "^2.1.0",
|
||||||
"colorette": "^1.2.2",
|
"colorette": "^1.4.0",
|
||||||
"log-update": "^4.0.0",
|
"log-update": "^4.0.0",
|
||||||
"p-map": "^4.0.0",
|
"p-map": "^4.0.0",
|
||||||
"rxjs": "^6.6.7",
|
"rxjs": "^6.6.7",
|
||||||
|
|
@ -9147,7 +9390,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
|
||||||
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
|
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-escapes": "^4.3.0",
|
"ansi-escapes": "^4.3.0",
|
||||||
"cli-cursor": "^3.1.0",
|
"cli-cursor": "^3.1.0",
|
||||||
|
|
@ -9160,7 +9402,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
|
||||||
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
|
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-styles": "^4.0.0",
|
"ansi-styles": "^4.0.0",
|
||||||
"astral-regex": "^2.0.0",
|
"astral-regex": "^2.0.0",
|
||||||
|
|
@ -9172,7 +9413,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-styles": "^4.0.0",
|
"ansi-styles": "^4.0.0",
|
||||||
"string-width": "^4.1.0",
|
"string-width": "^4.1.0",
|
||||||
|
|
@ -9237,6 +9477,16 @@
|
||||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
|
"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": {
|
"mime-db": {
|
||||||
"version": "1.49.0",
|
"version": "1.49.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
|
||||||
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
|
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"aggregate-error": "^3.0.0"
|
"aggregate-error": "^3.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -9593,6 +9842,15 @@
|
||||||
"find-up": "^4.0.0"
|
"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": {
|
"pluralize": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
||||||
|
|
@ -9844,6 +10102,12 @@
|
||||||
"lru-cache": "^6.0.0"
|
"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": {
|
"serialize-javascript": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
|
||||||
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
|
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-styles": "^4.0.0",
|
"ansi-styles": "^4.0.0",
|
||||||
"astral-regex": "^2.0.0",
|
"astral-regex": "^2.0.0",
|
||||||
|
|
@ -9956,6 +10219,12 @@
|
||||||
"safe-buffer": "~5.2.0"
|
"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": {
|
"string-width": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
|
||||||
|
|
@ -9966,6 +10235,17 @@
|
||||||
"strip-ansi": "^6.0.0"
|
"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": {
|
"strip-ansi": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-styles": "^4.0.0",
|
"ansi-styles": "^4.0.0",
|
||||||
"string-width": "^4.1.0",
|
"string-width": "^4.1.0",
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,6 @@
|
||||||
"eslint --fix"
|
"eslint --fix"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"husky": {
|
|
||||||
"hooks": {
|
|
||||||
"pre-commit": "lint-staged"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@4tw/cypress-drag-drop": "^1.8.0",
|
"@4tw/cypress-drag-drop": "^1.8.0",
|
||||||
"@babel/core": "^7.4.3",
|
"@babel/core": "^7.4.3",
|
||||||
|
|
@ -25,6 +20,7 @@
|
||||||
"babel-loader": "^8.0.5",
|
"babel-loader": "^8.0.5",
|
||||||
"html-webpack-plugin": "^5.3.2",
|
"html-webpack-plugin": "^5.3.2",
|
||||||
"husky": "^7.0.2",
|
"husky": "^7.0.2",
|
||||||
|
"lint-staged": "^11.2.3",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"webpack": "^5.55.1",
|
"webpack": "^5.55.1",
|
||||||
"webpack-cli": "^4.8.0"
|
"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 = {};
|
let result = {};
|
||||||
|
|
||||||
/* Prefixing the base url of datasouce if datasource exists */
|
/* 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 method = queryOptions['method'];
|
||||||
const json = method !== 'get' ? this.body(sourceOptions, queryOptions, hasDataSource) : undefined;
|
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 { EmailService } from '@services/email.service';
|
||||||
import { MetaModule } from './modules/meta/meta.module';
|
import { MetaModule } from './modules/meta/meta.module';
|
||||||
import { AppController } from './controllers/app.controller';
|
import { AppController } from './controllers/app.controller';
|
||||||
import { AppService } from './services/app.service';
|
|
||||||
import { AuthModule } from './modules/auth/auth.module';
|
import { AuthModule } from './modules/auth/auth.module';
|
||||||
import { UsersModule } from './modules/users/users.module';
|
import { UsersModule } from './modules/users/users.module';
|
||||||
import { AppConfigModule } from './modules/app_config/app_config.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 { DataSourcesModule } from './modules/data_sources/data_sources.module';
|
||||||
import { OrganizationsModule } from './modules/organizations/organizations.module';
|
import { OrganizationsModule } from './modules/organizations/organizations.module';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { GroupPermissionsModule } from './modules/group_permissions/group_permissions.module';
|
||||||
|
|
||||||
const imports = [
|
const imports = [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
|
|
@ -63,6 +63,7 @@ const imports = [
|
||||||
OrganizationsModule,
|
OrganizationsModule,
|
||||||
CaslModule,
|
CaslModule,
|
||||||
MetaModule,
|
MetaModule,
|
||||||
|
GroupPermissionsModule,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (process.env.SERVE_CLIENT !== 'false') {
|
if (process.env.SERVE_CLIENT !== 'false') {
|
||||||
|
|
@ -86,7 +87,7 @@ if (process.env.APM_VENDOR == 'sentry') {
|
||||||
@Module({
|
@Module({
|
||||||
imports,
|
imports,
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService, EmailService, SeedsService],
|
providers: [EmailService, SeedsService],
|
||||||
})
|
})
|
||||||
export class AppModule implements OnModuleInit, OnApplicationBootstrap {
|
export class AppModule implements OnModuleInit, OnApplicationBootstrap {
|
||||||
constructor(private connection: Connection) {}
|
constructor(private connection: Connection) {}
|
||||||
|
|
@ -98,11 +99,11 @@ export class AppModule implements OnModuleInit, OnApplicationBootstrap {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit(): void {
|
||||||
console.log(`Initializing ToolJet server modules 📡 `);
|
console.log(`Initializing ToolJet server modules 📡 `);
|
||||||
}
|
}
|
||||||
|
|
||||||
onApplicationBootstrap() {
|
onApplicationBootstrap(): void {
|
||||||
console.log(`Initialized ToolJet server, waiting for requests 🚀`);
|
console.log(`Initialized ToolJet server, waiting for requests 🚀`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export class AppUsersController {
|
||||||
private appsAbilityFactory: AppsAbilityFactory
|
private appsAbilityFactory: AppsAbilityFactory
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
// TODO: remove deprecated
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Post()
|
@Post()
|
||||||
async create(@Request() req) {
|
async create(@Request() req) {
|
||||||
|
|
@ -22,7 +23,7 @@ export class AppUsersController {
|
||||||
const { role } = params;
|
const { role } = params;
|
||||||
|
|
||||||
const app = await this.appsService.find(appId);
|
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)) {
|
if (!ability.can('createUsers', 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');
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,14 @@ import { decamelizeKeys } from 'humps';
|
||||||
import { AppsAbilityFactory } from 'src/modules/casl/abilities/apps-ability.factory';
|
import { AppsAbilityFactory } from 'src/modules/casl/abilities/apps-ability.factory';
|
||||||
import { AppAuthGuard } from 'src/modules/auth/app-auth.guard';
|
import { AppAuthGuard } from 'src/modules/auth/app-auth.guard';
|
||||||
import { FoldersService } from '@services/folders.service';
|
import { FoldersService } from '@services/folders.service';
|
||||||
|
import { App } from 'src/entities/app.entity';
|
||||||
|
import { AppImportExportService } from '@services/app_import_export.service';
|
||||||
|
|
||||||
@Controller('apps')
|
@Controller('apps')
|
||||||
export class AppsController {
|
export class AppsController {
|
||||||
constructor(
|
constructor(
|
||||||
private appsService: AppsService,
|
private appsService: AppsService,
|
||||||
|
private appImportExportService: AppImportExportService,
|
||||||
private foldersService: FoldersService,
|
private foldersService: FoldersService,
|
||||||
private appsAbilityFactory: AppsAbilityFactory
|
private appsAbilityFactory: AppsAbilityFactory
|
||||||
) {}
|
) {}
|
||||||
|
|
@ -28,12 +31,12 @@ export class AppsController {
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Post()
|
@Post()
|
||||||
async create(@Request() req) {
|
async create(@Request() req) {
|
||||||
const app = await this.appsService.create(req.user);
|
|
||||||
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
|
||||||
|
|
||||||
if (!ability.can('createApp', app)) {
|
if (!ability.can('createApp', 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 app = await this.appsService.create(req.user);
|
||||||
|
|
||||||
await this.appsService.update(req.user, app.id, {
|
await this.appsService.update(req.user, app.id, {
|
||||||
slug: app.id,
|
slug: app.id,
|
||||||
|
|
@ -46,6 +49,11 @@ export class AppsController {
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
async show(@Request() req, @Param() params) {
|
async show(@Request() req, @Param() params) {
|
||||||
const app = await this.appsService.find(params.id);
|
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 response = decamelizeKeys(app);
|
||||||
|
|
||||||
const seralizedQueries = [];
|
const seralizedQueries = [];
|
||||||
|
|
@ -68,10 +76,12 @@ export class AppsController {
|
||||||
async appFromSlug(@Request() req, @Param() params) {
|
async appFromSlug(@Request() req, @Param() params) {
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
const app = await this.appsService.findBySlug(params.slug);
|
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)) {
|
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')
|
@Put(':id')
|
||||||
async update(@Request() req, @Param() params) {
|
async update(@Request() req, @Param() params) {
|
||||||
const app = await this.appsService.find(params.id);
|
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)) {
|
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);
|
const result = await this.appsService.update(req.user, params.id, req.body.app);
|
||||||
|
|
@ -108,10 +118,10 @@ export class AppsController {
|
||||||
@Post(':id/clone')
|
@Post(':id/clone')
|
||||||
async clone(@Request() req, @Param() params) {
|
async clone(@Request() req, @Param() params) {
|
||||||
const existingApp = await this.appsService.find(params.id);
|
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)) {
|
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);
|
const result = await this.appsService.clone(existingApp, req.user);
|
||||||
|
|
@ -120,11 +130,38 @@ export class AppsController {
|
||||||
return response;
|
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)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
async delete(@Request() req, @Param() params) {
|
async delete(@Request() req, @Param() params) {
|
||||||
const app = await this.appsService.find(params.id);
|
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)) {
|
if (!ability.can('deleteApp', app)) {
|
||||||
throw new ForbiddenException('Only administrators are allowed to delete apps.');
|
throw new ForbiddenException('Only administrators are allowed to delete apps.');
|
||||||
|
|
@ -172,14 +209,15 @@ export class AppsController {
|
||||||
return decamelizeKeys(response);
|
return decamelizeKeys(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deprecated
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Get(':id/users')
|
@Get(':id/users')
|
||||||
async fetchUsers(@Request() req, @Param() params) {
|
async fetchUsers(@Request() req, @Param() params) {
|
||||||
const app = await this.appsService.find(params.id);
|
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)) {
|
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);
|
const result = await this.appsService.fetchUsers(req.user, params.id);
|
||||||
|
|
@ -190,10 +228,10 @@ export class AppsController {
|
||||||
@Get(':id/versions')
|
@Get(':id/versions')
|
||||||
async fetchVersions(@Request() req, @Param() params) {
|
async fetchVersions(@Request() req, @Param() params) {
|
||||||
const app = await this.appsService.find(params.id);
|
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)) {
|
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);
|
const result = await this.appsService.fetchVersions(req.user, params.id);
|
||||||
|
|
@ -206,10 +244,10 @@ export class AppsController {
|
||||||
const versionName = req.body['versionName'];
|
const versionName = req.body['versionName'];
|
||||||
|
|
||||||
const app = await this.appsService.find(params.id);
|
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)) {
|
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);
|
const appUser = await this.appsService.createVersion(req.user, app, versionName);
|
||||||
|
|
@ -220,10 +258,10 @@ export class AppsController {
|
||||||
@Get(':id/versions/:versionId')
|
@Get(':id/versions/:versionId')
|
||||||
async version(@Request() req, @Param() params) {
|
async version(@Request() req, @Param() params) {
|
||||||
const app = await this.appsService.find(params.id);
|
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)) {
|
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);
|
const appVersion = await this.appsService.findVersion(params.versionId);
|
||||||
|
|
@ -237,10 +275,10 @@ export class AppsController {
|
||||||
const definition = req.body['definition'];
|
const definition = req.body['definition'];
|
||||||
|
|
||||||
const version = await this.appsService.findVersion(params.versionId);
|
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)) {
|
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);
|
const appUser = await this.appsService.updateVersion(req.user, version, definition);
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,9 @@ export class DataQueriesController {
|
||||||
@Get()
|
@Get()
|
||||||
async index(@Request() req, @Query() query) {
|
async index(@Request() req, @Query() query) {
|
||||||
const app = await this.appsService.find(query.app_id);
|
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)) {
|
if (!ability.can('getQueries', 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');
|
||||||
|
|
@ -61,7 +63,9 @@ export class DataQueriesController {
|
||||||
const appId = req.body.app_id;
|
const appId = req.body.app_id;
|
||||||
|
|
||||||
const app = await this.appsService.find(appId);
|
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)) {
|
if (!ability.can('createQuery', 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');
|
||||||
|
|
@ -88,7 +92,9 @@ export class DataQueriesController {
|
||||||
const dataQueryId = params.id;
|
const dataQueryId = params.id;
|
||||||
|
|
||||||
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
|
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)) {
|
if (!ability.can('updateQuery', dataQuery.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');
|
||||||
|
|
@ -104,7 +110,9 @@ export class DataQueriesController {
|
||||||
const dataQueryId = params.id;
|
const dataQueryId = params.id;
|
||||||
|
|
||||||
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
|
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)) {
|
if (!ability.can('deleteQuery', dataQuery.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');
|
||||||
|
|
@ -123,7 +131,9 @@ export class DataQueriesController {
|
||||||
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
|
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
|
||||||
|
|
||||||
if (req.user) {
|
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)) {
|
if (!ability.can('runQuery', dataQuery.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');
|
||||||
|
|
@ -166,7 +176,9 @@ export class DataQueriesController {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (dataQueryEntity.dataSource) {
|
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)) {
|
if (!ability.can('previewQuery', dataQueryEntity.dataSource.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');
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ export class DataSourcesController {
|
||||||
@Get()
|
@Get()
|
||||||
async index(@Request() req, @Query() query) {
|
async index(@Request() req, @Query() query) {
|
||||||
const app = await this.appsService.find(query.app_id);
|
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)) {
|
if (!ability.can('getDataSources', 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');
|
||||||
|
|
@ -38,7 +40,9 @@ export class DataSourcesController {
|
||||||
const appId = req.body.app_id;
|
const appId = req.body.app_id;
|
||||||
|
|
||||||
const app = await this.appsService.find(appId);
|
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)) {
|
if (!ability.can('createDataSource', 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');
|
||||||
|
|
@ -57,7 +61,9 @@ export class DataSourcesController {
|
||||||
const dataSource = await this.dataSourcesService.findOne(dataSourceId);
|
const dataSource = await this.dataSourcesService.findOne(dataSourceId);
|
||||||
|
|
||||||
const app = await this.appsService.find(dataSource.appId);
|
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)) {
|
if (!ability.can('updateDataSource', 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');
|
||||||
|
|
@ -90,7 +96,9 @@ export class DataSourcesController {
|
||||||
const dataSource = await this.dataSourcesService.findOne(dataSourceId);
|
const dataSource = await this.dataSourcesService.findOne(dataSourceId);
|
||||||
|
|
||||||
const app = await this.appsService.find(dataSource.appId);
|
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)) {
|
if (!ability.can('authorizeOauthForSource', 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');
|
||||||
|
|
|
||||||
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,
|
OneToMany,
|
||||||
AfterLoad,
|
AfterLoad,
|
||||||
BaseEntity,
|
BaseEntity,
|
||||||
|
ManyToMany,
|
||||||
|
JoinTable,
|
||||||
|
AfterInsert,
|
||||||
|
getRepository,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { User } from './user.entity';
|
import { User } from './user.entity';
|
||||||
import { AppVersion } from './app_version.entity';
|
import { AppVersion } from './app_version.entity';
|
||||||
import { DataQuery } from './data_query.entity';
|
import { DataQuery } from './data_query.entity';
|
||||||
import { DataSource } from './data_source.entity';
|
import { DataSource } from './data_source.entity';
|
||||||
|
import { GroupPermission } from './group_permission.entity';
|
||||||
|
import { AppGroupPermission } from './app_group_permission.entity';
|
||||||
|
|
||||||
@Entity({ name: 'apps' })
|
@Entity({ name: 'apps' })
|
||||||
export class App extends BaseEntity {
|
export class App extends BaseEntity {
|
||||||
|
|
@ -45,17 +51,47 @@ export class App extends BaseEntity {
|
||||||
@JoinColumn({ name: 'user_id' })
|
@JoinColumn({ name: 'user_id' })
|
||||||
user: User;
|
user: User;
|
||||||
|
|
||||||
@OneToMany(() => AppVersion, (appVersion) => appVersion.app, { eager: true, onDelete: 'CASCADE' })
|
@OneToMany(() => AppVersion, (appVersion) => appVersion.app, {
|
||||||
|
eager: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
appVersions: AppVersion[];
|
appVersions: AppVersion[];
|
||||||
|
|
||||||
@OneToMany(() => DataQuery, (dataQuery) => dataQuery.app, { onDelete: 'CASCADE' })
|
@OneToMany(() => DataQuery, (dataQuery) => dataQuery.app, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
dataQueries: DataQuery[];
|
dataQueries: DataQuery[];
|
||||||
|
|
||||||
@OneToMany(() => DataSource, (dataSource) => dataSource.app, { onDelete: 'CASCADE' })
|
@OneToMany(() => DataSource, (dataSource) => dataSource.app, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
dataSources: DataSource[];
|
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;
|
public editingVersion;
|
||||||
|
|
||||||
|
@AfterInsert()
|
||||||
|
updateSlug(): void {
|
||||||
|
if (!this.slug) {
|
||||||
|
const appRepository = getRepository(App);
|
||||||
|
appRepository.update(this.id, { slug: this.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@AfterLoad()
|
@AfterLoad()
|
||||||
async afterLoad(): Promise<void> {
|
async afterLoad(): Promise<void> {
|
||||||
if (this.currentVersionId) {
|
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