Merge branch 'release/1.2.0'

This commit is contained in:
Akshay Sasidharan 2022-02-23 19:08:27 +05:30
commit a5716a8d7e
113 changed files with 14724 additions and 837 deletions

View file

@ -1 +1 @@
1.1.0
1.2.0

View file

@ -52,4 +52,4 @@ We use GitHub issues to track public bugs. Report a bug by [opening a new issue]
By contributing, you agree that your contributions will be licensed under its AGPL v3 License.
## Questions?
Contact us on slack [Slack](https://join.slack.com/t/tooljet/shared_invite/zt-r2neyfcw-KD1COL6t2kgVTlTtAV5rtg) or mail us at [hello@tooljet.io](mailto:hello@tooljet.io.).
Contact us on [Slack](https://join.slack.com/t/tooljet/shared_invite/zt-r2neyfcw-KD1COL6t2kgVTlTtAV5rtg) or mail us at [hello@tooljet.io](mailto:hello@tooljet.io).

View file

@ -1,5 +1,5 @@
---
sidebar_position: 18
sidebar_position: 3
---
# BigQuery

View file

@ -1,5 +1,5 @@
---
sidebar_position: 4
sidebar_position: 5
---
# Custom JavaScript

View file

@ -1,5 +1,5 @@
---
sidebar_position: 5
sidebar_position: 6
---
# DynamoDB

View file

@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
---
# Elasticsearch

View file

@ -1,5 +1,5 @@
---
sidebar_position: 3
sidebar_position: 4
---
# Cloud Firestore

View file

@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 8
---
# Google Cloud Storage

View file

@ -1,5 +1,5 @@
---
sidebar_position: 8
sidebar_position: 9
---
# Google Sheets

View file

@ -1,5 +1,5 @@
---
sidebar_position: 9
sidebar_position: 10
---
# GraphQL

View file

@ -1,3 +1,7 @@
---
sidebar_position: 11
---
# MinIO
ToolJet can connect to minio and perform various operation on them.

View file

@ -1,5 +1,5 @@
---
sidebar_position: 10
sidebar_position: 12
---
# MongoDB

View file

@ -1,5 +1,5 @@
---
sidebar_position: 11
sidebar_position: 13
---
# MS SQL Server / Azure SQL databases

View file

@ -1,5 +1,5 @@
---
sidebar_position: 12
sidebar_position: 14
---
# MySQL

View file

@ -0,0 +1,49 @@
# n8n
ToolJet can trigger n8n workflows using webhook URLs. Please refer [this](https://docs.n8n.io/) to know more about n8n.
## Connection
Go to the data source manager on the left sidebar and click on `+` button to add new data source. Select n8n from the list of available data sources in the modal that pops-up.
n8n webhooks can be called with or without an **Authentication**. You can keep the `Authentication type` as `none` if your webhook didn't have one or if it has one then you can choose the one from the dropdown and provide credentials:
#### Authentication Types
- **Basic Auth**: To connect your n8n webhooks using basic auth you'll need to provide the following credentials:
- **Username**
- **Password**
<div style={{textAlign: 'center'}}>
![ToolJet - Data source - n8n](/img/datasource-reference/n8n/basicauth.png)
</div>
- **Header Auth**: To connect your n8n webhooks using header auth the following fields are required:
- **Name / Key**
- **Value**
<div style={{textAlign: 'center'}}>
![ToolJet - Data source - n8n](/img/datasource-reference/n8n/headerauth.png)
</div>
:::tip
Webhook credentials and instance credentials are different. Please use the credentials that you use with the webhook trigger. Know more: **[Webhook Authentication](https://docs.n8n.io/nodes/n8n-nodes-base.webhook/#:~:text=then%20gets%20deactivated.-,Authentication,-%3A%20The%20Webhook%20node)**.
:::
## Trigger Workflow
Click on `+` button of the query manager at the bottom panel of the editor and the select n8n as the datasource.
You can trigger a workflow with `GET/POST` URL. Choose the request type from the `Methods` dropdown and then provide the required fields:
- **URL parameters** (Support for GET & POST) `Optional`
- **Body** (Only for POST URL) `Required`
<div style={{textAlign: 'center'}}>
![ToolJet - Data source - n8n](/img/datasource-reference/n8n/query.png)
</div>

View file

@ -1,5 +1,5 @@
---
sidebar_position: 13
sidebar_position: 15
---
# PostgreSQL

View file

@ -1,5 +1,5 @@
---
sidebar_position: 14
sidebar_position: 16
---
# Redis

View file

@ -1,5 +1,5 @@
---
sidebar_position: 15
sidebar_position: 17
---
# REST API

View file

@ -10,7 +10,7 @@ ToolJet can connect to Amazon S3 buckets and perform various operation on them.
To add a new S3 source, go to the **Datasources manager** on the left sidebar of the app editor and click on `Add datasource` button. Select **AWS S3** from the modal that pops up.
ToolJet requires the following to connect to your DynamoDB:
ToolJet requires the following to connect to your AWS S3:
- **Region**
- **Access key**
@ -18,7 +18,11 @@ ToolJet requires the following to connect to your DynamoDB:
It is recommended to create a new IAM user for the database so that you can control the access levels of ToolJet.
![ToolJet - AWS S3 connection](/img/datasource-reference/aws-s3-connect.png)
<div style={{textAlign: 'center'}}>
![ToolJet - AWS S3 connection](/img/datasource-reference/aws-s3/aws-s3-modal.png)
</div>
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 data source.
@ -26,16 +30,103 @@ Click on **Test connection** button to verify if the credentials are correct and
Click on `+` button of the **query manager** at the bottom panel of the editor and select the data source added in the previous step as the data source. Select the operation that you want to perform and click **Save** to save the query.
![ToolJet - AWS S3 query](/img/datasource-reference/aws-s3-query.png)
<div style={{textAlign: 'center'}}>
![ToolJet - AWS S3 connection](/img/datasource-reference/aws-s3/aws-s3-query.png)
</div>
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)
Query results can be transformed using transformations. Read our transformations documentation to see how: **[link](/docs/tutorial/transformations)**
:::
## Query operations
You can create query for AWS S3 data source to perform several actions such as:
1. **[Read object](/docs/data-sources/s3#read-object)**
2. **[Upload object](/docs/data-sources/s3#upload-object)**
3. **[List buckets](/docs/data-sources/s3#list-buckets)**
4. **[List objects in a bucket](/docs/data-sources/s3#list-objects-in-a-bucket)**
5. **[Signed url for download](/docs/data-sources/s3#signed-url-for-download)**
6. **[Signed url for upload](/docs/data-sources/s3#signed-url-for-upload)**
### Read object
You can read an object in a bucket by using this operation. It requires two parameters - **Bucket** name and **Key**.
<div style={{textAlign: 'center'}}>
![ToolJet - AWS S3 connection](/img/datasource-reference/aws-s3/read-object.png)
</div>
### Upload object
You can use this operation to upload objects(files) to your S3 bucket. It requires four parameters:
1. **Bucket**: Specify the bucket name
2. **Key**: Key of the object/file
3. **Content type**: Specify file type such as text, image etc.
4. **Upload data**: File/object that is to be uploaded.
<div style={{textAlign: 'center'}}>
![ToolJet - AWS S3 connection](/img/datasource-reference/aws-s3/upload-object.png)
</div>
### List buckets
This operation will list all the buckets in your S3. This does not require any parameter.
<div style={{textAlign: 'center'}}>
![ToolJet - AWS S3 connection](/img/datasource-reference/aws-s3/list-buckets.png)
</div>
### List objects in a bucket
This operation will fetch the list of all the files in your bucket. It requires two parameters:
1. **Bucket**: Bucket name (mandatory)
2. **Prefix**: To limit the response to keys that begin with the specified prefix (optional)
<div style={{textAlign: 'center'}}>
![ToolJet - AWS S3 connection](/img/datasource-reference/aws-s3/list-objects.png)
</div>
### Signed url for download
The object owner can optionally share objects with others by creating a presigned URL, using their own security credentials, to grant time-limited permission to download the objects. For creating a presigned URL, the required parameters are:
1. **Bucket**: name of the bucket for uploading the file
2. **Key**: an object key
3. **Expires in**: an expiration time of URL
<div style={{textAlign: 'center'}}>
![ToolJet - AWS S3 connection](/img/datasource-reference/aws-s3/signed-download.png)
</div>
### Signed url for upload
The presigned URLs are useful if you want your user/customer to be able to upload a specific object to your bucket, but you don't require them to have AWS security credentials or permissions. For creating a presigned URL, the required parameters are:
1. **Bucket**: name of the bucket for uploading the file
2. **Key**: an object key
3. **Expires in**: an expiration time of URL
4. **Content type**: the content type such as text, image etc.
<div style={{textAlign: 'center'}}>
![ToolJet - AWS S3 connection](/img/datasource-reference/aws-s3/signed-upload.png)
</div>
:::info
We built an app to view and upload files to AWS S3 buckets. Check out the complete tutorial [here](https://blog.tooljet.com/building-an-app-to-view-and-upload-files-in-aws-s3-bucket/).
We built an app to view and upload files to AWS S3 buckets. Check out the complete tutorial **[here](https://blog.tooljet.com/building-an-app-to-view-and-upload-files-in-aws-s3-bucket/)**.
:::

View file

@ -1,5 +1,5 @@
---
sidebar_position: 16
sidebar_position: 18
---
# SendGrid

View file

@ -0,0 +1,49 @@
# SMTP
SMTP plugin can connect ToolJet applications to **SMTP servers** for sending emails.
## Connection
A SMTP server can be connected with the following credentails:
- **Host**
- **Port**
- **User**
- **Password**
:::info
You can also test your connection before saving the configuration by clicking on `Test Connection` button.
:::
<div style={{textAlign: 'center'}}>
![ToolJet - Data source - n8n](/img/datasource-reference/smtp/connect.png)
</div>
## Querying SMTP
Go to the query manager at the bottom panel of the editor and click on the `+` button on the left to create a new query. Select `SMTP` from the datasource dropdown.
To create a query for sending email, you will need to provide the following properties:
- **From** `required` : Email address of the sender
- **From Name** : Name of the sender
- **To** `required` : Recipient's email address
- **Subject** : Subject of the email
<div style={{textAlign: 'center'}}>
![ToolJet - Data source - n8n](/img/datasource-reference/smtp/query1.png)
</div>
- **Body** : You can enter the body text either in the form of `raw text` or `html` in their respective fields.
- **Attachments** : Attachments can be added to a SMTP query by referencing the file from the `File Picker` component in the attachments field.
For example, you can set the `Attachments` field value to `{{ components.filepicker1.file }}` or you can pass an array of `{{ name: 'filename.jpg', dataURL: '......' }}` object to accomplish this.
<div style={{textAlign: 'center'}}>
![ToolJet - Data source - n8n](/img/datasource-reference/smtp/query2.png)
</div>

View file

@ -1,3 +1,7 @@
---
sidebar_position: 19
---
# Snowflake
ToolJet can connect to Snowflake databases to read and write data.
@ -7,10 +11,10 @@ ToolJet can connect to Snowflake databases 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. You can find snowflake docs on network policies [https://docs.snowflake.com/en/user-guide/network-policies.html](here)
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. You can find snowflake docs on network policies **[here](https://docs.snowflake.com/en/user-guide/network-policies.html)**.
To add a new Snowflake database, click on the '+' button on data sources panel at the left-bottom corner of the app editor. Select Snowflake from the modal that pops up.
To add a new Snowflake database, click on the `+` button on data sources panel at the left-bottom corner of the app editor. Select Snowflake from the modal that pops up.
ToolJet requires the following to connect to your Snowflake database.
@ -18,17 +22,29 @@ ToolJet requires the following to connect to your Snowflake database.
- **Username**
- **Password**
You can also configure for [additional optional parameters](https://docs.snowflake.com/en/user-guide/nodejs-driver-use.html#additional-connection-options).
:::info
You can also configure for **[additional optional parameters](https://docs.snowflake.com/en/user-guide/nodejs-driver-use.html#additional-connection-options)**.
:::
<img src="/img/datasource-reference/snowflake/snowflake-connect.png" alt="ToolJet - Snowflake connection" height="250"/>
<div style={{textAlign: 'center'}}>
![ToolJet - Snowflake connection](/img/datasource-reference/snowflake/snowflake-connect.png)
</div>
## Querying Snowflake
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. Query manager then can be used to write raw SQL queries.
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. Query manager then can be used to write raw SQL queries.
<img src="/img/datasource-reference/snowflake/snowflake-query.png" alt="ToolJet - Snowflake query" height="250"/>
<div style={{textAlign: 'center'}}>
Click on the 'run' button to run the query. NOTE: Query should be saved before running.
![ToolJet - Snowflake query](/img/datasource-reference/snowflake/snowflake-query.png)
</div>
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)

View file

@ -1,5 +1,5 @@
---
sidebar_position: 17
sidebar_position: 20
---
# TypeSense

View file

@ -10,16 +10,18 @@ sidebar_label: Architecture
ToolJet have two main components: **ToolJet Server** and **ToolJet Client**.
1. ### ToolJet Server
ToolJet server is a Node.js API application. Server is responsible for authentication, authorization, persisting application definitions, running queries, storing data source credentials securely and more.
### 1. ToolJet Server
Dependencies:
- PostgreSQL - ToolJet server persists data to a postgres database.
- Email service (SMTP/Sendgrid/Mailgun/etc) - Required to send user invitations and password reset emails.
ToolJet server is a Node.js API application. Server is responsible for authentication, authorization, persisting application definitions, running queries, storing data source credentials securely and more.
**Dependencies:**
- **PostgreSQL** - ToolJet server persists data to a postgres database.
- **Email service** (SMTP/Sendgrid/Mailgun/etc) - Required to send user invitations and password reset emails.
2. ### ToolJet Client
ToolJet client is a ReactJS application. Client is responsible for visually editing the applications, building & editing queries, rendering applications, executing events and their trigger, etc.
### 2. ToolJet Client
ToolJet client is a ReactJS application. Client is responsible for visually editing the applications, building & editing queries, rendering applications, executing events and their trigger, etc.
## Requirements
1. Node version 14.x
1. **Node version 14.x**

View file

@ -6,83 +6,84 @@ sidebar_label: Docker
# Deploying ToolJet using docker-compose
:::info
You should setup a PostgreSQL database manually to be used by the ToolJet server.
You should setup a PostgreSQL database manually to be used by the ToolJet server.
:::
Follow the steps below to deploy ToolJet on a server using docker-compose. This setup will deploy both ToolJet server and ToolJet client.
Follow the steps below to deploy ToolJet on a server using docker-compose. This setup will deploy both **ToolJet server** and **ToolJet client**.
1. Setup a PostgreSQL database and make sure that the database is accessible.
2. Make sure that the server can receive traffic on port 80, 443 and 22.
For example, if the server is an AWS EC2 instance and the installation should receive traffic from the internet, the inbound rules of the security group should look like this:
2. Make sure that the server can receive traffic on port 80, 443 and 22.
For example, if the server is an AWS EC2 instance and the installation should receive traffic from the internet, the inbound rules of the security group should look like this:
protocol| port | allowed_cidr|
--------| ------- | ----------- |
tcp | 22 | your IP |
tcp | 80 | 0.0.0.0/0 |
tcp | 443 | 0.0.0.0/0 |
| protocol | port | allowed_cidr |
| -------- | ---- | ------------ |
| tcp | 22 | your IP |
| tcp | 80 | 0.0.0.0/0 |
| tcp | 443 | 0.0.0.0/0 |
3. Install docker and docker-compose on the server.
[Docker Installation](https://docs.docker.com/engine/install/)
[Docker Compose Installation](https://docs.docker.com/compose/install/)
- Docs for [Docker Installation](https://docs.docker.com/engine/install/)
- Docs for [Docker Compose Installation](https://docs.docker.com/compose/install/)
4. Download our production docker-compose file into the server by running:
```bash
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/docker/docker-compose.yaml
```
```bash
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/docker/docker-compose.yaml
```
5. Create `.env` file in the current directory (where the docker-compose.yaml file is downloaded):
```bash
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/.env.example mv .env.example .env
```
```bash
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/.env.example mv .env.example .env
```
Set up environment variables in `.env` file as explained in [environment variables reference](/docs/deployment/env-vars)
Set up environment variables in `.env` file as explained in [environment variables reference](/docs/deployment/env-vars)
`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
We use a [lets encrypt](https://letsencrypt.org/) plugin on top of nginx to create TLS certificates on the fly.
:::
:::info
We use a [lets encrypt](https://letsencrypt.org/) plugin on top of nginx to create TLS certificates on the fly.
:::
Examples:
`TOOLJET_HOST=http://12.34.56.78` or
`TOOLJET_HOST=https://yourdomain.com` or
`TOOLJET_HOST=https://tooljet.yourdomain.com`
Examples:
`TOOLJET_HOST=http://12.34.56.78` or
`TOOLJET_HOST=https://yourdomain.com` or
`TOOLJET_HOST=https://tooljet.yourdomain.com`
:::info
Please make sure that `TOOLJET_HOST` starts with either `http://` or `https://`
:::
:::info
Please make sure that `TOOLJET_HOST` starts with either `http://` or `https://`
:::
:::info
If there are self signed HTTPS endpoints that Tooljet needs to connect to, please make sure that `NODE_EXTRA_CA_CERTS` environment variable is set to the absolute path containing the certificates.
:::
:::info
If there are self signed HTTPS endpoints that Tooljet needs to connect to, please make sure that `NODE_EXTRA_CA_CERTS` environment variable is set to the absolute path containing the certificates.
:::
6. Once you've populated the `.env` file, run
```bash
docker-compose up -d
```
to start all the required services.
```bash
docker-compose up -d
```
:::info
If you're running on a linux server, `docker` might need sudo permissions. In that case you can either run:
`sudo docker-compose up -d`
OR
Setup docker to run without root privileges by following the instructions written here https://docs.docker.com/engine/install/linux-postinstall/
:::
to start all the required services.
:::info
If you're running on a linux server, `docker` might need sudo permissions. In that case you can either run:
`sudo docker-compose up -d`
OR
Setup docker to run without root 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.
8. Seed the database:
8. Seed the database:
```bash
docker-compose run server npm run db:seed
```
This seeds the database with a default user with the following credentials:
email: `dev@tooljet.io`
password: `password`
```bash
docker-compose run server npm run db:seed
```
This seeds the database with a default user with the following credentials:
email: `dev@tooljet.io`
password: `password`
9. You're all done, ToolJet client would now be served at the URL you've set in `TOOLJET_HOST`.

View file

@ -34,7 +34,7 @@ Follow the steps below to deploy ToolJet on AWS EC2 instances.
7. Switch to the app directory by running `cd ~/app`. Modify the contents of the `.env` file. ( Eg: `vim .env` )
The default `.env` file looks like this:
```
```bash
TOOLJET_HOST=http://<example>
LOCKBOX_MASTER_KEY=<example>
SECRET_KEY_BASE=<example>
@ -43,7 +43,7 @@ Follow the steps below to deploy ToolJet on AWS EC2 instances.
PG_HOST=<pg host>
PG_PASS=<pg user password>
```
Read [environment variables reference](/docs/deployment/env-vars)
Read **[environment variables reference](/docs/deployment/env-vars)**
:::info
If there are self signed HTTPS endpoints that Tooljet needs to connect to, please make sure that `NODE_EXTRA_CA_CERTS` environment variable is set to the absolute path containing the certificates.

View file

@ -24,7 +24,7 @@ Follow the steps below to deploy ToolJet on Cloud run with `gcloud` CLI.
2. Deploy new cloud run service
:::note
:::info
This command takes the assumption that certain required environment has already been created in secrets. If you haven't created already then use the [secret manager](https://console.cloud.google.com/security/secret-manager).
:::
@ -89,7 +89,7 @@ The default username of the admin is `dev@tooljet.io` and the password is `passw
2. Deploy new cloud run service
:::note
:::info
This command takes the assumption that certain required environment has already been created in secrets. If you haven't created already then use the [secret manager](https://console.cloud.google.com/security/secret-manager).
:::
@ -123,7 +123,7 @@ If you are to use [Public IP](https://cloud.google.com/sql/docs/mysql/connect-ru
If there are self signed HTTPS endpoints that Tooljet needs to connect to, please make sure that `NODE_EXTRA_CA_CERTS` environment variable is set to the absolute path containing the certificates. The certificate can be mount as a volume onto the container using secrets.
:::
3. Create default user (Optional)
3. Create default user **(Optional)**
Signing up requires [SMTP configuration](https://docs.tooljet.com/docs/deployment/env-vars#smtp-configuration--optional-) to be done, but if you want to start off with default user you can run the command by modifying the `args` flag for a one time usage.

View file

@ -8,12 +8,16 @@ sidebar_label: Heroku
Follow the steps below to deploy ToolJet on Heroku:
1. Click the button below to start one click deployment.
<div style={{textAlign: 'center'}}>
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/tooljet/tooljet/tree/main)
</div>
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.
Read [environment variables reference](/docs/deployment/env-vars)
Read **[environment variables reference](/docs/deployment/env-vars)**
4. Open the app.
5. The default username of the admin is `dev@tooljet.io` and the password is `password`.

View file

@ -11,7 +11,7 @@ You should setup a PostgreSQL database manually to be used by ToolJet. We recomm
Follow the steps below to deploy ToolJet on a AKS Kubernetes cluster.
1. Create an AKS cluster and connect to it to start with the deployement. You can follow the steps as mentioned on the [doc](https://docs.microsoft.com/en-us/azure/aks/kubernetes-walkthrough-portal).
1. Create an AKS cluster and connect to it to start with the deployement. You can follow the steps as mentioned on the [ Azure's documentation](https://docs.microsoft.com/en-us/azure/aks/kubernetes-walkthrough-portal).
2. Create k8s deployment
@ -21,9 +21,10 @@ Follow the steps below to deploy ToolJet on a AKS Kubernetes cluster.
Make sure to edit the environment variables in the `deployment.yaml`. We advise to use secrets to setup sensitive information. You can check out the available options [here](https://docs.tooljet.com/docs/deployment/env-vars).
:::info
If there are self signed HTTPS endpoints that Tooljet needs to connect to, please make sure that `NODE_EXTRA_CA_CERTS` environment variable is set to the absolute path containing the certificates. You can make use of kubernetes secrets to mount the certificate file onto the containers.
:::
:::info
If there are self signed HTTPS endpoints that Tooljet needs to connect to, please make sure that `NODE_EXTRA_CA_CERTS` environment variable is set to the absolute path containing the certificates. You can make use of kubernetes secrets to mount the certificate file onto the containers.
:::
3. Create k8s service and reserve a static IP and inorder expose it via a service load balancer as mentioned in the [doc](https://docs.microsoft.com/en-us/azure/aks/static-ip). You can refer `service.yaml`.
```bash
curl -LO https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/AKS/service.yaml
@ -38,8 +39,11 @@ Make sure to edit the environment variables in the `deployment.yaml`. We advise
You will be able to access your ToolJet installation once the pods and services running.
If you want to seed the database with a sample user, please SSH into a pod and run:
`npm run db:seed --prefix server`.
`npm run db:seed --prefix server`
This seeds the database with a default user with the following credentials:
email: `dev@tooljet.io`
password: `password`
**email**: `dev@tooljet.io`
**password**: `password`

View file

@ -64,8 +64,11 @@ It might take a few minutes to provision the managed certificates. [Managed cert
You will be able to access your ToolJet installation once the pods, service and the ingress is running.
If you want to seed the database with a sample user, please SSH into a pod and run:
`npm run db:seed --prefix server`.
`npm run db:seed --prefix server`
This seeds the database with a default user with the following credentials:
email: `dev@tooljet.io`
password: `password`
**emai**: `dev@tooljet.io`
**password**: `password`

View file

@ -16,7 +16,7 @@ Follow the steps below to deploy ToolJet on a Kubernetes cluster.
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
@ -39,10 +39,11 @@ If there are self signed HTTPS endpoints that Tooljet needs to connect to, pleas
```
5. Create a Kubernetes services to publish the Kubernetes deployment that you've created. This step varies with cloud providers. We have a [template](https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/service.yaml) for exposing the ToolJet server as a service using an AWS loadbalancer.
Examples:
Application load balancing on Amazon EKS: https://docs.aws.amazon.com/eks/latest/userguide/alb-ingress.html
GKE Ingress for HTTP(S) Load Balancing: https://cloud.google.com/kubernetes-engine/docs/concepts/ingress
**Examples:**
- [Application load balancing on Amazon EKS](https://docs.aws.amazon.com/eks/latest/userguide/alb-ingress.html)
- [GKE Ingress for HTTP(S) Load Balancing](https://cloud.google.com/kubernetes-engine/docs/concepts/ingress)
:::tip
If you want to serve ToolJet client from services such as Firebase or Netlify, please read the client deployment documentation [here](/docs/deployment/client).
:::
If you want to serve ToolJet client from services such as Firebase or Netlify, please read the client deployment documentation **[here](/docs/deployment/client)**.
:::

View file

@ -4,19 +4,27 @@ sidebar_position: 1
# Introduction
ToolJet is an **open-source low-code framework** to build and deploy custom internal tools. ToolJet can connect to your data sources such as databases ( PostgreSQL, MongoDB, MySQL, Elasticsearch, Firestore, DynamoDB, Redis and more ), API endpoints ( ToolJet supports OAuth2 authorization ) and external services ( Stripe, Slack, Google Sheets, airtable and more ). Once the data sources are connected, ToolJet can run queries on these data sources to fetch and update data. The data fetched from data sources can be visualised and modified using the UI widgets such as tables, charts, forms, etc.
ToolJet is an **open-source low-code framework** to build and deploy custom internal tools. ToolJet can connect to your data sources such as databases ( PostgreSQL, MongoDB, MySQL, Elasticsearch, Firestore, DynamoDB, Redis and more ), API endpoints ( ToolJet supports OAuth2 authorization ) and external services ( Stripe, Slack, Google Sheets, Airtable and more ). Once the data sources are connected, ToolJet can run queries on these data sources to fetch and update data. The data fetched from data sources can be visualised and modified using the UI widgets such as tables, charts, forms, etc.
<img class="screenshot-full" src="https://user-images.githubusercontent.com/7828962/144586771-c6d6cba5-8f79-4e0c-80b4-aa1a38657229.png" alt="ToolJet - introduction" />
<div style={{textAlign: 'center'}}>
![ToolJet - List view widget](/img/introduction/githubstar.png)
</div>
## How ToolJet works
<img class="screenshot-full" src="/img/how-it-works.png" alt="ToolJet - adding datasources" />
<div style={{textAlign: 'center'}}>
ToolJet has just 3 fundamental principles to build apps:
![ToolJet - List view widget](/img/introduction/how-it-works.png)
</div>
**ToolJet has just 3 fundamental principles to build apps:**
- **Connect to data sources:** Connect to your existing data sources such as PostgreSQL, MySQL, Firestore, Stripe, Google Sheets, API endpoints, etc.
- **Build queries:** ToolJet comes with query builders for all supported data sources. ToolJet also supports the use of custom JavaScript code to transform the query results.
- **Customise widgets:** Widgets are the UI components that can be edited using ToolJet's visual app builder ( Eg: tables, charts, forms, etc ) Widgets have events such as `on click`, `on row selected`, `on page changed`, etc. Every UI widget has a dark version.
- **Customise widgets:** Widgets are the UI components that can be edited using ToolJet's visual app builder ( Eg: tables, charts, forms, etc ). Widgets have events such as `on click`, `on row selected`, `on page changed`, etc. Every UI widget has a dark version.
ToolJet binds together the data sources, queries and widgets to convert business logic into custom applications.
## Getting Started
@ -27,18 +35,20 @@ These resources will help you to quickly build and deploy apps using ToolJet:
- **[Basic Tutorial](/docs/tutorial/creating-app)** - Learn how to build simple UI and connect to data sources.
- **[Deploy](/docs/contributing-guide/setup/docker)** - Learn how to deploy ToolJet on Heroku, Kubernetes, etc
The references for datasources and widgets:
The references for data sources and widgets:
- **[Datasource Reference](/docs/data-sources/redis)**
- **[Widget Reference](/docs/widgets/table)**
## Complete tutorials
- [Build a WhatsApp CRM](https://blog.tooljet.com/build-a-whatsapp-crm-using-tooljet-within-10-mins/)
- [Build a cryptocurrency dashboard](https://blog.tooljet.com/how-to-build-a-cryptocurrency-dashboard-in-10-minutes/)
- [Build a Redis GUI](https://blog.tooljet.com/building-a-redis-gui-using-tooljet-in-5-minutes/)
- **[Build a GitHub star history tracker](https://blog.tooljet.com/build-github-stars-history-app-in-5-minutes-using-low-code/)**
- **[Build an AWS S3 file explorer app](https://blog.tooljet.com/building-an-app-to-view-and-upload-files-in-aws-s3-bucket/)**
- **[Build a WhatsApp CRM](https://blog.tooljet.com/build-a-whatsapp-crm-using-tooljet-within-10-mins/)**
- **[Build a cryptocurrency dashboard](https://blog.tooljet.com/how-to-build-a-cryptocurrency-dashboard-in-10-minutes/)**
- **[Build a Redis GUI](https://blog.tooljet.com/building-a-redis-gui-using-tooljet-in-5-minutes/)**
## Help and Support
We have extensively documented the features of ToolJet, but in case you are stuck, please feel to mail us: hello@tooljet.com.
If you are using ToolJet cloud, click on the chat icon at the bottom-left corner for instant help.
If you have found a bug, please create a GitHub issue for the same.
Also, feel free to join our highly active [slack community](https://join.slack.com/t/tooljet/shared_invite/zt-r2neyfcw-KD1COL6t2kgVTlTtAV5rtg).
- We have extensively documented the features of ToolJet, but in case you are stuck, please feel to e-mail us at **hello@tooljet.com**
- If you are using ToolJet cloud, click on the chat icon at the bottom-left corner for instant help.
- If you have found a bug, please create a **[GitHub issue](https://github.com/ToolJet/ToolJet/issues)** for the same.
- Feel free to join our highly active **[Slack Community](https://join.slack.com/t/tooljet/shared_invite/zt-r2neyfcw-KD1COL6t2kgVTlTtAV5rtg)**.

View file

@ -39,7 +39,7 @@ Before triggering `Show modal action` we need to add a modal widget to the canva
<div style={{textAlign: 'center'}}>
![ToolJet - Tutorial - Adding a widget](/img/tutorial/adding-widget/adding-widget-to-modal.gif)
![ToolJet - Tutorial - Adding a widget](/img/tutorial/adding-widget/modal.gif)
</div>
@ -64,13 +64,15 @@ Click on the widget to open the inspect panel on right sidebar. Here you can cha
## Connecting data with widget
Now we will connect the `data` object of the `fetch customers` query with the table. Click on the table widget to open the inspector on right sidebar. We can see that the data property of the table have an empty array as the value. The data field, like almost every other field on the editor supports single-line javascript code within double brackets. Variable suggestions will be shows as a dropdown while you type the code in the field.
Let's select `data` object of the 'postgresql' query.
Let's select `data` object of the 'postgresql' query.
` {{queries.postgresql1.data}}`
Since we have already run the query in previous step, the data will be immediately displayed in the table.
<div style={{textAlign: 'center'}}>
![ToolJet - Tutorial - Adding a widget](/img/tutorial/adding-widget/table-data.gif)
![ToolJet - Tutorial - Adding a widget](/img/tutorial/adding-widget/table-data.png)
</div>

View file

@ -29,7 +29,7 @@ Query results can be previewed by clicking the `preview` button. Previewing quer
<div style={{textAlign: 'center'}}>
![ToolJet - Tutorial - Building a query](/img/tutorial/building-queries/preview.gif)
![ToolJet - Tutorial - Building a query](/img/tutorial/building-queries/preview.png)
</div>

View file

@ -22,7 +22,7 @@ The query will now look like this:
<div style={{textAlign: 'center'}}>
![ToolJet - Tutorial - Query result transformations](/img/tutorial/transformations/transform.gif)
![ToolJet - Tutorial - Query result transformations](/img/tutorial/transformations/transform.png)
</div>
@ -30,7 +30,7 @@ Click the `create` button to create the query. Saved queries can be run using th
<div style={{textAlign: 'center'}}>
![ToolJet - Tutorial - Query result transformations](/img/tutorial/transformations/result.gif)
![ToolJet - Tutorial - Query result transformations](/img/tutorial/transformations/result.png)
</div>

View file

@ -14,6 +14,6 @@ On select event is triggered when an option is selected.
| properties | description |
| ----------- | ----------- |
| Label | The text is to be used as the label for the multiselect widget. |
| Default value | The value of the default option. |
| Default value | The value of the default option. This should always be an array. |
| Option values | Values for different items/options in the list of the multiselect. |
| Option labels | Labels for different items/options in the list of the multiselect. |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

View file

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

View file

@ -1,26 +0,0 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
[
"@babel/plugin-proposal-class-properties"
],
[
"console-source",
{
"segments": 1, // NOT REQUIRED
// 0 = full file path (Default)
// 1 = file name ONLY
// 2 = file name and last segment
"splitSegment": "/" // How to split the path - NOT REQUIRED
// Default is / for Linux and OSX
// Windows users can use "\\" here if needed
}
],
[
"@babel/transform-runtime"
]
]
}

View file

@ -2,7 +2,8 @@
"env": {
"browser": true,
"amd": true,
"es2021": true
"es2021": true,
"jest/globals": true
},
"extends": [
"eslint:recommended",
@ -20,7 +21,7 @@
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["react", "prettier"],
"plugins": ["react", "prettier", "jest"],
"rules": {
"prettier/prettier": [
"error",
@ -40,7 +41,12 @@
"varsIgnorePattern": "^_"
}],
"react/no-deprecated": 0,
"no-prototype-builtins": 0
"no-prototype-builtins": 0,
"jest/no-disabled-tests": "warn",
"jest/no-focused-tests": "error",
"jest/no-identical-title": "error",
"jest/prefer-to-have-length": "warn",
"jest/valid-expect": "error"
},
"settings": {
"react": {

View file

@ -0,0 +1,9 @@
module.exports = {
process() {
return 'module.exports = {};';
},
getCacheKey() {
// The output is always the same.
return 'svgTransform';
},
};

19
frontend/babel.config.js Normal file
View file

@ -0,0 +1,19 @@
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: [
['@babel/plugin-proposal-class-properties'],
[
'console-source',
{
segments: 1, // NOT REQUIRED
// 0 = full file path (Default)
// 1 = file name ONLY
// 2 = file name and last segment
splitSegment: '/', // How to split the path - NOT REQUIRED
// Default is / for Linux and OSX
// Windows users can use "\\" here if needed
},
],
['@babel/transform-runtime'],
],
};

13596
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -88,9 +88,11 @@
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-webpack": "^0.13.1",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jest": "^26.1.1",
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-react": "^7.25.2",
"eslint-plugin-react-hooks": "^4.2.0",
"jest": "^27.5.1",
"path": "^0.12.7",
"prettier": "^2.3.2",
"webpack-dev-server": "^3.11.2"
@ -99,7 +101,8 @@
"start": "webpack serve --port 8082 --host 0.0.0.0",
"build": "webpack --mode production && cp -a ./assets/. ./build/assets/",
"lint": "eslint . '**/*.{js,jsx}'",
"format": "eslint . --fix '**/*.{js,jsx}'"
"format": "eslint . --fix '**/*.{js,jsx}'",
"test": "jest"
},
"eslintConfig": {
"extends": "react-app"
@ -107,5 +110,27 @@
"browserslist": {
"production": [],
"development": []
},
"jest": {
"transform": {
"^.+\\.js?$": "babel-jest",
"^.+\\.svg$": "<rootDir>/__mocks__/svg.js"
},
"transformIgnorePatterns": [
"node_modules/(?!(react|mpx-error-boundary)/)"
],
"moduleNameMapper": {
"@/(.*)": "<rootDir>/src/$1"
},
"testEnvironment": "jest-environment-jsdom",
"moduleDirectories": [
"node_modules",
"src"
],
"moduleFileExtensions": [
"js",
"json",
"jsx"
]
}
}

View file

@ -21,12 +21,14 @@ export const AppVersionsManager = function AppVersionsManager({
useEffect(() => {
setCreateAppVersionFrom(editingAppVersion);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appVersions]);
useEffect(() => {
appVersionService.getAll(appId).then((data) => {
setAppVersions(data.versions);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const wrapperRef = useRef(null);

View file

@ -1,8 +1,8 @@
import React from 'react';
import { ToolTip } from './Components/ToolTip';
// import { ToolTip } from './Components/ToolTip';
import FxButton from './FxButton';
export const Toggle = ({ value, onChange, paramLabel, forceCodeBox }) => {
export const Toggle = ({ value, onChange, forceCodeBox }) => {
return (
<div className="row">
<div className="col">

View file

@ -172,20 +172,19 @@ export const FilePicker = ({
});
setSelectedFiles(fileData);
onComponentOptionChanged(component, 'file', fileData).then(() =>
onEvent('onFileSelected', { component }).then(() => {
setAccepted(true);
// eslint-disable-next-line no-unused-vars
return new Promise(function (resolve, reject) {
setTimeout(() => {
setShowSelectedFiles(true);
setAccepted(false);
onComponentOptionChanged(component, 'isParsing', false);
resolve();
}, 600);
});
})
);
onComponentOptionChanged(component, 'file', fileData);
onEvent('onFileSelected', { component }).then(() => {
setAccepted(true);
// eslint-disable-next-line no-unused-vars
return new Promise(function (resolve, reject) {
setTimeout(() => {
setShowSelectedFiles(true);
setAccepted(false);
onComponentOptionChanged(component, 'isParsing', false);
resolve();
}, 600);
});
});
}
if (fileRejections.length > 0) {
@ -231,7 +230,7 @@ export const FilePicker = ({
<FilePicker.Signifiers
signifier={selectedFiles.length > 0}
feedback={acceptedFile.name}
cls="text-secondary d-flex justify-content-start file-list"
cls="text-secondary d-flex justify-content-start file-list mb-2"
/>
</div>
<div className="col-2 mt-1">
@ -274,7 +273,7 @@ export const FilePicker = ({
FilePicker.Signifiers = ({ signifier, feedback, cls }) => {
if (signifier) {
return <center>{feedback === null ? <div className={cls}></div> : <p className={cls}>{feedback}</p>}</center>;
return <>{feedback === null ? <div className={cls}></div> : <p className={cls}>{feedback}</p>}</>;
}
return null;

View file

@ -46,7 +46,7 @@ export const Modal = function Modal({
keyboard={true}
enforceFocus={false}
animation={false}
onEscapeKeyDown={() => setShowModal(false)}
onEscapeKeyDown={() => hideModal()}
>
{containerProps.mode === 'edit' && (
<ConfigHandle id={id} component={component} setSelectedComponent={containerProps.onComponentClick} />

View file

@ -243,12 +243,22 @@ export function Table({
return rows.filter((row) => row.values[columnIds[0]] === filterValue.value);
}
if (filterValue.operation === 'ne') {
return rows.filter((row) => row.values[columnIds[0]] !== filterValue.value);
}
if (filterValue.operation === 'matches') {
return rows.filter((row) =>
row.values[columnIds[0]].toString().toLowerCase().includes(filterValue.value.toLowerCase())
);
}
if (filterValue.operation === 'nl') {
return rows.filter(
(row) => !row.values[columnIds[0]].toString().toLowerCase().includes(filterValue.value.toLowerCase())
);
}
if (filterValue.operation === 'gt') {
return rows.filter((row) => row.values[columnIds[0]] > filterValue.value);
}
@ -689,6 +699,7 @@ export function Table({
() => [...leftActionsCellData, ...columnData, ...rightActionsCellData],
[
JSON.stringify(columnData),
JSON.stringify(actions),
leftActionsCellData.length,
rightActionsCellData.length,
componentState.changeSet,
@ -1051,7 +1062,9 @@ export function Table({
options={[
{ name: 'contains', value: 'contains' },
{ name: 'matches', value: 'matches' },
{ name: 'does not match', value: 'nl' },
{ name: 'equals', value: 'equals' },
{ name: 'does not equal', value: 'ne' },
{ name: 'greater than', value: 'gt' },
{ name: 'less than', value: 'lt' },
{ name: 'greater than or equals', value: 'gte' },

View file

@ -19,6 +19,7 @@ export const Toggle = ({ readOnly, value, onChange, activeColor }) => {
onClick={() => {
if (!readOnly) toggle();
}}
disabled={readOnly}
/>
</label>
</div>

View file

@ -30,12 +30,14 @@ export const Timer = function Timer({ height, properties = {}, styles, setExpose
setState('initial');
fireEvent('onCountDownFinish');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [time]);
useEffect(() => {
intervalId && clearInterval(intervalId);
setState('initial');
setTime(getTimeObj(getDefaultValue));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [properties.type, getDefaultValue]);
useEffect(() => {

View file

@ -25,8 +25,6 @@ import {
setStateAsync,
computeComponentState,
getSvgIcon,
addToLocalStorage,
getDataFromLocalStorage,
} from '@/_helpers/appUtils';
import { Confirm } from './Viewer/Confirm';
import ReactTooltip from 'react-tooltip';
@ -45,9 +43,9 @@ import DesktopSelectedIcon from './Icons/desktop-selected.svg';
import Modal from 'react-bootstrap/Modal';
import Button from 'react-bootstrap/Button';
import { AppVersionsManager } from './AppVersionsManager';
import * as Driver from 'driver.js';
import 'driver.js/dist/driver.min.css';
import { SearchBoxComponent } from '@/_ui/Search';
import { initEditorWalkThrough } from '@/_helpers/createWalkThrough';
import { createWebsocketConnection } from '@/_helpers/websocketConnection';
setAutoFreeze(false);
enablePatches();
@ -58,8 +56,13 @@ class Editor extends React.Component {
const appId = this.props.match.params.id;
const currentUser = authenticationService.currentUserValue;
const { socket } = createWebsocketConnection(appId);
let userVars = {};
this.socket = socket;
if (currentUser) {
userVars = {
email: currentUser.email,
@ -114,7 +117,6 @@ class Editor extends React.Component {
isDeletingDataQuery: false,
showHiddenOptionsForDataQueryId: null,
showQueryConfirmation: false,
socket: null,
showInitVersionCreateModal: false,
isCreatingInitVersion: false,
initVersionName: 'v1',
@ -139,100 +141,12 @@ class Editor extends React.Component {
this.fetchApp();
this.initComponentVersioning();
this.initEventListeners();
config.COMMENT_FEATURE_ENABLE && this.initWebSocket();
this.setState({
currentSidebarTab: 2,
selectedComponent: null,
});
}
initWalkThrough() {
const driver = new Driver({
allowClose: true,
closeBtnText: 'Skip',
nextBtnText: 'Next',
prevBtnText: 'Previous',
padding: 2,
onReset: () => {
// Here we need to write the logic to update walkthroughCompleted column of the current user.
addToLocalStorage({ key: 'walkthroughCompleted', value: true });
},
className: `${this.props.darkMode ? 'dark-theme' : 'light-theme'}-walkthrough`,
});
if (
getDataFromLocalStorage('walkthroughCompleted') == undefined ||
!getDataFromLocalStorage('walkthroughCompleted')
) {
driver.defineSteps([
{
element: '.component-image-holder',
popover: {
title: 'Drag and drop widgets',
description: 'From the widget sidebar, drag and drop widgets to the canvas.',
position: 'left',
closeBtnText: 'Skip (1/6)',
},
},
{
element: '.sidebar-datasources',
popover: {
title: 'Connect to data sources',
description: 'You can manage your data sources from here.',
position: 'right',
closeBtnText: 'Skip (2/6)',
},
},
{
element: '.left-sidebar-inspector',
popover: {
title: 'Inspector',
description: 'Inspector lets you check the properties of widgets, results of queries etc.',
position: 'right',
closeBtnText: 'Skip (3/6)',
},
},
{
element: '.queries-header ',
popover: {
title: 'Create queries',
description:
'Create queries to interact with your data sources, run JavaScript snippets and to make API requests.',
position: 'top',
closeBtnText: 'Skip (4/6)',
},
},
{
element: '.release-buttons',
popover: {
title: 'Preview, release & share',
description:
'Click on preview to view the current changes on app viewer. Click on share button to view the sharing options. Release the editing version to make the changes live. Released versions cannot be modified, you will have to create another version to make more changes.',
position: 'bottom',
closeBtnText: 'Skip (5/6)',
},
},
{
element: '.sidebar-comments',
popover: {
title: 'Collaborate',
description: 'Add comments on canvas and tag your team members to collaborate.',
position: 'right',
closeBtnText: 'Skip (6/6)',
},
},
]);
driver.start();
}
}
componentDidUpdate(prevProps, prevState) {
if (prevState.editingVersion == undefined && this.state.editingVersion) {
this.initWalkThrough();
}
}
isVersionReleased = (version = this.state.editingVersion) => {
if (isEmpty(version)) {
return false;
@ -291,59 +205,10 @@ class Editor extends React.Component {
componentWillUnmount() {
document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('mouseup', this.onMouseUp);
if (this.state.socket) {
this.state.socket?.close();
}
document.title = 'Tooljet - Dashboard';
this.socket && this.socket?.close();
}
getWebsocketUrl = () => {
const re = /https?:\/\//g;
if (re.test(config.apiUrl)) return config.apiUrl.replace(/(^\w+:|^)\/\//, '').replace('/api', '');
return window.location.host;
};
initWebSocket = () => {
// TODO: add retry policy
const socket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${this.getWebsocketUrl()}`);
const appId = this.props.match.params.id;
// Connection opened
socket.addEventListener('open', function (event) {
console.log('connection established', event);
const currentUser = JSON.parse(localStorage.getItem('currentUser'));
socket.send(
JSON.stringify({
event: 'authenticate',
data: currentUser.auth_token,
})
);
socket.send(
JSON.stringify({
event: 'subscribe',
data: appId,
})
);
});
// Connection closed
socket.addEventListener('close', function (event) {
console.log('connection closed', event);
});
// Listen for possible errors
socket.addEventListener('error', function (event) {
console.log('WebSocket error: ', event);
});
this.setState({
socket,
});
};
// 1. When we receive an undoable action we can always undo but cannot redo anymore.
// 2. Whenever you perform an undo you can always redo and keep doing undo as long as we have a patch for it.
// 3. Whenever you redo you can always undo and keep doing redo as long as we have a patch for it.
@ -912,12 +777,14 @@ class Editor extends React.Component {
</span>
);
};
handleKeyPress = (event) => {
if (event.key === 'Enter') {
// eslint-disable-next-line no-undef
this.createInitVersion();
}
};
createInitVersion = () => {
const newVersionName = this.state.initVersionName;
const appId = this.state.appId;
@ -925,10 +792,15 @@ class Editor extends React.Component {
if (!isEmpty(newVersionName?.trim())) {
this.setState({ isCreatingInitVersion: true });
appVersionService.create(appId, newVersionName).then(() => {
this.setState({
showInitVersionCreateModal: false,
isCreatingInitVersion: false,
});
this.setState(
{
showInitVersionCreateModal: false,
isCreatingInitVersion: false,
},
() => {
initEditorWalkThrough();
}
);
toast.success('Version Created');
this.fetchApp();
});
@ -961,7 +833,6 @@ class Editor extends React.Component {
enforceFocus={false}
animation={false}
centered={true}
// eslint-disable-next-line no-undef
>
<Modal.Header>
<Modal.Title>Create Version</Modal.Title>
@ -1211,7 +1082,7 @@ class Editor extends React.Component {
<>
<Container
canvasWidth={this.getCanvasWidth()}
socket={this.state.socket}
socket={this.socket}
showComments={showComments}
appVersionsId={this.state?.editingVersion?.id}
appDefinition={appDefinition}
@ -1445,7 +1316,7 @@ class Editor extends React.Component {
</div>
{config.COMMENT_FEATURE_ENABLE && showComments && (
<CommentNotifications
socket={this.state.socket}
socket={this.socket}
appVersionsId={this.state?.editingVersion?.id}
toggleComments={this.toggleComments}
/>

View file

@ -11,6 +11,7 @@ import { allSvgs } from '@tooljet/plugins/client';
import { EventManager } from '../Inspector/EventManager';
import { CodeHinter } from '../CodeBuilder/CodeHinter';
import { DataSourceTypes } from '../DataSourceManager/SourceComponents';
import RunjsIcon from '../Icons/runjs.svg';
const queryNameRegex = new RegExp('^[A-Za-z0-9_-]*$');
@ -223,12 +224,21 @@ let QueryManager = class QueryManager extends React.Component {
};
renderDataSourceOption = (props) => {
//Todo: add icon for the "runjs" query
const Icon = allSvgs[props.kind];
return (
<div>
{Icon && <Icon style={{ height: 25, width: 25 }} />}
<span className={`mx-2 ${this.props.darkMode ? 'text-white' : 'text-muted'}`}>{props.label}</span>
{props.kind === 'runjs' ? (
<RunjsIcon style={{ height: 25, width: 25, marginTop: '-3px' }} />
) : (
Icon && <Icon style={{ height: 25, width: 25 }} />
)}
<span
style={{ height: '25px', display: 'inline-block', marginTop: '3.5px' }}
className={`mx-2 ${this.props.darkMode ? 'text-white' : 'text-muted'}`}
>
{props.label}
</span>
</div>
);
};
@ -399,7 +409,7 @@ let QueryManager = class QueryManager extends React.Component {
}}
className={`btn button-family-secondary m-1 float-right1 ${previewLoading ? 'button-loading' : ''} ${
this.props.darkMode ? 'dark' : ''
} `}
} ${this.state.selectedDataSource ? '' : 'disabled'}`}
style={{ width: '72px', height: '28px' }}
>
Preview
@ -409,7 +419,9 @@ let QueryManager = class QueryManager extends React.Component {
<button
onClick={this.createOrUpdateDataQuery}
disabled={buttonDisabled}
className={`btn btn-primary m-1 float-right ${isUpdating || isCreating ? 'btn-loading' : ''}`}
className={`btn btn-primary m-1 float-right ${isUpdating || isCreating ? 'btn-loading' : ''} ${
this.state.selectedDataSource ? '' : 'disabled'
}`}
style={{ width: '72px', height: '28px' }}
>
{buttonText}

View file

@ -390,7 +390,7 @@ export const SubContainer = ({
id={`canvas-${parent}`}
className={`real-canvas ${(isDragging || isResizing) && !readOnly ? ' show-grid' : ''}`}
>
{Object.keys(childComponents).map((key, index) => (
{Object.keys(childComponents).map((key) => (
<DraggableBox
onComponentClick={onComponentClick}
onEvent={onEvent}

View file

@ -40,6 +40,7 @@ export default function AppCard({
useEffect(() => {
!isMenuOpen && setFocused(!!isHovered);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isHovered]);
const updated = moment(app.created_at).fromNow(true);

View file

@ -201,9 +201,18 @@ const DynamicForm = ({
return (
<>
<div className="row">
<div className="col-md-12 my-2">
{flipComponentDropdown.commonFields && getLayout(flipComponentDropdown.commonFields)}
<div
className={cx('my-2', {
'col-md-12': !flipComponentDropdown.className,
[flipComponentDropdown.className]: !!flipComponentDropdown.className,
})}
>
{flipComponentDropdown.label && <label className="form-label">{flipComponentDropdown.label}</label>}
<Select {...getElementProps(flipComponentDropdown)} />
{flipComponentDropdown.helpText && (
<span className="flip-dropdown-help-text">{flipComponentDropdown.helpText}</span>
)}
</div>
</div>
{getLayout(obj[selector])}

View file

@ -3,10 +3,11 @@ import { Link } from 'react-router-dom';
import { authenticationService } from '@/_services';
import { history } from '@/_helpers';
import { DarkModeToggle } from './DarkModeToggle';
import cx from 'classnames';
import LogoIcon from '../Editor/Icons/logo.svg';
export const Header = function Header({ switchDarkMode, darkMode }) {
// eslint-disable-next-line no-unused-vars
const [pathName, setPathName] = useState(document.location.pathname);
useEffect(() => {

View file

@ -0,0 +1,113 @@
const { getQueryVariables, runQuery, setTablePageIndex, computeComponentState } = require('../appUtils.js');
jest.mock(
'config',
() => {
return {
apiUrl: `http://localhost:3000/api`,
SERVER_IP: process.env.SERVER_IP,
COMMENT_FEATURE_ENABLE: true,
};
},
{ virtual: true }
);
describe('getQueryVariables method', () => {
test('Returns empty object.', async () => {
const options = []; // not one.of(string, object) which are the available options
await expect(getQueryVariables(options)).toStrictEqual({});
});
test('Tests when options type is string', async () => {
const options = 'options type is string';
await expect(getQueryVariables(options)).toStrictEqual({});
});
test('Tests when options type is object', async () => {
const options = { key: 'value' };
await expect(getQueryVariables(options)).toStrictEqual({});
});
});
describe('runQuery method', () => {
test('Returns undefined when no query has been associated with the action.', async () => {
const _ref = {
state: {
app: {
data_queries: [],
},
},
};
await expect(runQuery(_ref)).toBe(undefined);
});
test('Returns undefined when requestConfirmation is true.', async () => {
const _ref = {
state: {
app: {
data_queries: [{ id: 1, options: { requestConfirmation: true } }],
},
},
setState: jest.fn(),
};
await expect(runQuery(_ref, 1)).toBe(undefined);
});
// test('Tests when kind=runjs.', async () => {
// const _ref = {
// state: {
// app: {
// data_queries: [{ id: 1, kind: 'runjs', options: { requestConfirmation: false } }],
// },
// currentState: {
// queries: {},
// },
// },
// setState: jest.fn(() => {}),
// };
// // Unable to call the callback function in setState ..
// await expect(runQuery(_ref, 1)).resolves.toBe({});
// });
});
describe('setTablePageIndex method', () => {
test('Returns undefined when no table is associated with this event', async () => {
const _ref = {
state: {
app: {
data_queries: [],
},
currentState: {
components: {},
},
},
setState: jest.fn(() => {}),
};
await expect(setTablePageIndex(_ref)).resolves.toBe(undefined);
});
});
describe('computeComponentState method', () => {
test('Returns default', async () => {
const _ref = {
state: {
app: {
data_queries: [],
},
currentState: {
components: {},
},
},
setState: (_, resolve) => resolve('resolved'),
};
await expect(computeComponentState(_ref)).resolves.toBe('resolved');
});
});

View file

@ -464,7 +464,7 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') {
}
}
function getQueryVariables(options, state) {
export function getQueryVariables(options, state) {
let queryVariables = {};
const optionsType = typeof options;
switch (optionsType) {
@ -591,6 +591,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode)
_self.setState({ currentState: newState }, () => {
let queryExecutionPromise = null;
if (query.kind === 'runjs') {
console.log('here');
queryExecutionPromise = executeMultilineJS(_self.state.currentState, query.options.code);
} else {
queryExecutionPromise = dataqueryService.run(queryId, options);
@ -732,7 +733,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode)
});
}
function setTablePageIndex(_ref, tableId, index) {
export function setTablePageIndex(_ref, tableId, index) {
if (_.isEmpty(tableId)) {
console.log('No table is associated with this event.');
return Promise.resolve();
@ -752,7 +753,7 @@ export function renderTooltip({ props, text }) {
);
}
export function computeComponentState(_ref, components) {
export function computeComponentState(_ref, components = {}) {
let componentState = {};
const currentComponents = _ref.state.currentState.components;
Object.keys(components).forEach((key) => {

View file

@ -0,0 +1,85 @@
import * as Driver from 'driver.js';
import { addToLocalStorage, getDataFromLocalStorage } from '@/_helpers/appUtils';
import 'driver.js/dist/driver.min.css';
export const initEditorWalkThrough = () => {
if (
getDataFromLocalStorage('walkthroughCompleted') == undefined ||
!getDataFromLocalStorage('walkthroughCompleted')
) {
const darkMode = getDataFromLocalStorage('darkMode') === 'true';
const driver = new Driver({
allowClose: true,
closeBtnText: 'Skip',
nextBtnText: 'Next',
prevBtnText: 'Previous',
padding: 2,
onReset: () => {
// Here we need to write the logic to update walkthroughCompleted column of the current user.
addToLocalStorage({ key: 'walkthroughCompleted', value: true });
},
className: `${darkMode ? 'dark-theme' : 'light-theme'}-walkthrough`,
});
driver.defineSteps([
{
element: '.component-image-holder',
popover: {
title: 'Drag and drop widgets',
description: 'From the widget sidebar, drag and drop widgets to the canvas.',
position: 'left',
closeBtnText: 'Skip (1/6)',
},
},
{
element: '.sidebar-datasources',
popover: {
title: 'Connect to data sources',
description: 'You can manage your data sources from here.',
position: 'right',
closeBtnText: 'Skip (2/6)',
},
},
{
element: '.left-sidebar-inspector',
popover: {
title: 'Inspector',
description: 'Inspector lets you check the properties of widgets, results of queries etc.',
position: 'right',
closeBtnText: 'Skip (3/6)',
},
},
{
element: '.queries-header ',
popover: {
title: 'Create queries',
description:
'Create queries to interact with your data sources, run JavaScript snippets and to make API requests.',
position: 'top',
closeBtnText: 'Skip (4/6)',
},
},
{
element: '.release-buttons',
popover: {
title: 'Preview, release & share',
description:
'Click on preview to view the current changes on app viewer. Click on share button to view the sharing options. Release the editing version to make the changes live. Released versions cannot be modified, you will have to create another version to make more changes.',
position: 'bottom',
closeBtnText: 'Skip (5/6)',
},
},
{
element: '.sidebar-comments',
popover: {
title: 'Collaborate',
description: 'Add comments on canvas and tag your team members to collaborate.',
position: 'right',
closeBtnText: 'Skip (6/6)',
},
},
]);
driver.start();
}
};

View file

@ -0,0 +1,45 @@
import config from 'config';
class WebSocketConnection {
constructor(appId) {
this.socket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${this.getWebsocketUrl()}`);
this.addListeners(appId);
}
getWebsocketUrl() {
const re = /https?:\/\//g;
if (re.test(config.apiUrl)) return config.apiUrl.replace(/(^\w+:|^)\/\//, '').replace('/api', '');
return window.location.host;
}
addListeners(appId) {
// Connection opened
this.socket.addEventListener('open', (event) => {
console.log('connection established', event);
const currentUser = JSON.parse(localStorage.getItem('currentUser'));
this.socket.send(
JSON.stringify({
event: 'authenticate',
data: currentUser.auth_token,
})
);
});
// Connection closed
this.socket.addEventListener('close', (event) => {
console.log('connection closed', event);
});
// Listen for possible errors
this.socket.addEventListener('error', (event) => {
console.log('WebSocket error: ', event);
});
}
}
export const createWebsocketConnection = (appId) => {
return new WebSocketConnection(appId);
};

View file

@ -163,7 +163,7 @@ button {
}
.query-name-field input {
max-width: 120px;
max-width: 180px;
font-weight: 600;
}
}
@ -1094,9 +1094,9 @@ button {
background-color: #2c405c;
button.edit-button {
background: #ffffff;
border: 1px solid #4d72fa;
color: #4d72fa;
background: transparent;
border: 1px solid #ffffff;
color: #ffffff;
}
button.launch-button {
@ -2808,7 +2808,7 @@ input:focus-visible {
.editor-sidebar {
border: solid rgba(255, 255, 255, 0.09);
border-width: 0px 0px 0px 1px !important;
border-width: 0px 0px 0px 0px !important;
.nav-tabs {
border-bottom: 1px solid rgba(255, 255, 255, 0.09) !important;
@ -4425,4 +4425,11 @@ div#driver-highlighted-element-stage, div#driver-page-overlay {
}
.tablr-gutter-x-0 {
--tblr-gutter-x: 0!important;
}
}
.flip-dropdown-help-text{
padding: 10px 5px 0 0;
float: left;
font-size: 14px;
color: $light-gray;
}

View file

@ -21,12 +21,14 @@
"@tooljet-plugins/mongodb": "file:packages/mongodb",
"@tooljet-plugins/mssql": "file:packages/mssql",
"@tooljet-plugins/mysql": "file:packages/mysql",
"@tooljet-plugins/n8n": "file:packages/n8n",
"@tooljet-plugins/postgresql": "file:packages/postgresql",
"@tooljet-plugins/redis": "file:packages/redis",
"@tooljet-plugins/restapi": "file:packages/restapi",
"@tooljet-plugins/s3": "file:packages/s3",
"@tooljet-plugins/sendgrid": "file:packages/sendgrid",
"@tooljet-plugins/slack": "file:packages/slack",
"@tooljet-plugins/smtp": "file:packages/smtp",
"@tooljet-plugins/snowflake": "file:packages/snowflake",
"@tooljet-plugins/stripe": "file:packages/stripe",
"@tooljet-plugins/twilio": "file:packages/twilio",
@ -4438,6 +4440,10 @@
"resolved": "packages/mysql",
"link": true
},
"node_modules/@tooljet-plugins/n8n": {
"resolved": "packages/n8n",
"link": true
},
"node_modules/@tooljet-plugins/postgresql": {
"resolved": "packages/postgresql",
"link": true
@ -4462,6 +4468,10 @@
"resolved": "packages/slack",
"link": true
},
"node_modules/@tooljet-plugins/smtp": {
"resolved": "packages/smtp",
"link": true
},
"node_modules/@tooljet-plugins/snowflake": {
"resolved": "packages/snowflake",
"link": true
@ -4599,6 +4609,15 @@
"form-data": "^3.0.0"
}
},
"node_modules/@types/nodemailer": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.4.tgz",
"integrity": "sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/normalize-package-data": {
"version": "2.4.1",
"dev": true,
@ -11448,6 +11467,14 @@
"dev": true,
"license": "MIT"
},
"node_modules/nodemailer": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.2.tgz",
"integrity": "sha512-Dz7zVwlef4k5R71fdmxwR8Q39fiboGbu3xgswkzGwczUfjp873rVxt1O46+Fh0j1ORnAC6L9+heI8uUpO6DT7Q==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nopt": {
"version": "4.0.3",
"dev": true,
@ -15478,6 +15505,14 @@
"rimraf": "^3.0.2"
}
},
"packages/n8n": {
"name": "@tooljet-plugins/n8n",
"version": "1.0.0",
"dependencies": {
"@tooljet-plugins/common": "file:../common",
"react": "^17.0.2"
}
},
"packages/postgresql": {
"name": "@tooljet-plugins/postgresql",
"version": "1.0.0",
@ -15540,6 +15575,18 @@
"rimraf": "^3.0.2"
}
},
"packages/smtp": {
"name": "@tooljet-plugins/smtp",
"version": "1.0.0",
"dependencies": {
"@tooljet-plugins/common": "file:../common",
"nodemailer": "^6.7.2",
"react": "^17.0.2"
},
"devDependencies": {
"@types/nodemailer": "^6.4.4"
}
},
"packages/snowflake": {
"name": "@tooljet-plugins/snowflake",
"version": "1.0.0",
@ -19084,6 +19131,13 @@
"rimraf": "^3.0.2"
}
},
"@tooljet-plugins/n8n": {
"version": "file:packages/n8n",
"requires": {
"@tooljet-plugins/common": "file:../common",
"react": "^17.0.2"
}
},
"@tooljet-plugins/postgresql": {
"version": "file:packages/postgresql",
"requires": {
@ -19140,6 +19194,15 @@
"rimraf": "^3.0.2"
}
},
"@tooljet-plugins/smtp": {
"version": "file:packages/smtp",
"requires": {
"@tooljet-plugins/common": "file:../common",
"@types/nodemailer": "^6.4.4",
"nodemailer": "^6.7.2",
"react": "^17.0.2"
}
},
"@tooljet-plugins/snowflake": {
"version": "file:packages/snowflake",
"requires": {
@ -19278,6 +19341,15 @@
"form-data": "^3.0.0"
}
},
"@types/nodemailer": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.4.tgz",
"integrity": "sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/normalize-package-data": {
"version": "2.4.1",
"dev": true
@ -23963,6 +24035,11 @@
"version": "2.0.1",
"dev": true
},
"nodemailer": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.2.tgz",
"integrity": "sha512-Dz7zVwlef4k5R71fdmxwR8Q39fiboGbu3xgswkzGwczUfjp873rVxt1O46+Fh0j1ORnAC6L9+heI8uUpO6DT7Q=="
},
"nopt": {
"version": "4.0.3",
"dev": true,

View file

@ -28,12 +28,14 @@
"@tooljet-plugins/mongodb": "file:packages/mongodb",
"@tooljet-plugins/mssql": "file:packages/mssql",
"@tooljet-plugins/mysql": "file:packages/mysql",
"@tooljet-plugins/n8n": "file:packages/n8n",
"@tooljet-plugins/postgresql": "file:packages/postgresql",
"@tooljet-plugins/redis": "file:packages/redis",
"@tooljet-plugins/restapi": "file:packages/restapi",
"@tooljet-plugins/s3": "file:packages/s3",
"@tooljet-plugins/sendgrid": "file:packages/sendgrid",
"@tooljet-plugins/slack": "file:packages/slack",
"@tooljet-plugins/smtp": "file:packages/smtp",
"@tooljet-plugins/snowflake": "file:packages/snowflake",
"@tooljet-plugins/stripe": "file:packages/stripe",
"@tooljet-plugins/twilio": "file:packages/twilio",

4
plugins/packages/n8n/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules
lib/*.d.*
lib/*.js
lib/*.js.map

View file

@ -0,0 +1,4 @@
# N8n
Documentation on: https://docs.tooljet.com/docs/data-sources/n8n

View file

@ -0,0 +1,7 @@
'use strict';
const n8n = require('../lib');
describe('n8n', () => {
it.todo('needs tests');
});

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 500 500" style="enable-background:new 0 0 500 500;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FF6D5A;}
</style>
<path class="st0" d="M448.22,122.54c-23.84,0-43.89,16.42-49.55,38.52h-71.31c-27.98,0-50.75,22.76-50.75,50.75
c0,13.98-11.37,25.37-25.37,25.37h-10.17c-5.66-22.1-25.69-38.52-49.55-38.52c-23.84,0-43.89,16.42-49.55,38.52h-40.74
c-5.66-22.1-25.69-38.52-49.55-38.52c-28.23,0-51.21,22.96-51.21,51.21c0,28.23,22.96,51.21,51.21,51.21
c23.84,0,43.89-16.42,49.55-38.52h40.79c5.66,22.1,25.69,38.52,49.55,38.52c23.69,0,43.6-16.18,49.43-38.06h10.27
c13.98,0,25.37,11.37,25.37,25.37c0,27.98,22.76,50.75,50.75,50.75h16.61c5.66,22.1,25.69,38.52,49.55,38.52
c28.23,0,51.21-22.96,51.21-51.21c0-28.23-22.96-51.21-51.21-51.21c-23.84,0-43.89,16.42-49.55,38.52h-16.61
c-13.98,0-25.37-11.37-25.37-25.37c0-15.27-6.83-28.98-17.54-38.28c10.73-9.32,17.54-23.01,17.54-38.28
c0-13.98,11.37-25.37,25.37-25.37h71.31c5.66,22.1,25.69,38.52,49.55,38.52c28.23,0,51.21-22.96,51.21-51.21
C499.43,145.52,476.45,122.54,448.22,122.54z M51.71,275.68c-14.25,0-25.84-11.59-25.84-25.84c0-14.25,11.59-25.84,25.84-25.84
s25.84,11.59,25.84,25.84C77.55,264.09,65.96,275.68,51.71,275.68z M191.56,275.68c-14.25,0-25.84-11.59-25.84-25.84
c0-14.25,11.59-25.84,25.84-25.84s25.84,11.59,25.84,25.84C217.39,264.09,205.81,275.68,191.56,275.68z M393.52,300.59
c14.25,0,25.84,11.59,25.84,25.84s-11.59,25.84-25.84,25.84c-14.25,0-25.84-11.59-25.84-25.84
C367.71,312.18,379.3,300.59,393.52,300.59z M448.22,199.59c-14.25,0-25.84-11.59-25.84-25.84c0-14.25,11.59-25.84,25.84-25.84
s25.84,11.59,25.84,25.84C474.06,188,462.47,199.59,448.22,199.59z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,81 @@
import { QueryError, QueryResult, QueryService, ConnectionTestResult, parseJson } from '@tooljet-plugins/common';
import { SourceOptions, QueryOptions } from './types';
import got from 'got';
import { HTTPError, OptionsOfTextResponseBody } from 'got';
const constructHeaders = (sourceOptions: SourceOptions) =>{
let headers = {};
if(sourceOptions.auth_type === 'header'){
headers[sourceOptions.name] = sourceOptions.value
}
return headers;
}
const extractJsonData = (content:any)=>{
return typeof content === 'string' ? JSON.parse(content) : content;
}
export default class N8n implements QueryService {
async run(sourceOptions: SourceOptions, queryOptions: QueryOptions, dataSourceId: string): Promise<QueryResult> {
const authType = sourceOptions.auth_type;
const headers = constructHeaders(sourceOptions);
const operation = queryOptions.method;
const url = queryOptions.url;
const url_params = queryOptions.url_params;
const body = queryOptions.body;
let result = {};
// Remove invalid headers from the headers object
Object.keys(headers).forEach((key) => (headers[key] === '' ? delete headers[key] : {}));
const paramsContent = url_params ? extractJsonData(url_params) : '';
const bodyContent = body ? extractJsonData(body) : '';
const constructPayload = (method:string) : OptionsOfTextResponseBody => {
return {
method: method === 'post' ? 'POST' : 'GET',
headers: headers,
username: authType === 'basic' && sourceOptions.username,
password: authType === 'basic' && sourceOptions.password,
searchParams: paramsContent,
json: method === 'post' ? bodyContent : undefined,
}
}
try {
switch(operation){
case 'post' : {
if(bodyContent === ''){
throw new Error("Please provide body content");
}
const response = await got(url, constructPayload('post'));
result = JSON.parse(response.body);
break;
}
case 'get': {
const response = await got(url,constructPayload('get'));
result = JSON.parse(response.body);
break;
}
default : {
throw new Error("Select a method");
}
}
} catch (error) {
console.log(error);
if (error instanceof HTTPError) {
result = {
code: error.code,
};
}
throw new QueryError('Query could not be completed', error.message, result);
}
return {
status: 'ok',
data: result,
};
}
}

View file

@ -0,0 +1,85 @@
{
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/manifest.schema.json",
"title": "n8n datasource",
"description": "A schema defining N8n datasource",
"type": "api",
"source": {
"name": "n8n",
"kind": "n8n",
"exposedVariables": {
"isLoading": false,
"data": {},
"rawData": {}
},
"options": {
"password": {
"encrypted": true
},
"name": {
"encrypted": true
},
"value": {
"encrypted": true
}
},
"customTesting": true
},
"defaults": {
"auth_type": {"value":"none"}
},
"properties": {
"auth_type": {
"label": "Authentication Type",
"key": "auth_type",
"type": "dropdown-component-flip",
"description": "Select auth type",
"helpText": "Webhook credentials and instance credentials are different. Please use the credentials that you use with the Webhook trigger.",
"list": [
{
"value": "none",
"name": "None"
},
{
"value": "basic",
"name": "Basic Auth"
},
{
"value": "header",
"name": "Header Auth"
}
]
},
"basic":{
"username": {
"label": "Username",
"key": "username",
"type": "text",
"description": "Enter username"
},
"password": {
"label": "Password",
"key": "password",
"type": "password",
"description": "Enter username"
}
},
"header":{
"name": {
"label": "Name",
"key": "name",
"type": "text",
"encrypted": true,
"description": "Enter key name"
},
"value": {
"label": "Value",
"key": "value",
"type": "text",
"encrypted": true,
"description": "Enter value"
}
}
},
"required": []
}

View file

@ -0,0 +1,69 @@
{
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/operations.schema.json",
"title": "N8n datasource",
"description": "A schema defining N8n datasource",
"type": "api",
"defaults": {},
"properties": {
"method": {
"label": "Method",
"key": "method",
"className":"col-md-4",
"type": "dropdown-component-flip",
"description": "Single request type",
"list": [
{
"value": "get",
"name": "GET"
},
{
"value": "post",
"name": "POST"
}
],
"commonFields":{
"url":{
"label": "Webhook URL",
"key": "url",
"type": "codehinter",
"lineNumbers": false,
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "webhook URL of your workflow",
"description": "Field for url"
}
}
},
"get":{
"parameters": {
"label": "URL parameters",
"key": "url_params",
"type": "codehinter",
"mode": "javascript",
"placeholder": "{ \"name\": \"bob\" }",
"description": "Enter options",
"height": "150px"
}
},
"post":{
"parameters": {
"label": "URL parameters",
"key": "url_params",
"type": "codehinter",
"mode": "javascript",
"placeholder": "{ \"name\": \"bob\" }",
"description": "Enter url parameters",
"height": "150px"
},
"body": {
"label": "Body",
"key": "body",
"type": "codehinter",
"mode": "javascript",
"placeholder": "{ \"age\": \"12\" }",
"description": "Enter body parameters",
"height": "150px"
}
}
}
}

View file

@ -0,0 +1,7 @@
export type SourceOptions = { auth_type:string; username: string; password: string; name:string; value:string; };
export type QueryOptions = {
url:string,
method: string;
url_params: string;
body: string;
};

View file

@ -0,0 +1,23 @@
{
"name": "@tooljet-plugins/n8n",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"directories": {
"lib": "lib",
"test": "__tests__"
},
"files": [
"lib"
],
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
"build": "tsc -b",
"clean": "rimraf ./dist && rimraf tsconfig.tsbuildinfo"
},
"homepage": "https://github.com/tooljet/tooljet#readme",
"dependencies": {
"@tooljet-plugins/common": "file:../common",
"react": "^17.0.2"
}
}

View file

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "lib"
},
"exclude": [
"node_modules",
"dist"
]
}

Some files were not shown because too many files have changed in this diff Show more