Merge branch 'develop' into feature/posthog-v2
This commit is contained in:
commit
1caf4b1965
|
@ -93,6 +93,8 @@ then `cd ` into your local copy.
|
||||||
|
|
||||||
#### 3. Install and Build
|
#### 3. Install and Build
|
||||||
|
|
||||||
|
| **NOTE**: On Windows, all yarn commands must be executed on a bash shell (e.g. git bash)
|
||||||
|
|
||||||
To develop the Budibase platform you'll need [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) installed.
|
To develop the Budibase platform you'll need [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) installed.
|
||||||
|
|
||||||
##### Quick method
|
##### Quick method
|
||||||
|
|
|
@ -7,6 +7,15 @@ assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
**Hosting**
|
||||||
|
<!-- Delete as appropriate -->
|
||||||
|
- Self
|
||||||
|
- Method: <method> <!-- One of: k8s, docker single image, docker compose, digital ocean: -->
|
||||||
|
- Budibase Version: <version> <!-- e.g. 1.0.105 -->
|
||||||
|
- App Version: <version> <!-- Indicate app version if bug is related to an application -->
|
||||||
|
- Cloud
|
||||||
|
- Tenant ID: <tenantId> <!-- shown in URL as <tenantID>.budibase.app -->
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ staleLabel: stale
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
markComment: >
|
markComment: >
|
||||||
This issue has been automatically marked as stale because it has not had
|
This issue has been automatically marked as stale because it has not had
|
||||||
recent activity. It will be closed if no further activity occurs. Thank you
|
recent activity.
|
||||||
for your contributions.
|
|
||||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
closeComment: false
|
closeComment: false
|
||||||
|
|
|
@ -12,6 +12,11 @@ on:
|
||||||
- master
|
- master
|
||||||
- develop
|
- develop
|
||||||
|
|
||||||
|
env:
|
||||||
|
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||||
|
BASE_BRANCH: ${{ github.event.pull_request.base.ref}}
|
||||||
|
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -27,6 +32,10 @@ jobs:
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
- name: Install Pro
|
||||||
|
run: yarn install:pro $BRANCH $BASE_BRANCH
|
||||||
|
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn bootstrap
|
- run: yarn bootstrap
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
|
|
|
@ -19,6 +19,7 @@ env:
|
||||||
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
|
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
|
||||||
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
||||||
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
|
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
|
||||||
|
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
|
@ -29,6 +30,10 @@ jobs:
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
|
||||||
|
- name: Install Pro
|
||||||
|
run: yarn install:pro develop
|
||||||
|
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn bootstrap
|
- run: yarn bootstrap
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
|
@ -46,9 +51,9 @@ jobs:
|
||||||
env:
|
env:
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default
|
# setup the username and email.
|
||||||
git config user.name "Budibase Staging Release Bot"
|
git config --global user.name "Budibase Staging Release Bot"
|
||||||
git config user.email "<>"
|
git config --global user.email "<>"
|
||||||
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
||||||
yarn release:develop
|
yarn release:develop
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ env:
|
||||||
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
|
||||||
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
|
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
|
||||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||||
|
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
|
@ -30,6 +31,10 @@ jobs:
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
|
|
||||||
|
- name: Install Pro
|
||||||
|
run: yarn install:pro master
|
||||||
|
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn bootstrap
|
- run: yarn bootstrap
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
|
@ -66,3 +71,57 @@ jobs:
|
||||||
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||||
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}
|
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}
|
||||||
|
|
||||||
|
- 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: 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
|
||||||
|
run: |
|
||||||
|
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \
|
||||||
|
-H 'Accept: application/vnd.github.v3.raw' \
|
||||||
|
-o values.preprod.yaml \
|
||||||
|
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-preprod/values.yaml
|
||||||
|
wc -l values.preprod.yaml
|
||||||
|
|
||||||
|
- name: Deploy to Preprod Environment
|
||||||
|
uses: glopezep/helm@v1.7.1
|
||||||
|
with:
|
||||||
|
release: budibase-preprod
|
||||||
|
namespace: budibase
|
||||||
|
chart: charts/budibase
|
||||||
|
token: ${{ github.token }}
|
||||||
|
helm: helm3
|
||||||
|
values: |
|
||||||
|
globals:
|
||||||
|
appVersion: ${{ steps.previoustag.outputs.tag }}
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
nginx: true
|
||||||
|
value-files: >-
|
||||||
|
[
|
||||||
|
"values.preprod.yaml"
|
||||||
|
]
|
||||||
|
env:
|
||||||
|
KUBECONFIG_FILE: '${{ secrets.PREPROD_KUBECONFIG }}'
|
||||||
|
|
||||||
|
- name: Discord Webhook Action
|
||||||
|
uses: tsickert/discord-webhook@v4.0.0
|
||||||
|
with:
|
||||||
|
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
||||||
|
content: "Preprod Deployment Complete: ${{ steps.previoustag.outputs.tag }} deployed to Budibase Pre-prod."
|
||||||
|
embed-title: ${{ steps.previoustag.outputs.tag }}
|
||||||
|
|
|
@ -11,7 +11,7 @@ sources:
|
||||||
- https://github.com/Budibase/budibase
|
- https://github.com/Budibase/budibase
|
||||||
- https://budibase.com
|
- https://budibase.com
|
||||||
type: application
|
type: application
|
||||||
version: 0.2.8
|
version: 0.2.9
|
||||||
appVersion: 1.0.48
|
appVersion: 1.0.48
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: couchdb
|
- name: couchdb
|
||||||
|
|
|
@ -110,12 +110,23 @@ spec:
|
||||||
value: {{ .Values.globals.google.clientId | quote }}
|
value: {{ .Values.globals.google.clientId | quote }}
|
||||||
- name: GOOGLE_CLIENT_SECRET
|
- name: GOOGLE_CLIENT_SECRET
|
||||||
value: {{ .Values.globals.google.secret | quote }}
|
value: {{ .Values.globals.google.secret | quote }}
|
||||||
|
- name: AUTOMATION_MAX_ITERATIONS
|
||||||
|
value: {{ .Values.globals.automationMaxIterations | quote }}
|
||||||
|
|
||||||
image: budibase/apps:{{ .Values.globals.appVersion }}
|
image: budibase/apps:{{ .Values.globals.appVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
name: bbapps
|
name: bbapps
|
||||||
ports:
|
ports:
|
||||||
- containerPort: {{ .Values.services.apps.port }}
|
- containerPort: {{ .Values.services.apps.port }}
|
||||||
resources: {}
|
resources: {}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
serviceAccountName: ""
|
serviceAccountName: ""
|
||||||
status: {}
|
status: {}
|
||||||
|
|
|
@ -39,5 +39,13 @@ spec:
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
name: couchdb-backup
|
name: couchdb-backup
|
||||||
resources: {}
|
resources: {}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
status: {}
|
status: {}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
|
@ -12,5 +12,10 @@ spec:
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: {{ .Values.services.objectStore.storage }}
|
storage: {{ .Values.services.objectStore.storage }}
|
||||||
|
{{- if (eq "-" .Values.services.objectStore.storageClass) }}
|
||||||
|
storageClassName: ""
|
||||||
|
{{- else }}
|
||||||
|
storageClassName: "{{ .Values.services.objectStore.storageClass }}"
|
||||||
|
{{- end }}
|
||||||
status: {}
|
status: {}
|
||||||
{{- end }}
|
{{- end }}
|
|
@ -60,6 +60,14 @@ spec:
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: /data
|
- mountPath: /data
|
||||||
name: minio-data
|
name: minio-data
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
serviceAccountName: ""
|
serviceAccountName: ""
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -32,6 +32,14 @@ spec:
|
||||||
- containerPort: {{ .Values.services.proxy.port }}
|
- containerPort: {{ .Values.services.proxy.port }}
|
||||||
resources: {}
|
resources: {}
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
serviceAccountName: ""
|
serviceAccountName: ""
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -12,5 +12,10 @@ spec:
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: {{ .Values.services.redis.storage }}
|
storage: {{ .Values.services.redis.storage }}
|
||||||
|
{{- if (eq "-" .Values.services.redis.storageClass) }}
|
||||||
|
storageClassName: ""
|
||||||
|
{{- else }}
|
||||||
|
storageClassName: "{{ .Values.services.redis.storageClass }}"
|
||||||
|
{{- end }}
|
||||||
status: {}
|
status: {}
|
||||||
{{- end }}
|
{{- end }}
|
|
@ -39,6 +39,14 @@ spec:
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: /data
|
- mountPath: /data
|
||||||
name: redis-data
|
name: redis-data
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
serviceAccountName: ""
|
serviceAccountName: ""
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -121,6 +121,14 @@ spec:
|
||||||
ports:
|
ports:
|
||||||
- containerPort: {{ .Values.services.worker.port }}
|
- containerPort: {{ .Values.services.worker.port }}
|
||||||
resources: {}
|
resources: {}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
serviceAccountName: ""
|
serviceAccountName: ""
|
||||||
status: {}
|
status: {}
|
||||||
|
|
|
@ -47,6 +47,8 @@ ingress:
|
||||||
className: ""
|
className: ""
|
||||||
annotations:
|
annotations:
|
||||||
kubernetes.io/ingress.class: nginx
|
kubernetes.io/ingress.class: nginx
|
||||||
|
nginx.ingress.kubernetes.io/client-max-body-size: 150M
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: 50m
|
||||||
hosts:
|
hosts:
|
||||||
- host: # change if using custom domain
|
- host: # change if using custom domain
|
||||||
paths:
|
paths:
|
||||||
|
@ -101,6 +103,7 @@ globals:
|
||||||
google:
|
google:
|
||||||
clientId: ""
|
clientId: ""
|
||||||
secret: ""
|
secret: ""
|
||||||
|
automationMaxIterations: "500"
|
||||||
|
|
||||||
createSecrets: true # creates an internal API key, JWT secrets and redis password for you
|
createSecrets: true # creates an internal API key, JWT secrets and redis password for you
|
||||||
|
|
||||||
|
@ -148,6 +151,11 @@ services:
|
||||||
url: "" # only change if pointing to existing redis cluster and enabled: false
|
url: "" # only change if pointing to existing redis cluster and enabled: false
|
||||||
password: "budibase" # recommended to override if using built-in redis
|
password: "budibase" # recommended to override if using built-in redis
|
||||||
storage: 100Mi
|
storage: 100Mi
|
||||||
|
## If defined, storageClassName: <storageClass>
|
||||||
|
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||||
|
## If undefined (the default) or set to null, no storageClassName spec is
|
||||||
|
## set, choosing the default provisioner.
|
||||||
|
storageClass: "-"
|
||||||
|
|
||||||
objectStore:
|
objectStore:
|
||||||
minio: true
|
minio: true
|
||||||
|
@ -159,6 +167,11 @@ services:
|
||||||
region: "" # AWS_REGION if using S3 or existing minio secret
|
region: "" # AWS_REGION if using S3 or existing minio secret
|
||||||
url: "http://minio-service:9000" # only change if pointing to existing minio cluster or S3 and minio: false
|
url: "http://minio-service:9000" # only change if pointing to existing minio cluster or S3 and minio: false
|
||||||
storage: 100Mi
|
storage: 100Mi
|
||||||
|
## If defined, storageClassName: <storageClass>
|
||||||
|
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||||
|
## If undefined (the default) or set to null, no storageClassName spec is
|
||||||
|
## set, choosing the default provisioner.
|
||||||
|
storageClass: "-"
|
||||||
|
|
||||||
# Override values in couchDB subchart
|
# Override values in couchDB subchart
|
||||||
couchdb:
|
couchdb:
|
||||||
|
@ -228,6 +241,8 @@ couchdb:
|
||||||
## Optional tolerations
|
## Optional tolerations
|
||||||
tolerations: []
|
tolerations: []
|
||||||
|
|
||||||
|
affinity: {}
|
||||||
|
|
||||||
service:
|
service:
|
||||||
# annotations:
|
# annotations:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
|
@ -27,6 +27,7 @@ services:
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
volumes:
|
volumes:
|
||||||
- ./.generated-nginx.dev.conf:/etc/nginx/nginx.conf
|
- ./.generated-nginx.dev.conf:/etc/nginx/nginx.conf
|
||||||
|
- ./proxy/error.html:/usr/share/nginx/html/error.html
|
||||||
ports:
|
ports:
|
||||||
- "${MAIN_PORT}:10000"
|
- "${MAIN_PORT}:10000"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
|
@ -28,6 +28,12 @@ http {
|
||||||
ignore_invalid_headers off;
|
ignore_invalid_headers off;
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
|
|
||||||
|
error_page 502 503 504 /error.html;
|
||||||
|
location = /error.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
internal;
|
||||||
|
}
|
||||||
|
|
||||||
location /db/ {
|
location /db/ {
|
||||||
proxy_pass http://couchdb-service:5984;
|
proxy_pass http://couchdb-service:5984;
|
||||||
rewrite ^/db/(.*)$ /$1 break;
|
rewrite ^/db/(.*)$ /$1 break;
|
||||||
|
|
|
@ -56,6 +56,12 @@ http {
|
||||||
set $csp_media "media-src 'self' https://js.intercomcdn.com";
|
set $csp_media "media-src 'self' https://js.intercomcdn.com";
|
||||||
set $csp_worker "worker-src 'none'";
|
set $csp_worker "worker-src 'none'";
|
||||||
|
|
||||||
|
error_page 502 503 504 /error.html;
|
||||||
|
location = /error.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
internal;
|
||||||
|
}
|
||||||
|
|
||||||
# Security Headers
|
# Security Headers
|
||||||
add_header X-Frame-Options SAMEORIGIN always;
|
add_header X-Frame-Options SAMEORIGIN always;
|
||||||
add_header X-Content-Type-Options nosniff always;
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
FROM nginx:latest
|
FROM nginx:latest
|
||||||
COPY .generated-nginx.prod.conf /etc/nginx/nginx.conf
|
COPY .generated-nginx.prod.conf /etc/nginx/nginx.conf
|
||||||
|
COPY error.html /usr/share/nginx/html/error.html
|
|
@ -0,0 +1,175 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Budibase</title>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function checkStatusButton() {
|
||||||
|
if (window.location.href.includes("budibase.app")) {
|
||||||
|
var button = document.getElementById("statusButton")
|
||||||
|
button.removeAttribute("hidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToStatus() {
|
||||||
|
window.location.href = "https://status.budibase.com";
|
||||||
|
}
|
||||||
|
function goHome() {
|
||||||
|
window.location.href = window.location.origin;
|
||||||
|
}
|
||||||
|
function getStatus() {
|
||||||
|
var http = new XMLHttpRequest()
|
||||||
|
var url = window.location.href
|
||||||
|
http.open('GET', url, true)
|
||||||
|
http.send()
|
||||||
|
http.onreadystatechange = (e) => {
|
||||||
|
var status = http.status
|
||||||
|
document.getElementById("status").innerHTML = status
|
||||||
|
|
||||||
|
var message
|
||||||
|
if (status === 502) {
|
||||||
|
message = "Bad gateway. Please try again later."
|
||||||
|
} else if (status === 503) {
|
||||||
|
message = "Service Unavailable. Please try again later."
|
||||||
|
} else if (status === 504) {
|
||||||
|
message = "Gateway timeout. Please try again later."
|
||||||
|
} else {
|
||||||
|
message = "Please try again later."
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("message").innerHTML = message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onload = function() {
|
||||||
|
checkStatusButton()
|
||||||
|
getStatus()
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--spectrum-global-color-gray-600: rgb(144,144,144);
|
||||||
|
--spectrum-global-color-gray-900: rgb(255,255,255);
|
||||||
|
--spectrum-global-color-gray-800: rgb(227,227,227);
|
||||||
|
--spectrum-global-color-static-blue-600: rgb(20,115,230);
|
||||||
|
--spectrum-global-color-static-blue-hover: rgb( 18, 103, 207);
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #e7e7e7;
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
color: #e7e7e7;
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
border: none;
|
||||||
|
font-size: 15px;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 8px 22px;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.info {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
color: var(--spectrum-global-color-gray-600)
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--spectrum-global-color-gray-900)
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
font-weight: 200;
|
||||||
|
color: var(--spectrum-global-color-gray-800)
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.homeButton {
|
||||||
|
background-color: var(--spectrum-global-color-static-blue-600);
|
||||||
|
}
|
||||||
|
.homeButton:hover {
|
||||||
|
background-color: var(--spectrum-global-color-static-blue-hover);
|
||||||
|
}
|
||||||
|
.statusButton {
|
||||||
|
background-color: transparent;
|
||||||
|
margin-left: 20px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.hero {
|
||||||
|
height: 160px;
|
||||||
|
width: 160px;
|
||||||
|
margin-right: 80px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.content {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script src="">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="main">
|
||||||
|
<div class="content">
|
||||||
|
<div class="hero">
|
||||||
|
<img src="https://raw.githubusercontent.com/Budibase/budibase/master/packages/builder/assets/bb-space-man.svg" alt="Budibase Logo">
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<div>
|
||||||
|
<h4 id="status" class="status"></h4>
|
||||||
|
<h1 class="title">
|
||||||
|
Houston we have a problem!
|
||||||
|
</h1>
|
||||||
|
<h3 id="message" class="message">
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="homeButton" onclick=goHome()>Return home</button>
|
||||||
|
<button id="statusButton" class="statusButton" hidden="true" onclick=goToStatus()>Check out status</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.105-alpha.10",
|
"version": "1.0.124-alpha.0",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
13
package.json
13
package.json
|
@ -21,11 +21,12 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
"setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
||||||
"bootstrap": "lerna link && lerna bootstrap",
|
"bootstrap": "lerna link && lerna bootstrap && ./scripts/link-dependencies.sh",
|
||||||
"build": "lerna run build",
|
"build": "lerna run build",
|
||||||
"publishnpm": "yarn build && lerna publish --force-publish",
|
"release": "lerna publish patch --yes --force-publish && yarn release:pro",
|
||||||
"release": "lerna publish patch --yes --force-publish",
|
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop && yarn release:pro:develop",
|
||||||
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop",
|
"release:pro": "bash scripts/pro/release.sh",
|
||||||
|
"release:pro:develop": "bash scripts/pro/release.sh develop",
|
||||||
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
||||||
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
||||||
"nuke:packages": "yarn run restore",
|
"nuke:packages": "yarn run restore",
|
||||||
|
@ -72,6 +73,8 @@
|
||||||
"mode:cloud": "yarn env:selfhost:disable && yarn env:multi:enable && yarn env:account:disable",
|
"mode:cloud": "yarn env:selfhost:disable && yarn env:multi:enable && yarn env:account:disable",
|
||||||
"mode:account": "yarn mode:cloud && yarn env:account:enable",
|
"mode:account": "yarn mode:cloud && yarn env:account:enable",
|
||||||
"security:audit": "node scripts/audit.js",
|
"security:audit": "node scripts/audit.js",
|
||||||
"postinstall": "husky install"
|
"postinstall": "husky install",
|
||||||
|
"install:pro": "bash scripts/pro/install.sh",
|
||||||
|
"dep:clean": "yarn clean && yarn bootstrap"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.0.105-alpha.10",
|
"version": "1.0.124-alpha.0",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
@ -25,6 +25,7 @@
|
||||||
"passport-jwt": "^4.0.0",
|
"passport-jwt": "^4.0.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"posthog-node": "^1.3.0",
|
"posthog-node": "^1.3.0",
|
||||||
|
"pouchdb": "7.3.0",
|
||||||
"pouchdb-find": "^7.2.2",
|
"pouchdb-find": "^7.2.2",
|
||||||
"pouchdb-replication-stream": "^1.2.9",
|
"pouchdb-replication-stream": "^1.2.9",
|
||||||
"sanitize-s3-objectkey": "^0.0.1",
|
"sanitize-s3-objectkey": "^0.0.1",
|
||||||
|
@ -41,7 +42,6 @@
|
||||||
"@shopify/jest-koa-mocks": "^3.1.5",
|
"@shopify/jest-koa-mocks": "^3.1.5",
|
||||||
"ioredis-mock": "^5.5.5",
|
"ioredis-mock": "^5.5.5",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"pouchdb": "^7.2.1",
|
|
||||||
"pouchdb-adapter-memory": "^7.2.2",
|
"pouchdb-adapter-memory": "^7.2.2",
|
||||||
"pouchdb-all-dbs": "^1.0.2",
|
"pouchdb-all-dbs": "^1.0.2",
|
||||||
"timekeeper": "^2.2.0"
|
"timekeeper": "^2.2.0"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const redis = require("../redis/authRedis")
|
const redis = require("../redis/authRedis")
|
||||||
const { getDB } = require("../db")
|
const { doWithDB } = require("../db")
|
||||||
const { DocumentTypes } = require("../db/constants")
|
const { DocumentTypes } = require("../db/constants")
|
||||||
|
|
||||||
const AppState = {
|
const AppState = {
|
||||||
|
@ -11,8 +11,13 @@ const EXPIRY_SECONDS = 3600
|
||||||
* The default populate app metadata function
|
* The default populate app metadata function
|
||||||
*/
|
*/
|
||||||
const populateFromDB = async appId => {
|
const populateFromDB = async appId => {
|
||||||
const db = getDB(appId, { skip_setup: true })
|
return doWithDB(
|
||||||
|
appId,
|
||||||
|
db => {
|
||||||
return db.get(DocumentTypes.APP_METADATA)
|
return db.get(DocumentTypes.APP_METADATA)
|
||||||
|
},
|
||||||
|
{ skip_setup: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isInvalid = metadata => {
|
const isInvalid = metadata => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const redis = require("../redis/authRedis")
|
const redis = require("../redis/authRedis")
|
||||||
const { getTenantId, lookupTenantId, getGlobalDB } = require("../tenancy")
|
const { getTenantId, lookupTenantId, doWithGlobalDB } = require("../tenancy")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
const accounts = require("../cloud/accounts")
|
const accounts = require("../cloud/accounts")
|
||||||
|
|
||||||
|
@ -9,9 +9,8 @@ const EXPIRY_SECONDS = 3600
|
||||||
* The default populate user function
|
* The default populate user function
|
||||||
*/
|
*/
|
||||||
const populateFromDB = async (userId, tenantId) => {
|
const populateFromDB = async (userId, tenantId) => {
|
||||||
const user = await getGlobalDB(tenantId).get(userId)
|
const user = await doWithGlobalDB(tenantId, db => db.get(userId))
|
||||||
user.budibaseAccess = true
|
user.budibaseAccess = true
|
||||||
|
|
||||||
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||||
const account = await accounts.getAccount(user.email)
|
const account = await accounts.getAccount(user.email)
|
||||||
if (account) {
|
if (account) {
|
||||||
|
|
|
@ -29,9 +29,7 @@ class API {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await fetch(`${this.host}${url}`, requestOptions)
|
return await fetch(`${this.host}${url}`, requestOptions)
|
||||||
|
|
||||||
return resp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
post = this.apiCall("POST")
|
post = this.apiCall("POST")
|
||||||
|
|
|
@ -17,6 +17,7 @@ exports.Headers = {
|
||||||
API_VER: "x-budibase-api-version",
|
API_VER: "x-budibase-api-version",
|
||||||
APP_ID: "x-budibase-app-id",
|
APP_ID: "x-budibase-app-id",
|
||||||
TYPE: "x-budibase-type",
|
TYPE: "x-budibase-type",
|
||||||
|
PREVIEW_ROLE: "x-budibase-role",
|
||||||
TENANT_ID: "x-budibase-tenant-id",
|
TENANT_ID: "x-budibase-tenant-id",
|
||||||
TOKEN: "x-budibase-token",
|
TOKEN: "x-budibase-token",
|
||||||
CSRF_TOKEN: "x-csrf-token",
|
CSRF_TOKEN: "x-csrf-token",
|
||||||
|
|
|
@ -4,7 +4,11 @@ const { newid } = require("../hashing")
|
||||||
const REQUEST_ID_KEY = "requestId"
|
const REQUEST_ID_KEY = "requestId"
|
||||||
|
|
||||||
class FunctionContext {
|
class FunctionContext {
|
||||||
static getMiddleware(updateCtxFn = null, contextName = "session") {
|
static getMiddleware(
|
||||||
|
updateCtxFn = null,
|
||||||
|
destroyFn = null,
|
||||||
|
contextName = "session"
|
||||||
|
) {
|
||||||
const namespace = this.createNamespace(contextName)
|
const namespace = this.createNamespace(contextName)
|
||||||
|
|
||||||
return async function (ctx, next) {
|
return async function (ctx, next) {
|
||||||
|
@ -18,7 +22,14 @@ class FunctionContext {
|
||||||
if (updateCtxFn) {
|
if (updateCtxFn) {
|
||||||
updateCtxFn(ctx)
|
updateCtxFn(ctx)
|
||||||
}
|
}
|
||||||
next().then(resolve).catch(reject)
|
next()
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject)
|
||||||
|
.finally(() => {
|
||||||
|
if (destroyFn) {
|
||||||
|
return destroyFn(ctx)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const { getGlobalUserParams, getAllApps } = require("../db/utils")
|
const { getGlobalUserParams, getAllApps } = require("../db/utils")
|
||||||
const { getDB } = require("../db")
|
const { doWithDB } = require("../db")
|
||||||
const { getGlobalDB } = require("../tenancy")
|
const { doWithGlobalDB } = require("../tenancy")
|
||||||
const { StaticDatabases } = require("../db/constants")
|
const { StaticDatabases } = require("../db/constants")
|
||||||
|
|
||||||
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
|
||||||
|
@ -8,11 +8,12 @@ const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
|
||||||
|
|
||||||
const removeTenantFromInfoDB = async tenantId => {
|
const removeTenantFromInfoDB = async tenantId => {
|
||||||
try {
|
try {
|
||||||
const infoDb = getDB(PLATFORM_INFO_DB)
|
await doWithDB(PLATFORM_INFO_DB, async infoDb => {
|
||||||
let tenants = await infoDb.get(TENANT_DOC)
|
let tenants = await infoDb.get(TENANT_DOC)
|
||||||
tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId)
|
tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId)
|
||||||
|
|
||||||
await infoDb.put(tenants)
|
await infoDb.put(tenants)
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error removing tenant ${tenantId} from info db`, err)
|
console.error(`Error removing tenant ${tenantId} from info db`, err)
|
||||||
throw err
|
throw err
|
||||||
|
@ -20,7 +21,7 @@ const removeTenantFromInfoDB = async tenantId => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.removeUserFromInfoDB = async dbUser => {
|
exports.removeUserFromInfoDB = async dbUser => {
|
||||||
const infoDb = getDB(PLATFORM_INFO_DB)
|
await doWithDB(PLATFORM_INFO_DB, async infoDb => {
|
||||||
const keys = [dbUser._id, dbUser.email]
|
const keys = [dbUser._id, dbUser.email]
|
||||||
const userDocs = await infoDb.allDocs({
|
const userDocs = await infoDb.allDocs({
|
||||||
keys,
|
keys,
|
||||||
|
@ -33,17 +34,18 @@ exports.removeUserFromInfoDB = async dbUser => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await infoDb.bulkDocs(toDelete)
|
await infoDb.bulkDocs(toDelete)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeUsersFromInfoDB = async tenantId => {
|
const removeUsersFromInfoDB = async tenantId => {
|
||||||
|
return doWithGlobalDB(tenantId, async db => {
|
||||||
try {
|
try {
|
||||||
const globalDb = getGlobalDB(tenantId)
|
const allUsers = await db.allDocs(
|
||||||
const infoDb = getDB(PLATFORM_INFO_DB)
|
|
||||||
const allUsers = await globalDb.allDocs(
|
|
||||||
getGlobalUserParams(null, {
|
getGlobalUserParams(null, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
await doWithDB(PLATFORM_INFO_DB, async infoDb => {
|
||||||
const allEmails = allUsers.rows.map(row => row.doc.email)
|
const allEmails = allUsers.rows.map(row => row.doc.email)
|
||||||
// get the id docs
|
// get the id docs
|
||||||
let keys = allUsers.rows.map(row => row.id)
|
let keys = allUsers.rows.map(row => row.id)
|
||||||
|
@ -61,26 +63,31 @@ const removeUsersFromInfoDB = async tenantId => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await infoDb.bulkDocs(toDelete)
|
await infoDb.bulkDocs(toDelete)
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error removing tenant ${tenantId} users from info db`, err)
|
console.error(`Error removing tenant ${tenantId} users from info db`, err)
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeGlobalDB = async tenantId => {
|
const removeGlobalDB = async tenantId => {
|
||||||
|
return doWithGlobalDB(tenantId, async db => {
|
||||||
try {
|
try {
|
||||||
const globalDb = getGlobalDB(tenantId)
|
await db.destroy()
|
||||||
await globalDb.destroy()
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error removing tenant ${tenantId} users from info db`, err)
|
console.error(`Error removing tenant ${tenantId} users from info db`, err)
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeTenantApps = async tenantId => {
|
const removeTenantApps = async tenantId => {
|
||||||
try {
|
try {
|
||||||
const apps = await getAllApps({ all: true })
|
const apps = await getAllApps({ all: true })
|
||||||
const destroyPromises = apps.map(app => getDB(app.appId).destroy())
|
const destroyPromises = apps.map(app =>
|
||||||
|
doWithDB(app.appId, db => db.destroy())
|
||||||
|
)
|
||||||
await Promise.allSettled(destroyPromises)
|
await Promise.allSettled(destroyPromises)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error removing tenant ${tenantId} apps`, err)
|
console.error(`Error removing tenant ${tenantId} apps`, err)
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
const { Headers } = require("../../constants")
|
const { Headers } = require("../../constants")
|
||||||
const { SEPARATOR, DocumentTypes } = require("../db/constants")
|
const { SEPARATOR, DocumentTypes } = require("../db/constants")
|
||||||
|
const { DEFAULT_TENANT_ID } = require("../constants")
|
||||||
const cls = require("./FunctionContext")
|
const cls = require("./FunctionContext")
|
||||||
const { getDB } = require("../db")
|
const { dangerousGetDB, closeDB } = require("../db")
|
||||||
const { getProdAppID, getDevelopmentAppID } = require("../db/conversions")
|
const { getProdAppID, getDevelopmentAppID } = require("../db/conversions")
|
||||||
|
const { baseGlobalDBName } = require("../tenancy/utils")
|
||||||
const { isEqual } = require("lodash")
|
const { isEqual } = require("lodash")
|
||||||
|
|
||||||
// some test cases call functions directly, need to
|
// some test cases call functions directly, need to
|
||||||
|
@ -12,6 +14,7 @@ let TEST_APP_ID = null
|
||||||
|
|
||||||
const ContextKeys = {
|
const ContextKeys = {
|
||||||
TENANT_ID: "tenantId",
|
TENANT_ID: "tenantId",
|
||||||
|
GLOBAL_DB: "globalDb",
|
||||||
APP_ID: "appId",
|
APP_ID: "appId",
|
||||||
// whatever the request app DB was
|
// whatever the request app DB was
|
||||||
CURRENT_DB: "currentDb",
|
CURRENT_DB: "currentDb",
|
||||||
|
@ -20,9 +23,37 @@ const ContextKeys = {
|
||||||
// get the dev app DB from the request
|
// get the dev app DB from the request
|
||||||
DEV_DB: "devDb",
|
DEV_DB: "devDb",
|
||||||
DB_OPTS: "dbOpts",
|
DB_OPTS: "dbOpts",
|
||||||
|
// check if something else is using the context, don't close DB
|
||||||
|
IN_USE: "inUse",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.DEFAULT_TENANT_ID = "default"
|
exports.DEFAULT_TENANT_ID = DEFAULT_TENANT_ID
|
||||||
|
|
||||||
|
// this function makes sure the PouchDB objects are closed and
|
||||||
|
// fully deleted when finished - this protects against memory leaks
|
||||||
|
async function closeAppDBs() {
|
||||||
|
const dbKeys = [
|
||||||
|
ContextKeys.CURRENT_DB,
|
||||||
|
ContextKeys.PROD_DB,
|
||||||
|
ContextKeys.DEV_DB,
|
||||||
|
]
|
||||||
|
for (let dbKey of dbKeys) {
|
||||||
|
const db = cls.getFromContext(dbKey)
|
||||||
|
if (!db) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await closeDB(db)
|
||||||
|
// clear the DB from context, incase someone tries to use it again
|
||||||
|
cls.setOnContext(dbKey, null)
|
||||||
|
}
|
||||||
|
// clear the app ID now that the databases are closed
|
||||||
|
if (cls.getFromContext(ContextKeys.APP_ID)) {
|
||||||
|
cls.setOnContext(ContextKeys.APP_ID, null)
|
||||||
|
}
|
||||||
|
if (cls.getFromContext(ContextKeys.DB_OPTS)) {
|
||||||
|
cls.setOnContext(ContextKeys.DB_OPTS, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exports.isDefaultTenant = () => {
|
exports.isDefaultTenant = () => {
|
||||||
return exports.getTenantId() === exports.DEFAULT_TENANT_ID
|
return exports.getTenantId() === exports.DEFAULT_TENANT_ID
|
||||||
|
@ -34,14 +65,45 @@ exports.isMultiTenant = () => {
|
||||||
|
|
||||||
// used for automations, API endpoints should always be in context already
|
// used for automations, API endpoints should always be in context already
|
||||||
exports.doInTenant = (tenantId, task) => {
|
exports.doInTenant = (tenantId, task) => {
|
||||||
return cls.run(() => {
|
// the internal function is so that we can re-use an existing
|
||||||
|
// context - don't want to close DB on a parent context
|
||||||
|
async function internal(opts = { existing: false }) {
|
||||||
// set the tenant id
|
// set the tenant id
|
||||||
|
if (!opts.existing) {
|
||||||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
||||||
|
if (env.USE_COUCH) {
|
||||||
|
exports.setGlobalDB(tenantId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
// invoke the task
|
// invoke the task
|
||||||
return task()
|
return await task()
|
||||||
|
} finally {
|
||||||
|
const using = cls.getFromContext(ContextKeys.IN_USE)
|
||||||
|
if (!using || using <= 1) {
|
||||||
|
if (env.USE_COUCH) {
|
||||||
|
await closeDB(exports.getGlobalDB())
|
||||||
|
}
|
||||||
|
// clear from context now that database is closed/task is finished
|
||||||
|
cls.setOnContext(ContextKeys.TENANT_ID, null)
|
||||||
|
cls.setOnContext(ContextKeys.GLOBAL_DB, null)
|
||||||
|
} else {
|
||||||
|
cls.setOnContext(using - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const using = cls.getFromContext(ContextKeys.IN_USE)
|
||||||
|
if (using && cls.getFromContext(ContextKeys.TENANT_ID) === tenantId) {
|
||||||
|
cls.setOnContext(ContextKeys.IN_USE, using + 1)
|
||||||
|
return internal({ existing: true })
|
||||||
|
} else {
|
||||||
|
return cls.run(async () => {
|
||||||
|
cls.setOnContext(ContextKeys.IN_USE, 1)
|
||||||
|
return internal()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an app ID this will attempt to retrieve the tenant ID from it.
|
* Given an app ID this will attempt to retrieve the tenant ID from it.
|
||||||
|
@ -64,37 +126,59 @@ exports.getTenantIDFromAppID = appId => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const setAppTenantId = appId => {
|
const setAppTenantId = appId => {
|
||||||
const appTenantId = this.getTenantIDFromAppID(appId) || this.DEFAULT_TENANT_ID
|
const appTenantId =
|
||||||
this.updateTenantId(appTenantId)
|
exports.getTenantIDFromAppID(appId) || exports.DEFAULT_TENANT_ID
|
||||||
|
exports.updateTenantId(appTenantId)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.doInAppContext = (appId, task) => {
|
exports.doInAppContext = (appId, task) => {
|
||||||
if (!appId) {
|
if (!appId) {
|
||||||
throw new Error("appId is required")
|
throw new Error("appId is required")
|
||||||
}
|
}
|
||||||
return cls.run(() => {
|
|
||||||
// set the app tenant id
|
|
||||||
setAppTenantId(appId)
|
|
||||||
|
|
||||||
|
// the internal function is so that we can re-use an existing
|
||||||
|
// context - don't want to close DB on a parent context
|
||||||
|
async function internal(opts = { existing: false }) {
|
||||||
|
// set the app tenant id
|
||||||
|
if (!opts.existing) {
|
||||||
|
setAppTenantId(appId)
|
||||||
|
}
|
||||||
// set the app ID
|
// set the app ID
|
||||||
cls.setOnContext(ContextKeys.APP_ID, appId)
|
cls.setOnContext(ContextKeys.APP_ID, appId)
|
||||||
|
try {
|
||||||
// invoke the task
|
// invoke the task
|
||||||
return task()
|
return await task()
|
||||||
|
} finally {
|
||||||
|
const using = cls.getFromContext(ContextKeys.IN_USE)
|
||||||
|
if (!using || using <= 1) {
|
||||||
|
await closeAppDBs()
|
||||||
|
} else {
|
||||||
|
cls.setOnContext(using - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const using = cls.getFromContext(ContextKeys.IN_USE)
|
||||||
|
if (using && cls.getFromContext(ContextKeys.APP_ID) === appId) {
|
||||||
|
cls.setOnContext(ContextKeys.IN_USE, using + 1)
|
||||||
|
return internal({ existing: true })
|
||||||
|
} else {
|
||||||
|
return cls.run(async () => {
|
||||||
|
cls.setOnContext(ContextKeys.IN_USE, 1)
|
||||||
|
return internal()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exports.updateTenantId = tenantId => {
|
exports.updateTenantId = tenantId => {
|
||||||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
||||||
|
exports.setGlobalDB(tenantId)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.updateAppId = appId => {
|
exports.updateAppId = async appId => {
|
||||||
try {
|
try {
|
||||||
|
// have to close first, before removing the databases from context
|
||||||
|
await closeAppDBs()
|
||||||
cls.setOnContext(ContextKeys.APP_ID, appId)
|
cls.setOnContext(ContextKeys.APP_ID, appId)
|
||||||
cls.setOnContext(ContextKeys.PROD_DB, null)
|
|
||||||
cls.setOnContext(ContextKeys.DEV_DB, null)
|
|
||||||
cls.setOnContext(ContextKeys.CURRENT_DB, null)
|
|
||||||
cls.setOnContext(ContextKeys.DB_OPTS, null)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (env.isTest()) {
|
if (env.isTest()) {
|
||||||
TEST_APP_ID = appId
|
TEST_APP_ID = appId
|
||||||
|
@ -111,8 +195,8 @@ exports.setTenantId = (
|
||||||
let tenantId
|
let tenantId
|
||||||
// exit early if not multi-tenant
|
// exit early if not multi-tenant
|
||||||
if (!exports.isMultiTenant()) {
|
if (!exports.isMultiTenant()) {
|
||||||
cls.setOnContext(ContextKeys.TENANT_ID, this.DEFAULT_TENANT_ID)
|
cls.setOnContext(ContextKeys.TENANT_ID, exports.DEFAULT_TENANT_ID)
|
||||||
return
|
return exports.DEFAULT_TENANT_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowQs = opts && opts.allowQs
|
const allowQs = opts && opts.allowQs
|
||||||
|
@ -140,6 +224,22 @@ exports.setTenantId = (
|
||||||
if (tenantId) {
|
if (tenantId) {
|
||||||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
||||||
}
|
}
|
||||||
|
return tenantId
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.setGlobalDB = tenantId => {
|
||||||
|
const dbName = baseGlobalDBName(tenantId)
|
||||||
|
const db = dangerousGetDB(dbName)
|
||||||
|
cls.setOnContext(ContextKeys.GLOBAL_DB, db)
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getGlobalDB = () => {
|
||||||
|
const db = cls.getFromContext(ContextKeys.GLOBAL_DB)
|
||||||
|
if (!db) {
|
||||||
|
throw new Error("Global DB not found")
|
||||||
|
}
|
||||||
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.isTenantIdSet = () => {
|
exports.isTenantIdSet = () => {
|
||||||
|
@ -174,8 +274,10 @@ function getContextDB(key, opts) {
|
||||||
if (db && isEqual(opts, storedOpts)) {
|
if (db && isEqual(opts, storedOpts)) {
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
const appId = exports.getAppId()
|
const appId = exports.getAppId()
|
||||||
let toUseAppId
|
let toUseAppId
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case ContextKeys.CURRENT_DB:
|
case ContextKeys.CURRENT_DB:
|
||||||
toUseAppId = appId
|
toUseAppId = appId
|
||||||
|
@ -187,7 +289,7 @@ function getContextDB(key, opts) {
|
||||||
toUseAppId = getDevelopmentAppID(appId)
|
toUseAppId = getDevelopmentAppID(appId)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
db = getDB(toUseAppId, opts)
|
db = dangerousGetDB(toUseAppId, opts)
|
||||||
try {
|
try {
|
||||||
cls.setOnContext(key, db)
|
cls.setOnContext(key, db)
|
||||||
if (opts) {
|
if (opts) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const { getDB } = require(".")
|
const { dangerousGetDB, closeDB } = require(".")
|
||||||
|
|
||||||
class Replication {
|
class Replication {
|
||||||
/**
|
/**
|
||||||
|
@ -7,8 +7,12 @@ class Replication {
|
||||||
* @param {String} target - the DB you want to replicate to, or rollback from
|
* @param {String} target - the DB you want to replicate to, or rollback from
|
||||||
*/
|
*/
|
||||||
constructor({ source, target }) {
|
constructor({ source, target }) {
|
||||||
this.source = getDB(source)
|
this.source = dangerousGetDB(source)
|
||||||
this.target = getDB(target)
|
this.target = dangerousGetDB(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
return Promise.all([closeDB(this.source), closeDB(this.target)])
|
||||||
}
|
}
|
||||||
|
|
||||||
promisify(operation, opts = {}) {
|
promisify(operation, opts = {}) {
|
||||||
|
@ -51,7 +55,7 @@ class Replication {
|
||||||
async rollback() {
|
async rollback() {
|
||||||
await this.target.destroy()
|
await this.target.destroy()
|
||||||
// Recreate the DB again
|
// Recreate the DB again
|
||||||
this.target = getDB(this.target.name)
|
this.target = dangerousGetDB(this.target.name)
|
||||||
await this.replicate()
|
await this.replicate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const pouch = require("./pouch")
|
const pouch = require("./pouch")
|
||||||
|
const env = require("../environment")
|
||||||
|
|
||||||
let PouchDB
|
let PouchDB
|
||||||
let initialised = false
|
let initialised = false
|
||||||
|
@ -24,7 +25,10 @@ exports.init = opts => {
|
||||||
initialised = true
|
initialised = true
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getDB = (dbName, opts) => {
|
// NOTE: THIS IS A DANGEROUS FUNCTION - USE WITH CAUTION
|
||||||
|
// this function is prone to leaks, should only be used
|
||||||
|
// in situations that using the function doWithDB does not work
|
||||||
|
exports.dangerousGetDB = (dbName, opts) => {
|
||||||
checkInitialised()
|
checkInitialised()
|
||||||
const db = new PouchDB(dbName, opts)
|
const db = new PouchDB(dbName, opts)
|
||||||
const dbPut = db.put
|
const dbPut = db.put
|
||||||
|
@ -32,6 +36,33 @@ exports.getDB = (dbName, opts) => {
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// use this function if you have called dangerousGetDB - close
|
||||||
|
// the databases you've opened once finished
|
||||||
|
exports.closeDB = async db => {
|
||||||
|
if (!db || env.isTest()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return db.close()
|
||||||
|
} catch (err) {
|
||||||
|
// ignore error, already closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have to use a callback for this so that we can close
|
||||||
|
// the DB when we're done, without this manual requests would
|
||||||
|
// need to close the database when done with it to avoid memory leaks
|
||||||
|
exports.doWithDB = async (dbName, cb, opts) => {
|
||||||
|
const db = exports.dangerousGetDB(dbName, opts)
|
||||||
|
// need this to be async so that we can correctly close DB after all
|
||||||
|
// async operations have been completed
|
||||||
|
try {
|
||||||
|
return await cb(db)
|
||||||
|
} finally {
|
||||||
|
await exports.closeDB(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exports.allDbs = () => {
|
exports.allDbs = () => {
|
||||||
checkInitialised()
|
checkInitialised()
|
||||||
return PouchDB.allDbs()
|
return PouchDB.allDbs()
|
||||||
|
|
|
@ -20,16 +20,47 @@ exports.getCouchUrl = () => {
|
||||||
return `${protocol}://${env.COUCH_DB_USERNAME}:${env.COUCH_DB_PASSWORD}@${rest}`
|
return `${protocol}://${env.COUCH_DB_USERNAME}:${env.COUCH_DB_PASSWORD}@${rest}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.splitCouchUrl = url => {
|
||||||
|
const [protocol, rest] = url.split("://")
|
||||||
|
const [auth, host] = rest.split("@")
|
||||||
|
const [username, password] = auth.split(":")
|
||||||
|
return {
|
||||||
|
url: `${protocol}://${host}`,
|
||||||
|
auth: {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a constructor for PouchDB.
|
* Return a constructor for PouchDB.
|
||||||
* This should be rarely used outside of the main application config.
|
* This should be rarely used outside of the main application config.
|
||||||
* Exposed for exceptional cases such as in-memory views.
|
* Exposed for exceptional cases such as in-memory views.
|
||||||
*/
|
*/
|
||||||
exports.getPouch = (opts = {}) => {
|
exports.getPouch = (opts = {}) => {
|
||||||
const COUCH_DB_URL = exports.getCouchUrl() || "http://localhost:4005"
|
let auth = {
|
||||||
|
username: env.COUCH_DB_USERNAME,
|
||||||
|
password: env.COUCH_DB_PASSWORD,
|
||||||
|
}
|
||||||
|
let url = exports.getCouchUrl() || "http://localhost:4005"
|
||||||
|
// need to update security settings
|
||||||
|
if (!auth.username || !auth.password || url.includes("@")) {
|
||||||
|
const split = exports.splitCouchUrl(url)
|
||||||
|
url = split.url
|
||||||
|
auth = split.auth
|
||||||
|
}
|
||||||
|
|
||||||
|
const authCookie = Buffer.from(`${auth.username}:${auth.password}`).toString(
|
||||||
|
"base64"
|
||||||
|
)
|
||||||
let POUCH_DB_DEFAULTS = {
|
let POUCH_DB_DEFAULTS = {
|
||||||
prefix: COUCH_DB_URL,
|
prefix: url,
|
||||||
|
fetch: (url, opts) => {
|
||||||
|
// use a specific authorization cookie - be very explicit about how we authenticate
|
||||||
|
opts.headers.set("Authorization", `Basic ${authCookie}`)
|
||||||
|
return PouchDB.fetch(url, opts)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.inMemory) {
|
if (opts.inMemory) {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
require("../../tests/utilities/TestConfiguration")
|
require("../../tests/utilities/TestConfiguration")
|
||||||
const { getDB, allDbs } = require("../")
|
const { dangerousGetDB, allDbs } = require("../")
|
||||||
|
|
||||||
describe("db", () => {
|
describe("db", () => {
|
||||||
|
|
||||||
describe("getDB", () => {
|
describe("getDB", () => {
|
||||||
it("returns a db", async () => {
|
it("returns a db", async () => {
|
||||||
const db = getDB("test")
|
const db = dangerousGetDB("test")
|
||||||
expect(db).toBeDefined()
|
expect(db).toBeDefined()
|
||||||
expect(db._adapter).toBe("memory")
|
expect(db._adapter).toBe("memory")
|
||||||
expect(db.prefix).toBe("_pouch_")
|
expect(db.prefix).toBe("_pouch_")
|
||||||
|
@ -13,7 +13,7 @@ describe("db", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("uses the custom put function", async () => {
|
it("uses the custom put function", async () => {
|
||||||
const db = getDB("test")
|
const db = dangerousGetDB("test")
|
||||||
let doc = { _id: "test" }
|
let doc = { _id: "test" }
|
||||||
await db.put(doc)
|
await db.put(doc)
|
||||||
doc = await db.get(doc._id)
|
doc = await db.get(doc._id)
|
||||||
|
@ -27,9 +27,9 @@ describe("db", () => {
|
||||||
it("returns all dbs", async () => {
|
it("returns all dbs", async () => {
|
||||||
let all = await allDbs()
|
let all = await allDbs()
|
||||||
expect(all).toStrictEqual([])
|
expect(all).toStrictEqual([])
|
||||||
const db1 = getDB("test1")
|
const db1 = dangerousGetDB("test1")
|
||||||
await db1.put({ _id: "test1" })
|
await db1.put({ _id: "test1" })
|
||||||
const db2 = getDB("test2")
|
const db2 = dangerousGetDB("test2")
|
||||||
await db2.put({ _id: "test2" })
|
await db2.put({ _id: "test2" })
|
||||||
all = await allDbs()
|
all = await allDbs()
|
||||||
expect(all.length).toBe(2)
|
expect(all.length).toBe(2)
|
||||||
|
|
|
@ -11,7 +11,7 @@ const {
|
||||||
} = require("./constants")
|
} = require("./constants")
|
||||||
const { getTenantId, getGlobalDBName } = require("../tenancy")
|
const { getTenantId, getGlobalDBName } = require("../tenancy")
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
const { getDB, allDbs } = require("./index")
|
const { doWithDB, allDbs } = require("./index")
|
||||||
const { getCouchUrl } = require("./pouch")
|
const { getCouchUrl } = require("./pouch")
|
||||||
const { getAppMetadata } = require("../cache/appMetadata")
|
const { getAppMetadata } = require("../cache/appMetadata")
|
||||||
const { checkSlashesInUrl } = require("../helpers")
|
const { checkSlashesInUrl } = require("../helpers")
|
||||||
|
@ -281,8 +281,10 @@ exports.getDevAppIDs = async () => {
|
||||||
|
|
||||||
exports.dbExists = async dbName => {
|
exports.dbExists = async dbName => {
|
||||||
let exists = false
|
let exists = false
|
||||||
|
return doWithDB(
|
||||||
|
dbName,
|
||||||
|
async db => {
|
||||||
try {
|
try {
|
||||||
const db = getDB(dbName, { skip_setup: true })
|
|
||||||
// check if database exists
|
// check if database exists
|
||||||
const info = await db.info()
|
const info = await db.info()
|
||||||
if (info && !info.error) {
|
if (info && !info.error) {
|
||||||
|
@ -292,6 +294,9 @@ exports.dbExists = async dbName => {
|
||||||
exists = false
|
exists = false
|
||||||
}
|
}
|
||||||
return exists
|
return exists
|
||||||
|
},
|
||||||
|
{ skip_setup: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -416,3 +421,4 @@ exports.generateConfigID = generateConfigID
|
||||||
exports.getConfigParams = getConfigParams
|
exports.getConfigParams = getConfigParams
|
||||||
exports.getScopedFullConfig = getScopedFullConfig
|
exports.getScopedFullConfig = getScopedFullConfig
|
||||||
exports.generateDevInfoID = generateDevInfoID
|
exports.generateDevInfoID = generateDevInfoID
|
||||||
|
exports.getPlatformUrl = getPlatformUrl
|
||||||
|
|
|
@ -22,7 +22,8 @@ module.exports = {
|
||||||
MINIO_URL: process.env.MINIO_URL,
|
MINIO_URL: process.env.MINIO_URL,
|
||||||
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
||||||
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
||||||
ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL,
|
ACCOUNT_PORTAL_URL:
|
||||||
|
process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app",
|
||||||
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY,
|
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY,
|
||||||
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
||||||
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
|
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
|
||||||
|
@ -31,6 +32,7 @@ module.exports = {
|
||||||
POSTHOG_TOKEN: process.env.POSTHOG_TOKEN,
|
POSTHOG_TOKEN: process.env.POSTHOG_TOKEN,
|
||||||
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
|
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
|
||||||
TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS,
|
TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS,
|
||||||
|
USE_COUCH: process.env.USE_COUCH || true,
|
||||||
isTest,
|
isTest,
|
||||||
_set(key, value) {
|
_set(key, value) {
|
||||||
process.env[key] = value
|
process.env[key] = value
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
const google = require("../google")
|
const google = require("../google")
|
||||||
const { Cookies, Configs } = require("../../../constants")
|
const { Cookies, Configs } = require("../../../constants")
|
||||||
const { clearCookie, getCookie } = require("../../../utils")
|
const { clearCookie, getCookie } = require("../../../utils")
|
||||||
const { getDB } = require("../../../db")
|
const { getScopedConfig, getPlatformUrl } = require("../../../db/utils")
|
||||||
const { getScopedConfig } = require("../../../db/utils")
|
const { doWithDB } = require("../../../db")
|
||||||
const environment = require("../../../environment")
|
const environment = require("../../../environment")
|
||||||
const { getGlobalDB } = require("../../../tenancy")
|
const { getGlobalDB } = require("../../../tenancy")
|
||||||
|
|
||||||
|
@ -13,18 +13,28 @@ async function fetchGoogleCreds() {
|
||||||
type: Configs.GOOGLE,
|
type: Configs.GOOGLE,
|
||||||
})
|
})
|
||||||
// or fall back to env variables
|
// or fall back to env variables
|
||||||
const config = googleConfig || {
|
return (
|
||||||
|
googleConfig || {
|
||||||
clientID: environment.GOOGLE_CLIENT_ID,
|
clientID: environment.GOOGLE_CLIENT_ID,
|
||||||
clientSecret: environment.GOOGLE_CLIENT_SECRET,
|
clientSecret: environment.GOOGLE_CLIENT_SECRET,
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return config
|
async function platformUrl() {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
const publicConfig = await getScopedConfig(db, {
|
||||||
|
type: Configs.SETTINGS,
|
||||||
|
})
|
||||||
|
return getPlatformUrl(publicConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function preAuth(passport, ctx, next) {
|
async function preAuth(passport, ctx, next) {
|
||||||
// get the relevant config
|
// get the relevant config
|
||||||
const googleConfig = await fetchGoogleCreds()
|
const googleConfig = await fetchGoogleCreds()
|
||||||
let callbackUrl = `${environment.PLATFORM_URL}/api/global/auth/datasource/google/callback`
|
const platUrl = await platformUrl()
|
||||||
|
|
||||||
|
let callbackUrl = `${platUrl}/api/global/auth/datasource/google/callback`
|
||||||
const strategy = await google.strategyFactory(googleConfig, callbackUrl)
|
const strategy = await google.strategyFactory(googleConfig, callbackUrl)
|
||||||
|
|
||||||
if (!ctx.query.appId || !ctx.query.datasourceId) {
|
if (!ctx.query.appId || !ctx.query.datasourceId) {
|
||||||
|
@ -41,14 +51,15 @@ async function preAuth(passport, ctx, next) {
|
||||||
async function postAuth(passport, ctx, next) {
|
async function postAuth(passport, ctx, next) {
|
||||||
// get the relevant config
|
// get the relevant config
|
||||||
const config = await fetchGoogleCreds()
|
const config = await fetchGoogleCreds()
|
||||||
|
const platUrl = await platformUrl()
|
||||||
|
|
||||||
let callbackUrl = `${environment.PLATFORM_URL}/api/global/auth/datasource/google/callback`
|
let callbackUrl = `${platUrl}/api/global/auth/datasource/google/callback`
|
||||||
const strategy = await google.strategyFactory(
|
const strategy = await google.strategyFactory(
|
||||||
config,
|
config,
|
||||||
callbackUrl,
|
callbackUrl,
|
||||||
(accessToken, refreshToken, profile, done) => {
|
(accessToken, refreshToken, profile, done) => {
|
||||||
clearCookie(ctx, Cookies.DatasourceAuth)
|
clearCookie(ctx, Cookies.DatasourceAuth)
|
||||||
done(null, { accessToken, refreshToken })
|
done(null, { refreshToken })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -59,7 +70,7 @@ async function postAuth(passport, ctx, next) {
|
||||||
{ successRedirect: "/", failureRedirect: "/error" },
|
{ successRedirect: "/", failureRedirect: "/error" },
|
||||||
async (err, tokens) => {
|
async (err, tokens) => {
|
||||||
// update the DB for the datasource with all the user info
|
// update the DB for the datasource with all the user info
|
||||||
const db = getDB(authStateCookie.appId)
|
await doWithDB(authStateCookie.appId, async db => {
|
||||||
const datasource = await db.get(authStateCookie.datasourceId)
|
const datasource = await db.get(authStateCookie.datasourceId)
|
||||||
if (!datasource.config) {
|
if (!datasource.config) {
|
||||||
datasource.config = {}
|
datasource.config = {}
|
||||||
|
@ -69,6 +80,7 @@ async function postAuth(passport, ctx, next) {
|
||||||
ctx.redirect(
|
ctx.redirect(
|
||||||
`/builder/app/${authStateCookie.appId}/data/datasource/${authStateCookie.datasourceId}`
|
`/builder/app/${authStateCookie.appId}/data/datasource/${authStateCookie.datasourceId}`
|
||||||
)
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)(ctx, next)
|
)(ctx, next)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
||||||
|
|
||||||
const { authenticateThirdParty } = require("./third-party-common")
|
const { authenticateThirdParty } = require("./third-party-common")
|
||||||
|
|
||||||
const buildVerifyFn = async saveUserFn => {
|
const buildVerifyFn = saveUserFn => {
|
||||||
return (accessToken, refreshToken, profile, done) => {
|
return (accessToken, refreshToken, profile, done) => {
|
||||||
const thirdPartyUser = {
|
const thirdPartyUser = {
|
||||||
provider: profile.provider, // should always be 'google'
|
provider: profile.provider, // should always be 'google'
|
||||||
|
|
|
@ -1,15 +1,11 @@
|
||||||
require("../../../tests/utilities/TestConfiguration")
|
require("../../../tests/utilities/TestConfiguration")
|
||||||
const database = require("../../../db")
|
|
||||||
const { authenticateThirdParty } = require("../third-party-common")
|
const { authenticateThirdParty } = require("../third-party-common")
|
||||||
const { data } = require("./utilities/mock-data")
|
const { data } = require("./utilities/mock-data")
|
||||||
|
const { DEFAULT_TENANT_ID } = require("../../../constants")
|
||||||
|
|
||||||
const {
|
const { generateGlobalUserID } = require("../../../db/utils")
|
||||||
StaticDatabases,
|
|
||||||
generateGlobalUserID
|
|
||||||
} = require("../../../db/utils")
|
|
||||||
const { newid } = require("../../../hashing")
|
const { newid } = require("../../../hashing")
|
||||||
|
const { doWithGlobalDB, doInTenant } = require("../../../tenancy")
|
||||||
let db
|
|
||||||
|
|
||||||
const done = jest.fn()
|
const done = jest.fn()
|
||||||
|
|
||||||
|
@ -18,7 +14,15 @@ const getErrorMessage = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveUser = async (user) => {
|
const saveUser = async (user) => {
|
||||||
|
return doWithGlobalDB(DEFAULT_TENANT_ID, async db => {
|
||||||
return await db.put(user)
|
return await db.put(user)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function authenticate(user, requireLocal, saveFn) {
|
||||||
|
return doInTenant(DEFAULT_TENANT_ID, () => {
|
||||||
|
return authenticateThirdParty(user, requireLocal, done, saveFn)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("third party common", () => {
|
describe("third party common", () => {
|
||||||
|
@ -26,35 +30,36 @@ describe("third party common", () => {
|
||||||
let thirdPartyUser
|
let thirdPartyUser
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
db = database.getDB(StaticDatabases.GLOBAL.name)
|
|
||||||
thirdPartyUser = data.buildThirdPartyUser()
|
thirdPartyUser = data.buildThirdPartyUser()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
return doWithGlobalDB(DEFAULT_TENANT_ID, async db => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
await db.destroy()
|
await db.destroy()
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("validation", () => {
|
describe("validation", () => {
|
||||||
const testValidation = async (message) => {
|
const testValidation = async (message) => {
|
||||||
await authenticateThirdParty(thirdPartyUser, false, done, saveUser)
|
await authenticate(thirdPartyUser, false, saveUser)
|
||||||
expect(done.mock.calls.length).toBe(1)
|
expect(done.mock.calls.length).toBe(1)
|
||||||
expect(getErrorMessage()).toContain(message)
|
expect(getErrorMessage()).toContain(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
it("provider fails", async () => {
|
it("provider fails", async () => {
|
||||||
delete thirdPartyUser.provider
|
delete thirdPartyUser.provider
|
||||||
testValidation("third party user provider required")
|
await testValidation("third party user provider required")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("user id fails", async () => {
|
it("user id fails", async () => {
|
||||||
delete thirdPartyUser.userId
|
delete thirdPartyUser.userId
|
||||||
testValidation("third party user id required")
|
await testValidation("third party user id required")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("email fails", async () => {
|
it("email fails", async () => {
|
||||||
delete thirdPartyUser.email
|
delete thirdPartyUser.email
|
||||||
testValidation("third party user email required")
|
await testValidation("third party user email required")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -78,7 +83,7 @@ describe("third party common", () => {
|
||||||
describe("when the user doesn't exist", () => {
|
describe("when the user doesn't exist", () => {
|
||||||
describe("when a local account is required", () => {
|
describe("when a local account is required", () => {
|
||||||
it("returns an error message", async () => {
|
it("returns an error message", async () => {
|
||||||
await authenticateThirdParty(thirdPartyUser, true, done, saveUser)
|
await authenticate(thirdPartyUser, true, saveUser)
|
||||||
expect(done.mock.calls.length).toBe(1)
|
expect(done.mock.calls.length).toBe(1)
|
||||||
expect(getErrorMessage()).toContain("Email does not yet exist. You must set up your local budibase account first.")
|
expect(getErrorMessage()).toContain("Email does not yet exist. You must set up your local budibase account first.")
|
||||||
})
|
})
|
||||||
|
@ -86,7 +91,7 @@ describe("third party common", () => {
|
||||||
|
|
||||||
describe("when a local account isn't required", () => {
|
describe("when a local account isn't required", () => {
|
||||||
it("creates and authenticates the user", async () => {
|
it("creates and authenticates the user", async () => {
|
||||||
await authenticateThirdParty(thirdPartyUser, false, done, saveUser)
|
await authenticate(thirdPartyUser, false, saveUser)
|
||||||
const user = expectUserIsAuthenticated()
|
const user = expectUserIsAuthenticated()
|
||||||
expectUserIsSynced(user, thirdPartyUser)
|
expectUserIsSynced(user, thirdPartyUser)
|
||||||
expect(user.roles).toStrictEqual({})
|
expect(user.roles).toStrictEqual({})
|
||||||
|
@ -100,12 +105,15 @@ describe("third party common", () => {
|
||||||
let email
|
let email
|
||||||
|
|
||||||
const createUser = async () => {
|
const createUser = async () => {
|
||||||
|
return doWithGlobalDB(DEFAULT_TENANT_ID, async db => {
|
||||||
dbUser = {
|
dbUser = {
|
||||||
_id: id,
|
_id: id,
|
||||||
email: email,
|
email: email,
|
||||||
}
|
}
|
||||||
const response = await db.put(dbUser)
|
const response = await db.put(dbUser)
|
||||||
dbUser._rev = response.rev
|
dbUser._rev = response.rev
|
||||||
|
return dbUser
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectUserIsUpdated = (user) => {
|
const expectUserIsUpdated = (user) => {
|
||||||
|
@ -123,7 +131,7 @@ describe("third party common", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("syncs and authenticates the user", async () => {
|
it("syncs and authenticates the user", async () => {
|
||||||
await authenticateThirdParty(thirdPartyUser, true, done, saveUser)
|
await authenticate(thirdPartyUser, true, saveUser)
|
||||||
|
|
||||||
const user = expectUserIsAuthenticated()
|
const user = expectUserIsAuthenticated()
|
||||||
expectUserIsSynced(user, thirdPartyUser)
|
expectUserIsSynced(user, thirdPartyUser)
|
||||||
|
@ -139,7 +147,7 @@ describe("third party common", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("syncs and authenticates the user", async () => {
|
it("syncs and authenticates the user", async () => {
|
||||||
await authenticateThirdParty(thirdPartyUser, true, done, saveUser)
|
await authenticate(thirdPartyUser, true, saveUser)
|
||||||
|
|
||||||
const user = expectUserIsAuthenticated()
|
const user = expectUserIsAuthenticated()
|
||||||
expectUserIsSynced(user, thirdPartyUser)
|
expectUserIsSynced(user, thirdPartyUser)
|
||||||
|
@ -157,7 +165,7 @@ describe("third party common", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("syncs and authenticates the user", async () => {
|
it("syncs and authenticates the user", async () => {
|
||||||
await authenticateThirdParty(thirdPartyUser, true, done, saveUser)
|
await authenticate(thirdPartyUser, true, saveUser)
|
||||||
|
|
||||||
const user = expectUserIsAuthenticated()
|
const user = expectUserIsAuthenticated()
|
||||||
expectUserIsSynced(user, thirdPartyUser)
|
expectUserIsSynced(user, thirdPartyUser)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const { setTenantId } = require("../tenancy")
|
const { setTenantId, setGlobalDB, getGlobalDB } = require("../tenancy")
|
||||||
|
const { closeDB } = require("../db")
|
||||||
const ContextFactory = require("../context/FunctionContext")
|
const ContextFactory = require("../context/FunctionContext")
|
||||||
const { buildMatcherRegex, matches } = require("./matchers")
|
const { buildMatcherRegex, matches } = require("./matchers")
|
||||||
|
|
||||||
|
@ -10,10 +11,17 @@ module.exports = (
|
||||||
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns)
|
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns)
|
||||||
const noTenancyOptions = buildMatcherRegex(noTenancyPatterns)
|
const noTenancyOptions = buildMatcherRegex(noTenancyPatterns)
|
||||||
|
|
||||||
return ContextFactory.getMiddleware(ctx => {
|
const updateCtxFn = ctx => {
|
||||||
const allowNoTenant =
|
const allowNoTenant =
|
||||||
opts.noTenancyRequired || !!matches(ctx, noTenancyOptions)
|
opts.noTenancyRequired || !!matches(ctx, noTenancyOptions)
|
||||||
const allowQs = !!matches(ctx, allowQsOptions)
|
const allowQs = !!matches(ctx, allowQsOptions)
|
||||||
setTenantId(ctx, { allowQs, allowNoTenant })
|
const tenantId = setTenantId(ctx, { allowQs, allowNoTenant })
|
||||||
})
|
setGlobalDB(tenantId)
|
||||||
|
}
|
||||||
|
const destroyFn = async () => {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
await closeDB(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ContextFactory.getMiddleware(updateCtxFn, destroyFn)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const { DEFAULT_TENANT_ID } = require("../constants")
|
const { DEFAULT_TENANT_ID } = require("../constants")
|
||||||
const { getDB } = require("../db")
|
const { doWithDB } = require("../db")
|
||||||
const { DocumentTypes } = require("../db/constants")
|
const { DocumentTypes } = require("../db/constants")
|
||||||
const { getAllApps } = require("../db/utils")
|
const { getAllApps } = require("../db/utils")
|
||||||
const environment = require("../environment")
|
const environment = require("../environment")
|
||||||
|
@ -47,7 +47,7 @@ const runMigration = async (migration, options = {}) => {
|
||||||
|
|
||||||
// run the migration against each db
|
// run the migration against each db
|
||||||
for (const dbName of dbNames) {
|
for (const dbName of dbNames) {
|
||||||
const db = getDB(dbName)
|
await doWithDB(dbName, async db => {
|
||||||
try {
|
try {
|
||||||
const doc = await exports.getMigrationsDoc(db)
|
const doc = await exports.getMigrationsDoc(db)
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ const runMigration = async (migration, options = {}) => {
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// the migration has already been performed
|
// the migration has already been performed
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,6 +86,7 @@ const runMigration = async (migration, options = {}) => {
|
||||||
)
|
)
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
require("../../tests/utilities/TestConfiguration")
|
require("../../tests/utilities/TestConfiguration")
|
||||||
const { runMigrations, getMigrationsDoc } = require("../index")
|
const { runMigrations, getMigrationsDoc } = require("../index")
|
||||||
const { getDB } = require("../../db")
|
const { dangerousGetDB } = require("../../db")
|
||||||
const {
|
const {
|
||||||
StaticDatabases,
|
StaticDatabases,
|
||||||
} = require("../../db/utils")
|
} = require("../../db/utils")
|
||||||
|
@ -18,7 +18,7 @@ describe("migrations", () => {
|
||||||
}]
|
}]
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
db = getDB(StaticDatabases.GLOBAL.name)
|
db = dangerousGetDB(StaticDatabases.GLOBAL.name)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
|
|
@ -7,7 +7,7 @@ const {
|
||||||
SEPARATOR,
|
SEPARATOR,
|
||||||
} = require("../db/utils")
|
} = require("../db/utils")
|
||||||
const { getAppDB } = require("../context")
|
const { getAppDB } = require("../context")
|
||||||
const { getDB } = require("../db")
|
const { doWithDB } = require("../db")
|
||||||
|
|
||||||
const BUILTIN_IDS = {
|
const BUILTIN_IDS = {
|
||||||
ADMIN: "ADMIN",
|
ADMIN: "ADMIN",
|
||||||
|
@ -199,7 +199,12 @@ exports.checkForRoleResourceArray = (rolePerms, resourceId) => {
|
||||||
* @return {Promise<object[]>} An array of the role objects that were found.
|
* @return {Promise<object[]>} An array of the role objects that were found.
|
||||||
*/
|
*/
|
||||||
exports.getAllRoles = async appId => {
|
exports.getAllRoles = async appId => {
|
||||||
const db = appId ? getDB(appId) : getAppDB()
|
if (appId) {
|
||||||
|
return doWithDB(appId, internal)
|
||||||
|
} else {
|
||||||
|
return internal(getAppDB())
|
||||||
|
}
|
||||||
|
async function internal(db) {
|
||||||
const body = await db.allDocs(
|
const body = await db.allDocs(
|
||||||
getRoleParams(null, {
|
getRoleParams(null, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
|
@ -237,6 +242,7 @@ exports.getAllRoles = async appId => {
|
||||||
}
|
}
|
||||||
return roles
|
return roles
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This retrieves the required role for a resource
|
* This retrieves the required role for a resource
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const { getDB } = require("../db")
|
const { doWithDB } = require("../db")
|
||||||
const { SEPARATOR, StaticDatabases } = require("../db/constants")
|
const { StaticDatabases } = require("../db/constants")
|
||||||
|
const { baseGlobalDBName } = require("./utils")
|
||||||
const {
|
const {
|
||||||
getTenantId,
|
getTenantId,
|
||||||
DEFAULT_TENANT_ID,
|
DEFAULT_TENANT_ID,
|
||||||
|
@ -23,7 +24,7 @@ exports.addTenantToUrl = url => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.doesTenantExist = async tenantId => {
|
exports.doesTenantExist = async tenantId => {
|
||||||
const db = getDB(PLATFORM_INFO_DB)
|
return doWithDB(PLATFORM_INFO_DB, async db => {
|
||||||
let tenants
|
let tenants
|
||||||
try {
|
try {
|
||||||
tenants = await db.get(TENANT_DOC)
|
tenants = await db.get(TENANT_DOC)
|
||||||
|
@ -36,10 +37,11 @@ exports.doesTenantExist = async tenantId => {
|
||||||
Array.isArray(tenants.tenantIds) &&
|
Array.isArray(tenants.tenantIds) &&
|
||||||
tenants.tenantIds.indexOf(tenantId) !== -1
|
tenants.tenantIds.indexOf(tenantId) !== -1
|
||||||
)
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.tryAddTenant = async (tenantId, userId, email) => {
|
exports.tryAddTenant = async (tenantId, userId, email) => {
|
||||||
const db = getDB(PLATFORM_INFO_DB)
|
return doWithDB(PLATFORM_INFO_DB, async db => {
|
||||||
const getDoc = async id => {
|
const getDoc = async id => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return null
|
return null
|
||||||
|
@ -76,6 +78,7 @@ exports.tryAddTenant = async (tenantId, userId, email) => {
|
||||||
promises.push(db.put(tenants))
|
promises.push(db.put(tenants))
|
||||||
}
|
}
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getGlobalDBName = (tenantId = null) => {
|
exports.getGlobalDBName = (tenantId = null) => {
|
||||||
|
@ -84,23 +87,15 @@ exports.getGlobalDBName = (tenantId = null) => {
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
tenantId = getTenantId()
|
tenantId = getTenantId()
|
||||||
}
|
}
|
||||||
|
return baseGlobalDBName(tenantId)
|
||||||
let dbName
|
|
||||||
if (tenantId === DEFAULT_TENANT_ID) {
|
|
||||||
dbName = StaticDatabases.GLOBAL.name
|
|
||||||
} else {
|
|
||||||
dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}`
|
|
||||||
}
|
|
||||||
return dbName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getGlobalDB = (tenantId = null) => {
|
exports.doWithGlobalDB = (tenantId, cb) => {
|
||||||
const dbName = exports.getGlobalDBName(tenantId)
|
return doWithDB(exports.getGlobalDBName(tenantId), cb)
|
||||||
return getDB(dbName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.lookupTenantId = async userId => {
|
exports.lookupTenantId = async userId => {
|
||||||
const db = getDB(StaticDatabases.PLATFORM_INFO.name)
|
return doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => {
|
||||||
let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null
|
let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null
|
||||||
try {
|
try {
|
||||||
const doc = await db.get(userId)
|
const doc = await db.get(userId)
|
||||||
|
@ -111,16 +106,18 @@ exports.lookupTenantId = async userId => {
|
||||||
// just return the default
|
// just return the default
|
||||||
}
|
}
|
||||||
return tenantId
|
return tenantId
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookup, could be email or userId, either will return a doc
|
// lookup, could be email or userId, either will return a doc
|
||||||
exports.getTenantUser = async identifier => {
|
exports.getTenantUser = async identifier => {
|
||||||
const db = getDB(PLATFORM_INFO_DB)
|
return doWithDB(PLATFORM_INFO_DB, async db => {
|
||||||
try {
|
try {
|
||||||
return await db.get(identifier)
|
return await db.get(identifier)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.isUserInAppTenant = (appId, user = null) => {
|
exports.isUserInAppTenant = (appId, user = null) => {
|
||||||
|
@ -135,7 +132,7 @@ exports.isUserInAppTenant = (appId, user = null) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getTenantIds = async () => {
|
exports.getTenantIds = async () => {
|
||||||
const db = getDB(PLATFORM_INFO_DB)
|
return doWithDB(PLATFORM_INFO_DB, async db => {
|
||||||
let tenants
|
let tenants
|
||||||
try {
|
try {
|
||||||
tenants = await db.get(TENANT_DOC)
|
tenants = await db.get(TENANT_DOC)
|
||||||
|
@ -144,4 +141,5 @@ exports.getTenantIds = async () => {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return (tenants && tenants.tenantIds) || []
|
return (tenants && tenants.tenantIds) || []
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
const { DEFAULT_TENANT_ID } = require("../constants")
|
||||||
|
const { StaticDatabases, SEPARATOR } = require("../db/constants")
|
||||||
|
|
||||||
|
exports.baseGlobalDBName = tenantId => {
|
||||||
|
let dbName
|
||||||
|
if (!tenantId || tenantId === DEFAULT_TENANT_ID) {
|
||||||
|
dbName = StaticDatabases.GLOBAL.name
|
||||||
|
} else {
|
||||||
|
dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}`
|
||||||
|
}
|
||||||
|
return dbName
|
||||||
|
}
|
|
@ -17,15 +17,5 @@ exports.getGlobalUserByEmail = async email => {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response) {
|
|
||||||
if (Array.isArray(response)) {
|
|
||||||
for (let user of response) {
|
|
||||||
delete user.password
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete response.password
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
|
@ -150,10 +150,24 @@ exports.isClient = ctx => {
|
||||||
return ctx.headers[Headers.TYPE] === "client"
|
return ctx.headers[Headers.TYPE] === "client"
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getBuildersCount = async () => {
|
const getBuilders = async () => {
|
||||||
const builders = await queryGlobalView(ViewNames.USER_BY_BUILDERS, {
|
const builders = await queryGlobalView(ViewNames.USER_BY_BUILDERS, {
|
||||||
include_docs: false,
|
include_docs: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!builders) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(builders)) {
|
||||||
|
return builders
|
||||||
|
} else {
|
||||||
|
return [builders]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getBuildersCount = async () => {
|
||||||
|
const builders = await getBuilders()
|
||||||
return builders.length
|
return builders.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -259,9 +259,9 @@
|
||||||
"@babel/helper-plugin-utils" "^7.14.5"
|
"@babel/helper-plugin-utils" "^7.14.5"
|
||||||
|
|
||||||
"@babel/runtime@^7.15.4":
|
"@babel/runtime@^7.15.4":
|
||||||
version "7.17.8"
|
version "7.17.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
|
||||||
integrity sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==
|
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
|
@ -1086,7 +1086,7 @@ buffer-from@1.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
||||||
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
|
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
|
||||||
|
|
||||||
buffer-from@^1.0.0:
|
buffer-from@1.1.2, buffer-from@^1.0.0:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
||||||
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
||||||
|
@ -1905,6 +1905,13 @@ fetch-cookie@0.10.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
tough-cookie "^2.3.3 || ^3.0.1 || ^4.0.0"
|
tough-cookie "^2.3.3 || ^3.0.1 || ^4.0.0"
|
||||||
|
|
||||||
|
fetch-cookie@0.11.0:
|
||||||
|
version "0.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fetch-cookie/-/fetch-cookie-0.11.0.tgz#e046d2abadd0ded5804ce7e2cae06d4331c15407"
|
||||||
|
integrity sha512-BQm7iZLFhMWFy5CZ/162sAGjBfdNWb7a8LEqqnzsHFhxT/X/SVj/z2t2nu3aJvjlbQkrAlTUApplPRjWyH4mhA==
|
||||||
|
dependencies:
|
||||||
|
tough-cookie "^2.3.3 || ^3.0.1 || ^4.0.0"
|
||||||
|
|
||||||
fill-range@^4.0.0:
|
fill-range@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
|
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
|
||||||
|
@ -3710,7 +3717,7 @@ node-fetch@2.6.0:
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
|
||||||
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
|
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
|
||||||
|
|
||||||
node-fetch@^2.6.1:
|
node-fetch@2.6.7, node-fetch@^2.6.1:
|
||||||
version "2.6.7"
|
version "2.6.7"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||||
|
@ -4309,17 +4316,17 @@ pouchdb-utils@7.2.2:
|
||||||
pouchdb-md5 "7.2.2"
|
pouchdb-md5 "7.2.2"
|
||||||
uuid "8.1.0"
|
uuid "8.1.0"
|
||||||
|
|
||||||
pouchdb@^7.2.1:
|
pouchdb@7.3.0:
|
||||||
version "7.2.2"
|
version "7.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/pouchdb/-/pouchdb-7.2.2.tgz#fcae82862db527e4cf7576ed8549d1384961f364"
|
resolved "https://registry.yarnpkg.com/pouchdb/-/pouchdb-7.3.0.tgz#440fbef12dfd8f9002320802528665e883a3b7f8"
|
||||||
integrity sha512-5gf5nw5XH/2H/DJj8b0YkvG9fhA/4Jt6kL0Y8QjtztVjb1y4J19Rg4rG+fUbXu96gsUrlyIvZ3XfM0b4mogGmw==
|
integrity sha512-OwsIQGXsfx3TrU1pLruj6PGSwFH+h5k4hGNxFkZ76Um7/ZI8F5TzUHFrpldVVIhfXYi2vP31q0q7ot1FSLFYOw==
|
||||||
dependencies:
|
dependencies:
|
||||||
abort-controller "3.0.0"
|
abort-controller "3.0.0"
|
||||||
argsarray "0.0.1"
|
argsarray "0.0.1"
|
||||||
buffer-from "1.1.1"
|
buffer-from "1.1.2"
|
||||||
clone-buffer "1.0.0"
|
clone-buffer "1.0.0"
|
||||||
double-ended-queue "2.1.0-0"
|
double-ended-queue "2.1.0-0"
|
||||||
fetch-cookie "0.10.1"
|
fetch-cookie "0.11.0"
|
||||||
immediate "3.3.0"
|
immediate "3.3.0"
|
||||||
inherits "2.0.4"
|
inherits "2.0.4"
|
||||||
level "6.0.1"
|
level "6.0.1"
|
||||||
|
@ -4328,11 +4335,11 @@ pouchdb@^7.2.1:
|
||||||
leveldown "5.6.0"
|
leveldown "5.6.0"
|
||||||
levelup "4.4.0"
|
levelup "4.4.0"
|
||||||
ltgt "2.2.1"
|
ltgt "2.2.1"
|
||||||
node-fetch "2.6.0"
|
node-fetch "2.6.7"
|
||||||
readable-stream "1.1.14"
|
readable-stream "1.1.14"
|
||||||
spark-md5 "3.0.1"
|
spark-md5 "3.0.2"
|
||||||
through2 "3.0.2"
|
through2 "3.0.2"
|
||||||
uuid "8.1.0"
|
uuid "8.3.2"
|
||||||
vuvuzela "1.0.3"
|
vuvuzela "1.0.3"
|
||||||
|
|
||||||
prelude-ls@~1.1.2:
|
prelude-ls@~1.1.2:
|
||||||
|
@ -4891,6 +4898,11 @@ spark-md5@3.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.1.tgz#83a0e255734f2ab4e5c466e5a2cfc9ba2aa2124d"
|
resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.1.tgz#83a0e255734f2ab4e5c466e5a2cfc9ba2aa2124d"
|
||||||
integrity sha512-0tF3AGSD1ppQeuffsLDIOWlKUd3lS92tFxcsrh5Pe3ZphhnoK+oXIBTzOAThZCiuINZLvpiLH/1VS1/ANEJVig==
|
integrity sha512-0tF3AGSD1ppQeuffsLDIOWlKUd3lS92tFxcsrh5Pe3ZphhnoK+oXIBTzOAThZCiuINZLvpiLH/1VS1/ANEJVig==
|
||||||
|
|
||||||
|
spark-md5@3.0.2:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc"
|
||||||
|
integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==
|
||||||
|
|
||||||
spdx-correct@^3.0.0:
|
spdx-correct@^3.0.0:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
|
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
|
||||||
|
@ -5393,6 +5405,11 @@ uuid@8.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d"
|
||||||
integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==
|
integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==
|
||||||
|
|
||||||
|
uuid@8.3.2, uuid@^8.3.0, uuid@^8.3.2:
|
||||||
|
version "8.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||||
|
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||||
|
|
||||||
uuid@^3.0.0:
|
uuid@^3.0.0:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1"
|
||||||
|
@ -5403,11 +5420,6 @@ uuid@^3.3.2:
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||||
|
|
||||||
uuid@^8.3.0, uuid@^8.3.2:
|
|
||||||
version "8.3.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
|
||||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
|
||||||
|
|
||||||
v8-to-istanbul@^7.0.0:
|
v8-to-istanbul@^7.0.0:
|
||||||
version "7.1.2"
|
version "7.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz#30898d1a7fa0c84d225a2c1434fb958f290883c1"
|
resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz#30898d1a7fa0c84d225a2c1434fb958f290883c1"
|
||||||
|
|
|
@ -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": "1.0.105-alpha.10",
|
"version": "1.0.124-alpha.0",
|
||||||
"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": "^1.0.105-alpha.10",
|
"@budibase/string-templates": "^1.0.124-alpha.0",
|
||||||
"@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",
|
||||||
|
|
|
@ -80,8 +80,4 @@
|
||||||
.active svg {
|
.active svg {
|
||||||
color: var(--spectrum-global-color-blue-600);
|
color: var(--spectrum-global-color-blue-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
.spectrum-ActionButton-label {
|
|
||||||
padding-bottom: 2px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let align = "left"
|
export let align = "left"
|
||||||
export let portalTarget
|
export let portalTarget
|
||||||
|
export let dataCy
|
||||||
|
|
||||||
let anchor
|
let anchor
|
||||||
let dropdown
|
let dropdown
|
||||||
|
@ -36,7 +37,7 @@
|
||||||
<div use:getAnchor on:click={openMenu}>
|
<div use:getAnchor on:click={openMenu}>
|
||||||
<slot name="control" />
|
<slot name="control" />
|
||||||
</div>
|
</div>
|
||||||
<Popover bind:this={dropdown} {anchor} {align} {portalTarget}>
|
<Popover bind:this={dropdown} {anchor} {align} {portalTarget} {dataCy}>
|
||||||
<Menu>
|
<Menu>
|
||||||
<slot />
|
<slot />
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
|
|
||||||
export let direction = "n"
|
export let direction = "n"
|
||||||
export let name = "Add"
|
export let name = "Add"
|
||||||
export let hidden = false
|
export let hidden = false
|
||||||
|
@ -10,14 +13,24 @@
|
||||||
export let hoverable = false
|
export let hoverable = false
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let color
|
export let color
|
||||||
|
export let tooltip
|
||||||
|
|
||||||
$: rotation = getRotation(direction)
|
$: rotation = getRotation(direction)
|
||||||
|
|
||||||
|
let showTooltip = false
|
||||||
|
|
||||||
const getRotation = direction => {
|
const getRotation = direction => {
|
||||||
return directions.indexOf(direction) * 45
|
return directions.indexOf(direction) * 45
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="icon"
|
||||||
|
on:mouseover={() => (showTooltip = true)}
|
||||||
|
on:focus={() => (showTooltip = true)}
|
||||||
|
on:mouseleave={() => (showTooltip = false)}
|
||||||
|
on:click={() => (showTooltip = false)}
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
on:click
|
on:click
|
||||||
class:hoverable
|
class:hoverable
|
||||||
|
@ -30,10 +43,22 @@
|
||||||
color ? `color: ${color};` : ""
|
color ? `color: ${color};` : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<use xlink:href="#spectrum-icon-18-{name}" />
|
<use style="pointer-events: none;" xlink:href="#spectrum-icon-18-{name}" />
|
||||||
</svg>
|
</svg>
|
||||||
|
{#if tooltip && showTooltip}
|
||||||
|
<div class="tooltip" in:fade={{ duration: 130, delay: 250 }}>
|
||||||
|
<Tooltip textWrapping direction={"bottom"} text={tooltip} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.icon {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
svg.hoverable {
|
svg.hoverable {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
transition: color var(--spectrum-global-animation-duration-100, 130ms);
|
transition: color var(--spectrum-global-animation-duration-100, 130ms);
|
||||||
|
@ -47,4 +72,15 @@
|
||||||
color: var(--spectrum-global-color-gray-500) !important;
|
color: var(--spectrum-global-color-gray-500) !important;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
left: 50%;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
width: 100vw;
|
||||||
|
max-width: 150px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -36,6 +36,10 @@
|
||||||
padding-left: var(--spacing-l);
|
padding-left: var(--spacing-l);
|
||||||
padding-right: var(--spacing-l);
|
padding-right: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
.paddingX-XL {
|
||||||
|
padding-left: var(--spacing-xl);
|
||||||
|
padding-right: var(--spacing-xl);
|
||||||
|
}
|
||||||
.paddingY-S {
|
.paddingY-S {
|
||||||
padding-top: var(--spacing-s);
|
padding-top: var(--spacing-s);
|
||||||
padding-bottom: var(--spacing-s);
|
padding-bottom: var(--spacing-s);
|
||||||
|
@ -48,6 +52,10 @@
|
||||||
padding-top: var(--spacing-l);
|
padding-top: var(--spacing-l);
|
||||||
padding-bottom: var(--spacing-l);
|
padding-bottom: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
.paddingY-XL {
|
||||||
|
padding-top: var(--spacing-xl);
|
||||||
|
padding-bottom: var(--spacing-xl);
|
||||||
|
}
|
||||||
.gap-XXS {
|
.gap-XXS {
|
||||||
grid-gap: var(--spacing-xs);
|
grid-gap: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
export let secondaryButtonText = undefined
|
export let secondaryButtonText = undefined
|
||||||
export let secondaryAction = undefined
|
export let secondaryAction = undefined
|
||||||
export let secondaryButtonWarning = false
|
export let secondaryButtonWarning = false
|
||||||
|
export let dataCy = null
|
||||||
|
|
||||||
const { hide, cancel } = getContext(Context.Modal)
|
const { hide, cancel } = getContext(Context.Modal)
|
||||||
let loading = false
|
let loading = false
|
||||||
|
@ -63,21 +64,26 @@
|
||||||
role="dialog"
|
role="dialog"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
|
data-cy={dataCy}
|
||||||
>
|
>
|
||||||
<div class="spectrum-Dialog-grid">
|
<div class="spectrum-Dialog-grid">
|
||||||
{#if title}
|
{#if title || $$slots.header}
|
||||||
<h1
|
<h1
|
||||||
class="spectrum-Dialog-heading spectrum-Dialog-heading--noHeader"
|
class="spectrum-Dialog-heading spectrum-Dialog-heading--noHeader"
|
||||||
class:noDivider={!showDivider}
|
class:noDivider={!showDivider}
|
||||||
class:header-spacing={$$slots.header}
|
class:header-spacing={$$slots.header}
|
||||||
>
|
>
|
||||||
|
{#if title}
|
||||||
{title}
|
{title}
|
||||||
|
{:else if $$slots.header}
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
|
{/if}
|
||||||
</h1>
|
</h1>
|
||||||
{#if showDivider}
|
{#if showDivider}
|
||||||
<Divider size="M" />
|
<Divider size="M" />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- TODO: Remove content-grid class once Layout components are in bbui -->
|
<!-- TODO: Remove content-grid class once Layout components are in bbui -->
|
||||||
<section class="spectrum-Dialog-content content-grid">
|
<section class="spectrum-Dialog-content content-grid">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -10,6 +10,17 @@
|
||||||
export let anchor
|
export let anchor
|
||||||
export let align = "right"
|
export let align = "right"
|
||||||
export let portalTarget
|
export let portalTarget
|
||||||
|
export let dataCy
|
||||||
|
|
||||||
|
export let direction = "bottom"
|
||||||
|
export let showTip = false
|
||||||
|
|
||||||
|
let tipSvg =
|
||||||
|
'<svg xmlns="http://www.w3.org/svg/2000" width="23" height="12" class="spectrum-Popover-tip" > <path class="spectrum-Popover-tip-triangle" d="M 0.7071067811865476 0 L 11.414213562373096 10.707106781186548 L 22.121320343559645 0" /> </svg>'
|
||||||
|
|
||||||
|
$: tooltipClasses = showTip
|
||||||
|
? `spectrum-Popover--withTip spectrum-Popover--${direction}`
|
||||||
|
: ""
|
||||||
|
|
||||||
export const show = () => {
|
export const show = () => {
|
||||||
dispatch("open")
|
dispatch("open")
|
||||||
|
@ -37,9 +48,14 @@
|
||||||
use:positionDropdown={{ anchor, align }}
|
use:positionDropdown={{ anchor, align }}
|
||||||
use:clickOutside={hide}
|
use:clickOutside={hide}
|
||||||
on:keydown={handleEscape}
|
on:keydown={handleEscape}
|
||||||
class="spectrum-Popover is-open"
|
class={"spectrum-Popover is-open " + (tooltipClasses || "")}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
|
data-cy={dataCy}
|
||||||
>
|
>
|
||||||
|
{#if showTip}
|
||||||
|
{@html tipSvg}
|
||||||
|
{/if}
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
@ -49,4 +65,13 @@
|
||||||
.spectrum-Popover {
|
.spectrum-Popover {
|
||||||
min-width: var(--spectrum-global-dimension-size-2000) !important;
|
min-width: var(--spectrum-global-dimension-size-2000) !important;
|
||||||
}
|
}
|
||||||
|
.spectrum-Popover.is-open.spectrum-Popover--withTip {
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
margin-left: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
:global(.spectrum-Popover--bottom .spectrum-Popover-tip),
|
||||||
|
:global(.spectrum-Popover--top .spectrum-Popover-tip) {
|
||||||
|
left: 90%;
|
||||||
|
margin-left: calc(var(--spectrum-global-dimension-size-150) * -1);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,42 +1,21 @@
|
||||||
<script>
|
<script>
|
||||||
import Icon from "../Icon/Icon.svelte"
|
import Icon from "../Icon/Icon.svelte"
|
||||||
|
import { copyToClipboard } from "../helpers"
|
||||||
import { notifications } from "../Stores/notifications"
|
import { notifications } from "../Stores/notifications"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
const onClick = e => {
|
const onClick = async e => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
copyToClipboard(value)
|
try {
|
||||||
}
|
await copyToClipboard(value)
|
||||||
|
|
||||||
const copyToClipboard = value => {
|
|
||||||
return new Promise(res => {
|
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
|
||||||
// Try using the clipboard API first
|
|
||||||
navigator.clipboard.writeText(value).then(res)
|
|
||||||
} else {
|
|
||||||
// Fall back to the textarea hack
|
|
||||||
let textArea = document.createElement("textarea")
|
|
||||||
textArea.value = value
|
|
||||||
textArea.style.position = "fixed"
|
|
||||||
textArea.style.left = "-9999px"
|
|
||||||
textArea.style.top = "-9999px"
|
|
||||||
document.body.appendChild(textArea)
|
|
||||||
textArea.focus()
|
|
||||||
textArea.select()
|
|
||||||
document.execCommand("copy")
|
|
||||||
textArea.remove()
|
|
||||||
res()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
notifications.success("Copied to clipboard")
|
notifications.success("Copied to clipboard")
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
|
||||||
notifications.error(
|
notifications.error(
|
||||||
"Failed to copy to clipboard. Check the dev console for the value."
|
"Failed to copy to clipboard. Check the dev console for the value."
|
||||||
)
|
)
|
||||||
console.warn("Failed to copy the value", value)
|
console.warn("Failed to copy the value", value)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
export let disableSorting = false
|
export let disableSorting = false
|
||||||
export let autoSortColumns = true
|
export let autoSortColumns = true
|
||||||
export let compact = false
|
export let compact = false
|
||||||
|
export let customPlaceholder = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -387,13 +388,24 @@
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="placeholder" class:placeholder--no-fields={!fields?.length}>
|
<div
|
||||||
|
class="placeholder"
|
||||||
|
class:placeholder--custom={customPlaceholder}
|
||||||
|
class:placeholder--no-fields={!fields?.length}
|
||||||
|
>
|
||||||
|
{#if customPlaceholder}
|
||||||
|
<slot name="placeholder" />
|
||||||
|
{:else}
|
||||||
<div class="placeholder-content">
|
<div class="placeholder-content">
|
||||||
<svg class="spectrum-Icon spectrum-Icon--sizeXXL" focusable="false">
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--sizeXXL"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
<use xlink:href="#spectrum-icon-18-Table" />
|
<use xlink:href="#spectrum-icon-18-Table" />
|
||||||
</svg>
|
</svg>
|
||||||
<div>No rows found</div>
|
<div>No rows found</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -458,6 +470,13 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
border-top: var(--table-border);
|
||||||
|
}
|
||||||
|
.spectrum-Table-headCell:first-of-type {
|
||||||
|
border-left: var(--table-border);
|
||||||
|
}
|
||||||
|
.spectrum-Table-headCell:last-of-type {
|
||||||
|
border-right: var(--table-border);
|
||||||
}
|
}
|
||||||
.spectrum-Table-headCell--alignCenter {
|
.spectrum-Table-headCell--alignCenter {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -576,16 +595,19 @@
|
||||||
border-top: none;
|
border-top: none;
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
background-color: var(--table-bg);
|
background-color: var(--table-bg);
|
||||||
|
padding: 40px;
|
||||||
}
|
}
|
||||||
.placeholder--no-fields {
|
.placeholder--no-fields {
|
||||||
border-top: var(--table-border);
|
border-top: var(--table-border);
|
||||||
}
|
}
|
||||||
|
.placeholder--custom {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
.wrapper--quiet .placeholder {
|
.wrapper--quiet .placeholder {
|
||||||
border-left: none;
|
border-left: none;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
.placeholder-content {
|
.placeholder-content {
|
||||||
padding: 40px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -108,7 +108,7 @@
|
||||||
padding-left: var(--spacing-xl);
|
padding-left: var(--spacing-xl);
|
||||||
padding-right: var(--spacing-xl);
|
padding-right: var(--spacing-xl);
|
||||||
position: relative;
|
position: relative;
|
||||||
border-bottom: var(--border-light);
|
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
}
|
}
|
||||||
.spectrum-Tabs-content {
|
.spectrum-Tabs-content {
|
||||||
margin-top: var(--spectrum-global-dimension-static-size-150);
|
margin-top: var(--spectrum-global-dimension-static-size-150);
|
||||||
|
|
|
@ -106,3 +106,29 @@ export const deepSet = (obj, key, value) => {
|
||||||
export const cloneDeep = obj => {
|
export const cloneDeep = obj => {
|
||||||
return JSON.parse(JSON.stringify(obj))
|
return JSON.parse(JSON.stringify(obj))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a value to the clipboard
|
||||||
|
* @param value the value to copy
|
||||||
|
*/
|
||||||
|
export const copyToClipboard = value => {
|
||||||
|
return new Promise(res => {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
// Try using the clipboard API first
|
||||||
|
navigator.clipboard.writeText(value).then(res)
|
||||||
|
} else {
|
||||||
|
// Fall back to the textarea hack
|
||||||
|
let textArea = document.createElement("textarea")
|
||||||
|
textArea.value = value
|
||||||
|
textArea.style.position = "fixed"
|
||||||
|
textArea.style.left = "-9999px"
|
||||||
|
textArea.style.top = "-9999px"
|
||||||
|
document.body.appendChild(textArea)
|
||||||
|
textArea.focus()
|
||||||
|
textArea.select()
|
||||||
|
document.execCommand("copy")
|
||||||
|
textArea.remove()
|
||||||
|
res()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
import filterTests from "../support/filterTests"
|
||||||
|
|
||||||
|
filterTests(['all'], () => {
|
||||||
|
context("Publish Application Workflow", () => {
|
||||||
|
before(() => {
|
||||||
|
cy.login()
|
||||||
|
cy.createTestApp()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should reflect the unpublished status correctly", () => {
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.get(".appTable .app-status").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.contains("Unpublished")
|
||||||
|
cy.get("svg[aria-label='GlobeStrike']").should("exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Preview")
|
||||||
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".deployment-top-nav svg[aria-label='GlobeStrike']").should("exist")
|
||||||
|
cy.get(".deployment-top-nav svg[aria-label='Globe']").should("not.exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should publish an application and correctly reflect that", () => {
|
||||||
|
//Assuming the previous test was run and the unpublished app is open in edit mode.
|
||||||
|
cy.get(".toprightnav button.spectrum-Button").contains("Publish").click({ force : true })
|
||||||
|
|
||||||
|
cy.get(".spectrum-Modal [data-cy='deploy-app-modal']").should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Publish").click({ force : true })
|
||||||
|
cy.wait(1000)
|
||||||
|
});
|
||||||
|
|
||||||
|
//Verify that the app url is presented correctly to the user
|
||||||
|
cy.get(".spectrum-Modal [data-cy='deploy-app-success-modal']")
|
||||||
|
.should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
let appUrl = Cypress.config().baseUrl + '/app/cypress-tests'
|
||||||
|
cy.get("[data-cy='deployed-app-url'] input").should('have.value', appUrl)
|
||||||
|
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
cy.wait(1000)
|
||||||
|
|
||||||
|
cy.get(".appTable .app-status").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.contains("Published")
|
||||||
|
cy.get("svg[aria-label='Globe']").should("exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("View app")
|
||||||
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".deployment-top-nav svg[aria-label='Globe']").should("exist").click({ force: true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='publish-popover-menu']").should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get("[data-cy='publish-popover-action']").should("exist")
|
||||||
|
cy.get("button").contains("View app").should("exist")
|
||||||
|
cy.get(".publish-popover-message").should("have.text", "Last published a few seconds ago")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should unpublish an application from the top navigation and reflect the status change", () => {
|
||||||
|
//Assuming the previous test app exists and is published
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .app-status").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.contains("Published")
|
||||||
|
cy.get("svg[aria-label='Globe']").should("exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".appTable .app-row-actions").eq(0)
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("View app")
|
||||||
|
cy.get(".spectrum-Button").contains("Edit").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
//The published status
|
||||||
|
cy.get(".deployment-top-nav svg[aria-label='Globe']").should("exist")
|
||||||
|
.click({ force: true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='publish-popover-menu']").should("be.visible")
|
||||||
|
cy.get("[data-cy='publish-popover-menu'] [data-cy='publish-popover-action']")
|
||||||
|
.click({ force : true })
|
||||||
|
|
||||||
|
cy.get("[data-cy='unpublish-modal']").should("be.visible")
|
||||||
|
.within(() => {
|
||||||
|
cy.get(".confirm-wrap button").click({ force: true }
|
||||||
|
)})
|
||||||
|
|
||||||
|
cy.get(".deployment-top-nav svg[aria-label='GlobeStrike']").should("exist")
|
||||||
|
|
||||||
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
|
|
||||||
|
cy.get(".appTable .app-status").eq(0).contains("Unpublished")
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -4,12 +4,44 @@ filterTests(['smoke', 'all'], () => {
|
||||||
context("Auto Screens UI", () => {
|
context("Auto Screens UI", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
cy.createTestApp()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should disable the autogenerated screen options if no sources are available", () => {
|
||||||
|
cy.createApp("First Test App", false)
|
||||||
|
|
||||||
|
cy.closeModal();
|
||||||
|
|
||||||
|
cy.contains("Design").click()
|
||||||
|
cy.get("[aria-label=AddCircle]").click()
|
||||||
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
cy.get(".item.disabled").contains("Autogenerated screens")
|
||||||
|
cy.get(".confirm-wrap .spectrum-Button").should('be.disabled')
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.deleteAllApps()
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not display incompatible sources", () => {
|
||||||
|
cy.createApp("Test App")
|
||||||
|
|
||||||
|
cy.selectExternalDatasource("REST")
|
||||||
|
cy.selectExternalDatasource("S3")
|
||||||
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Save and continue to query").click({ force : true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.navigateToAutogeneratedModal()
|
||||||
|
|
||||||
|
cy.get('.data-source-entry').should('have.length', 1)
|
||||||
|
cy.get('.data-source-entry')
|
||||||
|
|
||||||
|
cy.deleteAllApps()
|
||||||
|
});
|
||||||
|
|
||||||
it("should generate internal table screens", () => {
|
it("should generate internal table screens", () => {
|
||||||
// Create autogenerated screens from the internal table
|
cy.createTestApp()
|
||||||
cy.createAutogeneratedScreens(["Cypress Tests"])
|
// Create Autogenerated screens from the internal table
|
||||||
|
cy.createDatasourceScreen(["Cypress Tests"])
|
||||||
// Confirm screens have been auto generated
|
// Confirm screens have been auto generated
|
||||||
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true })
|
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true })
|
||||||
cy.get(".nav-items-container").should('contain', 'cypress-tests/:id')
|
cy.get(".nav-items-container").should('contain', 'cypress-tests/:id')
|
||||||
|
@ -21,8 +53,8 @@ filterTests(['smoke', 'all'], () => {
|
||||||
const initialTable = "Cypress Tests"
|
const initialTable = "Cypress Tests"
|
||||||
const secondTable = "Table Two"
|
const secondTable = "Table Two"
|
||||||
cy.createTable(secondTable)
|
cy.createTable(secondTable)
|
||||||
// Create autogenerated screens from the internal tables
|
// Create Autogenerated screens from the internal tables
|
||||||
cy.createAutogeneratedScreens([initialTable, secondTable])
|
cy.createDatasourceScreen([initialTable, secondTable])
|
||||||
// Confirm screens have been auto generated
|
// Confirm screens have been auto generated
|
||||||
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true })
|
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true })
|
||||||
// Previously generated tables are suffixed with numbers - as expected
|
// Previously generated tables are suffixed with numbers - as expected
|
||||||
|
@ -33,6 +65,25 @@ filterTests(['smoke', 'all'], () => {
|
||||||
.and('contain', 'table-two/new/row')
|
.and('contain', 'table-two/new/row')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should generate multiple internal table screens with the same screen access level", () => {
|
||||||
|
//The tables created in the previous step still exist
|
||||||
|
cy.createTable("Table Three")
|
||||||
|
cy.createTable("Table Four")
|
||||||
|
cy.createDatasourceScreen(["Table Three", "Table Four"], "Admin")
|
||||||
|
|
||||||
|
cy.get(".nav-items-container").contains("table-three").click()
|
||||||
|
cy.get(".nav-items-container").should('contain', 'table-three/:id')
|
||||||
|
.and('contain', 'table-three/new/row')
|
||||||
|
|
||||||
|
cy.get(".nav-items-container").contains("table-four").click()
|
||||||
|
cy.get(".nav-items-container").should('contain', 'table-four/:id')
|
||||||
|
.and('contain', 'table-four/new/row')
|
||||||
|
|
||||||
|
//The access level should now be set to admin. Previous screens should be filtered.
|
||||||
|
cy.get(".nav-items-container").contains("table-two").should('not.exist')
|
||||||
|
cy.get(".nav-items-container").contains("cypress-tests").should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
if (Cypress.env("TEST_ENV")) {
|
if (Cypress.env("TEST_ENV")) {
|
||||||
it("should generate data source screens", () => {
|
it("should generate data source screens", () => {
|
||||||
// Using MySQL data source for testing this
|
// Using MySQL data source for testing this
|
||||||
|
@ -40,8 +91,9 @@ filterTests(['smoke', 'all'], () => {
|
||||||
// Select & configure MySQL data source
|
// Select & configure MySQL data source
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
cy.addDatasourceConfig(datasource)
|
cy.addDatasourceConfig(datasource)
|
||||||
// Create autogenerated screens from a MySQL table - MySQL contains books table
|
// Create Autogenerated screens from a MySQL table - MySQL contains books table
|
||||||
cy.createAutogeneratedScreens(["books"])
|
cy.createDatasourceScreen(["books"])
|
||||||
|
|
||||||
cy.get(".nav-items-container").contains("books").click()
|
cy.get(".nav-items-container").contains("books").click()
|
||||||
cy.get(".nav-items-container").should('contain', 'books/:id')
|
cy.get(".nav-items-container").should('contain', 'books/:id')
|
||||||
.and('contain', 'books/new/row')
|
.and('contain', 'books/new/row')
|
||||||
|
|
|
@ -11,7 +11,7 @@ filterTests(['all'], () => {
|
||||||
cy.applicationInAppTable("Cypress Tests")
|
cy.applicationInAppTable("Cypress Tests")
|
||||||
cy.get(".appTable")
|
cy.get(".appTable")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.get(".app-row-actions-icon").eq(0).click()
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Menu").contains("Edit icon").click()
|
cy.get(".spectrum-Menu").contains("Edit icon").click()
|
||||||
// Select random icon
|
// Select random icon
|
||||||
|
@ -38,6 +38,7 @@ filterTests(['all'], () => {
|
||||||
cy.get(".title").children().children()
|
cy.get(".title").children().children()
|
||||||
.should('have.attr', 'style').and('contains', 'color')
|
.should('have.attr', 'style').and('contains', 'color')
|
||||||
})
|
})
|
||||||
|
cy.deleteAllApps()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -25,9 +25,13 @@ filterTests(['smoke', 'all'], () => {
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
|
|
||||||
if (Cypress.env("TEST_ENV")) {
|
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||||
|
.its("body")
|
||||||
|
.then(val => {
|
||||||
|
if (val.length > 0) {
|
||||||
cy.get(".spectrum-Button").contains("Templates").click({force: true})
|
cy.get(".spectrum-Button").contains("Templates").click({force: true})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
cy.get(".template-category-filters").should("exist")
|
cy.get(".template-category-filters").should("exist")
|
||||||
cy.get(".template-categories").should("exist")
|
cy.get(".template-categories").should("exist")
|
||||||
|
|
|
@ -11,7 +11,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
cy.createTestTableWithData()
|
cy.createTestTableWithData()
|
||||||
cy.wait(2000)
|
cy.wait(2000)
|
||||||
cy.contains("Automate").click()
|
cy.contains("Automate").click()
|
||||||
cy.get("[data-cy='new-screen'] > .spectrum-Icon").click()
|
cy.get(".add-button .spectrum-Icon").click()
|
||||||
cy.get(".modal-inner-wrapper").within(() => {
|
cy.get(".modal-inner-wrapper").within(() => {
|
||||||
cy.get("input").type("Add Row")
|
cy.get("input").type("Add Row")
|
||||||
cy.contains("Row Created").click({ force: true })
|
cy.contains("Row Created").click({ force: true })
|
||||||
|
@ -20,7 +20,6 @@ filterTests(['smoke', 'all'], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Setup trigger
|
// Setup trigger
|
||||||
cy.contains("Setup").click()
|
|
||||||
cy.get(".spectrum-Picker-label").click()
|
cy.get(".spectrum-Picker-label").click()
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
cy.contains("dog").click()
|
cy.contains("dog").click()
|
||||||
|
@ -32,7 +31,6 @@ filterTests(['smoke', 'all'], () => {
|
||||||
cy.contains("Create Row").trigger('mouseover').click().click()
|
cy.contains("Create Row").trigger('mouseover').click().click()
|
||||||
cy.get(".spectrum-Button--cta").click()
|
cy.get(".spectrum-Button--cta").click()
|
||||||
})
|
})
|
||||||
cy.contains("Setup").click()
|
|
||||||
cy.get(".spectrum-Picker-label").eq(1).click()
|
cy.get(".spectrum-Picker-label").eq(1).click()
|
||||||
cy.contains("dog").click()
|
cy.contains("dog").click()
|
||||||
cy.get(".spectrum-Textfield-input")
|
cy.get(".spectrum-Textfield-input")
|
||||||
|
|
|
@ -26,7 +26,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
|
|
||||||
it("should add a URL param binding", () => {
|
it("should add a URL param binding", () => {
|
||||||
const paramName = "foo"
|
const paramName = "foo"
|
||||||
cy.createScreen("Test Param", `/test/:${paramName}`)
|
cy.createScreen(`/test/:${paramName}`)
|
||||||
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
||||||
addSettingBinding("text", `URL.${paramName}`)
|
addSettingBinding("text", `URL.${paramName}`)
|
||||||
// The builder preview pages don't have a real URL, so all we can do
|
// The builder preview pages don't have a real URL, so all we can do
|
||||||
|
|
|
@ -9,17 +9,33 @@ filterTests(["smoke", "all"], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Should successfully create a screen", () => {
|
it("Should successfully create a screen", () => {
|
||||||
cy.createScreen("Test Screen", "/test")
|
cy.createScreen("/test")
|
||||||
cy.get(".nav-items-container").within(() => {
|
cy.get(".nav-items-container").within(() => {
|
||||||
cy.contains("/test").should("exist")
|
cy.contains("/test").should("exist")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Should update the url", () => {
|
it("Should update the url", () => {
|
||||||
cy.createScreen("Test Screen", "test with spaces")
|
cy.createScreen("test with spaces")
|
||||||
cy.get(".nav-items-container").within(() => {
|
cy.get(".nav-items-container").within(() => {
|
||||||
cy.contains("/test-with-spaces").should("exist")
|
cy.contains("/test-with-spaces").should("exist")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("Should create a blank screen with the selected access level", () => {
|
||||||
|
cy.createScreen("admin only", "Admin")
|
||||||
|
|
||||||
|
cy.get(".nav-items-container").within(() => {
|
||||||
|
cy.contains("/admin-only").should("exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.createScreen("open to all", "Public")
|
||||||
|
|
||||||
|
cy.get(".nav-items-container").within(() => {
|
||||||
|
cy.contains("/open-to-all").should("exist")
|
||||||
|
//The access level should now be set to admin. Previous screens should be filtered.
|
||||||
|
cy.get(".nav-item").contains("/test-screen").should("not.exist")
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -55,13 +55,14 @@ filterTests(["smoke", "all"], () => {
|
||||||
|
|
||||||
if (Cypress.env("TEST_ENV")) {
|
if (Cypress.env("TEST_ENV")) {
|
||||||
// No Pagination in CI - Test env only for the next two tests
|
// No Pagination in CI - Test env only for the next two tests
|
||||||
it("Adds 15 rows and checks pagination", () => {
|
xit("Adds 15 rows and checks pagination", () => {
|
||||||
// 10 rows per page, 15 rows should create 2 pages within table
|
// 10 rows per page, 15 rows should create 2 pages within table
|
||||||
const totalRows = 16
|
const totalRows = 16
|
||||||
for (let i = 1; i < totalRows; i++) {
|
for (let i = 1; i < totalRows; i++) {
|
||||||
cy.addRow([i])
|
cy.addRow([i])
|
||||||
}
|
}
|
||||||
cy.wait(1000)
|
cy.reload()
|
||||||
|
cy.wait(2000)
|
||||||
cy.get(".spectrum-Pagination").within(() => {
|
cy.get(".spectrum-Pagination").within(() => {
|
||||||
cy.get(".spectrum-ActionButton").eq(1).click()
|
cy.get(".spectrum-ActionButton").eq(1).click()
|
||||||
})
|
})
|
||||||
|
@ -70,13 +71,13 @@ filterTests(["smoke", "all"], () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Deletes rows and checks pagination", () => {
|
xit("Deletes rows and checks pagination", () => {
|
||||||
// Delete rows, removing second page of rows from table
|
// Delete rows, removing second page from table
|
||||||
const deleteRows = 5
|
|
||||||
cy.get(".spectrum-Checkbox-input").check({ force: true })
|
cy.get(".spectrum-Checkbox-input").check({ force: true })
|
||||||
cy.get(".spectrum-Table")
|
cy.get(".popovers").within(() => {
|
||||||
cy.contains("Delete 5 row(s)").click()
|
cy.get(".spectrum-Button").click({ force: true })
|
||||||
cy.get(".spectrum-Modal").contains("Delete").click()
|
})
|
||||||
|
cy.get(".spectrum-Dialog-grid").contains("Delete").click({ force: true })
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
|
|
||||||
// Confirm table only has one page
|
// Confirm table only has one page
|
||||||
|
|
|
@ -125,7 +125,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
|
|
||||||
it("renames a view", () => {
|
it("renames a view", () => {
|
||||||
cy.contains(".nav-item", "Test View")
|
cy.contains(".nav-item", "Test View")
|
||||||
.find(".actions .icon")
|
.find(".actions .icon.open-popover")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
cy.get(".spectrum-Menu-itemLabel").contains("Edit").click()
|
cy.get(".spectrum-Menu-itemLabel").contains("Edit").click()
|
||||||
cy.get(".modal-inner-wrapper").within(() => {
|
cy.get(".modal-inner-wrapper").within(() => {
|
||||||
|
@ -138,7 +138,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
|
|
||||||
it("deletes a view", () => {
|
it("deletes a view", () => {
|
||||||
cy.contains(".nav-item", "Test View Updated")
|
cy.contains(".nav-item", "Test View Updated")
|
||||||
.find(".actions .icon")
|
.find(".actions .icon.open-popover")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
cy.contains("Delete").click()
|
cy.contains("Delete").click()
|
||||||
cy.contains("Delete View").click()
|
cy.contains("Delete View").click()
|
||||||
|
|
|
@ -19,6 +19,7 @@ filterTests(["all"], () => {
|
||||||
cy.get(".spectrum-Button")
|
cy.get(".spectrum-Button")
|
||||||
.contains("Save and fetch tables")
|
.contains("Save and fetch tables")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
// Intercept Request after button click & apply assertions
|
// Intercept Request after button click & apply assertions
|
||||||
cy.wait("@datasource")
|
cy.wait("@datasource")
|
||||||
cy.get("@datasource")
|
cy.get("@datasource")
|
||||||
|
@ -31,6 +32,7 @@ filterTests(["all"], () => {
|
||||||
cy.get("@datasource")
|
cy.get("@datasource")
|
||||||
.its("response.body")
|
.its("response.body")
|
||||||
.should("have.property", "status", 500)
|
.should("have.property", "status", 500)
|
||||||
|
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should add MySQL data source and fetch tables", () => {
|
it("should add MySQL data source and fetch tables", () => {
|
||||||
|
@ -72,10 +74,13 @@ filterTests(["all"], () => {
|
||||||
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
|
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
|
||||||
cy.get(".spectrum-Picker").eq(4).click()
|
cy.get(".spectrum-Picker").eq(4).click()
|
||||||
cy.get(".spectrum-Popover").contains("REGION_ID").click()
|
cy.get(".spectrum-Popover").contains("REGION_ID").click()
|
||||||
// Save relationship & reload page
|
|
||||||
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
|
||||||
cy.reload()
|
|
||||||
})
|
})
|
||||||
|
// Save relationship & reload page
|
||||||
|
cy.get(".spectrum-ButtonGroup").within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||||
|
})
|
||||||
|
cy.reload()
|
||||||
|
|
||||||
// Confirm table length & column name
|
// Confirm table length & column name
|
||||||
cy.get(".spectrum-Table")
|
cy.get(".spectrum-Table")
|
||||||
.eq(1)
|
.eq(1)
|
||||||
|
@ -131,7 +136,7 @@ filterTests(["all"], () => {
|
||||||
cy.get(".spectrum-Table")
|
cy.get(".spectrum-Table")
|
||||||
.eq(1)
|
.eq(1)
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Table-row").eq(0).click()
|
cy.get(".spectrum-Table-row").eq(0).click({ force: true })
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
@ -175,11 +180,12 @@ filterTests(["all"], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should duplicate a query", () => {
|
it("should duplicate a query", () => {
|
||||||
// Get last nav item - The query
|
/// Get query nav item - QueryName
|
||||||
cy.get(".nav-item")
|
cy.get(".nav-item")
|
||||||
.last()
|
.contains(queryName)
|
||||||
|
.parent()
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".icon").eq(1).click({ force: true })
|
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
||||||
})
|
})
|
||||||
// Select and confirm duplication
|
// Select and confirm duplication
|
||||||
cy.get(".spectrum-Menu").contains("Duplicate").click()
|
cy.get(".spectrum-Menu").contains("Duplicate").click()
|
||||||
|
@ -199,12 +205,12 @@ filterTests(["all"], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should delete a query", () => {
|
it("should delete a query", () => {
|
||||||
// Get last nav item - The query
|
// Get query nav item - QueryName
|
||||||
for (let i = 0; i < 2; i++) {
|
|
||||||
cy.get(".nav-item")
|
cy.get(".nav-item")
|
||||||
.last()
|
.contains(queryName)
|
||||||
|
.parent()
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".icon").eq(1).click({ force: true })
|
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
||||||
})
|
})
|
||||||
// Select Delete
|
// Select Delete
|
||||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||||
|
@ -212,10 +218,8 @@ filterTests(["all"], () => {
|
||||||
.contains("Delete Query")
|
.contains("Delete Query")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
}
|
|
||||||
// Confirm deletion
|
// Confirm deletion
|
||||||
cy.get(".nav-item").should("not.contain", queryName)
|
cy.get(".nav-item").should("not.contain", queryName)
|
||||||
cy.get(".nav-item").should("not.contain", queryRename)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -46,9 +46,10 @@ filterTests(["all"], () => {
|
||||||
cy.get("@datasource")
|
cy.get("@datasource")
|
||||||
.its("response.body")
|
.its("response.body")
|
||||||
.should("have.property", "status", 500)
|
.should("have.property", "status", 500)
|
||||||
|
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should add Oracle data source and fetch tables", () => {
|
xit("should add Oracle data source and fetch tables", () => {
|
||||||
// Add & configure Oracle data source
|
// Add & configure Oracle data source
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
cy.intercept("**/datasources").as("datasource")
|
cy.intercept("**/datasources").as("datasource")
|
||||||
|
@ -64,7 +65,7 @@ filterTests(["all"], () => {
|
||||||
.should("be.gt", 0)
|
.should("be.gt", 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should define a One relationship type", () => {
|
xit("should define a One relationship type", () => {
|
||||||
// Select relationship type & configure
|
// Select relationship type & configure
|
||||||
cy.get(".spectrum-Button")
|
cy.get(".spectrum-Button")
|
||||||
.contains("Define relationship")
|
.contains("Define relationship")
|
||||||
|
@ -93,7 +94,7 @@ filterTests(["all"], () => {
|
||||||
cy.get(".spectrum-Table-cell").should("contain", "COUNTRIES to REGIONS")
|
cy.get(".spectrum-Table-cell").should("contain", "COUNTRIES to REGIONS")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should define a Many relationship type", () => {
|
xit("should define a Many relationship type", () => {
|
||||||
// Select relationship type & configure
|
// Select relationship type & configure
|
||||||
cy.get(".spectrum-Button")
|
cy.get(".spectrum-Button")
|
||||||
.contains("Define relationship")
|
.contains("Define relationship")
|
||||||
|
@ -127,7 +128,7 @@ filterTests(["all"], () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should delete relationships", () => {
|
xit("should delete relationships", () => {
|
||||||
// Delete both relationships
|
// Delete both relationships
|
||||||
cy.get(".spectrum-Table")
|
cy.get(".spectrum-Table")
|
||||||
.eq(1)
|
.eq(1)
|
||||||
|
@ -156,7 +157,7 @@ filterTests(["all"], () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should add a query", () => {
|
xit("should add a query", () => {
|
||||||
// Add query
|
// Add query
|
||||||
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
|
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
|
||||||
cy.get(".spectrum-Form-item")
|
cy.get(".spectrum-Form-item")
|
||||||
|
@ -181,7 +182,7 @@ filterTests(["all"], () => {
|
||||||
cy.get(".nav-item").should("contain", queryName)
|
cy.get(".nav-item").should("contain", queryName)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should duplicate a query", () => {
|
xit("should duplicate a query", () => {
|
||||||
// Get query nav item
|
// Get query nav item
|
||||||
cy.get(".nav-item")
|
cy.get(".nav-item")
|
||||||
.contains(queryName)
|
.contains(queryName)
|
||||||
|
@ -194,7 +195,7 @@ filterTests(["all"], () => {
|
||||||
cy.get(".nav-item").should("contain", queryName + " (1)")
|
cy.get(".nav-item").should("contain", queryName + " (1)")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should edit a query name", () => {
|
xit("should edit a query name", () => {
|
||||||
// Rename query
|
// Rename query
|
||||||
cy.get(".spectrum-Form-item")
|
cy.get(".spectrum-Form-item")
|
||||||
.eq(0)
|
.eq(0)
|
||||||
|
@ -206,7 +207,7 @@ filterTests(["all"], () => {
|
||||||
cy.get(".nav-item").should("contain", queryRename)
|
cy.get(".nav-item").should("contain", queryRename)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should delete a query", () => {
|
xit("should delete a query", () => {
|
||||||
// Get query nav item - QueryName
|
// Get query nav item - QueryName
|
||||||
cy.get(".nav-item")
|
cy.get(".nav-item")
|
||||||
.contains(queryName)
|
.contains(queryName)
|
||||||
|
|
|
@ -21,16 +21,10 @@ filterTests(["all"], () => {
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
// Intercept Request after button click & apply assertions
|
// Intercept Request after button click & apply assertions
|
||||||
cy.wait("@datasource")
|
cy.wait("@datasource")
|
||||||
cy.get("@datasource")
|
|
||||||
.its("response.body")
|
|
||||||
.should(
|
|
||||||
"have.property",
|
|
||||||
"message",
|
|
||||||
"connect ECONNREFUSED 127.0.0.1:5432"
|
|
||||||
)
|
|
||||||
cy.get("@datasource")
|
cy.get("@datasource")
|
||||||
.its("response.body")
|
.its("response.body")
|
||||||
.should("have.property", "status", 500)
|
.should("have.property", "status", 500)
|
||||||
|
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should add PostgreSQL data source and fetch tables", () => {
|
it("should add PostgreSQL data source and fetch tables", () => {
|
||||||
|
@ -113,13 +107,13 @@ filterTests(["all"], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should delete a relationship", () => {
|
it("should delete a relationship", () => {
|
||||||
cy.get(".hierarchy-items-container").contains(datasource).click()
|
cy.get(".hierarchy-items-container").contains("PostgreSQL-2").click()
|
||||||
cy.reload()
|
cy.reload()
|
||||||
// Delete one relationship
|
// Delete one relationship
|
||||||
cy.get(".spectrum-Table")
|
cy.get(".spectrum-Table")
|
||||||
.eq(1)
|
.eq(1)
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Table-row").eq(0).click()
|
cy.get(".spectrum-Table-row").eq(0).click({ force: true })
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
@ -161,7 +155,7 @@ filterTests(["all"], () => {
|
||||||
|
|
||||||
it("should switch to schema with no tables", () => {
|
it("should switch to schema with no tables", () => {
|
||||||
// Switch Schema - To one without any tables
|
// Switch Schema - To one without any tables
|
||||||
cy.get(".hierarchy-items-container").contains(datasource).click()
|
cy.get(".hierarchy-items-container").contains("PostgreSQL-2").click()
|
||||||
switchSchema("randomText")
|
switchSchema("randomText")
|
||||||
|
|
||||||
// No tables displayed
|
// No tables displayed
|
||||||
|
@ -208,11 +202,12 @@ filterTests(["all"], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should duplicate a query", () => {
|
it("should duplicate a query", () => {
|
||||||
// Get last nav item - The query
|
// Locate previously created query
|
||||||
cy.get(".nav-item")
|
cy.get(".nav-item")
|
||||||
.last()
|
.contains(queryName)
|
||||||
|
.siblings(".actions")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".icon").eq(1).click({ force: true })
|
cy.get(".icon").click({ force: true })
|
||||||
})
|
})
|
||||||
// Select and confirm duplication
|
// Select and confirm duplication
|
||||||
cy.get(".spectrum-Menu").contains("Duplicate").click()
|
cy.get(".spectrum-Menu").contains("Duplicate").click()
|
||||||
|
@ -240,12 +235,12 @@ filterTests(["all"], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should delete a query", () => {
|
it("should delete a query", () => {
|
||||||
// Get last nav item - The query
|
// Get query nav item - QueryName
|
||||||
for (let i = 0; i < 2; i++) {
|
|
||||||
cy.get(".nav-item")
|
cy.get(".nav-item")
|
||||||
.last()
|
.contains(queryName)
|
||||||
|
.parent()
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".icon").eq(1).click({ force: true })
|
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
||||||
})
|
})
|
||||||
// Select Delete
|
// Select Delete
|
||||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||||
|
@ -253,10 +248,8 @@ filterTests(["all"], () => {
|
||||||
.contains("Delete Query")
|
.contains("Delete Query")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
}
|
|
||||||
// Confirm deletion
|
// Confirm deletion
|
||||||
cy.get(".nav-item").should("not.contain", queryName)
|
cy.get(".nav-item").should("not.contain", queryName)
|
||||||
cy.get(".nav-item").should("not.contain", queryRename)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const switchSchema = schema => {
|
const switchSchema = schema => {
|
||||||
|
|
|
@ -99,7 +99,7 @@ filterTests(['all'], () => {
|
||||||
cy.searchForApplication(originalName)
|
cy.searchForApplication(originalName)
|
||||||
cy.get(".appTable")
|
cy.get(".appTable")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.get("[aria-label='More']").eq(0).click()
|
||||||
})
|
})
|
||||||
// Check for when an app is published
|
// Check for when an app is published
|
||||||
if (published == true) {
|
if (published == true) {
|
||||||
|
@ -109,7 +109,9 @@ filterTests(['all'], () => {
|
||||||
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
||||||
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
|
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
|
||||||
}
|
}
|
||||||
cy.contains("Edit").click()
|
cy.get("[data-cy='app-row-actions-menu-popover']").eq(0).within(() => {
|
||||||
|
cy.get(".spectrum-Menu-item").contains("Edit").click({ force: true })
|
||||||
|
})
|
||||||
cy.get(".spectrum-Modal")
|
cy.get(".spectrum-Modal")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
if (noName == true) {
|
if (noName == true) {
|
||||||
|
|
|
@ -10,9 +10,9 @@ filterTests(['smoke', 'all'], () => {
|
||||||
it("should try to revert an unpublished app", () => {
|
it("should try to revert an unpublished app", () => {
|
||||||
// Click revert icon
|
// Click revert icon
|
||||||
cy.get(".toprightnav").within(() => {
|
cy.get(".toprightnav").within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.get("[aria-label='Revert']").click({ force: true })
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
// Enter app name before revert
|
// Enter app name before revert
|
||||||
cy.get("input").type("Cypress Tests")
|
cy.get("input").type("Cypress Tests")
|
||||||
cy.intercept('**/revert').as('revertApp')
|
cy.intercept('**/revert').as('revertApp')
|
||||||
|
@ -33,11 +33,15 @@ filterTests(['smoke', 'all'], () => {
|
||||||
cy.get(".spectrum-ButtonGroup").within(() => {
|
cy.get(".spectrum-ButtonGroup").within(() => {
|
||||||
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
||||||
})
|
})
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.get(".spectrum-ButtonGroup").within(() => {
|
||||||
|
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||||
|
})
|
||||||
// Add second component - Button
|
// Add second component - Button
|
||||||
cy.addComponent("Elements", "Button")
|
cy.addComponent("Elements", "Button")
|
||||||
// Click Revert
|
// Click Revert
|
||||||
cy.get(".toprightnav").within(() => {
|
cy.get(".toprightnav").within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.get("[aria-label='Revert']").click({ force: true })
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
// Click Revert
|
// Click Revert
|
||||||
|
@ -54,7 +58,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
it("should enter incorrect app name when reverting", () => {
|
it("should enter incorrect app name when reverting", () => {
|
||||||
// Click Revert
|
// Click Revert
|
||||||
cy.get(".toprightnav").within(() => {
|
cy.get(".toprightnav").within(() => {
|
||||||
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
cy.get("[aria-label='Revert']").click({ force: true })
|
||||||
})
|
})
|
||||||
// Enter incorrect app name
|
// Enter incorrect app name
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
|
|
@ -32,7 +32,17 @@ Cypress.Commands.add("login", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createApp", name => {
|
Cypress.Commands.add("closeModal", () => {
|
||||||
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
cy.get(".close-icon").click()
|
||||||
|
cy.wait(500)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("createApp", (name, addDefaultTable) => {
|
||||||
|
const shouldCreateDefaultTable =
|
||||||
|
typeof addDefaultTable != "boolean" ? true : addDefaultTable
|
||||||
|
|
||||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
|
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
|
||||||
|
@ -51,7 +61,9 @@ Cypress.Commands.add("createApp", name => {
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
||||||
cy.wait(10000)
|
cy.wait(10000)
|
||||||
})
|
})
|
||||||
|
if (shouldCreateDefaultTable) {
|
||||||
cy.createTable("Cypress Tests", true)
|
cy.createTable("Cypress Tests", true)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("deleteApp", name => {
|
Cypress.Commands.add("deleteApp", name => {
|
||||||
|
@ -60,6 +72,8 @@ Cypress.Commands.add("deleteApp", name => {
|
||||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||||
.its("body")
|
.its("body")
|
||||||
.then(val => {
|
.then(val => {
|
||||||
|
const findAppName = val.some(val => val.name == name)
|
||||||
|
if (findAppName) {
|
||||||
if (val.length > 0) {
|
if (val.length > 0) {
|
||||||
if (Cypress.env("TEST_ENV")) {
|
if (Cypress.env("TEST_ENV")) {
|
||||||
cy.searchForApplication(name)
|
cy.searchForApplication(name)
|
||||||
|
@ -100,6 +114,9 @@ Cypress.Commands.add("deleteApp", name => {
|
||||||
} else {
|
} else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -130,7 +147,7 @@ Cypress.Commands.add("createTestApp", () => {
|
||||||
const appName = "Cypress Tests"
|
const appName = "Cypress Tests"
|
||||||
cy.deleteApp(appName)
|
cy.deleteApp(appName)
|
||||||
cy.createApp(appName, "This app is used for Cypress testing.")
|
cy.createApp(appName, "This app is used for Cypress testing.")
|
||||||
cy.createScreen("home", "home")
|
cy.createScreen("home")
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createTestTableWithData", () => {
|
Cypress.Commands.add("createTestTableWithData", () => {
|
||||||
|
@ -270,32 +287,98 @@ Cypress.Commands.add("navigateToDataSection", () => {
|
||||||
cy.contains("Data").click()
|
cy.contains("Data").click()
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createScreen", (screenName, route) => {
|
//Blank
|
||||||
|
Cypress.Commands.add("createScreen", (route, accessLevelLabel) => {
|
||||||
cy.contains("Design").click()
|
cy.contains("Design").click()
|
||||||
cy.get("[aria-label=AddCircle]").click()
|
cy.get("[aria-label=AddCircle]").click()
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get(".item").contains("Blank").click()
|
cy.get("[data-cy='blank-screen']").click()
|
||||||
cy.get(".spectrum-Button").contains("Add screens").click({ force: true })
|
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
cy.get(".spectrum-Form-itemField").eq(0).type(screenName)
|
cy.get(".spectrum-Form-itemField").eq(0).type(route)
|
||||||
cy.get(".spectrum-Form-itemField").eq(1).type(route)
|
|
||||||
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
if (accessLevelLabel) {
|
||||||
|
cy.get(".spectrum-Picker-label").click()
|
||||||
|
cy.wait(500)
|
||||||
|
cy.contains(accessLevelLabel).click()
|
||||||
|
}
|
||||||
|
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createAutogeneratedScreens", screenNames => {
|
Cypress.Commands.add(
|
||||||
|
"createDatasourceScreen",
|
||||||
|
(datasourceNames, accessLevelLabel) => {
|
||||||
|
cy.contains("Design").click()
|
||||||
|
cy.get("[aria-label=AddCircle]").click()
|
||||||
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
cy.get(".item").contains("Autogenerated screens").click()
|
||||||
|
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Modal [data-cy='data-source-modal']").within(() => {
|
||||||
|
for (let i = 0; i < datasourceNames.length; i++) {
|
||||||
|
cy.get(".data-source-entry").contains(datasourceNames[i]).click()
|
||||||
|
//Ensure the check mark is visible
|
||||||
|
cy.get(".data-source-entry")
|
||||||
|
.contains(datasourceNames[i])
|
||||||
|
.get(".data-source-check")
|
||||||
|
.should("exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.get(".spectrum-Button").contains("Confirm").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
if (accessLevelLabel) {
|
||||||
|
cy.get(".spectrum-Picker-label").click()
|
||||||
|
cy.wait(500)
|
||||||
|
cy.contains(accessLevelLabel).click()
|
||||||
|
}
|
||||||
|
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.contains("Design").click()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Cypress.Commands.add("navigateToAutogeneratedModal", () => {
|
||||||
// Screen name must already exist within data source
|
// Screen name must already exist within data source
|
||||||
cy.contains("Design").click()
|
cy.contains("Design").click()
|
||||||
cy.get("[aria-label=AddCircle]").click()
|
cy.get("[aria-label=AddCircle]").click()
|
||||||
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
cy.get(".item").contains("Autogenerated screens").click()
|
||||||
|
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add(
|
||||||
|
"createAutogeneratedScreens",
|
||||||
|
(screenNames, accessLevelLabel) => {
|
||||||
|
cy.navigateToAutogeneratedModal()
|
||||||
|
|
||||||
for (let i = 0; i < screenNames.length; i++) {
|
for (let i = 0; i < screenNames.length; i++) {
|
||||||
cy.get(".item").contains(screenNames[i]).click()
|
cy.get(".data-source-entry").contains(screenNames[i]).click()
|
||||||
}
|
}
|
||||||
cy.get(".spectrum-Button").contains("Add screens").click({ force: true })
|
|
||||||
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
if (accessLevelLabel) {
|
||||||
|
cy.get(".spectrum-Picker-label").click()
|
||||||
|
cy.wait(500)
|
||||||
|
cy.contains(accessLevelLabel).click()
|
||||||
|
}
|
||||||
|
cy.get(".spectrum-Button").contains("Confirm").click({ force: true })
|
||||||
cy.wait(4000)
|
cy.wait(4000)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Cypress.Commands.add("addRow", values => {
|
Cypress.Commands.add("addRow", values => {
|
||||||
cy.contains("Create row").click()
|
cy.contains("Create row").click()
|
||||||
|
@ -390,6 +473,7 @@ Cypress.Commands.add("selectExternalDatasource", datasourceName => {
|
||||||
cy.get(".add-button").click()
|
cy.get(".add-button").click()
|
||||||
})
|
})
|
||||||
// Clicks specified datasource & continue
|
// Clicks specified datasource & continue
|
||||||
|
cy.wait(1000)
|
||||||
cy.get(".item-list").contains(datasourceName).click()
|
cy.get(".item-list").contains(datasourceName).click()
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||||
|
@ -410,7 +494,9 @@ Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => {
|
||||||
if (datasource == "Oracle") {
|
if (datasource == "Oracle") {
|
||||||
cy.get("input").clear().type(Cypress.env("oracle").HOST)
|
cy.get("input").clear().type(Cypress.env("oracle").HOST)
|
||||||
} else {
|
} else {
|
||||||
cy.get("input").clear().type(Cypress.env("HOST_IP"))
|
cy.get("input")
|
||||||
|
.clear({ force: true })
|
||||||
|
.type(Cypress.env("HOST_IP"), { force: true })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.0.105-alpha.10",
|
"version": "1.0.124-alpha.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -65,10 +65,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.105-alpha.10",
|
"@budibase/bbui": "^1.0.124-alpha.0",
|
||||||
"@budibase/client": "^1.0.105-alpha.10",
|
"@budibase/client": "^1.0.124-alpha.0",
|
||||||
"@budibase/frontend-core": "^1.0.105-alpha.10",
|
"@budibase/frontend-core": "^1.0.124-alpha.0",
|
||||||
"@budibase/string-templates": "^1.0.105-alpha.10",
|
"@budibase/string-templates": "^1.0.124-alpha.0",
|
||||||
"@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",
|
||||||
|
|
|
@ -23,6 +23,7 @@ export const Events = {
|
||||||
},
|
},
|
||||||
SCREEN: {
|
SCREEN: {
|
||||||
CREATED: "Screen Created",
|
CREATED: "Screen Created",
|
||||||
|
CREATE_ROLE_UPDATED: "Changed Role On Screen Creation",
|
||||||
},
|
},
|
||||||
AUTOMATION: {
|
AUTOMATION: {
|
||||||
CREATED: "Automation Created",
|
CREATED: "Automation Created",
|
||||||
|
@ -36,6 +37,7 @@ export const Events = {
|
||||||
CREATED: "budibase:app_created",
|
CREATED: "budibase:app_created",
|
||||||
PUBLISHED: "budibase:app_published",
|
PUBLISHED: "budibase:app_published",
|
||||||
UNPUBLISHED: "budibase:app_unpublished",
|
UNPUBLISHED: "budibase:app_unpublished",
|
||||||
|
VIEW_PUBLISHED: "budibase:view_published_app",
|
||||||
},
|
},
|
||||||
ANALYTICS: {
|
ANALYTICS: {
|
||||||
OPT_IN: "budibase:analytics_opt_in",
|
OPT_IN: "budibase:analytics_opt_in",
|
||||||
|
@ -51,3 +53,9 @@ export const Events = {
|
||||||
SAVED: "budibase:sso_saved",
|
SAVED: "budibase:sso_saved",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EventSource = {
|
||||||
|
PORTAL: "portal",
|
||||||
|
URL: "url",
|
||||||
|
NOTIFICATION: "notification",
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { API } from "api"
|
||||||
import PosthogClient from "./PosthogClient"
|
import PosthogClient from "./PosthogClient"
|
||||||
import IntercomClient from "./IntercomClient"
|
import IntercomClient from "./IntercomClient"
|
||||||
import SentryClient from "./SentryClient"
|
import SentryClient from "./SentryClient"
|
||||||
import { Events } from "./constants"
|
import { Events, EventSource } from "./constants"
|
||||||
|
|
||||||
const posthog = new PosthogClient(
|
const posthog = new PosthogClient(
|
||||||
process.env.POSTHOG_TOKEN,
|
process.env.POSTHOG_TOKEN,
|
||||||
|
@ -58,5 +58,5 @@ class AnalyticsHub {
|
||||||
|
|
||||||
const analytics = new AnalyticsHub()
|
const analytics = new AnalyticsHub()
|
||||||
|
|
||||||
export { Events }
|
export { Events, EventSource }
|
||||||
export default analytics
|
export default analytics
|
||||||
|
|
|
@ -654,7 +654,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
||||||
* Builds a form schema given a form component.
|
* Builds a form schema given a form component.
|
||||||
* A form schema is a schema of all the fields nested anywhere within a form.
|
* A form schema is a schema of all the fields nested anywhere within a form.
|
||||||
*/
|
*/
|
||||||
const buildFormSchema = component => {
|
export const buildFormSchema = component => {
|
||||||
let schema = {}
|
let schema = {}
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return schema
|
return schema
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
if (v.internal) {
|
if (v.internal) {
|
||||||
acc[k] = v
|
acc[k] = v
|
||||||
}
|
}
|
||||||
|
delete acc.LOOP
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
|
|
|
@ -72,7 +72,9 @@
|
||||||
animate:flip={{ duration: 500 }}
|
animate:flip={{ duration: 500 }}
|
||||||
in:fly|local={{ x: 500, duration: 1500 }}
|
in:fly|local={{ x: 500, duration: 1500 }}
|
||||||
>
|
>
|
||||||
|
{#if block.stepId !== "LOOP"}
|
||||||
<FlowItem {testDataModal} {block} />
|
<FlowItem {testDataModal} {block} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
Modal,
|
Modal,
|
||||||
Button,
|
Button,
|
||||||
StatusLight,
|
StatusLight,
|
||||||
ActionButton,
|
|
||||||
Select,
|
Select,
|
||||||
|
ActionButton,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||||
|
@ -25,8 +25,8 @@
|
||||||
let webhookModal
|
let webhookModal
|
||||||
let actionModal
|
let actionModal
|
||||||
let resultsModal
|
let resultsModal
|
||||||
let setupToggled
|
|
||||||
let blockComplete
|
let blockComplete
|
||||||
|
let showLooping = false
|
||||||
|
|
||||||
$: rowControl = $automationStore.selectedAutomation.automation.rowControl
|
$: rowControl = $automationStore.selectedAutomation.automation.rowControl
|
||||||
$: showBindingPicker =
|
$: showBindingPicker =
|
||||||
|
@ -48,12 +48,21 @@
|
||||||
$automationStore.selectedAutomation?.automation?.definition?.steps.length +
|
$automationStore.selectedAutomation?.automation?.definition?.steps.length +
|
||||||
1
|
1
|
||||||
|
|
||||||
$: hasCompletedInputs = Object.keys(
|
$: loopingSelected =
|
||||||
block.schema?.inputs?.properties || {}
|
$automationStore.selectedAutomation?.automation.definition.steps.find(
|
||||||
).every(x => block?.inputs[x])
|
x => x.blockToLoop === block.id
|
||||||
|
)
|
||||||
|
|
||||||
async function deleteStep() {
|
async function deleteStep() {
|
||||||
|
let loopBlock =
|
||||||
|
$automationStore.selectedAutomation?.automation.definition.steps.find(
|
||||||
|
x => x.blockToLoop === block.id
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (loopBlock) {
|
||||||
|
automationStore.actions.deleteAutomationBlock(loopBlock)
|
||||||
|
}
|
||||||
automationStore.actions.deleteAutomationBlock(block)
|
automationStore.actions.deleteAutomationBlock(block)
|
||||||
await automationStore.actions.save(
|
await automationStore.actions.save(
|
||||||
$automationStore.selectedAutomation?.automation
|
$automationStore.selectedAutomation?.automation
|
||||||
|
@ -76,6 +85,23 @@
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addLooping() {
|
||||||
|
loopingSelected = true
|
||||||
|
const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP
|
||||||
|
|
||||||
|
const loopBlock = $automationStore.selectedAutomation.constructBlock(
|
||||||
|
"ACTION",
|
||||||
|
"LOOP",
|
||||||
|
loopDefinition
|
||||||
|
)
|
||||||
|
loopBlock.blockToLoop = block.id
|
||||||
|
block.loopBlock = loopBlock.id
|
||||||
|
automationStore.actions.addBlockToAutomation(loopBlock, blockIdx)
|
||||||
|
await automationStore.actions.save(
|
||||||
|
$automationStore.selectedAutomation?.automation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function onSelect(block) {
|
async function onSelect(block) {
|
||||||
await automationStore.update(state => {
|
await automationStore.update(state => {
|
||||||
state.selectedBlock = block
|
state.selectedBlock = block
|
||||||
|
@ -84,13 +110,68 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class={`block ${block.type} hoverable`} class:selected on:click={() => {}}>
|
||||||
|
{#if loopingSelected}
|
||||||
|
<div class="blockSection">
|
||||||
<div
|
<div
|
||||||
class={`block ${block.type} hoverable`}
|
on:click={() => {
|
||||||
class:selected
|
showLooping = !showLooping
|
||||||
|
}}
|
||||||
|
class="splitHeader"
|
||||||
|
>
|
||||||
|
<div class="center-items">
|
||||||
|
<svg
|
||||||
|
width="28px"
|
||||||
|
height="28px"
|
||||||
|
class="spectrum-Icon"
|
||||||
|
style="color:grey;"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-Reuse" />
|
||||||
|
</svg>
|
||||||
|
<div class="iconAlign">
|
||||||
|
<Detail size="S">Looping</Detail>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="blockTitle">
|
||||||
|
<div
|
||||||
|
style="margin-left: 10px;"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
onSelect(block)
|
onSelect(block)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Icon name={showLooping ? "ChevronDown" : "ChevronUp"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider noMargin />
|
||||||
|
{#if !showLooping}
|
||||||
|
<div class="blockSection">
|
||||||
|
<div class="block-options">
|
||||||
|
<div class="delete-padding" on:click={() => deleteStep()}>
|
||||||
|
<Icon name="DeleteOutline" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
<AutomationBlockSetup
|
||||||
|
schemaProperties={Object.entries(
|
||||||
|
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
|
||||||
|
.properties
|
||||||
|
)}
|
||||||
|
block={$automationStore.selectedAutomation?.automation.definition.steps.find(
|
||||||
|
x => x.blockToLoop === block.id
|
||||||
|
)}
|
||||||
|
{webhookModal}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
<Divider noMargin />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="blockSection">
|
<div class="blockSection">
|
||||||
<div
|
<div
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -127,55 +208,57 @@
|
||||||
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
|
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="blockTitle">
|
||||||
{#if testResult && testResult[0]}
|
{#if testResult && testResult[0]}
|
||||||
<span on:click={() => resultsModal.show()}>
|
<div style="float: right;" on:click={() => resultsModal.show()}>
|
||||||
<StatusLight
|
<StatusLight
|
||||||
positive={isTrigger || testResult[0].outputs?.success}
|
positive={isTrigger || testResult[0].outputs?.success}
|
||||||
negative={!testResult[0].outputs?.success}
|
negative={!testResult[0].outputs?.success}
|
||||||
><Body size="XS">View response</Body></StatusLight
|
><Body size="XS">View response</Body></StatusLight
|
||||||
>
|
>
|
||||||
</span>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<div
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
on:click={() => {
|
||||||
|
onSelect(block)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name={blockComplete ? "ChevronDown" : "ChevronUp"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if !blockComplete}
|
{#if !blockComplete}
|
||||||
<Divider noMargin />
|
<Divider noMargin />
|
||||||
<div class="blockSection">
|
<div class="blockSection">
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
<div class="splitHeader">
|
|
||||||
<ActionButton
|
|
||||||
on:click={() => {
|
|
||||||
onSelect(block)
|
|
||||||
setupToggled = !setupToggled
|
|
||||||
}}
|
|
||||||
quiet
|
|
||||||
icon={setupToggled ? "ChevronDown" : "ChevronRight"}
|
|
||||||
>
|
|
||||||
<Detail size="S">Setup</Detail>
|
|
||||||
</ActionButton>
|
|
||||||
{#if !isTrigger}
|
{#if !isTrigger}
|
||||||
<div class="block-options">
|
|
||||||
{#if showBindingPicker}
|
|
||||||
<div>
|
<div>
|
||||||
|
<div class="block-options">
|
||||||
|
{#if !loopingSelected}
|
||||||
|
<ActionButton on:click={() => addLooping()} icon="Reuse"
|
||||||
|
>Add Looping</ActionButton
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{#if showBindingPicker}
|
||||||
<Select
|
<Select
|
||||||
on:change={toggleFieldControl}
|
on:change={toggleFieldControl}
|
||||||
quiet
|
|
||||||
defaultValue="Use values"
|
defaultValue="Use values"
|
||||||
autoWidth
|
autoWidth
|
||||||
value={rowControl ? "Use bindings" : "Use values"}
|
value={rowControl ? "Use bindings" : "Use values"}
|
||||||
options={["Use values", "Use bindings"]}
|
options={["Use values", "Use bindings"]}
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
<div class="delete-padding" on:click={() => deleteStep()}>
|
<ActionButton
|
||||||
<Icon name="DeleteOutline" />
|
on:click={() => deleteStep()}
|
||||||
|
icon="DeleteOutline"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if setupToggled}
|
|
||||||
<AutomationBlockSetup
|
<AutomationBlockSetup
|
||||||
schemaProperties={Object.entries(block.schema.inputs.properties)}
|
schemaProperties={Object.entries(block.schema.inputs.properties)}
|
||||||
{block}
|
{block}
|
||||||
|
@ -186,7 +269,6 @@
|
||||||
>Finish and test automation</Button
|
>Finish and test automation</Button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -204,13 +286,7 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
<div class="separator" />
|
<div class="separator" />
|
||||||
<Icon
|
<Icon on:click={() => actionModal.show()} hoverable name="AddCircle" size="S" />
|
||||||
on:click={() => actionModal.show()}
|
|
||||||
disabled={!hasCompletedInputs}
|
|
||||||
hoverable
|
|
||||||
name="AddCircle"
|
|
||||||
size="S"
|
|
||||||
/>
|
|
||||||
{#if isTrigger ? totalBlocks > 1 : blockIdx !== totalBlocks - 2}
|
{#if isTrigger ? totalBlocks > 1 : blockIdx !== totalBlocks - 2}
|
||||||
<div class="separator" />
|
<div class="separator" />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -220,8 +296,10 @@
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
}
|
}
|
||||||
.block-options {
|
.block-options {
|
||||||
display: flex;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
.center-items {
|
.center-items {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -256,4 +334,9 @@
|
||||||
/* center horizontally */
|
/* center horizontally */
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blockTitle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Icon, Detail, TextArea } from "@budibase/bbui"
|
import { ModalContent, Icon, Detail, TextArea, Label } from "@budibase/bbui"
|
||||||
|
|
||||||
export let testResult
|
export let testResult
|
||||||
export let isTrigger
|
export let isTrigger
|
||||||
|
@ -10,11 +10,11 @@
|
||||||
<ModalContent
|
<ModalContent
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
showConfirmButton={false}
|
showConfirmButton={false}
|
||||||
title="Test Automation"
|
|
||||||
cancelText="Close"
|
cancelText="Close"
|
||||||
>
|
>
|
||||||
<div slot="header">
|
<div slot="header" class="result-modal-header">
|
||||||
<div style="float: right;">
|
<span>Test Results</span>
|
||||||
|
<div>
|
||||||
{#if isTrigger || testResult[0].outputs.success}
|
{#if isTrigger || testResult[0].outputs.success}
|
||||||
<div class="iconSuccess">
|
<div class="iconSuccess">
|
||||||
<Icon size="S" name="CheckmarkCircle" />
|
<Icon size="S" name="CheckmarkCircle" />
|
||||||
|
@ -26,7 +26,18 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<span>
|
||||||
|
{#if testResult[0].outputs.iterations}
|
||||||
|
<div style="display: flex;">
|
||||||
|
<Icon name="Reuse" />
|
||||||
|
<div style="margin-left: 10px;">
|
||||||
|
<Label>
|
||||||
|
This loop ran {testResult[0].outputs.iterations} times.</Label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
<div
|
<div
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
inputToggled = !inputToggled
|
inputToggled = !inputToggled
|
||||||
|
@ -89,6 +100,14 @@
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.result-modal-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.iconSuccess {
|
.iconSuccess {
|
||||||
color: var(--spectrum-global-color-green-600);
|
color: var(--spectrum-global-color-green-600);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<div class="add-button" data-cy="new-screen">
|
<div class="add-button">
|
||||||
<Icon hoverable name="AddCircle" on:click={modal.show} />
|
<Icon hoverable name="AddCircle" on:click={modal.show} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -25,11 +25,11 @@
|
||||||
import QueryParamSelector from "./QueryParamSelector.svelte"
|
import QueryParamSelector from "./QueryParamSelector.svelte"
|
||||||
import CronBuilder from "./CronBuilder.svelte"
|
import CronBuilder from "./CronBuilder.svelte"
|
||||||
import Editor from "components/integration/QueryEditor.svelte"
|
import Editor from "components/integration/QueryEditor.svelte"
|
||||||
import { debounce } from "lodash"
|
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||||
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
|
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
|
||||||
import { LuceneUtils } from "@budibase/frontend-core"
|
import { LuceneUtils } from "@budibase/frontend-core"
|
||||||
import { getSchemaForTable } from "builderStore/dataBinding"
|
import { getSchemaForTable } from "builderStore/dataBinding"
|
||||||
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let testData
|
export let testData
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
$: schema = getSchemaForTable(tableId, { searchableSchema: true }).schema
|
$: schema = getSchemaForTable(tableId, { searchableSchema: true }).schema
|
||||||
$: schemaFields = Object.values(schema || {})
|
$: schemaFields = Object.values(schema || {})
|
||||||
|
|
||||||
const onChange = debounce(async function (e, key) {
|
const onChange = Utils.sequential(async (e, key) => {
|
||||||
try {
|
try {
|
||||||
if (isTestModal) {
|
if (isTestModal) {
|
||||||
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
|
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
|
||||||
|
@ -82,39 +82,71 @@
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error saving automation")
|
notifications.error("Error saving automation")
|
||||||
}
|
}
|
||||||
}, 800)
|
})
|
||||||
|
|
||||||
function getAvailableBindings(block, automation) {
|
function getAvailableBindings(block, automation) {
|
||||||
if (!block || !automation) {
|
if (!block || !automation) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find previous steps to the selected one
|
// Find previous steps to the selected one
|
||||||
let allSteps = [...automation.steps]
|
let allSteps = [...automation.steps]
|
||||||
|
|
||||||
if (automation.trigger) {
|
if (automation.trigger) {
|
||||||
allSteps = [automation.trigger, ...allSteps]
|
allSteps = [automation.trigger, ...allSteps]
|
||||||
}
|
}
|
||||||
const blockIdx = allSteps.findIndex(step => step.id === block.id)
|
let blockIdx = allSteps.findIndex(step => step.id === block.id)
|
||||||
|
|
||||||
// Extract all outputs from all previous steps as available bindings
|
// Extract all outputs from all previous steps as available bindins
|
||||||
let bindings = []
|
let bindings = []
|
||||||
for (let idx = 0; idx < blockIdx; idx++) {
|
for (let idx = 0; idx < blockIdx; idx++) {
|
||||||
const outputs = Object.entries(
|
let wasLoopBlock = allSteps[idx]?.stepId === "LOOP"
|
||||||
allSteps[idx].schema?.outputs?.properties ?? {}
|
let isLoopBlock =
|
||||||
)
|
allSteps[idx]?.stepId === "LOOP" &&
|
||||||
|
allSteps.find(x => x.blockToLoop === block.id)
|
||||||
|
|
||||||
|
// If the previous block was a loop block, decerement the index so the following
|
||||||
|
// steps are in the correct order
|
||||||
|
if (wasLoopBlock) {
|
||||||
|
blockIdx--
|
||||||
|
}
|
||||||
|
|
||||||
|
let schema = allSteps[idx]?.schema?.outputs?.properties ?? {}
|
||||||
|
|
||||||
|
// If its a Loop Block, we need to add this custom schema
|
||||||
|
if (isLoopBlock) {
|
||||||
|
schema = {
|
||||||
|
currentItem: {
|
||||||
|
type: "string",
|
||||||
|
description: "the item currently being executed",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const outputs = Object.entries(schema)
|
||||||
|
|
||||||
bindings = bindings.concat(
|
bindings = bindings.concat(
|
||||||
outputs.map(([name, value]) => {
|
outputs.map(([name, value]) => {
|
||||||
const runtime = idx === 0 ? `trigger.${name}` : `steps.${idx}.${name}`
|
let runtimeName = isLoopBlock
|
||||||
|
? `loop.${name}`
|
||||||
|
: block.name.startsWith("JS")
|
||||||
|
? `steps[${idx}].${name}`
|
||||||
|
: `steps.${idx}.${name}`
|
||||||
|
const runtime = idx === 0 ? `trigger.${name}` : runtimeName
|
||||||
return {
|
return {
|
||||||
label: runtime,
|
label: runtime,
|
||||||
type: value.type,
|
type: value.type,
|
||||||
description: value.description,
|
description: value.description,
|
||||||
category: idx === 0 ? "Trigger outputs" : `Step ${idx} outputs`,
|
category:
|
||||||
|
idx === 0
|
||||||
|
? "Trigger outputs"
|
||||||
|
: isLoopBlock
|
||||||
|
? "Loop Outputs"
|
||||||
|
: `Step ${idx} outputs`,
|
||||||
path: runtime,
|
path: runtime,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,6 +226,7 @@
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
{bindings}
|
{bindings}
|
||||||
fillWidth
|
fillWidth
|
||||||
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
|
@ -205,6 +238,7 @@
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
{bindings}
|
{bindings}
|
||||||
allowJS={false}
|
allowJS={false}
|
||||||
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if value.customType === "query"}
|
{:else if value.customType === "query"}
|
||||||
|
@ -261,6 +295,14 @@
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
/>
|
/>
|
||||||
</CodeEditorModal>
|
</CodeEditorModal>
|
||||||
|
{:else if value.customType === "loopOption"}
|
||||||
|
<Select
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
autoWidth
|
||||||
|
value={inputData[key]}
|
||||||
|
options={["Array", "String"]}
|
||||||
|
defaultValue={"Array"}
|
||||||
|
/>
|
||||||
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
||||||
{#if isTestModal}
|
{#if isTestModal}
|
||||||
<ModalBindableInput
|
<ModalBindableInput
|
||||||
|
@ -270,6 +312,7 @@
|
||||||
type={value.customType}
|
type={value.customType}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="test">
|
<div class="test">
|
||||||
|
@ -281,6 +324,7 @@
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -43,6 +43,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const coerce = (value, type) => {
|
const coerce = (value, type) => {
|
||||||
|
const re = new RegExp(/{{([^{].*?)}}/g)
|
||||||
|
if (re.test(value)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
if (type === "boolean") {
|
if (type === "boolean") {
|
||||||
if (typeof value === "boolean") {
|
if (typeof value === "boolean") {
|
||||||
return value
|
return value
|
||||||
|
@ -120,6 +125,7 @@
|
||||||
{bindings}
|
{bindings}
|
||||||
fillWidth={true}
|
fillWidth={true}
|
||||||
allowJS={true}
|
allowJS={true}
|
||||||
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if !rowControl}
|
{:else if !rowControl}
|
||||||
|
@ -137,6 +143,7 @@
|
||||||
{bindings}
|
{bindings}
|
||||||
fillWidth={true}
|
fillWidth={true}
|
||||||
allowJS={true}
|
allowJS={true}
|
||||||
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -60,5 +60,6 @@
|
||||||
{bindings}
|
{bindings}
|
||||||
fillWidth={true}
|
fillWidth={true}
|
||||||
allowJS={true}
|
allowJS={true}
|
||||||
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -30,6 +30,10 @@
|
||||||
label: "DateTime",
|
label: "DateTime",
|
||||||
value: "datetime",
|
value: "datetime",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Array",
|
||||||
|
value: "array",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
function addField() {
|
function addField() {
|
||||||
|
@ -70,6 +74,7 @@
|
||||||
secondary
|
secondary
|
||||||
placeholder="Enter field name"
|
placeholder="Enter field name"
|
||||||
on:change={fieldNameChanged(field.name)}
|
on:change={fieldNameChanged(field.name)}
|
||||||
|
updateOnChange={false}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={field.type}
|
value={field.type}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||||
import { Pagination } from "@budibase/bbui"
|
import { Pagination, Heading, Body, Layout } from "@budibase/bbui"
|
||||||
import { fetchData } from "@budibase/frontend-core"
|
import { fetchData } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
|
@ -27,6 +27,8 @@
|
||||||
$: enrichedSchema = enrichSchema($tables.selected?.schema)
|
$: enrichedSchema = enrichSchema($tables.selected?.schema)
|
||||||
$: id = $tables.selected?._id
|
$: id = $tables.selected?._id
|
||||||
$: fetch = createFetch(id)
|
$: fetch = createFetch(id)
|
||||||
|
$: hasCols = checkHasCols(schema)
|
||||||
|
$: hasRows = !!$fetch.rows?.length
|
||||||
|
|
||||||
const enrichSchema = schema => {
|
const enrichSchema = schema => {
|
||||||
let tempSchema = { ...schema }
|
let tempSchema = { ...schema }
|
||||||
|
@ -47,6 +49,20 @@
|
||||||
|
|
||||||
return tempSchema
|
return tempSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkHasCols = schema => {
|
||||||
|
if (!schema || Object.keys(schema).length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let fields = Object.values(schema)
|
||||||
|
for (let field of fields) {
|
||||||
|
if (!field.autocolumn) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Fetches new data whenever the table changes
|
// Fetches new data whenever the table changes
|
||||||
const createFetch = tableId => {
|
const createFetch = tableId => {
|
||||||
return fetchData({
|
return fetchData({
|
||||||
|
@ -104,19 +120,28 @@
|
||||||
disableSorting
|
disableSorting
|
||||||
on:updatecolumns={onUpdateColumns}
|
on:updatecolumns={onUpdateColumns}
|
||||||
on:updaterows={onUpdateRows}
|
on:updaterows={onUpdateRows}
|
||||||
|
customPlaceholder
|
||||||
>
|
>
|
||||||
<CreateColumnButton on:updatecolumns={onUpdateColumns} />
|
<div class="buttons">
|
||||||
{#if schema && Object.keys(schema).length > 0}
|
<div class="left-buttons">
|
||||||
|
<CreateColumnButton
|
||||||
|
highlighted={$fetch.loaded && (!hasCols || !hasRows)}
|
||||||
|
on:updatecolumns={onUpdateColumns}
|
||||||
|
/>
|
||||||
{#if !isUsersTable}
|
{#if !isUsersTable}
|
||||||
<CreateRowButton
|
<CreateRowButton
|
||||||
on:updaterows={onUpdateRows}
|
on:updaterows={onUpdateRows}
|
||||||
title={"Create row"}
|
title={"Create row"}
|
||||||
modalContentComponent={CreateEditRow}
|
modalContentComponent={CreateEditRow}
|
||||||
|
disabled={!hasCols}
|
||||||
|
highlighted={$fetch.loaded && hasCols && !hasRows}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if isInternal}
|
{#if isInternal}
|
||||||
<CreateViewButton />
|
<CreateViewButton disabled={!hasCols || !hasRows} />
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="right-buttons">
|
||||||
<ManageAccessButton resourceId={$tables.selected?._id} />
|
<ManageAccessButton resourceId={$tables.selected?._id} />
|
||||||
{#if isUsersTable}
|
{#if isUsersTable}
|
||||||
<EditRolesButton />
|
<EditRolesButton />
|
||||||
|
@ -128,16 +153,40 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<HideAutocolumnButton bind:hideAutocolumns />
|
<HideAutocolumnButton bind:hideAutocolumns />
|
||||||
<!-- always have the export last -->
|
|
||||||
<ExportButton view={$tables.selected?._id} />
|
|
||||||
<ImportButton
|
<ImportButton
|
||||||
tableId={$tables.selected?._id}
|
tableId={$tables.selected?._id}
|
||||||
on:updaterows={onUpdateRows}
|
on:updaterows={onUpdateRows}
|
||||||
/>
|
/>
|
||||||
|
<ExportButton
|
||||||
|
disabled={!hasRows || !hasCols}
|
||||||
|
view={$tables.selected?._id}
|
||||||
|
/>
|
||||||
{#key id}
|
{#key id}
|
||||||
<TableFilterButton {schema} on:change={onFilter} />
|
<TableFilterButton
|
||||||
|
{schema}
|
||||||
|
on:change={onFilter}
|
||||||
|
disabled={!hasCols || !hasRows}
|
||||||
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div slot="placeholder">
|
||||||
|
<Layout gap="S">
|
||||||
|
{#if !hasCols}
|
||||||
|
<Heading>Let's create some columns</Heading>
|
||||||
|
<Body>
|
||||||
|
Start building out your table structure<br />
|
||||||
|
by adding some columns
|
||||||
|
</Body>
|
||||||
|
{:else}
|
||||||
|
<Heading>Now let's add a row</Heading>
|
||||||
|
<Body>
|
||||||
|
Add some data to your table<br />
|
||||||
|
by adding some rows
|
||||||
|
</Body>
|
||||||
{/if}
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
</Table>
|
</Table>
|
||||||
{#key id}
|
{#key id}
|
||||||
<div in:fade={{ delay: 200, duration: 100 }}>
|
<div in:fade={{ delay: 200, duration: 100 }}>
|
||||||
|
@ -162,4 +211,20 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: var(--spacing-xl);
|
margin-top: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
.buttons {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.left-buttons,
|
||||||
|
.right-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -34,10 +34,10 @@
|
||||||
$: label = meta.name ? capitalise(meta.name) : ""
|
$: label = meta.name ? capitalise(meta.name) : ""
|
||||||
|
|
||||||
const timeStamp = resolveTimeStamp(value)
|
const timeStamp = resolveTimeStamp(value)
|
||||||
const isTimeStamp = timeStamp ? true : false
|
const isTimeStamp = !!timeStamp
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if type === "options"}
|
{#if type === "options" && meta.constraints.inclusion.length !== 0}
|
||||||
<Select
|
<Select
|
||||||
{label}
|
{label}
|
||||||
data-cy="{meta.name}-select"
|
data-cy="{meta.name}-select"
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
<Dropzone {label} bind:value />
|
<Dropzone {label} bind:value />
|
||||||
{:else if type === "boolean"}
|
{:else if type === "boolean"}
|
||||||
<Toggle text={label} bind:value data-cy="{meta.name}-input" />
|
<Toggle text={label} bind:value data-cy="{meta.name}-input" />
|
||||||
{:else if type === "array"}
|
{:else if type === "array" && meta.constraints.inclusion.length !== 0}
|
||||||
<Multiselect bind:value {label} options={meta.constraints.inclusion} />
|
<Multiselect bind:value {label} options={meta.constraints.inclusion} />
|
||||||
{:else if type === "link"}
|
{:else if type === "link"}
|
||||||
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
export let rowCount
|
export let rowCount
|
||||||
export let type
|
export let type
|
||||||
export let disableSorting = false
|
export let disableSorting = false
|
||||||
|
export let customPlaceholder = false
|
||||||
|
|
||||||
let selectedRows = []
|
let selectedRows = []
|
||||||
let editableColumn
|
let editableColumn
|
||||||
|
@ -117,10 +118,10 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
<div>
|
<Layout noPadding gap="XS">
|
||||||
{#if title}
|
{#if title}
|
||||||
<div class="table-title">
|
<div class="table-title">
|
||||||
<Heading size="S">{title}</Heading>
|
<Heading size="M">{title}</Heading>
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div transition:fade|local>
|
<div transition:fade|local>
|
||||||
<Spinner size="10" />
|
<Spinner size="10" />
|
||||||
|
@ -134,7 +135,7 @@
|
||||||
<DeleteRowsButton on:updaterows {selectedRows} {deleteRows} />
|
<DeleteRowsButton on:updaterows {selectedRows} {deleteRows} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Layout>
|
||||||
{#key tableId}
|
{#key tableId}
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<Table
|
<Table
|
||||||
|
@ -144,6 +145,7 @@
|
||||||
{customRenderers}
|
{customRenderers}
|
||||||
{rowCount}
|
{rowCount}
|
||||||
{disableSorting}
|
{disableSorting}
|
||||||
|
{customPlaceholder}
|
||||||
bind:selectedRows
|
bind:selectedRows
|
||||||
allowSelectRows={allowEditing && !isUsersTable}
|
allowSelectRows={allowEditing && !isUsersTable}
|
||||||
allowEditRows={allowEditing}
|
allowEditRows={allowEditing}
|
||||||
|
@ -153,7 +155,9 @@
|
||||||
on:editrow={e => editRow(e.detail)}
|
on:editrow={e => editRow(e.detail)}
|
||||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||||
on:sort
|
on:sort
|
||||||
/>
|
>
|
||||||
|
<slot slot="placeholder" name="placeholder" />
|
||||||
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -176,6 +180,7 @@
|
||||||
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);
|
||||||
|
|
|
@ -2,10 +2,21 @@
|
||||||
import { ActionButton, Modal } from "@budibase/bbui"
|
import { ActionButton, Modal } from "@budibase/bbui"
|
||||||
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
|
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
|
||||||
|
|
||||||
|
export let highlighted = false
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="TableColumnAddRight" quiet size="S" on:click={modal.show}>
|
<ActionButton
|
||||||
|
{disabled}
|
||||||
|
selected={highlighted}
|
||||||
|
emphasized={highlighted}
|
||||||
|
icon="TableColumnAddRight"
|
||||||
|
quiet
|
||||||
|
size="S"
|
||||||
|
on:click={modal.show}
|
||||||
|
>
|
||||||
Create column
|
Create column
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -4,11 +4,21 @@
|
||||||
|
|
||||||
export let modalContentComponent = CreateEditRow
|
export let modalContentComponent = CreateEditRow
|
||||||
export let title = "Create row"
|
export let title = "Create row"
|
||||||
|
export let disabled = false
|
||||||
|
export let highlighted = false
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="TableRowAddBottom" size="S" quiet on:click={modal.show}>
|
<ActionButton
|
||||||
|
{disabled}
|
||||||
|
emphasized={highlighted}
|
||||||
|
selected={highlighted}
|
||||||
|
icon="TableRowAddBottom"
|
||||||
|
size="S"
|
||||||
|
quiet
|
||||||
|
on:click={modal.show}
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -2,10 +2,18 @@
|
||||||
import { Modal, ActionButton } from "@budibase/bbui"
|
import { Modal, ActionButton } from "@budibase/bbui"
|
||||||
import CreateViewModal from "../modals/CreateViewModal.svelte"
|
import CreateViewModal from "../modals/CreateViewModal.svelte"
|
||||||
|
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="CollectionAdd" size="S" quiet on:click={modal.show}>
|
<ActionButton
|
||||||
|
{disabled}
|
||||||
|
icon="CollectionAdd"
|
||||||
|
size="S"
|
||||||
|
quiet
|
||||||
|
on:click={modal.show}
|
||||||
|
>
|
||||||
Create view
|
Create view
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -3,11 +3,18 @@
|
||||||
import ExportModal from "../modals/ExportModal.svelte"
|
import ExportModal from "../modals/ExportModal.svelte"
|
||||||
|
|
||||||
export let view
|
export let view
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="DataDownload" size="S" quiet on:click={modal.show}>
|
<ActionButton
|
||||||
|
{disabled}
|
||||||
|
icon="DataDownload"
|
||||||
|
size="S"
|
||||||
|
quiet
|
||||||
|
on:click={modal.show}
|
||||||
|
>
|
||||||
Export
|
Export
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -8,6 +8,12 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="MagicWand" primary size="S" quiet on:click={hideOrUnhide}>
|
<ActionButton
|
||||||
{#if hideAutocolumns}Show auto columns{:else}Hide auto columns{/if}
|
icon={hideAutocolumns ? "VisibilityOff" : "Visibility"}
|
||||||
|
primary
|
||||||
|
size="S"
|
||||||
|
quiet
|
||||||
|
on:click={hideOrUnhide}
|
||||||
|
>
|
||||||
|
Auto columns
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
export let schema
|
export let schema
|
||||||
export let filters
|
export let filters
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let modal
|
let modal
|
||||||
|
@ -17,6 +18,7 @@
|
||||||
icon="Filter"
|
icon="Filter"
|
||||||
size="S"
|
size="S"
|
||||||
quiet
|
quiet
|
||||||
|
{disabled}
|
||||||
on:click={modal.show}
|
on:click={modal.show}
|
||||||
active={tempValue?.length > 0}
|
active={tempValue?.length > 0}
|
||||||
>
|
>
|
||||||
|
|
|
@ -60,6 +60,7 @@ export function getBindings({
|
||||||
)
|
)
|
||||||
|
|
||||||
const label = path == null ? column : `${path}.0.${column}`
|
const label = path == null ? column : `${path}.0.${column}`
|
||||||
|
const binding = path == null ? `[${column}]` : `${path}.0.[${column}]`
|
||||||
// only supply a description for relationship paths
|
// only supply a description for relationship paths
|
||||||
const description =
|
const description =
|
||||||
path == null
|
path == null
|
||||||
|
@ -73,8 +74,8 @@ export function getBindings({
|
||||||
description,
|
description,
|
||||||
// don't include path, it messes things up, relationship path
|
// don't include path, it messes things up, relationship path
|
||||||
// will be replaced by the main array binding
|
// will be replaced by the main array binding
|
||||||
readableBinding: column,
|
readableBinding: label,
|
||||||
runtimeBinding: `[${column}]`,
|
runtimeBinding: binding,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return bindings
|
return bindings
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import GoogleButton from "../_components/GoogleButton.svelte"
|
|
||||||
|
|
||||||
export let datasource
|
export let datasource
|
||||||
export let save
|
export let save
|
||||||
|
@ -161,11 +160,6 @@
|
||||||
Fetch tables
|
Fetch tables
|
||||||
</Button>
|
</Button>
|
||||||
<Button cta icon="Add" on:click={createNewTable}>New table</Button>
|
<Button cta icon="Add" on:click={createNewTable}>New table</Button>
|
||||||
{#if integration.auth}
|
|
||||||
{#if integration.auth.type === "google"}
|
|
||||||
<GoogleButton {datasource} />
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Body>
|
<Body>
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
<script>
|
||||||
|
export let width = "100"
|
||||||
|
export let height = "100"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
{width}
|
||||||
|
{height}
|
||||||
|
viewBox="0 0 256 220"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M245.97 168.943C232.308 176.064 161.536 205.163 146.469 213.018C131.402 220.874 123.032 220.798 111.129 215.108C99.227 209.418 23.913 178.996 10.346 172.511C3.566 169.271 0 166.535 0 163.951V138.075C0 138.075 98.05 116.73 113.879 111.051C129.707 105.372 135.199 105.167 148.669 110.101C162.141 115.037 242.687 129.569 256 134.445L255.994 159.955C255.996 162.513 252.924 165.319 245.97 168.943"
|
||||||
|
fill="#912626"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M245.965 143.22C232.304 150.338 161.534 179.438 146.467 187.292C131.401 195.149 123.031 195.072 111.129 189.382C99.226 183.696 23.915 153.269 10.349 146.788C-3.21698 140.303 -3.50098 135.84 9.82502 130.622C23.151 125.402 98.049 96.017 113.88 90.338C129.708 84.661 135.199 84.454 148.669 89.39C162.14 94.324 232.488 122.325 245.799 127.2C259.115 132.081 259.626 136.1 245.965 143.22"
|
||||||
|
fill="#C6302B"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M245.97 127.074C232.308 134.196 161.536 163.294 146.469 171.152C131.402 179.005 123.032 178.929 111.129 173.239C99.226 167.552 23.913 137.127 10.346 130.642C3.566 127.402 0 124.67 0 122.085V96.206C0 96.206 98.05 74.862 113.879 69.183C129.707 63.504 135.199 63.298 148.669 68.233C162.142 73.168 242.688 87.697 256 92.574L255.994 118.087C255.996 120.644 252.924 123.45 245.97 127.074Z"
|
||||||
|
fill="#912626"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M245.965 101.351C232.304 108.471 161.534 137.569 146.467 145.426C131.401 153.28 123.031 153.203 111.129 147.513C99.226 141.827 23.915 111.401 10.349 104.919C-3.21698 98.436 -3.50098 93.972 9.82502 88.752C23.151 83.535 98.05 54.148 113.88 48.47C129.708 42.792 135.199 42.586 148.669 47.521C162.14 52.455 232.488 80.454 245.799 85.331C259.115 90.211 259.626 94.231 245.965 101.351"
|
||||||
|
fill="#C6302B"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M245.97 83.653C232.308 90.773 161.536 119.873 146.469 127.731C131.402 135.585 123.032 135.508 111.129 129.818C99.226 124.131 23.913 93.705 10.346 87.223C3.566 83.98 0 81.247 0 78.665V52.785C0 52.785 98.05 31.442 113.879 25.764C129.707 20.084 135.199 19.88 148.669 24.814C162.142 29.749 242.688 44.278 256 49.155L255.994 74.667C255.996 77.222 252.924 80.028 245.97 83.653Z"
|
||||||
|
fill="#912626"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M245.965 57.93C232.304 65.05 161.534 94.15 146.467 102.004C131.401 109.858 123.031 109.781 111.129 104.094C99.227 98.404 23.915 67.98 10.35 61.497C-3.21699 55.015 -3.49999 50.55 9.82501 45.331C23.151 40.113 98.05 10.73 113.88 5.04999C129.708 -0.629006 135.199 -0.833006 148.669 4.10199C162.14 9.03699 232.488 37.036 245.799 41.913C259.115 46.789 259.626 50.81 245.965 57.93"
|
||||||
|
fill="#C6302B"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M159.283 32.757L137.273 35.042L132.346 46.898L124.388 33.668L98.9729 31.384L117.937 24.545L112.247 14.047L130.002 20.991L146.74 15.511L142.216 26.366L159.283 32.757V32.757ZM131.032 90.275L89.9549 73.238L148.815 64.203L131.032 90.275V90.275ZM74.0819 39.347C91.4569 39.347 105.542 44.807 105.542 51.541C105.542 58.277 91.4569 63.736 74.0819 63.736C56.7069 63.736 42.6219 58.276 42.6219 51.541C42.6219 44.807 56.7069 39.347 74.0819 39.347"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M185.295 35.998L220.131 49.764L185.325 63.517L185.295 35.997"
|
||||||
|
fill="#621B1C"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M146.755 51.243L185.295 35.998L185.325 63.517L181.546 64.995L146.755 51.243Z"
|
||||||
|
fill="#9A2928"
|
||||||
|
/>
|
||||||
|
</svg>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue