Merge pull request #4417 from Budibase/develop

NGINX and frontend-core
This commit is contained in:
Martin McKeaveney 2022-02-10 19:07:04 +01:00 committed by GitHub
commit 4517607345
287 changed files with 4893 additions and 3997 deletions

View File

@ -137,7 +137,7 @@ If you wish to delete all the apps created in development and reset the environm
### Backend
For the backend we run [Redis](https://redis.io/), [CouchDB](https://couchdb.apache.org/), [MinIO](https://min.io/) and [Envoy](https://www.envoyproxy.io/) in Docker compose. This means that to develop Budibase you will need Docker and Docker compose installed. The backend services are then ran separately as Node services with nodemon so that they can be debugged outside of Docker.
For the backend we run [Redis](https://redis.io/), [CouchDB](https://couchdb.apache.org/), [MinIO](https://min.io/) and [NGINX](https://www.nginx.com/) in Docker compose. This means that to develop Budibase you will need Docker and Docker compose installed. The backend services are then ran separately as Node services with nodemon so that they can be debugged outside of Docker.
### Data Storage

2
.gitignore vendored
View File

@ -64,7 +64,7 @@ typings/
# dotenv environment variables file
.env
!hosting/.env
hosting/.generated-envoy.dev.yaml
hosting/.generated-nginx.dev.conf
# parcel-bundler cache (https://parceljs.org/)
.cache

View File

@ -12,7 +12,7 @@ sources:
- https://budibase.com
type: application
version: 0.2.6
appVersion: 1.0.47
appVersion: 1.0.48
dependencies:
- name: couchdb
version: 3.3.4

View File

@ -25,7 +25,7 @@ spec:
app.kubernetes.io/name: budibase-proxy
spec:
containers:
- image: budibase/proxy
- image: budibase/proxy:k8s
imagePullPolicy: Always
name: proxy-service
ports:

View File

@ -1 +0,0 @@
hosting.properties

21
hosting/.env Normal file
View File

@ -0,0 +1,21 @@
# Use the main port in the builder for your self hosting URL, e.g. localhost:10000
MAIN_PORT=10000
# This section contains all secrets pertaining to the system
# These should be updated
JWT_SECRET=testsecret
MINIO_ACCESS_KEY=budibase
MINIO_SECRET_KEY=budibase
COUCH_DB_PASSWORD=budibase
COUCH_DB_USER=budibase
REDIS_PASSWORD=budibase
INTERNAL_API_KEY=budibase
# This section contains variables that do not need to be altered under normal circumstances
APP_PORT=4002
WORKER_PORT=4003
MINIO_PORT=4004
COUCH_DB_PORT=4005
REDIS_PORT=6379
WATCHTOWER_PORT=6161
BUDIBASE_ENVIRONMENT=PRODUCTION

View File

@ -3,9 +3,8 @@
# go into the app dir
cd /root
# fetch envoy and docker-compose files
# fetch nginx and docker-compose files
wget https://raw.githubusercontent.com/Budibase/budibase/master/hosting/docker-compose.yaml
wget https://raw.githubusercontent.com/Budibase/budibase/master/hosting/envoy.yaml
wget https://raw.githubusercontent.com/Budibase/budibase/master/hosting/hosting.properties
# Create .env file from hosting.properties using bash and then remove it

View File

@ -22,18 +22,21 @@ services:
retries: 3
proxy-service:
container_name: budi-envoy-dev
container_name: budi-nginx-dev
restart: always
image: envoyproxy/envoy:v1.16-latest
image: nginx:latest
volumes:
- ./.generated-envoy.dev.yaml:/etc/envoy/envoy.yaml
- ./.generated-nginx.dev.conf:/etc/nginx/nginx.conf
ports:
- "${MAIN_PORT}:10000"
depends_on:
- minio-service
- couchdb-service
extra_hosts:
- "host.docker.internal:host-gateway"
couchdb-service:
# platform: linux/amd64
container_name: budi-couchdb-dev
restart: always
image: ibmcom/couchdb3

View File

@ -80,9 +80,8 @@ services:
proxy-service:
restart: always
image: envoyproxy/envoy:v1.16-latest
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml
container_name: bbproxy
image: budibase/proxy
ports:
- "${MAIN_PORT}:10000"
depends_on:
@ -125,7 +124,7 @@ services:
- "${WATCHTOWER_PORT}:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --debug --http-api-update bbapps bbworker
command: --debug --http-api-update bbapps bbworker bbproxy
environment:
- WATCHTOWER_HTTP_API=true
- WATCHTOWER_HTTP_API_TOKEN=budibase

View File

@ -1,149 +0,0 @@
static_resources:
listeners:
- name: main_listener
address:
socket_address: { address: 0.0.0.0, port_value: 10000 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress
codec_type: auto
route_config:
name: local_route
virtual_hosts:
- name: local_services
domains: ["*"]
routes:
# special case to redirect specifically the route path
# to the builder, if this were a prefix then it would break minio
- match: { path: "/" }
redirect: { path_redirect: "/builder/" }
- match: { prefix: "/db/" }
route:
cluster: couchdb-service
prefix_rewrite: "/"
- match: { prefix: "/api/system/" }
route:
cluster: worker-dev
- match: { prefix: "/api/admin/" }
route:
cluster: worker-dev
- match: { prefix: "/api/global/" }
route:
cluster: worker-dev
- match: { prefix: "/api/" }
route:
cluster: server-dev
timeout: 120s
- match: { prefix: "/app_" }
route:
cluster: server-dev
- match: { prefix: "/app/" }
route:
cluster: server-dev
prefix_rewrite: "/"
# the below three cases are needed to make sure
# all traffic prefixed for the builder is passed through
# correctly.
- match: { path: "/" }
route:
cluster: builder-dev
- match: { prefix: "/builder/" }
route:
cluster: builder-dev
- match: { prefix: "/builder" }
route:
cluster: builder-dev
prefix_rewrite: "/builder/"
# minio is on the default route because this works
# best, minio + AWS SDK doesn't handle path proxy
- match: { prefix: "/" }
route:
cluster: minio-service
http_filters:
- name: envoy.filters.http.router
clusters:
- name: minio-service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: minio-service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: minio-service
port_value: 9000
- name: couchdb-service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: couchdb-service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: couchdb-service
port_value: 5984
- name: server-dev
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: server-dev
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: {{ address }}
port_value: 4001
- name: builder-dev
connect_timeout: 15s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: builder-dev
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: {{ address }}
port_value: 3000
- name: worker-dev
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: worker-dev
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: {{ address }}
port_value: 4002

View File

@ -1,152 +0,0 @@
static_resources:
listeners:
- name: main_listener
address:
socket_address: { address: 0.0.0.0, port_value: 10000 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress
codec_type: auto
route_config:
name: local_route
virtual_hosts:
- name: local_services
domains: ["*"]
routes:
- match: { prefix: "/app/" }
route:
cluster: app-service
prefix_rewrite: "/"
- match: { path: "/v1/update" }
route:
cluster: watchtower-service
- match: { prefix: "/builder/" }
route:
cluster: app-service
- match: { prefix: "/builder" }
route:
cluster: app-service
- match: { prefix: "/app_" }
route:
cluster: app-service
# special cases for worker admin (deprecated), global and system API
- match: { prefix: "/api/global/" }
route:
cluster: worker-service
- match: { prefix: "/api/admin/" }
route:
cluster: worker-service
- match: { prefix: "/api/system/" }
route:
cluster: worker-service
- match: { path: "/" }
route:
cluster: app-service
# special case for when API requests are made, can just forward, not to minio
- match: { prefix: "/api/" }
route:
cluster: app-service
timeout: 120s
- match: { prefix: "/worker/" }
route:
cluster: worker-service
prefix_rewrite: "/"
- match: { prefix: "/db/" }
route:
cluster: couchdb-service
prefix_rewrite: "/"
# minio is on the default route because this works
# best, minio + AWS SDK doesn't handle path proxy
- match: { prefix: "/" }
route:
cluster: minio-service
http_filters:
- name: envoy.filters.http.router
clusters:
- name: app-service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: app-service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: app-service
port_value: 4002
- name: minio-service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: minio-service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: minio-service
port_value: 9000
- name: worker-service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: worker-service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: worker-service
port_value: 4003
- name: couchdb-service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: couchdb-service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: couchdb-service
port_value: 5984
- name: watchtower-service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: watchtower-service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: watchtower-service
port_value: 8080

View File

@ -1,21 +0,0 @@
# Use the main port in the builder for your self hosting URL, e.g. localhost:10000
MAIN_PORT=10000
# This section contains all secrets pertaining to the system
# These should be updated
JWT_SECRET=testsecret
MINIO_ACCESS_KEY=budibase
MINIO_SECRET_KEY=budibase
COUCH_DB_PASSWORD=budibase
COUCH_DB_USER=budibase
REDIS_PASSWORD=budibase
INTERNAL_API_KEY=budibase
# This section contains variables that do not need to be altered under normal circumstances
APP_PORT=4002
WORKER_PORT=4003
MINIO_PORT=4004
COUCH_DB_PORT=4005
REDIS_PORT=6379
WATCHTOWER_PORT=6161
BUDIBASE_ENVIRONMENT=PRODUCTION

View File

@ -1,4 +0,0 @@
FROM envoyproxy/envoy:v1.16-latest
COPY envoy.yaml /etc/envoy/envoy.yaml
RUN chmod go+r /etc/envoy/envoy.yaml

View File

@ -1,146 +0,0 @@
static_resources:
listeners:
- name: main_listener
address:
socket_address: { address: 0.0.0.0, port_value: 10000 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress
codec_type: auto
route_config:
name: local_route
virtual_hosts:
- name: local_services
domains: ["*"]
routes:
- match: { prefix: "/app/" }
route:
cluster: app-service
prefix_rewrite: "/"
- match: { prefix: "/builder/" }
route:
cluster: app-service
- match: { prefix: "/builder" }
route:
cluster: app-service
- match: { prefix: "/app_" }
route:
cluster: app-service
# special cases for worker admin (deprecated), global and system API
- match: { prefix: "/api/global/" }
route:
cluster: worker-service
- match: { prefix: "/api/admin/" }
route:
cluster: worker-service
- match: { prefix: "/api/system/" }
route:
cluster: worker-service
- match: { path: "/" }
route:
cluster: app-service
- match:
safe_regex:
google_re2: {}
regex: "/api/.*/export"
route:
timeout: 0s
cluster: app-service
- match: { path: "/api/deploy" }
route:
timeout: 60s
cluster: app-service
# special case for when API requests are made, can just forward, not to minio
- match: { prefix: "/api/" }
route:
cluster: app-service
- match: { prefix: "/worker/" }
route:
cluster: worker-service
prefix_rewrite: "/"
- match: { prefix: "/db/" }
route:
cluster: couchdb-service
prefix_rewrite: "/"
# minio is on the default route because this works
# best, minio + AWS SDK doesn't handle path proxy
- match: { prefix: "/" }
route:
cluster: minio-service
http_filters:
- name: envoy.filters.http.router
clusters:
- name: app-service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: app-service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: app-service.budibase.svc.cluster.local
port_value: 4002
- name: minio-service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: minio-service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: minio-service.budibase.svc.cluster.local
port_value: 9000
- name: worker-service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: worker-service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: worker-service.budibase.svc.cluster.local
port_value: 4001
- name: couchdb-service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: couchdb-service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: budibase-prod-svc-couchdb
port_value: 5984

View File

@ -0,0 +1,2 @@
FROM nginx:latest
COPY nginx.conf /etc/nginx/nginx.conf

View File

@ -0,0 +1,127 @@
user nginx;
error_log /var/log/nginx/error.log debug;
pid /var/run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 33282;
events {
worker_connections 1024;
}
http {
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=10r/s;
include /etc/nginx/mime.types;
default_type application/octet-stream;
charset utf-8;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
types_hash_max_size 2048;
# buffering
client_body_buffer_size 1K;
client_header_buffer_size 1k;
client_max_body_size 1k;
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;
server_name _;
# Security Headers
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me; object-src 'none'; base-uri 'self'; connect-src 'self' https://api-iam.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io; font-src 'self' https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me; frame-src 'self'; img-src https: data:; manifest-src 'self'; media-src 'self'; worker-src 'none';" always;
location /app {
proxy_pass http://app-service:4002;
rewrite ^/app/(.*)$ /$1 break;
}
location = / {
proxy_pass http://app-service.budibase.svc.cluster.local:4002;
}
location /builder/ {
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://app-service.budibase.svc.cluster.local:4002;
}
location ^/(builder|app_) {
proxy_pass http://app-service.budibase.svc.cluster.local:4002;
}
location ~ ^/api/(system|admin|global)/ {
proxy_pass http://worker-service.budibase.svc.cluster.local:4003;
}
location /worker/ {
proxy_pass http://worker-service.budibase.svc.cluster.local:4003;
rewrite ^/worker/(.*)$ /$1 break;
}
location /api/ {
# calls to the API are rate limited with bursting
limit_req zone=ratelimit burst=10 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 Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://app-service.budibase.svc.cluster.local:4002;
}
location /db/ {
proxy_pass http://budibase-prod-svc-couchdb: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_set_header Host $http_host;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;
proxy_pass http://minio-service.budibase.svc.cluster.local: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;
}
}

View File

@ -0,0 +1,91 @@
user nginx;
error_log /var/log/nginx/error.log debug;
pid /var/run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 33282;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
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;
server_name _;
client_max_body_size 1000m;
ignore_invalid_headers off;
proxy_buffering off;
location /db/ {
proxy_pass http://couchdb-service:5984;
rewrite ^/db/(.*)$ /$1 break;
}
location ~ ^/api/(system|admin|global)/ {
proxy_pass http://{{ address }}:4002;
}
location /api/ {
proxy_read_timeout 120s;
proxy_connect_timeout 120s;
proxy_send_timeout 120s;
proxy_pass http://{{ address }}:4001;
}
location /app_ {
proxy_pass http://{{ address }}:4001;
}
location /app/ {
proxy_pass http://{{ address }}:4001;
rewrite ^/app/(.*)$ /$1 break;
}
location /builder {
proxy_pass http://{{ address }}:3000;
rewrite ^/builder(.*)$ /builder/$1 break;
}
location /builder/ {
proxy_pass http://{{ address }}:3000;
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
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_set_header Host $http_host;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;
proxy_pass http://minio-service:9000;
}
client_header_timeout 60;
client_body_timeout 60;
keepalive_timeout 60;
gzip off;
gzip_comp_level 4;
}
}

2
hosting/proxy/Dockerfile Normal file
View File

@ -0,0 +1,2 @@
FROM nginx:latest
COPY nginx.conf /etc/nginx/nginx.conf

135
hosting/proxy/nginx.conf Normal file
View File

@ -0,0 +1,135 @@
user nginx;
error_log /var/log/nginx/error.log debug;
pid /var/run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 33282;
events {
worker_connections 1024;
}
http {
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=10r/s;
include /etc/nginx/mime.types;
default_type application/octet-stream;
charset utf-8;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
types_hash_max_size 2048;
# buffering
client_body_buffer_size 1K;
client_header_buffer_size 1k;
client_max_body_size 1k;
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;
server_name _;
client_max_body_size 1000m;
ignore_invalid_headers off;
proxy_buffering off;
# Security Headers
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'unsafe-inline' 'unsafe-eval' 'self' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me; object-src 'none'; base-uri 'self'; connect-src 'self' https://api-iam.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io; font-src 'self' https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me; frame-src 'self'; img-src https: data:; manifest-src 'self'; media-src 'self'; worker-src 'none';" always;
location /app {
proxy_pass http://app-service:4002;
rewrite ^/app/(.*)$ /$1 break;
}
location = / {
proxy_pass http://app-service:4002;
}
location = /v1/update {
proxy_pass http://watchtower-service:8080;
}
location /builder/ {
proxy_http_version 1.1;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://app-service:4002;
}
location ^/(builder|app_) {
proxy_pass http://app-service:4002;
}
location ~ ^/api/(system|admin|global)/ {
proxy_pass http://worker-service:4003;
}
location /worker/ {
proxy_pass http://worker-service:4003;
rewrite ^/worker/(.*)$ /$1 break;
}
location /api/ {
# calls to the API are rate limited with bursting
limit_req zone=ratelimit burst=10 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 Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://app-service:4002;
}
location /db/ {
proxy_pass http://couchdb-service: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_set_header Host $http_host;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;
proxy_pass http://minio-service: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;
}
}

View File

@ -5,7 +5,7 @@ const path = require("path")
const IMAGES = {
worker: "budibase/worker",
apps: "budibase/apps",
proxy: "envoyproxy/envoy:v1.16-latest",
proxy: "budibase/proxy",
minio: "minio/minio",
couch: "ibmcom/couchdb3",
curl: "curlimages/curl",
@ -15,8 +15,7 @@ const IMAGES = {
const FILES = {
COMPOSE: "docker-compose.yaml",
ENVOY: "envoy.yaml",
PROPERTIES: "hosting.properties"
NGINX: "nginx.conf"
}
const OUTPUT_DIR = path.join(__dirname, "../", "bb-airgapped")

View File

@ -9,8 +9,10 @@ fi
echo "Tagging images with tag: $tag"
docker tag proxy-service budibase/proxy:$tag
docker tag app-service budibase/apps:$tag
docker tag worker-service budibase/worker:$tag
docker push --all-tags budibase/apps
docker push --all-tags budibase/worker
docker push --all-tags budibase/proxy

View File

@ -1,5 +1,5 @@
{
"version": "1.0.57",
"version": "1.0.58-alpha.0",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

@ -44,9 +44,10 @@
"lint:fix": "yarn run lint:fix:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"test:e2e": "lerna run cy:test",
"test:e2e:ci": "lerna run cy:ci",
"build:docker": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
"build:docker": "lerna run build:docker && npm run build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
"build:docker:selfhost": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy && 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:docs": "lerna run build:docs",

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "1.0.57",
"version": "1.0.58-alpha.0",
"description": "Budibase backend core libraries used in server and worker",
"main": "src/index.js",
"author": "Budibase",

View File

@ -8,7 +8,7 @@ const { newid } = require("../../hashing")
const { createASession } = require("../../security/sessions")
const { getTenantId } = require("../../tenancy")
const INVALID_ERR = "Invalid Credentials"
const INVALID_ERR = "Invalid credentials"
const SSO_NO_PASSWORD = "SSO user does not have a password set"
const EXPIRED = "This account has expired. Please reset your password"

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "1.0.57",
"version": "1.0.58-alpha.0",
"license": "MPL-2.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",

View File

@ -1,5 +1,6 @@
<script>
import "@spectrum-css/button/dist/index-vars.css"
import Tooltip from "../Tooltip/Tooltip.svelte"
export let disabled = false
export let size = "M"
@ -11,36 +12,67 @@
export let quiet = false
export let icon = undefined
export let active = false
export let tooltip = undefined
let showTooltip = false
</script>
<button
class:spectrum-Button--cta={cta}
class:spectrum-Button--primary={primary}
class:spectrum-Button--secondary={secondary}
class:spectrum-Button--warning={warning}
class:spectrum-Button--overBackground={overBackground}
class:spectrum-Button--quiet={quiet}
class:active
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
{disabled}
on:click|preventDefault
>
{#if icon}
<svg
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
focusable="false"
aria-hidden="true"
aria-label={icon}
>
<use xlink:href="#spectrum-icon-18-{icon}" />
</svg>
<div class:container={!!tooltip}>
<button
class:spectrum-Button--cta={cta}
class:spectrum-Button--primary={primary}
class:spectrum-Button--secondary={secondary}
class:spectrum-Button--warning={warning}
class:spectrum-Button--overBackground={overBackground}
class:spectrum-Button--quiet={quiet}
class:active
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
{disabled}
on:click|preventDefault
on:mouseover={() => (showTooltip = true)}
on:mouseleave={() => (showTooltip = false)}
>
{#if icon}
<svg
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
focusable="false"
aria-hidden="true"
aria-label={icon}
>
<use xlink:href="#spectrum-icon-18-{icon}" />
</svg>
{/if}
{#if $$slots}
<span class="spectrum-Button-label"><slot /></span>
{/if}
{#if !disabled && tooltip}
<div class="tooltip-icon">
<svg
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
focusable="false"
aria-hidden="true"
aria-label="Info"
>
<use xlink:href="#spectrum-icon-18-InfoOutline" />
</svg>
</div>
{/if}
</button>
{#if showTooltip && tooltip}
<div class="position">
<div class="tooltip">
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
</div>
</div>
{/if}
{#if $$slots}
<span class="spectrum-Button-label"><slot /></span>
{/if}
</button>
</div>
<style>
.container {
display: flex;
align-items: center;
flex-direction: column;
}
.spectrum-Button-label {
white-space: nowrap;
overflow: hidden;
@ -49,4 +81,23 @@
.active {
color: var(--spectrum-global-color-blue-600) !important;
}
.tooltip {
position: absolute;
display: flex;
justify-content: center;
z-index: 100;
width: 160px;
text-align: center;
transform: translateX(-50%);
top: -5px;
}
.position {
position: relative;
width: 0;
height: 0;
}
.tooltip-icon {
padding-left: var(--spacing-m);
line-height: 0;
}
</style>

View File

@ -5,7 +5,7 @@
import { fly } from "svelte/transition"
import Icon from "../Icon/Icon.svelte"
import Input from "../Form/Input.svelte"
import { capitalise } from "../utils/helpers"
import { capitalise } from "../helpers"
export let value
export let size = "M"

View File

@ -5,7 +5,7 @@
import "@spectrum-css/textfield/dist/index-vars.css"
import "@spectrum-css/picker/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
import { generateID } from "../../utils/helpers"
import { uuid } from "../../helpers"
export let id = null
export let disabled = false
@ -14,16 +14,20 @@
export let value = null
export let placeholder = null
export let appendTo = undefined
export let timeOnly = false
const dispatch = createEventDispatcher()
const flatpickrId = `${generateID()}-wrapper`
const flatpickrId = `${uuid()}-wrapper`
let open = false
let flatpickr
let flatpickr, flatpickrOptions, isTimeOnly
$: isTimeOnly = !timeOnly && value ? !isNaN(new Date(`0-${value}`)) : timeOnly
$: flatpickrOptions = {
element: `#${flatpickrId}`,
enableTime: enableTime || false,
enableTime: isTimeOnly || enableTime || false,
noCalendar: isTimeOnly || false,
altInput: true,
altFormat: enableTime ? "F j Y, H:i" : "F j, Y",
altFormat: isTimeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
wrap: true,
appendTo,
disableMobile: "true",
@ -35,6 +39,11 @@
if (newValue) {
newValue = newValue.toISOString()
}
// if time only set date component to today
if (timeOnly) {
const todayDate = new Date().toISOString().split("T")[0]
newValue = `${todayDate}T${newValue.split("T")[1]}`
}
dispatch("change", newValue)
}
@ -67,7 +76,11 @@
return null
}
let date
if (val instanceof Date) {
let time = new Date(`0-${val}`)
// it is a string like 00:00:00, just time
if (timeOnly || (typeof val === "string" && !isNaN(time))) {
date = time
} else if (val instanceof Date) {
// Use real date obj if already parsed
date = val
} else if (isNaN(val)) {
@ -77,7 +90,7 @@
// Treat as numerical timestamp
date = new Date(parseInt(val))
}
const time = date.getTime()
time = date.getTime()
if (isNaN(time)) {
return null
}
@ -88,69 +101,71 @@
}
</script>
<Flatpickr
bind:flatpickr
value={parseDate(value)}
on:open={onOpen}
on:close={onClose}
options={flatpickrOptions}
on:change={handleChange}
element={`#${flatpickrId}`}
>
<div
id={flatpickrId}
class:is-disabled={disabled}
class:is-invalid={!!error}
class="flatpickr spectrum-InputGroup spectrum-Datepicker"
class:is-focused={open}
aria-readonly="false"
aria-required="false"
aria-haspopup="true"
{#key isTimeOnly}
<Flatpickr
bind:flatpickr
value={parseDate(value)}
on:open={onOpen}
on:close={onClose}
options={flatpickrOptions}
on:change={handleChange}
element={`#${flatpickrId}`}
>
<div
on:click={flatpickr?.open}
class="spectrum-Textfield spectrum-InputGroup-textfield"
id={flatpickrId}
class:is-disabled={disabled}
class:is-invalid={!!error}
class="flatpickr spectrum-InputGroup spectrum-Datepicker"
class:is-focused={open}
aria-readonly="false"
aria-required="false"
aria-haspopup="true"
>
{#if !!error}
<div
on:click={flatpickr?.open}
class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-disabled={disabled}
class:is-invalid={!!error}
>
{#if !!error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<input
data-input
type="text"
{disabled}
class="spectrum-Textfield-input spectrum-InputGroup-input"
{placeholder}
{id}
{value}
/>
</div>
<button
type="button"
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
tabindex="-1"
{disabled}
class:is-invalid={!!error}
on:click={flatpickr?.open}
>
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
class="spectrum-Icon spectrum-Icon--sizeM"
focusable="false"
aria-hidden="true"
aria-label="Calendar"
>
<use xlink:href="#spectrum-icon-18-Alert" />
<use xlink:href="#spectrum-icon-18-Calendar" />
</svg>
{/if}
<input
data-input
type="text"
{disabled}
class="spectrum-Textfield-input spectrum-InputGroup-input"
{placeholder}
{id}
{value}
/>
</button>
</div>
<button
type="button"
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
tabindex="-1"
{disabled}
class:is-invalid={!!error}
on:click={flatpickr?.open}
>
<svg
class="spectrum-Icon spectrum-Icon--sizeM"
focusable="false"
aria-hidden="true"
aria-label="Calendar"
>
<use xlink:href="#spectrum-icon-18-Calendar" />
</svg>
</button>
</div>
</Flatpickr>
</Flatpickr>
{/key}
{#if open}
<div class="overlay" on:mousedown|self={flatpickr?.close} />
{/if}

View File

@ -3,7 +3,7 @@
import "@spectrum-css/typography/dist/index-vars.css"
import "@spectrum-css/illustratedmessage/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
import { generateID } from "../../utils/helpers"
import { uuid } from "../../helpers"
import Icon from "../../Icon/Icon.svelte"
import Link from "../../Link/Link.svelte"
import Tag from "../../Tags/Tag.svelte"
@ -37,7 +37,7 @@
"jfif",
]
const fieldId = id || generateID()
const fieldId = id || uuid()
let selectedImageIdx = 0
let fileDragged = false
let selectedUrl

View File

@ -9,6 +9,7 @@
export let disabled = false
export let error = null
export let enableTime = true
export let timeOnly = false
export let placeholder = null
export let appendTo = undefined
@ -27,6 +28,7 @@
{value}
{placeholder}
{enableTime}
{timeOnly}
{appendTo}
on:change={onChange}
/>

View File

@ -2,9 +2,18 @@
import dayjs from "dayjs"
export let value
// adding the 0- will turn a string like 00:00:00 into a valid ISO
// date, but will make actual ISO dates invalid
$: time = new Date(`0-${value}`)
$: isTime = !isNaN(time)
</script>
<div>{dayjs(value).format("MMMM D YYYY, HH:mm")}</div>
<div>
{dayjs(isTime ? time : value).format(
isTime ? "HH:mm:ss" : "MMMM D YYYY, HH:mm"
)}
</div>
<style>
div {

View File

@ -4,7 +4,7 @@
import CellRenderer from "./CellRenderer.svelte"
import SelectEditRenderer from "./SelectEditRenderer.svelte"
import { cloneDeep } from "lodash"
import { deepGet } from "../utils/helpers"
import { deepGet } from "../helpers"
/**
* The expected schema is our normal couch schemas for our tables.

View File

@ -17,6 +17,7 @@
class:icon-small={size === "M" || size === "S"}
on:mouseover={() => (showTooltip = true)}
on:mouseleave={() => (showTooltip = false)}
on:focus
>
<Icon name="InfoOutline" size="S" disabled={true} />
</div>
@ -47,7 +48,7 @@
display: flex;
justify-content: center;
top: 15px;
z-index: 1;
z-index: 100;
width: 160px;
}
.icon {

View File

@ -1,11 +1,45 @@
export const generateID = () => {
const rand = Math.random().toString(32).substring(2)
// Starts with a letter so that its a valid DOM ID
return `A${rand}`
/**
* Generates a DOM safe UUID.
* Starting with a letter is important to make it DOM safe.
* @return {string} a random DOM safe UUID
*/
export function uuid() {
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0
const v = c === "x" ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
/**
* Capitalises a string
* @param string the string to capitalise
* @return {string} the capitalised string
*/
export const capitalise = string => {
if (!string) {
return string
}
return string.substring(0, 1).toUpperCase() + string.substring(1)
}
/**
* Computes a short hash of a string
* @param string the string to compute a hash of
* @return {string} the hash string
*/
export const hashString = string => {
if (!string) {
return "0"
}
let hash = 0
for (let i = 0; i < string.length; i++) {
let char = string.charCodeAt(i)
hash = (hash << 5) - hash + char
hash = hash & hash // Convert to 32bit integer
}
return hash.toString()
}
/**
* Gets a key within an object. The key supports dot syntax for retrieving deep

View File

@ -85,5 +85,5 @@ export { default as clickOutside } from "./Actions/click_outside"
// Stores
export { notifications, createNotificationStore } from "./Stores/notifications"
// Utils
export * from "./utils/helpers"
// Helpers
export * as Helpers from "./helpers"

View File

@ -15,7 +15,7 @@ process.env.COUCH_URL = `leveldb://${tmpdir}/.data/`
process.env.SELF_HOSTED = 1
process.env.WORKER_URL = "http://localhost:10002/"
process.env.APPS_URL = `http://localhost:${MAIN_PORT}/`
process.env.MINIO_URL = `http://localhost:${MAIN_PORT}/`
process.env.MINIO_URL = `http://localhost:4004`
process.env.MINIO_ACCESS_KEY = "budibase"
process.env.MINIO_SECRET_KEY = "budibase"
process.env.COUCH_DB_USER = "budibase"

View File

@ -1,13 +1,11 @@
{
"name": "@budibase/builder",
"version": "1.0.57",
"version": "1.0.58-alpha.0",
"license": "GPL-3.0",
"private": true,
"scripts": {
"build": "routify -b && vite build --emptyOutDir",
"start": "routify -c rollup",
"test": "jest",
"test:watch": "jest --watchAll",
"dev:builder": "routify -c dev:vite",
"dev:vite": "vite --host 0.0.0.0",
"rollup": "rollup -c -w",
@ -66,10 +64,10 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.0.57",
"@budibase/client": "^1.0.57",
"@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^1.0.57",
"@budibase/bbui": "^1.0.58-alpha.0",
"@budibase/client": "^1.0.58-alpha.0",
"@budibase/frontend-core": "^1.0.58-alpha.0",
"@budibase/string-templates": "^1.0.58-alpha.0",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",

View File

@ -1,4 +1,4 @@
import api from "builderStore/api"
import { API } from "api"
import PosthogClient from "./PosthogClient"
import IntercomClient from "./IntercomClient"
import SentryClient from "./SentryClient"
@ -17,13 +17,11 @@ class AnalyticsHub {
}
async activate() {
const analyticsStatus = await api.get("/api/analytics")
const json = await analyticsStatus.json()
// Analytics disabled
if (!json.enabled) return
this.clients.forEach(client => client.init())
// Check analytics are enabled
const analyticsStatus = await API.getAnalyticsStatus()
if (analyticsStatus.enabled) {
this.clients.forEach(client => client.init())
}
}
identify(id, metadata) {

View File

@ -0,0 +1,48 @@
import {
createAPIClient,
CookieUtils,
Constants,
} from "@budibase/frontend-core"
import { store } from "./builderStore"
import { get } from "svelte/store"
import { auth } from "./stores/portal"
export const API = createAPIClient({
attachHeaders: headers => {
// Attach app ID header from store
headers["x-budibase-app-id"] = get(store).appId
// Add csrf token if authenticated
const user = get(auth).user
if (user?.csrfToken) {
headers["x-csrf-token"] = user.csrfToken
}
},
onError: error => {
const { url, message, status, method, handled } = error || {}
// Log all API errors to Sentry
// analytics.captureException(error)
// Log any errors that we haven't manually handled
if (!handled) {
console.error("Unhandled error from API client", error)
return
}
// Log all errors to console
console.warn(`[Builder] HTTP ${status} on ${method}:${url}\n\t${message}`)
// Logout on 403's
if (status === 403) {
// Remove cookies
CookieUtils.removeCookie(Constants.Cookies.Auth)
// Reload after removing cookie, go to login
if (!url.includes("self") && !url.includes("login")) {
location.reload()
}
}
},
})

View File

@ -1,49 +0,0 @@
import { store } from "./index"
import { get as svelteGet } from "svelte/store"
import { removeCookie, Cookies } from "./cookies"
import { auth } from "stores/portal"
const apiCall =
method =>
async (url, body, headers = { "Content-Type": "application/json" }) => {
headers["x-budibase-app-id"] = svelteGet(store).appId
headers["x-budibase-api-version"] = "1"
// add csrf token if authenticated
const user = svelteGet(auth).user
if (user && user.csrfToken) {
headers["x-csrf-token"] = user.csrfToken
}
const json = headers["Content-Type"] === "application/json"
const resp = await fetch(url, {
method: method,
body: json ? JSON.stringify(body) : body,
headers,
})
if (resp.status === 403) {
if (url.includes("/api/templates")) {
return { json: () => [] }
}
removeCookie(Cookies.Auth)
// reload after removing cookie, go to login
if (!url.includes("self") && !url.includes("login")) {
location.reload()
}
}
return resp
}
export const post = apiCall("POST")
export const get = apiCall("GET")
export const patch = apiCall("PATCH")
export const del = apiCall("DELETE")
export const put = apiCall("PUT")
export default {
post: apiCall("POST"),
get: apiCall("GET"),
patch: apiCall("PATCH"),
delete: apiCall("DELETE"),
put: apiCall("PUT"),
}

View File

@ -15,10 +15,7 @@ import {
encodeJSBinding,
} from "@budibase/string-templates"
import { TableNames } from "../constants"
import {
convertJSONSchemaToTableSchema,
getJSONArrayDatasourceSchema,
} from "./jsonUtils"
import { JSONUtils } from "@budibase/frontend-core"
import ActionDefinitions from "components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json"
// Regex to match all instances of template strings
@ -439,7 +436,7 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
else if (type === "jsonarray") {
table = tables.find(table => table._id === datasource.tableId)
let tableSchema = table?.schema
schema = getJSONArrayDatasourceSchema(tableSchema, datasource)
schema = JSONUtils.getJSONArrayDatasourceSchema(tableSchema, datasource)
}
// Otherwise we assume we're targeting an internal table or a plus
@ -471,9 +468,12 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
Object.keys(schema).forEach(fieldKey => {
const fieldSchema = schema[fieldKey]
if (fieldSchema?.type === "json") {
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
squashObjects: true,
})
const jsonSchema = JSONUtils.convertJSONSchemaToTableSchema(
fieldSchema,
{
squashObjects: true,
}
)
Object.keys(jsonSchema).forEach(jsonKey => {
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
type: jsonSchema[jsonKey].type,

View File

@ -1,16 +0,0 @@
import { get } from "builderStore/api"
/**
* Fetches the definitions for component library components. This includes
* their props and other metadata from components.json.
* @param {string} appId - ID of the currently running app
*/
export const fetchComponentLibDefinitions = async appId => {
const LIB_DEFINITION_URL = `/api/${appId}/components/definitions`
try {
const libDefinitionResponse = await get(LIB_DEFINITION_URL)
return await libDefinitionResponse.json()
} catch (err) {
console.error(`Error fetching component definitions for ${appId}`, err)
}
}

View File

@ -1,26 +1,40 @@
import { writable } from "svelte/store"
import api from "../../api"
import { API } from "api"
import Automation from "./Automation"
import { cloneDeep } from "lodash/fp"
import analytics, { Events } from "analytics"
const initialAutomationState = {
automations: [],
blockDefinitions: {
TRIGGER: [],
ACTION: [],
},
selectedAutomation: null,
}
export const getAutomationStore = () => {
const store = writable(initialAutomationState)
store.actions = automationActions(store)
return store
}
const automationActions = store => ({
fetch: async () => {
const responses = await Promise.all([
api.get(`/api/automations`),
api.get(`/api/automations/definitions/list`),
API.getAutomations(),
API.getAutomationDefinitions(),
])
const jsonResponses = await Promise.all(responses.map(x => x.json()))
store.update(state => {
let selected = state.selectedAutomation?.automation
state.automations = jsonResponses[0]
state.automations = responses[0]
state.blockDefinitions = {
TRIGGER: jsonResponses[1].trigger,
ACTION: jsonResponses[1].action,
TRIGGER: responses[1].trigger,
ACTION: responses[1].action,
}
// if previously selected find the new obj and select it
// If previously selected find the new obj and select it
if (selected) {
selected = jsonResponses[0].filter(
selected = responses[0].filter(
automation => automation._id === selected._id
)
state.selectedAutomation = new Automation(selected[0])
@ -36,40 +50,36 @@ const automationActions = store => ({
steps: [],
},
}
const CREATE_AUTOMATION_URL = `/api/automations`
const response = await api.post(CREATE_AUTOMATION_URL, automation)
const json = await response.json()
const response = await API.createAutomation(automation)
store.update(state => {
state.automations = [...state.automations, json.automation]
store.actions.select(json.automation)
state.automations = [...state.automations, response.automation]
store.actions.select(response.automation)
return state
})
},
save: async automation => {
const UPDATE_AUTOMATION_URL = `/api/automations`
const response = await api.put(UPDATE_AUTOMATION_URL, automation)
const json = await response.json()
const response = await API.updateAutomation(automation)
store.update(state => {
const newAutomation = json.automation
const updatedAutomation = response.automation
const existingIdx = state.automations.findIndex(
existing => existing._id === automation._id
)
if (existingIdx !== -1) {
state.automations.splice(existingIdx, 1, newAutomation)
state.automations.splice(existingIdx, 1, updatedAutomation)
state.automations = [...state.automations]
store.actions.select(newAutomation)
store.actions.select(updatedAutomation)
return state
}
})
},
delete: async automation => {
const { _id, _rev } = automation
const DELETE_AUTOMATION_URL = `/api/automations/${_id}/${_rev}`
await api.delete(DELETE_AUTOMATION_URL)
await API.deleteAutomation({
automationId: automation?._id,
automationRev: automation?._rev,
})
store.update(state => {
const existingIdx = state.automations.findIndex(
existing => existing._id === _id
existing => existing._id === automation?._id
)
state.automations.splice(existingIdx, 1)
state.automations = [...state.automations]
@ -78,16 +88,17 @@ const automationActions = store => ({
return state
})
},
trigger: async automation => {
const { _id } = automation
return await api.post(`/api/automations/${_id}/trigger`)
},
test: async (automation, testData) => {
const { _id } = automation
const response = await api.post(`/api/automations/${_id}/test`, testData)
const json = await response.json()
store.update(state => {
state.selectedAutomation.testResults = json
state.selectedAutomation.testResults = null
return state
})
const result = await API.testAutomation({
automationId: automation?._id,
testData,
})
store.update(state => {
state.selectedAutomation.testResults = result
return state
})
},
@ -143,17 +154,3 @@ const automationActions = store => ({
})
},
})
export const getAutomationStore = () => {
const INITIAL_AUTOMATION_STATE = {
automations: [],
blockDefinitions: {
TRIGGER: [],
ACTION: [],
},
selectedAutomation: null,
}
const store = writable(INITIAL_AUTOMATION_STATE)
store.actions = automationActions(store)
return store
}

View File

@ -14,8 +14,7 @@ import {
database,
tables,
} from "stores/backend"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api"
import { API } from "api"
import { FrontendTypes } from "constants"
import analytics, { Events } from "analytics"
import {
@ -26,7 +25,7 @@ import {
findComponent,
getComponentSettings,
} from "../componentUtils"
import { uuid } from "../uuid"
import { Helpers } from "@budibase/bbui"
import { removeBindings } from "../dataBinding"
const INITIAL_FRONTEND_STATE = {
@ -70,15 +69,12 @@ export const getFrontendStore = () => {
},
initialise: async pkg => {
const { layouts, screens, application, clientLibPath } = pkg
const components = await fetchComponentLibDefinitions(application.appId)
// make sure app isn't locked
if (
components &&
components.status === 400 &&
components.message?.includes("lock")
) {
throw { ok: false, reason: "locked" }
}
// Fetch component definitions.
// Allow errors to propagate.
let components = await API.fetchComponentLibDefinitions(application.appId)
// Reset store state
store.update(state => ({
...state,
libraries: application.componentLibraries,
@ -91,8 +87,8 @@ export const getFrontendStore = () => {
description: application.description,
appId: application.appId,
url: application.url,
layouts,
screens,
layouts: layouts || [],
screens: screens || [],
theme: application.theme || "spectrum--light",
customTheme: application.customTheme,
hasAppPackage: true,
@ -104,51 +100,43 @@ export const getFrontendStore = () => {
}))
// Initialise backend stores
const [_integrations] = await Promise.all([
api.get("/api/integrations").then(r => r.json()),
])
datasources.init()
integrations.set(_integrations)
queries.init()
database.set(application.instance)
tables.init()
await datasources.init()
await integrations.init()
await queries.init()
await tables.init()
},
theme: {
save: async theme => {
const appId = get(store).appId
const response = await api.put(`/api/applications/${appId}`, { theme })
if (response.status === 200) {
store.update(state => {
state.theme = theme
return state
})
} else {
throw new Error("Error updating theme")
}
await API.saveAppMetadata({
appId,
metadata: { theme },
})
store.update(state => {
state.theme = theme
return state
})
},
},
customTheme: {
save: async customTheme => {
const appId = get(store).appId
const response = await api.put(`/api/applications/${appId}`, {
customTheme,
await API.saveAppMetadata({
appId,
metadata: { customTheme },
})
store.update(state => {
state.customTheme = customTheme
return state
})
if (response.status === 200) {
store.update(state => {
state.customTheme = customTheme
return state
})
} else {
throw new Error("Error updating theme")
}
},
},
routing: {
fetch: async () => {
const response = await api.get("/api/routing")
const json = await response.json()
const response = await API.fetchAppRoutes()
store.update(state => {
state.routes = json.routes
state.routes = response.routes
return state
})
},
@ -172,82 +160,76 @@ export const getFrontendStore = () => {
return state
})
},
create: async screen => {
screen = await store.actions.screens.save(screen)
store.update(state => {
state.selectedScreenId = screen._id
state.selectedComponentId = screen.props._id
state.currentFrontEndType = FrontendTypes.SCREEN
selectedAccessRole.set(screen.routing.roleId)
return state
})
return screen
},
save: async screen => {
const creatingNewScreen = screen._id === undefined
const response = await api.post(`/api/screens`, screen)
if (response.status !== 200) {
return
}
screen = await response.json()
await store.actions.routing.fetch()
const savedScreen = await API.saveScreen(screen)
store.update(state => {
const foundScreen = state.screens.findIndex(
el => el._id === screen._id
)
if (foundScreen !== -1) {
state.screens.splice(foundScreen, 1)
const idx = state.screens.findIndex(x => x._id === savedScreen._id)
if (idx !== -1) {
state.screens.splice(idx, 1, savedScreen)
} else {
state.screens.push(savedScreen)
}
state.screens.push(screen)
return state
})
if (creatingNewScreen) {
store.actions.screens.select(screen._id)
}
// Refresh routes
await store.actions.routing.fetch()
return screen
// Select the new screen if creating a new one
if (creatingNewScreen) {
store.actions.screens.select(savedScreen._id)
}
return savedScreen
},
delete: async screens => {
const screensToDelete = Array.isArray(screens) ? screens : [screens]
const screenDeletePromises = []
// Build array of promises to speed up bulk deletions
const promises = []
screensToDelete.forEach(screen => {
// Delete the screen
promises.push(
API.deleteScreen({
screenId: screen._id,
screenRev: screen._rev,
})
)
// Remove links to this screen
promises.push(
store.actions.components.links.delete(
screen.routing.route,
screen.props._instanceName
)
)
})
await Promise.all(promises)
const deletedIds = screensToDelete.map(screen => screen._id)
store.update(state => {
for (let screenToDelete of screensToDelete) {
state.screens = state.screens.filter(
screen => screen._id !== screenToDelete._id
)
screenDeletePromises.push(
api.delete(
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
)
)
if (screenToDelete._id === state.selectedScreenId) {
state.selectedScreenId = null
}
//remove the link for this screen
screenDeletePromises.push(
store.actions.components.links.delete(
screenToDelete.routing.route,
screenToDelete.props._instanceName
)
)
// Remove deleted screens from state
state.screens = state.screens.filter(screen => {
return !deletedIds.includes(screen._id)
})
// Deselect the current screen if it was deleted
if (deletedIds.includes(state.selectedScreenId)) {
state.selectedScreenId = null
}
return state
})
await Promise.all(screenDeletePromises)
// Refresh routes
await store.actions.routing.fetch()
},
},
preview: {
saveSelected: async () => {
const state = get(store)
const selectedAsset = get(currentAsset)
if (state.currentFrontEndType !== FrontendTypes.LAYOUT) {
await store.actions.screens.save(selectedAsset)
return await store.actions.screens.save(selectedAsset)
} else {
await store.actions.layouts.save(selectedAsset)
return await store.actions.layouts.save(selectedAsset)
}
},
setDevice: device => {
@ -271,25 +253,13 @@ export const getFrontendStore = () => {
})
},
save: async layout => {
const layoutToSave = cloneDeep(layout)
const creatingNewLayout = layoutToSave._id === undefined
const response = await api.post(`/api/layouts`, layoutToSave)
const savedLayout = await response.json()
// Abort if saving failed
if (response.status !== 200) {
return
}
const creatingNewLayout = layout._id === undefined
const savedLayout = await API.saveLayout(layout)
store.update(state => {
const layoutIdx = state.layouts.findIndex(
stateLayout => stateLayout._id === savedLayout._id
)
if (layoutIdx >= 0) {
// update existing layout
state.layouts.splice(layoutIdx, 1, savedLayout)
const idx = state.layouts.findIndex(x => x._id === savedLayout._id)
if (idx !== -1) {
state.layouts.splice(idx, 1, savedLayout)
} else {
// save new layout
state.layouts.push(savedLayout)
}
return state
@ -299,7 +269,6 @@ export const getFrontendStore = () => {
if (creatingNewLayout) {
store.actions.layouts.select(savedLayout._id)
}
return savedLayout
},
find: layoutId => {
@ -309,21 +278,20 @@ export const getFrontendStore = () => {
const storeContents = get(store)
return storeContents.layouts.find(layout => layout._id === layoutId)
},
delete: async layoutToDelete => {
const response = await api.delete(
`/api/layouts/${layoutToDelete._id}/${layoutToDelete._rev}`
)
if (response.status !== 200) {
const json = await response.json()
throw new Error(json.message)
delete: async layout => {
if (!layout?._id) {
return
}
await API.deleteLayout({
layoutId: layout._id,
layoutRev: layout._rev,
})
store.update(state => {
state.layouts = state.layouts.filter(
layout => layout._id !== layoutToDelete._id
)
if (layoutToDelete._id === state.selectedLayoutId) {
// Select main layout if we deleted the selected layout
if (layout._id === state.selectedLayoutId) {
state.selectedLayoutId = get(mainLayout)._id
}
state.layouts = state.layouts.filter(x => x._id !== layout._id)
return state
})
},
@ -398,7 +366,7 @@ export const getFrontendStore = () => {
}
return {
_id: uuid(),
_id: Helpers.uuid(),
_component: definition.component,
_styles: { normal: {}, hover: {}, active: {} },
_instanceName: `New ${definition.name}`,
@ -415,16 +383,12 @@ export const getFrontendStore = () => {
componentName,
presetProps
)
if (!componentInstance) {
if (!componentInstance || !asset) {
return
}
// Find parent node to attach this component to
let parentComponent
if (!asset) {
return
}
if (selected) {
// Use current screen or layout as parent if no component is selected
const definition = store.actions.components.getDefinition(
@ -552,7 +516,7 @@ export const getFrontendStore = () => {
if (!component) {
return
}
component._id = uuid()
component._id = Helpers.uuid()
component._children?.forEach(randomizeIds)
}
randomizeIds(componentToPaste)
@ -606,11 +570,6 @@ export const getFrontendStore = () => {
selected._styles.custom = style
await store.actions.preview.saveSelected()
},
resetStyles: async () => {
const selected = get(selectedComponent)
selected._styles = { normal: {}, hover: {}, active: {} }
await store.actions.preview.saveSelected()
},
updateConditions: async conditions => {
const selected = get(selectedComponent)
selected._conditions = conditions
@ -665,7 +624,7 @@ export const getFrontendStore = () => {
newLink = cloneDeep(nav._children[0])
// Set our new props
newLink._id = uuid()
newLink._id = Helpers.uuid()
newLink._instanceName = `${title} Link`
newLink.url = url
newLink.text = title

View File

@ -1,23 +0,0 @@
import { writable } from "svelte/store"
import { generate } from "shortid"
export const notificationStore = writable({
notifications: [],
})
export function send(message, type = "default") {
notificationStore.update(state => {
state.notifications = [
...state.notifications,
{ id: generate(), type, message },
]
return state
})
}
export const notifier = {
danger: msg => send(msg, "danger"),
warning: msg => send(msg, "warning"),
info: msg => send(msg, "info"),
success: msg => send(msg, "success"),
}

View File

@ -1,4 +1,4 @@
import { uuid } from "builderStore/uuid"
import { Helpers } from "@budibase/bbui"
import { BaseStructure } from "./BaseStructure"
export class Component extends BaseStructure {
@ -6,7 +6,7 @@ export class Component extends BaseStructure {
super(false)
this._children = []
this._json = {
_id: uuid(),
_id: Helpers.uuid(),
_component: name,
_styles: {
normal: {},

View File

@ -1,5 +1,5 @@
import { BaseStructure } from "./BaseStructure"
import { uuid } from "builderStore/uuid"
import { Helpers } from "@budibase/bbui"
export class Screen extends BaseStructure {
constructor() {
@ -7,7 +7,7 @@ export class Screen extends BaseStructure {
this._json = {
layoutId: "layout_private_master",
props: {
_id: uuid(),
_id: Helpers.uuid(),
_component: "@budibase/standard-components/container",
_styles: {
normal: {},

View File

@ -1,4 +1,4 @@
import { localStorageStore } from "./localStorage"
import { createLocalStorageStore } from "@budibase/frontend-core"
export const getThemeStore = () => {
const themeElement = document.documentElement
@ -6,7 +6,7 @@ export const getThemeStore = () => {
theme: "darkest",
options: ["lightest", "light", "dark", "darkest"],
}
const store = localStorageStore("bb-theme", initialValue)
const store = createLocalStorageStore("bb-theme", initialValue)
// Update theme class when store changes
store.subscribe(state => {

View File

@ -1,9 +0,0 @@
export function uuid() {
// always want to make this start with a letter, as this makes it
// easier to use with template string bindings in the client
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}

View File

@ -6,6 +6,7 @@
Body,
Icon,
Tooltip,
notifications,
} from "@budibase/bbui"
import { automationStore } from "builderStore"
import { admin } from "stores/portal"
@ -47,15 +48,19 @@
}
async function addBlockToAutomation() {
const newBlock = $automationStore.selectedAutomation.constructBlock(
"ACTION",
actionVal.stepId,
actionVal
)
automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
try {
const newBlock = $automationStore.selectedAutomation.constructBlock(
"ACTION",
actionVal.stepId,
actionVal
)
automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} catch (error) {
notifications.error("Error saving automation")
}
}
</script>

View File

@ -30,26 +30,13 @@
}
async function deleteAutomation() {
await automationStore.actions.delete(
$automationStore.selectedAutomation?.automation
)
notifications.success("Automation deleted.")
}
async function testAutomation() {
const result = await automationStore.actions.trigger(
$automationStore.selectedAutomation.automation
)
if (result.status === 200) {
notifications.success(
`Automation ${$automationStore.selectedAutomation.automation.name} triggered successfully.`
)
} else {
notifications.error(
`Failed to trigger automation ${$automationStore.selectedAutomation.automation.name}.`
try {
await automationStore.actions.delete(
$automationStore.selectedAutomation?.automation
)
} catch (error) {
notifications.error("Error deleting automation")
}
return result
}
</script>
@ -85,7 +72,7 @@
animate:flip={{ duration: 500 }}
in:fly|local={{ x: 500, duration: 1500 }}
>
<FlowItem {testDataModal} {testAutomation} {onSelect} {block} />
<FlowItem {testDataModal} {onSelect} {block} />
</div>
{/each}
</div>
@ -101,7 +88,7 @@
</ConfirmDialog>
<Modal bind:this={testDataModal} width="30%">
<TestDataModal {testAutomation} />
<TestDataModal />
</Modal>
</div>

View File

@ -10,6 +10,7 @@
Button,
StatusLight,
ActionButton,
notifications,
} from "@budibase/bbui"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
@ -54,10 +55,14 @@
).every(x => block?.inputs[x])
async function deleteStep() {
automationStore.actions.deleteAutomationBlock(block)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
try {
automationStore.actions.deleteAutomationBlock(block)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} catch (error) {
notifications.error("Error saving notification")
}
}
</script>

View File

@ -1,5 +1,12 @@
<script>
import { ModalContent, Tabs, Tab, TextArea, Label } from "@budibase/bbui"
import {
ModalContent,
Tabs,
Tab,
TextArea,
Label,
notifications,
} from "@budibase/bbui"
import { automationStore } from "builderStore"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import { cloneDeep } from "lodash/fp"
@ -37,6 +44,17 @@
failedParse = "Invalid JSON"
}
}
const testAutomation = async () => {
try {
await automationStore.actions.test(
$automationStore.selectedAutomation?.automation,
testData
)
} catch (error) {
notifications.error("Error testing notification")
}
}
</script>
<ModalContent
@ -44,12 +62,7 @@
confirmText="Test"
showConfirmButton={true}
disabled={isError}
onConfirm={() => {
automationStore.actions.test(
$automationStore.selectedAutomation?.automation,
testData
)
}}
onConfirm={testAutomation}
cancelText="Cancel"
>
<Tabs selected="Form" quiet

View File

@ -4,10 +4,16 @@
import { automationStore } from "builderStore"
import NavItem from "components/common/NavItem.svelte"
import EditAutomationPopover from "./EditAutomationPopover.svelte"
import { notifications } from "@budibase/bbui"
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
onMount(() => {
automationStore.actions.fetch()
onMount(async () => {
try {
await automationStore.actions.fetch()
} catch (error) {
notifications.error("Error getting automations list")
}
})
function selectAutomation(automation) {

View File

@ -24,29 +24,33 @@
nameTouched && !name ? "Please specify a name for the automation." : null
async function createAutomation() {
await automationStore.actions.create({
name,
instanceId,
})
const newBlock = $automationStore.selectedAutomation.constructBlock(
"TRIGGER",
triggerVal.stepId,
triggerVal
)
try {
await automationStore.actions.create({
name,
instanceId,
})
const newBlock = $automationStore.selectedAutomation.constructBlock(
"TRIGGER",
triggerVal.stepId,
triggerVal
)
automationStore.actions.addBlockToAutomation(newBlock)
if (triggerVal.stepId === "WEBHOOK") {
webhookModal.show
automationStore.actions.addBlockToAutomation(newBlock)
if (triggerVal.stepId === "WEBHOOK") {
webhookModal.show
}
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
notifications.success(`Automation ${name} created`)
$goto(`./${$automationStore.selectedAutomation.automation._id}`)
analytics.captureEvent(Events.AUTOMATION.CREATED, { name })
} catch (error) {
notifications.error("Error creating automation")
}
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
notifications.success(`Automation ${name} created.`)
$goto(`./${$automationStore.selectedAutomation.automation._id}`)
analytics.captureEvent(Events.AUTOMATION.CREATED, { name })
}
$: triggers = Object.entries($automationStore.blockDefinitions.TRIGGER)

View File

@ -11,9 +11,13 @@
let updateAutomationDialog
async function deleteAutomation() {
await automationStore.actions.delete(automation)
notifications.success("Automation deleted.")
$goto("../automate")
try {
await automationStore.actions.delete(automation)
notifications.success("Automation deleted successfully")
$goto("../automate")
} catch (error) {
notifications.error("Error deleting automation")
}
}
</script>

View File

@ -20,14 +20,18 @@
}
async function saveAutomation() {
const updatedAutomation = {
...automation,
name,
try {
const updatedAutomation = {
...automation,
name,
}
await automationStore.actions.save(updatedAutomation)
notifications.success(`Automation ${name} updated successfully`)
analytics.captureEvent(Events.AUTOMATION.SAVED, { name })
hide()
} catch (error) {
notifications.error("Error saving automation")
}
await automationStore.actions.save(updatedAutomation)
notifications.success(`Automation ${name} updated successfully.`)
analytics.captureEvent(Events.AUTOMATION.SAVED, { name })
hide()
}
function checkValid(evt) {

View File

@ -11,6 +11,7 @@
Drawer,
Modal,
Detail,
notifications,
} from "@budibase/bbui"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
@ -28,7 +29,7 @@
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
// need the client lucene builder to convert to the structure API expects
import { buildLuceneQuery } from "helpers/lucene"
import { LuceneUtils } from "@budibase/frontend-core"
export let block
export let testData
@ -54,28 +55,32 @@
$: schemaFields = table ? Object.values(table.schema) : []
const onChange = debounce(async function (e, key) {
if (isTestModal) {
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
if (stepId === "WEBHOOK") {
try {
if (isTestModal) {
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
if (stepId === "WEBHOOK") {
automationStore.actions.addTestDataToAutomation({
body: {
[key]: e.detail,
...$automationStore.selectedAutomation.automation.testData.body,
},
})
}
automationStore.actions.addTestDataToAutomation({
body: {
[key]: e.detail,
...$automationStore.selectedAutomation.automation.testData.body,
},
[key]: e.detail,
})
testData[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} else {
block.inputs[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
}
automationStore.actions.addTestDataToAutomation({
[key]: e.detail,
})
testData[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} else {
block.inputs[key] = e.detail
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
} catch (error) {
notifications.error("Error saving automation")
}
}, 800)
@ -131,7 +136,7 @@
}
function saveFilters(key) {
const filters = buildLuceneQuery(tempFilters)
const filters = LuceneUtils.buildLuceneQuery(tempFilters)
const defKey = `${key}-def`
inputData[key] = filters
inputData[defKey] = tempFilters

View File

@ -1,5 +1,5 @@
<script>
import { Icon } from "@budibase/bbui"
import { Icon, notifications } from "@budibase/bbui"
import { automationStore } from "builderStore"
import WebhookDisplay from "./WebhookDisplay.svelte"
import { ModalContent } from "@budibase/bbui"
@ -16,15 +16,24 @@
onMount(async () => {
if (!automation?.definition?.trigger?.inputs.schemaUrl) {
// save the automation initially
await automationStore.actions.save(automation)
try {
await automationStore.actions.save(automation)
} catch (error) {
notifications.error("Error saving automation")
}
}
interval = setInterval(async () => {
await automationStore.actions.fetch()
const outputs = automation?.definition?.trigger.schema.outputs?.properties
// always one prop for the "body"
if (Object.keys(outputs).length > 1) {
propCount = Object.keys(outputs).length - 1
finished = true
try {
await automationStore.actions.fetch()
const outputs =
automation?.definition?.trigger.schema.outputs?.properties
// always one prop for the "body"
if (Object.keys(outputs).length > 1) {
propCount = Object.keys(outputs).length - 1
finished = true
}
} catch (error) {
notifications.error("Error getting automations list")
}
}, POLL_RATE_MS)
schemaURL = automation?.definition?.trigger?.inputs.schemaUrl

View File

@ -14,18 +14,19 @@
import Table from "./Table.svelte"
import { TableNames } from "constants"
import CreateEditRow from "./modals/CreateEditRow.svelte"
import { fetchTableData } from "helpers/fetchTableData"
import { Pagination } from "@budibase/bbui"
import { fetchData } from "@budibase/frontend-core"
import { API } from "api"
let hideAutocolumns = true
$: isUsersTable = $tables.selected?._id === TableNames.USERS
$: type = $tables.selected?.type
$: isInternal = type !== "external"
$: schema = $tables.selected?.schema
$: enrichedSchema = enrichSchema($tables.selected?.schema)
$: id = $tables.selected?._id
$: search = searchTable(id)
$: columnOptions = Object.keys($search.schema || {})
$: fetch = createFetch(id)
const enrichSchema = schema => {
let tempSchema = { ...schema }
@ -47,18 +48,24 @@
return tempSchema
}
// Fetches new data whenever the table changes
const searchTable = tableId => {
return fetchTableData({
tableId,
schema,
limit: 10,
paginate: true,
const createFetch = tableId => {
return fetchData({
API,
datasource: {
tableId,
type: "table",
},
options: {
schema,
limit: 10,
paginate: true,
},
})
}
// Fetch data whenever sorting option changes
const onSort = e => {
search.update({
fetch.update({
sortColumn: e.detail.column,
sortOrder: e.detail.order,
})
@ -66,22 +73,20 @@
// Fetch data whenever filters change
const onFilter = e => {
search.update({
filters: e.detail,
fetch.update({
filter: e.detail,
})
}
// Fetch data whenever schema changes
const onUpdateColumns = () => {
search.update({
schema,
})
fetch.refresh()
}
// Fetch data whenever rows are modified. Unfortunately we have to lose
// our pagination place, as our bookmarks will have shifted.
const onUpdateRows = () => {
search.update()
fetch.refresh()
}
</script>
@ -91,9 +96,9 @@
schema={enrichedSchema}
{type}
tableId={id}
data={$search.rows}
data={$fetch.rows}
bind:hideAutocolumns
loading={$search.loading}
loading={$fetch.loading}
on:sort={onSort}
allowEditing
disableSorting
@ -138,11 +143,11 @@
<div in:fade={{ delay: 200, duration: 100 }}>
<div class="pagination">
<Pagination
page={$search.pageNumber + 1}
hasPrevPage={$search.hasPrevPage}
hasNextPage={$search.hasNextPage}
goToPrevPage={$search.loading ? null : search.prevPage}
goToNextPage={$search.loading ? null : search.nextPage}
page={$fetch.pageNumber + 1}
hasPrevPage={$fetch.hasPrevPage}
hasNextPage={$fetch.hasNextPage}
goToPrevPage={$fetch.loading ? null : fetch.prevPage}
goToNextPage={$fetch.loading ? null : fetch.nextPage}
/>
</div>
</div>

View File

@ -1,7 +1,8 @@
<script>
import api from "builderStore/api"
import { API } from "api"
import Table from "./Table.svelte"
import { tables } from "stores/backend"
import { notifications } from "@budibase/bbui"
export let tableId
export let rowId
@ -27,9 +28,15 @@
}
async function fetchData(tableId, rowId) {
const QUERY_VIEW_URL = `/api/${tableId}/${rowId}/enrich`
const response = await api.get(QUERY_VIEW_URL)
row = await response.json()
try {
row = await API.fetchRelationshipData({
tableId,
rowId,
})
} catch (error) {
row = null
notifications.error("Error fetching relationship data")
}
}
</script>

View File

@ -2,7 +2,7 @@
import { fade } from "svelte/transition"
import { goto, params } from "@roxi/routify"
import { Table, Modal, Heading, notifications, Layout } from "@budibase/bbui"
import api from "builderStore/api"
import { API } from "api"
import Spinner from "components/common/Spinner.svelte"
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
import CreateEditRow from "./modals/CreateEditRow.svelte"
@ -88,12 +88,17 @@
}
const deleteRows = async () => {
await api.delete(`/api/${tableId}/rows`, {
rows: selectedRows,
})
data = data.filter(row => !selectedRows.includes(row))
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
selectedRows = []
try {
await API.deleteRows({
tableId,
rows: selectedRows,
})
data = data.filter(row => !selectedRows.includes(row))
notifications.success(`Successfully deleted ${selectedRows.length} rows`)
selectedRows = []
} catch (error) {
notifications.error("Error deleting rows")
}
}
const editRow = row => {

View File

@ -1,5 +1,5 @@
<script>
import api from "builderStore/api"
import { API } from "api"
import { tables } from "stores/backend"
import Table from "./Table.svelte"
@ -9,6 +9,7 @@
import ExportButton from "./buttons/ExportButton.svelte"
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
import { notifications } from "@budibase/bbui"
export let view = {}
@ -20,33 +21,31 @@
$: name = view.name
// Fetch rows for specified view
$: {
loading = true
fetchViewData(name, view.field, view.groupBy, view.calculation)
}
$: fetchViewData(name, view.field, view.groupBy, view.calculation)
async function fetchViewData(name, field, groupBy, calculation) {
loading = true
const _tables = $tables.list
const allTableViews = _tables.map(table => table.views)
const thisView = allTableViews.filter(
views => views != null && views[name] != null
)[0]
// don't fetch view data if the view no longer exists
// Don't fetch view data if the view no longer exists
if (!thisView) {
loading = false
return
}
const params = new URLSearchParams()
if (calculation) {
params.set("field", field)
params.set("calculation", calculation)
try {
data = await API.fetchViewData({
name,
calculation,
field,
groupBy,
})
} catch (error) {
notifications.error("Error fetching view data")
}
if (groupBy) {
params.set("group", groupBy)
}
const QUERY_VIEW_URL = `/api/views/${name}?${params}`
const response = await api.get(QUERY_VIEW_URL)
data = await response.json()
loading = false
}
</script>

View File

@ -1,34 +0,0 @@
import api from "builderStore/api"
export async function createUser(user) {
const CREATE_USER_URL = `/api/users/metadata`
const response = await api.post(CREATE_USER_URL, user)
return await response.json()
}
export async function saveRow(row, tableId) {
const SAVE_ROW_URL = `/api/${tableId}/rows`
const response = await api.post(SAVE_ROW_URL, row)
return await response.json()
}
export async function deleteRow(row) {
const DELETE_ROWS_URL = `/api/${row.tableId}/rows`
return api.delete(DELETE_ROWS_URL, {
_id: row._id,
_rev: row._rev,
})
}
export async function fetchDataForTable(tableId) {
const FETCH_ROWS_URL = `/api/${tableId}/rows`
const response = await api.get(FETCH_ROWS_URL)
const json = await response.json()
if (response.status !== 200) {
throw new Error(json.message)
}
return json
}

View File

@ -38,9 +38,13 @@
})
function saveView() {
views.save(view)
notifications.success(`View ${view.name} saved.`)
analytics.captureEvent(Events.VIEW.ADDED_CALCULATE, { field: view.field })
try {
views.save(view)
notifications.success(`View ${view.name} saved`)
analytics.captureEvent(Events.VIEW.ADDED_CALCULATE, { field: view.field })
} catch (error) {
notifications.error("Error saving view")
}
}
</script>

View File

@ -124,7 +124,7 @@
})
dispatch("updatecolumns")
} catch (err) {
notifications.error(err)
notifications.error("Error saving column")
}
}
@ -133,17 +133,21 @@
}
function deleteColumn() {
field.name = deleteColName
if (field.name === $tables.selected.primaryDisplay) {
notifications.error("You cannot delete the display column")
} else {
tables.deleteField(field)
notifications.success(`Column ${field.name} deleted.`)
confirmDeleteDialog.hide()
hide()
deletion = false
try {
field.name = deleteColName
if (field.name === $tables.selected.primaryDisplay) {
notifications.error("You cannot delete the display column")
} else {
tables.deleteField(field)
notifications.success(`Column ${field.name} deleted.`)
confirmDeleteDialog.hide()
hide()
deletion = false
dispatch("updatecolumns")
}
} catch (error) {
notifications.error("Error deleting column")
}
dispatch("updatecolumns")
}
function handleTypeChange(event) {

View File

@ -3,7 +3,7 @@
import { tables, rows } from "stores/backend"
import { notifications } from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte"
import * as api from "../api"
import { API } from "api"
import { ModalContent } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import { FIELDS } from "constants/backend"
@ -22,30 +22,30 @@
$: tableSchema = Object.entries(table?.schema ?? {})
async function saveRow() {
const rowResponse = await api.saveRow(
{ ...row, tableId: table._id },
table._id
)
if (rowResponse.errors) {
errors = Object.entries(rowResponse.errors)
.map(([key, error]) => ({ dataPath: key, message: error }))
.flat()
errors = []
try {
await API.saveRow({ ...row, tableId: table._id })
notifications.success("Row saved successfully")
rows.save()
dispatch("updaterows")
} catch (error) {
if (error.handled) {
const response = error.json
if (response?.errors) {
errors = Object.entries(response.errors)
.map(([key, error]) => ({ dataPath: key, message: error }))
.flat()
} else if (error.status === 400 && response?.validationErrors) {
errors = Object.keys(response.validationErrors).map(field => ({
message: `${field} ${response.validationErrors[field][0]}`,
}))
}
} else {
notifications.error("Failed to save row")
}
// Prevent modal closing if there were errors
return false
} else if (rowResponse.status === 400 && rowResponse.validationErrors) {
errors = Object.keys(rowResponse.validationErrors).map(field => ({
message: `${field} ${rowResponse.validationErrors[field][0]}`,
}))
return false
} else if (rowResponse.status >= 400) {
errors = [{ message: rowResponse.message }]
return false
}
notifications.success("Row saved successfully.")
rows.save(rowResponse)
dispatch("updaterows")
}
</script>

View File

@ -4,7 +4,7 @@
import { roles } from "stores/backend"
import { notifications } from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte"
import * as backendApi from "../api"
import { API } from "api"
import { ModalContent, Select } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
@ -53,27 +53,31 @@
return false
}
const rowResponse = await backendApi.saveRow(
{ ...row, tableId: table._id },
table._id
)
if (rowResponse.errors) {
if (Array.isArray(rowResponse.errors)) {
errors = rowResponse.errors.map(error => ({ message: error }))
try {
await API.saveRow({ ...row, tableId: table._id })
notifications.success("User saved successfully")
rows.save()
dispatch("updaterows")
} catch (error) {
if (error.handled) {
const response = error.json
if (response?.errors) {
if (Array.isArray(response.errors)) {
errors = response.errors.map(error => ({ message: error }))
} else {
errors = Object.entries(response.errors)
.map(([key, error]) => ({ dataPath: key, message: error }))
.flat()
}
} else if (error.status === 400) {
errors = [{ message: response?.message || "Unknown error" }]
}
} else {
errors = Object.entries(rowResponse.errors)
.map(([key, error]) => ({ dataPath: key, message: error }))
.flat()
notifications.error("Error saving user")
}
return false
} else if (rowResponse.status === 400 || rowResponse.status === 500) {
errors = [{ message: rowResponse.message }]
// Prevent closing the modal on errors
return false
}
notifications.success("User saved successfully")
rows.save(rowResponse)
dispatch("updaterows")
}
</script>

View File

@ -12,17 +12,21 @@
function saveView() {
if (views.includes(name)) {
notifications.error(`View exists with name ${name}.`)
notifications.error(`View exists with name ${name}`)
return
}
viewsStore.save({
name,
tableId: $tables.selected._id,
field,
})
notifications.success(`View ${name} created`)
analytics.captureEvent(Events.VIEW.CREATED, { name })
$goto(`../../view/${name}`)
try {
viewsStore.save({
name,
tableId: $tables.selected._id,
field,
})
notifications.success(`View ${name} created`)
analytics.captureEvent(Events.VIEW.CREATED, { name })
$goto(`../../view/${name}`)
} catch (error) {
notifications.error("Error creating view")
}
}
</script>

View File

@ -1,7 +1,7 @@
<script>
import { ModalContent, Select, Input, Button } from "@budibase/bbui"
import { onMount } from "svelte"
import api from "builderStore/api"
import { API } from "api"
import { notifications } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import { roles } from "stores/backend"
@ -24,8 +24,12 @@
!builtInRoles.includes(selectedRole.name)
const fetchBasePermissions = async () => {
const permissionsResponse = await api.get("/api/permission/builtin")
basePermissions = await permissionsResponse.json()
try {
basePermissions = await API.getBasePermissions()
} catch (error) {
notifications.error("Error fetching base permission options")
basePermissions = []
}
}
// Changes the selected role
@ -68,23 +72,23 @@
}
// Save/create the role
const response = await roles.save(selectedRole)
if (response.status === 200) {
notifications.success("Role saved successfully.")
} else {
notifications.error("Error saving role.")
try {
await roles.save(selectedRole)
notifications.success("Role saved successfully")
} catch (error) {
notifications.error("Error saving role")
return false
}
}
// Deletes the selected role
const deleteRole = async () => {
const response = await roles.delete(selectedRole)
if (response.status === 200) {
try {
await roles.delete(selectedRole)
changeRole()
notifications.success("Role deleted successfully.")
} else {
notifications.error("Error deleting role.")
notifications.success("Role deleted successfully")
} catch (error) {
notifications.error("Error deleting role")
}
}

View File

@ -1,7 +1,7 @@
<script>
import { Select, ModalContent, notifications } from "@budibase/bbui"
import download from "downloadjs"
import { get } from "builderStore/api"
import { API } from "api"
const FORMATS = [
{
@ -19,17 +19,14 @@
let exportFormat = FORMATS[0].key
async function exportView() {
const uri = encodeURIComponent(view)
const response = await get(
`/api/views/export?view=${uri}&format=${exportFormat}`
)
if (response.status === 200) {
const data = await response.text()
try {
const data = await API.exportView({
viewName: view,
format: exportFormat,
})
download(data, `export.${exportFormat}`)
} else {
notifications.error(
`Unable to export ${exportFormat.toUpperCase()} data.`
)
} catch (error) {
notifications.error(`Unable to export ${exportFormat.toUpperCase()} data`)
}
}
</script>

View File

@ -72,11 +72,15 @@
$: schema = viewTable && viewTable.schema ? viewTable.schema : {}
function saveView() {
views.save(view)
notifications.success(`View ${view.name} saved.`)
analytics.captureEvent(Events.VIEW.ADDED_FILTER, {
filters: JSON.stringify(view.filters),
})
try {
views.save(view)
notifications.success(`View ${view.name} saved`)
analytics.captureEvent(Events.VIEW.ADDED_FILTER, {
filters: JSON.stringify(view.filters),
})
} catch (error) {
notifications.error("Error saving view")
}
}
function removeFilter(idx) {
@ -158,7 +162,7 @@
<Select
bind:value={filter.value}
options={fieldOptions(filter.key)}
getOptionLabel={x => x.toString()}
getOptionLabel={x => x?.toString() || ""}
/>
{:else if filter.key && isDate(filter.key)}
<DatePicker

View File

@ -19,8 +19,12 @@
.map(([key]) => key)
function saveView() {
views.save(view)
notifications.success(`View ${view.name} saved.`)
try {
views.save(view)
notifications.success(`View ${view.name} saved`)
} catch (error) {
notifications.error("Error saving view")
}
}
</script>

View File

@ -1,7 +1,13 @@
<script>
import { ModalContent, Label, notifications, Body } from "@budibase/bbui"
import {
ModalContent,
Label,
notifications,
Body,
Layout,
} from "@budibase/bbui"
import TableDataImport from "../../TableNavigator/TableDataImport.svelte"
import api from "builderStore/api"
import { API } from "api"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
@ -12,15 +18,17 @@
$: valid = dataImport?.csvString != null && dataImport?.valid
async function importData() {
const response = await api.post(`/api/tables/${tableId}/import`, {
dataImport,
})
if (response.status !== 200) {
const error = await response.text()
notifications.error(`Unable to import data - ${error}`)
} else {
notifications.success("Rows successfully imported.")
try {
await API.importTableData({
tableId,
data: dataImport,
})
notifications.success("Rows successfully imported")
} catch (error) {
notifications.error("Unable to import data")
}
// Always refresh rows just to be sure
dispatch("updaterows")
}
</script>
@ -31,12 +39,14 @@
onConfirm={importData}
disabled={!valid}
>
<Body
>Import rows to an existing table from a CSV. Only columns from the CSV
which exist in the table will be imported.</Body
>
<Label grey extraSmall>CSV to import</Label>
<TableDataImport bind:dataImport bind:existingTableId={tableId} />
<Body size="S">
Import rows to an existing table from a CSV. Only columns from the CSV which
exist in the table will be imported.
</Body>
<Layout gap="XS" noPadding>
<Label grey extraSmall>CSV to import</Label>
<TableDataImport bind:dataImport bind:existingTableId={tableId} />
</Layout>
</ModalContent>
<style>

View File

@ -14,15 +14,19 @@
export let permissions
async function changePermission(level, role) {
await permissionsStore.save({
level,
role,
resource: resourceId,
})
try {
await permissionsStore.save({
level,
role,
resource: resourceId,
})
// Show updated permissions in UI: REMOVE
permissions = await permissionsStore.forResource(resourceId)
notifications.success("Updated permissions.")
// Show updated permissions in UI: REMOVE
permissions = await permissionsStore.forResource(resourceId)
notifications.success("Updated permissions")
} catch (error) {
notifications.error("Error updating permissions")
}
}
</script>

View File

@ -10,6 +10,7 @@
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
import { customQueryIconText, customQueryIconColor } from "helpers/data/utils"
import ICONS from "./icons"
import { notifications } from "@budibase/bbui"
let openDataSources = []
$: enrichedDataSources = Array.isArray($datasources.list)
@ -63,9 +64,13 @@
}
}
onMount(() => {
datasources.fetch()
queries.fetch()
onMount(async () => {
try {
await datasources.fetch()
await queries.fetch()
} catch (error) {
notifications.error("Error fetching datasources and queries")
}
})
const containsActiveEntity = datasource => {

View File

@ -1,5 +1,5 @@
<script>
import { ModalContent, Body, Input } from "@budibase/bbui"
import { ModalContent, Body, Input, notifications } from "@budibase/bbui"
import { tables, datasources } from "stores/backend"
import { goto } from "@roxi/routify"
@ -29,10 +29,14 @@
}
async function saveTable() {
submitted = true
const table = await tables.save(buildDefaultTable(name, datasource._id))
await datasources.fetch()
$goto(`../../table/${table._id}`)
try {
submitted = true
const table = await tables.save(buildDefaultTable(name, datasource._id))
await datasources.fetch()
$goto(`../../table/${table._id}`)
} catch (error) {
notifications.error("Error saving table")
}
}
</script>

View File

@ -90,8 +90,8 @@
await datasources.updateSchema(datasource)
notifications.success(`Datasource ${name} tables updated successfully.`)
await tables.fetch()
} catch (err) {
notifications.error(`Error updating datasource schema: ${err}`)
} catch (error) {
notifications.error("Error updating datasource schema")
}
}

View File

@ -1,7 +1,7 @@
<script>
import { Body } from "@budibase/bbui"
import { Body, notifications } from "@budibase/bbui"
import { onMount } from "svelte"
import api from "builderStore/api"
import { API } from "api"
import ICONS from "../icons"
export let integration = {}
@ -9,14 +9,17 @@
const INTERNAL = "BUDIBASE"
async function fetchIntegrations() {
const response = await api.get("/api/integrations")
const json = await response.json()
let otherIntegrations
try {
otherIntegrations = await API.getIntegrations()
} catch (error) {
otherIntegrations = {}
notifications.error("Error getting integrations")
}
integrations = {
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
...json,
...otherIntegrations,
}
return json
}
function selectIntegration(integrationType) {

View File

@ -2,7 +2,7 @@
import { Table, Modal, Layout, ActionButton } from "@budibase/bbui"
import AuthTypeRenderer from "./AuthTypeRenderer.svelte"
import RestAuthenticationModal from "./RestAuthenticationModal.svelte"
import { uuid } from "builderStore/uuid"
import { Helpers } from "@budibase/bbui"
export let configs = []
@ -29,7 +29,7 @@
return c
})
} else {
config._id = uuid()
config._id = Helpers.uuid()
configs = [...configs, config]
}
}

View File

@ -1,8 +1,15 @@
<script>
import { ModalContent, Modal, Body, Layout, Detail } from "@budibase/bbui"
import {
ModalContent,
Modal,
Body,
Layout,
Detail,
notifications,
} from "@budibase/bbui"
import { onMount } from "svelte"
import ICONS from "../icons"
import api from "builderStore/api"
import { API } from "api"
import { IntegrationNames, IntegrationTypes } from "constants/backend"
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
@ -12,7 +19,7 @@
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
export let modal
let integrations = []
let integrations = {}
let integration = {}
let internalTableModal
let externalDatasourceModal
@ -57,22 +64,32 @@
externalDatasourceModal.hide()
internalTableModal.show()
} else if (integration.type === IntegrationTypes.REST) {
// skip modal for rest, create straight away
const resp = await createRestDatasource(integration)
$goto(`./datasource/${resp._id}`)
try {
// Skip modal for rest, create straight away
const resp = await createRestDatasource(integration)
$goto(`./datasource/${resp._id}`)
} catch (error) {
notifications.error("Error creating datasource")
}
} else {
externalDatasourceModal.show()
}
}
async function fetchIntegrations() {
const response = await api.get("/api/integrations")
const json = await response.json()
integrations = {
let newIntegrations = {
[IntegrationTypes.INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
...json,
}
return json
try {
const integrationList = await API.getIntegrations()
newIntegrations = {
...newIntegrations,
...integrationList,
}
} catch (error) {
notifications.error("Error fetching integrations")
}
integrations = newIntegrations
}
</script>

View File

@ -20,7 +20,7 @@
$goto(`./datasource/${resp._id}`)
notifications.success(`Datasource updated successfully.`)
} catch (err) {
notifications.error(`Error saving datasource: ${err}`)
notifications.error("Error saving datasource")
}
}

View File

@ -79,8 +79,8 @@
})
return true
} catch (err) {
notifications.error(`Error importing: ${err}`)
} catch (error) {
notifications.error("Error importing queries")
return false
}
}

View File

@ -12,24 +12,28 @@
let updateDatasourceDialog
async function deleteDatasource() {
let wasSelectedSource = $datasources.selected
if (!wasSelectedSource && $queries.selected) {
const queryId = $queries.selected
wasSelectedSource = $datasources.list.find(ds =>
queryId.includes(ds._id)
)?._id
}
const wasSelectedTable = $tables.selected
await datasources.delete(datasource)
notifications.success("Datasource deleted")
// navigate to first index page if the source you are deleting is selected
const entities = Object.values(datasource?.entities || {})
if (
wasSelectedSource === datasource._id ||
(entities &&
entities.find(entity => entity._id === wasSelectedTable?._id))
) {
$goto("./datasource")
try {
let wasSelectedSource = $datasources.selected
if (!wasSelectedSource && $queries.selected) {
const queryId = $queries.selected
wasSelectedSource = $datasources.list.find(ds =>
queryId.includes(ds._id)
)?._id
}
const wasSelectedTable = $tables.selected
await datasources.delete(datasource)
notifications.success("Datasource deleted")
// Navigate to first index page if the source you are deleting is selected
const entities = Object.values(datasource?.entities || {})
if (
wasSelectedSource === datasource._id ||
(entities &&
entities.find(entity => entity._id === wasSelectedTable?._id))
) {
$goto("./datasource")
}
} catch (error) {
notifications.error("Error deleting datasource")
}
}
</script>

View File

@ -10,26 +10,30 @@
let confirmDeleteDialog
async function deleteQuery() {
const wasSelectedQuery = $queries.selected
// need to calculate this before the query is deleted
const navigateToDatasource = wasSelectedQuery === query._id
try {
const wasSelectedQuery = $queries.selected
// need to calculate this before the query is deleted
const navigateToDatasource = wasSelectedQuery === query._id
await queries.delete(query)
await datasources.fetch()
await queries.delete(query)
await datasources.fetch()
if (navigateToDatasource) {
await datasources.select(query.datasourceId)
$goto(`./datasource/${query.datasourceId}`)
if (navigateToDatasource) {
await datasources.select(query.datasourceId)
$goto(`./datasource/${query.datasourceId}`)
}
notifications.success("Query deleted")
} catch (error) {
notifications.error("Error deleting query")
}
notifications.success("Query deleted")
}
async function duplicateQuery() {
try {
const newQuery = await queries.duplicate(query)
onClickQuery(newQuery)
} catch (e) {
notifications.error(e.message)
} catch (error) {
notifications.error("Error duplicating query")
}
}
</script>

View File

@ -9,7 +9,7 @@
Body,
} from "@budibase/bbui"
import { tables } from "stores/backend"
import { uuid } from "builderStore/uuid"
import { Helpers } from "@budibase/bbui"
import { writable } from "svelte/store"
export let save
@ -140,7 +140,7 @@
const manyToMany =
fromRelationship.relationshipType === RelationshipTypes.MANY_TO_MANY
// main is simply used to know this is the side the user configured it from
const id = uuid()
const id = Helpers.uuid()
if (!manyToMany) {
delete fromRelationship.through
delete toRelationship.through

View File

@ -1,7 +1,7 @@
<script>
import { Select, InlineAlert, notifications } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import api from "builderStore/api"
import { API } from "api"
const BYTES_IN_MB = 1000000
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
@ -50,28 +50,26 @@
}
async function validateCSV() {
const response = await api.post("/api/tables/csv/validate", {
csvString,
schema: schema || {},
tableId: existingTableId,
})
try {
const parseResult = await API.validateTableCSV({
csvString,
schema: schema || {},
tableId: existingTableId,
})
schema = parseResult?.schema
fields = Object.keys(schema || {}).filter(
key => schema[key].type !== "omit"
)
const parseResult = await response.json()
schema = parseResult && parseResult.schema
fields = Object.keys(schema || {}).filter(
key => schema[key].type !== "omit"
)
// Check primary display is valid
if (!primaryDisplay || fields.indexOf(primaryDisplay) === -1) {
primaryDisplay = fields[0]
}
// Check primary display is valid
if (!primaryDisplay || fields.indexOf(primaryDisplay) === -1) {
primaryDisplay = fields[0]
}
if (response.status !== 200) {
hasValidated = true
} catch (error) {
notifications.error("CSV Invalid, please try another CSV file")
return []
}
hasValidated = true
}
async function handleFile(evt) {

View File

@ -49,8 +49,8 @@
if (wasSelectedTable && wasSelectedTable._id === table._id) {
$goto("./table")
}
} catch (err) {
notifications.error(err)
} catch (error) {
notifications.error("Error deleting table")
}
}

View File

@ -27,11 +27,15 @@
}
async function deleteView() {
const name = view.name
const id = view.tableId
await views.delete(name)
notifications.success("View deleted")
$goto(`./table/${id}`)
try {
const name = view.name
const id = view.tableId
await views.delete(name)
notifications.success("View deleted")
$goto(`./table/${id}`)
} catch (error) {
notifications.error("Error deleting view")
}
}
</script>

View File

@ -1,6 +1,6 @@
<script>
import { Dropzone, notifications } from "@budibase/bbui"
import api from "builderStore/api"
import { API } from "api"
export let value = []
export let label
@ -20,8 +20,12 @@
for (let i = 0; i < fileList.length; i++) {
data.append("file", fileList[i])
}
const response = await api.post(`/api/attachments/process`, data, {})
return await response.json()
try {
return await API.uploadBuilderAttachment(data)
} catch (error) {
notifications.error("Failed to upload attachment")
return []
}
}
</script>

View File

@ -1,6 +1,6 @@
<script>
import { tables } from "stores/backend"
import api from "builderStore/api"
import { API } from "api"
import { Select, Label, Multiselect } from "@budibase/bbui"
import { capitalise } from "../../helpers"
@ -17,12 +17,9 @@
$: fetchRows(linkedTableId)
async function fetchRows(linkedTableId) {
const FETCH_ROWS_URL = `/api/${linkedTableId}/rows`
try {
const response = await api.get(FETCH_ROWS_URL)
rows = await response.json()
rows = await API.fetchTableData(linkedTableId)
} catch (error) {
console.log(error)
rows = []
}
}

View File

@ -17,6 +17,10 @@
dispatch("change")
}
}
function save() {
dispatch("save", value)
}
</script>
<div class="parent">
@ -39,7 +43,10 @@
name="SaveFloppy"
hoverable
size="S"
on:click={() => setEditing(false)}
on:click={() => {
setEditing(false)
save()
}}
/>
{/if}
</div>

View File

@ -1,6 +1,6 @@
<script>
import { Button, Modal, notifications, ModalContent } from "@budibase/bbui"
import api from "builderStore/api"
import { API } from "api"
import analytics, { Events } from "analytics"
import { store } from "builderStore"
@ -9,18 +9,14 @@
async function deployApp() {
try {
const response = await api.post("/api/deploy")
if (response.status !== 200) {
throw new Error(`status ${response.status}`)
} else {
analytics.captureEvent(Events.APP.PUBLISHED, {
appId: $store.appId,
})
notifications.success(`Application published successfully`)
}
} catch (err) {
analytics.captureException(err)
notifications.error(`Error publishing app: ${err}`)
await API.deployAppChanges()
analytics.captureEvent(Events.APP.PUBLISHED, {
appId: $store.appId,
})
notifications.success("Application published successfully")
} catch (error) {
analytics.captureException(error)
notifications.error("Error publishing app")
}
}
</script>

View File

@ -3,7 +3,7 @@
import Spinner from "components/common/Spinner.svelte"
import { slide } from "svelte/transition"
import { Heading, Button, Modal, ModalContent } from "@budibase/bbui"
import api from "builderStore/api"
import { API } from "api"
import { notifications } from "@budibase/bbui"
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
import { store } from "builderStore"
@ -63,20 +63,14 @@
async function fetchDeployments() {
try {
const response = await api.get(`/api/deployments`)
const json = await response.json()
const newDeployments = await API.getAppDeployments()
if (deployments.length > 0) {
checkIncomingDeploymentStatus(deployments, json)
checkIncomingDeploymentStatus(deployments, newDeployments)
}
deployments = json
deployments = newDeployments
} catch (err) {
console.error(err)
clearInterval(poll)
notifications.error(
"Error fetching deployment history. Please try again."
)
notifications.error("Error fetching deployment history")
}
}

View File

@ -7,7 +7,7 @@
ModalContent,
} from "@budibase/bbui"
import { store } from "builderStore"
import api from "builderStore/api"
import { API } from "api"
let revertModal
let appName
@ -16,24 +16,14 @@
const revert = async () => {
try {
const response = await api.post(`/api/dev/${appId}/revert`)
const json = await response.json()
if (response.status !== 200) throw json.message
await API.revertAppChanges(appId)
// Reset frontend state after revert
const applicationPkg = await api.get(
`/api/applications/${appId}/appPackage`
)
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
await store.actions.initialise(pkg)
} else {
throw new Error(pkg)
}
notifications.info("Changes reverted.")
} catch (err) {
notifications.error(`Error reverting changes: ${err}`)
const applicationPkg = await API.fetchAppPackage(appId)
await store.actions.initialise(applicationPkg)
notifications.info("Changes reverted successfully")
} catch (error) {
notifications.error(`Error reverting changes: ${error}`)
}
}
</script>

View File

@ -8,7 +8,7 @@
Button,
} from "@budibase/bbui"
import { store } from "builderStore"
import api from "builderStore/api"
import { API } from "api"
import clientPackage from "@budibase/client/package.json"
let updateModal
@ -18,26 +18,17 @@
$: revertAvailable = $store.revertableVersion != null
const refreshAppPackage = async () => {
const applicationPkg = await api.get(
`/api/applications/${appId}/appPackage`
)
const pkg = await applicationPkg.json()
if (applicationPkg.ok) {
try {
const pkg = await API.fetchAppPackage(appId)
await store.actions.initialise(pkg)
} else {
throw new Error(pkg)
} catch (error) {
notifications.error("Error fetching app package")
}
}
const update = async () => {
try {
const response = await api.post(
`/api/applications/${appId}/client/update`
)
const json = await response.json()
if (response.status !== 200) {
throw json.message
}
await API.updateAppClientVersion(appId)
// Don't wait for the async refresh, since this causes modal flashing
refreshAppPackage()
@ -47,23 +38,17 @@
} catch (err) {
notifications.error(`Error updating app: ${err}`)
}
updateModal.hide()
}
const revert = async () => {
try {
const revertableVersion = $store.revertableVersion
const response = await api.post(
`/api/applications/${appId}/client/revert`
)
const json = await response.json()
if (response.status !== 200) {
throw json.message
}
await API.revertAppClientVersion(appId)
// Don't wait for the async refresh, since this causes modal flashing
refreshAppPackage()
notifications.success(
`App reverted successfully to version ${revertableVersion}`
`App reverted successfully to version ${$store.revertableVersion}`
)
} catch (err) {
notifications.error(`Error reverting app: ${err}`)

View File

@ -1,5 +1,5 @@
<script>
import { Select } from "@budibase/bbui"
import { notifications, Select } from "@budibase/bbui"
import { store } from "builderStore"
import { get } from "svelte/store"
@ -23,14 +23,18 @@
]
const onChangeTheme = async theme => {
await store.actions.theme.save(theme)
await store.actions.customTheme.save({
...get(store).customTheme,
navBackground:
theme === "spectrum--light"
? "var(--spectrum-global-color-gray-50)"
: "var(--spectrum-global-color-gray-100)",
})
try {
await store.actions.theme.save(theme)
await store.actions.customTheme.save({
...get(store).customTheme,
navBackground:
theme === "spectrum--light"
? "var(--spectrum-global-color-gray-50)"
: "var(--spectrum-global-color-gray-100)",
})
} catch (error) {
notifications.error("Error updating theme")
}
}
</script>

View File

@ -1,5 +1,11 @@
<script>
import { ActionMenu, ActionButton, MenuItem, Icon } from "@budibase/bbui"
import {
ActionMenu,
ActionButton,
MenuItem,
Icon,
notifications,
} from "@budibase/bbui"
import { store, currentAssetName, selectedComponent } from "builderStore"
import structure from "./componentStructure.json"
@ -36,7 +42,11 @@
const onItemChosen = async item => {
if (!item.isCategory) {
await store.actions.components.create(item.component)
try {
await store.actions.components.create(item.component)
} catch (error) {
notifications.error("Error creating component")
}
}
}
</script>

View File

@ -13,7 +13,7 @@
Body,
notifications,
} from "@budibase/bbui"
import ErrorSVG from "assets/error.svg?raw"
import ErrorSVG from "@budibase/frontend-core/assets/error.svg?raw"
import { findComponent, findComponentPath } from "builderStore/componentUtils"
let iframe
@ -146,44 +146,49 @@
}
})
const handleBudibaseEvent = event => {
const handleBudibaseEvent = async event => {
const { type, data } = event.data || event.detail
if (!type) {
return
}
if (type === "select-component" && data.id) {
store.actions.components.select({ _id: data.id })
} else if (type === "update-prop") {
store.actions.components.updateProp(data.prop, data.value)
} else if (type === "delete-component" && data.id) {
confirmDeleteComponent(data.id)
} else if (type === "preview-loaded") {
// Wait for this event to show the client library if intelligent
// loading is supported
loading = false
} else if (type === "move-component") {
const { componentId, destinationComponentId } = data
const rootComponent = get(currentAsset).props
try {
if (type === "select-component" && data.id) {
store.actions.components.select({ _id: data.id })
} else if (type === "update-prop") {
await store.actions.components.updateProp(data.prop, data.value)
} else if (type === "delete-component" && data.id) {
confirmDeleteComponent(data.id)
} else if (type === "preview-loaded") {
// Wait for this event to show the client library if intelligent
// loading is supported
loading = false
} else if (type === "move-component") {
const { componentId, destinationComponentId } = data
const rootComponent = get(currentAsset).props
// Get source and destination components
const source = findComponent(rootComponent, componentId)
const destination = findComponent(rootComponent, destinationComponentId)
// Get source and destination components
const source = findComponent(rootComponent, componentId)
const destination = findComponent(rootComponent, destinationComponentId)
// Stop if the target is a child of source
const path = findComponentPath(source, destinationComponentId)
const ids = path.map(component => component._id)
if (ids.includes(data.destinationComponentId)) {
return
// Stop if the target is a child of source
const path = findComponentPath(source, destinationComponentId)
const ids = path.map(component => component._id)
if (ids.includes(data.destinationComponentId)) {
return
}
// Cut and paste the component to the new destination
if (source && destination) {
store.actions.components.copy(source, true)
await store.actions.components.paste(destination, data.mode)
}
} else {
console.warn(`Client sent unknown event type: ${type}`)
}
// Cut and paste the component to the new destination
if (source && destination) {
store.actions.components.copy(source, true)
store.actions.components.paste(destination, data.mode)
}
} else {
console.warn(`Client sent unknown event type: ${type}`)
} catch (error) {
console.warn(error)
notifications.error("Error handling event from app preview")
}
}
@ -196,7 +201,7 @@
try {
await store.actions.components.delete({ _id: idToDelete })
} catch (error) {
notifications.error(error)
notifications.error("Error deleting component")
}
idToDelete = null
}

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