Merge branch 'release/v0.6.1' into main
2
.version
|
|
@ -1 +1 @@
|
|||
0.6.0
|
||||
0.6.1
|
||||
|
|
@ -8,7 +8,8 @@
|
|||
"empty-state-dashboard.spec.js",
|
||||
"dashboard.spec.js",
|
||||
"apps-page-operations.spec.js",
|
||||
"editor-navigation-bar.spec.js"
|
||||
"editor-navigation-bar.spec.js",
|
||||
"editor-datasource-postgres.spec.js"
|
||||
]
|
||||
|
||||
}
|
||||
55
cypress/integration/editor-datasource-postgres.spec.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
describe('Editor- Add "PostgreSQL" datasource', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
//read login data from fixtures
|
||||
cy.fixture('login-data').then(function (testdata) {
|
||||
cy.login(testdata.email, testdata.password)
|
||||
})
|
||||
cy.wait(1000)
|
||||
cy.get('body').then(($title => {
|
||||
//check you are not running tests on empty dashboard state
|
||||
if ($title.text().includes('You haven\'t created any apps yet.')) {
|
||||
cy.get('a.btn').eq(0).should('have.text', 'Create your first app')
|
||||
.click()
|
||||
cy.go('back')
|
||||
}
|
||||
cy.wait(2000)
|
||||
cy.get('.badge').contains('Edit').click()
|
||||
cy.get('title').should('have.text', 'ToolJet - Dashboard')
|
||||
}))
|
||||
})
|
||||
|
||||
it('should add First data source successfully', () => {
|
||||
|
||||
cy.get('.left-sidebar')
|
||||
.find('.datasources-container.w-100.mt-3')
|
||||
.find('.p-1.text-muted')
|
||||
.should('have.text', 'DATASOURCES')
|
||||
.and('be.visible')
|
||||
|
||||
cy.get('.table-responsive')
|
||||
.find('.p-2')
|
||||
.should('include.text', "You haven't added data sources yet. ")
|
||||
|
||||
cy.get('center[class="p-2 text-muted"]')
|
||||
.find('button[class="btn btn-sm btn-outline-azure mt-3"]')
|
||||
.should('have.text', 'add datasource')
|
||||
.click()
|
||||
|
||||
cy.addPostgresDataSource()
|
||||
});
|
||||
|
||||
it('should add data source from "Add new datasource button', () => {
|
||||
|
||||
cy.get('.left-sidebar')
|
||||
.find('.datasources-container.w-100.mt-3')
|
||||
.find('.p-1.text-muted')
|
||||
.should('have.text', 'DATASOURCES')
|
||||
.and('be.visible')
|
||||
|
||||
cy.get('[data-tip="Add new datasource"]').click()
|
||||
|
||||
cy.addPostgresDataSource()
|
||||
});
|
||||
})
|
||||
|
|
@ -164,7 +164,7 @@ describe('Editor - Navigation Bar', () => {
|
|||
cy.get('.modal-content')
|
||||
.find('.modal-header')
|
||||
.find('.modal-title')
|
||||
.should('have.text', 'Users and permissions')
|
||||
.should('have.text', 'Share')
|
||||
|
||||
cy.get('.form-label')
|
||||
.should('have.text', 'Get shareable link for this application')
|
||||
|
|
@ -172,13 +172,7 @@ describe('Editor - Navigation Bar', () => {
|
|||
cy.get('.input-group')
|
||||
.find('.btn.btn-secondary.btn-sm')
|
||||
.should('have.text', 'Copy')
|
||||
.click() // check how to validate cliboard content
|
||||
|
||||
cy.document().then(doc => {
|
||||
doc.execCommand('copy');
|
||||
});
|
||||
cy.task('getClipboard').should('contain', '/applications/')
|
||||
|
||||
.click() //check how to validate clipboard content
|
||||
});
|
||||
|
||||
it('should deploy app', () => {
|
||||
|
|
@ -214,6 +208,7 @@ describe('Editor - Navigation Bar', () => {
|
|||
});
|
||||
|
||||
it('should launch app', () => {
|
||||
|
||||
cy.get('.navbar-nav.flex-row.order-md-last')
|
||||
.find('a[target="_blank"]')
|
||||
.should('have.text', 'Launch')
|
||||
|
|
|
|||
|
|
@ -19,11 +19,10 @@ module.exports = (on, config) => {
|
|||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
const clipboardy = require('clipboardy');
|
||||
module.exports = (on, config) => {
|
||||
on('task', {
|
||||
getClipboard() {
|
||||
return clipboardy.readSync();
|
||||
}
|
||||
});
|
||||
};
|
||||
// modify env value
|
||||
config.env = process.env
|
||||
|
||||
// return config
|
||||
return config
|
||||
}
|
||||
|
|
@ -8,3 +8,62 @@ Cypress.Commands.add('login', (email, password) => {
|
|||
Cypress.Commands.add('checkToastMessage', (toastId, message) => {
|
||||
cy.get(`[id=${toastId}]`).should('contain', message);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('addPostgresDataSource', fn => {
|
||||
|
||||
cy.get('div[class="modal-title h4"] span[class="text-muted"]')
|
||||
.should('have.text', 'Add new datasource')
|
||||
.and('be.visible')
|
||||
|
||||
cy.get('.modal-body')
|
||||
.find('div[class="row row-deck"]')
|
||||
.find('h4[class="text-muted mb-2"]')
|
||||
.should('have.text', 'DATABASES')
|
||||
|
||||
cy.get('.modal-body')
|
||||
.find('.col-md-2')
|
||||
.contains('PostgreSQL')
|
||||
.and('be.visible')
|
||||
.click()
|
||||
|
||||
cy.get('.row.mt-3')
|
||||
.find('.col-md-4')
|
||||
.find('.form-label')
|
||||
.contains('Database Name')
|
||||
|
||||
cy.get('div[class="row mt-3"] div:nth-child(1)')
|
||||
.find('.form-control')
|
||||
.should('have.attr', 'type', 'text')
|
||||
.type(Cypress.env('TEST_PG_DB'))
|
||||
|
||||
cy.get('.row.mt-3')
|
||||
.find('.col-md-4')
|
||||
.find('.form-label')
|
||||
.contains('Username')
|
||||
|
||||
cy.get('div[class="row mt-3"] div:nth-child(2)')
|
||||
.find('.form-control')
|
||||
.should('have.attr', 'type', 'text')
|
||||
.type(Cypress.env('TEST_PG_USERNAME'))
|
||||
|
||||
cy.get('.row.mt-3')
|
||||
.find('.col-md-4')
|
||||
.find('.form-label')
|
||||
.contains('Password')
|
||||
|
||||
cy.get('div[class="row mt-3"] div:nth-child(3)')
|
||||
.find('.form-control')
|
||||
.should('have.attr', 'type', 'password')
|
||||
.type(Cypress.env('TEST_PG_PASSWORD'))
|
||||
|
||||
cy.get('button[class="m-2 btn btn-success"]')
|
||||
.should('have.text', 'Test Connection')
|
||||
.click()
|
||||
|
||||
cy.get('.badge')
|
||||
.should('have.text', 'connection verified')
|
||||
|
||||
cy.get('div[class="col-auto"] button[type="button"]')
|
||||
.should('have.text', 'Save')
|
||||
.click()
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ services:
|
|||
env_file: .env
|
||||
environment:
|
||||
RAILS_LOG_TO_STDOUT: "true"
|
||||
SERVE_CLIENT: "false"
|
||||
command: ['npm', 'run', '--prefix', 'server', 'start:prod']
|
||||
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ source "amazon-ebs" "ubuntu" {
|
|||
ami_name = "${var.ami_name}"
|
||||
instance_type = "${var.instance_type}"
|
||||
region = "${var.ami_region}"
|
||||
ami_regions = "${var.ami_regions}"
|
||||
source_ami_filter {
|
||||
filters = {
|
||||
name = "ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*"
|
||||
|
|
|
|||
|
|
@ -11,3 +11,8 @@ variable "ami_region" {
|
|||
type = string
|
||||
default = "us-west-2"
|
||||
}
|
||||
|
||||
variable "ami_regions" {
|
||||
type = list(string)
|
||||
default = ["us-west-1", "us-east-1", "us-east-2", "eu-west-2", "eu-central-1", "ap-northeast-1", "ap-southeast-1", "ap-northeast-3", "ap-south-1", "ap-northeast-2", "ap-southeast-2", "ca-central-1", "eu-west-1", "eu-north-1", "sa-east-1"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ services:
|
|||
command: ['npm', 'start']
|
||||
|
||||
server:
|
||||
# env_file: .env
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./docker/server.Dockerfile.dev
|
||||
|
|
@ -30,12 +29,7 @@ services:
|
|||
ports:
|
||||
- 3000:3000
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- PG_DB=tooljet_development
|
||||
- PG_USER=postgres
|
||||
- PG_PASS=postgres
|
||||
- PG_HOST=postgres
|
||||
entrypoint: ./entrypoint.sh
|
||||
- SERVE_CLIENT=false
|
||||
command: npm run start:dev
|
||||
|
||||
postgres:
|
||||
|
|
@ -46,8 +40,6 @@ services:
|
|||
volumes:
|
||||
- postgres:/data/postgres
|
||||
environment:
|
||||
- POSTGRES_DB=tooljet_development
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ Follow these steps to setup and run ToolJet on Mac OS for development purposes.
|
|||
5. ## Setup database
|
||||
```bash
|
||||
npm run --prefix server db:reset
|
||||
npm run --prefix server start:dev
|
||||
```
|
||||
6. ## Install webpack & nest-cli
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -30,9 +30,10 @@ We recommend:
|
|||
2. Create a `.env` file by copying `.env.example`. More information on the variables that can be set is given here: env variable reference
|
||||
```bash
|
||||
cp .env.example .env
|
||||
cp .env.example .env.test
|
||||
```
|
||||
|
||||
3. Populate the keys in the `.env` file.
|
||||
3. Populate the keys in the `.env` and `.env.test` file.
|
||||
:::info
|
||||
`SECRET_KEY_BASE` requires a 64 byte key. (If you have `openssl` installed, run `openssl rand -hex 64` to create a 64 byte secure random key)
|
||||
|
||||
|
|
@ -42,9 +43,32 @@ We recommend:
|
|||
Example:
|
||||
```bash
|
||||
cat .env
|
||||
TOOLJET_HOST=http://localhost:8082
|
||||
LOCKBOX_MASTER_KEY=1d291a926ddfd221205a23adb4cc1db66cb9fcaf28d97c8c1950e3538e3b9281
|
||||
SECRET_KEY_BASE=4229d5774cfe7f60e75d6b3bf3a1dbb054a696b6d21b6d5de7b73291899797a222265e12c0a8e8d844f83ebacdf9a67ec42584edf1c2b23e1e7813f8a3339041
|
||||
TOOLJET_HOST=http://localhost:8082
|
||||
LOCKBOX_MASTER_KEY=13c9b8364ae71f714774c82498ba328813069e48d80029bb29f49d0ada5a8e40
|
||||
SECRET_KEY_BASE=ea85064ed42ad02cfc022e66d8bccf452e3fa1142421cbd7a13592d91a2cbb866d6001060b73a98a65be57e65524357d445efae00a218461088a706decd62dcb
|
||||
NODE_ENV=development
|
||||
# DATABASE CONFIG
|
||||
PG_HOST=postgres
|
||||
PG_PORT=5432
|
||||
PG_USER=postgres
|
||||
PG_PASS=postgres
|
||||
PG_DB=tooljet_development
|
||||
ORM_LOGGING=all
|
||||
```
|
||||
|
||||
```bash
|
||||
cat .env.test
|
||||
TOOLJET_HOST=http://localhost:8082
|
||||
LOCKBOX_MASTER_KEY=13c9b8364ae71f714774c82498ba328813069e48d80029bb29f49d0ada5a8e40
|
||||
SECRET_KEY_BASE=ea85064ed42ad02cfc022e66d8bccf452e3fa1142421cbd7a13592d91a2cbb866d6001060b73a98a65be57e65524357d445efae00a218461088a706decd62dcb
|
||||
NODE_ENV=test
|
||||
# DATABASE CONFIG
|
||||
PG_HOST=postgres
|
||||
PG_PORT=5432
|
||||
PG_USER=postgres
|
||||
PG_PASS=postgres
|
||||
PG_DB=tooljet_test
|
||||
ORM_LOGGING=error
|
||||
```
|
||||
|
||||
4. Build docker images
|
||||
|
|
@ -52,9 +76,10 @@ We recommend:
|
|||
docker-compose build
|
||||
```
|
||||
|
||||
5. ToolJet server is built using NestJS and the data such as application definitions are persisted on a postgres database. You have to reset the database if building for the first time.
|
||||
5. ToolJet server is built using NestJS and the data such as application definitions are persisted on a postgres database. You have to create and migrate the database if building for the first time.
|
||||
```bash
|
||||
docker-compose run server npm run db:reset
|
||||
docker-compose run server npm run db:create
|
||||
docker-compose run server npm run db:migrate
|
||||
docker-compose run server npm run db:seed
|
||||
```
|
||||
|
||||
|
|
@ -118,7 +143,13 @@ Once you've updated the Dockerfile, rebuild the image by running `docker-compose
|
|||
|
||||
## Running tests
|
||||
|
||||
Test config requires the presence of `.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
|
||||
```bash
|
||||
docker-compose run -e NODE_ENV=test server npm run db:create
|
||||
docker-compose run -e NODE_ENV=test server npm run db:migrate
|
||||
```
|
||||
|
||||
To run the unit tests
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ Follow these steps to setup and run ToolJet on Ubuntu. Open terminal and run the
|
|||
5. ## Setup database
|
||||
```bash
|
||||
npm run --prefix server db:reset
|
||||
npm run --prefix server start:dev
|
||||
```
|
||||
6. ## Running the server
|
||||
```bash
|
||||
|
|
@ -62,11 +61,11 @@ Follow these steps to setup and run ToolJet on Ubuntu. Open terminal and run the
|
|||
|
||||
The client will start running on the port 8082, you can access the client by visiting: [https://localhost:8082](https://localhost:8082)
|
||||
|
||||
9. ## Creating login credentials
|
||||
8. ## Creating login credentials
|
||||
Visiting https://localhost:8082 should redirect you to the login page, click on the signup link and enter your email. The emails sent by the server in development environment are captured and are opened in your default browser. Click the invitation link in the email preview to setup the account.
|
||||
|
||||
|
||||
10. ## Running tests
|
||||
9. ## Running tests
|
||||
|
||||
Test config requires the presence of `.env.test` file at the root of the project.
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ sidebar_position: 3
|
|||
|
||||
ToolJet can connect to Airtable using Airtable API ( https://airtable.com/api ). Airtable API key is required to create Airtable datasource on ToolJet. You can generate API key by visiting [Airtable account page](https://airtable.com/account).
|
||||
|
||||
<img class="screenshot-full" src="/img/datasource-reference/airtable-intro.gif" alt="ToolJet - Airtable" height="420" />
|
||||
|
||||
:::tip
|
||||
Airtable API has a rate limit and at the time of writing this documentation, the limit is 5 requests per second per base. You can read more about rate limits here ( https://airtable.com/api ).
|
||||
:::
|
||||
|
|
@ -20,6 +22,8 @@ Supported queries:
|
|||
|
||||
- Listing records
|
||||
- Retrieving a record
|
||||
- Updating a record
|
||||
- Deleting a record
|
||||
|
||||
## Listing records
|
||||
|
||||
|
|
@ -88,4 +92,61 @@ Example response from Airtable:
|
|||
},
|
||||
"createdTime": "2021-05-12T14:30:33.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Updating a record
|
||||
|
||||
Required parameters:
|
||||
- Base ID
|
||||
- Table name
|
||||
- Record ID
|
||||
|
||||
<img class="screenshot-full" src="/img/datasource-reference/airtable-update.png" alt="ToolJet - Airtable Update Operarion" height="420"/>
|
||||
|
||||
#### Example body:
|
||||
|
||||
<img class="screenshot-full" src="/img/datasource-reference/airtable-update-example-body.png" alt="ToolJet - Airtable Update Operarion Body" height="200" width="650" />
|
||||
|
||||
|
||||
Click on the `run` button to run the query.
|
||||
|
||||
:::info
|
||||
NOTE: Query must be saved before running.
|
||||
:::
|
||||
|
||||
Example response from Airtable:
|
||||
```json
|
||||
{
|
||||
"id": "recu9xMnUdr2n2cw8",
|
||||
"fields": {
|
||||
"Notes": "Example Notes",
|
||||
"Name": "change"
|
||||
},
|
||||
"createdTime": "2021-08-08T17:27:17.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Deleting a record
|
||||
|
||||
Required parameters:
|
||||
- Base ID
|
||||
- Table name
|
||||
- Record ID
|
||||
|
||||
<img class="screenshot-full" src="/img/datasource-reference/airtable-delete.png" alt="ToolJet - Airtable Delete Operarion" height="420" width="650" />
|
||||
|
||||
|
||||
Click on the `run` button to run the query.
|
||||
|
||||
:::info
|
||||
NOTE: Query must be saved before running.
|
||||
:::
|
||||
|
||||
Example response from Airtable:
|
||||
|
||||
```json
|
||||
{
|
||||
deleted: true
|
||||
id: "recIKsyZgqI4zoqS7"
|
||||
}
|
||||
```
|
||||
|
|
@ -51,6 +51,10 @@ If want to restrict the signups and allow new users only by invitations, set the
|
|||
You will still be able to see the signup page but won't be able to successfully submit the form.
|
||||
:::
|
||||
|
||||
#### 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.
|
||||
You can set `SERVE_CLIENT` to `true` and the server will attempt to serve the client at its root end-point (`/`).
|
||||
|
||||
#### SMTP configuration ( optional )
|
||||
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ Follow the steps below to deploy ToolJet on a Kubernetes cluster.
|
|||
3. Create a Kubernetes deployment
|
||||
|
||||
```bash
|
||||
kubectl apply -f https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/server-deployment.yaml
|
||||
kubectl apply -f https://raw.githubusercontent.com/ToolJet/ToolJet/main/deploy/kubernetes/deployment.yaml
|
||||
```
|
||||
|
||||
:::info
|
||||
The file given above is just a template and might not suit production environments. You should download the file and configure parameters such as the replica count and environment variables according to your needs.
|
||||
:::
|
||||
:::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.
|
||||
:::
|
||||
|
||||
4. Verify if ToolJet is running
|
||||
|
||||
|
|
|
|||
24
docs/docs/widgets/qr-scanner.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# QR Scanner
|
||||
Scan QR codes using device camera and hold the data they carry.
|
||||
<img class="screenshot-full" src="/img/widgets/qr-scanner/qr-scanner.jpeg" alt="ToolJet - QR Scanner" height="420"/>
|
||||
|
||||
#### Known issue:
|
||||
In IOS, you might have to stick to the Safari browser as camera access had been restricted for third party browsers.
|
||||
|
||||
## Exposed variables
|
||||
#### lastDetectedValue
|
||||
This variable holds the data contained in the last QR code scanned by the widget.
|
||||
|
||||
## Events
|
||||
#### onDetect
|
||||
This event is fired whenever the widget successfully scans a QR code.
|
||||
|
||||
## Debugging tip
|
||||
Browser camera APIs restrict this widget to only work in either `localhost` or `https`.
|
||||
|
||||
So if you're testing it out, be sure to either use `localhost` or `https`.
|
||||
|
||||
35
docs/docs/widgets/radio-button.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Radio Button
|
||||
|
||||
Radio Buttons can be used to select one option from a group of options.
|
||||
|
||||
<img class="screenshot-full" src="/img/widgets/radio-button/widget.gif" alt="ToolJet - Radio Button Widget" height="420"/>
|
||||
|
||||
:::tip
|
||||
Radio buttons are preferred when the list of options is less than six and all the options can be displayed at once.
|
||||
:::
|
||||
|
||||
:::info
|
||||
For more than six options, consider using [Dropdown](/docs/widgets/dropdown) widget.
|
||||
:::
|
||||
|
||||
|
||||
## Event: onSelectionChange
|
||||
|
||||
This event is triggered when an option is clicked.
|
||||
|
||||
|
||||
## Property : Values (Array)
|
||||
|
||||
This property can be used for setting the value of the input it was called on.
|
||||
|
||||
## Property : Display Values (Array of strings)
|
||||
|
||||
This property specifies the values to be displayed as options
|
||||
|
||||
|
||||
## Example
|
||||
<img class="screenshot-full" src="/img/widgets/radio-button/property.gif" alt="ToolJet - Radio Button Widget Properties" height="420"/>
|
||||
|
|
@ -39,6 +39,7 @@ To display email column, the key for the column should be `user.email`.
|
|||
- Tags - similar to badges but the values are not predefined.
|
||||
- Dropdown
|
||||
- Multiselect dropdown
|
||||
- Toggle switch
|
||||
|
||||
## Client-side pagination
|
||||
|
||||
|
|
@ -51,6 +52,8 @@ Server-side pagination can be used to run a query whenever the page is changed.
|
|||
## Search
|
||||
Client-side search is enabled by default and server-side search can be enabled from the events section of the inspector. Whenever the search text is changed, the `searchText` property of the table component is updated. If server-side search is enabled, `on search` event is fired after the content of `searchText` property is changed. `searchText` can be used to run a specific query to search for the records in your datasource.
|
||||
|
||||
If you don't wish to use the search feature altogether, you can disable it from the inspector.
|
||||
|
||||
## Event: On row clicked
|
||||
|
||||
This event is triggered when a table row is clicked. `selectedRow` property of the table object will have the table data of the selected row.
|
||||
|
|
|
|||
20
docs/docs/widgets/toggle-switch.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Toggle Switch
|
||||
|
||||
Toggle switch widget allows the user to change a setting between two states.
|
||||
|
||||
The Toggle switch widget should be used if we want to make a binary choice,
|
||||
such as turning something **on or off** or **enable or disable**.
|
||||
|
||||
<img class="screenshot-full" src="/img/widgets/toggle-switch/toggle-switch.gif" alt="ToolJet - Toggle switch widget" height="420"/>
|
||||
|
||||
|
||||
## Event: onChange
|
||||
This event is triggered whenever toggle switch is clicked.
|
||||
|
||||
## Property : Label
|
||||
This property can be used to set a label for the switch.
|
||||
Default Label: **Toggle label**
|
||||
BIN
docs/static/img/datasource-reference/airtable-delete.png
vendored
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
docs/static/img/datasource-reference/airtable-intro.gif
vendored
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/static/img/datasource-reference/airtable-update-example-body.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/static/img/datasource-reference/airtable-update.png
vendored
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/static/img/widgets/qr-scanner/qr-scanner.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
docs/static/img/widgets/radio-button/property.gif
vendored
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
docs/static/img/widgets/radio-button/widget.gif
vendored
Normal file
|
After Width: | Height: | Size: 1 MiB |
BIN
docs/static/img/widgets/toggle-switch/toggle-switch.gif
vendored
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
40
frontend/assets/images/icons/editor/left-sidebar/back.svg
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 52.502 52.502" style="enable-background:new 0 0 52.502 52.502;" xml:space="preserve">
|
||||
<path d="M51.718,50.857l-1.341-2.252C40.075,31.295,25.975,32.357,22.524,32.917v13.642L0,23.995L22.524,1.644v13.43
|
||||
c0.115,0,0.229-0.001,0.344-0.001c12.517,0,18.294,5.264,18.542,5.496c13.781,11.465,10.839,27.554,10.808,27.715L51.718,50.857z
|
||||
M25.505,30.735c5.799,0,16.479,1.923,24.993,14.345c0.128-4.872-0.896-15.095-10.41-23.012c-0.099-0.088-5.935-5.364-18.533-4.975
|
||||
l-1.03,0.03V6.447L2.832,24.001l17.692,17.724V31.311l0.76-0.188C21.338,31.109,22.947,30.735,25.505,30.735z"/>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,92 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 55 55" style="enable-background:new 0 0 55 55;" xml:space="preserve">
|
||||
<path d="M52.354,8.51C51.196,4.22,42.577,0,27.5,0C12.423,0,3.803,4.22,2.646,8.51C2.562,8.657,2.5,8.818,2.5,9v0.5V21v0.5V22v11
|
||||
v0.5V34v12c0,0.162,0.043,0.315,0.117,0.451C3.798,51.346,14.364,55,27.5,55c13.106,0,23.655-3.639,24.875-8.516
|
||||
C52.455,46.341,52.5,46.176,52.5,46V34v-0.5V33V22v-0.5V21V9.5V9C52.5,8.818,52.438,8.657,52.354,8.51z M50.421,33.985
|
||||
c-0.028,0.121-0.067,0.241-0.116,0.363c-0.04,0.099-0.089,0.198-0.143,0.297c-0.067,0.123-0.142,0.246-0.231,0.369
|
||||
c-0.066,0.093-0.141,0.185-0.219,0.277c-0.111,0.131-0.229,0.262-0.363,0.392c-0.081,0.079-0.17,0.157-0.26,0.236
|
||||
c-0.164,0.143-0.335,0.285-0.526,0.426c-0.082,0.061-0.17,0.12-0.257,0.18c-0.226,0.156-0.462,0.311-0.721,0.463
|
||||
c-0.068,0.041-0.141,0.08-0.212,0.12c-0.298,0.168-0.609,0.335-0.945,0.497c-0.043,0.021-0.088,0.041-0.132,0.061
|
||||
c-0.375,0.177-0.767,0.351-1.186,0.519c-0.012,0.005-0.024,0.009-0.036,0.014c-2.271,0.907-5.176,1.67-8.561,2.17
|
||||
c-0.017,0.002-0.034,0.004-0.051,0.007c-0.658,0.097-1.333,0.183-2.026,0.259c-0.113,0.012-0.232,0.02-0.346,0.032
|
||||
c-0.605,0.063-1.217,0.121-1.847,0.167c-0.288,0.021-0.59,0.031-0.883,0.049c-0.474,0.028-0.943,0.059-1.429,0.076
|
||||
C29.137,40.984,28.327,41,27.5,41s-1.637-0.016-2.432-0.044c-0.486-0.017-0.955-0.049-1.429-0.076
|
||||
c-0.293-0.017-0.595-0.028-0.883-0.049c-0.63-0.046-1.242-0.104-1.847-0.167c-0.114-0.012-0.233-0.02-0.346-0.032
|
||||
c-0.693-0.076-1.368-0.163-2.026-0.259c-0.017-0.002-0.034-0.004-0.051-0.007c-3.385-0.5-6.29-1.263-8.561-2.17
|
||||
c-0.012-0.004-0.024-0.009-0.036-0.014c-0.419-0.168-0.812-0.342-1.186-0.519c-0.043-0.021-0.089-0.041-0.132-0.061
|
||||
c-0.336-0.162-0.647-0.328-0.945-0.497c-0.07-0.04-0.144-0.079-0.212-0.12c-0.259-0.152-0.495-0.307-0.721-0.463
|
||||
c-0.086-0.06-0.175-0.119-0.257-0.18c-0.191-0.141-0.362-0.283-0.526-0.426c-0.089-0.078-0.179-0.156-0.26-0.236
|
||||
c-0.134-0.13-0.252-0.26-0.363-0.392c-0.078-0.092-0.153-0.184-0.219-0.277c-0.088-0.123-0.163-0.246-0.231-0.369
|
||||
c-0.054-0.099-0.102-0.198-0.143-0.297c-0.049-0.121-0.088-0.242-0.116-0.363C4.541,33.823,4.5,33.661,4.5,33.5
|
||||
c0-0.113,0.013-0.226,0.031-0.338c0.025-0.151,0.011-0.302-0.031-0.445v-7.424c0.028,0.026,0.063,0.051,0.092,0.077
|
||||
c0.218,0.192,0.44,0.383,0.69,0.567C9.049,28.786,16.582,31,27.5,31c10.872,0,18.386-2.196,22.169-5.028
|
||||
c0.302-0.22,0.574-0.447,0.83-0.678l0.001-0.001v7.424c-0.042,0.143-0.056,0.294-0.031,0.445c0.019,0.112,0.031,0.225,0.031,0.338
|
||||
C50.5,33.661,50.459,33.823,50.421,33.985z M50.5,13.293v7.424c-0.042,0.143-0.056,0.294-0.031,0.445
|
||||
c0.019,0.112,0.031,0.225,0.031,0.338c0,0.161-0.041,0.323-0.079,0.485c-0.028,0.121-0.067,0.241-0.116,0.363
|
||||
c-0.04,0.099-0.089,0.198-0.143,0.297c-0.067,0.123-0.142,0.246-0.231,0.369c-0.066,0.093-0.141,0.185-0.219,0.277
|
||||
c-0.111,0.131-0.229,0.262-0.363,0.392c-0.081,0.079-0.17,0.157-0.26,0.236c-0.164,0.143-0.335,0.285-0.526,0.426
|
||||
c-0.082,0.061-0.17,0.12-0.257,0.18c-0.226,0.156-0.462,0.311-0.721,0.463c-0.068,0.041-0.141,0.08-0.212,0.12
|
||||
c-0.298,0.168-0.609,0.335-0.945,0.497c-0.043,0.021-0.088,0.041-0.132,0.061c-0.375,0.177-0.767,0.351-1.186,0.519
|
||||
c-0.012,0.005-0.024,0.009-0.036,0.014c-2.271,0.907-5.176,1.67-8.561,2.17c-0.017,0.002-0.034,0.004-0.051,0.007
|
||||
c-0.658,0.097-1.333,0.183-2.026,0.259c-0.113,0.012-0.232,0.02-0.346,0.032c-0.605,0.063-1.217,0.121-1.847,0.167
|
||||
c-0.288,0.021-0.59,0.031-0.883,0.049c-0.474,0.028-0.943,0.059-1.429,0.076C29.137,28.984,28.327,29,27.5,29
|
||||
s-1.637-0.016-2.432-0.044c-0.486-0.017-0.955-0.049-1.429-0.076c-0.293-0.017-0.595-0.028-0.883-0.049
|
||||
c-0.63-0.046-1.242-0.104-1.847-0.167c-0.114-0.012-0.233-0.02-0.346-0.032c-0.693-0.076-1.368-0.163-2.026-0.259
|
||||
c-0.017-0.002-0.034-0.004-0.051-0.007c-3.385-0.5-6.29-1.263-8.561-2.17c-0.012-0.004-0.024-0.009-0.036-0.014
|
||||
c-0.419-0.168-0.812-0.342-1.186-0.519c-0.043-0.021-0.089-0.041-0.132-0.061c-0.336-0.162-0.647-0.328-0.945-0.497
|
||||
c-0.07-0.04-0.144-0.079-0.212-0.12c-0.259-0.152-0.495-0.307-0.721-0.463c-0.086-0.06-0.175-0.119-0.257-0.18
|
||||
c-0.191-0.141-0.362-0.283-0.526-0.426c-0.089-0.078-0.179-0.156-0.26-0.236c-0.134-0.13-0.252-0.26-0.363-0.392
|
||||
c-0.078-0.092-0.153-0.184-0.219-0.277c-0.088-0.123-0.163-0.246-0.231-0.369c-0.054-0.099-0.102-0.198-0.143-0.297
|
||||
c-0.049-0.121-0.088-0.242-0.116-0.363C4.541,21.823,4.5,21.661,4.5,21.5c0-0.113,0.013-0.226,0.031-0.338
|
||||
c0.025-0.151,0.011-0.302-0.031-0.445v-7.424c0.12,0.109,0.257,0.216,0.387,0.324c0.072,0.06,0.139,0.12,0.215,0.18
|
||||
c0.3,0.236,0.624,0.469,0.975,0.696c0.073,0.047,0.155,0.093,0.231,0.14c0.294,0.183,0.605,0.362,0.932,0.538
|
||||
c0.121,0.065,0.242,0.129,0.367,0.193c0.365,0.186,0.748,0.367,1.151,0.542c0.066,0.029,0.126,0.059,0.193,0.087
|
||||
c0.469,0.199,0.967,0.389,1.485,0.573c0.143,0.051,0.293,0.099,0.44,0.149c0.412,0.139,0.838,0.272,1.279,0.401
|
||||
c0.159,0.046,0.315,0.094,0.478,0.138c0.585,0.162,1.189,0.316,1.823,0.458c0.087,0.02,0.181,0.036,0.269,0.055
|
||||
c0.559,0.122,1.139,0.235,1.735,0.341c0.202,0.036,0.407,0.07,0.613,0.104c0.567,0.093,1.151,0.178,1.75,0.256
|
||||
c0.154,0.02,0.301,0.043,0.457,0.062c0.744,0.09,1.514,0.167,2.305,0.233c0.195,0.016,0.398,0.028,0.596,0.042
|
||||
c0.633,0.046,1.28,0.084,1.942,0.114c0.241,0.011,0.481,0.022,0.727,0.031C25.712,18.979,26.59,19,27.5,19s1.788-0.021,2.65-0.05
|
||||
c0.245-0.009,0.485-0.02,0.727-0.031c0.662-0.03,1.309-0.068,1.942-0.114c0.198-0.015,0.4-0.026,0.596-0.042
|
||||
c0.791-0.065,1.561-0.143,2.305-0.233c0.156-0.019,0.303-0.042,0.457-0.062c0.599-0.078,1.182-0.163,1.75-0.256
|
||||
c0.206-0.034,0.411-0.068,0.613-0.104c0.596-0.106,1.176-0.219,1.735-0.341c0.088-0.019,0.182-0.036,0.269-0.055
|
||||
c0.634-0.142,1.238-0.297,1.823-0.458c0.163-0.045,0.319-0.092,0.478-0.138c0.441-0.129,0.867-0.262,1.279-0.401
|
||||
c0.147-0.05,0.297-0.098,0.44-0.149c0.518-0.184,1.017-0.374,1.485-0.573c0.067-0.028,0.127-0.058,0.193-0.087
|
||||
c0.403-0.176,0.786-0.356,1.151-0.542c0.125-0.064,0.247-0.128,0.367-0.193c0.327-0.175,0.638-0.354,0.932-0.538
|
||||
c0.076-0.047,0.158-0.093,0.231-0.14c0.351-0.227,0.675-0.459,0.975-0.696c0.075-0.06,0.142-0.12,0.215-0.18
|
||||
C50.243,13.509,50.38,13.402,50.5,13.293z M27.5,2c13.555,0,23,3.952,23,7.5s-9.445,7.5-23,7.5s-23-3.952-23-7.5S13.945,2,27.5,2z
|
||||
M50.5,45.703c-0.014,0.044-0.024,0.089-0.032,0.135C49.901,49.297,40.536,53,27.5,53S5.099,49.297,4.532,45.838
|
||||
c-0.008-0.045-0.019-0.089-0.032-0.131v-8.414c0.028,0.026,0.063,0.051,0.092,0.077c0.218,0.192,0.44,0.383,0.69,0.567
|
||||
C9.049,40.786,16.582,43,27.5,43c10.872,0,18.386-2.196,22.169-5.028c0.302-0.22,0.574-0.447,0.83-0.678l0.001-0.001V45.703z"/>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
|
|
@ -0,0 +1,69 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, 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 480.002 480.002" style="enable-background:new 0 0 480.002 480.002;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M454.937,253.555h-46.129c-0.737-24.46-4.62-48.219-11.407-70.503h46.216c5.522,0,10-4.478,10-10V93.87
|
||||
c0-5.522-4.478-10-10-10c-5.522,0-10,4.478-10,10v69.183h-43.249c-7.879-19.665-18.15-37.878-30.643-54.015
|
||||
c-12.418-16.041-26.445-29.301-41.623-39.564l41.302-53.352c3.381-4.367,2.581-10.648-1.786-14.028
|
||||
c-4.365-3.38-10.647-2.582-14.028,1.786l-42.882,55.394c-19.67-9.842-40.834-14.994-62.653-14.994
|
||||
c-19.866,0-39.188,4.275-57.333,12.466L139.78,3.878c-3.382-4.367-9.661-5.165-14.029-1.783
|
||||
c-4.366,3.382-5.165,9.662-1.783,14.029l38.879,50.204c-17.042,10.643-32.736,24.978-46.461,42.711
|
||||
c-12.488,16.136-22.758,34.348-30.635,54.014H46.386V93.87c0-5.522-4.478-10-10-10c-5.523,0-10,4.478-10,10v79.183
|
||||
c0,5.522,4.477,10,10,10H78.72c-6.784,22.284-10.666,46.042-11.403,70.503H25.065c-5.522,0-10,4.477-10,10
|
||||
c0,5.522,4.477,10,10,10h42.344c0.984,24.499,5.134,48.26,12.191,70.502H36.386c-5.523,0-10,4.478-10,10v79.183
|
||||
c0,5.522,4.477,10,10,10c5.522,0,10-4.478,10-10v-69.183h40.509c7.718,18.586,17.604,35.822,29.491,51.182
|
||||
c32.321,41.763,75.531,64.762,121.669,64.762c46.133,0,89.343-22.999,121.671-64.761c11.891-15.36,21.779-32.597,29.498-51.183
|
||||
h44.393v69.183c0,5.522,4.478,10,10,10c5.522,0,10-4.478,10-10v-79.183c0-5.522-4.478-10-10-10H396.52
|
||||
c7.06-22.242,11.21-46.003,12.195-70.502h46.222c5.523,0,10-4.478,10-10C464.937,258.033,460.46,253.555,454.937,253.555z
|
||||
M238.055,64.279c32.317,0,62.292,13.396,86.871,36.167c-58.356,25.081-103.911,23.195-132.054,17.058
|
||||
c-20.526-4.478-35.005-11.78-42.527-16.266C175.079,77.985,205.368,64.279,238.055,64.279z M230.001,459.717
|
||||
C153.423,454.41,91.808,373.87,87.432,273.555h142.569V459.717z M250.001,459.382V273.555h138.691
|
||||
C384.39,372.165,324.774,451.665,250.001,459.382z M87.325,253.555c0.875-26.747,5.817-52.131,14.036-75.121
|
||||
c0.762-1.189,1.274-2.549,1.476-4.01c8.366-22.088,19.781-41.852,33.529-58.327c7.627,4.987,24.998,14.808,50.975,20.665
|
||||
c11.776,2.655,26.268,4.662,43.177,4.662c28.859,0,64.756-5.846,106.179-24.283c0.957-0.426,1.813-0.984,2.558-1.643
|
||||
c28.994,34.435,47.759,83.41,49.546,138.057H87.325z"/>
|
||||
<path d="M170.013,148.917c-23.06,0-41.819,18.761-41.819,41.82c0,23.051,18.76,41.805,41.819,41.805
|
||||
c23.051,0,41.805-18.754,41.805-41.805C211.817,167.678,193.063,148.917,170.013,148.917z M170.013,212.542
|
||||
c-12.031,0-21.819-9.781-21.819-21.805c0-12.032,9.788-21.82,21.819-21.82c12.023,0,21.805,9.788,21.805,21.82
|
||||
C191.817,202.761,182.036,212.542,170.013,212.542z"/>
|
||||
<path d="M305.752,148.917c-23.06,0-41.819,18.761-41.819,41.82c-0.001,23.051,18.759,41.805,41.819,41.805
|
||||
s41.819-18.754,41.819-41.805C347.571,167.678,328.811,148.917,305.752,148.917z M305.752,212.542
|
||||
c-12.031,0-21.819-9.781-21.819-21.805c-0.001-12.032,9.788-21.82,21.819-21.82s21.819,9.788,21.819,21.82
|
||||
C327.571,202.76,317.783,212.542,305.752,212.542z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 58 58" style="enable-background:new 0 0 58 58;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M50.949,12.187l-1.361-1.361l-9.504-9.505c-0.001-0.001-0.001-0.001-0.002-0.001l-0.77-0.771
|
||||
C38.957,0.195,38.486,0,37.985,0H8.963C7.776,0,6.5,0.916,6.5,2.926V39v16.537V56c0,0.837,0.841,1.652,1.836,1.909
|
||||
c0.051,0.014,0.1,0.033,0.152,0.043C8.644,57.983,8.803,58,8.963,58h40.074c0.16,0,0.319-0.017,0.475-0.048
|
||||
c0.052-0.01,0.101-0.029,0.152-0.043C50.659,57.652,51.5,56.837,51.5,56v-0.463V39V13.978C51.5,13.211,51.407,12.644,50.949,12.187
|
||||
z M39.5,3.565L47.935,12H39.5V3.565z M8.963,56c-0.071,0-0.135-0.025-0.198-0.049C8.61,55.877,8.5,55.721,8.5,55.537V41h41v14.537
|
||||
c0,0.184-0.11,0.34-0.265,0.414C49.172,55.975,49.108,56,49.037,56H8.963z M8.5,39V2.926C8.5,2.709,8.533,2,8.963,2h28.595
|
||||
C37.525,2.126,37.5,2.256,37.5,2.391V13.78c-0.532-0.48-1.229-0.78-2-0.78c-0.553,0-1,0.448-1,1s0.447,1,1,1c0.552,0,1,0.449,1,1v4
|
||||
c0,1.2,0.542,2.266,1.382,3c-0.84,0.734-1.382,1.8-1.382,3v4c0,0.551-0.448,1-1,1c-0.553,0-1,0.448-1,1s0.447,1,1,1
|
||||
c1.654,0,3-1.346,3-3v-4c0-1.103,0.897-2,2-2c0.553,0,1-0.448,1-1s-0.447-1-1-1c-1.103,0-2-0.897-2-2v-4
|
||||
c0-0.771-0.301-1.468-0.78-2h11.389c0.135,0,0.265-0.025,0.391-0.058c0,0.015,0.001,0.021,0.001,0.036V39H8.5z"/>
|
||||
<path d="M16.354,51.43c-0.019,0.446-0.171,0.764-0.458,0.95s-0.672,0.28-1.155,0.28c-0.191,0-0.396-0.022-0.615-0.068
|
||||
s-0.429-0.098-0.629-0.157s-0.385-0.123-0.554-0.191s-0.299-0.135-0.39-0.198l-0.697,1.107c0.183,0.137,0.405,0.26,0.67,0.369
|
||||
s0.54,0.207,0.827,0.294s0.565,0.15,0.834,0.191s0.504,0.062,0.704,0.062c0.401,0,0.791-0.039,1.169-0.116
|
||||
c0.378-0.077,0.713-0.214,1.005-0.41s0.524-0.456,0.697-0.779s0.26-0.723,0.26-1.196v-7.848h-1.668V51.43z"/>
|
||||
<path d="M25.083,49.064c-0.314-0.228-0.654-0.422-1.019-0.581s-0.702-0.323-1.012-0.492s-0.569-0.364-0.779-0.588
|
||||
s-0.314-0.518-0.314-0.882c0-0.146,0.036-0.299,0.109-0.458s0.173-0.303,0.301-0.431s0.273-0.234,0.438-0.321
|
||||
s0.337-0.139,0.52-0.157c0.328-0.027,0.597-0.032,0.807-0.014s0.378,0.05,0.506,0.096s0.226,0.091,0.294,0.137
|
||||
s0.13,0.082,0.185,0.109c0.009-0.009,0.036-0.055,0.082-0.137s0.101-0.185,0.164-0.308s0.132-0.255,0.205-0.396
|
||||
s0.137-0.271,0.191-0.39c-0.265-0.173-0.61-0.299-1.039-0.376s-0.853-0.116-1.271-0.116c-0.41,0-0.8,0.063-1.169,0.191
|
||||
s-0.692,0.313-0.971,0.554s-0.499,0.535-0.663,0.882S20.4,46.13,20.4,46.576c0,0.492,0.104,0.902,0.314,1.23
|
||||
s0.474,0.613,0.793,0.854s0.661,0.451,1.025,0.629s0.704,0.355,1.019,0.533s0.576,0.376,0.786,0.595s0.314,0.483,0.314,0.793
|
||||
c0,0.511-0.148,0.896-0.444,1.155s-0.723,0.39-1.278,0.39c-0.183,0-0.378-0.019-0.588-0.055s-0.419-0.084-0.629-0.144
|
||||
s-0.412-0.123-0.608-0.191s-0.357-0.139-0.485-0.212l-0.287,1.176c0.155,0.137,0.34,0.253,0.554,0.349s0.439,0.171,0.677,0.226
|
||||
c0.237,0.055,0.472,0.094,0.704,0.116s0.458,0.034,0.677,0.034c0.511,0,0.966-0.077,1.367-0.232s0.738-0.362,1.012-0.622
|
||||
s0.485-0.561,0.636-0.902s0.226-0.695,0.226-1.06c0-0.538-0.104-0.978-0.314-1.319S25.397,49.292,25.083,49.064z"/>
|
||||
<path d="M34.872,45.072c-0.378-0.429-0.82-0.754-1.326-0.978s-1.06-0.335-1.661-0.335s-1.155,0.111-1.661,0.335
|
||||
s-0.948,0.549-1.326,0.978s-0.675,0.964-0.889,1.606s-0.321,1.388-0.321,2.235s0.107,1.595,0.321,2.242s0.511,1.185,0.889,1.613
|
||||
s0.82,0.752,1.326,0.971s1.06,0.328,1.661,0.328s1.155-0.109,1.661-0.328s0.948-0.542,1.326-0.971s0.675-0.966,0.889-1.613
|
||||
s0.321-1.395,0.321-2.242s-0.107-1.593-0.321-2.235S35.25,45.501,34.872,45.072z M34.195,50.698
|
||||
c-0.137,0.487-0.326,0.882-0.567,1.183s-0.515,0.518-0.82,0.649s-0.627,0.198-0.964,0.198c-0.328,0-0.641-0.07-0.937-0.212
|
||||
s-0.561-0.364-0.793-0.67s-0.415-0.699-0.547-1.183s-0.203-1.066-0.212-1.75c0.009-0.702,0.082-1.294,0.219-1.777
|
||||
c0.137-0.483,0.326-0.877,0.567-1.183s0.515-0.521,0.82-0.649s0.627-0.191,0.964-0.191c0.328,0,0.641,0.068,0.937,0.205
|
||||
s0.561,0.36,0.793,0.67s0.415,0.704,0.547,1.183s0.203,1.06,0.212,1.743C34.405,49.616,34.332,50.211,34.195,50.698z"/>
|
||||
<polygon points="44.012,50.869 40.061,43.924 38.393,43.924 38.393,54 40.061,54 40.061,47.055 44.012,54 45.68,54 45.68,43.924
|
||||
44.012,43.924 "/>
|
||||
<path d="M20.5,20v-4c0-0.551,0.448-1,1-1c0.553,0,1-0.448,1-1s-0.447-1-1-1c-1.654,0-3,1.346-3,3v4c0,1.103-0.897,2-2,2
|
||||
c-0.553,0-1,0.448-1,1s0.447,1,1,1c1.103,0,2,0.897,2,2v4c0,1.654,1.346,3,3,3c0.553,0,1-0.448,1-1s-0.447-1-1-1
|
||||
c-0.552,0-1-0.449-1-1v-4c0-1.2-0.542-2.266-1.382-3C19.958,22.266,20.5,21.2,20.5,20z"/>
|
||||
<circle cx="28.5" cy="19.5" r="1.5"/>
|
||||
<path d="M28.5,25c-0.553,0-1,0.448-1,1v3c0,0.552,0.447,1,1,1s1-0.448,1-1v-3C29.5,25.448,29.053,25,28.5,25z"/>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version='1.0' encoding='iso-8859-1'?>
|
||||
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 408.352 408.352" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 408.352 408.352">
|
||||
<path d="m408.346,163.059c0-71.92-54.61-131.589-125.445-138.512-2.565-8.81-10.698-15.272-20.325-15.272h-116.801c-9.627,0-17.759,6.462-20.324,15.272-70.837,6.932-125.451,66.602-125.451,138.512v111.142c0,23.601 19.201,42.802 42.801,42.802h33.32c9.916,0 17.983-8.067 17.983-17.983v-118.102c0-9.916-8.067-17.983-17.983-17.983h-33.32c-10.606,0-20.316,3.886-27.801,10.298v-10.174c-1.06581e-14-64.07 48.585-117.252 111.653-123.559 3.401,7.16 10.682,12.134 19.122,12.134h116.801c8.44,0 15.721-4.974 19.123-12.134 63.065,6.299 111.647,59.481 111.647,123.56v10.169c-7.485-6.409-17.193-10.294-27.796-10.294h-33.32c-9.916,0-17.983,8.067-17.983,17.983v118.101c0,9.916 8.067,17.983 17.983,17.983h33.32c10.606,0 20.316-3.886 27.802-10.299v5.459c0,28.339-23.056,51.395-51.395,51.395h-90.885c-3.288-11.818-14.14-20.518-26.991-20.518h-27.357c-15.449,0-28.018,12.569-28.018,28.018s12.569,28.018 28.018,28.018h27.357c12.851,0 23.703-8.7 26.991-20.518h90.885c36.61,0 66.395-29.784 66.395-66.395l-.006-149.103zm-329.241,17.859v118.101c-1.42109e-14,1.645-1.338,2.983-2.983,2.983h-2.983v-124.067h2.983c1.645,0 2.983,1.338 2.983,2.983zm-36.304-2.983h15.337v124.068h-15.337c-15.33,0-27.801-12.472-27.801-27.802v-68.465c-3.55271e-15-15.33 12.472-27.801 27.801-27.801zm219.775-141.302h-116.801c-3.407-7.10543e-15-6.179-2.772-6.179-6.179s2.772-6.179 6.179-6.179h116.801c3.407,0 6.18,2.772 6.18,6.179s-2.773,6.179-6.18,6.179zm66.67,262.386v-118.101c0-1.645 1.338-2.983 2.983-2.983h2.983v124.068h-2.983c-1.645,0-2.983-1.339-2.983-2.984zm-105.165,85.057h-27.357c-7.178,0-13.018-5.84-13.018-13.018s5.84-13.018 13.018-13.018h27.357c7.179,0 13.019,5.84 13.019,13.018s-5.84,13.018-13.019,13.018zm141.469-82.073h-15.337v-124.068h15.337c15.33,0 27.802,12.472 27.802,27.801v68.465c-5.68434e-14,15.33-12.472,27.802-27.802,27.802z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
1
frontend/assets/images/icons/widgets/qrscanner.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><rect fill="none" height="24" width="24"/><path d="M9.5,6.5v3h-3v-3H9.5 M11,5H5v6h6V5L11,5z M9.5,14.5v3h-3v-3H9.5 M11,13H5v6h6V13L11,13z M17.5,6.5v3h-3v-3H17.5 M19,5h-6v6 h6V5L19,5z M13,13h1.5v1.5H13V13z M14.5,14.5H16V16h-1.5V14.5z M16,13h1.5v1.5H16V13z M13,16h1.5v1.5H13V16z M14.5,17.5H16V19h-1.5 V17.5z M16,16h1.5v1.5H16V16z M17.5,14.5H19V16h-1.5V14.5z M17.5,17.5H19V19h-1.5V17.5z M22,7h-2V4h-3V2h5V7z M22,22v-5h-2v3h-3v2 H22z M2,22h5v-2H4v-3H2V22z M2,2v5h2V4h3V2H2z"/></svg>
|
||||
|
After Width: | Height: | Size: 614 B |
1
frontend/assets/images/icons/widgets/radio-button.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g data-name="Layer 2"><g data-name="radio-button-on"><rect width="24" height="24" opacity="0"/><path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"/><path d="M12 7a5 5 0 1 0 5 5 5 5 0 0 0-5-5z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 330 B |
|
|
@ -11,4 +11,4 @@
|
|||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
5
frontend/assets/images/icons/widgets/toggle.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-toggle-right" 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>
|
||||
<circle cx="16" cy="12" r="2"></circle>
|
||||
<rect x="2" y="6" width="20" height="12" rx="6"></rect>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 401 B |
5
frontend/assets/images/icons/widgets/toggleswitch.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-toggle-right" 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>
|
||||
<circle cx="16" cy="12" r="2"></circle>
|
||||
<rect x="2" y="6" width="20" height="12" rx="6"></rect>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 401 B |
339
frontend/package-lock.json
generated
|
|
@ -5,6 +5,7 @@
|
|||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.4.3",
|
||||
|
|
@ -47,11 +48,13 @@
|
|||
"react-lazyload": "^3.2.0",
|
||||
"react-loading-skeleton": "^2.2.0",
|
||||
"react-plotly.js": "^2.5.1",
|
||||
"react-qr-reader": "^2.2.1",
|
||||
"react-resizable": "^1.11.1",
|
||||
"react-rnd": "^10.3.0",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"react-scripts": "3.4.3",
|
||||
"react-select-search": "^3.0.5",
|
||||
"react-spring": "^9.2.4",
|
||||
"react-table": "^7.6.3",
|
||||
"react-table-plugins": "^1.3.1",
|
||||
"react-toastify": "^7.0.3",
|
||||
|
|
@ -2227,6 +2230,133 @@
|
|||
"resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-2.1.1.tgz",
|
||||
"integrity": "sha512-LIqf1T2dVXRaf/gqk/7kEDpRvr1aQGlqOv/p5zwZus3qI6/3qK4jxNpc89gQ2WkCMrGwcI/fTQrnI55BZCxsLA=="
|
||||
},
|
||||
"node_modules/@react-spring/animated": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.2.4.tgz",
|
||||
"integrity": "sha512-AfV6ZM8pCCAT29GY5C8/1bOPjZrv/7kD0vedjiE/tEYvNDwg9GlscrvsTViWR2XykJoYrDfdkYArrldWpsCJ5g==",
|
||||
"dependencies": {
|
||||
"@react-spring/shared": "~9.2.0",
|
||||
"@react-spring/types": "~9.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/core": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.2.4.tgz",
|
||||
"integrity": "sha512-R+PwyfsjiuYCWqaTTfCpYpRmsP0h87RNm7uxC1Uxy7QAHUfHEm2sAHn+AdHPwq/MbVwDssVT8C5yf2WGcqiXGg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@react-spring/animated": "~9.2.0",
|
||||
"@react-spring/shared": "~9.2.0",
|
||||
"@react-spring/types": "~9.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/konva": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/konva/-/konva-9.2.4.tgz",
|
||||
"integrity": "sha512-19anDOIkfjcydDTfGgVIuZ3lruZxKubYGs9oHCswaP8SRLj7c1kkopJHUr/S4LXGxiIdqdF0XucWm0iTEPEq4w==",
|
||||
"dependencies": {
|
||||
"@react-spring/animated": "~9.2.0",
|
||||
"@react-spring/core": "~9.2.0",
|
||||
"@react-spring/shared": "~9.2.0",
|
||||
"@react-spring/types": "~9.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"konva": ">=2.6",
|
||||
"react": "^16.8.0 || ^17.0.0",
|
||||
"react-konva": "^16.8.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/native": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/native/-/native-9.2.4.tgz",
|
||||
"integrity": "sha512-xKJWKh5qOhSclpL3iuGwJRLoZzTNvlBEnIrMs8yh8xvX6z9Lmnu4uGu5DpfrnM1GzBvRoktoCoLEx/VcEYFSng==",
|
||||
"dependencies": {
|
||||
"@react-spring/animated": "~9.2.0",
|
||||
"@react-spring/core": "~9.2.0",
|
||||
"@react-spring/shared": "~9.2.0",
|
||||
"@react-spring/types": "~9.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0",
|
||||
"react-native": ">=0.58"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/rafz": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.2.4.tgz",
|
||||
"integrity": "sha512-SOKf9eue+vAX+DGo7kWYNl9i9J3gPUlQjifIcV9Bzw9h3i30wPOOP0TjS7iMG/kLp2cdHQYDNFte6nt23VAZkQ=="
|
||||
},
|
||||
"node_modules/@react-spring/shared": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.2.4.tgz",
|
||||
"integrity": "sha512-ZEr4l2BxmyFRUvRA2VCkPfCJii4E7cGkwbjmTBx1EmcGrOnde/V2eF5dxqCTY3k35QuCegkrWe0coRJVkh8q2Q==",
|
||||
"dependencies": {
|
||||
"@react-spring/rafz": "~9.2.0",
|
||||
"@react-spring/types": "~9.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/three": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.2.4.tgz",
|
||||
"integrity": "sha512-ljFig7XW099VWwRPKPUf+4yYLivp/sSWXN3oO5SJOF/9BSoV1quS/9chZ5Myl5J14od3CsHf89Tv4FdlX5kHlA==",
|
||||
"dependencies": {
|
||||
"@react-spring/animated": "~9.2.0",
|
||||
"@react-spring/core": "~9.2.0",
|
||||
"@react-spring/shared": "~9.2.0",
|
||||
"@react-spring/types": "~9.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-three/fiber": ">=6.0",
|
||||
"react": ">=16.11",
|
||||
"three": ">=0.126"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/types": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.2.4.tgz",
|
||||
"integrity": "sha512-zHUXrWO8nweUN/ISjrjqU7GgXXvoEbFca1CgiE0TY0H/dqJb3l+Rhx8ecPVNYimzFg3ZZ1/T0egpLop8SOv4aA=="
|
||||
},
|
||||
"node_modules/@react-spring/web": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.2.4.tgz",
|
||||
"integrity": "sha512-vtPvOalLFvuju/MDBtoSnCyt0xXSL6Amyv82fljOuWPl1yGd4M1WteijnYL9Zlriljl0a3oXcPunAVYTD9dbDQ==",
|
||||
"dependencies": {
|
||||
"@react-spring/animated": "~9.2.0",
|
||||
"@react-spring/core": "~9.2.0",
|
||||
"@react-spring/shared": "~9.2.0",
|
||||
"@react-spring/types": "~9.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-spring/zdog": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/zdog/-/zdog-9.2.4.tgz",
|
||||
"integrity": "sha512-rv7ptedS37SHr6yuCbRkUErAzAhebdgt8f4KUtZWzseC+7qLNkaZWf+uujgsb881qAuX9b9yz8rre9UKeYepgw==",
|
||||
"dependencies": {
|
||||
"@react-spring/animated": "~9.2.0",
|
||||
"@react-spring/core": "~9.2.0",
|
||||
"@react-spring/shared": "~9.2.0",
|
||||
"@react-spring/types": "~9.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0",
|
||||
"react-zdog": ">=1.0",
|
||||
"zdog": ">=1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@restart/context": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz",
|
||||
|
|
@ -8690,7 +8820,8 @@
|
|||
"node_modules/flatten": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz",
|
||||
"integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg=="
|
||||
"integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==",
|
||||
"deprecated": "flatten is deprecated in favor of utility frameworks such as lodash."
|
||||
},
|
||||
"node_modules/flush-write-stream": {
|
||||
"version": "1.1.1",
|
||||
|
|
@ -11633,6 +11764,11 @@
|
|||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jsqr": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",
|
||||
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A=="
|
||||
},
|
||||
"node_modules/killable": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||
|
|
@ -16191,6 +16327,20 @@
|
|||
"react": "^15.0.0-0 || ^16.0.0-0 || ^17.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-qr-reader": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-qr-reader/-/react-qr-reader-2.2.1.tgz",
|
||||
"integrity": "sha512-EL5JEj53u2yAOgtpAKAVBzD/SiKWn0Bl7AZy6ZrSf1lub7xHwtaXe6XSx36Wbhl1VMGmvmrwYMRwO1aSCT2fwA==",
|
||||
"dependencies": {
|
||||
"jsqr": "^1.2.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"webrtc-adapter": "^7.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "~16",
|
||||
"react-dom": "~16"
|
||||
}
|
||||
},
|
||||
"node_modules/react-resizable": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-1.11.1.tgz",
|
||||
|
|
@ -17207,6 +17357,19 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/react-spring": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react-spring/-/react-spring-9.2.4.tgz",
|
||||
"integrity": "sha512-bMjbyTW0ZGd+/h9cjtohLqCwOGqX2OuaTvalOVfLCGmhzEg/u3GgopI3LAm4UD2Br3MNdVdGgNVoESg4MGqKFQ==",
|
||||
"dependencies": {
|
||||
"@react-spring/core": "~9.2.0",
|
||||
"@react-spring/konva": "~9.2.0",
|
||||
"@react-spring/native": "~9.2.0",
|
||||
"@react-spring/three": "~9.2.0",
|
||||
"@react-spring/web": "~9.2.0",
|
||||
"@react-spring/zdog": "~9.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-table": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/react-table/-/react-table-7.6.3.tgz",
|
||||
|
|
@ -17954,6 +18117,18 @@
|
|||
"node": "6.* || >= 7.*"
|
||||
}
|
||||
},
|
||||
"node_modules/rtcpeerconnection-shim": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
|
||||
"integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
|
||||
"dependencies": {
|
||||
"sdp": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0",
|
||||
"npm": ">=3.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/run-async": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
|
||||
|
|
@ -18179,6 +18354,11 @@
|
|||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sdp": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz",
|
||||
"integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw=="
|
||||
},
|
||||
"node_modules/select-hose": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
|
||||
|
|
@ -19279,6 +19459,7 @@
|
|||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
|
||||
"integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
|
||||
"deprecated": "This version of tar is no longer supported, and will not receive security updates. Please upgrade asap.",
|
||||
"dependencies": {
|
||||
"block-stream": "*",
|
||||
"fstream": "^1.0.12",
|
||||
|
|
@ -20993,6 +21174,19 @@
|
|||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
|
||||
},
|
||||
"node_modules/webrtc-adapter": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz",
|
||||
"integrity": "sha512-TbrbBmiQBL9n0/5bvDdORc6ZfRY/Z7JnEj+EYOD1ghseZdpJ+nF2yx14k3LgQKc7JZnG7HAcL+zHnY25So9d7A==",
|
||||
"dependencies": {
|
||||
"rtcpeerconnection-shim": "^1.2.15",
|
||||
"sdp": "^2.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0",
|
||||
"npm": ">=3.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/websocket-driver": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
|
||||
|
|
@ -23233,6 +23427,99 @@
|
|||
"resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-2.1.1.tgz",
|
||||
"integrity": "sha512-LIqf1T2dVXRaf/gqk/7kEDpRvr1aQGlqOv/p5zwZus3qI6/3qK4jxNpc89gQ2WkCMrGwcI/fTQrnI55BZCxsLA=="
|
||||
},
|
||||
"@react-spring/animated": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.2.4.tgz",
|
||||
"integrity": "sha512-AfV6ZM8pCCAT29GY5C8/1bOPjZrv/7kD0vedjiE/tEYvNDwg9GlscrvsTViWR2XykJoYrDfdkYArrldWpsCJ5g==",
|
||||
"requires": {
|
||||
"@react-spring/shared": "~9.2.0",
|
||||
"@react-spring/types": "~9.2.0"
|
||||
}
|
||||
},
|
||||
"@react-spring/core": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.2.4.tgz",
|
||||
"integrity": "sha512-R+PwyfsjiuYCWqaTTfCpYpRmsP0h87RNm7uxC1Uxy7QAHUfHEm2sAHn+AdHPwq/MbVwDssVT8C5yf2WGcqiXGg==",
|
||||
"requires": {
|
||||
"@react-spring/animated": "~9.2.0",
|
||||
"@react-spring/shared": "~9.2.0",
|
||||
"@react-spring/types": "~9.2.0"
|
||||
}
|
||||
},
|
||||
"@react-spring/konva": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/konva/-/konva-9.2.4.tgz",
|
||||
"integrity": "sha512-19anDOIkfjcydDTfGgVIuZ3lruZxKubYGs9oHCswaP8SRLj7c1kkopJHUr/S4LXGxiIdqdF0XucWm0iTEPEq4w==",
|
||||
"requires": {
|
||||
"@react-spring/animated": "~9.2.0",
|
||||
"@react-spring/core": "~9.2.0",
|
||||
"@react-spring/shared": "~9.2.0",
|
||||
"@react-spring/types": "~9.2.0"
|
||||
}
|
||||
},
|
||||
"@react-spring/native": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/native/-/native-9.2.4.tgz",
|
||||
"integrity": "sha512-xKJWKh5qOhSclpL3iuGwJRLoZzTNvlBEnIrMs8yh8xvX6z9Lmnu4uGu5DpfrnM1GzBvRoktoCoLEx/VcEYFSng==",
|
||||
"requires": {
|
||||
"@react-spring/animated": "~9.2.0",
|
||||
"@react-spring/core": "~9.2.0",
|
||||
"@react-spring/shared": "~9.2.0",
|
||||
"@react-spring/types": "~9.2.0"
|
||||
}
|
||||
},
|
||||
"@react-spring/rafz": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.2.4.tgz",
|
||||
"integrity": "sha512-SOKf9eue+vAX+DGo7kWYNl9i9J3gPUlQjifIcV9Bzw9h3i30wPOOP0TjS7iMG/kLp2cdHQYDNFte6nt23VAZkQ=="
|
||||
},
|
||||
"@react-spring/shared": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.2.4.tgz",
|
||||
"integrity": "sha512-ZEr4l2BxmyFRUvRA2VCkPfCJii4E7cGkwbjmTBx1EmcGrOnde/V2eF5dxqCTY3k35QuCegkrWe0coRJVkh8q2Q==",
|
||||
"requires": {
|
||||
"@react-spring/rafz": "~9.2.0",
|
||||
"@react-spring/types": "~9.2.0"
|
||||
}
|
||||
},
|
||||
"@react-spring/three": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.2.4.tgz",
|
||||
"integrity": "sha512-ljFig7XW099VWwRPKPUf+4yYLivp/sSWXN3oO5SJOF/9BSoV1quS/9chZ5Myl5J14od3CsHf89Tv4FdlX5kHlA==",
|
||||
"requires": {
|
||||
"@react-spring/animated": "~9.2.0",
|
||||
"@react-spring/core": "~9.2.0",
|
||||
"@react-spring/shared": "~9.2.0",
|
||||
"@react-spring/types": "~9.2.0"
|
||||
}
|
||||
},
|
||||
"@react-spring/types": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.2.4.tgz",
|
||||
"integrity": "sha512-zHUXrWO8nweUN/ISjrjqU7GgXXvoEbFca1CgiE0TY0H/dqJb3l+Rhx8ecPVNYimzFg3ZZ1/T0egpLop8SOv4aA=="
|
||||
},
|
||||
"@react-spring/web": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.2.4.tgz",
|
||||
"integrity": "sha512-vtPvOalLFvuju/MDBtoSnCyt0xXSL6Amyv82fljOuWPl1yGd4M1WteijnYL9Zlriljl0a3oXcPunAVYTD9dbDQ==",
|
||||
"requires": {
|
||||
"@react-spring/animated": "~9.2.0",
|
||||
"@react-spring/core": "~9.2.0",
|
||||
"@react-spring/shared": "~9.2.0",
|
||||
"@react-spring/types": "~9.2.0"
|
||||
}
|
||||
},
|
||||
"@react-spring/zdog": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-spring/zdog/-/zdog-9.2.4.tgz",
|
||||
"integrity": "sha512-rv7ptedS37SHr6yuCbRkUErAzAhebdgt8f4KUtZWzseC+7qLNkaZWf+uujgsb881qAuX9b9yz8rre9UKeYepgw==",
|
||||
"requires": {
|
||||
"@react-spring/animated": "~9.2.0",
|
||||
"@react-spring/core": "~9.2.0",
|
||||
"@react-spring/shared": "~9.2.0",
|
||||
"@react-spring/types": "~9.2.0"
|
||||
}
|
||||
},
|
||||
"@restart/context": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz",
|
||||
|
|
@ -30684,6 +30971,11 @@
|
|||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"jsqr": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",
|
||||
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A=="
|
||||
},
|
||||
"killable": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||
|
|
@ -34357,6 +34649,16 @@
|
|||
"prop-types": "^15.5.8"
|
||||
}
|
||||
},
|
||||
"react-qr-reader": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-qr-reader/-/react-qr-reader-2.2.1.tgz",
|
||||
"integrity": "sha512-EL5JEj53u2yAOgtpAKAVBzD/SiKWn0Bl7AZy6ZrSf1lub7xHwtaXe6XSx36Wbhl1VMGmvmrwYMRwO1aSCT2fwA==",
|
||||
"requires": {
|
||||
"jsqr": "^1.2.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"webrtc-adapter": "^7.2.1"
|
||||
}
|
||||
},
|
||||
"react-resizable": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-1.11.1.tgz",
|
||||
|
|
@ -35148,6 +35450,19 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"react-spring": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react-spring/-/react-spring-9.2.4.tgz",
|
||||
"integrity": "sha512-bMjbyTW0ZGd+/h9cjtohLqCwOGqX2OuaTvalOVfLCGmhzEg/u3GgopI3LAm4UD2Br3MNdVdGgNVoESg4MGqKFQ==",
|
||||
"requires": {
|
||||
"@react-spring/core": "~9.2.0",
|
||||
"@react-spring/konva": "~9.2.0",
|
||||
"@react-spring/native": "~9.2.0",
|
||||
"@react-spring/three": "~9.2.0",
|
||||
"@react-spring/web": "~9.2.0",
|
||||
"@react-spring/zdog": "~9.2.0"
|
||||
}
|
||||
},
|
||||
"react-table": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/react-table/-/react-table-7.6.3.tgz",
|
||||
|
|
@ -35727,6 +36042,14 @@
|
|||
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
|
||||
"integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA=="
|
||||
},
|
||||
"rtcpeerconnection-shim": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
|
||||
"integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
|
||||
"requires": {
|
||||
"sdp": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"run-async": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
|
||||
|
|
@ -35891,6 +36214,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"sdp": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz",
|
||||
"integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw=="
|
||||
},
|
||||
"select-hose": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
|
||||
|
|
@ -38141,6 +38469,15 @@
|
|||
"source-map": "~0.6.1"
|
||||
}
|
||||
},
|
||||
"webrtc-adapter": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz",
|
||||
"integrity": "sha512-TbrbBmiQBL9n0/5bvDdORc6ZfRY/Z7JnEj+EYOD1ghseZdpJ+nF2yx14k3LgQKc7JZnG7HAcL+zHnY25So9d7A==",
|
||||
"requires": {
|
||||
"rtcpeerconnection-shim": "^1.2.15",
|
||||
"sdp": "^2.12.0"
|
||||
}
|
||||
},
|
||||
"websocket-driver": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
|
||||
|
|
|
|||
|
|
@ -43,11 +43,13 @@
|
|||
"react-lazyload": "^3.2.0",
|
||||
"react-loading-skeleton": "^2.2.0",
|
||||
"react-plotly.js": "^2.5.1",
|
||||
"react-qr-reader": "^2.2.1",
|
||||
"react-resizable": "^1.11.1",
|
||||
"react-rnd": "^10.3.0",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"react-scripts": "3.4.3",
|
||||
"react-select-search": "^3.0.5",
|
||||
"react-spring": "^9.2.4",
|
||||
"react-table": "^7.6.3",
|
||||
"react-table-plugins": "^1.3.1",
|
||||
"react-toastify": "^7.0.3",
|
||||
|
|
@ -55,6 +57,7 @@
|
|||
"rxjs": "^6.3.3",
|
||||
"semver": "^5.7.1",
|
||||
"tinycolor2": "^1.4.2",
|
||||
"uuid": "8.3.2",
|
||||
"webpack-cli": "^3.3.0",
|
||||
"yup": "^0.27.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import '@/_styles/theme.scss';
|
|||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { ManageOrgUsers } from '@/ManageOrgUsers';
|
||||
import { SettingsPage } from '../SettingsPage/SettingsPage';
|
||||
import { OnboardingModal } from '@/Onboarding/OnboardingModal';
|
||||
import {ForgotPassword} from '@/ForgotPassword'
|
||||
import { ResetPassword } from '@/ResetPassword';
|
||||
|
|
@ -87,6 +88,7 @@ class App extends React.Component {
|
|||
<PrivateRoute exact path="/applications/:slug" component={Viewer} switchDarkMode={this.switchDarkMode} darkMode={darkMode}/>
|
||||
<PrivateRoute exact path="/oauth2/authorize" component={Authorize} switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
<PrivateRoute exact path="/users" component={ManageOrgUsers} switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
<PrivateRoute exact path="/settings" component={SettingsPage} switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ import { Multiselect } from './Components/Multiselect';
|
|||
import { Modal } from './Components/Modal';
|
||||
import { Chart } from './Components/Chart';
|
||||
import { Map } from './Components/Map/Map';
|
||||
import { QrScanner } from './Components/QrScanner/QrScanner';
|
||||
import { ToggleSwitch } from './Components/Toggle'
|
||||
import { RadioButton } from './Components/RadioButton'
|
||||
import { renderTooltip } from '../_helpers/appUtils';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
|
||||
|
|
@ -35,6 +38,9 @@ const AllComponents = {
|
|||
Modal,
|
||||
Chart,
|
||||
Map,
|
||||
QrScanner,
|
||||
ToggleSwitch,
|
||||
RadioButton
|
||||
};
|
||||
|
||||
export const Box = function Box({
|
||||
|
|
|
|||
|
|
@ -24,11 +24,14 @@ export function CodeHinter({
|
|||
placeholder,
|
||||
ignoreBraces,
|
||||
enablePreview,
|
||||
height
|
||||
height,
|
||||
minHeight,
|
||||
lineWrapping
|
||||
}) {
|
||||
console.log('theme', theme)
|
||||
const options = {
|
||||
lineNumbers: lineNumbers,
|
||||
lineWrapping: lineWrapping,
|
||||
singleLine: true,
|
||||
mode: mode || 'handlebars',
|
||||
tabSize: 2,
|
||||
|
|
@ -55,12 +58,16 @@ export function CodeHinter({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={`code-hinter ${className || 'codehinter-default-input'}`} key={suggestions.length}>
|
||||
<div
|
||||
className={`code-hinter ${className || 'codehinter-default-input'}`}
|
||||
key={suggestions.length}
|
||||
style={{ height: height || 'auto', minHeight, maxHeight: '320px', overflow: 'scroll' }}
|
||||
>
|
||||
<CodeMirror
|
||||
value={initialValue}
|
||||
realState={realState}
|
||||
scrollbarStyle={null}
|
||||
height={height || '100%'}
|
||||
height={height}
|
||||
onBlur={(editor) => {
|
||||
const value = editor.getValue();
|
||||
onChange(value);
|
||||
|
|
@ -70,7 +77,7 @@ export function CodeHinter({
|
|||
options={options}
|
||||
/>
|
||||
{enablePreview &&
|
||||
<div className="dynamic-variable-preview bg-azure-lt px-2 py-1">
|
||||
<div className="dynamic-variable-preview bg-green-lt px-2 py-1">
|
||||
{resolveReferences(currentValue, realState)}
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,11 +57,11 @@ export const DropDown = function DropDown({
|
|||
}, [currentValue]);
|
||||
|
||||
return (
|
||||
<div className="row" style={{ width, height }} onClick={() => onComponentClick(id, component)}>
|
||||
<div className="row g-0" style={{ width, height }} onClick={() => onComponentClick(id, component)}>
|
||||
<div className="col-auto">
|
||||
<label className="form-label p-2">{label}</label>
|
||||
<label style={{marginRight: '1rem'}} className="form-label py-2">{label}</label>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="col px-0">
|
||||
<SelectSearch
|
||||
options={selectOptions}
|
||||
value={currentValue}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ export const Modal = function Modal({
|
|||
snapToGrid={true}
|
||||
parentRef={parentRef}
|
||||
parent={id}
|
||||
currentLayout={containerProps.currentLayout}
|
||||
/>
|
||||
</BootstrapModal.Body>
|
||||
</BootstrapModal>
|
||||
|
|
|
|||
|
|
@ -40,11 +40,11 @@ export const Multiselect = function Multiselect({
|
|||
}, [newValue]);
|
||||
|
||||
return (
|
||||
<div className="row" style={{ width, height }} onClick={() => onComponentClick(id, component)}>
|
||||
<div className="row g-0" style={{ width, height }} onClick={() => onComponentClick(id, component)}>
|
||||
<div className="col-auto">
|
||||
<label className="form-label p-2">{label}</label>
|
||||
<label style={{marginRight: '1rem'}} className="form-label py-2">{label}</label>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="col px-0">
|
||||
<SelectSearch
|
||||
options={selectOptions}
|
||||
value={currentValue}
|
||||
|
|
|
|||
34
frontend/src/Editor/Components/QrScanner/ErrorModal.jsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
export default function ErrorModal() {
|
||||
|
||||
const [show, setShow] = React.useState(true)
|
||||
|
||||
const close = () => {
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
return(
|
||||
<div>
|
||||
{
|
||||
show ?
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">QR Scanner is not working</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onClick={close}></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Please make sure a camera is available on your device. Try closing your browser and opening it again, if it doesn't work, please contact support.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" data-bs-dismiss="modal" onClick={close}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
''
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
36
frontend/src/Editor/Components/QrScanner/QrScanner.jsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import React, { useState } from 'react';
|
||||
import QrReader from 'react-qr-reader';
|
||||
import ErrorModal from './ErrorModal';
|
||||
|
||||
export const QrScanner = function QrScanner({
|
||||
component, onEvent, onComponentOptionChanged
|
||||
}) {
|
||||
|
||||
const handleError = async (errorMessage) => {
|
||||
console.log(errorMessage);
|
||||
setErrorOccured(true);
|
||||
};
|
||||
|
||||
const handleScan = async (data) => {
|
||||
if (data != null) {
|
||||
onEvent('onDetect', { component, data: data });
|
||||
onComponentOptionChanged(component, 'lastDetectedValue', data);
|
||||
};
|
||||
};
|
||||
|
||||
let [errorOccured, setErrorOccured] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
errorOccured ?
|
||||
<ErrorModal />
|
||||
:
|
||||
<QrReader
|
||||
onError={handleError}
|
||||
onScan={handleScan}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
69
frontend/src/Editor/Components/RadioButton.jsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import React from 'react';
|
||||
import { resolveReferences } from '@/_helpers/utils';
|
||||
|
||||
|
||||
export const RadioButton = function RadioButton({
|
||||
id,
|
||||
width,
|
||||
height,
|
||||
component,
|
||||
onComponentClick,
|
||||
currentState,
|
||||
onComponentOptionChanged,
|
||||
onEvent
|
||||
}) {
|
||||
|
||||
const label = component.definition.properties.label.value;
|
||||
const textColorProperty = component.definition.styles.textColor;
|
||||
const textColor = textColorProperty ? textColorProperty.value : '#000';
|
||||
|
||||
const values = component.definition.properties.values.value;
|
||||
const displayValues = component.definition.properties.display_values.value;
|
||||
|
||||
let parsedValues = values;
|
||||
|
||||
try {
|
||||
parsedValues = resolveReferences(values, currentState, []);
|
||||
} catch (err) { console.log(err); }
|
||||
|
||||
let parsedDisplayValues = displayValues;
|
||||
|
||||
try {
|
||||
parsedDisplayValues = resolveReferences(displayValues, currentState, []);
|
||||
} catch (err) { console.log(err); }
|
||||
|
||||
let selectOptions = [];
|
||||
|
||||
try {
|
||||
selectOptions = [
|
||||
...parsedValues.map((value, index) => {
|
||||
return { name: parsedDisplayValues[index], value: value };
|
||||
})
|
||||
];
|
||||
} catch (err) { console.log(err); }
|
||||
|
||||
|
||||
function onSelect(event) {
|
||||
const selection = event.target.value
|
||||
onComponentOptionChanged(component, 'value', selection);
|
||||
if (selection) {
|
||||
onEvent('onSelectionChange', { component });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="row" style={{ width, height }} onClick={() => onComponentClick(id, component)}>
|
||||
<span className="form-check-label form-check-label col-auto py-1" style={{color: textColor}}>{label}</span>
|
||||
<div className="col py-1" onChange={(e) => onSelect(e)}>
|
||||
{selectOptions.map((option, index) => (
|
||||
<label key={index} class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" value={option.value} name="radio-options" />
|
||||
<span className="form-check-label" style={{color: textColor}}>{option.name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
|||
|
||||
export const Radio = ({ options, value, onChange, readOnly }) => {
|
||||
|
||||
value = value || [];
|
||||
value = value === undefined ? [] : value;
|
||||
|
||||
return (
|
||||
<div className="radio row">
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { Pagination } from './Pagination';
|
|||
import { CustomSelect } from './CustomSelect';
|
||||
import { Tags } from './Tags';
|
||||
import { Radio } from './Radio';
|
||||
import { Toggle } from './Toggle'
|
||||
|
||||
var _ = require('lodash');
|
||||
|
||||
|
|
@ -43,6 +44,9 @@ export function Table({
|
|||
const serverSideSearchProperty = component.definition.properties.serverSideSearch;
|
||||
const serverSideSearch = serverSideSearchProperty ? serverSideSearchProperty.value : false;
|
||||
|
||||
const displaySearchBoxProperty = component.definition.properties.displaySearchBox;
|
||||
const displaySearchBox = displaySearchBoxProperty ? displaySearchBoxProperty.value : true;
|
||||
|
||||
const [loadingState, setLoadingState] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -226,7 +230,7 @@ export function Table({
|
|||
const changeSet = componentState ? componentState.changeSet : {};
|
||||
|
||||
const columnData = component.definition.properties.columns.value.map((column) => {
|
||||
const columnSize = columnSizes[column.key] || columnSizes[column.name];
|
||||
const columnSize = columnSizes[column.id] || columnSizes[column.name];
|
||||
const columnType = column.columnType;
|
||||
|
||||
const columnOptions = {};
|
||||
|
|
@ -244,6 +248,7 @@ export function Table({
|
|||
const width = columnSize || defaultColumn.width;
|
||||
|
||||
return {
|
||||
id: column.id,
|
||||
Header: column.name,
|
||||
accessor: column.key || column.name,
|
||||
filter: customFilter,
|
||||
|
|
@ -363,6 +368,18 @@ export function Table({
|
|||
onChange={(value) => {
|
||||
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} if (columnType === 'toggle') {
|
||||
return (
|
||||
<div>
|
||||
<Toggle
|
||||
value={cellValue}
|
||||
readOnly={!column.isEditable}
|
||||
onChange={(value) => {
|
||||
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -517,34 +534,37 @@ export function Table({
|
|||
style={{ width: `${width}px`, height: `${height}px` }}
|
||||
onClick={() => onComponentClick(id, component)}
|
||||
>
|
||||
<div className="card-body border-bottom py-3 jet-data-table-header">
|
||||
<div className="d-flex">
|
||||
{!serverSidePagination &&
|
||||
<div className="text-muted">
|
||||
Show
|
||||
<div className="mx-2 d-inline-block">
|
||||
<select
|
||||
value={pageSize}
|
||||
className="form-control form-control-sm"
|
||||
onChange={(e) => {
|
||||
setPageSize(Number(e.target.value));
|
||||
}}
|
||||
>
|
||||
{[10, 20, 30, 40, 50].map((itemsCount) => (
|
||||
<option key={itemsCount} value={itemsCount}>
|
||||
{itemsCount}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{/* Show top bar unless search box is disabled and server pagination is enabled */}
|
||||
{(!(!displaySearchBox && serverSidePagination) &&
|
||||
<div className="card-body border-bottom py-3 jet-data-table-header">
|
||||
<div className="d-flex">
|
||||
{!serverSidePagination &&
|
||||
<div className="text-muted">
|
||||
Show
|
||||
<div className="mx-2 d-inline-block">
|
||||
<select
|
||||
value={pageSize}
|
||||
className="form-control form-control-sm"
|
||||
onChange={(e) => {
|
||||
setPageSize(Number(e.target.value));
|
||||
}}
|
||||
>
|
||||
{[10, 20, 30, 40, 50].map((itemsCount) => (
|
||||
<option key={itemsCount} value={itemsCount}>
|
||||
{itemsCount}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
entries
|
||||
</div>
|
||||
entries
|
||||
</div>
|
||||
}
|
||||
<div className="ms-auto text-muted">
|
||||
<GlobalFilter />
|
||||
}
|
||||
{displaySearchBox && <div className="ms-auto text-muted">
|
||||
<GlobalFilter />
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="table-responsive jet-data-table">
|
||||
<table {...getTableProps()} className="table table-vcenter table-nowrap table-bordered" style={computedStyles}>
|
||||
<thead>
|
||||
|
|
@ -590,7 +610,8 @@ export function Table({
|
|||
|
||||
if (componentState.changeSet) {
|
||||
if (componentState.changeSet[cell.row.index]) {
|
||||
if (_.get(componentState.changeSet[cell.row.index], cell.column.id, undefined)) {
|
||||
|
||||
if (_.get(componentState.changeSet[cell.row.index], cell.column.Header, undefined) !== undefined) {
|
||||
console.log('componentState.changeSet', componentState.changeSet);
|
||||
cellProps.style.backgroundColor = '#ffffde';
|
||||
}
|
||||
|
|
@ -680,7 +701,7 @@ export function Table({
|
|||
<div className="col">
|
||||
<SelectSearch
|
||||
options={columnData.map((column) => {
|
||||
return { name: column.Header, value: column.accessor };
|
||||
return { name: column.Header, value: column.id };
|
||||
})}
|
||||
value={filter.id}
|
||||
search={true}
|
||||
|
|
|
|||
25
frontend/src/Editor/Components/Table/Toggle.jsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
export const Toggle = ({readOnly, value, onChange, options }) => {
|
||||
const [on, setOn] = useState(() => value)
|
||||
|
||||
const toggle = () => {
|
||||
setOn((prev) => !prev)
|
||||
onChange(!on)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="radio row">
|
||||
<div>
|
||||
<label className="form-check form-switch form-check-inline">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
checked={on}
|
||||
onClick={() => {if(!readOnly) toggle()}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
57
frontend/src/Editor/Components/Toggle.jsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import React from 'react';
|
||||
|
||||
class Switch extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
on,
|
||||
onClick,
|
||||
onChange
|
||||
} = this.props
|
||||
return (
|
||||
<label className="form-check form-switch form-check-inline">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
checked={on}
|
||||
onChange={onChange}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const ToggleSwitch = ({
|
||||
id,
|
||||
width,
|
||||
height,
|
||||
component,
|
||||
onComponentClick,
|
||||
currentState,
|
||||
onComponentOptionChanged,
|
||||
onEvent
|
||||
}) => {
|
||||
|
||||
const [on, setOn] = React.useState(false)
|
||||
const label = component.definition.properties.label.value;
|
||||
const textColorProperty = component.definition.styles.textColor;
|
||||
const textColor = textColorProperty ? textColorProperty.value : '#000';
|
||||
|
||||
function toggleValue(e) {
|
||||
const toggled = e.target.checked;
|
||||
onComponentOptionChanged(component, 'value', toggled);
|
||||
onEvent('onChange', { component });
|
||||
|
||||
}
|
||||
|
||||
const toggle = () => setOn(!on)
|
||||
|
||||
return (
|
||||
<div className="row py-1" style={{ width, height }} onClick={() => onComponentClick(id, component)}>
|
||||
<span className="form-check-label form-check-label col-auto" style={{color: textColor}}>{label}</span>
|
||||
<div className="col">
|
||||
<Switch on={on} onClick={toggle} onChange={toggleValue} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -12,7 +12,8 @@ export const componentTypes = [
|
|||
serverSidePagination: { type: 'toggle', displayName: 'Server-side pagination'},
|
||||
serverSideSearch: { type: 'toggle', displayName: 'Server-side search'},
|
||||
actionButtonBackgroundColor: { type: 'color', displayName: 'Background color'},
|
||||
actionButtonTextColor: { type: 'color', displayName: 'Text color'}
|
||||
actionButtonTextColor: { type: 'color', displayName: 'Text color'},
|
||||
displaySearchBox: { type: 'toggle', displayName: 'Display search box' }
|
||||
},
|
||||
others: {
|
||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? '},
|
||||
|
|
@ -47,13 +48,14 @@ export const componentTypes = [
|
|||
title: { value: 'Table' },
|
||||
visible: { value: true },
|
||||
loadingState: { value: false },
|
||||
data: { value: '{{[]}}' },
|
||||
data: { value: "{{ [ \n\t\t{ id: 1, name: 'Sarah', email: 'sarah@example.com'}, \n\t\t{ id: 2, name: 'Lisa', email: 'lisa@example.com'}, \n\t\t{ id: 3, name: 'Sam', email: 'sam@example.com'}, \n\t\t{ id: 4, name: 'Jon', email: 'jon@example.com'} \n] }}" },
|
||||
serverSidePagination: { value: false },
|
||||
displaySearchBox: { value: true },
|
||||
columns: {
|
||||
value: [
|
||||
{ name: 'id' },
|
||||
{ name: 'name' },
|
||||
{ name: 'email' }
|
||||
{ name: 'id', id: "e3ecbf7fa52c4d7210a93edb8f43776267a489bad52bd108be9588f790126737"},
|
||||
{ name: 'name', id: "5d2a3744a006388aadd012fcc15cc0dbcb5f9130e0fbb64c558561c97118754a"},
|
||||
{ name: 'email', id: "afc9a5091750a1bd4760e38760de3b4be11a43452ae8ae07ce2eebc569fe9a7f"}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -255,7 +257,7 @@ export const componentTypes = [
|
|||
|
||||
},
|
||||
exposedVariables: {
|
||||
value: {}
|
||||
value: ''
|
||||
},
|
||||
definition: {
|
||||
others: {
|
||||
|
|
@ -365,6 +367,104 @@ export const componentTypes = [
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Radio-button',
|
||||
displayName: 'Radio Button',
|
||||
description: 'Radio buttons',
|
||||
component: 'RadioButton',
|
||||
defaultSize: {
|
||||
width: 200,
|
||||
height: 30
|
||||
},
|
||||
others: {
|
||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? '},
|
||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?'},
|
||||
},
|
||||
properties: {
|
||||
label: { type: 'code', displayName: 'Label' },
|
||||
value: { type: 'code', displayName: 'Default value' },
|
||||
values: { type: 'code', displayName: 'Option values' },
|
||||
display_values: { type: 'code', displayName: 'Option labels' }
|
||||
},
|
||||
events: {
|
||||
onSelectionChange: { displayName: 'On select'},
|
||||
},
|
||||
styles: {
|
||||
textColor: { type: 'color', displayName: 'Text Color' }
|
||||
},
|
||||
exposedVariables: {},
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: true },
|
||||
showOnMobile: { value : false }
|
||||
},
|
||||
properties: {
|
||||
label: { value: 'Select' },
|
||||
value: { value: '' },
|
||||
values: { value: '{{[true,false]}}' },
|
||||
display_values: { value: '{{["yes", "no"]}}' },
|
||||
visible: { value: true }
|
||||
},
|
||||
events: {
|
||||
onSelectionChange: {
|
||||
options: {
|
||||
|
||||
}
|
||||
},
|
||||
},
|
||||
styles: {
|
||||
textColor: { value: '#000' }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'ToggleSwitch',
|
||||
displayName: 'Toggle Switch',
|
||||
description: 'Toggle Switch',
|
||||
component: 'ToggleSwitch',
|
||||
defaultSize: {
|
||||
width: 130,
|
||||
height: 30
|
||||
},
|
||||
others: {
|
||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? '},
|
||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?'},
|
||||
},
|
||||
properties: {
|
||||
label: { type: 'code', displayName: 'Label' }
|
||||
},
|
||||
events: {
|
||||
onChange: { displayName: 'On change'},
|
||||
},
|
||||
styles: {
|
||||
textColor: { type: 'color', displayName: 'Text Color' }
|
||||
},
|
||||
exposedVariables: {},
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: true },
|
||||
showOnMobile: { value : false }
|
||||
},
|
||||
properties: {
|
||||
label: { value: 'Toggle label' }
|
||||
},
|
||||
events: {
|
||||
onToggle: {
|
||||
options: {
|
||||
|
||||
}
|
||||
},
|
||||
onUnToggle: {
|
||||
options: {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
styles: {
|
||||
textColor: { value: '#000' }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Textarea',
|
||||
displayName: 'Textarea',
|
||||
|
|
@ -616,7 +716,7 @@ export const componentTypes = [
|
|||
description: 'Select multiple values from options',
|
||||
defaultSize: {
|
||||
width: 200,
|
||||
height: 60
|
||||
height: 37
|
||||
},
|
||||
component: 'Multiselect',
|
||||
others: {
|
||||
|
|
@ -759,4 +859,41 @@ defaultMarkers: { value: `[{
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'QrScanner',
|
||||
displayName: 'QR Scanner',
|
||||
description: 'Scan QR codes and hold its data',
|
||||
component: 'QrScanner',
|
||||
defaultSize: {
|
||||
width: 300,
|
||||
height: 300
|
||||
},
|
||||
others: {
|
||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop? '},
|
||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile?'},
|
||||
},
|
||||
properties: {},
|
||||
events: {
|
||||
onDetect: { displayName: 'On detect'},
|
||||
},
|
||||
styles: {
|
||||
|
||||
},
|
||||
exposedVariables: {
|
||||
lastDetectedValue: ''
|
||||
},
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: true },
|
||||
showOnMobile: { value : true },
|
||||
},
|
||||
properties: {},
|
||||
events: {
|
||||
onDetect: {
|
||||
}
|
||||
},
|
||||
styles: {
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
|||
|
|
@ -304,9 +304,8 @@ export const Container = ({
|
|||
}
|
||||
}
|
||||
)}
|
||||
|
||||
{Object.keys(boxes).length === 0 && !appLoading && !isDragging && (
|
||||
<div className="mx-auto w-50 p-5 bg-light no-components-box" style={{ marginTop: '15%' }}>
|
||||
<div className="mx-auto w-50 p-5 bg-light no-components-box" style={{ marginTop: '10%'}}>
|
||||
<center className="text-muted">You haven't added any components yet. Drag components from the right sidebar and drop here.</center>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,16 @@ class DataSourceManager extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.selectedDataSource != this.props.selectedDataSource) {
|
||||
this.setState({
|
||||
selectedDataSource: this.props.selectedDataSource,
|
||||
options: this.props.selectedDataSource?.options,
|
||||
dataSourceMeta: DataSourceTypes.find((source) => source.kind === this.props.selectedDataSource?.kind),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
selectDataSource = (source) => {
|
||||
this.setState({
|
||||
dataSourceMeta: source,
|
||||
|
|
@ -59,6 +69,14 @@ class DataSourceManager extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
onExit = () => {
|
||||
this.setState({
|
||||
dataSourceMeta: {},
|
||||
selectedDataSource: null,
|
||||
options: {},
|
||||
});
|
||||
};
|
||||
|
||||
setStateAsync = (state) => {
|
||||
return new Promise((resolve) => {
|
||||
this.setState(state, resolve);
|
||||
|
|
@ -76,6 +94,7 @@ class DataSourceManager extends React.Component {
|
|||
};
|
||||
|
||||
hideModal = () => {
|
||||
this.onExit();
|
||||
this.props.hideModal();
|
||||
};
|
||||
|
||||
|
|
@ -131,12 +150,10 @@ class DataSourceManager extends React.Component {
|
|||
|
||||
onConnectionTestFailed = (data) => {
|
||||
this.setState({ connectionTestError: data });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
dataSourceMeta, selectedDataSource, options, isSaving, connectionTestError
|
||||
} = this.state;
|
||||
const { dataSourceMeta, selectedDataSource, options, isSaving, connectionTestError } = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -147,6 +164,7 @@ class DataSourceManager extends React.Component {
|
|||
className="mt-5"
|
||||
contentClassName={this.props.darkMode ? 'theme-dark' : ''}
|
||||
animation={false}
|
||||
onExit={this.onExit}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>
|
||||
|
|
@ -207,7 +225,7 @@ class DataSourceManager extends React.Component {
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="row row-deck mt-5">
|
||||
<div className="row row-deck mt-2">
|
||||
<h4 className="text-muted mb-2">APIS</h4>
|
||||
{apiSources.map((dataSource) => (
|
||||
<div className="col-md-2" key={dataSource.name}>
|
||||
|
|
@ -240,33 +258,38 @@ class DataSourceManager extends React.Component {
|
|||
<Modal.Footer>
|
||||
<div className="row w-100">
|
||||
<div className="alert alert-info" role="alert">
|
||||
<div className="text-muted">
|
||||
Please white-list our IP address if your datasource is not publicly accessible.
|
||||
IP: <span className="px-2 py-1">{config.SERVER_IP}</span>
|
||||
<CopyToClipboard
|
||||
text={config.SERVER_IP}
|
||||
onCopy={() => toast.success('IP copied to clipboard', {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center'
|
||||
})
|
||||
}
|
||||
>
|
||||
<img src="/assets/images/icons/copy.svg" className="mx-1 svg-icon" width="14" height="14" role="button"/>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{connectionTestError &&
|
||||
<div className="row w-100">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<div className="text-muted">
|
||||
{connectionTestError.message}
|
||||
Please white-list our IP address if your datasource is not publicly accessible. IP:{' '}
|
||||
<span className="px-2 py-1">{config.SERVER_IP}</span>
|
||||
<CopyToClipboard
|
||||
text={config.SERVER_IP}
|
||||
onCopy={() =>
|
||||
toast.success('IP copied to clipboard', {
|
||||
hideProgressBar: true,
|
||||
position: 'top-center',
|
||||
})
|
||||
}
|
||||
>
|
||||
<img
|
||||
src="/assets/images/icons/copy.svg"
|
||||
className="mx-1 svg-icon"
|
||||
width="14"
|
||||
height="14"
|
||||
role="button"
|
||||
/>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{connectionTestError && (
|
||||
<div className="row w-100">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<div className="text-muted">{connectionTestError.message}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
)}
|
||||
|
||||
<div className="col">
|
||||
<small>
|
||||
<a href={`https://docs.tooljet.io/docs/data-sources/${selectedDataSource.kind}`} target="_blank">
|
||||
|
|
@ -275,9 +298,9 @@ class DataSourceManager extends React.Component {
|
|||
</small>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<TestConnection
|
||||
kind={selectedDataSource.kind}
|
||||
options={options}
|
||||
<TestConnection
|
||||
kind={selectedDataSource.kind}
|
||||
options={options}
|
||||
onConnectionTestFailed={this.onConnectionTestFailed}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -289,7 +312,7 @@ class DataSourceManager extends React.Component {
|
|||
onClick={this.createDataSource}
|
||||
>
|
||||
{'Save'}
|
||||
</Button>
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
datasourceService, dataqueryService, appService, authenticationService
|
||||
} from '@/_services';
|
||||
import { DarkModeToggle } from '@/_components/DarkModeToggle';
|
||||
import { datasourceService, dataqueryService, appService, authenticationService } from '@/_services';
|
||||
// import { DarkModeToggle } from '@/_components/DarkModeToggle';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { Container } from './Container';
|
||||
import { CustomDragLayer } from './CustomDragLayer';
|
||||
import { LeftSidebar } from './LeftSidebar';
|
||||
import { componentTypes } from './Components/components';
|
||||
import { Inspector } from './Inspector/Inspector';
|
||||
import ReactJson from 'react-json-view';
|
||||
import { DataSourceManager } from './DataSourceManager';
|
||||
// import ReactJson from 'react-json-view';
|
||||
// import { DataSourceManager } from './DataSourceManager';
|
||||
import { DataSourceTypes } from './DataSourceManager/DataSourceTypes';
|
||||
import { QueryManager } from './QueryManager';
|
||||
import { toast } from 'react-toastify';
|
||||
|
|
@ -25,11 +24,11 @@ import {
|
|||
onQueryConfirm,
|
||||
onQueryCancel,
|
||||
runQuery,
|
||||
setStateAsync
|
||||
setStateAsync,
|
||||
} from '@/_helpers/appUtils';
|
||||
import { Confirm } from './Viewer/Confirm';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { Resizable } from 're-resizable';
|
||||
// import { Resizable } from 're-resizable';
|
||||
import { WidgetManager } from './WidgetManager';
|
||||
import Fuse from 'fuse.js';
|
||||
import queryString from 'query-string';
|
||||
|
|
@ -47,7 +46,7 @@ class Editor extends React.Component {
|
|||
userVars = {
|
||||
email: currentUser.email,
|
||||
firstName: currentUser.first_name,
|
||||
lastName: currentUser.last_name
|
||||
lastName: currentUser.last_name,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -68,19 +67,19 @@ class Editor extends React.Component {
|
|||
scaleValue: 1,
|
||||
deviceWindowWidth: 450,
|
||||
appDefinition: {
|
||||
components: null
|
||||
components: null,
|
||||
},
|
||||
currentState: {
|
||||
queries: {},
|
||||
components: {},
|
||||
globals: {
|
||||
currentUser: userVars,
|
||||
urlparams: JSON.parse(JSON.stringify(queryString.parse(props.location.search)))
|
||||
}
|
||||
urlparams: JSON.parse(JSON.stringify(queryString.parse(props.location.search))),
|
||||
},
|
||||
},
|
||||
apps: [],
|
||||
dataQueriesDefaultText: 'You haven\'t created queries yet.',
|
||||
showQuerySearchField: false
|
||||
dataQueriesDefaultText: "You haven't created queries yet.",
|
||||
showQuerySearchField: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -88,41 +87,45 @@ class Editor extends React.Component {
|
|||
const appId = this.props.match.params.id;
|
||||
this.fetchApps(0);
|
||||
|
||||
appService.getApp(appId).then((data) => this.setState(
|
||||
{
|
||||
app: data,
|
||||
isLoading: false,
|
||||
appDefinition: { ...this.state.appDefinition, ...data.definition },
|
||||
slug: data.slug
|
||||
},
|
||||
() => {
|
||||
data.data_queries.forEach((query) => {
|
||||
if (query.options.runOnPageLoad) {
|
||||
runQuery(this, query.id, query.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
));
|
||||
appService.getApp(appId).then((data) =>
|
||||
this.setState(
|
||||
{
|
||||
app: data,
|
||||
isLoading: false,
|
||||
appDefinition: { ...this.state.appDefinition, ...data.definition },
|
||||
slug: data.slug,
|
||||
},
|
||||
() => {
|
||||
data.data_queries.forEach((query) => {
|
||||
if (query.options.runOnPageLoad) {
|
||||
runQuery(this, query.id, query.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.fetchDataSources();
|
||||
this.fetchDataQueries();
|
||||
|
||||
this.setState({
|
||||
currentSidebarTab: 2,
|
||||
selectedComponent: null
|
||||
selectedComponent: null,
|
||||
});
|
||||
}
|
||||
|
||||
fetchDataSources = () => {
|
||||
this.setState(
|
||||
{
|
||||
loadingDataSources: true
|
||||
loadingDataSources: true,
|
||||
},
|
||||
() => {
|
||||
datasourceService.getAll(this.state.appId).then((data) => this.setState({
|
||||
dataSources: data.data_sources,
|
||||
loadingDataSources: false
|
||||
}));
|
||||
datasourceService.getAll(this.state.appId).then((data) =>
|
||||
this.setState({
|
||||
dataSources: data.data_sources,
|
||||
loadingDataSources: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
@ -130,7 +133,7 @@ class Editor extends React.Component {
|
|||
fetchDataQueries = () => {
|
||||
this.setState(
|
||||
{
|
||||
loadingDataQueries: true
|
||||
loadingDataQueries: true,
|
||||
},
|
||||
() => {
|
||||
dataqueryService.getAll(this.state.appId).then((data) => {
|
||||
|
|
@ -140,15 +143,15 @@ class Editor extends React.Component {
|
|||
loadingDataQueries: false,
|
||||
app: {
|
||||
...this.state.app,
|
||||
data_queries: data.data_queries
|
||||
}
|
||||
data_queries: data.data_queries,
|
||||
},
|
||||
},
|
||||
() => {
|
||||
let queryState = {};
|
||||
data.data_queries.forEach((query) => {
|
||||
queryState[query.name] = {
|
||||
...DataSourceTypes.find((source) => source.kind === query.kind).exposedVariables,
|
||||
...this.state.currentState.queries[query.name]
|
||||
...this.state.currentState.queries[query.name],
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -170,9 +173,9 @@ class Editor extends React.Component {
|
|||
currentState: {
|
||||
...this.state.currentState,
|
||||
queries: {
|
||||
...queryState
|
||||
}
|
||||
}
|
||||
...queryState,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
@ -182,11 +185,13 @@ class Editor extends React.Component {
|
|||
};
|
||||
|
||||
fetchApps = (page) => {
|
||||
appService.getAll(page).then((data) => this.setState({
|
||||
apps: data.apps,
|
||||
isLoading: false
|
||||
}));
|
||||
}
|
||||
appService.getAll(page).then((data) =>
|
||||
this.setState({
|
||||
apps: data.apps,
|
||||
isLoading: false,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
computeComponentState = (components) => {
|
||||
let componentState = {};
|
||||
|
|
@ -205,9 +210,9 @@ class Editor extends React.Component {
|
|||
currentState: {
|
||||
...this.state.currentState,
|
||||
components: {
|
||||
...componentState
|
||||
}
|
||||
}
|
||||
...componentState,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -221,12 +226,11 @@ class Editor extends React.Component {
|
|||
};
|
||||
|
||||
switchSidebarTab = (tabIndex) => {
|
||||
if (tabIndex == 2)
|
||||
{
|
||||
if (tabIndex == 2) {
|
||||
this.setState({ selectedComponent: null });
|
||||
}
|
||||
this.setState({
|
||||
currentSidebarTab: tabIndex
|
||||
currentSidebarTab: tabIndex,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -280,6 +284,7 @@ class Editor extends React.Component {
|
|||
|
||||
componentDefinitionChanged = (newDefinition) => {
|
||||
let _self = this;
|
||||
|
||||
return setStateAsync(_self, {
|
||||
appDefinition: {
|
||||
...this.state.appDefinition,
|
||||
|
|
@ -287,11 +292,10 @@ class Editor extends React.Component {
|
|||
...this.state.appDefinition.components,
|
||||
[newDefinition.id]: {
|
||||
...this.state.appDefinition.components[newDefinition.id],
|
||||
component: newDefinition.component,
|
||||
layouts: newDefinition.layouts
|
||||
}
|
||||
}
|
||||
}
|
||||
component: newDefinition.component
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -303,10 +307,10 @@ class Editor extends React.Component {
|
|||
...this.state.appDefinition.components,
|
||||
[newComponent.id]: {
|
||||
...this.state.appDefinition.components[newComponent.id],
|
||||
...newComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
...newComponent,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -375,7 +379,7 @@ class Editor extends React.Component {
|
|||
runQuery(this, dataQuery.id, dataQuery.name).then(() => {
|
||||
toast.info(`Query (${dataQuery.name}) completed.`, {
|
||||
hideProgressBar: true,
|
||||
position: 'bottom-center'
|
||||
position: 'bottom-center',
|
||||
});
|
||||
});
|
||||
}}
|
||||
|
|
@ -397,13 +401,13 @@ class Editor extends React.Component {
|
|||
|
||||
onNameChanged = (newName) => {
|
||||
this.setState({
|
||||
app: { ...this.state.app, name: newName }
|
||||
app: { ...this.state.app, name: newName },
|
||||
});
|
||||
};
|
||||
|
||||
toggleQueryPaneHeight = () => {
|
||||
this.setState({
|
||||
queryPaneHeight: this.state.queryPaneHeight === '30%' ? '80%' : '30%'
|
||||
queryPaneHeight: this.state.queryPaneHeight === '30%' ? '80%' : '30%',
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -426,23 +430,31 @@ class Editor extends React.Component {
|
|||
const results = fuse.search(value);
|
||||
this.setState({
|
||||
dataQueries: results.map((result) => result.item),
|
||||
dataQueriesDefaultText: results.length || 'No Queries found.'
|
||||
dataQueriesDefaultText: results.length || 'No Queries found.',
|
||||
});
|
||||
} else {
|
||||
this.fetchDataQueries();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
toggleQuerySearch = () => {
|
||||
this.setState({ showQuerySearchField: !this.state.showQuerySearchField });
|
||||
}
|
||||
};
|
||||
|
||||
onVersionDeploy = (versionId) => {
|
||||
this.setState({ app: {
|
||||
...this.state.app,
|
||||
current_version_id: versionId
|
||||
}})
|
||||
}
|
||||
this.setState({
|
||||
app: {
|
||||
...this.state.app,
|
||||
current_version_id: versionId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onZoomChanged = (zoom) => {
|
||||
this.setState({
|
||||
zoomLevel: zoom,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
|
|
@ -471,7 +483,7 @@ class Editor extends React.Component {
|
|||
scaleValue,
|
||||
dataQueriesDefaultText,
|
||||
showQuerySearchField,
|
||||
apps
|
||||
apps,
|
||||
} = this.state;
|
||||
const appLink = slug ? `/applications/${slug}` : '';
|
||||
|
||||
|
|
@ -516,13 +528,6 @@ class Editor extends React.Component {
|
|||
/>
|
||||
)}
|
||||
<div className="editor-buttons">
|
||||
<span
|
||||
className={`btn ${showLeftSidebar ? 'btn-light' : 'btn-default'} mx-2`}
|
||||
onClick={this.toggleLeftSidebar}
|
||||
data-tip={showLeftSidebar ? 'Hide left sidebar' : 'Show left sidebar'}
|
||||
>
|
||||
<img src="/assets/images/icons/editor/sidebar-toggle.svg" width="12" height="12" />
|
||||
</span>
|
||||
<span
|
||||
className={`btn ${showQueryEditor ? 'btn-light' : 'btn-default'} mx-2`}
|
||||
onClick={this.toggleQueryEditor}
|
||||
|
|
@ -536,7 +541,7 @@ class Editor extends React.Component {
|
|||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="canvas-buttons">
|
||||
{/* <div className="canvas-buttons">
|
||||
<button
|
||||
className="btn btn-light mx-2"
|
||||
onClick={() => this.setState({ zoomLevel: ((Math.round(zoomLevel*10) - 1)/10).toFixed(1) }) }
|
||||
|
|
@ -554,7 +559,7 @@ class Editor extends React.Component {
|
|||
>
|
||||
<img src="/assets/images/icons/zoom-in.svg" width="12" height="12" />
|
||||
</button>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="layout-buttons">
|
||||
<div className="btn-group" role="group" aria-label="Basic example">
|
||||
<button
|
||||
|
|
@ -576,35 +581,42 @@ class Editor extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
<div className="navbar-nav flex-row order-md-last">
|
||||
<div className="mx-3" style={{ marginTop: '7px'}}>
|
||||
{/* <div className="mx-3" style={{ marginTop: '7px'}}>
|
||||
<DarkModeToggle
|
||||
switchDarkMode={this.props.switchDarkMode}
|
||||
darkMode={this.props.darkMode}
|
||||
/>
|
||||
</div> */}
|
||||
<div className="nav-item dropdown d-none d-md-flex me-3">
|
||||
{app.id && (
|
||||
<ManageAppUsers
|
||||
app={app}
|
||||
slug={slug}
|
||||
darkMode={this.props.darkMode}
|
||||
handleSlugChange={this.handleSlugChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="nav-item dropdown d-none d-md-flex me-3">
|
||||
{app.id
|
||||
&& <ManageAppUsers
|
||||
app={app}
|
||||
slug={slug}
|
||||
darkMode={this.props.darkMode}
|
||||
handleSlugChange={this.handleSlugChange} />}
|
||||
</div>
|
||||
<div className="nav-item dropdown d-none d-md-flex me-3">
|
||||
<a href={appLink} target="_blank" className={`btn btn-sm ${app?.current_version_id ? '': 'disabled'}`} rel="noreferrer">
|
||||
<a
|
||||
href={appLink}
|
||||
target="_blank"
|
||||
className={`btn btn-sm ${app?.current_version_id ? '' : 'disabled'}`}
|
||||
rel="noreferrer"
|
||||
>
|
||||
Launch
|
||||
</a>
|
||||
</div>
|
||||
<div className="nav-item dropdown me-2">
|
||||
{app.id && (
|
||||
<SaveAndPreview
|
||||
appId={app.id}
|
||||
appName={app.name}
|
||||
appDefinition={appDefinition}
|
||||
app={app}
|
||||
darkMode={this.props.darkMode}
|
||||
onVersionDeploy={this.onVersionDeploy}
|
||||
/>
|
||||
<SaveAndPreview
|
||||
appId={app.id}
|
||||
appName={app.name}
|
||||
appDefinition={appDefinition}
|
||||
app={app}
|
||||
darkMode={this.props.darkMode}
|
||||
onVersionDeploy={this.onVersionDeploy}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -612,7 +624,18 @@ class Editor extends React.Component {
|
|||
</header>
|
||||
</div>
|
||||
<div className="sub-section">
|
||||
<Resizable
|
||||
<LeftSidebar
|
||||
queries={currentState.queries}
|
||||
components={currentState.components}
|
||||
globals={currentState.globals}
|
||||
appId={appId}
|
||||
darkMode={this.props.darkMode}
|
||||
dataSources={this.state.dataSources}
|
||||
dataSourcesChanged={this.dataSourcesChanged}
|
||||
onZoomChanged={this.onZoomChanged}
|
||||
switchDarkMode={this.props.switchDarkMode}
|
||||
/>
|
||||
{/* <Resizable
|
||||
minWidth={showLeftSidebar ? '12%' : '0%'}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
|
|
@ -733,9 +756,12 @@ class Editor extends React.Component {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Resizable>
|
||||
</Resizable> */}
|
||||
<div className="main">
|
||||
<div className="canvas-container align-items-center" style={{ transform: `scale(${zoomLevel})` }}>
|
||||
<div
|
||||
className={`canvas-container align-items-center ${!showLeftSidebar && 'hide-sidebar'}`}
|
||||
style={{ transform: `scale(${zoomLevel})` }}
|
||||
>
|
||||
<div className="canvas-area" style={{ width: currentLayout === 'desktop' ? '1292px' : '450px' }}>
|
||||
<Container
|
||||
appDefinition={appDefinition}
|
||||
|
|
@ -750,16 +776,18 @@ class Editor extends React.Component {
|
|||
scaleValue={scaleValue}
|
||||
appLoading={isLoading}
|
||||
onEvent={(eventName, options) => onEvent(this, eventName, options)}
|
||||
onComponentOptionChanged={(component, optionName, value) => onComponentOptionChanged(this, component, optionName, value)
|
||||
onComponentOptionChanged={(component, optionName, value) =>
|
||||
onComponentOptionChanged(this, component, optionName, value)
|
||||
}
|
||||
onComponentOptionsChanged={(component, options) => onComponentOptionsChanged(this, component, options)
|
||||
onComponentOptionsChanged={(component, options) =>
|
||||
onComponentOptionsChanged(this, component, options)
|
||||
}
|
||||
currentState={this.state.currentState}
|
||||
configHandleClicked={this.configHandleClicked}
|
||||
removeComponent={this.removeComponent}
|
||||
onComponentClick={(id, component) => {
|
||||
// this.setState({ selectedComponent: { id, component } });
|
||||
// this.switchSidebarTab(1);
|
||||
this.setState({ selectedComponent: { id, component } });
|
||||
this.switchSidebarTab(1);
|
||||
onComponentClick(this, id, component);
|
||||
}}
|
||||
/>
|
||||
|
|
@ -771,7 +799,7 @@ class Editor extends React.Component {
|
|||
style={{
|
||||
height: showQueryEditor ? this.state.queryPaneHeight : '0px',
|
||||
width: !showLeftSidebar ? '85%' : '',
|
||||
left: !showLeftSidebar ? '0' : ''
|
||||
left: !showLeftSidebar ? '0' : '',
|
||||
}}
|
||||
>
|
||||
<div className="row main-row">
|
||||
|
|
@ -783,7 +811,7 @@ class Editor extends React.Component {
|
|||
</div>
|
||||
<div className="col-auto px-3">
|
||||
<button className="btn btn-sm btn-light mx-2" onClick={this.toggleQuerySearch}>
|
||||
<img className="py-1" src="/assets/images/icons/lens.svg" width="17" height="17"/>
|
||||
<img className="py-1" src="/assets/images/icons/lens.svg" width="17" height="17" />
|
||||
</button>
|
||||
|
||||
<span
|
||||
|
|
@ -796,8 +824,8 @@ class Editor extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{showQuerySearchField
|
||||
&& <div className="row mt-2 pt-1 px-2">
|
||||
{showQuerySearchField && (
|
||||
<div className="row mt-2 pt-1 px-2">
|
||||
<div className="col-12">
|
||||
<div className="queries-search">
|
||||
<input
|
||||
|
|
@ -810,7 +838,7 @@ class Editor extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
{loadingDataQueries ? (
|
||||
<div className="p-5">
|
||||
|
|
@ -827,7 +855,8 @@ class Editor extends React.Component {
|
|||
<span className="text-muted">{dataQueriesDefaultText}</span> <br />
|
||||
<button
|
||||
className="btn btn-sm btn-outline-azure mt-3"
|
||||
onClick={() => this.setState({ selectedQuery: {}, editingQuery: false, addingQuery: true })
|
||||
onClick={() =>
|
||||
this.setState({ selectedQuery: {}, editingQuery: false, addingQuery: true })
|
||||
}
|
||||
>
|
||||
create query
|
||||
|
|
@ -866,9 +895,7 @@ class Editor extends React.Component {
|
|||
</div>
|
||||
<div className="editor-sidebar">
|
||||
<div className="col-md-12">
|
||||
<div>
|
||||
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
{currentSidebarTab === 1 && (
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import Popover from 'react-bootstrap/Popover';
|
|||
import { EventSelector } from '../EventSelector';
|
||||
import { Color } from '../Elements/Color';
|
||||
import SelectSearch, { fuzzySearch } from 'react-select-search';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
class Table extends React.Component {
|
||||
constructor(props) {
|
||||
|
|
@ -102,7 +103,8 @@ class Table extends React.Component {
|
|||
{ name: 'Tags', value: 'tags' },
|
||||
{ name: 'Dropdown', value: 'dropdown' },
|
||||
{ name: 'Radio', value: 'radio' },
|
||||
{ name: 'Multiselect', value: 'multiselect' }
|
||||
{ name: 'Multiselect', value: 'multiselect' },
|
||||
{ name: 'Toggle switch', value: 'toggle' }
|
||||
]}
|
||||
value={column.columnType}
|
||||
search={true}
|
||||
|
|
@ -276,7 +278,7 @@ class Table extends React.Component {
|
|||
addNewColumn = () => {
|
||||
const columns = this.props.component.component.definition.properties.columns;
|
||||
const newValue = columns.value;
|
||||
newValue.push({ name: this.generateNewColumnName(columns.value) });
|
||||
newValue.push({ name: this.generateNewColumnName(columns.value), id: uuidv4() });
|
||||
this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties');
|
||||
};
|
||||
|
||||
|
|
@ -338,6 +340,10 @@ class Table extends React.Component {
|
|||
const columns = component.component.definition.properties.columns;
|
||||
const actions = component.component.definition.properties.actions || { value: [] };
|
||||
|
||||
if (!component.component.definition.properties.displaySearchBox)
|
||||
paramUpdated({ name: 'displaySearchBox' }, 'value', true, 'properties');
|
||||
const displaySearchBox = component.component.definition.properties.displaySearchBox.value;
|
||||
|
||||
return (
|
||||
<div className="properties-container p-2 " key={this.props.component.id}>
|
||||
{renderElement(component, componentMeta, paramUpdated, dataQueries, 'data', 'properties', currentState, components)}
|
||||
|
|
@ -403,7 +409,8 @@ class Table extends React.Component {
|
|||
<hr></hr>
|
||||
|
||||
{renderElement(component, componentMeta, paramUpdated, dataQueries, 'serverSidePagination', 'properties', currentState)}
|
||||
{renderElement(component, componentMeta, paramUpdated, dataQueries, 'serverSideSearch', 'properties', currentState)}
|
||||
{renderElement(component, componentMeta, paramUpdated, dataQueries, 'displaySearchBox', 'properties', currentState)}
|
||||
{displaySearchBox && renderElement(component, componentMeta, paramUpdated, dataQueries, 'serverSideSearch', 'properties', currentState)}
|
||||
|
||||
<div className="hr-text">Events</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export const Code = ({
|
|||
initialValue={initialValue}
|
||||
mode={options.mode}
|
||||
theme={darkMode? 'monokai' : options.theme}
|
||||
lineWrapping={true}
|
||||
className={options.className}
|
||||
onChange={(value) => handleCodeChanged(value)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export const EventSelector = ({
|
|||
apps.map((item) => {
|
||||
appsOptionsList.push({
|
||||
name: item.name,
|
||||
value: item.id
|
||||
value: item.slug
|
||||
})
|
||||
})
|
||||
return appsOptionsList;
|
||||
|
|
|
|||
29
frontend/src/Editor/LeftSidebar/index.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import '@/_styles/left-sidebar.scss';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { LeftSidebarItem } from './sidebar-item';
|
||||
import { LeftSidebarInspector } from './sidebar-inspector';
|
||||
import { LeftSidebarDataSources } from './sidebar-datasources';
|
||||
import { LeftSidebarZoom } from './sidebar-zoom';
|
||||
import { DarkModeToggle } from '../../_components/DarkModeToggle';
|
||||
import useRouter from '../../_hooks/use-router';
|
||||
|
||||
export const LeftSidebar = ({ appId, switchDarkMode, darkMode = false, globals, components, queries, onZoomChanged, dataSources = [], dataSourcesChanged }) => {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<div className='left-sidebar'>
|
||||
<LeftSidebarInspector darkMode={darkMode} globals={globals} components={components} queries={queries} />
|
||||
<LeftSidebarDataSources darkMode={darkMode} appId={appId} dataSources={dataSources} dataSourcesChanged={dataSourcesChanged} />
|
||||
{/* <LeftSidebarItem icon='debugger' className='left-sidebar-item' /> */}
|
||||
<LeftSidebarItem onClick={() => router.push('/')} tip='Back to home' icon='back' className='left-sidebar-item no-border' />
|
||||
<div className='left-sidebar-stack-bottom'>
|
||||
<LeftSidebarZoom onZoomChanged={onZoomChanged} />
|
||||
<div className='left-sidebar-item no-border'>
|
||||
<DarkModeToggle switchDarkMode={switchDarkMode} darkMode={darkMode} />
|
||||
</div>
|
||||
{/* <LeftSidebarItem icon='support' className='left-sidebar-item' /> */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
72
frontend/src/Editor/LeftSidebar/sidebar-datasources.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import React from 'react';
|
||||
import usePopover from '../../_hooks/use-popover';
|
||||
import { LeftSidebarItem } from './sidebar-item';
|
||||
import { DataSourceManager } from '../DataSourceManager';
|
||||
import { DataSourceTypes } from '../DataSourceManager/DataSourceTypes';
|
||||
|
||||
export const LeftSidebarDataSources = ({ appId, darkMode, dataSources= [], dataSourcesChanged }) => {
|
||||
const [open, trigger, content] = usePopover(false)
|
||||
const [showDataSourceManagerModal, toggleDataSourceManagerModal] = React.useState(false);
|
||||
const [selectedDataSource, setSelectedDataSource] = React.useState(null);
|
||||
|
||||
const renderDataSource = dataSource => {
|
||||
const sourceMeta = DataSourceTypes.find((source) => source.kind === dataSource.kind);
|
||||
return (
|
||||
<tr
|
||||
role="button"
|
||||
key={sourceMeta.kind.toLowerCase()}
|
||||
onClick={() => {
|
||||
setSelectedDataSource(dataSource)
|
||||
toggleDataSourceManagerModal(true)
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
<img
|
||||
src={`/assets/images/icons/editor/datasources/${sourceMeta.kind.toLowerCase()}.svg`}
|
||||
width="20"
|
||||
height="20"
|
||||
/>{' '}
|
||||
{dataSource.name}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<LeftSidebarItem tip='Add or edit datasources' {...trigger} icon='database' className='left-sidebar-item' />
|
||||
<div {...content} className={`card popover datasources-popover ${open ? 'show' : 'hide'}`}>
|
||||
<div className="card-body">
|
||||
<div className="table-responsive">
|
||||
<table className="table table-vcenter table-nowrap">
|
||||
<tbody>{dataSources?.map((source) => renderDataSource(source))}</tbody>
|
||||
</table>
|
||||
{dataSources?.length === 0 && (
|
||||
<center className="p-2 text-muted">
|
||||
You haven't added any datasources yet. <br />
|
||||
</center>
|
||||
)}
|
||||
<center>
|
||||
<button onClick={() => toggleDataSourceManagerModal(true)} className="btn btn-sm btn-outline-azure mt-3">
|
||||
Add datasource
|
||||
</button>
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DataSourceManager
|
||||
appId={appId}
|
||||
showDataSourceManagerModal={showDataSourceManagerModal}
|
||||
darkMode={darkMode}
|
||||
hideModal={() => {
|
||||
setSelectedDataSource(null)
|
||||
toggleDataSourceManagerModal(false)
|
||||
}
|
||||
}
|
||||
dataSourcesChanged={dataSourcesChanged}
|
||||
showDataSourceManagerModal={showDataSourceManagerModal}
|
||||
selectedDataSource={selectedDataSource}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
56
frontend/src/Editor/LeftSidebar/sidebar-inspector.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
import usePopover from '../../_hooks/use-popover';
|
||||
import { LeftSidebarItem } from './sidebar-item';
|
||||
import ReactJson from 'react-json-view';
|
||||
|
||||
export const LeftSidebarInspector = ({ darkMode, globals, components, queries }) => {
|
||||
const [open, trigger, content] = usePopover(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<LeftSidebarItem tip='Inspector' {...trigger} icon='inspector' className='left-sidebar-item' />
|
||||
<div {...content} className={`card popover ${open ? 'show' : 'hide'}`}>
|
||||
<div className="card-body">
|
||||
<ReactJson
|
||||
src={queries}
|
||||
theme={darkMode ? 'shapeshifter' : 'rjv-default'}
|
||||
name={'queries'}
|
||||
style={{ fontSize: '0.7rem' }}
|
||||
enableClipboard={false}
|
||||
displayDataTypes={false}
|
||||
collapsed={true}
|
||||
displayObjectSize={false}
|
||||
quotesOnKeys={false}
|
||||
sortKeys={true}
|
||||
/>
|
||||
<ReactJson
|
||||
src={components}
|
||||
theme={darkMode ? 'shapeshifter' : 'rjv-default'}
|
||||
name={'components'}
|
||||
style={{ fontSize: '0.7rem' }}
|
||||
enableClipboard={false}
|
||||
displayDataTypes={false}
|
||||
collapsed={true}
|
||||
displayObjectSize={false}
|
||||
quotesOnKeys={false}
|
||||
sortKeys={true}
|
||||
// indentWidth={0.5}
|
||||
/>
|
||||
<ReactJson
|
||||
style={{ fontSize: '0.7rem' }}
|
||||
theme={darkMode ? 'shapeshifter' : 'rjv-default'}
|
||||
enableClipboard={false}
|
||||
src={globals}
|
||||
name={'globals'}
|
||||
displayDataTypes={false}
|
||||
collapsed={true}
|
||||
displayObjectSize={false}
|
||||
quotesOnKeys={false}
|
||||
sortKeys={true}
|
||||
// indentWidth={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
22
frontend/src/Editor/LeftSidebar/sidebar-item.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import Tooltip from 'react-bootstrap/Tooltip';
|
||||
|
||||
export const LeftSidebarItem = ({ tip = '', className, icon, text, onClick, ...rest }) => {
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
trigger={['click','hover', 'focus']}
|
||||
placement="right"
|
||||
delay={{ show: 800, hide: 100 }}
|
||||
overlay={<Tooltip id="button-tooltip">
|
||||
{tip}
|
||||
</Tooltip>}
|
||||
>
|
||||
<div {...rest} className={className} onClick={onClick && onClick}>
|
||||
{icon && <img className="svg-icon" src={`/assets/images/icons/editor/left-sidebar/${icon}.svg`} width="20" height="20" />}
|
||||
{text && text}
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
)
|
||||
}
|
||||
83
frontend/src/Editor/LeftSidebar/sidebar-zoom.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import React from 'react';
|
||||
import usePopover from '../../_hooks/use-popover';
|
||||
import { LeftSidebarItem } from './sidebar-item';
|
||||
|
||||
export const LeftSidebarZoom = ({ onZoomChanged }) => {
|
||||
const [open, trigger, content, setOpen] = usePopover(false)
|
||||
const [text, setText] = React.useState(100)
|
||||
return (
|
||||
<>
|
||||
<LeftSidebarItem tip='Select zoom level' {...trigger} text={`${text} %`} className='left-sidebar-item sidebar-zoom' />
|
||||
<div {...content} className={`card popover zoom-popover ${open ? 'show' : 'hide'}`}>
|
||||
<div className="card-body">
|
||||
<div className="table-responsive">
|
||||
<table className="table table-vcenter table-nowrap">
|
||||
<tbody>
|
||||
<tr
|
||||
role="button"
|
||||
onClick={() => {
|
||||
setText(100)
|
||||
onZoomChanged(1)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
100%
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
role="button"
|
||||
onClick={() => {
|
||||
setText(90)
|
||||
onZoomChanged(0.9)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
90%
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
role="button"
|
||||
onClick={() => {
|
||||
setText(80)
|
||||
onZoomChanged(0.8)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
80%
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
role="button"
|
||||
onClick={() => {
|
||||
setText(70)
|
||||
onZoomChanged(0.7)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
70%
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
role="button"
|
||||
onClick={() => {
|
||||
setText(60)
|
||||
onZoomChanged(0.6)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
60%
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -46,6 +46,7 @@ class Airtable extends React.Component {
|
|||
options={[
|
||||
{ value: 'list_records', name: 'List records' },
|
||||
{ value: 'retrieve_record', name: 'Retrieve record' },
|
||||
{ value: 'update_record', name: 'Update record' },
|
||||
{ value: 'delete_record', name: 'Delete record' },
|
||||
]}
|
||||
value={this.state.options.operation}
|
||||
|
|
@ -138,6 +139,54 @@ class Airtable extends React.Component {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{['update_record'].includes(this.state.options.operation) && (
|
||||
<div>
|
||||
<div className="mb-3 mt-2">
|
||||
<label className="form-label text-muted">Base ID</label>
|
||||
<CodeHinter
|
||||
currentState={this.props.currentState}
|
||||
initialValue={this.state.options.base_id}
|
||||
className="codehinter-query-editor-input"
|
||||
theme={this.props.darkMode ? 'monokai' : 'default'}
|
||||
onChange={(value) => changeOption(this, 'base_id', value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 mt-2">
|
||||
<label className="form-label text-muted">Table name</label>
|
||||
<CodeHinter
|
||||
currentState={this.props.currentState}
|
||||
initialValue={this.state.options.table_name}
|
||||
className="codehinter-query-editor-input"
|
||||
theme={this.props.darkMode ? 'monokai' : 'default'}
|
||||
onChange={(value) => changeOption(this, 'table_name', value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 mt-2">
|
||||
<label className="form-label text-muted">Record ID</label>
|
||||
<CodeHinter
|
||||
currentState={this.props.currentState}
|
||||
initialValue={this.state.options.record_id}
|
||||
className="codehinter-query-editor-input"
|
||||
theme={this.props.darkMode ? 'monokai' : 'default'}
|
||||
onChange={(value) => changeOption(this, 'record_id', value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-3 mt-2">
|
||||
<label className="form-label">Body</label>
|
||||
<CodeHinter
|
||||
currentState={this.props.currentState}
|
||||
initialValue={'{}'}
|
||||
lineNumbers={true}
|
||||
className="query-hinter"
|
||||
theme={'duotone-light'}
|
||||
onChange={(value) => changeOption(this, 'body', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{['delete_record'].includes(this.state.options.operation) && (
|
||||
<div>
|
||||
<div className="mb-3 mt-2">
|
||||
|
|
@ -179,4 +228,4 @@ class Airtable extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export { Airtable };
|
||||
export { Airtable };
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import 'codemirror/theme/duotone-light.css';
|
||||
import SelectSearch, { fuzzySearch } from 'react-select-search';
|
||||
import { CodeHinter } from '../../CodeBuilder/CodeHinter';
|
||||
import { changeOption } from './utils';
|
||||
|
||||
|
|
@ -8,7 +9,9 @@ class Mysql extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
this.state = {
|
||||
options: this.props.options
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
|
@ -23,16 +26,98 @@ class Mysql extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
{options && (
|
||||
<div className="mb-3 mt-2">
|
||||
<CodeHinter
|
||||
currentState={this.props.currentState}
|
||||
initialValue={options.query}
|
||||
mode="sql"
|
||||
theme={this.props.darkMode ? 'monokai' : 'duotone-light'}
|
||||
lineNumbers={true}
|
||||
className="query-hinter"
|
||||
onChange={(value) => changeOption(this, 'query', value)}
|
||||
/>
|
||||
<div>
|
||||
<div className="mb-3 mt-2 col-md-2">
|
||||
<SelectSearch
|
||||
options={[
|
||||
{ name: 'SQL mode', value: 'sql' },
|
||||
{ name: 'GUI mode', value: 'gui' }
|
||||
]}
|
||||
value={options.mode}
|
||||
search={true}
|
||||
onChange={(value) => {
|
||||
changeOption(this, 'mode', value);
|
||||
}}
|
||||
filterOptions={fuzzySearch}
|
||||
placeholder="Select.."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{options.mode === 'sql' && (
|
||||
<div className="mb-3 mt-2">
|
||||
<CodeHinter
|
||||
currentState={this.props.currentState}
|
||||
initialValue={options.query}
|
||||
mode="sql"
|
||||
theme={this.props.darkMode ? 'monokai' : 'duotone-light'}
|
||||
lineNumbers={true}
|
||||
lineWrapping={true}
|
||||
className="query-hinter"
|
||||
enablePreview
|
||||
height="auto"
|
||||
minHeight="120px"
|
||||
onChange={(value) => changeOption(this, 'query', value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{options.mode === 'gui' && (
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<label className="form-label">Table</label>
|
||||
<CodeHinter
|
||||
currentState={this.props.currentState}
|
||||
initialValue={this.state.options.table}
|
||||
onChange={(value) => changeOption(this, 'table', value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col">
|
||||
<label className="form-label">Operation</label>
|
||||
<div className="gui-select-wrappper">
|
||||
<SelectSearch
|
||||
options={[{ name: 'Bulk update using primary key', value: 'bulk_update_pkey' }]}
|
||||
value={options.operation}
|
||||
search={true}
|
||||
onChange={(value) => {
|
||||
changeOption(this, 'operation', value);
|
||||
}}
|
||||
filterOptions={fuzzySearch}
|
||||
placeholder="Select.."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{options.operation === 'bulk_update_pkey' && (
|
||||
<div>
|
||||
<div className="mb-3 mt-2">
|
||||
<label className="form-label">Primary key column</label>
|
||||
<CodeHinter
|
||||
currentState={this.props.currentState}
|
||||
initialValue={options.primary_key_column}
|
||||
onChange={(value) => changeOption(this, 'primary_key_column', value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 mt-2">
|
||||
<label className="form-label">Records to update</label>
|
||||
<CodeMirror
|
||||
height="auto"
|
||||
fontSize="2"
|
||||
value={options.records}
|
||||
onChange={(instance) => changeOption(this, 'records', instance.getValue())}
|
||||
placeholder="{{ [ ] }}"
|
||||
options={{
|
||||
theme: 'duotone-light',
|
||||
mode: 'javascript',
|
||||
lineWrapping: true,
|
||||
scrollbarStyle: null
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ class Restapi extends React.Component {
|
|||
<CodeHinter
|
||||
currentState={this.props.currentState}
|
||||
initialValue={options.url}
|
||||
height='36px'
|
||||
className="codehinter-query-editor-input"
|
||||
theme={this.props.darkMode ? 'monokai' : 'default'}
|
||||
onChange={(value) => {
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ class Viewer extends React.Component {
|
|||
</a>
|
||||
</h1>
|
||||
{this.state.app && <span>{this.state.app.name}</span>}
|
||||
<div className="navbar-nav flex-row order-md-last">
|
||||
<div className="d-flex align-items-center m-1 p-1">
|
||||
<DarkModeToggle
|
||||
switchDarkMode={this.props.switchDarkMode}
|
||||
darkMode={this.props.darkMode}
|
||||
|
|
|
|||
|
|
@ -20,17 +20,18 @@ class HomePage extends React.Component {
|
|||
isLoading: true,
|
||||
creatingApp: false,
|
||||
currentFolder: {},
|
||||
showAppDeletionConfirmation: false,
|
||||
showAppDeletionConfirmation: false,
|
||||
apps: [],
|
||||
folders: [],
|
||||
meta: {
|
||||
count: 1
|
||||
count: 1,
|
||||
folders: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchApps(0, this.state.currentFolder.id);
|
||||
this.fetchApps(1, this.state.currentFolder.id);
|
||||
this.fetchFolders();
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ class HomePage extends React.Component {
|
|||
|
||||
appService.getAll(page, folder).then((data) => this.setState({
|
||||
apps: data.apps,
|
||||
meta: data.meta,
|
||||
meta: {...this.state.meta, ...data.meta},
|
||||
isLoading: false
|
||||
}));
|
||||
}
|
||||
|
|
@ -65,7 +66,7 @@ class HomePage extends React.Component {
|
|||
|
||||
folderChanged = (folder) => {
|
||||
this.setState({'currentFolder': folder});
|
||||
this.fetchApps(0, folder.id);
|
||||
this.fetchApps(1, folder.id);
|
||||
}
|
||||
|
||||
foldersChanged = () => {
|
||||
|
|
@ -82,7 +83,7 @@ class HomePage extends React.Component {
|
|||
};
|
||||
|
||||
deleteApp = (app) => {
|
||||
this.setState({ showAppDeletionConfirmation: true, appToBeDeleted: app })
|
||||
this.setState({ showAppDeletionConfirmation: true, appToBeDeleted: app });
|
||||
}
|
||||
|
||||
executeAppDeletion = () => {
|
||||
|
|
@ -92,16 +93,17 @@ class HomePage extends React.Component {
|
|||
hideProgressBar: true,
|
||||
position: 'top-center'
|
||||
});
|
||||
this.setState({
|
||||
isDeletingApp: false,
|
||||
this.setState({
|
||||
isDeletingApp: false,
|
||||
appToBeDeleted: null,
|
||||
showAppDeletionConfirmation: false
|
||||
});
|
||||
this.fetchApps(this.state.currentPage || 0, this.state.currentFolder.id)
|
||||
this.fetchApps(this.state.currentPage || 1, this.state.currentFolder.id);
|
||||
this.fetchFolders();
|
||||
}).catch(({ error }) => {
|
||||
toast.error('Could not delete the app.', { hideProgressBar: true, position: 'top-center' });
|
||||
this.setState({
|
||||
isDeletingApp: false,
|
||||
this.setState({
|
||||
isDeletingApp: false,
|
||||
appToBeDeleted: null,
|
||||
showAppDeletionConfirmation: false
|
||||
});
|
||||
|
|
@ -109,6 +111,10 @@ class HomePage extends React.Component {
|
|||
;
|
||||
}
|
||||
|
||||
pageCount = () => {
|
||||
return this.state.currentFolder.id ? this.state.meta.folder_count : this.state.meta.total_count;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
apps, isLoading, creatingApp, meta, currentFolder, showAppDeletionConfirmation, isDeletingApp
|
||||
|
|
@ -128,7 +134,7 @@ class HomePage extends React.Component {
|
|||
switchDarkMode={this.props.switchDarkMode}
|
||||
darkMode={this.props.darkMode}
|
||||
/>
|
||||
{!isLoading && meta.total_count === 0 &&
|
||||
{!isLoading && meta.total_count === 0 && !currentFolder.id &&
|
||||
<BlankPage
|
||||
createApp={this.createApp}
|
||||
/>
|
||||
|
|
@ -214,15 +220,16 @@ class HomePage extends React.Component {
|
|||
</OverlayTrigger>
|
||||
</Link>
|
||||
<Link
|
||||
to={`/applications/${app.slug}`}
|
||||
target="_blank"
|
||||
to={app?.current_version_id ? `/applications/${app.slug}` : '' }
|
||||
|
||||
target={app?.current_version_id ? '_blank' : ''}
|
||||
>
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
overlay={(props) => renderTooltip({props, text: 'Open in app viewer'})}
|
||||
overlay={(props) => renderTooltip({props, text: app?.current_version_id == null ? 'App does not have a deployed version' : 'Open in app viewer'})}
|
||||
>
|
||||
<span className="badge bg-blue-lt mx-2">launch</span>
|
||||
|
||||
<span className={`${app?.current_version_id ? 'badge bg-blue-lt mx-2 ' : 'badge bg-light-grey mx-2'}`}
|
||||
>launch </span>
|
||||
</OverlayTrigger>
|
||||
</Link>
|
||||
|
||||
|
|
@ -246,10 +253,10 @@ class HomePage extends React.Component {
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{meta.total_count > 0 && (
|
||||
{this.pageCount() > 10 && (
|
||||
<Pagination
|
||||
currentPage={meta.current_page}
|
||||
count={meta.folder_count}
|
||||
count={this.pageCount()}
|
||||
totalPages={meta.total_pages}
|
||||
pageChanged={this.pageChanged}
|
||||
/>
|
||||
|
|
|
|||
160
frontend/src/SettingsPage/SettingsPage.jsx
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import React from 'react';
|
||||
import { authenticationService, userService } from '@/_services';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { Header } from '@/_components';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
function SettingsPage(props) {
|
||||
const [firstName, setFirstName] = React.useState(authenticationService.currentUserValue.first_name)
|
||||
const [lastName, setLastName] = React.useState(authenticationService.currentUserValue.last_name)
|
||||
const [currentpassword, setCurrentPassword] = React.useState('')
|
||||
const [newPassword, setNewPassword] = React.useState('')
|
||||
const [updateInProgress, setUpdateInProgress] = React.useState(false)
|
||||
const [passwordChangeInProgress, setPasswordChangeInProgress] = React.useState(false)
|
||||
|
||||
const updateDetails = async () => {
|
||||
setUpdateInProgress(true);
|
||||
const updatedDetails = await userService.updateCurrentUser(firstName, lastName);
|
||||
authenticationService.updateCurrentUserDetails(updatedDetails)
|
||||
toast.success('Details updated!', { hideProgressBar: true, autoClose: 3000 });
|
||||
setUpdateInProgress(false);
|
||||
}
|
||||
|
||||
const changePassword = async () => {
|
||||
setPasswordChangeInProgress(true)
|
||||
const response = userService.changePassword(currentpassword, newPassword)
|
||||
response.then(() => {
|
||||
toast.success('Password updated successfully', { hideProgressBar: true, autoClose: 3000 });
|
||||
setCurrentPassword('')
|
||||
setNewPassword('')
|
||||
}).catch(() => {
|
||||
toast.error('Please verify that you have entered the correct password', { hideProgressBar: true, autoClose: 3000 });
|
||||
})
|
||||
setPasswordChangeInProgress(false);
|
||||
}
|
||||
|
||||
const newPasswordKeyPressHandler = async event => {
|
||||
if (event.key === 'Enter') {
|
||||
changePassword()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="wrapper">
|
||||
<Header switchDarkMode={props.switchDarkMode} darkMode={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">Settings</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-body">
|
||||
<div className="container-xl">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Profile</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">First name</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="first-name"
|
||||
placeholder="Enter first name"
|
||||
value={firstName}
|
||||
onChange={event => setFirstName(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Last name</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="last-name"
|
||||
placeholder="Enter last name"
|
||||
value={lastName}
|
||||
onChange={event => setLastName(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
className={"btn btn-primary" + (updateInProgress ? ' btn-loading' : '')}
|
||||
onClick={updateDetails}
|
||||
>
|
||||
Update
|
||||
</a>
|
||||
{/* An !important style on theme.scss is making the last child of every .card-body color to #c3c3c3!. */}
|
||||
{/* The div below is a placeholder to prevent it from affecting the button above. */}
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Change password</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Current password</label>
|
||||
<input
|
||||
type="password"
|
||||
className="form-control"
|
||||
name="last-name"
|
||||
placeholder="Enter current password"
|
||||
value={currentpassword}
|
||||
onChange={event => setCurrentPassword(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="mb-3">
|
||||
<label className="form-label">New password</label>
|
||||
<input
|
||||
type="password"
|
||||
className="form-control"
|
||||
name="last-name"
|
||||
placeholder="Enter new password"
|
||||
value={newPassword}
|
||||
onChange={event => setNewPassword(event.target.value)}
|
||||
onKeyPress={newPasswordKeyPressHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="#"
|
||||
className={"btn btn-primary" + (passwordChangeInProgress ? ' btn-loading' : '')}
|
||||
onClick={changePassword}
|
||||
>
|
||||
Change password
|
||||
</a>
|
||||
{/* An !important style on theme.scss is making the last child of every .card-body color to #c3c3c3!. */}
|
||||
{/* The div below is a placeholder to prevent it from affecting the button above. */}
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { SettingsPage };
|
||||
1
frontend/src/SettingsPage/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './SettingsPage';
|
||||
|
|
@ -1,22 +1,86 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { useSpring, animated } from 'react-spring';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import Tooltip from 'react-bootstrap/Tooltip';
|
||||
|
||||
export const DarkModeToggle = function DarkModeToggle({
|
||||
darkMode, switchDarkMode
|
||||
}) {
|
||||
export const DarkModeToggle = function DarkModeToggle({ darkMode = false, switchDarkMode }) {
|
||||
const toggleDarkMode = () => {
|
||||
switchDarkMode(!darkMode);
|
||||
};
|
||||
|
||||
const [darkModeEnabled, setMode] = useState(darkMode);
|
||||
const properties = {
|
||||
sun: {
|
||||
r: 9,
|
||||
transform: 'rotate(40deg)',
|
||||
cx: 12,
|
||||
cy: 4,
|
||||
opacity: 0,
|
||||
},
|
||||
moon: {
|
||||
r: 5,
|
||||
transform: 'rotate(90deg)',
|
||||
cx: 30,
|
||||
cy: 0,
|
||||
opacity: 1,
|
||||
},
|
||||
springConfig: { mass: 4, tension: 250, friction: 35 },
|
||||
};
|
||||
|
||||
const icon = darkModeEnabled ? 'night.svg' : 'day.svg';
|
||||
|
||||
return <div>
|
||||
<label className="form-check form-switch my-2">
|
||||
<img src={`/assets/images/icons/${icon}`} width="16" height="16" />
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
onClick={() => { switchDarkMode(!darkModeEnabled); setMode(!darkModeEnabled); } }
|
||||
checked={darkModeEnabled}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
const { r, transform, cx, cy, opacity } = properties[darkMode ? 'moon' : 'sun'];
|
||||
|
||||
const svgContainerProps = useSpring({
|
||||
transform,
|
||||
config: properties.springConfig,
|
||||
});
|
||||
const centerCircleProps = useSpring({ r, config: properties.springConfig });
|
||||
const maskedCircleProps = useSpring({
|
||||
cx,
|
||||
cy,
|
||||
color: 'white',
|
||||
config: properties.springConfig,
|
||||
});
|
||||
const linesProps = useSpring({ opacity, config: properties.springConfig });
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
placement="bottom"
|
||||
delay={{ show: 250, hide: 400 }}
|
||||
overlay={<Tooltip id="button-tooltip">{darkMode ? 'Activate light mode' : 'Activate dark mode'}</Tooltip>}
|
||||
>
|
||||
<animated.svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
color={darkMode ? '#fff' : '#808080'}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
stroke="currentColor"
|
||||
onClick={toggleDarkMode}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
...svgContainerProps,
|
||||
}}
|
||||
>
|
||||
<mask id="myMask2">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white" />
|
||||
<animated.circle style={maskedCircleProps} r="9" fill="black" />
|
||||
</mask>
|
||||
|
||||
<animated.circle cx="12" cy="12" style={centerCircleProps} fill={darkMode ? 'white' : '#808080'} mask="url(#myMask2)" />
|
||||
<animated.g stroke="currentColor" style={linesProps}>
|
||||
<line x1="12" y1="1" x2="12" y2="3" />
|
||||
<line x1="12" y1="21" x2="12" y2="23" />
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
||||
<line x1="1" y1="12" x2="3" y2="12" />
|
||||
<line x1="21" y1="12" x2="23" y2="12" />
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
||||
</animated.g>
|
||||
</animated.svg>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ export const Header = function Header({
|
|||
history.push('/login');
|
||||
}
|
||||
|
||||
function openSettings() {
|
||||
history.push('/settings')
|
||||
}
|
||||
|
||||
const { first_name, last_name } = authenticationService.currentUserValue;
|
||||
|
||||
return <header className="navbar navbar-expand-md navbar-light d-print-none">
|
||||
|
|
@ -56,7 +60,7 @@ export const Header = function Header({
|
|||
</li>
|
||||
</ul>
|
||||
<div className="navbar-nav flex-row order-md-last">
|
||||
<div className="p-1">
|
||||
<div className="p-1 m-1 d-flex align-items-center">
|
||||
<DarkModeToggle
|
||||
switchDarkMode={switchDarkMode}
|
||||
darkMode={darkMode}
|
||||
|
|
@ -78,6 +82,9 @@ export const Header = function Header({
|
|||
</div>
|
||||
</a>
|
||||
<div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
||||
<a data-testId="settingsBtn" onClick={openSettings} className="dropdown-item">
|
||||
Settings
|
||||
</a>
|
||||
<a data-testId="logoutBtn" onClick={logout} className="dropdown-item">
|
||||
Logout
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -19,20 +19,32 @@ export const Pagination = function Pagination({
|
|||
gotoPage(currentPage - 1);
|
||||
}
|
||||
|
||||
return (<div className="card-footer d-flex align-items-center">
|
||||
<p className="m-0 text-muted">Showing <span>{ (currentPage - 1) * 10 + 1}</span> to <span>{currentPage * 10}</span> of <span>{count}</span></p>
|
||||
<ul className="pagination m-0 ms-auto">
|
||||
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
|
||||
<a style={{ cursor: 'pointer' }} className="page-link" onClick={gotoPreviousPage}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="icon" width="24" height="24" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><polyline points="15 6 9 12 15 18"></polyline></svg>
|
||||
</a>
|
||||
</li>
|
||||
{Array.from(Array(totalPages).keys()).map((i) => (renderPageItem(i)))}
|
||||
<li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
|
||||
<a style={{ cursor: 'pointer' }} className="page-link" onClick={gotoNextPage}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="icon" width="24" height="24" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><polyline points="9 6 15 12 9 18"></polyline></svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>);
|
||||
function startingAppCount(currentPage) {
|
||||
return ((currentPage - 1) * 10) + 1;
|
||||
}
|
||||
|
||||
function endingAppCount(currentPage, totalCount) {
|
||||
const num = currentPage * 10;
|
||||
|
||||
return num > totalCount ? totalCount : num;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card-footer d-flex align-items-center">
|
||||
<p className="m-0 text-muted">Showing <span>{startingAppCount(currentPage)}</span> to <span>{endingAppCount(currentPage, count)}</span> of <span>{count}</span></p>
|
||||
<ul className="pagination m-0 ms-auto">
|
||||
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
|
||||
<a style={{ cursor: 'pointer' }} className="page-link" onClick={gotoPreviousPage}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="icon" width="24" height="24" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><polyline points="15 6 9 12 15 18"></polyline></svg>
|
||||
</a>
|
||||
</li>
|
||||
{Array.from(Array(totalPages).keys()).map((i) => (renderPageItem(i)))}
|
||||
<li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
|
||||
<a style={{ cursor: 'pointer' }} className="page-link" onClick={gotoNextPage}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="icon" width="24" height="24" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><polyline points="9 6 15 12 9 18"></polyline></svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ export function onEvent(_ref, eventName, options, mode = 'edit') {
|
|||
}
|
||||
}
|
||||
|
||||
if (['onPageChanged', 'onSearch'].includes(eventName)) {
|
||||
if (['onPageChanged', 'onSearch', 'onSelectionChange'].includes(eventName)) {
|
||||
const { component } = options;
|
||||
const event = component.definition.events[eventName];
|
||||
|
||||
|
|
@ -222,6 +222,16 @@ export function onEvent(_ref, eventName, options, mode = 'edit') {
|
|||
}
|
||||
}
|
||||
|
||||
/* Events for QrScanner */
|
||||
if (['onDetect'].includes(eventName)) {
|
||||
const { component } = options;
|
||||
const event = component.definition.events[eventName];
|
||||
|
||||
if (event.actionId) {
|
||||
executeAction(_self, event, mode);
|
||||
}
|
||||
}
|
||||
|
||||
if (eventName === 'onBulkUpdate') {
|
||||
return new Promise(function (resolve, reject) {
|
||||
onComponentOptionChanged(_self, options.component, 'isSavingChanges', true);
|
||||
|
|
@ -319,7 +329,6 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const newState = {
|
||||
..._ref.state.currentState,
|
||||
queries: {
|
||||
|
|
@ -346,7 +355,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined) {
|
|||
}
|
||||
|
||||
if (data.status === 'failed') {
|
||||
toast.error(data.error.message, { hideProgressBar: true, autoClose: 3000 });
|
||||
toast.error(data.message, { hideProgressBar: true, autoClose: 3000 });
|
||||
}
|
||||
|
||||
let rawData = data.data;
|
||||
|
|
|
|||
54
frontend/src/_hooks/use-popover.jsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { useRef, useState, useEffect, useCallback } from 'react';
|
||||
const noop = () => {};
|
||||
const useEscapeHandler = (handler = noop, dependencies = []) => {
|
||||
const escapeHandler = (e) => {
|
||||
if (e.code === 'Escape') {
|
||||
handler();
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
document === null || document === void 0 ? void 0 : document.addEventListener('keyup', escapeHandler);
|
||||
return () =>
|
||||
document === null || document === void 0 ? void 0 : document.removeEventListener('keyup', escapeHandler);
|
||||
}, dependencies);
|
||||
};
|
||||
const useClickOutside = (handler = noop, dependencies) => {
|
||||
const callbackRef = useRef(handler);
|
||||
const ref = useRef(null);
|
||||
const outsideClickHandler = (e) => {
|
||||
if (callbackRef.current && ref.current && !ref.current.contains(e.target)) {
|
||||
callbackRef.current(e);
|
||||
}
|
||||
};
|
||||
// useEffect wrapper to be safe for concurrent mode
|
||||
useEffect(() => {
|
||||
callbackRef.current = handler;
|
||||
});
|
||||
useEffect(() => {
|
||||
document === null || document === void 0 ? void 0 : document.addEventListener('click', outsideClickHandler, { capture: true });
|
||||
return () =>
|
||||
document === null || document === void 0 ? void 0 : document.removeEventListener('click', outsideClickHandler, { capture: true });
|
||||
}, dependencies);
|
||||
return ref;
|
||||
};
|
||||
const role = 'dialog';
|
||||
const usePopover = (defaultOpen = false) => {
|
||||
const triggerRef = useRef(null);
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const toggle = useCallback(() => setOpen(!open), []);
|
||||
const close = useCallback(() => setOpen(false), []);
|
||||
useEscapeHandler(close, []);
|
||||
const contentRef = useClickOutside(open ? close : undefined, []);
|
||||
const trigger = {
|
||||
ref: triggerRef,
|
||||
onClick: toggle,
|
||||
'aria-haspopup': role,
|
||||
'aria-expanded': open,
|
||||
};
|
||||
const content = {
|
||||
ref: contentRef,
|
||||
role,
|
||||
};
|
||||
return [open, trigger, content, setOpen];
|
||||
};
|
||||
export default usePopover;
|
||||
37
frontend/src/_hooks/use-router.jsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { useMemo } from 'react';
|
||||
import {
|
||||
useParams,
|
||||
useLocation,
|
||||
useHistory,
|
||||
useRouteMatch,
|
||||
} from 'react-router-dom';
|
||||
import queryString from 'query-string';
|
||||
|
||||
export default function useRouter() {
|
||||
const params = useParams();
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const match = useRouteMatch();
|
||||
// Return our custom router object
|
||||
// Memoize so that a new object is only returned if something changes
|
||||
return useMemo(() => {
|
||||
return {
|
||||
// For convenience add push(), replace(), pathname at top level
|
||||
push: history.push,
|
||||
replace: history.replace,
|
||||
pathname: location.pathname,
|
||||
// Merge params and parsed query string into single 'query' object
|
||||
// so that they can be used interchangeably.
|
||||
// Example: /:topic?sort=popular -> { topic: 'react', sort: 'popular' }
|
||||
query: {
|
||||
...queryString.parse(location.search), // Convert string to object
|
||||
...params,
|
||||
},
|
||||
// Include match, location, history objects so we have
|
||||
// access to extra React Router functionality if needed.
|
||||
match,
|
||||
location,
|
||||
history,
|
||||
};
|
||||
}, [params, match, location, history]);
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ export const authenticationService = {
|
|||
login,
|
||||
logout,
|
||||
signup,
|
||||
updateCurrentUserDetails,
|
||||
currentUser: currentUserSubject.asObservable(),
|
||||
get currentUserValue() { return currentUserSubject.value; }
|
||||
};
|
||||
|
|
@ -31,6 +32,13 @@ function login(email, password) {
|
|||
});
|
||||
}
|
||||
|
||||
function updateCurrentUserDetails(details) {
|
||||
const currentUserDetails = JSON.parse(localStorage.getItem('currentUser'));
|
||||
const updatedUserDetails = Object.assign({}, currentUserDetails, details)
|
||||
localStorage.setItem('currentUser', JSON.stringify(updatedUserDetails));
|
||||
currentUserSubject.next(updatedUserDetails);
|
||||
}
|
||||
|
||||
function signup(email) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ export const userService = {
|
|||
getAll,
|
||||
createUser,
|
||||
deleteUser,
|
||||
setPasswordFromToken
|
||||
setPasswordFromToken,
|
||||
updateCurrentUser,
|
||||
changePassword,
|
||||
};
|
||||
|
||||
function getAll() {
|
||||
|
|
@ -43,3 +45,15 @@ function setPasswordFromToken({ token, password, organization, newSignup, firstN
|
|||
const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
|
||||
return fetch(`${config.apiUrl}/users/set_password_from_token`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function updateCurrentUser(firstName , lastName) {
|
||||
const body = { firstName, lastName };
|
||||
const requestOptions = { method: 'PATCH', headers: authHeader(), body: JSON.stringify(body) };
|
||||
return fetch(`${config.apiUrl}/users/update`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function changePassword(currentPassword, newPassword) {
|
||||
const body = { currentPassword, newPassword };
|
||||
const requestOptions = { method: 'PATCH', headers: authHeader(), body: JSON.stringify(body) };
|
||||
return fetch(`${config.apiUrl}/users/change_password`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
|
|
|||
2
frontend/src/_styles/colors.scss
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
$white: #fff !default;
|
||||
$grey: #eee !default;
|
||||
55
frontend/src/_styles/left-sidebar.scss
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
@import './colors.scss';
|
||||
|
||||
.left-sidebar {
|
||||
background: $white;
|
||||
|
||||
.left-sidebar-item {
|
||||
margin: 5px;
|
||||
text-align: center;
|
||||
padding-top: 1vw;
|
||||
padding-bottom: 1vw;
|
||||
border-bottom: 1px solid $grey;
|
||||
transition: all .2s ease-in-out;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
.left-sidebar-stack-bottom {
|
||||
width: 3%;
|
||||
position: fixed;
|
||||
bottom: 12vw;
|
||||
height: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
.popover {
|
||||
position: fixed;
|
||||
left: 3%;
|
||||
top: 55px;
|
||||
overflow: auto;
|
||||
max-height: 60%;
|
||||
}
|
||||
.datasources-popover {
|
||||
top: 100px;
|
||||
}
|
||||
.zoom-popover {
|
||||
top: 500px;
|
||||
}
|
||||
.sidebar-zoom {
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
.show {
|
||||
display: block;
|
||||
}
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
.dark-mode {
|
||||
transform: scale(.7);
|
||||
}
|
||||
.no-border {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ body {
|
|||
.editor {
|
||||
.header-container {
|
||||
max-width: 100%;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.resizer-active {
|
||||
|
|
@ -64,12 +66,11 @@ body {
|
|||
|
||||
.left-sidebar {
|
||||
height: 100%;
|
||||
width: 12%;
|
||||
// position: fixed;
|
||||
width: 3%;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
overflow-x: hidden;
|
||||
|
||||
flex: 1 1 auto;
|
||||
background-color: #fff;
|
||||
background-clip: border-box;
|
||||
|
|
@ -237,8 +238,8 @@ body {
|
|||
}
|
||||
|
||||
.main {
|
||||
margin-left: 12%;
|
||||
width: 73%;
|
||||
margin-left: 3%;
|
||||
width: 82%;
|
||||
top: 0;
|
||||
|
||||
.canvas-container::-webkit-scrollbar {
|
||||
|
|
@ -258,7 +259,7 @@ body {
|
|||
|
||||
.canvas-container {
|
||||
height: 100%;
|
||||
width: 73%;
|
||||
width: 82%;
|
||||
top: 56px;
|
||||
position: fixed;
|
||||
overflow-y: auto;
|
||||
|
|
@ -298,10 +299,10 @@ body {
|
|||
|
||||
.query-pane {
|
||||
height: 350px;
|
||||
width: 73%;
|
||||
width: 82%;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: 12%;
|
||||
// z-index: 1;
|
||||
left: 3%;
|
||||
bottom: 0;
|
||||
overflow-x: hidden;
|
||||
flex: 1 1 auto;
|
||||
|
|
@ -366,9 +367,10 @@ body {
|
|||
border-width: 0px 0px 1px 0px;
|
||||
background: white;
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
width: 54%;
|
||||
z-index: 3;
|
||||
width: 61.5%;
|
||||
margin-top: 0px;
|
||||
padding: 0.5px;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
|
|
@ -1264,6 +1266,12 @@ body {
|
|||
margin-bottom: 0rem;
|
||||
}
|
||||
|
||||
.code-hinter::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.codehinter-query-editor-input {
|
||||
.CodeMirror {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
|
|
@ -1303,11 +1311,11 @@ body {
|
|||
.CodeMirror {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
height: 50px!important;
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror-focused {
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar {
|
||||
|
|
@ -1318,7 +1326,6 @@ body {
|
|||
|
||||
.CodeMirror-scroll {
|
||||
overflow:hidden !important;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1658,6 +1665,7 @@ input:focus-visible {
|
|||
margin-top: -2px;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
}
|
||||
|
||||
.theme-dark {
|
||||
|
|
@ -1666,7 +1674,7 @@ input:focus-visible {
|
|||
}
|
||||
|
||||
.card-body > :last-child {
|
||||
color: #c3c3c3!important;
|
||||
color: #fff!important;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
|
|
@ -1674,7 +1682,7 @@ input:focus-visible {
|
|||
}
|
||||
|
||||
.card {
|
||||
background-color: #2f3c4c!important;
|
||||
background-color: #324156!important;
|
||||
}
|
||||
|
||||
.DateInput {
|
||||
|
|
@ -1721,7 +1729,7 @@ input:focus-visible {
|
|||
.modal-content, .modal-header {
|
||||
background-color: #1f2936!important;
|
||||
.text-muted {
|
||||
color: #c3c3c3!important;
|
||||
color: #fff!important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1748,7 +1756,7 @@ input:focus-visible {
|
|||
|
||||
.query-list {
|
||||
.text-muted {
|
||||
color: #c3c3c3!important;
|
||||
color: #fff!important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1791,16 +1799,16 @@ input:focus-visible {
|
|||
border: solid rgba(255, 255, 255, 0.09);
|
||||
border-width: 0px 1px 3px 0px;
|
||||
.text-muted {
|
||||
color: #c3c3c3!important;
|
||||
color: #fff!important;
|
||||
}
|
||||
}
|
||||
|
||||
.folder-list {
|
||||
color: #c3c3c3!important;
|
||||
color: #fff!important;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
color: #c3c3c3!important;
|
||||
color: #fff!important;
|
||||
}
|
||||
|
||||
.RichEditor-root {
|
||||
|
|
@ -1809,7 +1817,7 @@ input:focus-visible {
|
|||
}
|
||||
|
||||
.app-description {
|
||||
color: #c3c3c3!important;
|
||||
color: #fff!important;
|
||||
}
|
||||
|
||||
.btn-light, .btn-outline-light {
|
||||
|
|
@ -1842,7 +1850,7 @@ input:focus-visible {
|
|||
|
||||
.app-users-list {
|
||||
.text-muted {
|
||||
color: #c3c3c3!important;
|
||||
color: #fff!important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1856,7 +1864,7 @@ input:focus-visible {
|
|||
border-width: 0px 0px 1px 0px!important;
|
||||
|
||||
.text-muted {
|
||||
color: #c3c3c3!important;
|
||||
color: #fff!important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1879,7 +1887,7 @@ input:focus-visible {
|
|||
.alert {
|
||||
background: transparent;
|
||||
.text-muted {
|
||||
color: #c3c3c3!important;
|
||||
color: #fff!important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1993,4 +2001,4 @@ input:focus-visible {
|
|||
|
||||
.theme-dark .input-group-text, .theme-dark .markdown > table thead th, .theme-dark .table thead th {
|
||||
background: #1c252f;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@
|
|||
"start:prod": "npm --prefix server run start:prod",
|
||||
"deploy": "cp -a frontend/build/. public/",
|
||||
"heroku-postbuild": "npm run build && npm run deploy",
|
||||
"heroku-prebuild": "npm --prefix frontend install && npm --prefix server install "
|
||||
"heroku-prebuild": "npm --prefix frontend install && npm --prefix server install ",
|
||||
"cy:run": "cypress run --env db.name=$TEST_PG_DB,db.user=$TEST_PG_USERNAME,db.password=$TEST_PG_PASSWORD",
|
||||
"cy:open": "cypress open --env db.name=$TEST_PG_DB,db.user=$TEST_PG_USERNAME,db.password=$TEST_PG_PASSWORD"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
0.6.0
|
||||
0.6.1
|
||||
12
server/migrations/1625814801415-MaybeCreateExtension.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MaybeCreateExtension1625814801415 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('CREATE EXTENSION IF NOT EXISTS pgcrypto;')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ function buildConnectionOptions(
|
|||
uuidExtension: 'pgcrypto',
|
||||
migrationsRun: false,
|
||||
logging: data.ORM_LOGGING || false,
|
||||
migrations: ['dist/migrations/**/*{.ts,.js}'],
|
||||
migrations: [__dirname + '/migrations/**/*{.ts,.js}'],
|
||||
cli: {
|
||||
migrationsDir: 'migrations',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@
|
|||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "NODE_ENV=production node dist/src/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "NODE_ENV=test npm run db:reset && jest --runInBand --config ./test/jest-e2e.json ",
|
||||
"test": "NODE_ENV=test jest",
|
||||
"test:watch": "NODE_ENV=test jest --watch",
|
||||
"test:cov": "NODE_ENV=test jest --coverage",
|
||||
"test:debug": "NODE_ENV=test node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "NODE_ENV=test jest --runInBand --config ./test/jest-e2e.json ",
|
||||
"db:create": "ts-node ./scripts/create-database.ts",
|
||||
"db:drop": "ts-node ./scripts/drop-database.ts",
|
||||
"db:migrate": "npm run typeorm migration:run",
|
||||
|
|
|
|||
|
|
@ -44,6 +44,25 @@ export default class AirtableQueryService implements QueryService {
|
|||
result = JSON.parse(response.body);
|
||||
break;
|
||||
|
||||
case 'update_record':
|
||||
|
||||
response = await got(`https://api.airtable.com/v0/${baseId}/${tableName}`, {
|
||||
method: 'patch',
|
||||
headers: this.authHeader(accessToken),
|
||||
json: {
|
||||
"records": [
|
||||
{
|
||||
"id": queryOptions['record_id'],
|
||||
"fields": JSON.parse(queryOptions['body'])
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
result = JSON.parse(response.body);
|
||||
|
||||
break;
|
||||
|
||||
case 'delete_record':
|
||||
const _recordId = queryOptions['record_id'];
|
||||
|
||||
|
|
@ -66,4 +85,4 @@ export default class AirtableQueryService implements QueryService {
|
|||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
40
server/plugins/datasources/mysql/index.service.spec.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import MysqlQueryService from '.';
|
||||
|
||||
describe('MysqlQueryService', () => {
|
||||
let service: MysqlQueryService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [MysqlQueryService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<MysqlQueryService>(MysqlQueryService);
|
||||
});
|
||||
|
||||
it('should generate the query for bulk update operation', async () => {
|
||||
const queryOptions = {
|
||||
table: 'customers',
|
||||
primary_key_column: 'id',
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'sam',
|
||||
email: 'sam@example.com'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'jon',
|
||||
email: 'jon@example.com'
|
||||
}
|
||||
]
|
||||
|
||||
};
|
||||
|
||||
const builtQuery = await service.buildBulkUpdateQuery(queryOptions);
|
||||
const expectedQuery = "UPDATE customers SET name = 'sam', email = 'sam@example.com' WHERE id = 1; UPDATE customers SET name = 'jon', email = 'jon@example.com' WHERE id = 2;";
|
||||
|
||||
expect(builtQuery).toBe(expectedQuery);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -9,9 +9,20 @@ import { cacheConnection, getCachedConnection } from 'src/helpers/utils.helper';
|
|||
export default class MysqlQueryService implements QueryService {
|
||||
|
||||
async run(sourceOptions: any, queryOptions: any, dataSourceId: string, dataSourceUpdatedAt: string): Promise<QueryResult> {
|
||||
|
||||
let result = { };
|
||||
let query = queryOptions.query;
|
||||
|
||||
let result = {
|
||||
rows: []
|
||||
};
|
||||
let query = '';
|
||||
|
||||
if(queryOptions.mode === 'gui') {
|
||||
if(queryOptions.operation === 'bulk_update_pkey') {
|
||||
query = await this.buildBulkUpdateQuery(queryOptions);
|
||||
}
|
||||
} else {
|
||||
query = queryOptions.query;
|
||||
}
|
||||
|
||||
const knexInstance = await this.getConnection(sourceOptions, {}, true, dataSourceId, dataSourceUpdatedAt);
|
||||
|
||||
try {
|
||||
|
|
@ -44,13 +55,14 @@ export default class MysqlQueryService implements QueryService {
|
|||
password : sourceOptions.password,
|
||||
database : sourceOptions.database,
|
||||
port: sourceOptions.port,
|
||||
multipleStatements: true
|
||||
}
|
||||
};
|
||||
|
||||
return knex(config);
|
||||
}
|
||||
|
||||
async getConnection(sourceOptions: any, options:any, checkCache: boolean, dataSourceId?: string, dataSourceUpdatedAt?: string): Promise<any> {
|
||||
async getConnection(sourceOptions: any, options:any, checkCache: boolean, dataSourceId?: string, dataSourceUpdatedAt?: string): Promise<any> {
|
||||
if(checkCache) {
|
||||
let connection = await getCachedConnection(dataSourceId, dataSourceUpdatedAt);
|
||||
|
||||
|
|
@ -64,6 +76,29 @@ export default class MysqlQueryService implements QueryService {
|
|||
} else {
|
||||
return await this.buildConnection(sourceOptions);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
async buildBulkUpdateQuery(queryOptions: any): Promise<string> {
|
||||
let queryText = '';
|
||||
|
||||
const tableName = queryOptions['table'];
|
||||
const primaryKey = queryOptions['primary_key_column'];
|
||||
const records = queryOptions['records'];
|
||||
|
||||
for(const record of records ) {
|
||||
queryText = `${queryText} UPDATE ${tableName} SET`;
|
||||
|
||||
for(const key of Object.keys(record)) {
|
||||
if(key !== primaryKey) {
|
||||
queryText = ` ${queryText} ${key} = '${record[key]}',`;
|
||||
}
|
||||
}
|
||||
|
||||
queryText = queryText.slice(0, -1);
|
||||
queryText = `${queryText} WHERE ${primaryKey} = ${record[primaryKey]};`;
|
||||
}
|
||||
|
||||
return queryText.trim();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export default class PostgresqlQueryService implements QueryService {
|
|||
database: sourceOptions.database,
|
||||
password: sourceOptions.password,
|
||||
port: sourceOptions.port,
|
||||
ssl: { rejectUnauthorized: false },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,12 +35,18 @@ function createDatabase(): void {
|
|||
process.env.PG_DB;
|
||||
|
||||
exec(createdb, (err, _stdout, _stderr) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
if (!err) {
|
||||
console.log(`Created database ${envVars.PG_DB}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Created database ${envVars.PG_DB}`);
|
||||
const errorMessage = `database "${envVars.PG_DB}" already exists`;
|
||||
|
||||
if (err.message.includes(errorMessage)) {
|
||||
console.log(errorMessage);
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,12 +35,18 @@ function dropDatabase(): void {
|
|||
process.env.PG_DB;
|
||||
|
||||
exec(dropdb, (err, _stdout, _stderr) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
if (!err) {
|
||||
console.log(`Dropped database ${envVars.PG_DB}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Dropped database ${envVars.PG_DB}`);
|
||||
const errorMessage = `database "${envVars.PG_DB}" does not exist`;
|
||||
|
||||
if (err.message.includes(errorMessage)) {
|
||||
console.log(errorMessage);
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,28 +21,34 @@ import { MetaModule } from './modules/meta/meta.module';
|
|||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { join } from 'path';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
const imports = [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: [`../.env.${process.env.NODE_ENV}`, '../.env']
|
||||
}),
|
||||
TypeOrmModule.forRoot(ormconfig),
|
||||
SeedsModule,
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
AppsModule,
|
||||
FoldersModule,
|
||||
FolderAppsModule,
|
||||
DataQueriesModule,
|
||||
DataSourcesModule,
|
||||
OrganizationsModule,
|
||||
CaslModule,
|
||||
MetaModule
|
||||
]
|
||||
|
||||
if(process.env.SERVE_CLIENT !== 'false')
|
||||
imports.unshift(
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(__dirname, '../../../', 'frontend/build'),
|
||||
}),
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: [`../.env.${process.env.NODE_ENV}`, '../.env']
|
||||
}),
|
||||
TypeOrmModule.forRoot(ormconfig),
|
||||
SeedsModule,
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
AppsModule,
|
||||
FoldersModule,
|
||||
FolderAppsModule,
|
||||
DataQueriesModule,
|
||||
DataSourcesModule,
|
||||
OrganizationsModule,
|
||||
CaslModule,
|
||||
MetaModule
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
@Module({
|
||||
imports,
|
||||
controllers: [AppController],
|
||||
providers: [AppService, EmailService, SeedsService],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export class AppsController {
|
|||
@Post()
|
||||
async create(@Request() req) {
|
||||
const params = req.body;
|
||||
|
||||
|
||||
const app = await this.appsService.create(req.user);
|
||||
return decamelizeKeys(app);
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ export class AppsController {
|
|||
@UseGuards(JwtAuthGuard)
|
||||
@Get(':id')
|
||||
async show(@Request() req, @Param() params) {
|
||||
|
||||
|
||||
const app = await this.appsService.find(params.id);
|
||||
let response = decamelizeKeys(app);
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ export class AppsController {
|
|||
if(!ability.can('updateParams', app)) {
|
||||
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);
|
||||
let response = decamelizeKeys(result);
|
||||
|
||||
|
|
@ -115,20 +115,26 @@ export class AppsController {
|
|||
const folderId = req.query.folder;
|
||||
|
||||
let apps = [];
|
||||
let folderCount = 0;
|
||||
|
||||
if(folderId) {
|
||||
const folder = await this.foldersService.findOne(folderId);
|
||||
apps = await this.foldersService.getAppsFor(req.user, folder, page);
|
||||
folderCount = await this.foldersService.userAppCount(req.user, folder);
|
||||
} else {
|
||||
apps = await this.appsService.all(req.user, page);
|
||||
}
|
||||
};
|
||||
|
||||
const totalCount = await this.appsService.count(req.user);
|
||||
let totalCount = await this.appsService.count(req.user);
|
||||
|
||||
const totalPageCount = folderId ? folderCount : totalCount;
|
||||
|
||||
console.log(folderCount, totalCount);
|
||||
const meta = {
|
||||
total_pages: Math.round(totalCount/10),
|
||||
total_pages: Math.ceil(totalPageCount/10),
|
||||
total_count: totalCount,
|
||||
current_page: parseInt(page || 0)
|
||||
folder_count: folderCount,
|
||||
current_page: parseInt(page || 1)
|
||||
}
|
||||
|
||||
const response = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { Controller, Get, Post, Query, Request, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Post, Patch, Request, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from 'src/modules/auth/jwt-auth.guard';
|
||||
import { PasswordRevalidateGuard } from 'src/modules/auth/password-revalidate.guard';
|
||||
import { UsersService } from 'src/services/users.service';
|
||||
|
||||
@Controller('users')
|
||||
|
|
@ -13,4 +15,22 @@ export class UsersController {
|
|||
return result;
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Patch('update')
|
||||
async update(@Request() req, @Body() body) {
|
||||
const {firstName, lastName } = body
|
||||
await this.usersService.update(req.user.id, { firstName, lastName });
|
||||
await req.user.reload()
|
||||
return {
|
||||
first_name: req.user.firstName,
|
||||
last_name: req.user.lastName
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard, PasswordRevalidateGuard)
|
||||
@Patch('change_password')
|
||||
async changePassword(@Request() req, @Body() body) {
|
||||
const { newPassword } = body
|
||||
return await this.usersService.update(req.user.id, { password: newPassword });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,8 +49,10 @@ export class App extends BaseEntity {
|
|||
|
||||
@AfterInsert()
|
||||
updateSlug() {
|
||||
const userRepository = getRepository(App);
|
||||
userRepository.update(this.id, { slug: this.id })
|
||||
if (!this.slug) {
|
||||
const appRepository = getRepository(App);
|
||||
appRepository.update(this.id, { slug: this.id })
|
||||
}
|
||||
}
|
||||
|
||||
protected definition;
|
||||
|
|
@ -59,12 +61,4 @@ export class App extends BaseEntity {
|
|||
afterLoad(): void {
|
||||
this.definition = this.currentVersion?.definition;
|
||||
}
|
||||
|
||||
@AfterInsert()
|
||||
generateSlug() {
|
||||
if (!this.slug) {
|
||||
this.slug = this.id;
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, ManyToMany, OneToMany, AfterLoad } from 'typeorm';
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, ManyToMany, OneToMany, AfterLoad, JoinTable } from 'typeorm';
|
||||
import { FolderApp } from './folder_app.entity';
|
||||
import { App } from './app.entity';
|
||||
|
||||
@Entity({ name: "folders" })
|
||||
export class Folder {
|
||||
|
|
@ -10,23 +11,39 @@ export class Folder {
|
|||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column({ name: 'organization_id' })
|
||||
@Column({ name: 'organization_id' })
|
||||
organizationId: string
|
||||
|
||||
@CreateDateColumn({ default: () => 'now()', name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
@UpdateDateColumn({ default: () => 'now()', name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
@OneToMany(() => FolderApp, folderApp => folderApp.folder, { eager: true })
|
||||
folderApps: FolderApp[];
|
||||
|
||||
@ManyToMany(type => App)
|
||||
@JoinTable({
|
||||
name: 'folder_apps',
|
||||
joinColumn: {
|
||||
name: 'folder_id'
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: 'app_id'
|
||||
}
|
||||
})
|
||||
apps: App[];
|
||||
|
||||
appCount: number;
|
||||
|
||||
protected count;
|
||||
|
||||
@AfterLoad()
|
||||
generateCount(): void {
|
||||
this.count = this.folderApps.length;
|
||||
if (this.folderApps) {
|
||||
this.count = this.folderApps.length;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||