Merge branch 'release/v0.6.1' into main

This commit is contained in:
navaneeth 2021-08-24 11:29:33 +05:30
commit 900e0e06fb
108 changed files with 2923 additions and 425 deletions

View file

@ -1 +1 @@
0.6.0
0.6.1

View file

@ -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"
]
}

View 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()
});
})

View file

@ -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')

View file

@ -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
}

View file

@ -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()
});

View file

@ -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:

View file

@ -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-*"

View file

@ -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"]
}

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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"
}
```

View file

@ -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 )

View file

@ -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

View 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`.

View 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"/>

View file

@ -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.

View 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**

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View 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

View file

@ -11,4 +11,4 @@
</g>
</g>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View 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

View 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

View file

@ -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",

View file

@ -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"
},

View file

@ -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>
);

View file

@ -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({

View file

@ -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>
}

View file

@ -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}

View file

@ -73,6 +73,7 @@ export const Modal = function Modal({
snapToGrid={true}
parentRef={parentRef}
parent={id}
currentLayout={containerProps.currentLayout}
/>
</BootstrapModal.Body>
</BootstrapModal>

View file

@ -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}

View 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>
);
};

View 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>
);
};

View 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>
);
};

View file

@ -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">

View file

@ -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}

View 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>
);
};

View 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>
);
};

View file

@ -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: {
}
}
}
];

View file

@ -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&apos;t added any components yet. Drag components from the right sidebar and drop here.</center>
</div>
)}

View file

@ -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>
)}

View file

@ -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 && (

View file

@ -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>

View file

@ -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)}
/>

View file

@ -54,7 +54,7 @@ export const EventSelector = ({
apps.map((item) => {
appsOptionsList.push({
name: item.name,
value: item.id
value: item.slug
})
})
return appsOptionsList;

View 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>
)
}

View 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&apos;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}
/>
</>
)
}

View 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>
</>
)
}

View 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>
)
}

View 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>
</>
)
}

View file

@ -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 };

View file

@ -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>

View file

@ -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) => {

View file

@ -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}

View file

@ -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}
/>

View 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 };

View file

@ -0,0 +1 @@
export * from './SettingsPage';

View file

@ -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>
);
};

View file

@ -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>

View file

@ -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>
);
};

View file

@ -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;

View 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;

View 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]);
}

View file

@ -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',

View file

@ -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);
}

View file

@ -0,0 +1,2 @@
$white: #fff !default;
$grey: #eee !default;

View 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;
}
}

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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"
}
}

View file

@ -1 +1 @@
0.6.0
0.6.1

View 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> {
}
}

View file

@ -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',
},

View file

@ -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",

View file

@ -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 {
}
}
}
}

View 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);
});
});

View file

@ -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();
}
}

View file

@ -49,6 +49,7 @@ export default class PostgresqlQueryService implements QueryService {
database: sourceOptions.database,
password: sourceOptions.password,
port: sourceOptions.port,
ssl: { rejectUnauthorized: false },
});
}

View file

@ -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);
}
});
});
}

View file

@ -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);
}
});
});
}

View file

@ -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],
})

View file

@ -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 = {

View file

@ -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 });
}
}

View file

@ -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();
}
}
}

View file

@ -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;
}
}
}

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