commit
7165ee43a7
|
@ -0,0 +1,9 @@
|
||||||
|
packages/server/node_modules
|
||||||
|
packages/builder
|
||||||
|
packages/frontend-core
|
||||||
|
packages/backend-core
|
||||||
|
packages/worker/node_modules
|
||||||
|
packages/cli
|
||||||
|
packages/client
|
||||||
|
packages/bbui
|
||||||
|
packages/string-templates
|
|
@ -93,6 +93,8 @@ then `cd ` into your local copy.
|
||||||
|
|
||||||
#### 3. Install and Build
|
#### 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 }}
|
||||||
|
|
|
@ -98,10 +98,6 @@ spec:
|
||||||
value: http://worker-service:{{ .Values.services.worker.port }}
|
value: http://worker-service:{{ .Values.services.worker.port }}
|
||||||
- name: PLATFORM_URL
|
- name: PLATFORM_URL
|
||||||
value: {{ .Values.globals.platformUrl | quote }}
|
value: {{ .Values.globals.platformUrl | quote }}
|
||||||
- name: USE_QUOTAS
|
|
||||||
value: {{ .Values.globals.useQuotas | quote }}
|
|
||||||
- name: EXCLUDE_QUOTAS_TENANTS
|
|
||||||
value: {{ .Values.globals.excludeQuotasTenants | quote }}
|
|
||||||
- name: ACCOUNT_PORTAL_URL
|
- name: ACCOUNT_PORTAL_URL
|
||||||
value: {{ .Values.globals.accountPortalUrl | quote }}
|
value: {{ .Values.globals.accountPortalUrl | quote }}
|
||||||
- name: ACCOUNT_PORTAL_API_KEY
|
- name: ACCOUNT_PORTAL_API_KEY
|
||||||
|
@ -114,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: {}
|
||||||
|
|
|
@ -95,16 +95,15 @@ globals:
|
||||||
logLevel: info
|
logLevel: info
|
||||||
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
|
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
|
||||||
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
|
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
|
||||||
useQuotas: "0"
|
|
||||||
excludeQuotasTenants: "" # comma seperated list of tenants to exclude from quotas
|
|
||||||
accountPortalUrl: ""
|
accountPortalUrl: ""
|
||||||
accountPortalApiKey: ""
|
accountPortalApiKey: ""
|
||||||
cookieDomain: ""
|
cookieDomain: ""
|
||||||
platformUrl: ""
|
platformUrl: ""
|
||||||
httpMigrations: "0"
|
httpMigrations: "0"
|
||||||
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
|
||||||
|
|
||||||
|
@ -152,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
|
||||||
|
@ -163,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:
|
||||||
|
@ -232,6 +241,8 @@ couchdb:
|
||||||
## Optional tolerations
|
## Optional tolerations
|
||||||
tolerations: []
|
tolerations: []
|
||||||
|
|
||||||
|
affinity: {}
|
||||||
|
|
||||||
service:
|
service:
|
||||||
# annotations:
|
# annotations:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
|
@ -1894,9 +1894,9 @@ minimist-options@4.1.0:
|
||||||
kind-of "^6.0.3"
|
kind-of "^6.0.3"
|
||||||
|
|
||||||
minimist@^1.2.0:
|
minimist@^1.2.0:
|
||||||
version "1.2.5"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||||
|
|
||||||
minipass-collect@^1.0.2:
|
minipass-collect@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -117,7 +117,6 @@ services:
|
||||||
labels:
|
labels:
|
||||||
- "com.centurylinklabs.watchtower.enable=false"
|
- "com.centurylinklabs.watchtower.enable=false"
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
couchdb3_data:
|
couchdb3_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
{
|
||||||
|
"version": "2",
|
||||||
|
"templates": [
|
||||||
|
{
|
||||||
|
"type": 3,
|
||||||
|
"title": "Budibase",
|
||||||
|
"categories": ["Tools"],
|
||||||
|
"description": "Build modern business apps in minutes",
|
||||||
|
"logo": "https://budibase.com/favicon.ico",
|
||||||
|
"platform": "linux",
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/Budibase/budibase",
|
||||||
|
"stackfile": "hosting/docker-compose.yaml"
|
||||||
|
},
|
||||||
|
"env": [
|
||||||
|
{
|
||||||
|
"name": "MAIN_PORT",
|
||||||
|
"label": "Main port",
|
||||||
|
"default": "10000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "JWT_SECRET",
|
||||||
|
"label": "JWT secret",
|
||||||
|
"default": "change-me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MINIO_ACCESS_KEY",
|
||||||
|
"label": "MinIO access key",
|
||||||
|
"default": "change-me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MINIO_SECRET_KEY",
|
||||||
|
"label": "MinIO secret key",
|
||||||
|
"default": "change-me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "COUCH_DB_USER",
|
||||||
|
"default": "budibase",
|
||||||
|
"preset": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "COUCH_DB_PASSWORD",
|
||||||
|
"label": "Couch DB password",
|
||||||
|
"default": "change-me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "REDIS_PASSWORD",
|
||||||
|
"label": "Redis password",
|
||||||
|
"default": "change-me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "INTERNAL_API_KEY",
|
||||||
|
"label": "Internal API key",
|
||||||
|
"default": "change-me"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "APP_PORT",
|
||||||
|
"default": "4002",
|
||||||
|
"preset": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WORKER_PORT",
|
||||||
|
"default": "4003",
|
||||||
|
"preset": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MINIO_PORT",
|
||||||
|
"default": "4004",
|
||||||
|
"preset": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "COUCH_DB_PORT",
|
||||||
|
"default": "4005",
|
||||||
|
"preset": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "REDIS_PORT",
|
||||||
|
"default": "6379",
|
||||||
|
"preset": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WATCHTOWER_PORT",
|
||||||
|
"default": "6161",
|
||||||
|
"preset": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BUDIBASE_ENVIRONMENT",
|
||||||
|
"default": "PRODUCTION",
|
||||||
|
"preset": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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>
|
|
@ -0,0 +1,97 @@
|
||||||
|
FROM couchdb
|
||||||
|
|
||||||
|
ENV COUCHDB_PASSWORD=budibase
|
||||||
|
ENV COUCHDB_USER=budibase
|
||||||
|
ENV COUCH_DB_URL=http://budibase:budibase@localhost:5984
|
||||||
|
ENV BUDIBASE_ENVIRONMENT=PRODUCTION
|
||||||
|
ENV MINIO_URL=http://localhost:9000
|
||||||
|
ENV REDIS_URL=localhost:6379
|
||||||
|
ENV WORKER_URL=http://localhost:4002
|
||||||
|
ENV INTERNAL_API_KEY=budibase
|
||||||
|
ENV JWT_SECRET=testsecret
|
||||||
|
ENV MINIO_ACCESS_KEY=budibase
|
||||||
|
ENV MINIO_SECRET_KEY=budibase
|
||||||
|
ENV SELF_HOSTED=1
|
||||||
|
ENV CLUSTER_PORT=10000
|
||||||
|
ENV REDIS_PASSWORD=budibase
|
||||||
|
ENV ARCHITECTURE=amd
|
||||||
|
ENV APP_PORT=4001
|
||||||
|
ENV WORKER_PORT=4002
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install software-properties-common wget nginx -y
|
||||||
|
RUN apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main'
|
||||||
|
RUN apt-get update
|
||||||
|
|
||||||
|
# setup nginx
|
||||||
|
ADD hosting/single/nginx.conf /etc/nginx
|
||||||
|
RUN mkdir /etc/nginx/logs
|
||||||
|
RUN useradd www
|
||||||
|
RUN touch /etc/nginx/logs/error.log
|
||||||
|
RUN touch /etc/nginx/logs/nginx.pid
|
||||||
|
|
||||||
|
# install java
|
||||||
|
RUN apt-get install openjdk-8-jdk -y
|
||||||
|
|
||||||
|
# setup nodejs
|
||||||
|
WORKDIR /nodejs
|
||||||
|
RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh
|
||||||
|
RUN bash /tmp/nodesource_setup.sh
|
||||||
|
RUN apt-get install nodejs
|
||||||
|
RUN npm install --global yarn
|
||||||
|
RUN npm install --global pm2
|
||||||
|
|
||||||
|
# setup redis
|
||||||
|
RUN apt install redis-server -y
|
||||||
|
|
||||||
|
# setup server
|
||||||
|
WORKDIR /app
|
||||||
|
ADD packages/server .
|
||||||
|
RUN ls -al
|
||||||
|
RUN yarn
|
||||||
|
RUN yarn build
|
||||||
|
# Install client for oracle datasource
|
||||||
|
RUN apt-get install unzip libaio1
|
||||||
|
RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh
|
||||||
|
|
||||||
|
# setup worker
|
||||||
|
WORKDIR /worker
|
||||||
|
ADD packages/worker .
|
||||||
|
RUN yarn
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
# setup clouseau
|
||||||
|
WORKDIR /
|
||||||
|
RUN wget https://github.com/cloudant-labs/clouseau/releases/download/2.21.0/clouseau-2.21.0-dist.zip
|
||||||
|
RUN unzip clouseau-2.21.0-dist.zip
|
||||||
|
RUN mv clouseau-2.21.0 /opt/clouseau
|
||||||
|
RUN rm clouseau-2.21.0-dist.zip
|
||||||
|
|
||||||
|
WORKDIR /opt/clouseau
|
||||||
|
RUN mkdir ./bin
|
||||||
|
ADD hosting/single/clouseau ./bin/
|
||||||
|
ADD hosting/single/log4j.properties .
|
||||||
|
ADD hosting/single/clouseau.ini .
|
||||||
|
RUN chmod +x ./bin/clouseau
|
||||||
|
|
||||||
|
# setup CouchDB
|
||||||
|
WORKDIR /opt/couchdb
|
||||||
|
ADD hosting/single/vm.args ./etc/
|
||||||
|
|
||||||
|
# setup minio
|
||||||
|
WORKDIR /minio
|
||||||
|
RUN wget https://dl.min.io/server/minio/release/linux-${ARCHITECTURE}64/minio
|
||||||
|
RUN chmod +x minio
|
||||||
|
|
||||||
|
# setup runner file
|
||||||
|
WORKDIR /
|
||||||
|
ADD hosting/single/runner.sh .
|
||||||
|
RUN chmod +x ./runner.sh
|
||||||
|
|
||||||
|
EXPOSE 10000
|
||||||
|
VOLUME /opt/couchdb/data
|
||||||
|
VOLUME /minio
|
||||||
|
|
||||||
|
# must set this just before running
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
CMD ["./runner.sh"]
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/sh
|
||||||
|
/usr/bin/java -server \
|
||||||
|
-Xmx2G \
|
||||||
|
-Dsun.net.inetaddr.ttl=30 \
|
||||||
|
-Dsun.net.inetaddr.negative.ttl=30 \
|
||||||
|
-Dlog4j.configuration=file:/opt/clouseau/log4j.properties \
|
||||||
|
-XX:OnOutOfMemoryError="kill -9 %p" \
|
||||||
|
-XX:+UseConcMarkSweepGC \
|
||||||
|
-XX:+CMSParallelRemarkEnabled \
|
||||||
|
-classpath '/opt/clouseau/*' \
|
||||||
|
com.cloudant.clouseau.Main \
|
||||||
|
/opt/clouseau/clouseau.ini
|
|
@ -0,0 +1,13 @@
|
||||||
|
[clouseau]
|
||||||
|
|
||||||
|
; the name of the Erlang node created by the service, leave this unchanged
|
||||||
|
name=clouseau@127.0.0.1
|
||||||
|
|
||||||
|
; set this to the same distributed Erlang cookie used by the CouchDB nodes
|
||||||
|
cookie=monster
|
||||||
|
|
||||||
|
; the path where you would like to store the search index files
|
||||||
|
dir=/opt/couchdb/data/search
|
||||||
|
|
||||||
|
; the number of search indexes that can be open simultaneously
|
||||||
|
max_indexes_open=500
|
|
@ -0,0 +1,4 @@
|
||||||
|
log4j.rootLogger=debug, CONSOLE
|
||||||
|
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
|
||||||
|
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
|
||||||
|
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %c [%p] %m%n
|
|
@ -0,0 +1,116 @@
|
||||||
|
user www www;
|
||||||
|
error_log /etc/nginx/logs/error.log;
|
||||||
|
pid /etc/nginx/logs/nginx.pid;
|
||||||
|
worker_processes auto;
|
||||||
|
worker_rlimit_nofile 8192;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
charset utf-8;
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
server_tokens off;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
|
||||||
|
# buffering
|
||||||
|
client_header_buffer_size 1k;
|
||||||
|
client_max_body_size 20M;
|
||||||
|
ignore_invalid_headers off;
|
||||||
|
proxy_buffering off;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 10000 default_server;
|
||||||
|
listen [::]:10000 default_server;
|
||||||
|
server_name _;
|
||||||
|
client_max_body_size 1000m;
|
||||||
|
ignore_invalid_headers off;
|
||||||
|
proxy_buffering off;
|
||||||
|
# port_in_redirect off;
|
||||||
|
|
||||||
|
location /app {
|
||||||
|
proxy_pass http://127.0.0.1:4001;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = / {
|
||||||
|
proxy_pass http://127.0.0.1:4001;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/(builder|app_) {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_pass http://127.0.0.1:4001;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/api/(system|admin|global)/ {
|
||||||
|
proxy_pass http://127.0.0.1:4002;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /worker/ {
|
||||||
|
proxy_pass http://127.0.0.1:4002;
|
||||||
|
rewrite ^/worker/(.*)$ /$1 break;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
# calls to the API are rate limited with bursting
|
||||||
|
limit_req zone=ratelimit burst=20 nodelay;
|
||||||
|
|
||||||
|
# 120s timeout on API requests
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
proxy_connect_timeout 120s;
|
||||||
|
proxy_send_timeout 120s;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
proxy_pass http://127.0.0.1:4001;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /db/ {
|
||||||
|
proxy_pass http://127.0.0.1:5984;
|
||||||
|
rewrite ^/db/(.*)$ /$1 break;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_connect_timeout 300;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
chunked_transfer_encoding off;
|
||||||
|
proxy_pass http://127.0.0.1:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
client_header_timeout 60;
|
||||||
|
client_body_timeout 60;
|
||||||
|
keepalive_timeout 60;
|
||||||
|
|
||||||
|
# gzip
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
redis-server --requirepass $REDIS_PASSWORD &
|
||||||
|
/opt/clouseau/bin/clouseau &
|
||||||
|
/minio/minio server /minio &
|
||||||
|
/docker-entrypoint.sh /opt/couchdb/bin/couchdb &
|
||||||
|
/etc/init.d/nginx restart
|
||||||
|
pushd app
|
||||||
|
pm2 start --name app "yarn run:docker"
|
||||||
|
popd
|
||||||
|
pushd worker
|
||||||
|
pm2 start --name worker "yarn run:docker"
|
||||||
|
popd
|
||||||
|
sleep 10
|
||||||
|
URL=http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984
|
||||||
|
curl -X PUT ${URL}/_users
|
||||||
|
curl -X PUT ${URL}/_replicator
|
||||||
|
sleep infinity
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
# use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
# the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations under
|
||||||
|
# the License.
|
||||||
|
|
||||||
|
# erlang cookie for clouseau security
|
||||||
|
-name couchdb@127.0.0.1
|
||||||
|
-setcookie monster
|
||||||
|
|
||||||
|
# Ensure that the Erlang VM listens on a known port
|
||||||
|
-kernel inet_dist_listen_min 9100
|
||||||
|
-kernel inet_dist_listen_max 9100
|
||||||
|
|
||||||
|
# Tell kernel and SASL not to log anything
|
||||||
|
-kernel error_logger silent
|
||||||
|
-sasl sasl_error_logger false
|
||||||
|
|
||||||
|
# Use kernel poll functionality if supported by emulator
|
||||||
|
+K true
|
||||||
|
|
||||||
|
# Start a pool of asynchronous IO threads
|
||||||
|
+A 16
|
||||||
|
|
||||||
|
# Comment this line out to enable the interactive Erlang shell on startup
|
||||||
|
+Bd -noinput
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.123",
|
"version": "1.0.124-alpha.0",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
14
package.json
14
package.json
|
@ -21,18 +21,17 @@
|
||||||
},
|
},
|
||||||
"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",
|
||||||
"publishdev": "lerna run publishdev",
|
"release": "lerna publish patch --yes --force-publish && yarn release:pro",
|
||||||
"publishnpm": "yarn build && lerna publish --force-publish",
|
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop && yarn release:pro:develop",
|
||||||
"release": "lerna publish patch --yes --force-publish",
|
"release:pro": "bash scripts/pro/release.sh",
|
||||||
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop",
|
"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",
|
||||||
"nuke:docker": "lerna run --parallel dev:stack:nuke",
|
"nuke:docker": "lerna run --parallel dev:stack:nuke",
|
||||||
"clean": "lerna clean",
|
"clean": "lerna clean",
|
||||||
"kill-port": "kill-port 4001",
|
|
||||||
"kill-builder": "kill-port 3000",
|
"kill-builder": "kill-port 3000",
|
||||||
"kill-server": "kill-port 4001 4002",
|
"kill-server": "kill-port 4001 4002",
|
||||||
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
||||||
|
@ -58,6 +57,8 @@
|
||||||
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
||||||
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||||
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
|
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
|
||||||
|
"build:docker:single:image": "docker build -f hosting/single/Dockerfile -t budibase:latest .",
|
||||||
|
"build:docker:single": "lerna run build && lerna run predocker && npm run build:docker:single:image",
|
||||||
"build:docs": "lerna run build:docs",
|
"build:docs": "lerna run build:docs",
|
||||||
"release:helm": "node scripts/releaseHelmChart",
|
"release:helm": "node scripts/releaseHelmChart",
|
||||||
"env:multi:enable": "lerna run env:multi:enable",
|
"env:multi:enable": "lerna run env:multi:enable",
|
||||||
|
@ -73,6 +74,7 @@
|
||||||
"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"
|
"dep:clean": "yarn clean && yarn bootstrap"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.0.123",
|
"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",
|
||||||
|
|
|
@ -13,9 +13,11 @@ exports.Cookies = {
|
||||||
|
|
||||||
exports.Headers = {
|
exports.Headers = {
|
||||||
API_KEY: "x-budibase-api-key",
|
API_KEY: "x-budibase-api-key",
|
||||||
|
LICENSE_KEY: "x-budibase-license-key",
|
||||||
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",
|
||||||
|
|
|
@ -71,7 +71,9 @@ exports.doInTenant = (tenantId, task) => {
|
||||||
// set the tenant id
|
// set the tenant id
|
||||||
if (!opts.existing) {
|
if (!opts.existing) {
|
||||||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
||||||
exports.setGlobalDB(tenantId)
|
if (env.USE_COUCH) {
|
||||||
|
exports.setGlobalDB(tenantId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -80,7 +82,9 @@ exports.doInTenant = (tenantId, task) => {
|
||||||
} finally {
|
} finally {
|
||||||
const using = cls.getFromContext(ContextKeys.IN_USE)
|
const using = cls.getFromContext(ContextKeys.IN_USE)
|
||||||
if (!using || using <= 1) {
|
if (!using || using <= 1) {
|
||||||
await closeDB(exports.getGlobalDB())
|
if (env.USE_COUCH) {
|
||||||
|
await closeDB(exports.getGlobalDB())
|
||||||
|
}
|
||||||
// clear from context now that database is closed/task is finished
|
// clear from context now that database is closed/task is finished
|
||||||
cls.setOnContext(ContextKeys.TENANT_ID, null)
|
cls.setOnContext(ContextKeys.TENANT_ID, null)
|
||||||
cls.setOnContext(ContextKeys.GLOBAL_DB, null)
|
cls.setOnContext(ContextKeys.GLOBAL_DB, null)
|
||||||
|
@ -167,6 +171,7 @@ exports.doInAppContext = (appId, task) => {
|
||||||
|
|
||||||
exports.updateTenantId = tenantId => {
|
exports.updateTenantId = tenantId => {
|
||||||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
||||||
|
exports.setGlobalDB(tenantId)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.updateAppId = async appId => {
|
exports.updateAppId = async appId => {
|
||||||
|
@ -269,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
|
||||||
|
|
|
@ -23,6 +23,7 @@ exports.StaticDatabases = {
|
||||||
docs: {
|
docs: {
|
||||||
apiKeys: "apikeys",
|
apiKeys: "apikeys",
|
||||||
usageQuota: "usage_quota",
|
usageQuota: "usage_quota",
|
||||||
|
licenseInfo: "license_info",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// contains information about tenancy and so on
|
// contains information about tenancy and so on
|
||||||
|
|
|
@ -28,6 +28,7 @@ const UNICODE_MAX = "\ufff0"
|
||||||
exports.ViewNames = {
|
exports.ViewNames = {
|
||||||
USER_BY_EMAIL: "by_email",
|
USER_BY_EMAIL: "by_email",
|
||||||
BY_API_KEY: "by_api_key",
|
BY_API_KEY: "by_api_key",
|
||||||
|
USER_BY_BUILDERS: "by_builders",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.StaticDatabases = StaticDatabases
|
exports.StaticDatabases = StaticDatabases
|
||||||
|
@ -414,35 +415,10 @@ async function getScopedConfig(db, params) {
|
||||||
return configDoc && configDoc.config ? configDoc.config : configDoc
|
return configDoc && configDoc.config ? configDoc.config : configDoc
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateNewUsageQuotaDoc() {
|
|
||||||
return {
|
|
||||||
_id: StaticDatabases.GLOBAL.docs.usageQuota,
|
|
||||||
quotaReset: Date.now() + 2592000000,
|
|
||||||
usageQuota: {
|
|
||||||
automationRuns: 0,
|
|
||||||
rows: 0,
|
|
||||||
storage: 0,
|
|
||||||
apps: 0,
|
|
||||||
users: 0,
|
|
||||||
views: 0,
|
|
||||||
emails: 0,
|
|
||||||
},
|
|
||||||
usageLimits: {
|
|
||||||
automationRuns: 1000,
|
|
||||||
rows: 4000,
|
|
||||||
apps: 4,
|
|
||||||
storage: 1000,
|
|
||||||
users: 10,
|
|
||||||
emails: 50,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.Replication = Replication
|
exports.Replication = Replication
|
||||||
exports.getScopedConfig = getScopedConfig
|
exports.getScopedConfig = getScopedConfig
|
||||||
exports.generateConfigID = generateConfigID
|
exports.generateConfigID = generateConfigID
|
||||||
exports.getConfigParams = getConfigParams
|
exports.getConfigParams = getConfigParams
|
||||||
exports.getScopedFullConfig = getScopedFullConfig
|
exports.getScopedFullConfig = getScopedFullConfig
|
||||||
exports.generateNewUsageQuotaDoc = generateNewUsageQuotaDoc
|
|
||||||
exports.generateDevInfoID = generateDevInfoID
|
exports.generateDevInfoID = generateDevInfoID
|
||||||
exports.getPlatformUrl = getPlatformUrl
|
exports.getPlatformUrl = getPlatformUrl
|
||||||
|
|
|
@ -56,10 +56,34 @@ exports.createApiKeyView = async () => {
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.createUserBuildersView = async () => {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
let designDoc
|
||||||
|
try {
|
||||||
|
designDoc = await db.get("_design/database")
|
||||||
|
} catch (err) {
|
||||||
|
// no design doc, make one
|
||||||
|
designDoc = DesignDoc()
|
||||||
|
}
|
||||||
|
const view = {
|
||||||
|
map: `function(doc) {
|
||||||
|
if (doc.builder && doc.builder.global === true) {
|
||||||
|
emit(doc._id, doc._id)
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
}
|
||||||
|
designDoc.views = {
|
||||||
|
...designDoc.views,
|
||||||
|
[ViewNames.USER_BY_BUILDERS]: view,
|
||||||
|
}
|
||||||
|
await db.put(designDoc)
|
||||||
|
}
|
||||||
|
|
||||||
exports.queryGlobalView = async (viewName, params, db = null) => {
|
exports.queryGlobalView = async (viewName, params, db = null) => {
|
||||||
const CreateFuncByName = {
|
const CreateFuncByName = {
|
||||||
[ViewNames.USER_BY_EMAIL]: exports.createUserEmailView,
|
[ViewNames.USER_BY_EMAIL]: exports.createUserEmailView,
|
||||||
[ViewNames.BY_API_KEY]: exports.createApiKeyView,
|
[ViewNames.BY_API_KEY]: exports.createApiKeyView,
|
||||||
|
[ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView,
|
||||||
}
|
}
|
||||||
// can pass DB in if working with something specific
|
// can pass DB in if working with something specific
|
||||||
if (!db) {
|
if (!db) {
|
||||||
|
|
|
@ -22,15 +22,26 @@ 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),
|
||||||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
||||||
PLATFORM_URL: process.env.PLATFORM_URL,
|
PLATFORM_URL: process.env.PLATFORM_URL,
|
||||||
|
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
|
||||||
module.exports[key] = value
|
module.exports[key] = value
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clean up any environment variable edge cases
|
||||||
|
for (let [key, value] of Object.entries(module.exports)) {
|
||||||
|
// handle the edge case of "0" to disable an environment variable
|
||||||
|
if (value === "0") {
|
||||||
|
module.exports[key] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
class BudibaseError extends Error {
|
||||||
|
constructor(message, type, code) {
|
||||||
|
super(message)
|
||||||
|
this.type = type
|
||||||
|
this.code = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
BudibaseError,
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
const licensing = require("./licensing")
|
||||||
|
|
||||||
|
const codes = {
|
||||||
|
...licensing.codes,
|
||||||
|
}
|
||||||
|
|
||||||
|
const types = {
|
||||||
|
...licensing.types,
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
...licensing.context,
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPublicError = err => {
|
||||||
|
let error
|
||||||
|
if (err.code || err.type) {
|
||||||
|
// add generic error information
|
||||||
|
error = {
|
||||||
|
code: err.code,
|
||||||
|
type: err.type,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.code && context[err.code]) {
|
||||||
|
error = {
|
||||||
|
...error,
|
||||||
|
// get any additional context from this error
|
||||||
|
...context[err.code](err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
codes,
|
||||||
|
types,
|
||||||
|
UsageLimitError: licensing.UsageLimitError,
|
||||||
|
getPublicError,
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
const { BudibaseError } = require("./base")
|
||||||
|
|
||||||
|
const types = {
|
||||||
|
LICENSE_ERROR: "license_error",
|
||||||
|
}
|
||||||
|
|
||||||
|
const codes = {
|
||||||
|
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
[codes.USAGE_LIMIT_EXCEEDED]: err => {
|
||||||
|
return {
|
||||||
|
limitName: err.limitName,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
class UsageLimitError extends BudibaseError {
|
||||||
|
constructor(message, limitName) {
|
||||||
|
super(message, types.LICENSE_ERROR, codes.USAGE_LIMIT_EXCEEDED)
|
||||||
|
this.limitName = limitName
|
||||||
|
this.status = 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
types,
|
||||||
|
codes,
|
||||||
|
context,
|
||||||
|
UsageLimitError,
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
const env = require("../environment")
|
||||||
|
const tenancy = require("../tenancy")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant.
|
||||||
|
* The env var is formatted as:
|
||||||
|
* tenant1:feature1:feature2,tenant2:feature1
|
||||||
|
*/
|
||||||
|
const getFeatureFlags = () => {
|
||||||
|
if (!env.TENANT_FEATURE_FLAGS) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenantFeatureFlags = {}
|
||||||
|
|
||||||
|
env.TENANT_FEATURE_FLAGS.split(",").forEach(tenantToFeatures => {
|
||||||
|
const [tenantId, ...features] = tenantToFeatures.split(":")
|
||||||
|
|
||||||
|
features.forEach(feature => {
|
||||||
|
if (!tenantFeatureFlags[tenantId]) {
|
||||||
|
tenantFeatureFlags[tenantId] = []
|
||||||
|
}
|
||||||
|
tenantFeatureFlags[tenantId].push(feature)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return tenantFeatureFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
const TENANT_FEATURE_FLAGS = getFeatureFlags()
|
||||||
|
|
||||||
|
exports.isEnabled = featureFlag => {
|
||||||
|
const tenantId = tenancy.getTenantId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
TENANT_FEATURE_FLAGS &&
|
||||||
|
TENANT_FEATURE_FLAGS[tenantId] &&
|
||||||
|
TENANT_FEATURE_FLAGS[tenantId].includes(featureFlag)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getTenantFeatureFlags = tenantId => {
|
||||||
|
if (TENANT_FEATURE_FLAGS && TENANT_FEATURE_FLAGS[tenantId]) {
|
||||||
|
return TENANT_FEATURE_FLAGS[tenantId]
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.FeatureFlag = {
|
||||||
|
LICENSING: "LICENSING",
|
||||||
|
}
|
|
@ -15,7 +15,9 @@ module.exports = {
|
||||||
auth: require("../auth"),
|
auth: require("../auth"),
|
||||||
constants: require("../constants"),
|
constants: require("../constants"),
|
||||||
migrations: require("../migrations"),
|
migrations: require("../migrations"),
|
||||||
|
errors: require("./errors"),
|
||||||
env: require("./environment"),
|
env: require("./environment"),
|
||||||
accounts: require("./cloud/accounts"),
|
accounts: require("./cloud/accounts"),
|
||||||
tenancy: require("./tenancy"),
|
tenancy: require("./tenancy"),
|
||||||
|
featureFlags: require("./featureFlags"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,24 +2,27 @@ const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
||||||
|
|
||||||
const { authenticateThirdParty } = require("./third-party-common")
|
const { authenticateThirdParty } = require("./third-party-common")
|
||||||
|
|
||||||
async function authenticate(accessToken, refreshToken, profile, done) {
|
const buildVerifyFn = saveUserFn => {
|
||||||
const thirdPartyUser = {
|
return (accessToken, refreshToken, profile, done) => {
|
||||||
provider: profile.provider, // should always be 'google'
|
const thirdPartyUser = {
|
||||||
providerType: "google",
|
provider: profile.provider, // should always be 'google'
|
||||||
userId: profile.id,
|
providerType: "google",
|
||||||
profile: profile,
|
userId: profile.id,
|
||||||
email: profile._json.email,
|
profile: profile,
|
||||||
oauth2: {
|
email: profile._json.email,
|
||||||
accessToken: accessToken,
|
oauth2: {
|
||||||
refreshToken: refreshToken,
|
accessToken: accessToken,
|
||||||
},
|
refreshToken: refreshToken,
|
||||||
}
|
},
|
||||||
|
}
|
||||||
|
|
||||||
return authenticateThirdParty(
|
return authenticateThirdParty(
|
||||||
thirdPartyUser,
|
thirdPartyUser,
|
||||||
true, // require local accounts to exist
|
true, // require local accounts to exist
|
||||||
done
|
done,
|
||||||
)
|
saveUserFn
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,11 +30,7 @@ async function authenticate(accessToken, refreshToken, profile, done) {
|
||||||
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
||||||
* @returns Dynamically configured Passport Google Strategy
|
* @returns Dynamically configured Passport Google Strategy
|
||||||
*/
|
*/
|
||||||
exports.strategyFactory = async function (
|
exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
|
||||||
config,
|
|
||||||
callbackUrl,
|
|
||||||
verify = authenticate
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const { clientID, clientSecret } = config
|
const { clientID, clientSecret } = config
|
||||||
|
|
||||||
|
@ -41,6 +40,7 @@ exports.strategyFactory = async function (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const verify = buildVerifyFn(saveUserFn)
|
||||||
return new GoogleStrategy(
|
return new GoogleStrategy(
|
||||||
{
|
{
|
||||||
clientID: config.clientID,
|
clientID: config.clientID,
|
||||||
|
@ -58,4 +58,4 @@ exports.strategyFactory = async function (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// expose for testing
|
// expose for testing
|
||||||
exports.authenticate = authenticate
|
exports.buildVerifyFn = buildVerifyFn
|
||||||
|
|
|
@ -2,46 +2,49 @@ const fetch = require("node-fetch")
|
||||||
const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
|
const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
|
||||||
const { authenticateThirdParty } = require("./third-party-common")
|
const { authenticateThirdParty } = require("./third-party-common")
|
||||||
|
|
||||||
/**
|
const buildVerifyFn = saveUserFn => {
|
||||||
* @param {*} issuer The identity provider base URL
|
/**
|
||||||
* @param {*} sub The user ID
|
* @param {*} issuer The identity provider base URL
|
||||||
* @param {*} profile The user profile information. Created by passport from the /userinfo response
|
* @param {*} sub The user ID
|
||||||
* @param {*} jwtClaims The parsed id_token claims
|
* @param {*} profile The user profile information. Created by passport from the /userinfo response
|
||||||
* @param {*} accessToken The access_token for contacting the identity provider - may or may not be a JWT
|
* @param {*} jwtClaims The parsed id_token claims
|
||||||
* @param {*} refreshToken The refresh_token for obtaining a new access_token - usually not a JWT
|
* @param {*} accessToken The access_token for contacting the identity provider - may or may not be a JWT
|
||||||
* @param {*} idToken The id_token - always a JWT
|
* @param {*} refreshToken The refresh_token for obtaining a new access_token - usually not a JWT
|
||||||
* @param {*} params The response body from requesting an access_token
|
* @param {*} idToken The id_token - always a JWT
|
||||||
* @param {*} done The passport callback: err, user, info
|
* @param {*} params The response body from requesting an access_token
|
||||||
*/
|
* @param {*} done The passport callback: err, user, info
|
||||||
async function authenticate(
|
*/
|
||||||
issuer,
|
return async (
|
||||||
sub,
|
issuer,
|
||||||
profile,
|
sub,
|
||||||
jwtClaims,
|
profile,
|
||||||
accessToken,
|
jwtClaims,
|
||||||
refreshToken,
|
accessToken,
|
||||||
idToken,
|
refreshToken,
|
||||||
params,
|
idToken,
|
||||||
done
|
params,
|
||||||
) {
|
|
||||||
const thirdPartyUser = {
|
|
||||||
// store the issuer info to enable sync in future
|
|
||||||
provider: issuer,
|
|
||||||
providerType: "oidc",
|
|
||||||
userId: profile.id,
|
|
||||||
profile: profile,
|
|
||||||
email: getEmail(profile, jwtClaims),
|
|
||||||
oauth2: {
|
|
||||||
accessToken: accessToken,
|
|
||||||
refreshToken: refreshToken,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return authenticateThirdParty(
|
|
||||||
thirdPartyUser,
|
|
||||||
false, // don't require local accounts to exist
|
|
||||||
done
|
done
|
||||||
)
|
) => {
|
||||||
|
const thirdPartyUser = {
|
||||||
|
// store the issuer info to enable sync in future
|
||||||
|
provider: issuer,
|
||||||
|
providerType: "oidc",
|
||||||
|
userId: profile.id,
|
||||||
|
profile: profile,
|
||||||
|
email: getEmail(profile, jwtClaims),
|
||||||
|
oauth2: {
|
||||||
|
accessToken: accessToken,
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return authenticateThirdParty(
|
||||||
|
thirdPartyUser,
|
||||||
|
false, // don't require local accounts to exist
|
||||||
|
done,
|
||||||
|
saveUserFn
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -86,7 +89,7 @@ function validEmail(value) {
|
||||||
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
* from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport.
|
||||||
* @returns Dynamically configured Passport OIDC Strategy
|
* @returns Dynamically configured Passport OIDC Strategy
|
||||||
*/
|
*/
|
||||||
exports.strategyFactory = async function (config, callbackUrl) {
|
exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
|
||||||
try {
|
try {
|
||||||
const { clientID, clientSecret, configUrl } = config
|
const { clientID, clientSecret, configUrl } = config
|
||||||
|
|
||||||
|
@ -106,6 +109,7 @@ exports.strategyFactory = async function (config, callbackUrl) {
|
||||||
|
|
||||||
const body = await response.json()
|
const body = await response.json()
|
||||||
|
|
||||||
|
const verify = buildVerifyFn(saveUserFn)
|
||||||
return new OIDCStrategy(
|
return new OIDCStrategy(
|
||||||
{
|
{
|
||||||
issuer: body.issuer,
|
issuer: body.issuer,
|
||||||
|
@ -116,7 +120,7 @@ exports.strategyFactory = async function (config, callbackUrl) {
|
||||||
clientSecret: clientSecret,
|
clientSecret: clientSecret,
|
||||||
callbackURL: callbackUrl,
|
callbackURL: callbackUrl,
|
||||||
},
|
},
|
||||||
authenticate
|
verify
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
@ -125,4 +129,4 @@ exports.strategyFactory = async function (config, callbackUrl) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// expose for testing
|
// expose for testing
|
||||||
exports.authenticate = authenticate
|
exports.buildVerifyFn = buildVerifyFn
|
||||||
|
|
|
@ -58,8 +58,10 @@ describe("google", () => {
|
||||||
|
|
||||||
it("delegates authentication to third party common", async () => {
|
it("delegates authentication to third party common", async () => {
|
||||||
const google = require("../google")
|
const google = require("../google")
|
||||||
|
const mockSaveUserFn = jest.fn()
|
||||||
|
const authenticate = await google.buildVerifyFn(mockSaveUserFn)
|
||||||
|
|
||||||
await google.authenticate(
|
await authenticate(
|
||||||
data.accessToken,
|
data.accessToken,
|
||||||
data.refreshToken,
|
data.refreshToken,
|
||||||
profile,
|
profile,
|
||||||
|
@ -69,7 +71,8 @@ describe("google", () => {
|
||||||
expect(authenticateThirdParty).toHaveBeenCalledWith(
|
expect(authenticateThirdParty).toHaveBeenCalledWith(
|
||||||
user,
|
user,
|
||||||
true,
|
true,
|
||||||
mockDone)
|
mockDone,
|
||||||
|
mockSaveUserFn)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -83,8 +83,10 @@ describe("oidc", () => {
|
||||||
|
|
||||||
async function doAuthenticate() {
|
async function doAuthenticate() {
|
||||||
const oidc = require("../oidc")
|
const oidc = require("../oidc")
|
||||||
|
const mockSaveUserFn = jest.fn()
|
||||||
|
const authenticate = await oidc.buildVerifyFn(mockSaveUserFn)
|
||||||
|
|
||||||
await oidc.authenticate(
|
await authenticate(
|
||||||
issuer,
|
issuer,
|
||||||
sub,
|
sub,
|
||||||
profile,
|
profile,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
const env = require("../../environment")
|
const env = require("../../environment")
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
const { generateGlobalUserID } = require("../../db/utils")
|
const { generateGlobalUserID } = require("../../db/utils")
|
||||||
const { saveUser } = require("../../utils")
|
|
||||||
const { authError } = require("./utils")
|
const { authError } = require("./utils")
|
||||||
const { newid } = require("../../hashing")
|
const { newid } = require("../../hashing")
|
||||||
const { createASession } = require("../../security/sessions")
|
const { createASession } = require("../../security/sessions")
|
||||||
|
@ -16,8 +15,11 @@ exports.authenticateThirdParty = async function (
|
||||||
thirdPartyUser,
|
thirdPartyUser,
|
||||||
requireLocalAccount = true,
|
requireLocalAccount = true,
|
||||||
done,
|
done,
|
||||||
saveUserFn = saveUser
|
saveUserFn
|
||||||
) {
|
) {
|
||||||
|
if (!saveUserFn) {
|
||||||
|
throw new Error("Save user function must be provided")
|
||||||
|
}
|
||||||
if (!thirdPartyUser.provider) {
|
if (!thirdPartyUser.provider) {
|
||||||
return authError(done, "third party user provider required")
|
return authError(done, "third party user provider required")
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ exports.Databases = {
|
||||||
FLAGS: "flags",
|
FLAGS: "flags",
|
||||||
APP_METADATA: "appMetadata",
|
APP_METADATA: "appMetadata",
|
||||||
QUERY_VARS: "queryVars",
|
QUERY_VARS: "queryVars",
|
||||||
|
LICENSES: "license",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.SEPARATOR = SEPARATOR
|
exports.SEPARATOR = SEPARATOR
|
||||||
|
|
|
@ -14,22 +14,7 @@ function makeSessionID(userId, sessionId) {
|
||||||
return `${userId}/${sessionId}`
|
return `${userId}/${sessionId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createASession = async (userId, session) => {
|
async function invalidateSessions(userId, sessionIds = null) {
|
||||||
const client = await redis.getSessionClient()
|
|
||||||
const sessionId = session.sessionId
|
|
||||||
if (!session.csrfToken) {
|
|
||||||
session.csrfToken = uuidv4()
|
|
||||||
}
|
|
||||||
session = {
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
lastAccessedAt: new Date().toISOString(),
|
|
||||||
...session,
|
|
||||||
userId,
|
|
||||||
}
|
|
||||||
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.invalidateSessions = async (userId, sessionIds = null) => {
|
|
||||||
let sessions = []
|
let sessions = []
|
||||||
|
|
||||||
// If no sessionIds, get all the sessions for the user
|
// If no sessionIds, get all the sessions for the user
|
||||||
|
@ -55,6 +40,24 @@ exports.invalidateSessions = async (userId, sessionIds = null) => {
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.createASession = async (userId, session) => {
|
||||||
|
// invalidate all other sessions
|
||||||
|
await invalidateSessions(userId)
|
||||||
|
|
||||||
|
const client = await redis.getSessionClient()
|
||||||
|
const sessionId = session.sessionId
|
||||||
|
if (!session.csrfToken) {
|
||||||
|
session.csrfToken = uuidv4()
|
||||||
|
}
|
||||||
|
session = {
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
lastAccessedAt: new Date().toISOString(),
|
||||||
|
...session,
|
||||||
|
userId,
|
||||||
|
}
|
||||||
|
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
exports.updateSessionTTL = async session => {
|
exports.updateSessionTTL = async session => {
|
||||||
const client = await redis.getSessionClient()
|
const client = await redis.getSessionClient()
|
||||||
const key = makeSessionID(session.userId, session.sessionId)
|
const key = makeSessionID(session.userId, session.sessionId)
|
||||||
|
@ -67,8 +70,6 @@ exports.endSession = async (userId, sessionId) => {
|
||||||
await client.delete(makeSessionID(userId, sessionId))
|
await client.delete(makeSessionID(userId, sessionId))
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getUserSessions = getSessionsForUser
|
|
||||||
|
|
||||||
exports.getSession = async (userId, sessionId) => {
|
exports.getSession = async (userId, sessionId) => {
|
||||||
try {
|
try {
|
||||||
const client = await redis.getSessionClient()
|
const client = await redis.getSessionClient()
|
||||||
|
@ -84,3 +85,6 @@ exports.getAllSessions = async () => {
|
||||||
const sessions = await client.scan()
|
const sessions = await client.scan()
|
||||||
return sessions.map(session => session.value)
|
return sessions.map(session => session.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getUserSessions = getSessionsForUser
|
||||||
|
exports.invalidateSessions = invalidateSessions
|
||||||
|
|
|
@ -176,6 +176,27 @@ exports.getGlobalUserByEmail = async email => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getBuilders = async () => {
|
||||||
|
const builders = await queryGlobalView(ViewNames.USER_BY_BUILDERS, {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
exports.saveUser = async (
|
exports.saveUser = async (
|
||||||
user,
|
user,
|
||||||
tenantId,
|
tenantId,
|
||||||
|
@ -290,4 +311,5 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
|
||||||
userId,
|
userId,
|
||||||
sessions.map(({ sessionId }) => sessionId)
|
sessions.map(({ sessionId }) => sessionId)
|
||||||
)
|
)
|
||||||
|
await userCache.invalidateUser(userId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3411,9 +3411,9 @@ mimic-fn@^2.1.0:
|
||||||
brace-expansion "^1.1.7"
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
|
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
|
||||||
version "1.2.5"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||||
|
|
||||||
mixin-deep@^1.2.0:
|
mixin-deep@^1.2.0:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
|
|
|
@ -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.123",
|
"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.123",
|
"@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>
|
||||||
|
|
|
@ -19,18 +19,33 @@
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const flatpickrId = `${uuid()}-wrapper`
|
const flatpickrId = `${uuid()}-wrapper`
|
||||||
let open = false
|
let open = false
|
||||||
let flatpickr, flatpickrOptions, isTimeOnly
|
let flatpickr, flatpickrOptions
|
||||||
|
|
||||||
|
const resolveTimeStamp = timestamp => {
|
||||||
|
let maskedDate = new Date(`0-${timestamp}`)
|
||||||
|
|
||||||
|
if (maskedDate instanceof Date && !isNaN(maskedDate.getTime())) {
|
||||||
|
return maskedDate
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$: isTimeOnly = !timeOnly && value ? !isNaN(new Date(`0-${value}`)) : timeOnly
|
|
||||||
$: flatpickrOptions = {
|
$: flatpickrOptions = {
|
||||||
element: `#${flatpickrId}`,
|
element: `#${flatpickrId}`,
|
||||||
enableTime: isTimeOnly || enableTime || false,
|
enableTime: timeOnly || enableTime || false,
|
||||||
noCalendar: isTimeOnly || false,
|
noCalendar: timeOnly || false,
|
||||||
altInput: true,
|
altInput: true,
|
||||||
altFormat: isTimeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
|
altFormat: timeOnly ? "H:i" : enableTime ? "F j Y, H:i" : "F j, Y",
|
||||||
wrap: true,
|
wrap: true,
|
||||||
appendTo,
|
appendTo,
|
||||||
disableMobile: "true",
|
disableMobile: "true",
|
||||||
|
onReady: () => {
|
||||||
|
let timestamp = resolveTimeStamp(value)
|
||||||
|
if (timeOnly && timestamp) {
|
||||||
|
dispatch("change", timestamp.toISOString())
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = event => {
|
const handleChange = event => {
|
||||||
|
@ -39,10 +54,9 @@
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
newValue = newValue.toISOString()
|
newValue = newValue.toISOString()
|
||||||
}
|
}
|
||||||
// if time only set date component to today
|
// if time only set date component to 2000-01-01
|
||||||
if (timeOnly) {
|
if (timeOnly) {
|
||||||
const todayDate = new Date().toISOString().split("T")[0]
|
newValue = `2000-01-01T${newValue.split("T")[1]}`
|
||||||
newValue = `${todayDate}T${newValue.split("T")[1]}`
|
|
||||||
}
|
}
|
||||||
dispatch("change", newValue)
|
dispatch("change", newValue)
|
||||||
}
|
}
|
||||||
|
@ -76,10 +90,13 @@
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
let date
|
let date
|
||||||
let time = new Date(`0-${val}`)
|
let time
|
||||||
|
|
||||||
// it is a string like 00:00:00, just time
|
// it is a string like 00:00:00, just time
|
||||||
if (timeOnly || (typeof val === "string" && !isNaN(time))) {
|
let ts = resolveTimeStamp(val)
|
||||||
date = time
|
|
||||||
|
if (timeOnly && ts) {
|
||||||
|
date = ts
|
||||||
} else if (val instanceof Date) {
|
} else if (val instanceof Date) {
|
||||||
// Use real date obj if already parsed
|
// Use real date obj if already parsed
|
||||||
date = val
|
date = val
|
||||||
|
@ -101,7 +118,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key isTimeOnly}
|
{#key timeOnly}
|
||||||
<Flatpickr
|
<Flatpickr
|
||||||
bind:flatpickr
|
bind:flatpickr
|
||||||
value={parseDate(value)}
|
value={parseDate(value)}
|
||||||
|
|
|
@ -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,30 +13,52 @@
|
||||||
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>
|
||||||
|
|
||||||
<svg
|
<div
|
||||||
on:click
|
class="icon"
|
||||||
class:hoverable
|
on:mouseover={() => (showTooltip = true)}
|
||||||
class:disabled
|
on:focus={() => (showTooltip = true)}
|
||||||
class="spectrum-Icon spectrum-Icon--size{size}"
|
on:mouseleave={() => (showTooltip = false)}
|
||||||
focusable="false"
|
on:click={() => (showTooltip = false)}
|
||||||
aria-hidden={hidden}
|
|
||||||
aria-label={name}
|
|
||||||
style={`transform: rotate(${rotation}deg); ${
|
|
||||||
color ? `color: ${color};` : ""
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<use xlink:href="#spectrum-icon-18-{name}" />
|
<svg
|
||||||
</svg>
|
on:click
|
||||||
|
class:hoverable
|
||||||
|
class:disabled
|
||||||
|
class="spectrum-Icon spectrum-Icon--size{size}"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden={hidden}
|
||||||
|
aria-label={name}
|
||||||
|
style={`transform: rotate(${rotation}deg); ${
|
||||||
|
color ? `color: ${color};` : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<use style="pointer-events: none;" xlink:href="#spectrum-icon-18-{name}" />
|
||||||
|
</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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
export let wide = false
|
export let wide = false
|
||||||
|
export let maxWidth = "80ch"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class:wide>
|
<div style="--max-width: {maxWidth}" class:wide>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -12,7 +13,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
max-width: 80ch;
|
max-width: var(--max-width);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: calc(var(--spacing-xl) * 2);
|
padding: calc(var(--spacing-xl) * 2);
|
||||||
min-height: calc(100% - var(--spacing-xl) * 4);
|
min-height: calc(100% - var(--spacing-xl) * 4);
|
||||||
|
|
|
@ -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}
|
||||||
>
|
>
|
||||||
{title}
|
{#if title}
|
||||||
<slot name="header" />
|
{title}
|
||||||
|
{:else if $$slots.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>
|
||||||
|
|
|
@ -16,11 +16,11 @@
|
||||||
easing: easing,
|
easing: easing,
|
||||||
})
|
})
|
||||||
|
|
||||||
$: if (value) $progress = value
|
$: if (value || value === 0) $progress = value
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class:spectrum-ProgressBar--indeterminate={!value}
|
class:spectrum-ProgressBar--indeterminate={!value && value !== 0}
|
||||||
class:spectrum-ProgressBar--sideLabel={sideLabel}
|
class:spectrum-ProgressBar--sideLabel={sideLabel}
|
||||||
class="spectrum-ProgressBar spectrum-ProgressBar--size{size}"
|
class="spectrum-ProgressBar spectrum-ProgressBar--size{size}"
|
||||||
value={$progress}
|
value={$progress}
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
aria-valuenow={$progress}
|
aria-valuenow={$progress}
|
||||||
aria-valuemin="0"
|
aria-valuemin="0"
|
||||||
aria-valuemax="100"
|
aria-valuemax="100"
|
||||||
style={width ? `width: ${width}px;` : ""}
|
style={width ? `width: ${width};` : ""}
|
||||||
>
|
>
|
||||||
{#if $$slots}
|
{#if $$slots}
|
||||||
<div
|
<div
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if value}
|
{#if value || value === 0}
|
||||||
<div
|
<div
|
||||||
class="spectrum-FieldLabel spectrum-ProgressBar-percentage spectrum-FieldLabel--size{size}"
|
class="spectrum-FieldLabel spectrum-ProgressBar-percentage spectrum-FieldLabel--size{size}"
|
||||||
>
|
>
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
<div class="spectrum-ProgressBar-track">
|
<div class="spectrum-ProgressBar-track">
|
||||||
<div
|
<div
|
||||||
class="spectrum-ProgressBar-fill"
|
class="spectrum-ProgressBar-fill"
|
||||||
style={value ? `width: ${$progress}%` : ""}
|
style={value || value === 0 ? `width: ${$progress}%` : ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="spectrum-ProgressBar-label" hidden="" />
|
<div class="spectrum-ProgressBar-label" hidden="" />
|
||||||
|
|
|
@ -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)
|
||||||
|
notifications.success("Copied to clipboard")
|
||||||
const copyToClipboard = value => {
|
} catch (error) {
|
||||||
return new Promise(res => {
|
notifications.error(
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
"Failed to copy to clipboard. Check the dev console for the value."
|
||||||
// Try using the clipboard API first
|
)
|
||||||
navigator.clipboard.writeText(value).then(res)
|
console.warn("Failed to copy the value", value)
|
||||||
} else {
|
}
|
||||||
// Fall back to the textarea hack
|
|
||||||
let textArea = document.createElement("textarea")
|
|
||||||
textArea.value = value
|
|
||||||
textArea.style.position = "fixed"
|
|
||||||
textArea.style.left = "-9999px"
|
|
||||||
textArea.style.top = "-9999px"
|
|
||||||
document.body.appendChild(textArea)
|
|
||||||
textArea.focus()
|
|
||||||
textArea.select()
|
|
||||||
document.execCommand("copy")
|
|
||||||
textArea.remove()
|
|
||||||
res()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
notifications.success("Copied to clipboard")
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
notifications.error(
|
|
||||||
"Failed to copy to clipboard. Check the dev console for the value."
|
|
||||||
)
|
|
||||||
console.warn("Failed to copy the value", value)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</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
|
||||||
<div class="placeholder-content">
|
class="placeholder"
|
||||||
<svg class="spectrum-Icon spectrum-Icon--sizeXXL" focusable="false">
|
class:placeholder--custom={customPlaceholder}
|
||||||
<use xlink:href="#spectrum-icon-18-Table" />
|
class:placeholder--no-fields={!fields?.length}
|
||||||
</svg>
|
>
|
||||||
<div>No rows found</div>
|
{#if customPlaceholder}
|
||||||
</div>
|
<slot name="placeholder" />
|
||||||
|
{:else}
|
||||||
|
<div class="placeholder-content">
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--sizeXXL"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-Table" />
|
||||||
|
</svg>
|
||||||
|
<div>No rows found</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</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);
|
||||||
|
|
|
@ -5,12 +5,14 @@
|
||||||
export let serif = false
|
export let serif = false
|
||||||
export let weight = null
|
export let weight = null
|
||||||
export let textAlign = null
|
export let textAlign = null
|
||||||
|
export let color = null
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
style={`
|
style={`
|
||||||
${weight ? `font-weight:${weight};` : ""}
|
${weight ? `font-weight:${weight};` : ""}
|
||||||
${textAlign ? `text-align:${textAlign};` : ""}
|
${textAlign ? `text-align:${textAlign};` : ""}
|
||||||
|
${color ? `color:${color};` : ""}
|
||||||
`}
|
`}
|
||||||
class="spectrum-Body spectrum-Body--size{size}"
|
class="spectrum-Body spectrum-Body--size{size}"
|
||||||
class:spectrum-Body--serif={serif}
|
class:spectrum-Body--serif={serif}
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
export let textAlign
|
export let textAlign
|
||||||
export let noPadding = false
|
export let noPadding = false
|
||||||
|
export let weight = "default" // light, heavy, default
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1
|
<h1
|
||||||
style={textAlign ? `text-align:${textAlign}` : ``}
|
style={textAlign ? `text-align:${textAlign}` : ``}
|
||||||
class:noPadding
|
class:noPadding
|
||||||
class="spectrum-Heading spectrum-Heading--size{size}"
|
class="spectrum-Heading spectrum-Heading--size{size} spectrum-Heading--{weight}"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -1574,9 +1574,9 @@ minimatch@^3.0.4:
|
||||||
brace-expansion "^1.1.7"
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
minimist@^1.2.0, minimist@^1.2.5:
|
minimist@^1.2.0, minimist@^1.2.5:
|
||||||
version "1.2.5"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||||
|
|
||||||
mkdirp@~0.5.1:
|
mkdirp@~0.5.1:
|
||||||
version "0.5.5"
|
version "0.5.5"
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,51 +1,103 @@
|
||||||
import filterTests from "../support/filterTests"
|
import filterTests from "../support/filterTests"
|
||||||
|
|
||||||
filterTests(['smoke', 'all'], () => {
|
filterTests(['smoke', 'all'], () => {
|
||||||
context("Auto Screens UI", () => {
|
context("Auto Screens UI", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
cy.createTestApp()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should generate internal table screens", () => {
|
|
||||||
// Create autogenerated screens from the internal table
|
|
||||||
cy.createAutogeneratedScreens(["Cypress Tests"])
|
|
||||||
// Confirm screens have been auto generated
|
|
||||||
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true })
|
|
||||||
cy.get(".nav-items-container").should('contain', 'cypress-tests/:id')
|
|
||||||
.and('contain', 'cypress-tests/new/row')
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should generate multiple internal table screens at once", () => {
|
|
||||||
// Create a second internal table
|
|
||||||
const initialTable = "Cypress Tests"
|
|
||||||
const secondTable = "Table Two"
|
|
||||||
cy.createTable(secondTable)
|
|
||||||
// Create autogenerated screens from the internal tables
|
|
||||||
cy.createAutogeneratedScreens([initialTable, secondTable])
|
|
||||||
// Confirm screens have been auto generated
|
|
||||||
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true })
|
|
||||||
// Previously generated tables are suffixed with numbers - as expected
|
|
||||||
cy.get(".nav-items-container").should('contain', 'cypress-tests-2/:id')
|
|
||||||
.and('contain', 'cypress-tests-2/new/row')
|
|
||||||
cy.get(".nav-items-container").contains("table-two").click()
|
|
||||||
cy.get(".nav-items-container").should('contain', 'table-two/:id')
|
|
||||||
.and('contain', 'table-two/new/row')
|
|
||||||
})
|
|
||||||
|
|
||||||
if (Cypress.env("TEST_ENV")) {
|
|
||||||
it("should generate data source screens", () => {
|
|
||||||
// Using MySQL data source for testing this
|
|
||||||
const datasource = "MySQL"
|
|
||||||
// Select & configure MySQL data source
|
|
||||||
cy.selectExternalDatasource(datasource)
|
|
||||||
cy.addDatasourceConfig(datasource)
|
|
||||||
// Create autogenerated screens from a MySQL table - MySQL contains books table
|
|
||||||
cy.createAutogeneratedScreens(["books"])
|
|
||||||
cy.get(".nav-items-container").contains("books").click()
|
|
||||||
cy.get(".nav-items-container").should('contain', 'books/:id')
|
|
||||||
.and('contain', 'books/new/row')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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", () => {
|
||||||
|
cy.createTestApp()
|
||||||
|
// Create Autogenerated screens from the internal table
|
||||||
|
cy.createDatasourceScreen(["Cypress Tests"])
|
||||||
|
// Confirm screens have been auto generated
|
||||||
|
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true })
|
||||||
|
cy.get(".nav-items-container").should('contain', 'cypress-tests/:id')
|
||||||
|
.and('contain', 'cypress-tests/new/row')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should generate multiple internal table screens at once", () => {
|
||||||
|
// Create a second internal table
|
||||||
|
const initialTable = "Cypress Tests"
|
||||||
|
const secondTable = "Table Two"
|
||||||
|
cy.createTable(secondTable)
|
||||||
|
// Create Autogenerated screens from the internal tables
|
||||||
|
cy.createDatasourceScreen([initialTable, secondTable])
|
||||||
|
// Confirm screens have been auto generated
|
||||||
|
cy.get(".nav-items-container").contains("cypress-tests").click({ force: true })
|
||||||
|
// Previously generated tables are suffixed with numbers - as expected
|
||||||
|
cy.get(".nav-items-container").should('contain', 'cypress-tests-2/:id')
|
||||||
|
.and('contain', 'cypress-tests-2/new/row')
|
||||||
|
cy.get(".nav-items-container").contains("table-two").click()
|
||||||
|
cy.get(".nav-items-container").should('contain', 'table-two/:id')
|
||||||
|
.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")) {
|
||||||
|
it("should generate data source screens", () => {
|
||||||
|
// Using MySQL data source for testing this
|
||||||
|
const datasource = "MySQL"
|
||||||
|
// Select & configure MySQL data source
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
cy.addDatasourceConfig(datasource)
|
||||||
|
// Create Autogenerated screens from a MySQL table - MySQL contains books table
|
||||||
|
cy.createDatasourceScreen(["books"])
|
||||||
|
|
||||||
|
cy.get(".nav-items-container").contains("books").click()
|
||||||
|
cy.get(".nav-items-container").should('contain', 'books/:id')
|
||||||
|
.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`)
|
||||||
cy.get(".spectrum-Button").contains("Templates").click({force: true})
|
.its("body")
|
||||||
}
|
.then(val => {
|
||||||
|
if (val.length > 0) {
|
||||||
|
cy.get(".spectrum-Button").contains("Templates").click({force: true})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
cy.get(".template-category-filters").should("exist")
|
cy.get(".template-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,12 +31,11 @@ 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")
|
||||||
.first()
|
.first()
|
||||||
.type("{{ trigger.row.name }}", { parseSpecialCharSequences: false })
|
.type("{{ trigger.row.name }}", { parseSpecialCharSequences: false })
|
||||||
cy.get(".spectrum-Textfield-input")
|
cy.get(".spectrum-Textfield-input")
|
||||||
.eq(1)
|
.eq(1)
|
||||||
.type("11")
|
.type("11")
|
||||||
|
|
|
@ -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,23 +205,21 @@ 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")
|
.contains(queryName)
|
||||||
.last()
|
.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()
|
||||||
cy.get(".spectrum-Button")
|
cy.get(".spectrum-Button")
|
||||||
.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,23 +235,21 @@ 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")
|
.contains(queryName)
|
||||||
.last()
|
.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()
|
||||||
cy.get(".spectrum-Button")
|
cy.get(".spectrum-Button")
|
||||||
.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,30 +99,32 @@ 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) {
|
||||||
// Should not have Edit as option, will unpublish app
|
// Should not have Edit as option, will unpublish app
|
||||||
cy.should("not.have.value", "Edit")
|
cy.should("not.have.value", "Edit")
|
||||||
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||||
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-Modal")
|
cy.get(".spectrum-Menu-item").contains("Edit").click({ force: true })
|
||||||
.within(() => {
|
})
|
||||||
if (noName == true) {
|
cy.get(".spectrum-Modal")
|
||||||
cy.get("input").clear()
|
.within(() => {
|
||||||
cy.get(".spectrum-Dialog-grid").click()
|
if (noName == true) {
|
||||||
.contains("App name must be letters, numbers and spaces only")
|
|
||||||
return cy
|
|
||||||
}
|
|
||||||
cy.get("input").clear()
|
cy.get("input").clear()
|
||||||
cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur()
|
cy.get(".spectrum-Dialog-grid").click()
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true })
|
.contains("App name must be letters, numbers and spaces only")
|
||||||
cy.wait(500)
|
return cy
|
||||||
})
|
}
|
||||||
}
|
cy.get("input").clear()
|
||||||
|
cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur()
|
||||||
|
cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true })
|
||||||
|
cy.wait(500)
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
cy.createTable("Cypress Tests", true)
|
if (shouldCreateDefaultTable) {
|
||||||
|
cy.createTable("Cypress Tests", true)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("deleteApp", name => {
|
Cypress.Commands.add("deleteApp", name => {
|
||||||
|
@ -60,43 +72,48 @@ 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 => {
|
||||||
if (val.length > 0) {
|
const findAppName = val.some(val => val.name == name)
|
||||||
if (Cypress.env("TEST_ENV")) {
|
if (findAppName) {
|
||||||
cy.searchForApplication(name)
|
if (val.length > 0) {
|
||||||
cy.get(".appTable").within(() => {
|
if (Cypress.env("TEST_ENV")) {
|
||||||
cy.get(".spectrum-Icon").eq(1).click()
|
cy.searchForApplication(name)
|
||||||
|
cy.get(".appTable").within(() => {
|
||||||
|
cy.get(".spectrum-Icon").eq(1).click()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const appId = val.reduce((acc, app) => {
|
||||||
|
if (name === app.name) {
|
||||||
|
acc = app.appId
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, "")
|
||||||
|
|
||||||
|
if (appId == "") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const appIdParsed = appId.split("_").pop()
|
||||||
|
const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
|
||||||
|
cy.get(actionEleId).within(() => {
|
||||||
|
cy.get(".spectrum-Icon").eq(0).click()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.get(".spectrum-Menu").then($menu => {
|
||||||
|
if ($menu.text().includes("Unpublish")) {
|
||||||
|
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||||
|
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
||||||
|
} else {
|
||||||
|
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||||
|
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||||
|
cy.get("input").type(name)
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Button--warning").click()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const appId = val.reduce((acc, app) => {
|
return
|
||||||
if (name === app.name) {
|
|
||||||
acc = app.appId
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, "")
|
|
||||||
|
|
||||||
if (appId == "") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const appIdParsed = appId.split("_").pop()
|
|
||||||
const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
|
|
||||||
cy.get(actionEleId).within(() => {
|
|
||||||
cy.get(".spectrum-Icon").eq(0).click()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cy.get(".spectrum-Menu").then($menu => {
|
|
||||||
if ($menu.text().includes("Unpublish")) {
|
|
||||||
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
|
||||||
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
|
||||||
} else {
|
|
||||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
|
||||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
|
||||||
cy.get("input").type(name)
|
|
||||||
})
|
|
||||||
cy.get(".spectrum-Button--warning").click()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
return
|
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,33 +287,99 @@ 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()
|
||||||
for (let i = 0; i < screenNames.length; i++) {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get(".item").contains(screenNames[i]).click()
|
cy.get(".item").contains("Autogenerated screens").click()
|
||||||
}
|
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||||
cy.get(".spectrum-Button").contains("Add screens").click({ force: true })
|
cy.wait(500)
|
||||||
cy.wait(4000)
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add(
|
||||||
|
"createAutogeneratedScreens",
|
||||||
|
(screenNames, accessLevelLabel) => {
|
||||||
|
cy.navigateToAutogeneratedModal()
|
||||||
|
|
||||||
|
for (let i = 0; i < screenNames.length; i++) {
|
||||||
|
cy.get(".data-source-entry").contains(screenNames[i]).click()
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Cypress.Commands.add("addRow", values => {
|
Cypress.Commands.add("addRow", values => {
|
||||||
cy.contains("Create row").click()
|
cy.contains("Create row").click()
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
@ -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.123",
|
"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.123",
|
"@budibase/bbui": "^1.0.124-alpha.0",
|
||||||
"@budibase/client": "^1.0.123",
|
"@budibase/client": "^1.0.124-alpha.0",
|
||||||
"@budibase/frontend-core": "^1.0.123",
|
"@budibase/frontend-core": "^1.0.124-alpha.0",
|
||||||
"@budibase/string-templates": "^1.0.123",
|
"@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",
|
||||||
|
|
|
@ -22,6 +22,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",
|
||||||
|
@ -35,6 +36,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",
|
||||||
|
@ -50,3 +52,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,
|
||||||
|
@ -57,5 +57,5 @@ class AnalyticsHub {
|
||||||
|
|
||||||
const analytics = new AnalyticsHub()
|
const analytics = new AnalyticsHub()
|
||||||
|
|
||||||
export { Events }
|
export { Events, EventSource }
|
||||||
export default analytics
|
export default analytics
|
||||||
|
|
|
@ -7,7 +7,11 @@ import {
|
||||||
getComponentSettings,
|
getComponentSettings,
|
||||||
} from "./componentUtils"
|
} from "./componentUtils"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { queries as queriesStores, tables as tablesStore } from "stores/backend"
|
import {
|
||||||
|
queries as queriesStores,
|
||||||
|
tables as tablesStore,
|
||||||
|
roles as rolesStore,
|
||||||
|
} from "stores/backend"
|
||||||
import {
|
import {
|
||||||
makePropSafe,
|
makePropSafe,
|
||||||
isJSBinding,
|
isJSBinding,
|
||||||
|
@ -33,6 +37,7 @@ export const getBindableProperties = (asset, componentId) => {
|
||||||
const deviceBindings = getDeviceBindings()
|
const deviceBindings = getDeviceBindings()
|
||||||
const stateBindings = getStateBindings()
|
const stateBindings = getStateBindings()
|
||||||
const selectedRowsBindings = getSelectedRowsBindings(asset)
|
const selectedRowsBindings = getSelectedRowsBindings(asset)
|
||||||
|
const roleBindings = getRoleBindings()
|
||||||
return [
|
return [
|
||||||
...contextBindings,
|
...contextBindings,
|
||||||
...urlBindings,
|
...urlBindings,
|
||||||
|
@ -40,6 +45,7 @@ export const getBindableProperties = (asset, componentId) => {
|
||||||
...userBindings,
|
...userBindings,
|
||||||
...deviceBindings,
|
...deviceBindings,
|
||||||
...selectedRowsBindings,
|
...selectedRowsBindings,
|
||||||
|
...roleBindings,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -391,6 +397,16 @@ const getUrlBindings = asset => {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRoleBindings = () => {
|
||||||
|
return (get(rolesStore) || []).map(role => {
|
||||||
|
return {
|
||||||
|
type: "context",
|
||||||
|
runtimeBinding: `trim "${role._id}"`,
|
||||||
|
readableBinding: `Role.${role.name}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all bindable properties exposed in a button actions flow up until
|
* Gets all bindable properties exposed in a button actions flow up until
|
||||||
* the specified action ID, as well as context provided for the action
|
* the specified action ID, as well as context provided for the action
|
||||||
|
@ -638,7 +654,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
||||||
* Builds a form schema given a form component.
|
* 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
|
||||||
|
|
|
@ -2,9 +2,15 @@ export default function (url) {
|
||||||
return url
|
return url
|
||||||
.split("/")
|
.split("/")
|
||||||
.map(part => {
|
.map(part => {
|
||||||
// if parameter, then use as is
|
part = decodeURIComponent(part)
|
||||||
if (part.startsWith(":")) return part
|
part = part.replace(/ /g, "-")
|
||||||
return encodeURIComponent(part.replace(/ /g, "-"))
|
|
||||||
|
// If parameter, then use as is
|
||||||
|
if (!part.startsWith(":")) {
|
||||||
|
part = encodeURIComponent(part)
|
||||||
|
}
|
||||||
|
|
||||||
|
return part
|
||||||
})
|
})
|
||||||
.join("/")
|
.join("/")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
|
@ -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 }}
|
||||||
>
|
>
|
||||||
<FlowItem {testDataModal} {block} />
|
{#if block.stepId !== "LOOP"}
|
||||||
|
<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
|
<div class={`block ${block.type} hoverable`} class:selected on:click={() => {}}>
|
||||||
class={`block ${block.type} hoverable`}
|
{#if loopingSelected}
|
||||||
class:selected
|
<div class="blockSection">
|
||||||
on:click={() => {
|
<div
|
||||||
onSelect(block)
|
on:click={() => {
|
||||||
}}
|
showLooping = !showLooping
|
||||||
>
|
}}
|
||||||
|
class="splitHeader"
|
||||||
|
>
|
||||||
|
<div class="center-items">
|
||||||
|
<svg
|
||||||
|
width="28px"
|
||||||
|
height="28px"
|
||||||
|
class="spectrum-Icon"
|
||||||
|
style="color:grey;"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-Reuse" />
|
||||||
|
</svg>
|
||||||
|
<div class="iconAlign">
|
||||||
|
<Detail size="S">Looping</Detail>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="blockTitle">
|
||||||
|
<div
|
||||||
|
style="margin-left: 10px;"
|
||||||
|
on:click={() => {
|
||||||
|
onSelect(block)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name={showLooping ? "ChevronDown" : "ChevronUp"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider noMargin />
|
||||||
|
{#if !showLooping}
|
||||||
|
<div class="blockSection">
|
||||||
|
<div class="block-options">
|
||||||
|
<div class="delete-padding" on:click={() => deleteStep()}>
|
||||||
|
<Icon name="DeleteOutline" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
<AutomationBlockSetup
|
||||||
|
schemaProperties={Object.entries(
|
||||||
|
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
|
||||||
|
.properties
|
||||||
|
)}
|
||||||
|
block={$automationStore.selectedAutomation?.automation.definition.steps.find(
|
||||||
|
x => x.blockToLoop === block.id
|
||||||
|
)}
|
||||||
|
{webhookModal}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
<Divider noMargin />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="blockSection">
|
<div class="blockSection">
|
||||||
<div
|
<div
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -127,65 +208,66 @@
|
||||||
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
|
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if testResult && testResult[0]}
|
<div class="blockTitle">
|
||||||
<span on:click={() => resultsModal.show()}>
|
{#if testResult && testResult[0]}
|
||||||
<StatusLight
|
<div style="float: right;" on:click={() => resultsModal.show()}>
|
||||||
positive={isTrigger || testResult[0].outputs?.success}
|
<StatusLight
|
||||||
negative={!testResult[0].outputs?.success}
|
positive={isTrigger || testResult[0].outputs?.success}
|
||||||
><Body size="XS">View response</Body></StatusLight
|
negative={!testResult[0].outputs?.success}
|
||||||
>
|
><Body size="XS">View response</Body></StatusLight
|
||||||
</span>
|
>
|
||||||
{/if}
|
</div>
|
||||||
|
{/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">
|
{#if !isTrigger}
|
||||||
<ActionButton
|
<div>
|
||||||
on:click={() => {
|
|
||||||
onSelect(block)
|
|
||||||
setupToggled = !setupToggled
|
|
||||||
}}
|
|
||||||
quiet
|
|
||||||
icon={setupToggled ? "ChevronDown" : "ChevronRight"}
|
|
||||||
>
|
|
||||||
<Detail size="S">Setup</Detail>
|
|
||||||
</ActionButton>
|
|
||||||
{#if !isTrigger}
|
|
||||||
<div class="block-options">
|
<div class="block-options">
|
||||||
{#if showBindingPicker}
|
{#if !loopingSelected}
|
||||||
<div>
|
<ActionButton on:click={() => addLooping()} icon="Reuse"
|
||||||
<Select
|
>Add Looping</ActionButton
|
||||||
on:change={toggleFieldControl}
|
>
|
||||||
quiet
|
|
||||||
defaultValue="Use values"
|
|
||||||
autoWidth
|
|
||||||
value={rowControl ? "Use bindings" : "Use values"}
|
|
||||||
options={["Use values", "Use bindings"]}
|
|
||||||
placeholder={null}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
<div class="delete-padding" on:click={() => deleteStep()}>
|
{#if showBindingPicker}
|
||||||
<Icon name="DeleteOutline" />
|
<Select
|
||||||
</div>
|
on:change={toggleFieldControl}
|
||||||
|
defaultValue="Use values"
|
||||||
|
autoWidth
|
||||||
|
value={rowControl ? "Use bindings" : "Use values"}
|
||||||
|
options={["Use values", "Use bindings"]}
|
||||||
|
placeholder={null}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<ActionButton
|
||||||
|
on:click={() => deleteStep()}
|
||||||
|
icon="DeleteOutline"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
{#if setupToggled}
|
<AutomationBlockSetup
|
||||||
<AutomationBlockSetup
|
schemaProperties={Object.entries(block.schema.inputs.properties)}
|
||||||
schemaProperties={Object.entries(block.schema.inputs.properties)}
|
{block}
|
||||||
{block}
|
{webhookModal}
|
||||||
{webhookModal}
|
/>
|
||||||
/>
|
{#if lastStep}
|
||||||
{#if lastStep}
|
<Button on:click={() => testDataModal.show()} cta
|
||||||
<Button on:click={() => testDataModal.show()} cta
|
>Finish and test automation</Button
|
||||||
>Finish and test automation</Button
|
>
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue