Merge branch 'develop' of github.com:Budibase/budibase into feature/test-image

This commit is contained in:
mike12345567 2022-12-22 17:24:34 +00:00
commit 84403253b1
148 changed files with 2866 additions and 1471 deletions

View File

@ -38,17 +38,6 @@ jobs:
fi fi
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Tag and release Proxy service docker image
run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
yarn build:docker:proxy:prod
docker tag proxy-service budibase/proxy:$PROD_TAG
docker push budibase/proxy:$PROD_TAG
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
PROD_TAG: k8s
- name: Configure AWS Credentials - name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1 uses: aws-actions/configure-aws-credentials@v1
with: with:

View File

@ -28,17 +28,6 @@ jobs:
release_version=$(cat lerna.json | jq -r '.version') release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Tag and release Proxy service docker image
run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
yarn build:docker:proxy:preprod
docker tag proxy-service budibase/proxy:$PREPROD_TAG
docker push budibase/proxy:$PREPROD_TAG
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
PREPROD_TAG: k8s-preprod
- name: Pull values.yaml from budibase-infra - name: Pull values.yaml from budibase-infra
run: | run: |
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \ curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \

View File

@ -29,17 +29,6 @@ jobs:
release_version=$(cat lerna.json | jq -r '.version') release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Tag and release Proxy service docker image
run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
yarn build:docker:proxy:release
docker tag proxy-service budibase/proxy:$RELEASE_TAG
docker push budibase/proxy:$RELEASE_TAG
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
RELEASE_TAG: k8s-release
- name: Pull values.yaml from budibase-infra - name: Pull values.yaml from budibase-infra
run: | run: |
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \ curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \

View File

@ -26,7 +26,7 @@ env:
FEATURE_PREVIEW_URL: https://budirelease.live FEATURE_PREVIEW_URL: https://budirelease.live
jobs: jobs:
release: release-images:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -44,19 +44,12 @@ jobs:
run: yarn install:pro develop run: yarn install:pro develop
- run: yarn - run: yarn
- run: yarn bootstrap - run: yarn bootstrap
- run: yarn lint - run: yarn lint
- run: yarn build - run: yarn build
- run: yarn build:sdk - run: yarn build:sdk
- run: yarn test - run: yarn test
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: Publish budibase packages to NPM - name: Publish budibase packages to NPM
env: env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
@ -76,22 +69,25 @@ jobs:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
- name: Get the latest budibase release version deploy-to-release-env:
needs: [release-images]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Get the current budibase release version
id: version id: version
run: | run: |
release_version=$(cat lerna.json | jq -r '.version') release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Tag and release Proxy service docker image - name: Configure AWS Credentials
run: | uses: aws-actions/configure-aws-credentials@v1
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD with:
yarn build:docker:proxy:release aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
docker tag proxy-service budibase/proxy:$RELEASE_TAG aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
docker push budibase/proxy:$RELEASE_TAG aws-region: eu-west-1
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
RELEASE_TAG: k8s-release
- name: Pull values.yaml from budibase-infra - name: Pull values.yaml from budibase-infra
run: | run: |
@ -149,3 +145,54 @@ jobs:
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }} webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
content: "Release Env Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Release Env." content: "Release Env Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Release Env."
embed-title: ${{ env.RELEASE_VERSION }} embed-title: ${{ env.RELEASE_VERSION }}
release-helm-chart:
needs: [release-images]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Helm
uses: azure/setup-helm@v1
id: helm-install
# due to helm repo index issue: https://github.com/helm/helm/issues/7363
# we need to create new package in a different dir, merge the index and move the package back
- name: Build and release helm chart
run: |
git config user.name "Budibase Helm Bot"
git config user.email "<>"
git reset --hard
git pull
mkdir sync
echo "Packaging chart to sync dir"
helm package charts/budibase --version 0.0.0-develop --app-version develop --destination sync
echo "Packaging successful"
git checkout gh-pages
echo "Indexing helm repo"
helm repo index --merge docs/index.yaml sync
mv -f sync/* docs
rm -rf sync
echo "Pushing new helm release"
git add -A
git commit -m "Helm Release: develop"
git push
trigger-deploy-to-qa-env:
needs: [release-helm-chart]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Get the current budibase release version
id: version
run: |
release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- uses: passeidireto/trigger-external-workflow-action@main
env:
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
with:
repository: budibase/budibase-deploys
event: deploy-develop-to-qa
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}

View File

@ -73,7 +73,7 @@ jobs:
git config user.email "<>" git config user.email "<>"
git reset --hard git reset --hard
git pull git pull
helm package charts/budibase helm package charts/budibase --version "$RELEASE_VERSION" --app-version "$RELEASE_VERSION"
git checkout gh-pages git checkout gh-pages
mv *.tgz docs mv *.tgz docs
helm repo index docs helm repo index docs

View File

@ -98,17 +98,6 @@ jobs:
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1 aws-region: eu-west-1
- name: Tag and release Proxy service docker image
run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
yarn build:docker:proxy:preprod
docker tag proxy-service budibase/proxy:$PREPROD_TAG
docker push budibase/proxy:$PREPROD_TAG
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
PREPROD_TAG: k8s-preprod
- name: Pull values.yaml from budibase-infra - name: Pull values.yaml from budibase-infra
run: | run: |
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \ curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \

7
.gitignore vendored
View File

@ -4,6 +4,7 @@ builder/*
packages/server/runtime_apps/ packages/server/runtime_apps/
.idea/ .idea/
bb-airgapped.tar.gz bb-airgapped.tar.gz
*.iml
# Logs # Logs
logs logs
@ -65,8 +66,6 @@ typings/
.env .env
!qa-core/.env !qa-core/.env
!hosting/.env !hosting/.env
hosting/.generated-nginx.dev.conf
hosting/proxy/.generated-nginx.prod.conf
# parcel-bundler cache (https://parceljs.org/) # parcel-bundler cache (https://parceljs.org/)
.cache .cache
@ -104,7 +103,9 @@ stats.html
# TypeScript cache # TypeScript cache
*.tsbuildinfo *.tsbuildinfo
# plugins
budibase-component budibase-component
budibase-datasource budibase-datasource
*.iml *.iml

View File

@ -11,8 +11,10 @@ sources:
- https://github.com/Budibase/budibase - https://github.com/Budibase/budibase
- https://budibase.com - https://budibase.com
type: application type: application
version: 0.2.11 # populates on packaging
appVersion: 1.0.214 version: 0.0.0
# populates on packaging
appVersion: 0.0.0
dependencies: dependencies:
- name: couchdb - name: couchdb
version: 3.6.1 version: 3.6.1

View File

@ -28,11 +28,26 @@ spec:
app.kubernetes.io/name: budibase-proxy app.kubernetes.io/name: budibase-proxy
spec: spec:
containers: containers:
- image: budibase/proxy:{{ .Values.services.proxy.tag | default "k8s" }} - image: budibase/proxy:{{ .Values.globals.appVersion }}
imagePullPolicy: Always imagePullPolicy: Always
name: proxy-service name: proxy-service
ports: ports:
- containerPort: {{ .Values.services.proxy.port }} - containerPort: {{ .Values.services.proxy.port }}
env:
- name: APPS_UPSTREAM_URL
value: {{ tpl .Values.services.proxy.upstreams.apps . | quote }}
- name: WORKER_UPSTREAM_URL
value: {{ tpl .Values.services.proxy.upstreams.worker . | quote }}
- name: MINIO_UPSTREAM_URL
value: {{ tpl .Values.services.proxy.upstreams.minio . | quote }}
- name: COUCHDB_UPSTREAM_URL
value: {{ .Values.services.couchdb.url | default (tpl .Values.services.proxy.upstreams.couchdb .) | quote }}
- name: RESOLVER
{{ if .Values.services.proxy.resolver }}
value: {{ .Values.services.proxy.resolver }}
{{ else }}
value: kube-dns.kube-system.svc.{{ .Values.services.dns }}
{{ end }}
{{ with .Values.services.proxy.resources }} {{ with .Values.services.proxy.resources }}
resources: resources:
{{- toYaml . | nindent 10 }} {{- toYaml . | nindent 10 }}

View File

@ -124,6 +124,11 @@ services:
proxy: proxy:
port: 10000 port: 10000
replicaCount: 1 replicaCount: 1
upstreams:
apps: 'http://app-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.apps.port }}'
worker: 'http://worker-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.worker.port }}'
minio: 'http://minio-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.objectStore.port }}'
couchdb: 'http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }}'
resources: {} resources: {}
apps: apps:

View File

@ -24,9 +24,9 @@ services:
proxy-service: proxy-service:
container_name: budi-nginx-dev container_name: budi-nginx-dev
restart: on-failure restart: on-failure
image: nginx:latest image: budibase/proxy:latest
volumes: volumes:
- ./.generated-nginx.dev.conf:/etc/nginx/nginx.conf - ./nginx.dev.conf:/etc/nginx/templates/nginx.conf.template
- ./proxy/error.html:/usr/share/nginx/html/error.html - ./proxy/error.html:/usr/share/nginx/html/error.html
ports: ports:
- "${MAIN_PORT}:10000" - "${MAIN_PORT}:10000"
@ -34,6 +34,8 @@ services:
- dev-service - dev-service
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"
environment:
- PROXY_ADDRESS=host.docker.internal
volumes: volumes:
dev_data: dev_data:

View File

@ -82,6 +82,12 @@ services:
environment: environment:
- PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10 - PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10
- PROXY_RATE_LIMIT_API_PER_SECOND=20 - PROXY_RATE_LIMIT_API_PER_SECOND=20
- APPS_UPSTREAM_URL=http://app-service:4002
- WORKER_UPSTREAM_URL=http://worker-service:4003
- MINIO_UPSTREAM_URL=http://minio-service:9000
- COUCHDB_UPSTREAM_URL=http://couchdb-service:5984
- WATCHTOWER_UPSTREAM_URL=http://watchtower-service:8080
- RESOLVER=127.0.0.11
depends_on: depends_on:
- minio-service - minio-service
- worker-service - worker-service

View File

@ -25,17 +25,17 @@ http {
} }
upstream app-service { upstream app-service {
server {{address}}:4001; server ${PROXY_ADDRESS}:4001;
keepalive 32; keepalive 32;
} }
upstream worker-service { upstream worker-service {
server {{address}}:4002; server ${PROXY_ADDRESS}:4002;
keepalive 32; keepalive 32;
} }
upstream builder { upstream builder {
server {{address}}:3000; server ${PROXY_ADDRESS}:3000;
keepalive 32; keepalive 32;
} }

View File

@ -4,7 +4,7 @@ FROM nginx:latest
# use the default nginx behaviour for *.template files which are processed with envsubst # use the default nginx behaviour for *.template files which are processed with envsubst
# override the output dir to output directly to /etc/nginx instead of /etc/nginx/conf.d # override the output dir to output directly to /etc/nginx instead of /etc/nginx/conf.d
ENV NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx ENV NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx
COPY .generated-nginx.prod.conf /etc/nginx/templates/nginx.conf.template COPY nginx.prod.conf /etc/nginx/templates/nginx.conf.template
# IPv6 removal needs to happen after envsubst # IPv6 removal needs to happen after envsubst
RUN rm -rf /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh RUN rm -rf /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
@ -16,4 +16,11 @@ COPY error.html /usr/share/nginx/html/error.html
# Default environment # Default environment
ENV PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10 ENV PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10
ENV PROXY_RATE_LIMIT_API_PER_SECOND=20 ENV PROXY_RATE_LIMIT_API_PER_SECOND=20
# Use docker-compose values as defaults for backwards compatibility
ENV APPS_UPSTREAM_URL=http://app-service:4002
ENV WORKER_UPSTREAM_URL=http://worker-service:4003
ENV MINIO_UPSTREAM_URL=http://minio-service:9000
ENV COUCHDB_UPSTREAM_URL=http://couchdb-service:5984
ENV WATCHTOWER_UPSTREAM_URL=http://watchtower-service:8080
ENV RESOLVER=127.0.0.11

View File

@ -23,7 +23,7 @@ http {
tcp_nodelay on; tcp_nodelay on;
server_tokens off; server_tokens off;
types_hash_max_size 2048; types_hash_max_size 2048;
resolver {{ resolver }} valid=10s ipv6=off; resolver ${RESOLVER} valid=10s ipv6=off;
# buffering # buffering
client_header_buffer_size 1k; client_header_buffer_size 1k;
@ -76,27 +76,23 @@ http {
add_header Content-Security-Policy "${csp_default}; ${csp_script}; ${csp_style}; ${csp_object}; ${csp_base_uri}; ${csp_connect}; ${csp_font}; ${csp_frame}; ${csp_img}; ${csp_manifest}; ${csp_media}; ${csp_worker};" always; add_header Content-Security-Policy "${csp_default}; ${csp_script}; ${csp_style}; ${csp_object}; ${csp_base_uri}; ${csp_connect}; ${csp_font}; ${csp_frame}; ${csp_img}; ${csp_manifest}; ${csp_media}; ${csp_worker};" always;
# upstreams # upstreams
set $apps {{ apps }}; set $apps ${APPS_UPSTREAM_URL};
set $worker {{ worker }}; set $worker ${WORKER_UPSTREAM_URL};
set $minio {{ minio }}; set $minio ${MINIO_UPSTREAM_URL};
set $couchdb {{ couchdb }}; set $couchdb ${COUCHDB_UPSTREAM_URL};
{{#if watchtower}} set $watchtower ${WATCHTOWER_UPSTREAM_URL};
set $watchtower {{ watchtower }};
{{/if}}
location /app { location /app {
proxy_pass http://$apps:4002; proxy_pass $apps;
} }
location = / { location = / {
proxy_pass http://$apps:4002; proxy_pass $apps;
} }
{{#if watchtower}}
location = /v1/update { location = /v1/update {
proxy_pass http://$watchtower:8080; proxy_pass $watchtower;
} }
{{/if}}
location ~ ^/(builder|app_) { location ~ ^/(builder|app_) {
proxy_http_version 1.1; proxy_http_version 1.1;
@ -107,19 +103,17 @@ http {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_pass http://$apps:4002; proxy_pass $apps;
} }
location ~ ^/api/(system|admin|global)/ { location ~ ^/api/(system|admin|global)/ {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_pass $worker;
proxy_pass http://$worker:4003;
} }
location /worker/ { location /worker/ {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_pass $worker;
proxy_pass http://$worker:4003;
rewrite ^/worker/(.*)$ /$1 break; rewrite ^/worker/(.*)$ /$1 break;
} }
@ -138,7 +132,7 @@ http {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://$apps:4002; proxy_pass $apps;
} }
location /api/ { location /api/ {
@ -157,7 +151,7 @@ http {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_pass http://$apps:4002; proxy_pass $apps;
} }
location /api/webhooks/ { location /api/webhooks/ {
@ -177,11 +171,11 @@ http {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_pass http://$apps:4002; proxy_pass $apps;
} }
location /db/ { location /db/ {
proxy_pass http://$couchdb:5984; proxy_pass $couchdb;
rewrite ^/db/(.*)$ /$1 break; rewrite ^/db/(.*)$ /$1 break;
} }
@ -191,7 +185,7 @@ http {
proxy_set_header Connection 'upgrade'; proxy_set_header Connection 'upgrade';
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade; proxy_cache_bypass $http_upgrade;
proxy_pass http://$apps:4002; proxy_pass $apps;
} }
location / { location / {
@ -205,7 +199,7 @@ http {
proxy_set_header Connection ""; proxy_set_header Connection "";
chunked_transfer_encoding off; chunked_transfer_encoding off;
proxy_pass http://$minio:9000; proxy_pass $minio;
} }
location /files/signed/ { location /files/signed/ {
@ -224,7 +218,7 @@ http {
proxy_set_header Connection ""; proxy_set_header Connection "";
chunked_transfer_encoding off; chunked_transfer_encoding off;
proxy_pass http://$minio:9000; proxy_pass $minio;
rewrite ^/files/signed/(.*)$ /$1 break; rewrite ^/files/signed/(.*)$ /$1 break;
} }

View File

@ -1,5 +1,5 @@
{ {
"version": "2.2.4-alpha.4", "version": "2.2.10-alpha.11",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -56,15 +56,11 @@
"test:e2e:ci:record": "lerna run cy:ci:record --stream", "test:e2e:ci:record": "lerna run cy:ci:record --stream",
"test:e2e:ci:notify": "lerna run cy:ci:notify", "test:e2e:ci:notify": "lerna run cy:ci:notify",
"build:specs": "lerna run specs", "build:specs": "lerna run specs",
"build:docker": "lerna run build:docker && npm run build:docker:proxy:compose && 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:pre": "lerna run build && lerna run predocker", "build:docker:pre": "lerna run build && lerna run predocker",
"build:docker:proxy": "docker build hosting/proxy -t proxy-service", "build:docker:proxy": "docker build hosting/proxy -t proxy-service",
"build:docker:proxy:compose": "node scripts/proxy/generateProxyConfig compose && npm run build:docker:proxy",
"build:docker:proxy:preprod": "node scripts/proxy/generateProxyConfig preprod && npm run build:docker:proxy",
"build:docker:proxy:release": "node scripts/proxy/generateProxyConfig release && npm run build:docker:proxy",
"build:docker:proxy:prod": "node scripts/proxy/generateProxyConfig prod && npm run build:docker:proxy",
"build:docker:selfhost": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -", "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 && npm run build:docker:proxy:compose && 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:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -", "build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
"build:docker:single:multiarch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/single/Dockerfile -t budibase:latest .", "build:docker:single:multiarch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/single/Dockerfile -t budibase:latest .",
@ -90,4 +86,4 @@
"install:pro": "bash scripts/pro/install.sh", "install:pro": "bash scripts/pro/install.sh",
"dep:clean": "yarn clean && yarn bootstrap" "dep:clean": "yarn clean && yarn bootstrap"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "2.2.4-alpha.4", "version": "2.2.10-alpha.11",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js", "main": "dist/src/index.js",
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
@ -15,13 +15,15 @@
"prebuild": "rimraf dist/", "prebuild": "rimraf dist/",
"prepack": "cp package.json dist", "prepack": "cp package.json dist",
"build": "tsc -p tsconfig.build.json", "build": "tsc -p tsconfig.build.json",
"build:pro": "../../scripts/pro/build.sh",
"postbuild": "yarn run build:pro",
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
"test": "jest --coverage --maxWorkers=2", "test": "jest --coverage --maxWorkers=2",
"test:watch": "jest --watchAll" "test:watch": "jest --watchAll"
}, },
"dependencies": { "dependencies": {
"@budibase/nano": "10.1.1", "@budibase/nano": "10.1.1",
"@budibase/types": "2.2.4-alpha.4", "@budibase/types": "2.2.10-alpha.11",
"@shopify/jest-koa-mocks": "5.0.1", "@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2", "@techpass/passport-openidconnect": "0.3.2",
"aws-cloudfront-sign": "2.2.0", "aws-cloudfront-sign": "2.2.0",

View File

@ -13,6 +13,18 @@ const getClient = async (type: LockType): Promise<Redlock> => {
} }
return noRetryRedlock return noRetryRedlock
} }
case LockType.DEFAULT: {
if (!noRetryRedlock) {
noRetryRedlock = await newRedlock(OPTIONS.DEFAULT)
}
return noRetryRedlock
}
case LockType.DELAY_500: {
if (!noRetryRedlock) {
noRetryRedlock = await newRedlock(OPTIONS.DELAY_500)
}
return noRetryRedlock
}
default: { default: {
throw new Error(`Could not get redlock client: ${type}`) throw new Error(`Could not get redlock client: ${type}`)
} }
@ -41,6 +53,9 @@ export const OPTIONS = {
// see https://www.awsarchitectureblog.com/2015/03/backoff.html // see https://www.awsarchitectureblog.com/2015/03/backoff.html
retryJitter: 100, // time in ms retryJitter: 100, // time in ms
}, },
DELAY_500: {
retryDelay: 500,
},
} }
export const newRedlock = async (opts: Options = {}) => { export const newRedlock = async (opts: Options = {}) => {
@ -55,19 +70,17 @@ export const doWithLock = async (opts: LockOptions, task: any) => {
let lock let lock
try { try {
// aquire lock // aquire lock
let name: string let name: string = `lock:${tenancy.getTenantId()}_${opts.name}`
if (opts.systemLock) {
name = opts.name
} else {
name = `${tenancy.getTenantId()}_${opts.name}`
}
if (opts.nameSuffix) { if (opts.nameSuffix) {
name = name + `_${opts.nameSuffix}` name = name + `_${opts.nameSuffix}`
} }
lock = await redlock.lock(name, opts.ttl) lock = await redlock.lock(name, opts.ttl)
// perform locked task // perform locked task
return task() // need to await to ensure completion before unlocking
const result = await task()
return result
} catch (e: any) { } catch (e: any) {
console.log("lock error")
// lock limit exceeded // lock limit exceeded
if (e.name === "LockError") { if (e.name === "LockError") {
if (opts.type === LockType.TRY_ONCE) { if (opts.type === LockType.TRY_ONCE) {

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "2.2.4-alpha.4", "version": "2.2.10-alpha.11",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
], ],
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "1.2.1", "@adobe/spectrum-css-workflow-icons": "1.2.1",
"@budibase/string-templates": "2.2.4-alpha.4", "@budibase/string-templates": "2.2.10-alpha.11",
"@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actionbutton": "1.0.1",
"@spectrum-css/actiongroup": "1.0.1", "@spectrum-css/actiongroup": "1.0.1",
"@spectrum-css/avatar": "3.0.2", "@spectrum-css/avatar": "3.0.2",

View File

@ -10,10 +10,13 @@
export let green = false export let green = false
export let active = false export let active = false
export let inactive = false export let inactive = false
export let hoverable = false
</script> </script>
<span <span
on:click
class="spectrum-Label" class="spectrum-Label"
class:hoverable
class:spectrum-Label--small={size === "S"} class:spectrum-Label--small={size === "S"}
class:spectrum-Label--large={size === "L"} class:spectrum-Label--large={size === "L"}
class:spectrum-Label--grey={grey} class:spectrum-Label--grey={grey}
@ -27,3 +30,13 @@
> >
<slot /> <slot />
</span> </span>
<style>
.spectrum-Label--grey {
background-color: var(--spectrum-global-color-gray-500);
font-weight: 600;
}
.hoverable:hover {
cursor: pointer;
}
</style>

View File

@ -1,6 +1,7 @@
<script> <script>
import "@spectrum-css/label/dist/index-vars.css" import "@spectrum-css/label/dist/index-vars.css"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import Badge from "../Badge/Badge.svelte"
export let row export let row
export let value export let value
@ -24,17 +25,11 @@
{#each relationships as relationship} {#each relationships as relationship}
{#if relationship?.primaryDisplay} {#if relationship?.primaryDisplay}
<span class="spectrum-Label spectrum-Label--grey" on:click={onClick}> <Badge hoverable grey on:click={onClick}>
{relationship.primaryDisplay} {relationship.primaryDisplay}
</span> </Badge>
{/if} {/if}
{/each} {/each}
{#if leftover} {#if leftover}
<div>+{leftover} more</div> <div>+{leftover} more</div>
{/if} {/if}
<style>
span:hover {
cursor: pointer;
}
</style>

View File

@ -3,17 +3,17 @@ const interact = require('../../support/interact')
filterTests(["smoke", "all"], () => { filterTests(["smoke", "all"], () => {
context("Account Portals", () => { context("Account Portals", () => {
const bbUserEmail = "bbuser@test.com" const bbUserEmail = "bbuser@test.com"
before(() => { before(() => {
cy.login() cy.login()
cy.deleteApp("Cypress Tests") cy.deleteApp("Cypress Tests")
cy.createApp("Cypress Tests", false) cy.createApp("Cypress Tests", false)
// Create new user // Create new user
cy.wait(500) cy.wait(500)
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000}) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.createUser(bbUserEmail) cy.createUser(bbUserEmail)
cy.contains("bbuser").click() cy.contains("bbuser").click()
cy.wait(500) cy.wait(500)
@ -25,18 +25,18 @@ filterTests(["smoke", "all"], () => {
cy.get(interact.SPECTRUM_MENU).within(() => { cy.get(interact.SPECTRUM_MENU).within(() => {
cy.get(interact.SPECTRUM_MENU_ITEM).contains("Force password reset").click({ force: true }) cy.get(interact.SPECTRUM_MENU_ITEM).contains("Force password reset").click({ force: true })
}) })
cy.get(interact.SPECTRUM_DIALOG_GRID) cy.get(interact.SPECTRUM_DIALOG_GRID)
.find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd') .find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd')
cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true }) cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true })
// Login as new user and set password // Login as new user and set password
cy.logOut() cy.logOut()
cy.get('@pwd').then((pwd) => { cy.get('@pwd').then((pwd) => {
cy.login(bbUserEmail, pwd) cy.login(bbUserEmail, pwd)
}) })
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test") cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test")
} }
@ -58,15 +58,15 @@ filterTests(["smoke", "all"], () => {
cy.logoutNoAppGrid() cy.logoutNoAppGrid()
}) })
it("should verify Admin Portal", () => {
cy.login()
// Configure user role
cy.setUserRole("bbuser", "Admin")
bbUserLogin()
// Verify available options for Admin portal xit("should verify Admin Portal", () => {
cy.get(interact.SPECTRUM_SIDENAV) cy.login()
// Configure user role
cy.setUserRole("bbuser", "Admin")
bbUserLogin()
// Verify available options for Admin portal
cy.get(interact.SPECTRUM_SIDENAV)
.should('contain', 'Apps') .should('contain', 'Apps')
//.and('contain', 'Usage') //.and('contain', 'Usage')
.and('contain', 'Users') .and('contain', 'Users')
@ -75,12 +75,12 @@ filterTests(["smoke", "all"], () => {
.and('contain', 'Organisation') .and('contain', 'Organisation')
.and('contain', 'Theming') .and('contain', 'Theming')
.and('contain', 'Update') .and('contain', 'Update')
//.and('contain', 'Upgrade') //.and('contain', 'Upgrade')
cy.logOut() cy.logOut()
}) })
it("should verify Development Portal", () => { xit("should verify Development Portal", () => {
// Only Development access should be enabled // Only Development access should be enabled
cy.login() cy.login()
cy.setUserRole("bbuser", "Developer") cy.setUserRole("bbuser", "Developer")
@ -98,7 +98,7 @@ filterTests(["smoke", "all"], () => {
.and('not.contain', 'Update') .and('not.contain', 'Update')
.and('not.contain', 'Upgrade') .and('not.contain', 'Upgrade')
cy.logOut() cy.logOut()
}) })
const bbUserLogin = () => { const bbUserLogin = () => {

View File

@ -9,7 +9,7 @@ filterTests(["all"], () => {
cy.createApp("Cypress Tests") cy.createApp("Cypress Tests")
}) })
it("Should be accessible from the applications list", () => { xit("Should be accessible from the applications list", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .title") cy.get(".appTable .title")
.eq(0) .eq(0)
@ -27,7 +27,7 @@ filterTests(["all"], () => {
}) })
// Find a more suitable place for this. // Find a more suitable place for this.
it("Should allow unlocking in the app list", () => { xit("Should allow unlocking in the app list", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .lock-status").eq(0).contains("Locked by you").click() cy.get(".appTable .lock-status").eq(0).contains("Locked by you").click()
@ -38,7 +38,7 @@ filterTests(["all"], () => {
cy.get(".lock-status").should("not.be.visible") cy.get(".lock-status").should("not.be.visible")
}) })
it("Should allow unlocking in the app overview screen", () => { xit("Should allow unlocking in the app overview screen", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .app-row-actions button") cy.get(".appTable .app-row-actions button")
@ -58,7 +58,7 @@ filterTests(["all"], () => {
cy.get(".lock-status").should("not.be.visible") cy.get(".lock-status").should("not.be.visible")
}) })
it("Should reflect the deploy state of an app that hasn't been published.", () => { xit("Should reflect the deploy state of an app that hasn't been published.", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .app-row-actions button") cy.get(".appTable .app-row-actions button")
@ -81,7 +81,7 @@ filterTests(["all"], () => {
}) })
}) })
it("Should reflect the app deployment state", () => { xit("Should reflect the app deployment state", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.get(".appTable .app-row-actions button") cy.get(".appTable .app-row-actions button")
.contains("Edit") .contains("Edit")
@ -117,7 +117,7 @@ filterTests(["all"], () => {
}) })
}) })
it("Should reflect an application that has been unpublished", () => { xit("Should reflect an application that has been unpublished", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .app-row-actions button") cy.get(".appTable .app-row-actions button")
.contains("Edit") .contains("Edit")
@ -154,7 +154,7 @@ filterTests(["all"], () => {
}) })
}) })
it("Should allow the editing of the application icon and colour", () => { xit("Should allow the editing of the application icon and colour", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .app-row-actions button") cy.get(".appTable .app-row-actions button")
.contains("Manage") .contains("Manage")
@ -196,7 +196,7 @@ filterTests(["all"], () => {
}) })
}) })
it("Should reflect the last time the application was edited", () => { xit("Should reflect the last time the application was edited", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .app-row-actions button") cy.get(".appTable .app-row-actions button")
.contains("Manage") .contains("Manage")
@ -221,7 +221,7 @@ filterTests(["all"], () => {
}) })
}) })
it("Should reflect application version is up-to-date", () => { xit("Should reflect application version is up-to-date", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .app-row-actions button") cy.get(".appTable .app-row-actions button")
.contains("Manage") .contains("Manage")
@ -302,7 +302,7 @@ filterTests(["all"], () => {
}) })
}) })
it("Should allow editing of the app details.", () => { xit("Should allow editing of the app details.", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.get(".appTable .app-row-actions button") cy.get(".appTable .app-row-actions button")
.contains("Manage") .contains("Manage")
@ -373,13 +373,13 @@ filterTests(["all"], () => {
.contains("Copy App ID") .contains("Copy App ID")
.click({ force: true }) .click({ force: true })
}) })
cy.get(".spectrum-Toast-content") cy.get(".spectrum-Toast-content")
.contains("App ID copied to clipboard.") .contains("App ID copied to clipboard.")
.should("be.visible") .should("be.visible")
}) })
it("Should allow unpublishing of the application via the Unpublish link", () => { xit("Should allow unpublishing of the application via the Unpublish link", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .app-row-actions button") cy.get(".appTable .app-row-actions button")
.contains("Manage") .contains("Manage")
@ -388,7 +388,7 @@ filterTests(["all"], () => {
cy.get(`[data-cy="app-status"]`).within(() => { cy.get(`[data-cy="app-status"]`).within(() => {
cy.contains("Unpublish").click({ force: true }) cy.contains("Unpublish").click({ force: true })
}) })
cy.get("[data-cy='unpublish-modal']") cy.get("[data-cy='unpublish-modal']")
.should("be.visible") .should("be.visible")
@ -399,11 +399,11 @@ filterTests(["all"], () => {
cy.get(".overview-tab [data-cy='app-status']").within(() => { cy.get(".overview-tab [data-cy='app-status']").within(() => {
cy.get(".status-display").contains("Unpublished") cy.get(".status-display").contains("Unpublished")
cy.get(".status-display .icon svg[aria-label='GlobeStrike']") cy.get(".status-display .icon svg[aria-label='GlobeStrike']")
.should("exist") .should("exist")
}) })
}) })
it("Should allow deleting of the application", () => { xit("Should allow deleting of the application", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.get(".appTable .app-row-actions button") cy.get(".appTable .app-row-actions button")
.contains("Manage") .contains("Manage")

View File

@ -2,13 +2,13 @@ import filterTests from "../support/filterTests"
const interact = require('../support/interact') const interact = require('../support/interact')
filterTests(['smoke', 'all'], () => { filterTests(['smoke', 'all'], () => {
context("Create a automation", () => { xcontext("Create a automation", () => {
before(() => { before(() => {
cy.login() cy.login()
cy.createTestApp() cy.createTestApp()
}) })
it("should create a automation", () => { xit("should create a automation", () => {
cy.createTestTableWithData() cy.createTestTableWithData()
cy.wait(2000) cy.wait(2000)
cy.contains("Automate").click() cy.contains("Automate").click()

View File

@ -26,13 +26,15 @@ filterTests(['smoke', 'all'], () => {
cy.get("input").type("Test View") cy.get("input").type("Test View")
cy.get("button").contains("Create View").click({ force: true }) cy.get("button").contains("Create View").click({ force: true })
}) })
cy.get(interact.TABLE_TITLE_H1).contains("Test View") cy.contains(interact.TABLE_TITLE_H1, "Test View", { timeout: 10000 })
cy.get(interact.TITLE).then($headers => { cy.get(".table-wrapper").within(() => {
expect($headers).to.have.length(3) cy.get(interact.TITLE).then($headers => {
const headers = Array.from($headers).map(header => expect($headers).to.have.length(3)
header.textContent.trim() const headers = Array.from($headers).map(header =>
) header.textContent.trim()
expect(removeSpacing(headers)).to.deep.eq(["group", "age", "rating"]) )
expect(removeSpacing(headers)).to.deep.eq(["group", "age", "rating"])
})
}) })
}) })
@ -70,20 +72,22 @@ filterTests(['smoke', 'all'], () => {
}) })
cy.wait(1000) cy.wait(1000)
cy.get(interact.TITLE).then($headers => { cy.get(".table-wrapper").within(() => {
expect($headers).to.have.length(7) cy.get(interact.TITLE).then($headers => {
const headers = Array.from($headers).map(header => expect($headers).to.have.length(7)
header.textContent.trim() const headers = Array.from($headers).map(header =>
) header.textContent.trim()
expect(removeSpacing(headers)).to.deep.eq([ )
"field", expect(removeSpacing(headers)).to.deep.eq([
"sum", "field",
"min", "sum",
"max", "min",
"count", "max",
"sumsqr", "count",
"avg", "sumsqr",
]) "avg",
])
})
}) })
cy.get(interact.SPECTRUM_TABLE_CELL).then($values => { cy.get(interact.SPECTRUM_TABLE_CELL).then($values => {
let values = Array.from($values).map(header => header.textContent.trim()) let values = Array.from($values).map(header => header.textContent.trim())

View File

@ -14,11 +14,13 @@ filterTests(["smoke", "all"], () => {
const restUrl = "https://api.openbrewerydb.org/breweries" const restUrl = "https://api.openbrewerydb.org/breweries"
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
cy.createRestQuery("GET", restUrl, "/breweries") cy.createRestQuery("GET", restUrl, "/breweries")
cy.get(interact.SPECTRUM_TABS_ITEM).contains("Transformer").click() cy.reload()
cy.contains(".nav-item-content", "/breweries", { timeout: 20000 }).click()
cy.contains(interact.SPECTRUM_TABS_ITEM, "Transformer", { timeout: 5000 }).click({ force: true })
// Get Transformer Function from file // Get Transformer Function from file
cy.readFile("cypress/support/queryLevelTransformerFunction.js").then( cy.readFile("cypress/support/queryLevelTransformerFunction.js").then(
transformerFunction => { transformerFunction => {
cy.get(interact.CODEMIRROR_TEXTAREA) cy.get(interact.CODEMIRROR_TEXTAREA, { timeout: 5000 })
// Highlight current text and overwrite with file contents // Highlight current text and overwrite with file contents
.type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", { .type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", {
force: true, force: true,
@ -28,6 +30,7 @@ filterTests(["smoke", "all"], () => {
) )
// Send Query // Send Query
cy.intercept("**/queries/preview").as("query") cy.intercept("**/queries/preview").as("query")
cy.get(interact.SPECTRUM_BUTTON).contains("Save").click({ force: true })
cy.get(interact.SPECTRUM_BUTTON).contains("Send").click({ force: true }) cy.get(interact.SPECTRUM_BUTTON).contains("Send").click({ force: true })
cy.wait("@query") cy.wait("@query")
// Assert against Status Code, body, & body rows // Assert against Status Code, body, & body rows
@ -42,7 +45,9 @@ filterTests(["smoke", "all"], () => {
const restUrl = "https://api.openbrewerydb.org/breweries" const restUrl = "https://api.openbrewerydb.org/breweries"
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
cy.createRestQuery("GET", restUrl, "/breweries") cy.createRestQuery("GET", restUrl, "/breweries")
cy.get(interact.SPECTRUM_TABS_ITEM).contains("Transformer").click() cy.reload()
cy.contains(".nav-item-content", "/breweries", { timeout: 2000 }).click()
cy.contains(interact.SPECTRUM_TABS_ITEM, "Transformer", { timeout: 5000 }).click({ force: true })
// Get Transformer Function with Data from file // Get Transformer Function with Data from file
cy.readFile( cy.readFile(
"cypress/support/queryLevelTransformerFunctionWithData.js" "cypress/support/queryLevelTransformerFunctionWithData.js"
@ -71,7 +76,9 @@ filterTests(["smoke", "all"], () => {
const restUrl = "https://api.openbrewerydb.org/breweries" const restUrl = "https://api.openbrewerydb.org/breweries"
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
cy.createRestQuery("GET", restUrl, "/breweries") cy.createRestQuery("GET", restUrl, "/breweries")
cy.get(interact.SPECTRUM_TABS_ITEM).contains("Transformer").click() cy.reload()
cy.contains(".nav-item-content", "/breweries", { timeout: 2000 }).click()
cy.contains(interact.SPECTRUM_TABS_ITEM, "Transformer", { timeout: 5000 }).click({ force: true })
// Clear the code box and add "test" // Clear the code box and add "test"
cy.get(interact.CODEMIRROR_TEXTAREA) cy.get(interact.CODEMIRROR_TEXTAREA)
.type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", { .type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", {

View File

@ -413,7 +413,7 @@ Cypress.Commands.add("searchForApplication", appName => {
// Assumes there are no others // Assumes there are no others
Cypress.Commands.add("applicationInAppTable", appName => { Cypress.Commands.add("applicationInAppTable", appName => {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 }) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 })
cy.get(".appTable", { timeout: 5000 }).within(() => { cy.get(".appTable", { timeout: 30000 }).within(() => {
cy.get(".title").contains(appName).should("exist") cy.get(".title").contains(appName).should("exist")
}) })
}) })
@ -441,7 +441,7 @@ Cypress.Commands.add("createTable", (tableName, initialTable) => {
if (!initialTable) { if (!initialTable) {
cy.navigateToDataSection() cy.navigateToDataSection()
} }
cy.get(`[data-cy="new-datasource"]`, { timeout: 2000 }).click() cy.get(`[data-cy="new-datasource"]`, { timeout: 20000 }).click()
cy.wait(2000) cy.wait(2000)
cy.get(".item", { timeout: 2000 }) cy.get(".item", { timeout: 2000 })
.contains("Budibase DB") .contains("Budibase DB")
@ -461,10 +461,7 @@ Cypress.Commands.add("createTable", (tableName, initialTable) => {
cy.get(".nav-item", { timeout: 2000 }) cy.get(".nav-item", { timeout: 2000 })
.contains("Budibase DB") .contains("Budibase DB")
.click({ force: true }) .click({ force: true })
cy.get(".spectrum-Tabs-content", { timeout: 2000 }).should( cy.get(".nav-item-content", { timeout: 2000 }).should("contain", tableName)
"contain",
tableName
)
}) })
Cypress.Commands.add("createTestTableWithData", () => { Cypress.Commands.add("createTestTableWithData", () => {
@ -483,7 +480,7 @@ Cypress.Commands.add(
// Configure column // Configure column
cy.get(".spectrum-Modal").within(() => { cy.get(".spectrum-Modal").within(() => {
cy.get("input").first().type(columnName).blur() cy.get("input").first().type(columnName)
// Unset table display column // Unset table display column
cy.contains("display column").click({ force: true }) cy.contains("display column").click({ force: true })
@ -795,7 +792,7 @@ Cypress.Commands.add("selectExternalDatasource", datasourceName => {
// Navigates to Data Section // Navigates to Data Section
cy.navigateToDataSection() cy.navigateToDataSection()
// Open Datasource modal // Open Datasource modal
cy.get(".nav").within(() => { cy.get(".container").within(() => {
cy.get("[data-cy='new-datasource']").click() cy.get("[data-cy='new-datasource']").click()
}) })
// Clicks specified datasource & continue // Clicks specified datasource & continue

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "2.2.4-alpha.4", "version": "2.2.10-alpha.11",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -71,10 +71,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "2.2.4-alpha.4", "@budibase/bbui": "2.2.10-alpha.11",
"@budibase/client": "2.2.4-alpha.4", "@budibase/client": "2.2.10-alpha.11",
"@budibase/frontend-core": "2.2.4-alpha.4", "@budibase/frontend-core": "2.2.10-alpha.11",
"@budibase/string-templates": "2.2.4-alpha.4", "@budibase/string-templates": "2.2.10-alpha.11",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",

View File

@ -180,7 +180,7 @@
onSelect(block) onSelect(block)
}} }}
> >
<Icon name={showLooping ? "ChevronDown" : "ChevronUp"} /> <Icon name={showLooping ? "ChevronUp" : "ChevronDown"} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -216,7 +216,6 @@
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
margin-top: var(--spacing-m);
} }
.table-title > div { .table-title > div {
margin-left: var(--spacing-xs); margin-left: var(--spacing-xs);

View File

@ -1,7 +1,7 @@
<script> <script>
import { ActionButton, Modal, notifications } from "@budibase/bbui" import { ActionButton, Modal, notifications } from "@budibase/bbui"
import CreateEditRelationship from "../../Datasources/CreateEditRelationship.svelte" import CreateEditRelationship from "../../Datasources/CreateEditRelationship.svelte"
import { datasources, tables } from "../../../../stores/backend" import { datasources } from "../../../../stores/backend"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
export let table export let table
@ -21,8 +21,6 @@
// Create datasource // Create datasource
await datasources.save(datasource) await datasources.save(datasource)
notifications.success(`Relationship information saved.`) notifications.success(`Relationship information saved.`)
const tableList = await tables.fetch()
await tables.select(tableList.find(tbl => tbl._id === table._id))
dispatch("updatecolumns") dispatch("updatecolumns")
} catch (err) { } catch (err) {
notifications.error(`Error saving relationship info: ${err}`) notifications.error(`Error saving relationship info: ${err}`)

View File

@ -102,7 +102,7 @@
// in the case of internal tables the sourceId will just be undefined // in the case of internal tables the sourceId will just be undefined
$: tableOptions = $tables.list.filter( $: tableOptions = $tables.list.filter(
opt => opt =>
opt._id !== $tables.draft._id && opt._id !== $tables.selected._id &&
opt.type === table.type && opt.type === table.type &&
table.sourceId === opt.sourceId table.sourceId === opt.sourceId
) )
@ -112,7 +112,7 @@
async function saveColumn() { async function saveColumn() {
if (field.type === AUTO_TYPE) { if (field.type === AUTO_TYPE) {
field = buildAutoColumn($tables.draft.name, field.name, field.subtype) field = buildAutoColumn($tables.selected.name, field.name, field.subtype)
} }
if (field.type !== LINK_TYPE) { if (field.type !== LINK_TYPE) {
delete field.fieldName delete field.fieldName
@ -310,7 +310,7 @@
newError.name = `${PROHIBITED_COLUMN_NAMES.join( newError.name = `${PROHIBITED_COLUMN_NAMES.join(
", " ", "
)} are not allowed as column names` )} are not allowed as column names`
} else if (inUse($tables.draft, fieldInfo.name, originalName)) { } else if (inUse($tables.selected, fieldInfo.name, originalName)) {
newError.name = `Column name already in use.` newError.name = `Column name already in use.`
} }
if (fieldInfo.fieldName && fieldInfo.tableId) { if (fieldInfo.fieldName && fieldInfo.tableId) {

View File

@ -1,6 +1,6 @@
<script> <script>
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { tables, rows } from "stores/backend" import { tables } from "stores/backend"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte" import RowFieldControl from "../RowFieldControl.svelte"
import { API } from "api" import { API } from "api"
@ -25,7 +25,6 @@
try { try {
await API.saveRow({ ...row, tableId: table._id }) await API.saveRow({ ...row, tableId: table._id })
notifications.success("Row saved successfully") notifications.success("Row saved successfully")
rows.save()
dispatch("updaterows") dispatch("updaterows")
} catch (error) { } catch (error) {
if (error.handled) { if (error.handled) {

View File

@ -1,6 +1,6 @@
<script> <script>
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { tables, rows } from "stores/backend" import { tables } from "stores/backend"
import { roles } from "stores/backend" import { roles } from "stores/backend"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte" import RowFieldControl from "../RowFieldControl.svelte"
@ -57,7 +57,6 @@
try { try {
await API.saveRow({ ...row, tableId: table._id }) await API.saveRow({ ...row, tableId: table._id })
notifications.success("User saved successfully") notifications.success("User saved successfully")
rows.save()
dispatch("updaterows") dispatch("updaterows")
} catch (error) { } catch (error) {
if (error.handled) { if (error.handled) {

View File

@ -9,19 +9,20 @@
$: views = $tables.list.flatMap(table => Object.keys(table.views || {})) $: views = $tables.list.flatMap(table => Object.keys(table.views || {}))
function saveView() { const saveView = async () => {
name = name?.trim()
if (views.includes(name)) { if (views.includes(name)) {
notifications.error(`View exists with name ${name}`) notifications.error(`View exists with name ${name}`)
return return
} }
try { try {
viewsStore.save({ await viewsStore.save({
name, name,
tableId: $tables.selected._id, tableId: $tables.selected._id,
field, field,
}) })
notifications.success(`View ${name} created`) notifications.success(`View ${name} created`)
$goto(`../../view/${name}`) $goto(`../../view/${encodeURIComponent(name)}`)
} catch (error) { } catch (error) {
notifications.error("Error creating view") notifications.error("Error creating view")
} }

View File

@ -1,7 +1,5 @@
<script> <script>
import { onMount } from "svelte" import { goto, isActive, params } from "@roxi/routify"
import { get } from "svelte/store"
import { goto, params } from "@roxi/routify"
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend" import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
import { database, datasources, queries, tables, views } from "stores/backend" import { database, datasources, queries, tables, views } from "stores/backend"
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte" import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte"
@ -14,40 +12,61 @@
customQueryText, customQueryText,
} from "helpers/data/utils" } from "helpers/data/utils"
import IntegrationIcon from "./IntegrationIcon.svelte" import IntegrationIcon from "./IntegrationIcon.svelte"
import { notifications } from "@budibase/bbui"
let openDataSources = [] let openDataSources = []
$: enrichedDataSources = Array.isArray($datasources.list) $: enrichedDataSources = enrichDatasources(
? $datasources.list.map(datasource => { $datasources,
const selected = $datasources.selected === datasource._id $params,
const open = openDataSources.includes(datasource._id) $isActive,
const containsSelected = containsActiveEntity(datasource) $tables,
const onlySource = $datasources.list.length === 1 $queries,
return { $views
...datasource, )
selected,
open: selected || open || containsSelected || onlySource,
}
})
: []
$: openDataSource = enrichedDataSources.find(x => x.open) $: openDataSource = enrichedDataSources.find(x => x.open)
$: { $: {
// Ensure the open datasource is always included in the list of open // Ensure the open datasource is always actually open
// datasources
if (openDataSource) { if (openDataSource) {
openNode(openDataSource) openNode(openDataSource)
} }
} }
function selectDatasource(datasource) { const enrichDatasources = (
openNode(datasource) datasources,
datasources.select(datasource._id) params,
$goto(`./datasource/${datasource._id}`) isActive,
tables,
queries,
views
) => {
if (!datasources?.list?.length) {
return []
}
return datasources.list.map(datasource => {
const selected =
isActive("./datasource") &&
datasources.selectedDatasourceId === datasource._id
const open = openDataSources.includes(datasource._id)
const containsSelected = containsActiveEntity(
datasource,
params,
isActive,
tables,
queries,
views
)
const onlySource = datasources.list.length === 1
return {
...datasource,
selected,
containsSelected,
open: selected || open || containsSelected || onlySource,
}
})
} }
function onClickQuery(query) { function selectDatasource(datasource) {
queries.select(query) openNode(datasource)
$goto(`./datasource/${query.datasourceId}/${query._id}`) $goto(`./datasource/${datasource._id}`)
} }
function closeNode(datasource) { function closeNode(datasource) {
@ -69,21 +88,39 @@
} }
} }
onMount(async () => { const containsActiveEntity = (
try { datasource,
await datasources.fetch() params,
await queries.fetch() isActive,
} catch (error) { tables,
notifications.error("Error fetching datasources and queries") queries,
} views
}) ) => {
// Check for being on a datasource page
const containsActiveEntity = datasource => { if (params.datasourceId === datasource._id) {
// If we're view a query then the datasource ID is in the URL
if ($params.selectedDatasource === datasource._id) {
return true return true
} }
// Check for hardcoded datasource edge cases
if (
isActive("./datasource/bb_internal") &&
datasource._id === "bb_internal"
) {
return true
}
if (
isActive("./datasource/datasource_internal_bb_default") &&
datasource._id === "datasource_internal_bb_default"
) {
return true
}
// Check for a matching query
if (params.queryId) {
const query = queries.list?.find(q => q._id === params.queryId)
return datasource._id === query?.datasourceId
}
// If there are no entities it can't contain anything // If there are no entities it can't contain anything
if (!datasource.entities) { if (!datasource.entities) {
return false return false
@ -96,13 +133,13 @@
} }
// Check for a matching table // Check for a matching table
if ($params.selectedTable) { if (params.tableId) {
const selectedTable = get(tables).selected?._id const selectedTable = tables.selected?._id
return options.find(x => x._id === selectedTable) != null return options.find(x => x._id === selectedTable) != null
} }
// Check for a matching view // Check for a matching view
const selectedView = get(views).selected?.name const selectedView = views.selected?.name
const table = options.find(table => { const table = options.find(table => {
return table.views?.[selectedView] != null return table.views?.[selectedView] != null
}) })
@ -117,7 +154,7 @@
border={idx > 0} border={idx > 0}
text={datasource.name} text={datasource.name}
opened={datasource.open} opened={datasource.open}
selected={datasource.selected} selected={$isActive("./datasource") && datasource.selected}
withArrow={true} withArrow={true}
on:click={() => selectDatasource(datasource)} on:click={() => selectDatasource(datasource)}
on:iconClick={() => toggleNode(datasource)} on:iconClick={() => toggleNode(datasource)}
@ -143,11 +180,11 @@
iconText={customQueryIconText(datasource, query)} iconText={customQueryIconText(datasource, query)}
iconColor={customQueryIconColor(datasource, query)} iconColor={customQueryIconColor(datasource, query)}
text={customQueryText(datasource, query)} text={customQueryText(datasource, query)}
opened={$queries.selected === query._id} selected={$isActive("./query/:queryId") &&
selected={$queries.selected === query._id} $queries.selectedQueryId === query._id}
on:click={() => onClickQuery(query)} on:click={() => $goto(`./query/${query._id}`)}
> >
<EditQueryPopover {query} {onClickQuery} /> <EditQueryPopover {query} />
</NavItem> </NavItem>
{/each} {/each}
{/if} {/if}
@ -156,6 +193,9 @@
{/if} {/if}
<style> <style>
.hierarchy-items-container {
margin: 0 calc(-1 * var(--spacing-xl));
}
.datasource-icon { .datasource-icon {
display: grid; display: grid;
place-items: center; place-items: center;

View File

@ -104,7 +104,6 @@
} }
function onClickTable(table) { function onClickTable(table) {
tables.select(table)
$goto(`../../table/${table._id}`) $goto(`../../table/${table._id}`)
} }

View File

@ -11,7 +11,7 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import ICONS from "../icons" import ICONS from "../icons"
import { API } from "api" import { API } from "api"
import { IntegrationTypes } from "constants/backend" import { IntegrationTypes, DatasourceTypes } from "constants/backend"
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte" import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte" import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
import GoogleDatasourceConfigModal from "components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte" import GoogleDatasourceConfigModal from "components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte"
@ -31,6 +31,7 @@
$: customIntegrations = Object.entries(integrations).filter( $: customIntegrations = Object.entries(integrations).filter(
entry => entry[1].custom entry => entry[1].custom
) )
$: sortedIntegrations = sortIntegrations(integrations)
checkShowImport() checkShowImport()
@ -99,6 +100,29 @@
} }
integrations = newIntegrations integrations = newIntegrations
} }
function sortIntegrations(integrations) {
let integrationsArray = Object.entries(integrations)
function getTypeOrder(schema) {
if (schema.type === DatasourceTypes.API) {
return 1
}
if (schema.type === DatasourceTypes.RELATIONAL) {
return 2
}
return schema.type?.charCodeAt(0)
}
integrationsArray.sort((a, b) => {
let typeOrderA = getTypeOrder(a[1])
let typeOrderB = getTypeOrder(b[1])
if (typeOrderA === typeOrderB) {
return a[1].friendlyName?.localeCompare(b[1].friendlyName)
}
return typeOrderA < typeOrderB ? -1 : 1
})
return integrationsArray
}
</script> </script>
<Modal bind:this={internalTableModal}> <Modal bind:this={internalTableModal}>
@ -157,7 +181,7 @@
<Layout noPadding gap="XS"> <Layout noPadding gap="XS">
<Body size="S">Connect to an external datasource</Body> <Body size="S">Connect to an external datasource</Body>
<div class="item-list"> <div class="item-list">
{#each Object.entries(integrations).filter(([key, val]) => key !== IntegrationTypes.INTERNAL && !val.custom) as [integrationType, schema]} {#each sortedIntegrations.filter(([key, val]) => key !== IntegrationTypes.INTERNAL && !val.custom) as [integrationType, schema]}
<DatasourceCard <DatasourceCard
on:selected={evt => selectIntegration(evt.detail)} on:selected={evt => selectIntegration(evt.detail)}
{schema} {schema}

View File

@ -64,7 +64,6 @@
// reload // reload
await datasources.fetch() await datasources.fetch()
await queries.fetch() await queries.fetch()
await datasources.select(datasourceId)
if (navigateDatasource) { if (navigateDatasource) {
$goto(`./datasource/${datasourceId}`) $goto(`./datasource/${datasourceId}`)

View File

@ -1,6 +1,6 @@
<script> <script>
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { datasources, queries, tables } from "stores/backend" import { datasources } from "stores/backend"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui" import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -14,23 +14,10 @@
async function deleteDatasource() { async function deleteDatasource() {
try { try {
let wasSelectedSource = $datasources.selected const isSelected = datasource.selected || datasource.containsSelected
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) await datasources.delete(datasource)
notifications.success("Datasource deleted") notifications.success("Datasource deleted")
// Navigate to first index page if the source you are deleting is selected if (isSelected) {
const entities = Object.values(datasource?.entities || {})
if (
wasSelectedSource === datasource._id ||
(entities &&
entities.find(entity => entity._id === wasSelectedTable?._id))
) {
$goto("./datasource") $goto("./datasource")
} }
} catch (error) { } catch (error) {

View File

@ -5,23 +5,17 @@
import { datasources, queries } from "stores/backend" import { datasources, queries } from "stores/backend"
export let query export let query
export let onClickQuery
let confirmDeleteDialog let confirmDeleteDialog
async function deleteQuery() { async function deleteQuery() {
try { try {
const wasSelectedQuery = $queries.selected // Go back to the datasource if we are deleting the active query
// need to calculate this before the query is deleted if ($queries.selectedQueryId === query._id) {
const navigateToDatasource = wasSelectedQuery === query._id
await queries.delete(query)
await datasources.fetch()
if (navigateToDatasource) {
await datasources.select(query.datasourceId)
$goto(`./datasource/${query.datasourceId}`) $goto(`./datasource/${query.datasourceId}`)
} }
await queries.delete(query)
await datasources.fetch()
notifications.success("Query deleted") notifications.success("Query deleted")
} catch (error) { } catch (error) {
notifications.error("Error deleting query") notifications.error("Error deleting query")
@ -31,7 +25,7 @@
async function duplicateQuery() { async function duplicateQuery() {
try { try {
const newQuery = await queries.duplicate(query) const newQuery = await queries.duplicate(query)
onClickQuery(newQuery) $goto(`./query/${newQuery._id}`)
} catch (error) { } catch (error) {
notifications.error("Error duplicating query") notifications.error("Error duplicating query")
} }

View File

@ -1,39 +1,18 @@
<script> <script>
import { goto } from "@roxi/routify"
import { tables, views, database } from "stores/backend" import { tables, views, database } from "stores/backend"
import { TableNames } from "constants" import { TableNames } from "constants"
import EditTablePopover from "./popovers/EditTablePopover.svelte" import EditTablePopover from "./popovers/EditTablePopover.svelte"
import EditViewPopover from "./popovers/EditViewPopover.svelte" import EditViewPopover from "./popovers/EditViewPopover.svelte"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
import { goto, isActive } from "@roxi/routify"
const alphabetical = (a, b) => a.name?.toLowerCase() > b.name?.toLowerCase() const alphabetical = (a, b) => a.name?.toLowerCase() > b.name?.toLowerCase()
export let sourceId export let sourceId
$: selectedView = $views.selected && $views.selected.name
$: sortedTables = $tables.list $: sortedTables = $tables.list
.filter(table => table.sourceId === sourceId) .filter(table => table.sourceId === sourceId)
.sort(alphabetical) .sort(alphabetical)
function selectTable(table) {
tables.select(table)
$goto(`./table/${table._id}`)
}
function selectView(view) {
views.select(view)
$goto(`./view/${view.name}`)
}
function onClickView(table, viewName) {
if (selectedView === viewName) {
return
}
selectView({
name: viewName,
...table.views[viewName],
})
}
</script> </script>
{#if $database?._id} {#if $database?._id}
@ -44,8 +23,9 @@
border={idx > 0} border={idx > 0}
icon={table._id === TableNames.USERS ? "UserGroup" : "Table"} icon={table._id === TableNames.USERS ? "UserGroup" : "Table"}
text={table.name} text={table.name}
selected={$tables.selected?._id === table._id} selected={$isActive("./table/:tableId") &&
on:click={() => selectTable(table)} $tables.selected?._id === table._id}
on:click={() => $goto(`./table/${table._id}`)}
> >
{#if table._id !== TableNames.USERS} {#if table._id !== TableNames.USERS}
<EditTablePopover {table} /> <EditTablePopover {table} />
@ -56,8 +36,8 @@
indentLevel={2} indentLevel={2}
icon="Remove" icon="Remove"
text={viewName} text={viewName}
selected={selectedView === viewName} selected={$isActive("./view") && $views.selected?.name === viewName}
on:click={() => onClickView(table, viewName)} on:click={() => $goto(`./view/${encodeURIComponent(viewName)}`)}
> >
<EditViewPopover <EditViewPopover
view={{ name: viewName, ...table.views[viewName] }} view={{ name: viewName, ...table.views[viewName] }}

View File

@ -1,5 +1,5 @@
<script> <script>
import { goto } from "@roxi/routify" import { goto, params } from "@roxi/routify"
import { store } from "builderStore" import { store } from "builderStore"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { tables, datasources } from "stores/backend" import { tables, datasources } from "stores/backend"
@ -41,17 +41,16 @@
} }
async function deleteTable() { async function deleteTable() {
const wasSelectedTable = $tables.selected const isSelected = $params.tableId === table._id
try { try {
await tables.delete(table) await tables.delete(table)
await store.actions.screens.delete(templateScreens) await store.actions.screens.delete(templateScreens)
await tables.fetch()
if (table.type === "external") { if (table.type === "external") {
await datasources.fetch() await datasources.fetch()
} }
notifications.success("Table deleted") notifications.success("Table deleted")
if (wasSelectedTable && wasSelectedTable._id === table._id) { if (isSelected) {
$goto("./table") $goto(`./datasource/${table.datasourceId}`)
} }
} catch (error) { } catch (error) {
notifications.error("Error deleting table") notifications.error("Error deleting table")

View File

@ -1,5 +1,5 @@
<script> <script>
import { goto } from "@roxi/routify" import { goto, params } from "@roxi/routify"
import { views } from "stores/backend" import { views } from "stores/backend"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -33,11 +33,15 @@
async function deleteView() { async function deleteView() {
try { try {
const isSelected =
decodeURIComponent($params.viewName) === $views.selectedViewName
const name = view.name const name = view.name
const id = view.tableId const id = view.tableId
await views.delete(name) await views.delete(name)
notifications.success("View deleted") notifications.success("View deleted")
$goto(`./table/${id}`) if (isSelected) {
$goto(`./table/${id}`)
}
} catch (error) { } catch (error) {
notifications.error("Error deleting view") notifications.error("Error deleting view")
} }

View File

@ -12,7 +12,6 @@
import { ProgressCircle } from "@budibase/bbui" import { ProgressCircle } from "@budibase/bbui"
import CopyInput from "components/common/inputs/CopyInput.svelte" import CopyInput from "components/common/inputs/CopyInput.svelte"
let feedbackModal
let publishModal let publishModal
let asyncModal let asyncModal
let publishCompleteModal let publishCompleteModal
@ -23,13 +22,13 @@
export let onOk export let onOk
async function deployApp() { async function publishApp() {
try { try {
//In Progress //In Progress
asyncModal.show() asyncModal.show()
publishModal.hide() publishModal.hide()
published = await API.deployAppChanges() published = await API.publishAppChanges($store.appId)
if (typeof onOk === "function") { if (typeof onOk === "function") {
await onOk() await onOk()
@ -56,20 +55,11 @@
</script> </script>
<Button cta on:click={publishModal.show}>Publish</Button> <Button cta on:click={publishModal.show}>Publish</Button>
<Modal bind:this={feedbackModal}>
<ModalContent
title="Enjoying Budibase?"
size="L"
showConfirmButton={false}
showCancelButton={false}
/>
</Modal>
<Modal bind:this={publishModal}> <Modal bind:this={publishModal}>
<ModalContent <ModalContent
title="Publish to Production" title="Publish to Production"
confirmText="Publish" confirmText="Publish"
onConfirm={deployApp} onConfirm={publishApp}
dataCy={"deploy-app-modal"} dataCy={"deploy-app-modal"}
> >
<span <span

View File

@ -51,6 +51,7 @@
<style> <style>
.panel { .panel {
width: 260px; width: 260px;
flex: 0 0 260px;
background: var(--background); background: var(--background);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -66,6 +67,7 @@
} }
.panel.wide { .panel.wide {
width: 420px; width: 420px;
flex: 0 0 420px;
} }
.header { .header {
flex: 0 0 48px; flex: 0 0 48px;

View File

@ -29,11 +29,12 @@
export let query export let query
const transformerDocs = "https://docs.budibase.com/docs/transformers"
let fields = query?.schema ? schemaToFields(query.schema) : [] let fields = query?.schema ? schemaToFields(query.schema) : []
let parameters let parameters
let data = [] let data = []
let saveId let saveId
const transformerDocs = "https://docs.budibase.com/docs/transformers"
$: datasource = $datasources.list.find(ds => ds._id === query.datasourceId) $: datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
$: query.schema = fieldsToSchema(fields) $: query.schema = fieldsToSchema(fields)
@ -94,132 +95,144 @@
try { try {
const { _id } = await queries.save(query.datasourceId, query) const { _id } = await queries.save(query.datasourceId, query)
saveId = _id saveId = _id
notifications.success(`Query saved successfully.`) notifications.success(`Query saved successfully`)
$goto(`../${_id}`)
// Go to the correct URL if we just created a new query
if (!query._rev) {
$goto(`../../${_id}`)
}
} catch (error) { } catch (error) {
notifications.error("Error creating query") notifications.error("Error saving query")
} }
} }
</script> </script>
<Layout gap="S" noPadding> <div class="wrapper">
<Heading size="M">Query {integrationInfo?.friendlyName}</Heading> <Layout gap="S" noPadding>
<Divider /> <Heading size="M">Query {integrationInfo?.friendlyName}</Heading>
<Heading size="S">Config</Heading>
<div class="config">
<div class="config-field">
<Label>Query Name</Label>
<Input bind:value={query.name} />
</div>
{#if queryConfig}
<div class="config-field">
<Label>Function</Label>
<Select
bind:value={query.queryVerb}
on:change={resetDependentFields}
options={Object.keys(queryConfig)}
getOptionLabel={verb =>
queryConfig[verb]?.displayName || capitalise(verb)}
/>
</div>
<div class="config-field">
<AccessLevelSelect {saveId} {query} label="Access Level" />
</div>
{#if integrationInfo?.extra && query.queryVerb}
<ExtraQueryConfig
{query}
{populateExtraQuery}
config={integrationInfo.extra}
/>
{/if}
{#key query.parameters}
<BindingBuilder
queryBindings={query.parameters}
bindable={false}
on:change={e => {
query.parameters = e.detail.map(binding => {
return {
name: binding.name,
default: binding.value,
}
})
}}
/>
{/key}
{/if}
</div>
{#if shouldShowQueryConfig}
<Divider /> <Divider />
<Heading size="S">Config</Heading>
<div class="config"> <div class="config">
<Heading size="S">Fields</Heading> <div class="config-field">
<Body size="S">Fill in the fields specific to this query.</Body> <Label>Query Name</Label>
<IntegrationQueryEditor <Input bind:value={query.name} />
{datasource}
{query}
height={200}
schema={queryConfig[query.queryVerb]}
bind:parameters
/>
<Divider />
</div>
<div class="config">
<div class="help-heading">
<Heading size="S">Transformer</Heading>
<Icon
on:click={() => window.open(transformerDocs)}
hoverable
name="Help"
size="L"
/>
</div> </div>
<Body size="S" {#if queryConfig}
>Add a JavaScript function to transform the query result.</Body <div class="config-field">
> <Label>Function</Label>
<CodeMirrorEditor <Select
height={200} bind:value={query.queryVerb}
label="Transformer" on:change={resetDependentFields}
value={query.transformer} options={Object.keys(queryConfig)}
resize="vertical" getOptionLabel={verb =>
on:change={e => (query.transformer = e.detail)} queryConfig[verb]?.displayName || capitalise(verb)}
/> />
<Divider /> </div>
</div> <div class="config-field">
<div class="viewer-controls"> <AccessLevelSelect {saveId} {query} label="Access Level" />
<Heading size="S">Results</Heading> </div>
<ButtonGroup gap="M"> {#if integrationInfo?.extra && query.queryVerb}
<Button cta disabled={queryInvalid} on:click={saveQuery}> <ExtraQueryConfig
Save Query {query}
</Button> {populateExtraQuery}
<Button secondary on:click={previewQuery}>Run Query</Button> config={integrationInfo.extra}
</ButtonGroup> />
</div> {/if}
<Body size="S"> {#key query.parameters}
Below, you can preview the results from your query and change the schema. <BindingBuilder
</Body> queryBindings={query.parameters}
<section class="viewer"> bindable={false}
{#if data} on:change={e => {
<Tabs selected="JSON"> query.parameters = e.detail.map(binding => {
<Tab title="JSON"> return {
<JSONPreview data={data[0]} minHeight="120" /> name: binding.name,
</Tab> default: binding.value,
<Tab title="Schema"> }
<KeyValueBuilder })
bind:object={fields} }}
name="field" />
headings {/key}
options={SchemaTypeOptions}
/>
</Tab>
<Tab title="Preview">
<ExternalDataSourceTable {query} {data} />
</Tab>
</Tabs>
{/if} {/if}
</section> </div>
{/if} {#if shouldShowQueryConfig}
</Layout> <Divider />
<div class="config">
<Heading size="S">Fields</Heading>
<Body size="S">Fill in the fields specific to this query.</Body>
<IntegrationQueryEditor
{datasource}
{query}
height={200}
schema={queryConfig[query.queryVerb]}
bind:parameters
/>
<Divider />
</div>
<div class="config">
<div class="help-heading">
<Heading size="S">Transformer</Heading>
<Icon
on:click={() => window.open(transformerDocs)}
hoverable
name="Help"
size="L"
/>
</div>
<Body size="S"
>Add a JavaScript function to transform the query result.</Body
>
<CodeMirrorEditor
height={200}
label="Transformer"
value={query.transformer}
resize="vertical"
on:change={e => (query.transformer = e.detail)}
/>
<Divider />
</div>
<div class="viewer-controls">
<Heading size="S">Results</Heading>
<ButtonGroup gap="XS">
<Button cta disabled={queryInvalid} on:click={saveQuery}>
Save Query
</Button>
<Button secondary on:click={previewQuery}>Run Query</Button>
</ButtonGroup>
</div>
<Body size="S">
Below, you can preview the results from your query and change the
schema.
</Body>
<section class="viewer">
{#if data}
<Tabs selected="JSON">
<Tab title="JSON">
<JSONPreview data={data[0]} minHeight="120" />
</Tab>
<Tab title="Schema">
<KeyValueBuilder
bind:object={fields}
name="field"
headings
options={SchemaTypeOptions}
/>
</Tab>
<Tab title="Preview">
<ExternalDataSourceTable {query} {data} />
</Tab>
</Tabs>
{/if}
</section>
{/if}
</Layout>
</div>
<style> <style>
.wrapper {
width: 640px;
margin: auto;
}
.config { .config {
display: grid; display: grid;
grid-gap: var(--spacing-s); grid-gap: var(--spacing-s);

View File

@ -1,5 +1,5 @@
<script> <script>
import { params } from "@roxi/routify" import { goto, params } from "@roxi/routify"
import { datasources, flags, integrations, queries } from "stores/backend" import { datasources, flags, integrations, queries } from "stores/backend"
import { import {
Banner, Banner,
@ -23,7 +23,7 @@
import CodeMirrorEditor, { import CodeMirrorEditor, {
EditorModes, EditorModes,
} from "components/common/CodeMirrorEditor.svelte" } from "components/common/CodeMirrorEditor.svelte"
import RestBodyInput from "../../_components/RestBodyInput.svelte" import RestBodyInput from "./RestBodyInput.svelte"
import { capitalise } from "helpers" import { capitalise } from "helpers"
import { onMount } from "svelte" import { onMount } from "svelte"
import restUtils from "helpers/data/utils" import restUtils from "helpers/data/utils"
@ -36,7 +36,7 @@
} from "constants/backend" } from "constants/backend"
import JSONPreview from "components/integration/JSONPreview.svelte" import JSONPreview from "components/integration/JSONPreview.svelte"
import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte" import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte"
import DynamicVariableModal from "../../_components/DynamicVariableModal.svelte" import DynamicVariableModal from "./DynamicVariableModal.svelte"
import Placeholder from "assets/bb-spaceship.svg" import Placeholder from "assets/bb-spaceship.svg"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
@ -49,6 +49,8 @@
toBindingsArray, toBindingsArray,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
export let queryId
let query, datasource let query, datasource
let breakQs = {}, let breakQs = {},
requestBindings = {} requestBindings = {}
@ -102,8 +104,8 @@
function getSelectedQuery() { function getSelectedQuery() {
return cloneDeep( return cloneDeep(
$queries.list.find(q => q._id === $queries.selected) || { $queries.list.find(q => q._id === queryId) || {
datasourceId: $params.selectedDatasource, datasourceId: $params.datasourceId,
parameters: [], parameters: [],
fields: { fields: {
// only init the objects, everything else is optional strings // only init the objects, everything else is optional strings
@ -159,6 +161,7 @@
async function saveQuery() { async function saveQuery() {
const toSave = buildQuery() const toSave = buildQuery()
try { try {
const isNew = !query._rev
const { _id } = await queries.save(toSave.datasourceId, toSave) const { _id } = await queries.save(toSave.datasourceId, toSave)
saveId = _id saveId = _id
query = getSelectedQuery() query = getSelectedQuery()
@ -174,6 +177,9 @@
staticVariables, staticVariables,
restBindings restBindings
) )
if (isNew) {
$goto(`../../${_id}`)
}
} catch (err) { } catch (err) {
notifications.error(`Error saving query`) notifications.error(`Error saving query`)
} }
@ -464,8 +470,9 @@
on:click={saveQuery} on:click={saveQuery}
tooltip={!hasSchema tooltip={!hasSchema
? "Saving a query before sending will mean no schema is generated" ? "Saving a query before sending will mean no schema is generated"
: null}>Save</Button : null}
> >Save
</Button>
</div> </div>
<Tabs selected="Bindings" quiet noPadding noHorizPadding onTop> <Tabs selected="Bindings" quiet noPadding noHorizPadding onTop>
<Tab title="Bindings"> <Tab title="Bindings">
@ -708,26 +715,33 @@
margin: 0 auto; margin: 0 auto;
height: 100%; height: 100%;
} }
.table { .table {
width: 960px; width: 960px;
} }
.url-block { .url-block {
display: flex; display: flex;
gap: var(--spacing-s); gap: var(--spacing-s);
z-index: 200; z-index: 200;
} }
.verb { .verb {
flex: 1; flex: 1;
} }
.url { .url {
flex: 4; flex: 4;
} }
.top { .top {
min-height: 50%; min-height: 50%;
} }
.bottom { .bottom {
padding-bottom: 50px; padding-bottom: 50px;
} }
.stats { .stats {
display: flex; display: flex;
gap: var(--spacing-xl); gap: var(--spacing-xl);
@ -735,40 +749,49 @@
margin-right: 0; margin-right: 0;
align-items: center; align-items: center;
} }
.green { .green {
color: #53a761; color: #53a761;
} }
.red { .red {
color: #ea7d82; color: #ea7d82;
} }
.top-bar { .top-bar {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.access { .access {
display: flex; display: flex;
gap: var(--spacing-m); gap: var(--spacing-m);
align-items: center; align-items: center;
} }
.placeholder-internal { .placeholder-internal {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 200px; width: 200px;
gap: var(--spacing-l); gap: var(--spacing-l);
} }
.placeholder { .placeholder {
display: flex; display: flex;
margin-top: var(--spacing-xl); margin-top: var(--spacing-xl);
justify-content: center; justify-content: center;
} }
.auth-container { .auth-container {
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.auth-select { .auth-select {
width: 200px; width: 200px;
} }
.pagination { .pagination {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;

View File

@ -261,3 +261,12 @@ export const BannedSearchTypes = [
"json", "json",
"jsonarray", "jsonarray",
] ]
export const DatasourceTypes = {
RELATIONAL: "Relational",
NON_RELATIONAL: "Non-relational",
SPREADSHEET: "Spreadsheet",
OBJECT_STORE: "Object store",
GRAPH: "Graph",
API: "API",
}

View File

@ -6,11 +6,13 @@ export const syncURLToState = options => {
urlParam, urlParam,
stateKey, stateKey,
validate, validate,
update,
baseUrl = "..", baseUrl = "..",
fallbackUrl, fallbackUrl,
store, store,
routify, routify,
beforeNavigate, beforeNavigate,
decode,
} = options || {} } = options || {}
if ( if (
!urlParam || !urlParam ||
@ -28,17 +30,29 @@ export const syncURLToState = options => {
return return
} }
// Decodes encoded URL params if required
const decodeParams = urlParams => {
if (!decode) {
return urlParams
}
let decoded = {}
Object.keys(urlParams || {}).forEach(key => {
decoded[key] = decode(urlParams[key])
})
return decoded
}
// We can't dynamically fetch the value of stateful routify stores so we need // We can't dynamically fetch the value of stateful routify stores so we need
// to just subscribe and cache the latest versions. // to just subscribe and cache the latest versions.
// We can grab their initial values as this is during component // We can grab their initial values as this is during component
// initialisation. // initialisation.
let cachedParams = get(routify.params) let cachedParams = decodeParams(get(routify.params))
let cachedGoto = get(routify.goto) let cachedGoto = get(routify.goto)
let cachedRedirect = get(routify.redirect) let cachedRedirect = get(routify.redirect)
let cachedPage = get(routify.page) let cachedPage = get(routify.page)
let previousParamsHash = null let previousParamsHash = null
let debug = false let debug = false
const log = (...params) => debug && console.log(...params) const log = (...params) => debug && console.log(`[${urlParam}]`, ...params)
// Navigate to a certain URL // Navigate to a certain URL
const gotoUrl = (url, params) => { const gotoUrl = (url, params) => {
@ -76,7 +90,7 @@ export const syncURLToState = options => {
// Check if new value is valid // Check if new value is valid
if (validate && fallbackUrl) { if (validate && fallbackUrl) {
if (!validate(urlValue)) { if (!validate(urlValue)) {
log("Invalid URL param!") log("Invalid URL param!", urlValue)
redirectUrl(fallbackUrl) redirectUrl(fallbackUrl)
return return
} }
@ -85,10 +99,16 @@ export const syncURLToState = options => {
// Only update state if we have a new value // Only update state if we have a new value
if (urlValue !== stateValue) { if (urlValue !== stateValue) {
log(`state.${stateKey} (${stateValue}) <= url.${urlParam} (${urlValue})`) log(`state.${stateKey} (${stateValue}) <= url.${urlParam} (${urlValue})`)
store.update(state => { if (update) {
state[stateKey] = urlValue // Use custom update function if provided
return state update(urlValue)
}) } else {
// Otherwise manually update the store
store.update(state => ({
...state,
[stateKey]: urlValue,
}))
}
} }
} }
@ -102,7 +122,7 @@ export const syncURLToState = options => {
log(`url.${urlParam} (${urlValue}) <= state.${stateKey} (${stateValue})`) log(`url.${urlParam} (${urlValue}) <= state.${stateKey} (${stateValue})`)
if (validate && fallbackUrl) { if (validate && fallbackUrl) {
if (!validate(stateValue)) { if (!validate(stateValue)) {
log("Invalid state param!") log("Invalid state param!", stateValue)
redirectUrl(fallbackUrl) redirectUrl(fallbackUrl)
return return
} }
@ -130,6 +150,7 @@ export const syncURLToState = options => {
// Subscribe to URL changes and cache them // Subscribe to URL changes and cache them
const unsubscribeParams = routify.params.subscribe($urlParams => { const unsubscribeParams = routify.params.subscribe($urlParams => {
$urlParams = decodeParams($urlParams)
cachedParams = $urlParams cachedParams = $urlParams
mapUrlToState($urlParams) mapUrlToState($urlParams)
}) })

View File

@ -1,75 +1,46 @@
<script> <script>
import { redirect } from "@roxi/routify" import { Button, Layout } from "@budibase/bbui"
import { Button, Tabs, Tab, Layout } from "@budibase/bbui"
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte" import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte" import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
import Panel from "components/design/Panel.svelte"
let selected = "Sources"
let modal let modal
function selectFirstDatasource() {
$redirect("./table")
}
</script> </script>
<!-- routify:options index=1 --> <!-- routify:options index=1 -->
<div class="root"> <div class="data">
<div class="nav"> <Panel title="Sources" borderRight>
<Tabs {selected} on:select={selectFirstDatasource}> <Layout paddingX="L" paddingY="XL" gap="S">
<Tab title="Sources"> <Button dataCy={`new-datasource`} cta on:click={modal.show}>
<Layout paddingX="L" paddingY="L" gap="S"> Add source
<Button dataCy={`new-datasource`} cta wide on:click={modal.show} </Button>
>Add source</Button <CreateDatasourceModal bind:modal />
> <DatasourceNavigator />
</Layout> </Layout>
<CreateDatasourceModal bind:modal /> </Panel>
<DatasourceNavigator />
</Tab>
</Tabs>
</div>
<div class="content"> <div class="content">
<slot /> <slot />
</div> </div>
</div> </div>
<style> <style>
.root { .data {
flex: 1 1 auto; flex: 1 1 auto;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: stretch;
height: 0; height: 0;
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
} }
.content { .content {
flex: 1 1 auto; padding: 28px 40px 40px 40px;
padding: var(--spacing-l) 40px 40px 40px;
overflow-y: auto; overflow-y: auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-l); gap: var(--spacing-l);
}
.content :global(> span) {
display: contents;
}
.nav {
overflow-y: auto;
background: var(--background);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
position: relative; flex: 1 1 auto;
border-right: var(--border-light);
padding-bottom: 60px;
}
.add-button {
position: absolute;
top: var(--spacing-l);
right: var(--spacing-xl);
} }
</style> </style>

View File

@ -0,0 +1,23 @@
<script>
import { params } from "@roxi/routify"
import { datasources } from "stores/backend"
import { syncURLToState } from "helpers/urlStateSync"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
const stopSyncing = syncURLToState({
urlParam: "datasourceId",
stateKey: "selectedDatasourceId",
validate: id => $datasources.list?.some(ds => ds._id === id),
update: datasources.select,
fallbackUrl: "../",
store: datasources,
routify,
})
onDestroy(stopSyncing)
</script>
{#key $params.datasourceId}
<slot />
{/key}

View File

@ -21,34 +21,31 @@
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte" import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
let importQueriesModal
let changed,
isValid = true
let integration, baseDatasource, datasource
let queryList
const querySchema = { const querySchema = {
name: {}, name: {},
queryVerb: { displayName: "Method" }, queryVerb: { displayName: "Method" },
} }
$: baseDatasource = $datasources.list.find( let importQueriesModal
ds => ds._id === $datasources.selected let changed = false
) let isValid = true
let integration, baseDatasource, datasource
let queryList
$: baseDatasource = $datasources.selected
$: queryList = $queries.list.filter( $: queryList = $queries.list.filter(
query => query.datasourceId === datasource?._id query => query.datasourceId === datasource?._id
) )
$: hasChanged(baseDatasource, datasource) $: hasChanged(baseDatasource, datasource)
$: updateDatasource(baseDatasource) $: updateDatasource(baseDatasource)
function hasChanged(base, ds) { const hasChanged = (base, ds) => {
if (base && ds) { if (base && ds) {
changed = !isEqual(base, ds) changed = !isEqual(base, ds)
} }
} }
async function saveDatasource() { const saveDatasource = async () => {
try { try {
// Create datasource // Create datasource
await datasources.save(datasource) await datasources.save(datasource)
@ -63,12 +60,7 @@
} }
} }
function onClickQuery(query) { const updateDatasource = base => {
queries.select(query)
$goto(`./${query._id}`)
}
function updateDatasource(base) {
if (base) { if (base) {
datasource = cloneDeep(base) datasource = cloneDeep(base)
integration = $integrations[datasource.source] integration = $integrations[datasource.source]
@ -87,7 +79,7 @@
{#if datasource && integration} {#if datasource && integration}
<section> <section>
<Layout> <Layout noPadding>
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<header> <header>
<svelte:component <svelte:component
@ -95,16 +87,16 @@
height="26" height="26"
width="26" width="26"
/> />
<Heading size="M">{datasource.name}</Heading> <Heading size="M">{$datasources.selected?.name}</Heading>
</header> </header>
<Body size="M">{integration.description}</Body> <Body size="M">{integration.description}</Body>
</Layout> </Layout>
<Divider /> <Divider />
<div class="config-header"> <div class="config-header">
<Heading size="S">Configuration</Heading> <Heading size="S">Configuration</Heading>
<Button disabled={!changed || !isValid} cta on:click={saveDatasource} <Button disabled={!changed || !isValid} cta on:click={saveDatasource}>
>Save</Button Save
> </Button>
</div> </div>
<IntegrationConfigForm <IntegrationConfigForm
on:change={hasChanged} on:change={hasChanged}
@ -120,12 +112,16 @@
<Heading size="S">Queries</Heading> <Heading size="S">Queries</Heading>
<div class="query-buttons"> <div class="query-buttons">
{#if datasource?.source === IntegrationTypes.REST} {#if datasource?.source === IntegrationTypes.REST}
<Button secondary on:click={() => importQueriesModal.show()} <Button secondary on:click={() => importQueriesModal.show()}>
>Import</Button Import
> </Button>
{/if} {/if}
<Button cta icon="Add" on:click={() => $goto("./new")} <Button
>Add query cta
icon="Add"
on:click={() => $goto(`../../query/new/${datasource._id}`)}
>
Add query
</Button> </Button>
</div> </div>
</div> </div>
@ -137,7 +133,7 @@
{#if queryList && queryList.length > 0} {#if queryList && queryList.length > 0}
<div class="query-list"> <div class="query-list">
<Table <Table
on:click={({ detail }) => onClickQuery(detail)} on:click={({ detail }) => $goto(`../../query/${detail._id}`)}
schema={querySchema} schema={querySchema}
data={queryList} data={queryList}
allowEditColumns={false} allowEditColumns={false}

View File

@ -1,23 +0,0 @@
<script>
import { params } from "@roxi/routify"
import { queries, datasources } from "stores/backend"
import { IntegrationTypes } from "constants/backend"
import { redirect } from "@roxi/routify"
let datasourceId
if ($params.query) {
const query = $queries.list.find(q => q._id === $params.query)
if (query) {
queries.select(query)
datasourceId = query.datasourceId
}
}
const datasource = $datasources.list.find(
ds => ds._id === $datasources.selected || ds._id === datasourceId
)
if (datasource?.source === IntegrationTypes.REST) {
$redirect(`../rest/${$params.query}`)
}
</script>
<slot />

View File

@ -1,39 +0,0 @@
<script>
import { params, redirect } from "@roxi/routify"
import { database, datasources, queries } from "stores/backend"
import QueryInterface from "components/integration/QueryViewer.svelte"
import { IntegrationTypes } from "constants/backend"
let selectedQuery, datasource
$: selectedQuery = $queries.list.find(
query => query._id === $queries.selected
) || {
datasourceId: $params.selectedDatasource,
parameters: [],
fields: {},
queryVerb: "read",
}
$: datasource = $datasources.list.find(
ds => ds._id === $params.selectedDatasource
)
$: {
if (datasource?.source === IntegrationTypes.REST) {
$redirect(`../rest/${$params.query}`)
}
}
</script>
<section>
<div class="inner">
{#if $database._id && selectedQuery}
<QueryInterface query={selectedQuery} />
{/if}
</div>
</section>
<style>
.inner {
width: 640px;
margin: 0 auto;
}
</style>

View File

@ -1,17 +0,0 @@
<script>
import { params } from "@roxi/routify"
import { datasources } from "stores/backend"
if ($params.selectedDatasource && !$params.query) {
const datasource = $datasources.list.find(
m => m._id === $params.selectedDatasource
)
if (datasource) {
datasources.select(datasource._id)
}
}
</script>
{#key $params.selectedDatasource}
<slot />
{/key}

View File

@ -1,13 +0,0 @@
<script>
import { params } from "@roxi/routify"
import { queries } from "stores/backend"
if ($params.query) {
const query = $queries.list.find(q => q._id === $params.query)
if (query) {
queries.select(query)
}
}
</script>
<slot />

View File

@ -1,7 +0,0 @@
<script>
import { datasources } from "stores/backend"
datasources.select("bb_internal")
</script>
<slot />

View File

@ -4,13 +4,19 @@
import ICONS from "components/backend/DatasourceNavigator/icons" import ICONS from "components/backend/DatasourceNavigator/icons"
import { tables, datasources } from "stores/backend" import { tables, datasources } from "stores/backend"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { onMount } from "svelte"
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
let modal let modal
$: internalTablesBySourceId = $tables.list.filter( $: internalTablesBySourceId = $tables.list.filter(
table => table =>
table.type !== "external" && $datasources.selected === table.sourceId table.type !== "external" && table.sourceId === BUDIBASE_INTERNAL_DB_ID
) )
onMount(() => {
datasources.select(BUDIBASE_INTERNAL_DB_ID)
})
</script> </script>
<Modal bind:this={modal}> <Modal bind:this={modal}>
@ -73,7 +79,7 @@
background: var(--background); background: var(--background);
border: var(--border-dark); border: var(--border-dark);
display: grid; display: grid;
grid-template-columns: 2fr 0.75fr 20px; grid-template-columns: 1fr auto;
align-items: center; align-items: center;
padding: var(--spacing-m); padding: var(--spacing-m);
gap: var(--layout-xs); gap: var(--layout-xs);

View File

@ -1,8 +0,0 @@
<script>
import { datasources } from "stores/backend"
import { DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
datasources.select(DEFAULT_BB_DATASOURCE_ID)
</script>
<slot />

View File

@ -4,12 +4,18 @@
import ICONS from "components/backend/DatasourceNavigator/icons" import ICONS from "components/backend/DatasourceNavigator/icons"
import { tables, datasources } from "stores/backend" import { tables, datasources } from "stores/backend"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
import { onMount } from "svelte"
let modal let modal
$: internalTablesBySourceId = $tables.list.filter( $: internalTablesBySourceId = $tables.list.filter(
table => table =>
table.type !== "external" && $datasources.selected === table.sourceId table.type !== "external" && table.sourceId === DEFAULT_BB_DATASOURCE_ID
) )
onMount(() => {
datasources.select(DEFAULT_BB_DATASOURCE_ID)
})
</script> </script>
<Modal bind:this={modal}> <Modal bind:this={modal}>
@ -23,10 +29,11 @@
<svelte:component this={ICONS.BUDIBASE} height="26" width="26" /> <svelte:component this={ICONS.BUDIBASE} height="26" width="26" />
<Heading size="M">Sample Data</Heading> <Heading size="M">Sample Data</Heading>
</header> </header>
<Body size="M">A little something to get you up and running!</Body> <Body size="M">
<Body size="M" A little something to get you up and running!
>If you have no need for this datasource, feel free to delete it.</Body <br />
> If you have no need for this datasource, feel free to delete it.
</Body>
</Layout> </Layout>
<Divider /> <Divider />
<Heading size="S">Tables</Heading> <Heading size="S">Tables</Heading>
@ -73,7 +80,7 @@
background: var(--background); background: var(--background);
border: var(--border-dark); border: var(--border-dark);
display: grid; display: grid;
grid-template-columns: 2fr 0.75fr 20px; grid-template-columns: 1fr auto;
align-items: center; align-items: center;
padding: var(--spacing-m); padding: var(--spacing-m);
gap: var(--layout-xs); gap: var(--layout-xs);

View File

@ -4,12 +4,16 @@
import { onMount } from "svelte" import { onMount } from "svelte"
onMount(async () => { onMount(async () => {
// navigate to first table in list, if not already selected const { list, selected } = $datasources
$datasources.list.length > 0 && $redirect(`./${$datasources.list[0]._id}`) if (selected) {
$redirect(`./${selected?._id}`)
} else {
$redirect(`./${list[0]._id}`)
}
}) })
</script> </script>
{#if $datasources.list.length === 0} {#if !$datasources.list?.length}
<i>Connect your first datasource to start building.</i> <i>Connect your first datasource to start building.</i>
{:else}<i>Select a datasource to edit</i>{/if} {:else}<i>Select a datasource to edit</i>{/if}

View File

@ -0,0 +1,22 @@
<script>
import { queries } from "stores/backend"
import { syncURLToState } from "helpers/urlStateSync"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
const stopSyncing = syncURLToState({
urlParam: "queryId",
stateKey: "selectedQueryId",
validate: id => id === "new" || $queries.list?.some(q => q._id === id),
update: queries.select,
fallbackUrl: "../",
store: queries,
routify,
})
onDestroy(stopSyncing)
</script>
{#key $queries.selectedQueryId}
<slot />
{/key}

View File

@ -0,0 +1,18 @@
<script>
import { database, queries, datasources } from "stores/backend"
import QueryViewer from "components/integration/QueryViewer.svelte"
import RestQueryViewer from "components/integration/RestQueryViewer.svelte"
import { IntegrationTypes } from "constants/backend"
$: query = $queries.selected
$: datasource = $datasources.list.find(ds => ds._id === query?.datasourceId)
$: isRestQuery = datasource?.source === IntegrationTypes.REST
</script>
{#if $database._id && query}
{#if isRestQuery}
<RestQueryViewer queryId={$queries.selectedQueryId} />
{:else}
<QueryViewer {query} />
{/if}
{/if}

View File

@ -0,0 +1,16 @@
<script>
import { onMount } from "svelte"
import { queries } from "stores/backend"
import { redirect } from "@roxi/routify"
onMount(async () => {
const { list, selected } = $queries
if (selected) {
$redirect(`./${selected?._id}`)
} else if (list?.length) {
$redirect(`./${list[0]._id}`)
} else {
$redirect("../")
}
})
</script>

View File

@ -0,0 +1,38 @@
<script>
import { params, redirect } from "@roxi/routify"
import { database, datasources } from "stores/backend"
import QueryViewer from "components/integration/QueryViewer.svelte"
import RestQueryViewer from "components/integration/RestQueryViewer.svelte"
import { IntegrationTypes } from "constants/backend"
$: datasource = $datasources.list.find(ds => ds._id === $params.datasourceId)
$: {
if (!datasource) {
$redirect("../../../")
}
}
$: isRestQuery = datasource?.source === IntegrationTypes.REST
$: query = buildNewQuery(isRestQuery)
const buildNewQuery = isRestQuery => {
let query = {
datasourceId: $params.datasourceId,
parameters: [],
fields: {},
queryVerb: "read",
}
if (isRestQuery) {
query.flags = {}
query.fields = { disabledHeaders: {}, headers: {} }
}
return query
}
</script>
{#if $database._id && datasource && query}
{#if isRestQuery}
<RestQueryViewer />
{:else}
<QueryViewer {query} />
{/if}
{/if}

View File

@ -0,0 +1,5 @@
<script>
import { redirect } from "@roxi/routify"
$redirect("../")
</script>

View File

@ -1,13 +0,0 @@
<script>
import { params } from "@roxi/routify"
import { tables } from "stores/backend"
if ($params.selectedTable) {
const table = $tables.list.find(m => m._id === $params.selectedTable)
if (table) {
tables.select(table)
}
}
</script>
<slot />

View File

@ -0,0 +1,20 @@
<script>
import { syncURLToState } from "helpers/urlStateSync"
import { tables } from "stores/backend"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
const stopSyncing = syncURLToState({
urlParam: "tableId",
stateKey: "selectedTableId",
validate: id => $tables.list?.some(table => table._id === id),
update: tables.select,
fallbackUrl: "../",
store: tables,
routify,
})
onDestroy(stopSyncing)
</script>
<slot />

View File

@ -3,9 +3,11 @@
import { tables, database } from "stores/backend" import { tables, database } from "stores/backend"
</script> </script>
{#if $database?._id && $tables?.selected?.name} {#if $database?._id && $tables?.selected}
<TableDataTable /> <TableDataTable />
{:else}<i>Create your first table to start building</i>{/if} {:else}
<i>Create your first table to start building</i>
{/if}
<style> <style>
i { i {

View File

@ -4,7 +4,7 @@
</script> </script>
<RelationshipDataTable <RelationshipDataTable
tableId={$params.selectedTable} tableId={$params.tableId}
rowId={$params.selectedRow} rowId={$params.rowId}
fieldName={decodeURI($params.selectedField)} fieldName={decodeURI($params.field)}
/> />

View File

@ -1,19 +0,0 @@
<script>
import { tables } from "stores/backend"
import { redirect, leftover } from "@roxi/routify"
import { onMount } from "svelte"
onMount(async () => {
// navigate to first table in list, if not already selected
// and this is the final url (i.e. no selectedTable)
if (
!$leftover &&
$tables.list.length > 0 &&
(!$tables.selected || !$tables.selected._id)
) {
$redirect(`./${$tables.list[0]._id}`)
}
})
</script>
<slot />

View File

@ -1,14 +1,19 @@
<script> <script>
import { redirect } from "@roxi/routify"
import { onMount } from "svelte" import { onMount } from "svelte"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { redirect } from "@roxi/routify"
onMount(async () => { onMount(async () => {
$tables.list.length > 0 && $redirect(`./${$tables.list[0]._id}`) const { list, selected } = $tables
if (selected) {
$redirect(`./${selected?._id}`)
} else if (list?.length) {
$redirect(`./${list[0]._id}`)
}
}) })
</script> </script>
{#if $tables.list.length === 0} {#if !$tables.list?.length}
<i>Create your first table to start building</i> <i>Create your first table to start building</i>
{:else}<i>Select a table to edit</i>{/if} {:else}<i>Select a table to edit</i>{/if}

View File

@ -1,22 +0,0 @@
<script>
import { params } from "@roxi/routify"
import { tables, views } from "stores/backend"
if ($params.selectedView) {
let view
const viewName = decodeURI($params.selectedView)
for (let table of $tables.list) {
if (table.views && table.views[viewName]) {
view = table.views[viewName]
}
}
if (view) {
views.select({
name: viewName,
...view,
})
}
}
</script>
<slot />

View File

@ -0,0 +1,21 @@
<script>
import { views } from "stores/backend"
import { syncURLToState } from "helpers/urlStateSync"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
const stopSyncing = syncURLToState({
urlParam: "viewName",
stateKey: "selectedViewName",
validate: name => $views.list?.some(view => view.name === name),
update: views.select,
fallbackUrl: "../",
store: views,
routify,
decode: decodeURIComponent,
})
onDestroy(stopSyncing)
</script>
<slot />

View File

@ -0,0 +1,16 @@
<script>
import { onMount } from "svelte"
import { views } from "stores/backend"
import { redirect } from "@roxi/routify"
onMount(async () => {
const { list, selected } = $views
if (selected) {
$redirect(`./${encodeURIComponent(selected?.name)}`)
} else if (list?.length) {
$redirect(`./${encodeURIComponent(list[0].name)}`)
} else {
$redirect("../")
}
})
</script>

View File

@ -186,7 +186,9 @@
<span>{$organisation?.company || "Budibase"}</span> <span>{$organisation?.company || "Budibase"}</span>
</div> </div>
<div class="onboarding"> <div class="onboarding">
<ConfigChecklist /> {#if $auth.user?.admin?.global}
<ConfigChecklist />
{/if}
</div> </div>
</div> </div>
<div class="menu"> <div class="menu">

View File

@ -1,20 +1,36 @@
import { writable, get } from "svelte/store" import { writable, derived } from "svelte/store"
import { queries, tables, views } from "./" import { queries, tables } from "./"
import { API } from "api" import { API } from "api"
export const INITIAL_DATASOURCE_VALUES = {
list: [],
selected: null,
schemaError: null,
}
export function createDatasourcesStore() { export function createDatasourcesStore() {
const store = writable(INITIAL_DATASOURCE_VALUES) const store = writable({
const { subscribe, update, set } = store list: [],
selectedDatasourceId: null,
schemaError: null,
})
const derivedStore = derived(store, $store => ({
...$store,
selected: $store.list?.find(ds => ds._id === $store.selectedDatasourceId),
}))
async function updateDatasource(response) { const fetch = async () => {
const datasources = await API.getDatasources()
store.update(state => ({
...state,
list: datasources,
}))
}
const select = id => {
store.update(state => ({
...state,
selectedDatasourceId: id,
}))
}
const updateDatasource = async response => {
const { datasource, error } = response const { datasource, error } = response
update(state => { store.update(state => {
const currentIdx = state.list.findIndex(ds => ds._id === datasource._id) const currentIdx = state.list.findIndex(ds => ds._id === datasource._id)
const sources = state.list const sources = state.list
if (currentIdx >= 0) { if (currentIdx >= 0) {
@ -24,82 +40,64 @@ export function createDatasourcesStore() {
} }
return { return {
list: sources, list: sources,
selected: datasource._id, selectedDatasourceId: datasource._id,
schemaError: error, schemaError: error,
} }
}) })
return datasource return datasource
} }
const updateSchema = async (datasource, tablesFilter) => {
const response = await API.buildDatasourceSchema({
datasourceId: datasource?._id,
tablesFilter,
})
return await updateDatasource(response)
}
const save = async (body, fetchSchema = false) => {
let response
if (body._id) {
response = await API.updateDatasource(body)
} else {
response = await API.createDatasource({
datasource: body,
fetchSchema,
})
}
return updateDatasource(response)
}
const deleteDatasource = async datasource => {
await API.deleteDatasource({
datasourceId: datasource?._id,
datasourceRev: datasource?._rev,
})
store.update(state => {
const sources = state.list.filter(
existing => existing._id !== datasource._id
)
return { list: sources, selected: null }
})
await queries.fetch()
await tables.fetch()
}
const removeSchemaError = () => {
store.update(state => {
return { ...state, schemaError: null }
})
}
return { return {
subscribe, subscribe: derivedStore.subscribe,
update, fetch,
init: async () => { init: fetch,
const datasources = await API.getDatasources() select,
set({ updateSchema,
list: datasources, save,
selected: null, delete: deleteDatasource,
}) removeSchemaError,
},
fetch: async () => {
const datasources = await API.getDatasources()
// Clear selected if it no longer exists, otherwise keep it
const selected = get(store).selected
let nextSelected = null
if (selected && datasources.find(source => source._id === selected)) {
nextSelected = selected
}
update(state => ({ ...state, list: datasources, selected: nextSelected }))
},
select: datasourceId => {
update(state => ({ ...state, selected: datasourceId }))
queries.unselect()
tables.unselect()
views.unselect()
},
unselect: () => {
update(state => ({ ...state, selected: null }))
},
updateSchema: async (datasource, tablesFilter) => {
const response = await API.buildDatasourceSchema({
datasourceId: datasource?._id,
tablesFilter,
})
return await updateDatasource(response)
},
save: async (body, fetchSchema = false) => {
let response
if (body._id) {
response = await API.updateDatasource(body)
} else {
response = await API.createDatasource({
datasource: body,
fetchSchema,
})
}
return updateDatasource(response)
},
delete: async datasource => {
await API.deleteDatasource({
datasourceId: datasource?._id,
datasourceRev: datasource?._rev,
})
update(state => {
const sources = state.list.filter(
existing => existing._id !== datasource._id
)
return { list: sources, selected: null }
})
await queries.fetch()
await tables.fetch()
},
removeSchemaError: () => {
update(state => {
return { ...state, schemaError: null }
})
},
} }
} }

View File

@ -1,7 +1,6 @@
export { database } from "./database" export { database } from "./database"
export { tables } from "./tables" export { tables } from "./tables"
export { views } from "./views" export { views } from "./views"
export { rows } from "./rows"
export { permissions } from "./permissions" export { permissions } from "./permissions"
export { roles } from "./roles" export { roles } from "./roles"
export { datasources } from "./datasources" export { datasources } from "./datasources"

View File

@ -1,5 +1,5 @@
import { writable, get } from "svelte/store" import { writable, get, derived } from "svelte/store"
import { datasources, integrations, tables, views } from "./" import { datasources, integrations } from "./"
import { API } from "api" import { API } from "api"
import { duplicateName } from "helpers/duplicate" import { duplicateName } from "helpers/duplicate"
@ -10,125 +10,127 @@ const sortQueries = queryList => {
} }
export function createQueriesStore() { export function createQueriesStore() {
const store = writable({ list: [], selected: null }) const store = writable({
const { subscribe, set, update } = store list: [],
selectedQueryId: null,
})
const derivedStore = derived(store, $store => ({
...$store,
selected: $store.list?.find(q => q._id === $store.selectedQueryId),
}))
const actions = { const fetch = async () => {
init: async () => { const queries = await API.getQueries()
const queries = await API.getQueries() sortQueries(queries)
set({ store.update(state => ({
list: queries, ...state,
selected: null, list: queries,
}) }))
}, }
fetch: async () => {
const queries = await API.getQueries() const save = async (datasourceId, query) => {
const _integrations = get(integrations)
const dataSource = get(datasources).list.filter(
ds => ds._id === datasourceId
)
// Check if readable attribute is found
if (dataSource.length !== 0) {
const integration = _integrations[dataSource[0].source]
const readable = integration.query[query.queryVerb].readable
if (readable) {
query.readable = readable
}
}
query.datasourceId = datasourceId
const savedQuery = await API.saveQuery(query)
store.update(state => {
const idx = state.list.findIndex(query => query._id === savedQuery._id)
const queries = state.list
if (idx >= 0) {
queries.splice(idx, 1, savedQuery)
} else {
queries.push(savedQuery)
}
sortQueries(queries) sortQueries(queries)
update(state => ({ return {
...state,
list: queries, list: queries,
})) selectedQueryId: savedQuery._id,
},
save: async (datasourceId, query) => {
const _integrations = get(integrations)
const dataSource = get(datasources).list.filter(
ds => ds._id === datasourceId
)
// Check if readable attribute is found
if (dataSource.length !== 0) {
const integration = _integrations[dataSource[0].source]
const readable = integration.query[query.queryVerb].readable
if (readable) {
query.readable = readable
}
} }
query.datasourceId = datasourceId })
const savedQuery = await API.saveQuery(query) return savedQuery
update(state => { }
const idx = state.list.findIndex(query => query._id === savedQuery._id)
const queries = state.list
if (idx >= 0) {
queries.splice(idx, 1, savedQuery)
} else {
queries.push(savedQuery)
}
sortQueries(queries)
return {
list: queries,
selected: savedQuery._id,
}
})
return savedQuery
},
import: async ({ data, datasourceId }) => {
return await API.importQueries({
datasourceId,
data,
})
},
select: query => {
update(state => ({ ...state, selected: query._id }))
views.unselect()
tables.unselect()
datasources.unselect()
},
unselect: () => {
update(state => ({ ...state, selected: null }))
},
preview: async query => {
const parameters = query.parameters.reduce(
(acc, next) => ({
...acc,
[next.name]: next.default,
}),
{}
)
const result = await API.previewQuery({
...query,
parameters,
})
// Assume all the fields are strings and create a basic schema from the
// unique fields returned by the server
const schema = {}
for (let [field, type] of Object.entries(result.schemaFields)) {
schema[field] = type || "string"
}
return { ...result, schema, rows: result.rows || [] }
},
delete: async query => {
await API.deleteQuery({
queryId: query?._id,
queryRev: query?._rev,
})
update(state => {
state.list = state.list.filter(existing => existing._id !== query._id)
if (state.selected === query._id) {
state.selected = null
}
return state
})
},
duplicate: async query => {
let list = get(store).list
const newQuery = { ...query }
const datasourceId = query.datasourceId
delete newQuery._id const importQueries = async ({ data, datasourceId }) => {
delete newQuery._rev return await API.importQueries({
newQuery.name = duplicateName( datasourceId,
query.name, data,
list.map(q => q.name) })
) }
return actions.save(datasourceId, newQuery) const select = id => {
}, store.update(state => ({
...state,
selectedQueryId: id,
}))
}
const preview = async query => {
const parameters = query.parameters.reduce(
(acc, next) => ({
...acc,
[next.name]: next.default,
}),
{}
)
const result = await API.previewQuery({
...query,
parameters,
})
// Assume all the fields are strings and create a basic schema from the
// unique fields returned by the server
const schema = {}
for (let [field, type] of Object.entries(result.schemaFields)) {
schema[field] = type || "string"
}
return { ...result, schema, rows: result.rows || [] }
}
const deleteQuery = async query => {
await API.deleteQuery({
queryId: query?._id,
queryRev: query?._rev,
})
store.update(state => {
state.list = state.list.filter(existing => existing._id !== query._id)
return state
})
}
const duplicate = async query => {
let list = get(store).list
const newQuery = { ...query }
const datasourceId = query.datasourceId
delete newQuery._id
delete newQuery._rev
newQuery.name = duplicateName(
query.name,
list.map(q => q.name)
)
return await save(datasourceId, newQuery)
} }
return { return {
subscribe, subscribe: derivedStore.subscribe,
set, fetch,
update, init: fetch,
...actions, select,
save,
import: importQueries,
delete: deleteQuery,
preview,
duplicate,
} }
} }

View File

@ -1,14 +0,0 @@
import { writable, get } from "svelte/store"
import { tables } from "./"
export function createRowsStore() {
const { subscribe } = writable([])
return {
subscribe,
save: () => tables.select(get(tables).selected),
delete: () => tables.select(get(tables).selected),
}
}
export const rows = createRowsStore()

View File

@ -1,41 +1,35 @@
import { get, writable } from "svelte/store" import { get, writable, derived } from "svelte/store"
import { datasources, queries, views } from "./" import { datasources } from "./"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { API } from "api" import { API } from "api"
import { SWITCHABLE_TYPES } from "constants/backend" import { SWITCHABLE_TYPES } from "constants/backend"
export function createTablesStore() { export function createTablesStore() {
const store = writable({}) const store = writable({
const { subscribe, update, set } = store list: [],
selectedTableId: null,
})
const derivedStore = derived(store, $store => ({
...$store,
selected: $store.list?.find(table => table._id === $store.selectedTableId),
}))
async function fetch() { const fetch = async () => {
const tables = await API.getTables() const tables = await API.getTables()
update(state => ({ store.update(state => ({
...state, ...state,
list: tables, list: tables,
})) }))
return tables
} }
async function select(table) { const select = tableId => {
if (!table) { store.update(state => ({
update(state => ({ ...state,
...state, selectedTableId: tableId,
selected: {}, }))
}))
} else {
update(state => ({
...state,
selected: table,
draft: cloneDeep(table),
}))
views.unselect()
queries.unselect()
datasources.unselect()
}
} }
async function save(table) { const save = async table => {
const updatedTable = cloneDeep(table) const updatedTable = cloneDeep(table)
const oldTable = get(store).list.filter(t => t._id === table._id)[0] const oldTable = get(store).list.filter(t => t._id === table._id)[0]
@ -72,96 +66,72 @@ export function createTablesStore() {
if (table.type === "external") { if (table.type === "external") {
await datasources.fetch() await datasources.fetch()
} }
await select(savedTable) await select(savedTable._id)
return savedTable return savedTable
} }
const deleteTable = async table => {
await API.deleteTable({
tableId: table?._id,
tableRev: table?._rev,
})
await fetch()
}
const saveField = async ({
originalName,
field,
primaryDisplay = false,
indexes,
}) => {
let draft = cloneDeep(get(derivedStore).selected)
// delete the original if renaming
// need to handle if the column had no name, empty string
if (originalName != null && originalName !== field.name) {
delete draft.schema[originalName]
draft._rename = {
old: originalName,
updated: field.name,
}
}
// Optionally set display column
if (primaryDisplay) {
draft.primaryDisplay = field.name
} else if (draft.primaryDisplay === originalName) {
const fields = Object.keys(draft.schema)
// pick another display column randomly if unselecting
draft.primaryDisplay = fields.filter(
name => name !== originalName || name !== field
)[0]
}
if (indexes) {
draft.indexes = indexes
}
draft.schema = {
...draft.schema,
[field.name]: cloneDeep(field),
}
await save(draft)
}
const deleteField = async field => {
let draft = cloneDeep(get(derivedStore).selected)
delete draft.schema[field.name]
await save(draft)
}
return { return {
subscribe, subscribe: derivedStore.subscribe,
update,
fetch, fetch,
init: fetch,
select, select,
unselect: () => {
update(state => ({
...state,
selected: null,
}))
},
save, save,
init: async () => { delete: deleteTable,
const tables = await API.getTables() saveField,
set({ deleteField,
list: tables,
selected: {},
draft: {},
})
},
delete: async table => {
await API.deleteTable({
tableId: table?._id,
tableRev: table?._rev,
})
update(state => ({
...state,
list: state.list.filter(existing => existing._id !== table._id),
selected: {},
}))
},
saveField: async ({
originalName,
field,
primaryDisplay = false,
indexes,
}) => {
let promise
update(state => {
// delete the original if renaming
// need to handle if the column had no name, empty string
if (originalName != null && originalName !== field.name) {
delete state.draft.schema[originalName]
state.draft._rename = {
old: originalName,
updated: field.name,
}
}
// Optionally set display column
if (primaryDisplay) {
state.draft.primaryDisplay = field.name
} else if (state.draft.primaryDisplay === originalName) {
const fields = Object.keys(state.draft.schema)
// pick another display column randomly if unselecting
state.draft.primaryDisplay = fields.filter(
name => name !== originalName || name !== field
)[0]
}
if (indexes) {
state.draft.indexes = indexes
}
state.draft.schema = {
...state.draft.schema,
[field.name]: cloneDeep(field),
}
promise = save(state.draft)
return state
})
if (promise) {
await promise
}
},
deleteField: async field => {
let promise
update(state => {
delete state.draft.schema[field.name]
promise = save(state.draft)
return state
})
if (promise) {
await promise
}
},
} }
} }

View File

@ -1,52 +1,54 @@
import { writable, get } from "svelte/store" import { writable, get, derived } from "svelte/store"
import { tables, datasources, queries } from "./" import { tables } from "./"
import { API } from "api" import { API } from "api"
export function createViewsStore() { export function createViewsStore() {
const { subscribe, update } = writable({ const store = writable({
list: [], selectedViewName: null,
selected: null, })
const derivedStore = derived([store, tables], ([$store, $tables]) => {
let list = []
$tables.list?.forEach(table => {
list = list.concat(Object.values(table?.views || {}))
})
return {
...$store,
list,
selected: list.find(view => view.name === $store.selectedViewName),
}
}) })
const select = name => {
store.update(state => ({
...state,
selectedViewName: name,
}))
}
const deleteView = async view => {
await API.deleteView(view)
await tables.fetch()
}
const save = async view => {
const savedView = await API.saveView(view)
const viewMeta = {
name: view.name,
...savedView,
}
const viewTable = get(tables).list.find(table => table._id === view.tableId)
if (view.originalName) delete viewTable.views[view.originalName]
viewTable.views[view.name] = viewMeta
await tables.save(viewTable)
}
return { return {
subscribe, subscribe: derivedStore.subscribe,
update, select,
select: view => { delete: deleteView,
update(state => ({ save,
...state,
selected: view,
}))
tables.unselect()
queries.unselect()
datasources.unselect()
},
unselect: () => {
update(state => ({
...state,
selected: null,
}))
},
delete: async view => {
await API.deleteView(view)
await tables.fetch()
},
save: async view => {
const savedView = await API.saveView(view)
const viewMeta = {
name: view.name,
...savedView,
}
const viewTable = get(tables).list.find(
table => table._id === view.tableId
)
if (view.originalName) delete viewTable.views[view.originalName]
viewTable.views[view.name] = viewMeta
await tables.save(viewTable)
update(state => ({ ...state, selected: viewMeta }))
},
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "2.2.4-alpha.4", "version": "2.2.10-alpha.11",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {
@ -26,9 +26,9 @@
"outputPath": "build" "outputPath": "build"
}, },
"dependencies": { "dependencies": {
"@budibase/backend-core": "2.2.4-alpha.4", "@budibase/backend-core": "2.2.10-alpha.11",
"@budibase/string-templates": "2.2.4-alpha.4", "@budibase/string-templates": "2.2.10-alpha.11",
"@budibase/types": "2.2.4-alpha.4", "@budibase/types": "2.2.10-alpha.11",
"axios": "0.21.2", "axios": "0.21.2",
"chalk": "4.1.0", "chalk": "4.1.0",
"cli-progress": "3.11.2", "cli-progress": "3.11.2",

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "2.2.4-alpha.4", "version": "2.2.10-alpha.11",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "2.2.4-alpha.4", "@budibase/bbui": "2.2.10-alpha.11",
"@budibase/frontend-core": "2.2.4-alpha.4", "@budibase/frontend-core": "2.2.10-alpha.11",
"@budibase/string-templates": "2.2.4-alpha.4", "@budibase/string-templates": "2.2.10-alpha.11",
"@spectrum-css/button": "^3.0.3", "@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3", "@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3", "@spectrum-css/divider": "^1.0.3",

View File

@ -1,12 +1,12 @@
{ {
"name": "@budibase/frontend-core", "name": "@budibase/frontend-core",
"version": "2.2.4-alpha.4", "version": "2.2.10-alpha.11",
"description": "Budibase frontend core libraries used in builder and client", "description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"dependencies": { "dependencies": {
"@budibase/bbui": "2.2.4-alpha.4", "@budibase/bbui": "2.2.10-alpha.11",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"svelte": "^3.46.2" "svelte": "^3.46.2"
} }

View File

@ -22,11 +22,11 @@ export const buildAppEndpoints = API => ({
}, },
/** /**
* Deploys the current app. * Publishes the current app.
*/ */
deployAppChanges: async () => { publishAppChanges: async appId => {
return await API.post({ return await API.post({
url: "/api/deploy", url: `/api/applications/${appId}/publish`,
}) })
}, },
@ -98,8 +98,8 @@ export const buildAppEndpoints = API => ({
* @param appId the production ID of the app to unpublish * @param appId the production ID of the app to unpublish
*/ */
unpublishApp: async appId => { unpublishApp: async appId => {
return await API.delete({ return await API.post({
url: `/api/applications/${appId}?unpublish=1`, url: `/api/applications/${appId}/unpublish`,
}) })
}, },

View File

@ -16,8 +16,8 @@ export const buildViewEndpoints = API => ({
params.set("group", groupBy) params.set("group", groupBy)
} }
const QUERY_VIEW_URL = field const QUERY_VIEW_URL = field
? `/api/views/${name}?${params}` ? `/api/views/${encodeURIComponent(name)}?${params}`
: `/api/views/${name}` : `/api/views/${encodeURIComponent(name)}`
return await API.get({ url: QUERY_VIEW_URL }) return await API.get({ url: QUERY_VIEW_URL })
}, },
@ -53,7 +53,7 @@ export const buildViewEndpoints = API => ({
*/ */
deleteView: async viewName => { deleteView: async viewName => {
return await API.delete({ return await API.delete({
url: `/api/views/${viewName}`, url: `/api/views/${encodeURIComponent(viewName)}`,
}) })
}, },
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/sdk", "name": "@budibase/sdk",
"version": "2.2.4-alpha.4", "version": "2.2.10-alpha.11",
"description": "Budibase Public API SDK", "description": "Budibase Public API SDK",
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "2.2.4-alpha.4", "version": "2.2.10-alpha.11",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -43,11 +43,11 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "10.0.3", "@apidevtools/swagger-parser": "10.0.3",
"@budibase/backend-core": "2.2.4-alpha.4", "@budibase/backend-core": "2.2.10-alpha.11",
"@budibase/client": "2.2.4-alpha.4", "@budibase/client": "2.2.10-alpha.11",
"@budibase/pro": "2.2.4-alpha.4", "@budibase/pro": "2.2.10-alpha.11",
"@budibase/string-templates": "2.2.4-alpha.4", "@budibase/string-templates": "2.2.10-alpha.11",
"@budibase/types": "2.2.4-alpha.4", "@budibase/types": "2.2.10-alpha.11",
"@bull-board/api": "3.7.0", "@bull-board/api": "3.7.0",
"@bull-board/koa": "3.9.4", "@bull-board/koa": "3.9.4",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",

View File

@ -2,12 +2,6 @@
const compose = require("docker-compose") const compose = require("docker-compose")
const path = require("path") const path = require("path")
const fs = require("fs") const fs = require("fs")
const isWsl = require("is-wsl")
const { processStringSync } = require("@budibase/string-templates")
function isLinux() {
return !isWsl && process.platform !== "darwin" && process.platform !== "win32"
}
// This script wraps docker-compose allowing you to manage your dev infrastructure with simple commands. // This script wraps docker-compose allowing you to manage your dev infrastructure with simple commands.
const CONFIG = { const CONFIG = {
@ -23,16 +17,6 @@ const Commands = {
} }
async function init() { async function init() {
// generate nginx file, always do this incase it has changed
const hostingPath = path.join(process.cwd(), "..", "..", "hosting")
const nginxHbsPath = path.join(hostingPath, "nginx.dev.conf.hbs")
const nginxOutputPath = path.join(hostingPath, ".generated-nginx.dev.conf")
const contents = fs.readFileSync(nginxHbsPath, "utf8")
const config = {
address: isLinux() ? "172.17.0.1" : "host.docker.internal",
}
fs.writeFileSync(nginxOutputPath, processStringSync(contents, config))
const envFilePath = path.join(process.cwd(), ".env") const envFilePath = path.join(process.cwd(), ".env")
if (!fs.existsSync(envFilePath)) { if (!fs.existsSync(envFilePath)) {
const envFileJson = { const envFileJson = {

View File

@ -567,6 +567,40 @@
"data" "data"
] ]
}, },
"deploymentOutput": {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"_id": {
"description": "The ID of the app.",
"type": "string"
},
"status": {
"description": "Status of the deployment, whether it succeeded or failed",
"type": "string",
"enum": [
"SUCCESS",
"FAILURE"
]
},
"appUrl": {
"description": "The URL of the published app",
"type": "string"
}
},
"required": [
"_id",
"status",
"appUrl"
]
}
},
"required": [
"data"
]
},
"row": { "row": {
"description": "The row to be created/updated, based on the table schema.", "description": "The row to be created/updated, based on the table schema.",
"type": "object", "type": "object",
@ -1933,6 +1967,56 @@
} }
} }
}, },
"/applications/{appId}/unpublish": {
"post": {
"operationId": "unpublish",
"summary": "Unpublish an application",
"tags": [
"applications"
],
"parameters": [
{
"$ref": "#/components/parameters/appIdUrl"
}
],
"responses": {
"204": {
"description": "The app was published successfully."
}
}
}
},
"/applications/{appId}/publish": {
"post": {
"operationId": "publish",
"summary": "Unpublish an application",
"tags": [
"applications"
],
"parameters": [
{
"$ref": "#/components/parameters/appIdUrl"
}
],
"responses": {
"200": {
"description": "Returns the deployment object.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/deploymentOutput"
},
"examples": {
"deployment": {
"$ref": "#/components/examples/deploymentOutput"
}
}
}
}
}
}
}
},
"/applications/search": { "/applications/search": {
"post": { "post": {
"operationId": "search", "operationId": "search",

View File

@ -411,6 +411,30 @@ components:
- version - version
required: required:
- data - data
deploymentOutput:
type: object
properties:
data:
type: object
properties:
_id:
description: The ID of the app.
type: string
status:
description: Status of the deployment, whether it succeeded or failed
type: string
enum:
- SUCCESS
- FAILURE
appUrl:
description: The URL of the published app
type: string
required:
- _id
- status
- appUrl
required:
- data
row: row:
description: The row to be created/updated, based on the table schema. description: The row to be created/updated, based on the table schema.
type: object type: object
@ -1453,6 +1477,35 @@ paths:
examples: examples:
application: application:
$ref: "#/components/examples/application" $ref: "#/components/examples/application"
"/applications/{appId}/unpublish":
post:
operationId: unpublish
summary: Unpublish an application
tags:
- applications
parameters:
- $ref: "#/components/parameters/appIdUrl"
responses:
"204":
description: The app was published successfully.
"/applications/{appId}/publish":
post:
operationId: publish
summary: Unpublish an application
tags:
- applications
parameters:
- $ref: "#/components/parameters/appIdUrl"
responses:
"200":
description: Returns the deployment object.
content:
application/json:
schema:
$ref: "#/components/schemas/deploymentOutput"
examples:
deployment:
$ref: "#/components/examples/deploymentOutput"
/applications/search: /applications/search:
post: post:
operationId: search operationId: search

View File

@ -80,6 +80,22 @@ const applicationOutputSchema = object(
} }
) )
const deploymentOutputSchema = object({
_id: {
description: "The ID of the app.",
type: "string",
},
status: {
description: "Status of the deployment, whether it succeeded or failed",
type: "string",
enum: ["SUCCESS", "FAILURE"],
},
appUrl: {
description: "The URL of the published app",
type: "string",
},
})
module.exports = new Resource() module.exports = new Resource()
.setExamples({ .setExamples({
application: { application: {
@ -104,4 +120,7 @@ module.exports = new Resource()
items: applicationOutputSchema, items: applicationOutputSchema,
}, },
}), }),
deploymentOutput: object({
data: deploymentOutputSchema,
}),
}) })

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