PR review
This commit is contained in:
commit
fa66c07578
|
@ -0,0 +1,9 @@
|
|||
packages/server/node_modules
|
||||
packages/builder
|
||||
packages/frontend-core
|
||||
packages/backend-core
|
||||
packages/worker/node_modules
|
||||
packages/cli
|
||||
packages/client
|
||||
packages/bbui
|
||||
packages/string-templates
|
|
@ -93,6 +93,8 @@ then `cd ` into your local copy.
|
|||
|
||||
#### 3. Install and Build
|
||||
|
||||
| **NOTE**: On Windows, all yarn commands must be executed on a bash shell (e.g. git bash)
|
||||
|
||||
To develop the Budibase platform you'll need [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) installed.
|
||||
|
||||
##### Quick method
|
||||
|
|
|
@ -7,6 +7,15 @@ assignees: ''
|
|||
|
||||
---
|
||||
|
||||
**Hosting**
|
||||
<!-- Delete as appropriate -->
|
||||
- Self
|
||||
- Method: <method> <!-- One of: k8s, docker single image, docker compose, digital ocean: -->
|
||||
- Budibase Version: <version> <!-- e.g. 1.0.105 -->
|
||||
- App Version: <version> <!-- Indicate app version if bug is related to an application -->
|
||||
- Cloud
|
||||
- Tenant ID: <tenantId> <!-- shown in URL as <tenantID>.budibase.app -->
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
|
|
|
@ -12,6 +12,11 @@ on:
|
|||
- master
|
||||
- develop
|
||||
|
||||
env:
|
||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
BASE_BRANCH: ${{ github.event.pull_request.base.ref}}
|
||||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -27,6 +32,10 @@ jobs:
|
|||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install Pro
|
||||
run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
- run: yarn lint
|
||||
|
|
|
@ -19,6 +19,7 @@ env:
|
|||
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
|
||||
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
||||
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
|
||||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
|
@ -29,6 +30,10 @@ jobs:
|
|||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
||||
- name: Install Pro
|
||||
run: yarn install:pro develop
|
||||
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
- run: yarn lint
|
||||
|
@ -46,9 +51,9 @@ jobs:
|
|||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default
|
||||
git config user.name "Budibase Staging Release Bot"
|
||||
git config user.email "<>"
|
||||
# setup the username and email.
|
||||
git config --global user.name "Budibase Staging Release Bot"
|
||||
git config --global user.email "<>"
|
||||
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
||||
yarn release:develop
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ env:
|
|||
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
||||
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
|
@ -30,6 +31,10 @@ jobs:
|
|||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
||||
- name: Install Pro
|
||||
run: yarn install:pro master
|
||||
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
- run: yarn lint
|
||||
|
|
|
@ -28,6 +28,7 @@ jobs:
|
|||
|
||||
- name: Cypress run
|
||||
id: cypress
|
||||
continue-on-error: true
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
install: false
|
||||
|
|
|
@ -11,7 +11,7 @@ sources:
|
|||
- https://github.com/Budibase/budibase
|
||||
- https://budibase.com
|
||||
type: application
|
||||
version: 0.2.8
|
||||
version: 0.2.9
|
||||
appVersion: 1.0.48
|
||||
dependencies:
|
||||
- name: couchdb
|
||||
|
|
|
@ -98,10 +98,6 @@ spec:
|
|||
value: http://worker-service:{{ .Values.services.worker.port }}
|
||||
- name: PLATFORM_URL
|
||||
value: {{ .Values.globals.platformUrl | quote }}
|
||||
- name: USE_QUOTAS
|
||||
value: {{ .Values.globals.useQuotas | quote }}
|
||||
- name: EXCLUDE_QUOTAS_TENANTS
|
||||
value: {{ .Values.globals.excludeQuotasTenants | quote }}
|
||||
- name: ACCOUNT_PORTAL_URL
|
||||
value: {{ .Values.globals.accountPortalUrl | quote }}
|
||||
- name: ACCOUNT_PORTAL_API_KEY
|
||||
|
@ -114,12 +110,23 @@ spec:
|
|||
value: {{ .Values.globals.google.clientId | quote }}
|
||||
- name: GOOGLE_CLIENT_SECRET
|
||||
value: {{ .Values.globals.google.secret | quote }}
|
||||
- name: AUTOMATION_MAX_ITERATIONS
|
||||
value: {{ .Values.globals.automationMaxIterations | quote }}
|
||||
|
||||
image: budibase/apps:{{ .Values.globals.appVersion }}
|
||||
imagePullPolicy: Always
|
||||
name: bbapps
|
||||
ports:
|
||||
- containerPort: {{ .Values.services.apps.port }}
|
||||
resources: {}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
restartPolicy: Always
|
||||
serviceAccountName: ""
|
||||
status: {}
|
||||
|
|
|
@ -39,5 +39,13 @@ spec:
|
|||
imagePullPolicy: Always
|
||||
name: couchdb-backup
|
||||
resources: {}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
status: {}
|
||||
{{- end }}
|
||||
|
|
|
@ -60,6 +60,14 @@ spec:
|
|||
volumeMounts:
|
||||
- mountPath: /data
|
||||
name: minio-data
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
restartPolicy: Always
|
||||
serviceAccountName: ""
|
||||
volumes:
|
||||
|
|
|
@ -32,6 +32,14 @@ spec:
|
|||
- containerPort: {{ .Values.services.proxy.port }}
|
||||
resources: {}
|
||||
volumeMounts:
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
restartPolicy: Always
|
||||
serviceAccountName: ""
|
||||
volumes:
|
||||
|
|
|
@ -39,6 +39,14 @@ spec:
|
|||
volumeMounts:
|
||||
- mountPath: /data
|
||||
name: redis-data
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
restartPolicy: Always
|
||||
serviceAccountName: ""
|
||||
volumes:
|
||||
|
|
|
@ -121,6 +121,14 @@ spec:
|
|||
ports:
|
||||
- containerPort: {{ .Values.services.worker.port }}
|
||||
resources: {}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
restartPolicy: Always
|
||||
serviceAccountName: ""
|
||||
status: {}
|
||||
|
|
|
@ -93,8 +93,6 @@ globals:
|
|||
logLevel: info
|
||||
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
|
||||
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
|
||||
useQuotas: "0"
|
||||
excludeQuotasTenants: "" # comma seperated list of tenants to exclude from quotas
|
||||
accountPortalUrl: ""
|
||||
accountPortalApiKey: ""
|
||||
cookieDomain: ""
|
||||
|
@ -103,6 +101,7 @@ globals:
|
|||
google:
|
||||
clientId: ""
|
||||
secret: ""
|
||||
automationMaxIterations: "500"
|
||||
|
||||
createSecrets: true # creates an internal API key, JWT secrets and redis password for you
|
||||
|
||||
|
@ -230,6 +229,8 @@ couchdb:
|
|||
## Optional tolerations
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
service:
|
||||
# annotations:
|
||||
enabled: true
|
||||
|
|
|
@ -1894,9 +1894,9 @@ minimist-options@4.1.0:
|
|||
kind-of "^6.0.3"
|
||||
|
||||
minimist@^1.2.0:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
minipass-collect@^1.0.2:
|
||||
version "1.0.2"
|
||||
|
|
|
@ -117,7 +117,6 @@ services:
|
|||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
|
||||
|
||||
volumes:
|
||||
couchdb3_data:
|
||||
driver: local
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"version": "2",
|
||||
"templates": [
|
||||
{
|
||||
"type": 3,
|
||||
"title": "Budibase",
|
||||
"categories": ["Tools"],
|
||||
"description": "Build modern business apps in minutes",
|
||||
"logo": "https://budibase.com/favicon.ico",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"url": "https://github.com/Budibase/budibase",
|
||||
"stackfile": "hosting/docker-compose.yaml"
|
||||
},
|
||||
"env": [
|
||||
{
|
||||
"name": "MAIN_PORT",
|
||||
"label": "Main port",
|
||||
"default": "10000"
|
||||
},
|
||||
{
|
||||
"name": "JWT_SECRET",
|
||||
"label": "JWT secret",
|
||||
"default": "change-me"
|
||||
},
|
||||
{
|
||||
"name": "MINIO_ACCESS_KEY",
|
||||
"label": "MinIO access key",
|
||||
"default": "change-me"
|
||||
},
|
||||
{
|
||||
"name": "MINIO_SECRET_KEY",
|
||||
"label": "MinIO secret key",
|
||||
"default": "change-me"
|
||||
},
|
||||
{
|
||||
"name": "COUCH_DB_USER",
|
||||
"default": "budibase",
|
||||
"preset": true
|
||||
},
|
||||
{
|
||||
"name": "COUCH_DB_PASSWORD",
|
||||
"label": "Couch DB password",
|
||||
"default": "change-me"
|
||||
},
|
||||
{
|
||||
"name": "REDIS_PASSWORD",
|
||||
"label": "Redis password",
|
||||
"default": "change-me"
|
||||
},
|
||||
{
|
||||
"name": "INTERNAL_API_KEY",
|
||||
"label": "Internal API key",
|
||||
"default": "change-me"
|
||||
},
|
||||
{
|
||||
"name": "APP_PORT",
|
||||
"default": "4002",
|
||||
"preset": true
|
||||
},
|
||||
{
|
||||
"name": "WORKER_PORT",
|
||||
"default": "4003",
|
||||
"preset": true
|
||||
},
|
||||
{
|
||||
"name": "MINIO_PORT",
|
||||
"default": "4004",
|
||||
"preset": true
|
||||
},
|
||||
{
|
||||
"name": "COUCH_DB_PORT",
|
||||
"default": "4005",
|
||||
"preset": true
|
||||
},
|
||||
{
|
||||
"name": "REDIS_PORT",
|
||||
"default": "6379",
|
||||
"preset": true
|
||||
},
|
||||
{
|
||||
"name": "WATCHTOWER_PORT",
|
||||
"default": "6161",
|
||||
"preset": true
|
||||
},
|
||||
{
|
||||
"name": "BUDIBASE_ENVIRONMENT",
|
||||
"default": "PRODUCTION",
|
||||
"preset": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
FROM couchdb
|
||||
|
||||
ENV COUCHDB_PASSWORD=budibase
|
||||
ENV COUCHDB_USER=budibase
|
||||
ENV COUCH_DB_URL=http://budibase:budibase@localhost:5984
|
||||
ENV BUDIBASE_ENVIRONMENT=PRODUCTION
|
||||
ENV MINIO_URL=http://localhost:9000
|
||||
ENV REDIS_URL=localhost:6379
|
||||
ENV WORKER_URL=http://localhost:4002
|
||||
ENV INTERNAL_API_KEY=budibase
|
||||
ENV JWT_SECRET=testsecret
|
||||
ENV MINIO_ACCESS_KEY=budibase
|
||||
ENV MINIO_SECRET_KEY=budibase
|
||||
ENV SELF_HOSTED=1
|
||||
ENV CLUSTER_PORT=10000
|
||||
ENV REDIS_PASSWORD=budibase
|
||||
ENV ARCHITECTURE=amd
|
||||
ENV APP_PORT=4001
|
||||
ENV WORKER_PORT=4002
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install software-properties-common wget nginx -y
|
||||
RUN apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main'
|
||||
RUN apt-get update
|
||||
|
||||
# setup nginx
|
||||
ADD hosting/single/nginx.conf /etc/nginx
|
||||
RUN mkdir /etc/nginx/logs
|
||||
RUN useradd www
|
||||
RUN touch /etc/nginx/logs/error.log
|
||||
RUN touch /etc/nginx/logs/nginx.pid
|
||||
|
||||
# install java
|
||||
RUN apt-get install openjdk-8-jdk -y
|
||||
|
||||
# setup nodejs
|
||||
WORKDIR /nodejs
|
||||
RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh
|
||||
RUN bash /tmp/nodesource_setup.sh
|
||||
RUN apt-get install nodejs
|
||||
RUN npm install --global yarn
|
||||
RUN npm install --global pm2
|
||||
|
||||
# setup redis
|
||||
RUN apt install redis-server -y
|
||||
|
||||
# setup server
|
||||
WORKDIR /app
|
||||
ADD packages/server .
|
||||
RUN ls -al
|
||||
RUN yarn
|
||||
RUN yarn build
|
||||
# Install client for oracle datasource
|
||||
RUN apt-get install unzip libaio1
|
||||
RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh
|
||||
|
||||
# setup worker
|
||||
WORKDIR /worker
|
||||
ADD packages/worker .
|
||||
RUN yarn
|
||||
RUN yarn build
|
||||
|
||||
# setup clouseau
|
||||
WORKDIR /
|
||||
RUN wget https://github.com/cloudant-labs/clouseau/releases/download/2.21.0/clouseau-2.21.0-dist.zip
|
||||
RUN unzip clouseau-2.21.0-dist.zip
|
||||
RUN mv clouseau-2.21.0 /opt/clouseau
|
||||
RUN rm clouseau-2.21.0-dist.zip
|
||||
|
||||
WORKDIR /opt/clouseau
|
||||
RUN mkdir ./bin
|
||||
ADD hosting/single/clouseau ./bin/
|
||||
ADD hosting/single/log4j.properties .
|
||||
ADD hosting/single/clouseau.ini .
|
||||
RUN chmod +x ./bin/clouseau
|
||||
|
||||
# setup CouchDB
|
||||
WORKDIR /opt/couchdb
|
||||
ADD hosting/single/vm.args ./etc/
|
||||
|
||||
# setup minio
|
||||
WORKDIR /minio
|
||||
RUN wget https://dl.min.io/server/minio/release/linux-${ARCHITECTURE}64/minio
|
||||
RUN chmod +x minio
|
||||
|
||||
# setup runner file
|
||||
WORKDIR /
|
||||
ADD hosting/single/runner.sh .
|
||||
RUN chmod +x ./runner.sh
|
||||
|
||||
EXPOSE 10000
|
||||
VOLUME /opt/couchdb/data
|
||||
VOLUME /minio
|
||||
|
||||
# must set this just before running
|
||||
ENV NODE_ENV=production
|
||||
CMD ["./runner.sh"]
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/sh
|
||||
/usr/bin/java -server \
|
||||
-Xmx2G \
|
||||
-Dsun.net.inetaddr.ttl=30 \
|
||||
-Dsun.net.inetaddr.negative.ttl=30 \
|
||||
-Dlog4j.configuration=file:/opt/clouseau/log4j.properties \
|
||||
-XX:OnOutOfMemoryError="kill -9 %p" \
|
||||
-XX:+UseConcMarkSweepGC \
|
||||
-XX:+CMSParallelRemarkEnabled \
|
||||
-classpath '/opt/clouseau/*' \
|
||||
com.cloudant.clouseau.Main \
|
||||
/opt/clouseau/clouseau.ini
|
|
@ -0,0 +1,13 @@
|
|||
[clouseau]
|
||||
|
||||
; the name of the Erlang node created by the service, leave this unchanged
|
||||
name=clouseau@127.0.0.1
|
||||
|
||||
; set this to the same distributed Erlang cookie used by the CouchDB nodes
|
||||
cookie=monster
|
||||
|
||||
; the path where you would like to store the search index files
|
||||
dir=/opt/couchdb/data/search
|
||||
|
||||
; the number of search indexes that can be open simultaneously
|
||||
max_indexes_open=500
|
|
@ -0,0 +1,4 @@
|
|||
log4j.rootLogger=debug, CONSOLE
|
||||
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
|
||||
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
|
||||
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %c [%p] %m%n
|
|
@ -0,0 +1,116 @@
|
|||
user www www;
|
||||
error_log /etc/nginx/logs/error.log;
|
||||
pid /etc/nginx/logs/nginx.pid;
|
||||
worker_processes auto;
|
||||
worker_rlimit_nofile 8192;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s;
|
||||
proxy_set_header Host $host;
|
||||
charset utf-8;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
server_tokens off;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
# buffering
|
||||
client_header_buffer_size 1k;
|
||||
client_max_body_size 20M;
|
||||
ignore_invalid_headers off;
|
||||
proxy_buffering off;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default "upgrade";
|
||||
}
|
||||
|
||||
server {
|
||||
listen 10000 default_server;
|
||||
listen [::]:10000 default_server;
|
||||
server_name _;
|
||||
client_max_body_size 1000m;
|
||||
ignore_invalid_headers off;
|
||||
proxy_buffering off;
|
||||
# port_in_redirect off;
|
||||
|
||||
location /app {
|
||||
proxy_pass http://127.0.0.1:4001;
|
||||
}
|
||||
|
||||
location = / {
|
||||
proxy_pass http://127.0.0.1:4001;
|
||||
}
|
||||
|
||||
location ~ ^/(builder|app_) {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://127.0.0.1:4001;
|
||||
}
|
||||
|
||||
location ~ ^/api/(system|admin|global)/ {
|
||||
proxy_pass http://127.0.0.1:4002;
|
||||
}
|
||||
|
||||
location /worker/ {
|
||||
proxy_pass http://127.0.0.1:4002;
|
||||
rewrite ^/worker/(.*)$ /$1 break;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
# calls to the API are rate limited with bursting
|
||||
limit_req zone=ratelimit burst=20 nodelay;
|
||||
|
||||
# 120s timeout on API requests
|
||||
proxy_read_timeout 120s;
|
||||
proxy_connect_timeout 120s;
|
||||
proxy_send_timeout 120s;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_pass http://127.0.0.1:4001;
|
||||
}
|
||||
|
||||
location /db/ {
|
||||
proxy_pass http://127.0.0.1:5984;
|
||||
rewrite ^/db/(.*)$ /$1 break;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
proxy_connect_timeout 300;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
chunked_transfer_encoding off;
|
||||
proxy_pass http://127.0.0.1:9000;
|
||||
}
|
||||
|
||||
client_header_timeout 60;
|
||||
client_body_timeout 60;
|
||||
keepalive_timeout 60;
|
||||
|
||||
# gzip
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
redis-server --requirepass $REDIS_PASSWORD &
|
||||
/opt/clouseau/bin/clouseau &
|
||||
/minio/minio server /minio &
|
||||
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
|
||||
/etc/init.d/nginx restart
|
||||
pushd app
|
||||
pm2 start --name app "yarn run:docker"
|
||||
popd
|
||||
pushd worker
|
||||
pm2 start --name worker "yarn run:docker"
|
||||
popd
|
||||
sleep 10
|
||||
URL=http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984
|
||||
curl -X PUT ${URL}/_users
|
||||
curl -X PUT ${URL}/_replicator
|
||||
sleep infinity
|
|
@ -0,0 +1,32 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
# use this file except in compliance with the License. You may obtain a copy of
|
||||
# the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under
|
||||
# the License.
|
||||
|
||||
# erlang cookie for clouseau security
|
||||
-name couchdb@127.0.0.1
|
||||
-setcookie monster
|
||||
|
||||
# Ensure that the Erlang VM listens on a known port
|
||||
-kernel inet_dist_listen_min 9100
|
||||
-kernel inet_dist_listen_max 9100
|
||||
|
||||
# Tell kernel and SASL not to log anything
|
||||
-kernel error_logger silent
|
||||
-sasl sasl_error_logger false
|
||||
|
||||
# Use kernel poll functionality if supported by emulator
|
||||
+K true
|
||||
|
||||
# Start a pool of asynchronous IO threads
|
||||
+A 16
|
||||
|
||||
# Comment this line out to enable the interactive Erlang shell on startup
|
||||
+Bd -noinput
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.0.105-alpha.0",
|
||||
"version": "1.0.105-alpha.35",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
16
package.json
16
package.json
|
@ -21,18 +21,17 @@
|
|||
},
|
||||
"scripts": {
|
||||
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
||||
"bootstrap": "lerna link && lerna bootstrap",
|
||||
"bootstrap": "lerna link && lerna bootstrap && ./scripts/link-dependencies.sh",
|
||||
"build": "lerna run build",
|
||||
"publishdev": "lerna run publishdev",
|
||||
"publishnpm": "yarn build && lerna publish --force-publish",
|
||||
"release": "lerna publish patch --yes --force-publish",
|
||||
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop",
|
||||
"release": "lerna publish patch --yes --force-publish && yarn release:pro",
|
||||
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop && yarn release:pro:develop",
|
||||
"release:pro": "bash scripts/pro/release.sh",
|
||||
"release:pro:develop": "bash scripts/pro/release.sh develop",
|
||||
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
||||
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
||||
"nuke:packages": "yarn run restore",
|
||||
"nuke:docker": "lerna run --parallel dev:stack:nuke",
|
||||
"clean": "lerna clean",
|
||||
"kill-port": "kill-port 4001",
|
||||
"kill-builder": "kill-port 3000",
|
||||
"kill-server": "kill-port 4001 4002",
|
||||
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
||||
|
@ -58,6 +57,8 @@
|
|||
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
||||
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
|
||||
"build:docker:single:image": "docker build -f hosting/single/Dockerfile -t budibase:latest .",
|
||||
"build:docker:single": "lerna run build && lerna run predocker && npm run build:docker:single:image",
|
||||
"build:docs": "lerna run build:docs",
|
||||
"release:helm": "node scripts/releaseHelmChart",
|
||||
"env:multi:enable": "lerna run env:multi:enable",
|
||||
|
@ -72,6 +73,7 @@
|
|||
"mode:cloud": "yarn env:selfhost:disable && yarn env:multi:enable && yarn env:account:disable",
|
||||
"mode:account": "yarn mode:cloud && yarn env:account:enable",
|
||||
"security:audit": "node scripts/audit.js",
|
||||
"postinstall": "husky install"
|
||||
"postinstall": "husky install",
|
||||
"install:pro": "bash scripts/pro/install.sh"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "1.0.105-alpha.0",
|
||||
"version": "1.0.105-alpha.35",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "src/index.js",
|
||||
"author": "Budibase",
|
||||
|
|
|
@ -13,9 +13,11 @@ exports.Cookies = {
|
|||
|
||||
exports.Headers = {
|
||||
API_KEY: "x-budibase-api-key",
|
||||
LICENSE_KEY: "x-budibase-license-key",
|
||||
API_VER: "x-budibase-api-version",
|
||||
APP_ID: "x-budibase-app-id",
|
||||
TYPE: "x-budibase-type",
|
||||
PREVIEW_ROLE: "x-budibase-role",
|
||||
TENANT_ID: "x-budibase-tenant-id",
|
||||
TOKEN: "x-budibase-token",
|
||||
CSRF_TOKEN: "x-csrf-token",
|
||||
|
|
|
@ -23,6 +23,7 @@ exports.StaticDatabases = {
|
|||
docs: {
|
||||
apiKeys: "apikeys",
|
||||
usageQuota: "usage_quota",
|
||||
licenseInfo: "license_info",
|
||||
},
|
||||
},
|
||||
// contains information about tenancy and so on
|
||||
|
|
|
@ -27,6 +27,7 @@ const UNICODE_MAX = "\ufff0"
|
|||
exports.ViewNames = {
|
||||
USER_BY_EMAIL: "by_email",
|
||||
BY_API_KEY: "by_api_key",
|
||||
USER_BY_BUILDERS: "by_builders",
|
||||
}
|
||||
|
||||
exports.StaticDatabases = StaticDatabases
|
||||
|
@ -429,34 +430,9 @@ async function getScopedConfig(db, params) {
|
|||
return configDoc && configDoc.config ? configDoc.config : configDoc
|
||||
}
|
||||
|
||||
function generateNewUsageQuotaDoc() {
|
||||
return {
|
||||
_id: StaticDatabases.GLOBAL.docs.usageQuota,
|
||||
quotaReset: Date.now() + 2592000000,
|
||||
usageQuota: {
|
||||
automationRuns: 0,
|
||||
rows: 0,
|
||||
storage: 0,
|
||||
apps: 0,
|
||||
users: 0,
|
||||
views: 0,
|
||||
emails: 0,
|
||||
},
|
||||
usageLimits: {
|
||||
automationRuns: 1000,
|
||||
rows: 4000,
|
||||
apps: 4,
|
||||
storage: 1000,
|
||||
users: 10,
|
||||
emails: 50,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
exports.Replication = Replication
|
||||
exports.getScopedConfig = getScopedConfig
|
||||
exports.generateConfigID = generateConfigID
|
||||
exports.getConfigParams = getConfigParams
|
||||
exports.getScopedFullConfig = getScopedFullConfig
|
||||
exports.generateNewUsageQuotaDoc = generateNewUsageQuotaDoc
|
||||
exports.generateDevInfoID = generateDevInfoID
|
||||
|
|
|
@ -56,10 +56,34 @@ exports.createApiKeyView = async () => {
|
|||
await db.put(designDoc)
|
||||
}
|
||||
|
||||
exports.createUserBuildersView = async () => {
|
||||
const db = getGlobalDB()
|
||||
let designDoc
|
||||
try {
|
||||
designDoc = await db.get("_design/database")
|
||||
} catch (err) {
|
||||
// no design doc, make one
|
||||
designDoc = DesignDoc()
|
||||
}
|
||||
const view = {
|
||||
map: `function(doc) {
|
||||
if (doc.builder && doc.builder.global === true) {
|
||||
emit(doc._id, doc._id)
|
||||
}
|
||||
}`,
|
||||
}
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
[ViewNames.USER_BY_BUILDERS]: view,
|
||||
}
|
||||
await db.put(designDoc)
|
||||
}
|
||||
|
||||
exports.queryGlobalView = async (viewName, params, db = null) => {
|
||||
const CreateFuncByName = {
|
||||
[ViewNames.USER_BY_EMAIL]: exports.createUserEmailView,
|
||||
[ViewNames.BY_API_KEY]: exports.createApiKeyView,
|
||||
[ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView,
|
||||
}
|
||||
// can pass DB in if working with something specific
|
||||
if (!db) {
|
||||
|
|
|
@ -28,6 +28,7 @@ module.exports = {
|
|||
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
|
||||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
||||
PLATFORM_URL: process.env.PLATFORM_URL,
|
||||
TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS,
|
||||
isTest,
|
||||
_set(key, value) {
|
||||
process.env[key] = value
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
class BudibaseError extends Error {
|
||||
constructor(message, type, code) {
|
||||
super(message)
|
||||
this.type = type
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BudibaseError,
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
const licensing = require("./licensing")
|
||||
|
||||
const codes = {
|
||||
...licensing.codes,
|
||||
}
|
||||
|
||||
const types = {
|
||||
...licensing.types,
|
||||
}
|
||||
|
||||
const context = {
|
||||
...licensing.context,
|
||||
}
|
||||
|
||||
const getPublicError = err => {
|
||||
let error
|
||||
if (err.code || err.type) {
|
||||
// add generic error information
|
||||
error = {
|
||||
code: err.code,
|
||||
type: err.type,
|
||||
}
|
||||
|
||||
if (err.code && context[err.code]) {
|
||||
error = {
|
||||
...error,
|
||||
// get any additional context from this error
|
||||
...context[err.code](err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return error
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
codes,
|
||||
types,
|
||||
UsageLimitError: licensing.UsageLimitError,
|
||||
getPublicError,
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
const { BudibaseError } = require("./base")
|
||||
|
||||
const types = {
|
||||
LICENSE_ERROR: "license_error",
|
||||
}
|
||||
|
||||
const codes = {
|
||||
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
||||
}
|
||||
|
||||
const context = {
|
||||
[codes.USAGE_LIMIT_EXCEEDED]: err => {
|
||||
return {
|
||||
limitName: err.limitName,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
class UsageLimitError extends BudibaseError {
|
||||
constructor(message, limitName) {
|
||||
super(message, types.LICENSE_ERROR, codes.USAGE_LIMIT_EXCEEDED)
|
||||
this.limitName = limitName
|
||||
this.status = 400
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
types,
|
||||
codes,
|
||||
context,
|
||||
UsageLimitError,
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
const env = require("../environment")
|
||||
const tenancy = require("../tenancy")
|
||||
|
||||
/**
|
||||
* Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant.
|
||||
* The env var is formatted as:
|
||||
* tenant1:feature1:feature2,tenant2:feature1
|
||||
*/
|
||||
const getFeatureFlags = () => {
|
||||
if (!env.TENANT_FEATURE_FLAGS) {
|
||||
return
|
||||
}
|
||||
|
||||
const tenantFeatureFlags = {}
|
||||
|
||||
env.TENANT_FEATURE_FLAGS.split(",").forEach(tenantToFeatures => {
|
||||
const [tenantId, ...features] = tenantToFeatures.split(":")
|
||||
|
||||
features.forEach(feature => {
|
||||
if (!tenantFeatureFlags[tenantId]) {
|
||||
tenantFeatureFlags[tenantId] = []
|
||||
}
|
||||
tenantFeatureFlags[tenantId].push(feature)
|
||||
})
|
||||
})
|
||||
|
||||
return tenantFeatureFlags
|
||||
}
|
||||
|
||||
const TENANT_FEATURE_FLAGS = getFeatureFlags()
|
||||
|
||||
exports.isEnabled = featureFlag => {
|
||||
const tenantId = tenancy.getTenantId()
|
||||
|
||||
return (
|
||||
TENANT_FEATURE_FLAGS &&
|
||||
TENANT_FEATURE_FLAGS[tenantId] &&
|
||||
TENANT_FEATURE_FLAGS[tenantId].includes(featureFlag)
|
||||
)
|
||||
}
|
||||
|
||||
exports.getTenantFeatureFlags = tenantId => {
|
||||
if (TENANT_FEATURE_FLAGS && TENANT_FEATURE_FLAGS[tenantId]) {
|
||||
return TENANT_FEATURE_FLAGS[tenantId]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
exports.FeatureFlag = {
|
||||
LICENSING: "LICENSING",
|
||||
}
|
|
@ -15,4 +15,9 @@ module.exports = {
|
|||
auth: require("../auth"),
|
||||
constants: require("../constants"),
|
||||
migrations: require("../migrations"),
|
||||
errors: require("./errors"),
|
||||
env: require("./environment"),
|
||||
accounts: require("./cloud/accounts"),
|
||||
tenancy: require("./tenancy"),
|
||||
featureFlags: require("./featureFlags"),
|
||||
}
|
||||
|
|
|
@ -2,24 +2,27 @@ const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
|||
|
||||
const { authenticateThirdParty } = require("./third-party-common")
|
||||
|
||||
async function authenticate(accessToken, refreshToken, profile, done) {
|
||||
const thirdPartyUser = {
|
||||
provider: profile.provider, // should always be 'google'
|
||||
providerType: "google",
|
||||
userId: profile.id,
|
||||
profile: profile,
|
||||
email: profile._json.email,
|
||||
oauth2: {
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
},
|
||||
}
|
||||
const buildVerifyFn = async saveUserFn => {
|
||||
return (accessToken, refreshToken, profile, done) => {
|
||||
const thirdPartyUser = {
|
||||
provider: profile.provider, // should always be 'google'
|
||||
providerType: "google",
|
||||
userId: profile.id,
|
||||
profile: profile,
|
||||
email: profile._json.email,
|
||||
oauth2: {
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
},
|
||||
}
|
||||
|
||||
return authenticateThirdParty(
|
||||
thirdPartyUser,
|
||||
true, // require local accounts to exist
|
||||
done
|
||||
)
|
||||
return authenticateThirdParty(
|
||||
thirdPartyUser,
|
||||
true, // require local accounts to exist
|
||||
done,
|
||||
saveUserFn
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,11 +30,7 @@ async function authenticate(accessToken, refreshToken, profile, done) {
|
|||
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
||||
* @returns Dynamically configured Passport Google Strategy
|
||||
*/
|
||||
exports.strategyFactory = async function (
|
||||
config,
|
||||
callbackUrl,
|
||||
verify = authenticate
|
||||
) {
|
||||
exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
|
||||
try {
|
||||
const { clientID, clientSecret } = config
|
||||
|
||||
|
@ -41,6 +40,7 @@ exports.strategyFactory = async function (
|
|||
)
|
||||
}
|
||||
|
||||
const verify = buildVerifyFn(saveUserFn)
|
||||
return new GoogleStrategy(
|
||||
{
|
||||
clientID: config.clientID,
|
||||
|
@ -58,4 +58,4 @@ exports.strategyFactory = async function (
|
|||
}
|
||||
}
|
||||
// expose for testing
|
||||
exports.authenticate = authenticate
|
||||
exports.buildVerifyFn = buildVerifyFn
|
||||
|
|
|
@ -2,46 +2,49 @@ const fetch = require("node-fetch")
|
|||
const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
|
||||
const { authenticateThirdParty } = require("./third-party-common")
|
||||
|
||||
/**
|
||||
* @param {*} issuer The identity provider base URL
|
||||
* @param {*} sub The user ID
|
||||
* @param {*} profile The user profile information. Created by passport from the /userinfo response
|
||||
* @param {*} jwtClaims The parsed id_token claims
|
||||
* @param {*} accessToken The access_token for contacting the identity provider - may or may not be a JWT
|
||||
* @param {*} refreshToken The refresh_token for obtaining a new access_token - usually not a JWT
|
||||
* @param {*} idToken The id_token - always a JWT
|
||||
* @param {*} params The response body from requesting an access_token
|
||||
* @param {*} done The passport callback: err, user, info
|
||||
*/
|
||||
async function authenticate(
|
||||
issuer,
|
||||
sub,
|
||||
profile,
|
||||
jwtClaims,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
idToken,
|
||||
params,
|
||||
done
|
||||
) {
|
||||
const thirdPartyUser = {
|
||||
// store the issuer info to enable sync in future
|
||||
provider: issuer,
|
||||
providerType: "oidc",
|
||||
userId: profile.id,
|
||||
profile: profile,
|
||||
email: getEmail(profile, jwtClaims),
|
||||
oauth2: {
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
},
|
||||
}
|
||||
|
||||
return authenticateThirdParty(
|
||||
thirdPartyUser,
|
||||
false, // don't require local accounts to exist
|
||||
const buildVerifyFn = saveUserFn => {
|
||||
/**
|
||||
* @param {*} issuer The identity provider base URL
|
||||
* @param {*} sub The user ID
|
||||
* @param {*} profile The user profile information. Created by passport from the /userinfo response
|
||||
* @param {*} jwtClaims The parsed id_token claims
|
||||
* @param {*} accessToken The access_token for contacting the identity provider - may or may not be a JWT
|
||||
* @param {*} refreshToken The refresh_token for obtaining a new access_token - usually not a JWT
|
||||
* @param {*} idToken The id_token - always a JWT
|
||||
* @param {*} params The response body from requesting an access_token
|
||||
* @param {*} done The passport callback: err, user, info
|
||||
*/
|
||||
return async (
|
||||
issuer,
|
||||
sub,
|
||||
profile,
|
||||
jwtClaims,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
idToken,
|
||||
params,
|
||||
done
|
||||
)
|
||||
) => {
|
||||
const thirdPartyUser = {
|
||||
// store the issuer info to enable sync in future
|
||||
provider: issuer,
|
||||
providerType: "oidc",
|
||||
userId: profile.id,
|
||||
profile: profile,
|
||||
email: getEmail(profile, jwtClaims),
|
||||
oauth2: {
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
},
|
||||
}
|
||||
|
||||
return authenticateThirdParty(
|
||||
thirdPartyUser,
|
||||
false, // don't require local accounts to exist
|
||||
done,
|
||||
saveUserFn
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -86,7 +89,7 @@ function validEmail(value) {
|
|||
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
||||
* @returns Dynamically configured Passport OIDC Strategy
|
||||
*/
|
||||
exports.strategyFactory = async function (config, callbackUrl) {
|
||||
exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
|
||||
try {
|
||||
const { clientID, clientSecret, configUrl } = config
|
||||
|
||||
|
@ -106,6 +109,7 @@ exports.strategyFactory = async function (config, callbackUrl) {
|
|||
|
||||
const body = await response.json()
|
||||
|
||||
const verify = buildVerifyFn(saveUserFn)
|
||||
return new OIDCStrategy(
|
||||
{
|
||||
issuer: body.issuer,
|
||||
|
@ -116,7 +120,7 @@ exports.strategyFactory = async function (config, callbackUrl) {
|
|||
clientSecret: clientSecret,
|
||||
callbackURL: callbackUrl,
|
||||
},
|
||||
authenticate
|
||||
verify
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
@ -125,4 +129,4 @@ exports.strategyFactory = async function (config, callbackUrl) {
|
|||
}
|
||||
|
||||
// expose for testing
|
||||
exports.authenticate = authenticate
|
||||
exports.buildVerifyFn = buildVerifyFn
|
||||
|
|
|
@ -58,8 +58,10 @@ describe("google", () => {
|
|||
|
||||
it("delegates authentication to third party common", async () => {
|
||||
const google = require("../google")
|
||||
const mockSaveUserFn = jest.fn()
|
||||
const authenticate = await google.buildVerifyFn(mockSaveUserFn)
|
||||
|
||||
await google.authenticate(
|
||||
await authenticate(
|
||||
data.accessToken,
|
||||
data.refreshToken,
|
||||
profile,
|
||||
|
@ -69,7 +71,8 @@ describe("google", () => {
|
|||
expect(authenticateThirdParty).toHaveBeenCalledWith(
|
||||
user,
|
||||
true,
|
||||
mockDone)
|
||||
mockDone,
|
||||
mockSaveUserFn)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -83,8 +83,10 @@ describe("oidc", () => {
|
|||
|
||||
async function doAuthenticate() {
|
||||
const oidc = require("../oidc")
|
||||
const mockSaveUserFn = jest.fn()
|
||||
const authenticate = await oidc.buildVerifyFn(mockSaveUserFn)
|
||||
|
||||
await oidc.authenticate(
|
||||
await authenticate(
|
||||
issuer,
|
||||
sub,
|
||||
profile,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
const env = require("../../environment")
|
||||
const jwt = require("jsonwebtoken")
|
||||
const { generateGlobalUserID } = require("../../db/utils")
|
||||
const { saveUser } = require("../../utils")
|
||||
const { authError } = require("./utils")
|
||||
const { newid } = require("../../hashing")
|
||||
const { createASession } = require("../../security/sessions")
|
||||
|
@ -16,8 +15,11 @@ exports.authenticateThirdParty = async function (
|
|||
thirdPartyUser,
|
||||
requireLocalAccount = true,
|
||||
done,
|
||||
saveUserFn = saveUser
|
||||
saveUserFn
|
||||
) {
|
||||
if (!saveUserFn) {
|
||||
throw new Error("Save user function must be provided")
|
||||
}
|
||||
if (!thirdPartyUser.provider) {
|
||||
return authError(done, "third party user provider required")
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ exports.Databases = {
|
|||
FLAGS: "flags",
|
||||
APP_METADATA: "appMetadata",
|
||||
QUERY_VARS: "queryVars",
|
||||
LICENSES: "license",
|
||||
}
|
||||
|
||||
exports.SEPARATOR = SEPARATOR
|
||||
|
|
|
@ -14,22 +14,7 @@ function makeSessionID(userId, sessionId) {
|
|||
return `${userId}/${sessionId}`
|
||||
}
|
||||
|
||||
exports.createASession = async (userId, session) => {
|
||||
const client = await redis.getSessionClient()
|
||||
const sessionId = session.sessionId
|
||||
if (!session.csrfToken) {
|
||||
session.csrfToken = uuidv4()
|
||||
}
|
||||
session = {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccessedAt: new Date().toISOString(),
|
||||
...session,
|
||||
userId,
|
||||
}
|
||||
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
|
||||
}
|
||||
|
||||
exports.invalidateSessions = async (userId, sessionIds = null) => {
|
||||
async function invalidateSessions(userId, sessionIds = null) {
|
||||
let sessions = []
|
||||
|
||||
// If no sessionIds, get all the sessions for the user
|
||||
|
@ -55,6 +40,24 @@ exports.invalidateSessions = async (userId, sessionIds = null) => {
|
|||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
exports.createASession = async (userId, session) => {
|
||||
// invalidate all other sessions
|
||||
await invalidateSessions(userId)
|
||||
|
||||
const client = await redis.getSessionClient()
|
||||
const sessionId = session.sessionId
|
||||
if (!session.csrfToken) {
|
||||
session.csrfToken = uuidv4()
|
||||
}
|
||||
session = {
|
||||
createdAt: new Date().toISOString(),
|
||||
lastAccessedAt: new Date().toISOString(),
|
||||
...session,
|
||||
userId,
|
||||
}
|
||||
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
|
||||
}
|
||||
|
||||
exports.updateSessionTTL = async session => {
|
||||
const client = await redis.getSessionClient()
|
||||
const key = makeSessionID(session.userId, session.sessionId)
|
||||
|
@ -67,8 +70,6 @@ exports.endSession = async (userId, sessionId) => {
|
|||
await client.delete(makeSessionID(userId, sessionId))
|
||||
}
|
||||
|
||||
exports.getUserSessions = getSessionsForUser
|
||||
|
||||
exports.getSession = async (userId, sessionId) => {
|
||||
try {
|
||||
const client = await redis.getSessionClient()
|
||||
|
@ -84,3 +85,6 @@ exports.getAllSessions = async () => {
|
|||
const sessions = await client.scan()
|
||||
return sessions.map(session => session.value)
|
||||
}
|
||||
|
||||
exports.getUserSessions = getSessionsForUser
|
||||
exports.invalidateSessions = invalidateSessions
|
||||
|
|
|
@ -176,6 +176,13 @@ exports.getGlobalUserByEmail = async email => {
|
|||
})
|
||||
}
|
||||
|
||||
exports.getBuildersCount = async () => {
|
||||
const builders = await queryGlobalView(ViewNames.USER_BY_BUILDERS, {
|
||||
include_docs: false,
|
||||
})
|
||||
return builders ? builders.length : 0
|
||||
}
|
||||
|
||||
exports.saveUser = async (
|
||||
user,
|
||||
tenantId,
|
||||
|
@ -289,4 +296,5 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
|
|||
userId,
|
||||
sessions.map(({ sessionId }) => sessionId)
|
||||
)
|
||||
await userCache.invalidateUser(userId)
|
||||
}
|
||||
|
|
|
@ -3338,9 +3338,9 @@ mimic-fn@^2.1.0:
|
|||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
mixin-deep@^1.2.0:
|
||||
version "1.3.2"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.0.105-alpha.0",
|
||||
"version": "1.0.105-alpha.35",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,7 +38,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||
"@budibase/string-templates": "^1.0.105-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.105-alpha.35",
|
||||
"@spectrum-css/actionbutton": "^1.0.1",
|
||||
"@spectrum-css/actiongroup": "^1.0.1",
|
||||
"@spectrum-css/avatar": "^3.0.2",
|
||||
|
|
|
@ -80,8 +80,4 @@
|
|||
.active svg {
|
||||
color: var(--spectrum-global-color-blue-600);
|
||||
}
|
||||
|
||||
.spectrum-ActionButton-label {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -19,18 +19,33 @@
|
|||
const dispatch = createEventDispatcher()
|
||||
const flatpickrId = `${uuid()}-wrapper`
|
||||
let open = false
|
||||
let flatpickr, flatpickrOptions, isTimeOnly
|
||||
let flatpickr, flatpickrOptions
|
||||
|
||||
const resolveTimeStamp = timestamp => {
|
||||
let maskedDate = new Date(`0-${timestamp}`)
|
||||
|
||||
if (maskedDate instanceof Date && !isNaN(maskedDate.getTime())) {
|
||||
return maskedDate
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
$: isTimeOnly = !timeOnly && value ? !isNaN(new Date(`0-${value}`)) : timeOnly
|
||||
$: flatpickrOptions = {
|
||||
element: `#${flatpickrId}`,
|
||||
enableTime: isTimeOnly || enableTime || false,
|
||||
noCalendar: isTimeOnly || false,
|
||||
enableTime: timeOnly || enableTime || false,
|
||||
noCalendar: timeOnly || false,
|
||||
altInput: true,
|
||||
altFormat: isTimeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
|
||||
altFormat: timeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
|
||||
wrap: true,
|
||||
appendTo,
|
||||
disableMobile: "true",
|
||||
onReady: () => {
|
||||
let timestamp = resolveTimeStamp(value)
|
||||
if (timeOnly && timestamp) {
|
||||
dispatch("change", timestamp.toISOString())
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const handleChange = event => {
|
||||
|
@ -39,10 +54,9 @@
|
|||
if (newValue) {
|
||||
newValue = newValue.toISOString()
|
||||
}
|
||||
// if time only set date component to today
|
||||
// if time only set date component to 2000-01-01
|
||||
if (timeOnly) {
|
||||
const todayDate = new Date().toISOString().split("T")[0]
|
||||
newValue = `${todayDate}T${newValue.split("T")[1]}`
|
||||
newValue = `2000-01-01T${newValue.split("T")[1]}`
|
||||
}
|
||||
dispatch("change", newValue)
|
||||
}
|
||||
|
@ -76,10 +90,13 @@
|
|||
return null
|
||||
}
|
||||
let date
|
||||
let time = new Date(`0-${val}`)
|
||||
let time
|
||||
|
||||
// it is a string like 00:00:00, just time
|
||||
if (timeOnly || (typeof val === "string" && !isNaN(time))) {
|
||||
date = time
|
||||
let ts = resolveTimeStamp(val)
|
||||
|
||||
if (timeOnly && ts) {
|
||||
date = ts
|
||||
} else if (val instanceof Date) {
|
||||
// Use real date obj if already parsed
|
||||
date = val
|
||||
|
@ -101,7 +118,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{#key isTimeOnly}
|
||||
{#key timeOnly}
|
||||
<Flatpickr
|
||||
bind:flatpickr
|
||||
value={parseDate(value)}
|
||||
|
|
|
@ -36,6 +36,10 @@
|
|||
padding-left: var(--spacing-l);
|
||||
padding-right: var(--spacing-l);
|
||||
}
|
||||
.paddingX-XL {
|
||||
padding-left: var(--spacing-xl);
|
||||
padding-right: var(--spacing-xl);
|
||||
}
|
||||
.paddingY-S {
|
||||
padding-top: var(--spacing-s);
|
||||
padding-bottom: var(--spacing-s);
|
||||
|
@ -48,6 +52,10 @@
|
|||
padding-top: var(--spacing-l);
|
||||
padding-bottom: var(--spacing-l);
|
||||
}
|
||||
.paddingY-XL {
|
||||
padding-top: var(--spacing-xl);
|
||||
padding-bottom: var(--spacing-xl);
|
||||
}
|
||||
.gap-XXS {
|
||||
grid-gap: var(--spacing-xs);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<script>
|
||||
export let wide = false
|
||||
export let maxWidth = "80ch"
|
||||
</script>
|
||||
|
||||
<div class:wide>
|
||||
<div style="--max-width: {maxWidth}" class:wide>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
|
@ -12,7 +13,7 @@
|
|||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
max-width: 80ch;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: calc(var(--spacing-xl) * 2);
|
||||
min-height: calc(100% - var(--spacing-xl) * 4);
|
||||
|
|
|
@ -16,11 +16,11 @@
|
|||
easing: easing,
|
||||
})
|
||||
|
||||
$: if (value) $progress = value
|
||||
$: if (value || value === 0) $progress = value
|
||||
</script>
|
||||
|
||||
<div
|
||||
class:spectrum-ProgressBar--indeterminate={!value}
|
||||
class:spectrum-ProgressBar--indeterminate={!value && value !== 0}
|
||||
class:spectrum-ProgressBar--sideLabel={sideLabel}
|
||||
class="spectrum-ProgressBar spectrum-ProgressBar--size{size}"
|
||||
value={$progress}
|
||||
|
@ -28,7 +28,7 @@
|
|||
aria-valuenow={$progress}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
style={width ? `width: ${width}px;` : ""}
|
||||
style={width ? `width: ${width};` : ""}
|
||||
>
|
||||
{#if $$slots}
|
||||
<div
|
||||
|
@ -37,7 +37,7 @@
|
|||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
{#if value}
|
||||
{#if value || value === 0}
|
||||
<div
|
||||
class="spectrum-FieldLabel spectrum-ProgressBar-percentage spectrum-FieldLabel--size{size}"
|
||||
>
|
||||
|
@ -47,7 +47,7 @@
|
|||
<div class="spectrum-ProgressBar-track">
|
||||
<div
|
||||
class="spectrum-ProgressBar-fill"
|
||||
style={value ? `width: ${$progress}%` : ""}
|
||||
style={value || value === 0 ? `width: ${$progress}%` : ""}
|
||||
/>
|
||||
</div>
|
||||
<div class="spectrum-ProgressBar-label" hidden="" />
|
||||
|
|
|
@ -1,42 +1,21 @@
|
|||
<script>
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import { copyToClipboard } from "../helpers"
|
||||
import { notifications } from "../Stores/notifications"
|
||||
|
||||
export let value
|
||||
|
||||
const onClick = e => {
|
||||
const onClick = async e => {
|
||||
e.stopPropagation()
|
||||
copyToClipboard(value)
|
||||
}
|
||||
|
||||
const copyToClipboard = value => {
|
||||
return new Promise(res => {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// Try using the clipboard API first
|
||||
navigator.clipboard.writeText(value).then(res)
|
||||
} else {
|
||||
// Fall back to the textarea hack
|
||||
let textArea = document.createElement("textarea")
|
||||
textArea.value = value
|
||||
textArea.style.position = "fixed"
|
||||
textArea.style.left = "-9999px"
|
||||
textArea.style.top = "-9999px"
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
document.execCommand("copy")
|
||||
textArea.remove()
|
||||
res()
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
notifications.success("Copied to clipboard")
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.error(
|
||||
"Failed to copy to clipboard. Check the dev console for the value."
|
||||
)
|
||||
console.warn("Failed to copy the value", value)
|
||||
})
|
||||
try {
|
||||
await copyToClipboard(value)
|
||||
notifications.success("Copied to clipboard")
|
||||
} catch (error) {
|
||||
notifications.error(
|
||||
"Failed to copy to clipboard. Check the dev console for the value."
|
||||
)
|
||||
console.warn("Failed to copy the value", value)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
export let disableSorting = false
|
||||
export let autoSortColumns = true
|
||||
export let compact = false
|
||||
export let customPlaceholder = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -387,13 +388,24 @@
|
|||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="placeholder" class:placeholder--no-fields={!fields?.length}>
|
||||
<div class="placeholder-content">
|
||||
<svg class="spectrum-Icon spectrum-Icon--sizeXXL" focusable="false">
|
||||
<use xlink:href="#spectrum-icon-18-Table" />
|
||||
</svg>
|
||||
<div>No rows found</div>
|
||||
</div>
|
||||
<div
|
||||
class="placeholder"
|
||||
class:placeholder--custom={customPlaceholder}
|
||||
class:placeholder--no-fields={!fields?.length}
|
||||
>
|
||||
{#if customPlaceholder}
|
||||
<slot name="placeholder" />
|
||||
{:else}
|
||||
<div class="placeholder-content">
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--sizeXXL"
|
||||
focusable="false"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-Table" />
|
||||
</svg>
|
||||
<div>No rows found</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -458,6 +470,13 @@
|
|||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
border-top: var(--table-border);
|
||||
}
|
||||
.spectrum-Table-headCell:first-of-type {
|
||||
border-left: var(--table-border);
|
||||
}
|
||||
.spectrum-Table-headCell:last-of-type {
|
||||
border-right: var(--table-border);
|
||||
}
|
||||
.spectrum-Table-headCell--alignCenter {
|
||||
justify-content: center;
|
||||
|
@ -576,16 +595,19 @@
|
|||
border-top: none;
|
||||
grid-column: 1 / -1;
|
||||
background-color: var(--table-bg);
|
||||
padding: 40px;
|
||||
}
|
||||
.placeholder--no-fields {
|
||||
border-top: var(--table-border);
|
||||
}
|
||||
.placeholder--custom {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.wrapper--quiet .placeholder {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
.placeholder-content {
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
|
|
@ -108,7 +108,7 @@
|
|||
padding-left: var(--spacing-xl);
|
||||
padding-right: var(--spacing-xl);
|
||||
position: relative;
|
||||
border-bottom: var(--border-light);
|
||||
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.spectrum-Tabs-content {
|
||||
margin-top: var(--spectrum-global-dimension-static-size-150);
|
||||
|
|
|
@ -5,12 +5,14 @@
|
|||
export let serif = false
|
||||
export let weight = null
|
||||
export let textAlign = null
|
||||
export let color = null
|
||||
</script>
|
||||
|
||||
<p
|
||||
style={`
|
||||
${weight ? `font-weight:${weight};` : ""}
|
||||
${textAlign ? `text-align:${textAlign};` : ""}
|
||||
${color ? `color:${color};` : ""}
|
||||
`}
|
||||
class="spectrum-Body spectrum-Body--size{size}"
|
||||
class:spectrum-Body--serif={serif}
|
||||
|
|
|
@ -5,12 +5,13 @@
|
|||
export let size = "M"
|
||||
export let textAlign
|
||||
export let noPadding = false
|
||||
export let weight = "default" // light, heavy, default
|
||||
</script>
|
||||
|
||||
<h1
|
||||
style={textAlign ? `text-align:${textAlign}` : ``}
|
||||
class:noPadding
|
||||
class="spectrum-Heading spectrum-Heading--size{size}"
|
||||
class="spectrum-Heading spectrum-Heading--size{size} spectrum-Heading--{weight}"
|
||||
>
|
||||
<slot />
|
||||
</h1>
|
||||
|
|
|
@ -106,3 +106,29 @@ export const deepSet = (obj, key, value) => {
|
|||
export const cloneDeep = obj => {
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a value to the clipboard
|
||||
* @param value the value to copy
|
||||
*/
|
||||
export const copyToClipboard = value => {
|
||||
return new Promise(res => {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// Try using the clipboard API first
|
||||
navigator.clipboard.writeText(value).then(res)
|
||||
} else {
|
||||
// Fall back to the textarea hack
|
||||
let textArea = document.createElement("textarea")
|
||||
textArea.value = value
|
||||
textArea.style.position = "fixed"
|
||||
textArea.style.left = "-9999px"
|
||||
textArea.style.top = "-9999px"
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
document.execCommand("copy")
|
||||
textArea.remove()
|
||||
res()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1574,9 +1574,9 @@ minimatch@^3.0.4:
|
|||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
mkdirp@~0.5.1:
|
||||
version "0.5.5"
|
||||
|
|
|
@ -25,9 +25,13 @@ filterTests(['smoke', 'all'], () => {
|
|||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
|
||||
if (Cypress.env("TEST_ENV")) {
|
||||
cy.get(".spectrum-Button").contains("Templates").click({force: true})
|
||||
}
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||
.its("body")
|
||||
.then(val => {
|
||||
if (val.length > 0) {
|
||||
cy.get(".spectrum-Button").contains("Templates").click({force: true})
|
||||
}
|
||||
})
|
||||
|
||||
cy.get(".template-category-filters").should("exist")
|
||||
cy.get(".template-categories").should("exist")
|
||||
|
|
|
@ -20,7 +20,6 @@ filterTests(['smoke', 'all'], () => {
|
|||
})
|
||||
|
||||
// Setup trigger
|
||||
cy.contains("Setup").click()
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
cy.wait(500)
|
||||
cy.contains("dog").click()
|
||||
|
@ -32,12 +31,11 @@ filterTests(['smoke', 'all'], () => {
|
|||
cy.contains("Create Row").trigger('mouseover').click().click()
|
||||
cy.get(".spectrum-Button--cta").click()
|
||||
})
|
||||
cy.contains("Setup").click()
|
||||
cy.get(".spectrum-Picker-label").eq(1).click()
|
||||
cy.contains("dog").click()
|
||||
cy.get(".spectrum-Textfield-input")
|
||||
.first()
|
||||
.type("{{ trigger.row.name }}", { parseSpecialCharSequences: false })
|
||||
.first()
|
||||
.type("{{ trigger.row.name }}", { parseSpecialCharSequences: false })
|
||||
cy.get(".spectrum-Textfield-input")
|
||||
.eq(1)
|
||||
.type("11")
|
||||
|
|
|
@ -55,13 +55,14 @@ filterTests(["smoke", "all"], () => {
|
|||
|
||||
if (Cypress.env("TEST_ENV")) {
|
||||
// No Pagination in CI - Test env only for the next two tests
|
||||
it("Adds 15 rows and checks pagination", () => {
|
||||
xit("Adds 15 rows and checks pagination", () => {
|
||||
// 10 rows per page, 15 rows should create 2 pages within table
|
||||
const totalRows = 16
|
||||
for (let i = 1; i < totalRows; i++) {
|
||||
cy.addRow([i])
|
||||
}
|
||||
cy.wait(1000)
|
||||
cy.reload()
|
||||
cy.wait(2000)
|
||||
cy.get(".spectrum-Pagination").within(() => {
|
||||
cy.get(".spectrum-ActionButton").eq(1).click()
|
||||
})
|
||||
|
@ -70,13 +71,13 @@ filterTests(["smoke", "all"], () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("Deletes rows and checks pagination", () => {
|
||||
// Delete rows, removing second page of rows from table
|
||||
const deleteRows = 5
|
||||
xit("Deletes rows and checks pagination", () => {
|
||||
// Delete rows, removing second page from table
|
||||
cy.get(".spectrum-Checkbox-input").check({ force: true })
|
||||
cy.get(".spectrum-Table")
|
||||
cy.contains("Delete 5 row(s)").click()
|
||||
cy.get(".spectrum-Modal").contains("Delete").click()
|
||||
cy.get(".popovers").within(() => {
|
||||
cy.get(".spectrum-Button").click({ force: true })
|
||||
})
|
||||
cy.get(".spectrum-Dialog-grid").contains("Delete").click({ force: true })
|
||||
cy.wait(1000)
|
||||
|
||||
// Confirm table only has one page
|
||||
|
|
|
@ -19,6 +19,7 @@ filterTests(["all"], () => {
|
|||
cy.get(".spectrum-Button")
|
||||
.contains("Save and fetch tables")
|
||||
.click({ force: true })
|
||||
cy.wait(500)
|
||||
// Intercept Request after button click & apply assertions
|
||||
cy.wait("@datasource")
|
||||
cy.get("@datasource")
|
||||
|
@ -31,6 +32,7 @@ filterTests(["all"], () => {
|
|||
cy.get("@datasource")
|
||||
.its("response.body")
|
||||
.should("have.property", "status", 500)
|
||||
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||
})
|
||||
|
||||
it("should add MySQL data source and fetch tables", () => {
|
||||
|
@ -72,10 +74,13 @@ filterTests(["all"], () => {
|
|||
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
|
||||
cy.get(".spectrum-Picker").eq(4).click()
|
||||
cy.get(".spectrum-Popover").contains("REGION_ID").click()
|
||||
// Save relationship & reload page
|
||||
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||
cy.reload()
|
||||
})
|
||||
// Save relationship & reload page
|
||||
cy.get(".spectrum-ButtonGroup").within(() => {
|
||||
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||
})
|
||||
cy.reload()
|
||||
|
||||
// Confirm table length & column name
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(1)
|
||||
|
@ -131,7 +136,7 @@ filterTests(["all"], () => {
|
|||
cy.get(".spectrum-Table")
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Table-row").eq(0).click()
|
||||
cy.get(".spectrum-Table-row").eq(0).click({ force: true })
|
||||
cy.wait(500)
|
||||
})
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
|
@ -175,11 +180,12 @@ filterTests(["all"], () => {
|
|||
})
|
||||
|
||||
it("should duplicate a query", () => {
|
||||
// Get last nav item - The query
|
||||
/// Get query nav item - QueryName
|
||||
cy.get(".nav-item")
|
||||
.last()
|
||||
.contains(queryName)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get(".icon").eq(1).click({ force: true })
|
||||
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
||||
})
|
||||
// Select and confirm duplication
|
||||
cy.get(".spectrum-Menu").contains("Duplicate").click()
|
||||
|
@ -199,23 +205,21 @@ filterTests(["all"], () => {
|
|||
})
|
||||
|
||||
it("should delete a query", () => {
|
||||
// Get last nav item - The query
|
||||
for (let i = 0; i < 2; i++) {
|
||||
cy.get(".nav-item")
|
||||
.last()
|
||||
.within(() => {
|
||||
cy.get(".icon").eq(1).click({ force: true })
|
||||
})
|
||||
// Select Delete
|
||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Delete Query")
|
||||
.click({ force: true })
|
||||
cy.wait(1000)
|
||||
}
|
||||
// Get query nav item - QueryName
|
||||
cy.get(".nav-item")
|
||||
.contains(queryName)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
||||
})
|
||||
// Select Delete
|
||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Delete Query")
|
||||
.click({ force: true })
|
||||
cy.wait(1000)
|
||||
// Confirm deletion
|
||||
cy.get(".nav-item").should("not.contain", queryName)
|
||||
cy.get(".nav-item").should("not.contain", queryRename)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -46,9 +46,10 @@ filterTests(["all"], () => {
|
|||
cy.get("@datasource")
|
||||
.its("response.body")
|
||||
.should("have.property", "status", 500)
|
||||
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||
})
|
||||
|
||||
it("should add Oracle data source and fetch tables", () => {
|
||||
xit("should add Oracle data source and fetch tables", () => {
|
||||
// Add & configure Oracle data source
|
||||
cy.selectExternalDatasource(datasource)
|
||||
cy.intercept("**/datasources").as("datasource")
|
||||
|
@ -64,7 +65,7 @@ filterTests(["all"], () => {
|
|||
.should("be.gt", 0)
|
||||
})
|
||||
|
||||
it("should define a One relationship type", () => {
|
||||
xit("should define a One relationship type", () => {
|
||||
// Select relationship type & configure
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Define relationship")
|
||||
|
@ -93,7 +94,7 @@ filterTests(["all"], () => {
|
|||
cy.get(".spectrum-Table-cell").should("contain", "COUNTRIES to REGIONS")
|
||||
})
|
||||
|
||||
it("should define a Many relationship type", () => {
|
||||
xit("should define a Many relationship type", () => {
|
||||
// Select relationship type & configure
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Define relationship")
|
||||
|
@ -127,7 +128,7 @@ filterTests(["all"], () => {
|
|||
)
|
||||
})
|
||||
|
||||
it("should delete relationships", () => {
|
||||
xit("should delete relationships", () => {
|
||||
// Delete both relationships
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(1)
|
||||
|
@ -156,7 +157,7 @@ filterTests(["all"], () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("should add a query", () => {
|
||||
xit("should add a query", () => {
|
||||
// Add query
|
||||
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
|
||||
cy.get(".spectrum-Form-item")
|
||||
|
@ -181,7 +182,7 @@ filterTests(["all"], () => {
|
|||
cy.get(".nav-item").should("contain", queryName)
|
||||
})
|
||||
|
||||
it("should duplicate a query", () => {
|
||||
xit("should duplicate a query", () => {
|
||||
// Get query nav item
|
||||
cy.get(".nav-item")
|
||||
.contains(queryName)
|
||||
|
@ -194,7 +195,7 @@ filterTests(["all"], () => {
|
|||
cy.get(".nav-item").should("contain", queryName + " (1)")
|
||||
})
|
||||
|
||||
it("should edit a query name", () => {
|
||||
xit("should edit a query name", () => {
|
||||
// Rename query
|
||||
cy.get(".spectrum-Form-item")
|
||||
.eq(0)
|
||||
|
@ -206,7 +207,7 @@ filterTests(["all"], () => {
|
|||
cy.get(".nav-item").should("contain", queryRename)
|
||||
})
|
||||
|
||||
it("should delete a query", () => {
|
||||
xit("should delete a query", () => {
|
||||
// Get query nav item - QueryName
|
||||
cy.get(".nav-item")
|
||||
.contains(queryName)
|
||||
|
|
|
@ -21,16 +21,10 @@ filterTests(["all"], () => {
|
|||
.click({ force: true })
|
||||
// Intercept Request after button click & apply assertions
|
||||
cy.wait("@datasource")
|
||||
cy.get("@datasource")
|
||||
.its("response.body")
|
||||
.should(
|
||||
"have.property",
|
||||
"message",
|
||||
"connect ECONNREFUSED 127.0.0.1:5432"
|
||||
)
|
||||
cy.get("@datasource")
|
||||
.its("response.body")
|
||||
.should("have.property", "status", 500)
|
||||
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||
})
|
||||
|
||||
it("should add PostgreSQL data source and fetch tables", () => {
|
||||
|
@ -113,13 +107,13 @@ filterTests(["all"], () => {
|
|||
})
|
||||
|
||||
it("should delete a relationship", () => {
|
||||
cy.get(".hierarchy-items-container").contains(datasource).click()
|
||||
cy.get(".hierarchy-items-container").contains("PostgreSQL-2").click()
|
||||
cy.reload()
|
||||
// Delete one relationship
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Table-row").eq(0).click()
|
||||
cy.get(".spectrum-Table-row").eq(0).click({ force: true })
|
||||
cy.wait(500)
|
||||
})
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
|
@ -161,7 +155,7 @@ filterTests(["all"], () => {
|
|||
|
||||
it("should switch to schema with no tables", () => {
|
||||
// Switch Schema - To one without any tables
|
||||
cy.get(".hierarchy-items-container").contains(datasource).click()
|
||||
cy.get(".hierarchy-items-container").contains("PostgreSQL-2").click()
|
||||
switchSchema("randomText")
|
||||
|
||||
// No tables displayed
|
||||
|
@ -208,11 +202,12 @@ filterTests(["all"], () => {
|
|||
})
|
||||
|
||||
it("should duplicate a query", () => {
|
||||
// Get last nav item - The query
|
||||
// Locate previously created query
|
||||
cy.get(".nav-item")
|
||||
.last()
|
||||
.contains(queryName)
|
||||
.siblings(".actions")
|
||||
.within(() => {
|
||||
cy.get(".icon").eq(1).click({ force: true })
|
||||
cy.get(".icon").click({ force: true })
|
||||
})
|
||||
// Select and confirm duplication
|
||||
cy.get(".spectrum-Menu").contains("Duplicate").click()
|
||||
|
@ -240,23 +235,21 @@ filterTests(["all"], () => {
|
|||
})
|
||||
|
||||
it("should delete a query", () => {
|
||||
// Get last nav item - The query
|
||||
for (let i = 0; i < 2; i++) {
|
||||
cy.get(".nav-item")
|
||||
.last()
|
||||
.within(() => {
|
||||
cy.get(".icon").eq(1).click({ force: true })
|
||||
})
|
||||
// Select Delete
|
||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Delete Query")
|
||||
.click({ force: true })
|
||||
cy.wait(1000)
|
||||
}
|
||||
// Get query nav item - QueryName
|
||||
cy.get(".nav-item")
|
||||
.contains(queryName)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
||||
})
|
||||
// Select Delete
|
||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Delete Query")
|
||||
.click({ force: true })
|
||||
cy.wait(1000)
|
||||
// Confirm deletion
|
||||
cy.get(".nav-item").should("not.contain", queryName)
|
||||
cy.get(".nav-item").should("not.contain", queryRename)
|
||||
})
|
||||
|
||||
const switchSchema = schema => {
|
||||
|
|
|
@ -60,43 +60,48 @@ Cypress.Commands.add("deleteApp", name => {
|
|||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||
.its("body")
|
||||
.then(val => {
|
||||
if (val.length > 0) {
|
||||
if (Cypress.env("TEST_ENV")) {
|
||||
cy.searchForApplication(name)
|
||||
cy.get(".appTable").within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click()
|
||||
const findAppName = val.some(val => val.name == name)
|
||||
if (findAppName) {
|
||||
if (val.length > 0) {
|
||||
if (Cypress.env("TEST_ENV")) {
|
||||
cy.searchForApplication(name)
|
||||
cy.get(".appTable").within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click()
|
||||
})
|
||||
} else {
|
||||
const appId = val.reduce((acc, app) => {
|
||||
if (name === app.name) {
|
||||
acc = app.appId
|
||||
}
|
||||
return acc
|
||||
}, "")
|
||||
|
||||
if (appId == "") {
|
||||
return
|
||||
}
|
||||
|
||||
const appIdParsed = appId.split("_").pop()
|
||||
const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
|
||||
cy.get(actionEleId).within(() => {
|
||||
cy.get(".spectrum-Icon").eq(0).click()
|
||||
})
|
||||
}
|
||||
|
||||
cy.get(".spectrum-Menu").then($menu => {
|
||||
if ($menu.text().includes("Unpublish")) {
|
||||
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
||||
} else {
|
||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get("input").type(name)
|
||||
})
|
||||
cy.get(".spectrum-Button--warning").click()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const appId = val.reduce((acc, app) => {
|
||||
if (name === app.name) {
|
||||
acc = app.appId
|
||||
}
|
||||
return acc
|
||||
}, "")
|
||||
|
||||
if (appId == "") {
|
||||
return
|
||||
}
|
||||
|
||||
const appIdParsed = appId.split("_").pop()
|
||||
const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
|
||||
cy.get(actionEleId).within(() => {
|
||||
cy.get(".spectrum-Icon").eq(0).click()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
cy.get(".spectrum-Menu").then($menu => {
|
||||
if ($menu.text().includes("Unpublish")) {
|
||||
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
||||
} else {
|
||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get("input").type(name)
|
||||
})
|
||||
cy.get(".spectrum-Button--warning").click()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
@ -410,7 +415,9 @@ Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => {
|
|||
if (datasource == "Oracle") {
|
||||
cy.get("input").clear().type(Cypress.env("oracle").HOST)
|
||||
} else {
|
||||
cy.get("input").clear().type(Cypress.env("HOST_IP"))
|
||||
cy.get("input")
|
||||
.clear({ force: true })
|
||||
.type(Cypress.env("mysql").HOST, { force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.0.105-alpha.0",
|
||||
"version": "1.0.105-alpha.35",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -65,10 +65,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.105-alpha.0",
|
||||
"@budibase/client": "^1.0.105-alpha.0",
|
||||
"@budibase/frontend-core": "^1.0.105-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.105-alpha.0",
|
||||
"@budibase/bbui": "^1.0.105-alpha.35",
|
||||
"@budibase/client": "^1.0.105-alpha.35",
|
||||
"@budibase/frontend-core": "^1.0.105-alpha.35",
|
||||
"@budibase/string-templates": "^1.0.105-alpha.35",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -7,7 +7,11 @@ import {
|
|||
getComponentSettings,
|
||||
} from "./componentUtils"
|
||||
import { store } from "builderStore"
|
||||
import { queries as queriesStores, tables as tablesStore } from "stores/backend"
|
||||
import {
|
||||
queries as queriesStores,
|
||||
tables as tablesStore,
|
||||
roles as rolesStore,
|
||||
} from "stores/backend"
|
||||
import {
|
||||
makePropSafe,
|
||||
isJSBinding,
|
||||
|
@ -33,6 +37,7 @@ export const getBindableProperties = (asset, componentId) => {
|
|||
const deviceBindings = getDeviceBindings()
|
||||
const stateBindings = getStateBindings()
|
||||
const selectedRowsBindings = getSelectedRowsBindings(asset)
|
||||
const roleBindings = getRoleBindings()
|
||||
return [
|
||||
...contextBindings,
|
||||
...urlBindings,
|
||||
|
@ -40,6 +45,7 @@ export const getBindableProperties = (asset, componentId) => {
|
|||
...userBindings,
|
||||
...deviceBindings,
|
||||
...selectedRowsBindings,
|
||||
...roleBindings,
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -391,6 +397,16 @@ const getUrlBindings = asset => {
|
|||
}))
|
||||
}
|
||||
|
||||
const getRoleBindings = () => {
|
||||
return (get(rolesStore) || []).map(role => {
|
||||
return {
|
||||
type: "context",
|
||||
runtimeBinding: `trim "${role._id}"`,
|
||||
readableBinding: `Role.${role.name}`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all bindable properties exposed in a button actions flow up until
|
||||
* the specified action ID, as well as context provided for the action
|
||||
|
@ -638,7 +654,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
|||
* Builds a form schema given a form component.
|
||||
* A form schema is a schema of all the fields nested anywhere within a form.
|
||||
*/
|
||||
const buildFormSchema = component => {
|
||||
export const buildFormSchema = component => {
|
||||
let schema = {}
|
||||
if (!component) {
|
||||
return schema
|
||||
|
|
|
@ -2,9 +2,15 @@ export default function (url) {
|
|||
return url
|
||||
.split("/")
|
||||
.map(part => {
|
||||
// if parameter, then use as is
|
||||
if (part.startsWith(":")) return part
|
||||
return encodeURIComponent(part.replace(/ /g, "-"))
|
||||
part = decodeURIComponent(part)
|
||||
part = part.replace(/ /g, "-")
|
||||
|
||||
// If parameter, then use as is
|
||||
if (!part.startsWith(":")) {
|
||||
part = encodeURIComponent(part)
|
||||
}
|
||||
|
||||
return part
|
||||
})
|
||||
.join("/")
|
||||
.toLowerCase()
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
if (v.internal) {
|
||||
acc[k] = v
|
||||
}
|
||||
delete acc.LOOP
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
|
|
|
@ -72,7 +72,9 @@
|
|||
animate:flip={{ duration: 500 }}
|
||||
in:fly|local={{ x: 500, duration: 1500 }}
|
||||
>
|
||||
<FlowItem {testDataModal} {block} />
|
||||
{#if block.stepId !== "LOOP"}
|
||||
<FlowItem {testDataModal} {block} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
Modal,
|
||||
Button,
|
||||
StatusLight,
|
||||
ActionButton,
|
||||
Select,
|
||||
ActionButton,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||
|
@ -25,8 +25,8 @@
|
|||
let webhookModal
|
||||
let actionModal
|
||||
let resultsModal
|
||||
let setupToggled
|
||||
let blockComplete
|
||||
let showLooping = false
|
||||
|
||||
$: rowControl = $automationStore.selectedAutomation.automation.rowControl
|
||||
$: showBindingPicker =
|
||||
|
@ -52,8 +52,21 @@
|
|||
block.schema?.inputs?.properties || {}
|
||||
).every(x => block?.inputs[x])
|
||||
|
||||
$: loopingSelected =
|
||||
$automationStore.selectedAutomation?.automation.definition.steps.find(
|
||||
x => x.blockToLoop === block.id
|
||||
)
|
||||
|
||||
async function deleteStep() {
|
||||
let loopBlock =
|
||||
$automationStore.selectedAutomation?.automation.definition.steps.find(
|
||||
x => x.blockToLoop === block.id
|
||||
)
|
||||
|
||||
try {
|
||||
if (loopBlock) {
|
||||
automationStore.actions.deleteAutomationBlock(loopBlock)
|
||||
}
|
||||
automationStore.actions.deleteAutomationBlock(block)
|
||||
await automationStore.actions.save(
|
||||
$automationStore.selectedAutomation?.automation
|
||||
|
@ -76,6 +89,23 @@
|
|||
)
|
||||
}
|
||||
|
||||
async function addLooping() {
|
||||
loopingSelected = true
|
||||
const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP
|
||||
|
||||
const loopBlock = $automationStore.selectedAutomation.constructBlock(
|
||||
"ACTION",
|
||||
"LOOP",
|
||||
loopDefinition
|
||||
)
|
||||
loopBlock.blockToLoop = block.id
|
||||
block.loopBlock = loopBlock.id
|
||||
automationStore.actions.addBlockToAutomation(loopBlock, blockIdx)
|
||||
await automationStore.actions.save(
|
||||
$automationStore.selectedAutomation?.automation
|
||||
)
|
||||
}
|
||||
|
||||
async function onSelect(block) {
|
||||
await automationStore.update(state => {
|
||||
state.selectedBlock = block
|
||||
|
@ -84,13 +114,68 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={`block ${block.type} hoverable`}
|
||||
class:selected
|
||||
on:click={() => {
|
||||
onSelect(block)
|
||||
}}
|
||||
>
|
||||
<div class={`block ${block.type} hoverable`} class:selected on:click={() => {}}>
|
||||
{#if loopingSelected}
|
||||
<div class="blockSection">
|
||||
<div
|
||||
on:click={() => {
|
||||
showLooping = !showLooping
|
||||
}}
|
||||
class="splitHeader"
|
||||
>
|
||||
<div class="center-items">
|
||||
<svg
|
||||
width="28px"
|
||||
height="28px"
|
||||
class="spectrum-Icon"
|
||||
style="color:grey;"
|
||||
focusable="false"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-Reuse" />
|
||||
</svg>
|
||||
<div class="iconAlign">
|
||||
<Detail size="S">Looping</Detail>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="blockTitle">
|
||||
<div
|
||||
style="margin-left: 10px;"
|
||||
on:click={() => {
|
||||
onSelect(block)
|
||||
}}
|
||||
>
|
||||
<Icon name={showLooping ? "ChevronDown" : "ChevronUp"} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider noMargin />
|
||||
{#if !showLooping}
|
||||
<div class="blockSection">
|
||||
<div class="block-options">
|
||||
<div class="delete-padding" on:click={() => deleteStep()}>
|
||||
<Icon name="DeleteOutline" />
|
||||
</div>
|
||||
</div>
|
||||
<Layout noPadding gap="S">
|
||||
<AutomationBlockSetup
|
||||
schemaProperties={Object.entries(
|
||||
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
|
||||
.properties
|
||||
)}
|
||||
block={$automationStore.selectedAutomation?.automation.definition.steps.find(
|
||||
x => x.blockToLoop === block.id
|
||||
)}
|
||||
{webhookModal}
|
||||
/>
|
||||
</Layout>
|
||||
</div>
|
||||
<Divider noMargin />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="blockSection">
|
||||
<div
|
||||
on:click={() => {
|
||||
|
@ -127,65 +212,66 @@
|
|||
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
|
||||
</div>
|
||||
</div>
|
||||
{#if testResult && testResult[0]}
|
||||
<span on:click={() => resultsModal.show()}>
|
||||
<StatusLight
|
||||
positive={isTrigger || testResult[0].outputs?.success}
|
||||
negative={!testResult[0].outputs?.success}
|
||||
><Body size="XS">View response</Body></StatusLight
|
||||
>
|
||||
</span>
|
||||
{/if}
|
||||
<div class="blockTitle">
|
||||
{#if testResult && testResult[0]}
|
||||
<div style="float: right;" on:click={() => resultsModal.show()}>
|
||||
<StatusLight
|
||||
positive={isTrigger || testResult[0].outputs?.success}
|
||||
negative={!testResult[0].outputs?.success}
|
||||
><Body size="XS">View response</Body></StatusLight
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
style="margin-left: 10px;"
|
||||
on:click={() => {
|
||||
onSelect(block)
|
||||
}}
|
||||
>
|
||||
<Icon name={blockComplete ? "ChevronDown" : "ChevronUp"} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if !blockComplete}
|
||||
<Divider noMargin />
|
||||
<div class="blockSection">
|
||||
<Layout noPadding gap="S">
|
||||
<div class="splitHeader">
|
||||
<ActionButton
|
||||
on:click={() => {
|
||||
onSelect(block)
|
||||
setupToggled = !setupToggled
|
||||
}}
|
||||
quiet
|
||||
icon={setupToggled ? "ChevronDown" : "ChevronRight"}
|
||||
>
|
||||
<Detail size="S">Setup</Detail>
|
||||
</ActionButton>
|
||||
{#if !isTrigger}
|
||||
{#if !isTrigger}
|
||||
<div>
|
||||
<div class="block-options">
|
||||
{#if showBindingPicker}
|
||||
<div>
|
||||
<Select
|
||||
on:change={toggleFieldControl}
|
||||
quiet
|
||||
defaultValue="Use values"
|
||||
autoWidth
|
||||
value={rowControl ? "Use bindings" : "Use values"}
|
||||
options={["Use values", "Use bindings"]}
|
||||
placeholder={null}
|
||||
/>
|
||||
</div>
|
||||
{#if !loopingSelected}
|
||||
<ActionButton on:click={() => addLooping()} icon="Reuse"
|
||||
>Add Looping</ActionButton
|
||||
>
|
||||
{/if}
|
||||
<div class="delete-padding" on:click={() => deleteStep()}>
|
||||
<Icon name="DeleteOutline" />
|
||||
</div>
|
||||
{#if showBindingPicker}
|
||||
<Select
|
||||
on:change={toggleFieldControl}
|
||||
defaultValue="Use values"
|
||||
autoWidth
|
||||
value={rowControl ? "Use bindings" : "Use values"}
|
||||
options={["Use values", "Use bindings"]}
|
||||
placeholder={null}
|
||||
/>
|
||||
{/if}
|
||||
<ActionButton
|
||||
on:click={() => deleteStep()}
|
||||
icon="DeleteOutline"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if setupToggled}
|
||||
<AutomationBlockSetup
|
||||
schemaProperties={Object.entries(block.schema.inputs.properties)}
|
||||
{block}
|
||||
{webhookModal}
|
||||
/>
|
||||
{#if lastStep}
|
||||
<Button on:click={() => testDataModal.show()} cta
|
||||
>Finish and test automation</Button
|
||||
>
|
||||
{/if}
|
||||
<AutomationBlockSetup
|
||||
schemaProperties={Object.entries(block.schema.inputs.properties)}
|
||||
{block}
|
||||
{webhookModal}
|
||||
/>
|
||||
{#if lastStep}
|
||||
<Button on:click={() => testDataModal.show()} cta
|
||||
>Finish and test automation</Button
|
||||
>
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
|
@ -220,8 +306,10 @@
|
|||
padding-left: 30px;
|
||||
}
|
||||
.block-options {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.center-items {
|
||||
display: flex;
|
||||
|
@ -256,4 +344,9 @@
|
|||
/* center horizontally */
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.blockTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { ModalContent, Icon, Detail, TextArea } from "@budibase/bbui"
|
||||
import { ModalContent, Icon, Detail, TextArea, Label } from "@budibase/bbui"
|
||||
|
||||
export let testResult
|
||||
export let isTrigger
|
||||
|
@ -10,7 +10,7 @@
|
|||
<ModalContent
|
||||
showCloseIcon={false}
|
||||
showConfirmButton={false}
|
||||
title="Test Automation"
|
||||
title="Test Results"
|
||||
cancelText="Close"
|
||||
>
|
||||
<div slot="header">
|
||||
|
@ -26,7 +26,18 @@
|
|||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{#if testResult[0].outputs.iterations}
|
||||
<div style="display: flex;">
|
||||
<Icon name="Reuse" />
|
||||
<div style="margin-left: 10px;">
|
||||
<Label>
|
||||
This loop ran {testResult[0].outputs.iterations} times.</Label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
<div
|
||||
on:click={() => {
|
||||
inputToggled = !inputToggled
|
||||
|
|
|
@ -88,33 +88,65 @@
|
|||
if (!block || !automation) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Find previous steps to the selected one
|
||||
let allSteps = [...automation.steps]
|
||||
|
||||
if (automation.trigger) {
|
||||
allSteps = [automation.trigger, ...allSteps]
|
||||
}
|
||||
const blockIdx = allSteps.findIndex(step => step.id === block.id)
|
||||
let blockIdx = allSteps.findIndex(step => step.id === block.id)
|
||||
|
||||
// Extract all outputs from all previous steps as available bindings
|
||||
// Extract all outputs from all previous steps as available bindins
|
||||
let bindings = []
|
||||
for (let idx = 0; idx < blockIdx; idx++) {
|
||||
const outputs = Object.entries(
|
||||
allSteps[idx].schema?.outputs?.properties ?? {}
|
||||
)
|
||||
let wasLoopBlock = allSteps[idx]?.stepId === "LOOP"
|
||||
let isLoopBlock =
|
||||
allSteps[idx]?.stepId === "LOOP" &&
|
||||
allSteps.find(x => x.blockToLoop === block.id)
|
||||
|
||||
// If the previous block was a loop block, decerement the index so the following
|
||||
// steps are in the correct order
|
||||
if (wasLoopBlock) {
|
||||
blockIdx--
|
||||
}
|
||||
|
||||
let schema = allSteps[idx]?.schema?.outputs?.properties ?? {}
|
||||
|
||||
// If its a Loop Block, we need to add this custom schema
|
||||
if (isLoopBlock) {
|
||||
schema = {
|
||||
currentItem: {
|
||||
type: "string",
|
||||
description: "the item currently being executed",
|
||||
},
|
||||
}
|
||||
}
|
||||
const outputs = Object.entries(schema)
|
||||
|
||||
bindings = bindings.concat(
|
||||
outputs.map(([name, value]) => {
|
||||
const runtime = idx === 0 ? `trigger.${name}` : `steps.${idx}.${name}`
|
||||
let runtimeName = isLoopBlock
|
||||
? `loop.${name}`
|
||||
: block.name.startsWith("JS")
|
||||
? `steps[${idx}].${name}`
|
||||
: `steps.${idx}.${name}`
|
||||
const runtime = idx === 0 ? `trigger.${name}` : runtimeName
|
||||
return {
|
||||
label: runtime,
|
||||
type: value.type,
|
||||
description: value.description,
|
||||
category: idx === 0 ? "Trigger outputs" : `Step ${idx} outputs`,
|
||||
category:
|
||||
idx === 0
|
||||
? "Trigger outputs"
|
||||
: isLoopBlock
|
||||
? "Loop Outputs"
|
||||
: `Step ${idx} outputs`,
|
||||
path: runtime,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return bindings
|
||||
}
|
||||
|
||||
|
@ -261,6 +293,14 @@
|
|||
value={inputData[key]}
|
||||
/>
|
||||
</CodeEditorModal>
|
||||
{:else if value.customType === "loopOption"}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
autoWidth
|
||||
value={inputData[key]}
|
||||
options={["Array", "String"]}
|
||||
defaultValue={"Array"}
|
||||
/>
|
||||
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
||||
{#if isTestModal}
|
||||
<ModalBindableInput
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
import Table from "./Table.svelte"
|
||||
import { TableNames } from "constants"
|
||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||
import { Pagination } from "@budibase/bbui"
|
||||
import { Pagination, Heading, Body, Layout } from "@budibase/bbui"
|
||||
import { fetchData } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
|
||||
|
@ -27,6 +27,8 @@
|
|||
$: enrichedSchema = enrichSchema($tables.selected?.schema)
|
||||
$: id = $tables.selected?._id
|
||||
$: fetch = createFetch(id)
|
||||
$: hasCols = checkHasCols(schema)
|
||||
$: hasRows = !!$fetch.rows?.length
|
||||
|
||||
const enrichSchema = schema => {
|
||||
let tempSchema = { ...schema }
|
||||
|
@ -47,6 +49,20 @@
|
|||
|
||||
return tempSchema
|
||||
}
|
||||
|
||||
const checkHasCols = schema => {
|
||||
if (!schema || Object.keys(schema).length === 0) {
|
||||
return false
|
||||
}
|
||||
let fields = Object.values(schema)
|
||||
for (let field of fields) {
|
||||
if (!field.autocolumn) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Fetches new data whenever the table changes
|
||||
const createFetch = tableId => {
|
||||
return fetchData({
|
||||
|
@ -104,40 +120,73 @@
|
|||
disableSorting
|
||||
on:updatecolumns={onUpdateColumns}
|
||||
on:updaterows={onUpdateRows}
|
||||
customPlaceholder
|
||||
>
|
||||
<CreateColumnButton on:updatecolumns={onUpdateColumns} />
|
||||
{#if schema && Object.keys(schema).length > 0}
|
||||
{#if !isUsersTable}
|
||||
<CreateRowButton
|
||||
on:updaterows={onUpdateRows}
|
||||
title={"Create row"}
|
||||
modalContentComponent={CreateEditRow}
|
||||
/>
|
||||
{/if}
|
||||
{#if isInternal}
|
||||
<CreateViewButton />
|
||||
{/if}
|
||||
<ManageAccessButton resourceId={$tables.selected?._id} />
|
||||
{#if isUsersTable}
|
||||
<EditRolesButton />
|
||||
{/if}
|
||||
{#if !isInternal}
|
||||
<ExistingRelationshipButton
|
||||
table={$tables.selected}
|
||||
<div class="buttons">
|
||||
<div class="left-buttons">
|
||||
<CreateColumnButton
|
||||
highlighted={$fetch.loaded && (!hasCols || !hasRows)}
|
||||
on:updatecolumns={onUpdateColumns}
|
||||
/>
|
||||
{/if}
|
||||
<HideAutocolumnButton bind:hideAutocolumns />
|
||||
<!-- always have the export last -->
|
||||
<ExportButton view={$tables.selected?._id} />
|
||||
<ImportButton
|
||||
tableId={$tables.selected?._id}
|
||||
on:updaterows={onUpdateRows}
|
||||
/>
|
||||
{#key id}
|
||||
<TableFilterButton {schema} on:change={onFilter} />
|
||||
{/key}
|
||||
{/if}
|
||||
{#if !isUsersTable}
|
||||
<CreateRowButton
|
||||
on:updaterows={onUpdateRows}
|
||||
title={"Create row"}
|
||||
modalContentComponent={CreateEditRow}
|
||||
disabled={!hasCols}
|
||||
highlighted={$fetch.loaded && hasCols && !hasRows}
|
||||
/>
|
||||
{/if}
|
||||
{#if isInternal}
|
||||
<CreateViewButton disabled={!hasCols || !hasRows} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="right-buttons">
|
||||
<ManageAccessButton resourceId={$tables.selected?._id} />
|
||||
{#if isUsersTable}
|
||||
<EditRolesButton />
|
||||
{/if}
|
||||
{#if !isInternal}
|
||||
<ExistingRelationshipButton
|
||||
table={$tables.selected}
|
||||
on:updatecolumns={onUpdateColumns}
|
||||
/>
|
||||
{/if}
|
||||
<HideAutocolumnButton bind:hideAutocolumns />
|
||||
<ImportButton
|
||||
tableId={$tables.selected?._id}
|
||||
on:updaterows={onUpdateRows}
|
||||
/>
|
||||
<ExportButton
|
||||
disabled={!hasRows || !hasCols}
|
||||
view={$tables.selected?._id}
|
||||
/>
|
||||
{#key id}
|
||||
<TableFilterButton
|
||||
{schema}
|
||||
on:change={onFilter}
|
||||
disabled={!hasCols || !hasRows}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
<div slot="placeholder">
|
||||
<Layout gap="S">
|
||||
{#if !hasCols}
|
||||
<Heading>Let's create some columns</Heading>
|
||||
<Body>
|
||||
Start building out your table structure<br />
|
||||
by adding some columns
|
||||
</Body>
|
||||
{:else}
|
||||
<Heading>Now let's add a row</Heading>
|
||||
<Body>
|
||||
Add some data to your table<br />
|
||||
by adding some rows
|
||||
</Body>
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
</Table>
|
||||
{#key id}
|
||||
<div in:fade={{ delay: 200, duration: 100 }}>
|
||||
|
@ -162,4 +211,20 @@
|
|||
align-items: center;
|
||||
margin-top: var(--spacing-xl);
|
||||
}
|
||||
.buttons {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.left-buttons,
|
||||
.right-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -19,13 +19,25 @@
|
|||
export let value = defaultValue || (meta.type === "boolean" ? false : "")
|
||||
export let readonly
|
||||
|
||||
const resolveTimeStamp = timestamp => {
|
||||
let maskedDate = new Date(`0-${timestamp}`)
|
||||
if (maskedDate instanceof Date && !isNaN(maskedDate.getTime())) {
|
||||
return maskedDate
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
$: stringVal =
|
||||
typeof value === "object" ? JSON.stringify(value, null, 2) : value
|
||||
$: type = meta?.type
|
||||
$: label = meta.name ? capitalise(meta.name) : ""
|
||||
|
||||
const timeStamp = resolveTimeStamp(value)
|
||||
const isTimeStamp = !!timeStamp
|
||||
</script>
|
||||
|
||||
{#if type === "options"}
|
||||
{#if type === "options" && meta.constraints.inclusion.length !== 0}
|
||||
<Select
|
||||
{label}
|
||||
data-cy="{meta.name}-select"
|
||||
|
@ -34,12 +46,12 @@
|
|||
sort
|
||||
/>
|
||||
{:else if type === "datetime"}
|
||||
<DatePicker {label} bind:value />
|
||||
<DatePicker {label} timeOnly={isTimeStamp} bind:value />
|
||||
{:else if type === "attachment"}
|
||||
<Dropzone {label} bind:value />
|
||||
{:else if type === "boolean"}
|
||||
<Toggle text={label} bind:value data-cy="{meta.name}-input" />
|
||||
{:else if type === "array"}
|
||||
{:else if type === "array" && meta.constraints.inclusion.length !== 0}
|
||||
<Multiselect bind:value {label} options={meta.constraints.inclusion} />
|
||||
{:else if type === "link"}
|
||||
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
export let rowCount
|
||||
export let type
|
||||
export let disableSorting = false
|
||||
export let customPlaceholder = false
|
||||
|
||||
let selectedRows = []
|
||||
let editableColumn
|
||||
|
@ -117,10 +118,10 @@
|
|||
</script>
|
||||
|
||||
<Layout noPadding gap="S">
|
||||
<div>
|
||||
<Layout noPadding gap="XS">
|
||||
{#if title}
|
||||
<div class="table-title">
|
||||
<Heading size="S">{title}</Heading>
|
||||
<Heading size="M">{title}</Heading>
|
||||
{#if loading}
|
||||
<div transition:fade|local>
|
||||
<Spinner size="10" />
|
||||
|
@ -134,7 +135,7 @@
|
|||
<DeleteRowsButton on:updaterows {selectedRows} {deleteRows} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
{#key tableId}
|
||||
<div class="table-wrapper">
|
||||
<Table
|
||||
|
@ -144,6 +145,7 @@
|
|||
{customRenderers}
|
||||
{rowCount}
|
||||
{disableSorting}
|
||||
{customPlaceholder}
|
||||
bind:selectedRows
|
||||
allowSelectRows={allowEditing && !isUsersTable}
|
||||
allowEditRows={allowEditing}
|
||||
|
@ -153,7 +155,9 @@
|
|||
on:editrow={e => editRow(e.detail)}
|
||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||
on:sort
|
||||
/>
|
||||
>
|
||||
<slot slot="placeholder" name="placeholder" />
|
||||
</Table>
|
||||
</div>
|
||||
{/key}
|
||||
</Layout>
|
||||
|
@ -176,6 +180,7 @@
|
|||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
.table-title > div {
|
||||
margin-left: var(--spacing-xs);
|
||||
|
|
|
@ -2,10 +2,21 @@
|
|||
import { ActionButton, Modal } from "@budibase/bbui"
|
||||
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
|
||||
|
||||
export let highlighted = false
|
||||
export let disabled = false
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
||||
<ActionButton icon="TableColumnAddRight" quiet size="S" on:click={modal.show}>
|
||||
<ActionButton
|
||||
{disabled}
|
||||
selected={highlighted}
|
||||
emphasized={highlighted}
|
||||
icon="TableColumnAddRight"
|
||||
quiet
|
||||
size="S"
|
||||
on:click={modal.show}
|
||||
>
|
||||
Create column
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
|
|
|
@ -4,11 +4,21 @@
|
|||
|
||||
export let modalContentComponent = CreateEditRow
|
||||
export let title = "Create row"
|
||||
export let disabled = false
|
||||
export let highlighted = false
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
||||
<ActionButton icon="TableRowAddBottom" size="S" quiet on:click={modal.show}>
|
||||
<ActionButton
|
||||
{disabled}
|
||||
emphasized={highlighted}
|
||||
selected={highlighted}
|
||||
icon="TableRowAddBottom"
|
||||
size="S"
|
||||
quiet
|
||||
on:click={modal.show}
|
||||
>
|
||||
{title}
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
|
|
|
@ -2,10 +2,18 @@
|
|||
import { Modal, ActionButton } from "@budibase/bbui"
|
||||
import CreateViewModal from "../modals/CreateViewModal.svelte"
|
||||
|
||||
export let disabled = false
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
||||
<ActionButton icon="CollectionAdd" size="S" quiet on:click={modal.show}>
|
||||
<ActionButton
|
||||
{disabled}
|
||||
icon="CollectionAdd"
|
||||
size="S"
|
||||
quiet
|
||||
on:click={modal.show}
|
||||
>
|
||||
Create view
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
|
|
|
@ -3,11 +3,18 @@
|
|||
import ExportModal from "../modals/ExportModal.svelte"
|
||||
|
||||
export let view
|
||||
export let disabled = false
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
||||
<ActionButton icon="DataDownload" size="S" quiet on:click={modal.show}>
|
||||
<ActionButton
|
||||
{disabled}
|
||||
icon="DataDownload"
|
||||
size="S"
|
||||
quiet
|
||||
on:click={modal.show}
|
||||
>
|
||||
Export
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
|
|
|
@ -8,6 +8,12 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<ActionButton icon="MagicWand" primary size="S" quiet on:click={hideOrUnhide}>
|
||||
{#if hideAutocolumns}Show auto columns{:else}Hide auto columns{/if}
|
||||
<ActionButton
|
||||
icon={hideAutocolumns ? "VisibilityOff" : "Visibility"}
|
||||
primary
|
||||
size="S"
|
||||
quiet
|
||||
on:click={hideOrUnhide}
|
||||
>
|
||||
Auto columns
|
||||
</ActionButton>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
export let schema
|
||||
export let filters
|
||||
export let disabled = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let modal
|
||||
|
@ -17,6 +18,7 @@
|
|||
icon="Filter"
|
||||
size="S"
|
||||
quiet
|
||||
{disabled}
|
||||
on:click={modal.show}
|
||||
active={tempValue?.length > 0}
|
||||
>
|
||||
|
|
|
@ -6,15 +6,10 @@
|
|||
export let overlayEnabled = true
|
||||
|
||||
let imageError = false
|
||||
let imageLoaded = false
|
||||
|
||||
const imageRenderError = () => {
|
||||
imageError = true
|
||||
}
|
||||
|
||||
const imageLoadSuccess = () => {
|
||||
imageLoaded = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="template-card" style="background-color:{backgroundColour};">
|
||||
|
@ -23,8 +18,7 @@
|
|||
alt={name}
|
||||
src={imageSrc}
|
||||
on:error={imageRenderError}
|
||||
on:load={imageLoadSuccess}
|
||||
class={`${imageLoaded ? "loaded" : ""}`}
|
||||
class:error={imageError}
|
||||
/>
|
||||
<div style={`display:${imageError ? "block" : "none"}`}>
|
||||
<svg
|
||||
|
@ -104,15 +98,14 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.template-card img.loaded {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.template-card img {
|
||||
display: none;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
border-radius: var(--border-radius-s) 0px var(--border-radius-s) 0px;
|
||||
}
|
||||
.template-card img.error {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="control">
|
||||
<div class="control" class:disabled>
|
||||
<Combobox
|
||||
{label}
|
||||
{disabled}
|
||||
|
@ -121,4 +121,8 @@
|
|||
background-color: var(--spectrum-global-color-gray-50);
|
||||
border-color: var(--spectrum-alias-border-color-hover);
|
||||
}
|
||||
|
||||
.control:not(.disabled) :global(.spectrum-Textfield-input) {
|
||||
padding-right: 40px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="control">
|
||||
<div class="control" class:disabled>
|
||||
<Input
|
||||
{label}
|
||||
{disabled}
|
||||
|
@ -103,4 +103,8 @@
|
|||
background-color: var(--spectrum-global-color-gray-50);
|
||||
border-color: var(--spectrum-alias-border-color-hover);
|
||||
}
|
||||
|
||||
.control:not(.disabled) :global(.spectrum-Textfield-input) {
|
||||
padding-right: 40px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,13 +3,14 @@
|
|||
import PathTree from "./PathTree.svelte"
|
||||
|
||||
let routes = {}
|
||||
$: paths = Object.keys(routes || {}).sort()
|
||||
let paths = []
|
||||
|
||||
$: {
|
||||
const allRoutes = $store.routes
|
||||
$: allRoutes = $store.routes
|
||||
$: selectedScreenId = $store.selectedScreenId
|
||||
$: updatePaths(allRoutes, $selectedAccessRole, selectedScreenId)
|
||||
|
||||
const updatePaths = (allRoutes, selectedRoleId, selectedScreenId) => {
|
||||
const sortedPaths = Object.keys(allRoutes || {}).sort()
|
||||
const selectedRoleId = $selectedAccessRole
|
||||
const selectedScreenId = $store.selectedScreenId
|
||||
|
||||
let found = false
|
||||
let firstValidScreenId
|
||||
|
@ -41,11 +42,15 @@
|
|||
})
|
||||
})
|
||||
})
|
||||
routes = filteredRoutes
|
||||
routes = { ...filteredRoutes }
|
||||
paths = Object.keys(routes || {}).sort()
|
||||
|
||||
// Select the correct role for the current screen ID
|
||||
if (!found && screenRoleId) {
|
||||
selectedAccessRole.set(screenRoleId)
|
||||
if (screenRoleId !== selectedRoleId) {
|
||||
updatePaths(allRoutes, screenRoleId, selectedScreenId)
|
||||
}
|
||||
}
|
||||
|
||||
// If the selected screen isn't in this filtered list, select the first one
|
||||
|
|
|
@ -26,14 +26,6 @@
|
|||
on:change={value => (parameters.rowId = value.detail)}
|
||||
/>
|
||||
|
||||
<Label small>Row Rev</Label>
|
||||
<DrawerBindableInput
|
||||
{bindings}
|
||||
title="Row rev to delete"
|
||||
value={parameters.revId}
|
||||
on:change={value => (parameters.revId = value.detail)}
|
||||
/>
|
||||
|
||||
<Label small />
|
||||
<Checkbox text="Require confirmation" bind:value={parameters.confirm} />
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
.filter(a => a.definition.trigger?.stepId === "APP")
|
||||
.map(automation => {
|
||||
const schema = Object.entries(
|
||||
automation.definition.trigger.inputs.fields
|
||||
automation.definition.trigger.inputs.fields || {}
|
||||
).map(([name, type]) => ({ name, type }))
|
||||
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
<script>
|
||||
import { Select, Label, Combobox } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
import { currentAsset, store } from "builderStore"
|
||||
import {
|
||||
getActionProviderComponents,
|
||||
buildFormSchema,
|
||||
} from "builderStore/dataBinding"
|
||||
import { findComponent } from "builderStore/componentUtils"
|
||||
|
||||
export let parameters
|
||||
export let bindings = []
|
||||
|
||||
const typeOptions = [
|
||||
{
|
||||
label: "Set value",
|
||||
value: "set",
|
||||
},
|
||||
{
|
||||
label: "Reset to default value",
|
||||
value: "reset",
|
||||
},
|
||||
]
|
||||
|
||||
$: formComponent = findComponent($currentAsset.props, parameters.componentId)
|
||||
$: formSchema = buildFormSchema(formComponent)
|
||||
$: fieldOptions = Object.keys(formSchema || {})
|
||||
$: actionProviders = getActionProviderComponents(
|
||||
$currentAsset,
|
||||
$store.selectedComponentId,
|
||||
"ValidateForm"
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
if (!parameters.type) {
|
||||
parameters.type = "set"
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Label small>Form</Label>
|
||||
<Select
|
||||
bind:value={parameters.componentId}
|
||||
options={actionProviders}
|
||||
getOptionLabel={x => x._instanceName}
|
||||
getOptionValue={x => x._id}
|
||||
/>
|
||||
<Label small>Type</Label>
|
||||
<Select
|
||||
placeholder={null}
|
||||
bind:value={parameters.type}
|
||||
options={typeOptions}
|
||||
/>
|
||||
<Label small>Field</Label>
|
||||
<Combobox bind:value={parameters.field} options={fieldOptions} />
|
||||
{#if parameters.type === "set"}
|
||||
<Label small>Value</Label>
|
||||
<DrawerBindableInput
|
||||
{bindings}
|
||||
value={parameters.value}
|
||||
on:change={e => (parameters.value = e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
display: grid;
|
||||
column-gap: var(--spacing-l);
|
||||
row-gap: var(--spacing-s);
|
||||
grid-template-columns: 60px 1fr;
|
||||
align-items: center;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
|
@ -14,3 +14,4 @@ export { default as DuplicateRow } from "./DuplicateRow.svelte"
|
|||
export { default as S3Upload } from "./S3Upload.svelte"
|
||||
export { default as ExportData } from "./ExportData.svelte"
|
||||
export { default as ContinueIf } from "./ContinueIf.svelte"
|
||||
export { default as UpdateFieldValue } from "./UpdateFieldValue.svelte"
|
||||
|
|
|
@ -42,25 +42,29 @@
|
|||
"name": "Trigger Automation",
|
||||
"component": "TriggerAutomation"
|
||||
},
|
||||
{
|
||||
"name": "Update Field Value",
|
||||
"component": "UpdateFieldValue"
|
||||
},
|
||||
{
|
||||
"name": "Validate Form",
|
||||
"component": "ValidateForm"
|
||||
},
|
||||
{
|
||||
"name": "Log Out",
|
||||
"component": "LogOut"
|
||||
"name": "Change Form Step",
|
||||
"component": "ChangeFormStep"
|
||||
},
|
||||
{
|
||||
"name": "Clear Form",
|
||||
"component": "ClearForm"
|
||||
},
|
||||
{
|
||||
"name": "Close Screen Modal",
|
||||
"component": "CloseScreenModal"
|
||||
"name": "Log Out",
|
||||
"component": "LogOut"
|
||||
},
|
||||
{
|
||||
"name": "Change Form Step",
|
||||
"component": "ChangeFormStep"
|
||||
"name": "Close Screen Modal",
|
||||
"component": "CloseScreenModal"
|
||||
},
|
||||
{
|
||||
"name": "Refresh Data Provider",
|
||||
|
|
|
@ -52,7 +52,6 @@
|
|||
.map(query => ({
|
||||
label: query.name,
|
||||
name: query.name,
|
||||
tableId: query._id,
|
||||
...query,
|
||||
type: "query",
|
||||
}))
|
||||
|
|
|
@ -10,9 +10,15 @@
|
|||
let drawer
|
||||
let tempValue = value || []
|
||||
|
||||
const saveFilter = async () => {
|
||||
// Filter out incomplete options
|
||||
tempValue = tempValue.filter(option => option.value && option.label)
|
||||
const saveOptions = async () => {
|
||||
// Filter out incomplete options, default if needed
|
||||
tempValue = tempValue.filter(option => option.value || option.label)
|
||||
for (let i = 0; i < tempValue.length; i++) {
|
||||
let option = tempValue[i]
|
||||
option.label = option.label ? option.label : option.value
|
||||
option.value = option.value ? option.value : option.label
|
||||
tempValue[i] = option
|
||||
}
|
||||
dispatch("change", tempValue)
|
||||
drawer.hide()
|
||||
}
|
||||
|
@ -23,6 +29,6 @@
|
|||
<svelte:fragment slot="description">
|
||||
Define the options for this picker.
|
||||
</svelte:fragment>
|
||||
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
||||
<Button cta slot="buttons" on:click={saveOptions}>Save</Button>
|
||||
<OptionsDrawer bind:options={tempValue} slot="body" />
|
||||
</Drawer>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { roles } from "stores/backend"
|
||||
|
||||
export let value
|
||||
export let error
|
||||
</script>
|
||||
|
||||
<Select
|
||||
|
@ -11,4 +12,5 @@
|
|||
options={$roles}
|
||||
getOptionLabel={role => role.name}
|
||||
getOptionValue={role => role._id}
|
||||
{error}
|
||||
/>
|
||||
|
|
|
@ -15,16 +15,14 @@
|
|||
|
||||
const dispatch = createEventDispatcher()
|
||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||
$: schema = getSchemaForDatasource($currentAsset, datasource, {
|
||||
searchableSchema: true,
|
||||
}).schema
|
||||
$: schema = getSchemaForDatasource($currentAsset, datasource).schema
|
||||
$: options = getOptions(datasource, schema || {})
|
||||
$: boundValue = getSelectedOption(value, options)
|
||||
|
||||
function getOptions(ds, dsSchema) {
|
||||
let base = Object.values(dsSchema)
|
||||
if (!ds?.tableId) {
|
||||
return base
|
||||
return base.map(field => field.name)
|
||||
}
|
||||
const currentTable = $tables.list.find(table => table._id === ds.tableId)
|
||||
return getFields(base, { allowLinks: currentTable?.sql }).map(
|
||||
|
|
|
@ -8,14 +8,50 @@
|
|||
import { currentAsset, store } from "builderStore"
|
||||
import { FrontendTypes } from "constants"
|
||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||
import { allScreens, selectedAccessRole } from "builderStore"
|
||||
|
||||
export let componentInstance
|
||||
export let bindings
|
||||
|
||||
function setAssetProps(name, value, parser) {
|
||||
if (parser && typeof parser === "function") {
|
||||
let errors = {}
|
||||
|
||||
const routeTaken = url => {
|
||||
const roleId = get(selectedAccessRole) || "BASIC"
|
||||
return get(allScreens).some(
|
||||
screen =>
|
||||
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
||||
screen.routing.roleId === roleId
|
||||
)
|
||||
}
|
||||
|
||||
const roleTaken = roleId => {
|
||||
const url = get(currentAsset)?.routing.route
|
||||
return get(allScreens).some(
|
||||
screen =>
|
||||
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
||||
screen.routing.roleId === roleId
|
||||
)
|
||||
}
|
||||
|
||||
const setAssetProps = (name, value, parser, validate) => {
|
||||
if (parser) {
|
||||
value = parser(value)
|
||||
}
|
||||
if (validate) {
|
||||
const error = validate(value)
|
||||
errors = {
|
||||
...errors,
|
||||
[name]: error,
|
||||
}
|
||||
if (error) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
errors = {
|
||||
...errors,
|
||||
[name]: null,
|
||||
}
|
||||
}
|
||||
|
||||
const selectedAsset = get(currentAsset)
|
||||
store.update(state => {
|
||||
|
@ -38,7 +74,6 @@
|
|||
}
|
||||
|
||||
const screenSettings = [
|
||||
// { key: "description", label: "Description", control: Input },
|
||||
{
|
||||
key: "routing.route",
|
||||
label: "Route",
|
||||
|
@ -49,8 +84,26 @@
|
|||
}
|
||||
return sanitizeUrl(val)
|
||||
},
|
||||
validate: val => {
|
||||
const exisingValue = get(currentAsset)?.routing.route
|
||||
if (val !== exisingValue && routeTaken(val)) {
|
||||
return "That URL is already in use for this role"
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "routing.roleId",
|
||||
label: "Access",
|
||||
control: RoleSelect,
|
||||
validate: val => {
|
||||
const exisingValue = get(currentAsset)?.routing.roleId
|
||||
if (val !== exisingValue && roleTaken(val)) {
|
||||
return "That role is already in use for this URL"
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
{ key: "routing.roleId", label: "Access", control: RoleSelect },
|
||||
{ key: "layoutId", label: "Layout", control: LayoutSelect },
|
||||
]
|
||||
</script>
|
||||
|
@ -62,9 +115,11 @@
|
|||
control={def.control}
|
||||
label={def.label}
|
||||
key={def.key}
|
||||
error="asdasds"
|
||||
value={deepGet($currentAsset, def.key)}
|
||||
onChange={val => setAssetProps(def.key, val, def.parser)}
|
||||
onChange={val => setAssetProps(def.key, val, def.parser, def.validate)}
|
||||
{bindings}
|
||||
props={{ error: errors[def.key] }}
|
||||
/>
|
||||
{/each}
|
||||
</DetailSummary>
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { auth } from "../stores/portal"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export const FEATURE_FLAGS = {
|
||||
LICENSING: "LICENSING",
|
||||
}
|
||||
|
||||
export const isEnabled = featureFlag => {
|
||||
const user = get(auth).user
|
||||
if (user?.featureFlags?.includes(featureFlag)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
notifications.success("Invitation accepted successfully")
|
||||
$goto("../auth/login")
|
||||
} catch (error) {
|
||||
notifications.error("Error accepting invitation")
|
||||
notifications.error(error.message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue