Merge remote-tracking branch 'origin/develop' into feature/db-query-save-prompt
This commit is contained in:
commit
24d57e8fd2
|
@ -22,44 +22,67 @@ jobs:
|
|||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn lint
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
# Run build all the projects
|
||||
- run: yarn build
|
||||
# Check the types of the projects built via esbuild
|
||||
- run: yarn check:types
|
||||
|
||||
test:
|
||||
test-libraries:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
- run: yarn build
|
||||
- run: yarn test --ignore=@budibase/pro
|
||||
- run: yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||
name: codecov-umbrella
|
||||
verbose: true
|
||||
|
||||
test-services:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn test --scope=@budibase/worker --scope=@budibase/server
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||
|
@ -69,32 +92,34 @@ jobs:
|
|||
test-pro:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
- run: yarn build --scope=@budibase/types --scope=@budibase/shared-core
|
||||
- run: yarn test --scope=@budibase/pro
|
||||
|
||||
integration-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14.x
|
||||
- run: yarn && yarn bootstrap && yarn build
|
||||
- run: |
|
||||
cache: "yarn"
|
||||
- run: yarn
|
||||
- run: yarn build
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd qa-core
|
||||
yarn setup
|
||||
yarn test:ci
|
||||
|
@ -106,7 +131,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
|
|
@ -37,7 +37,7 @@ jobs:
|
|||
with:
|
||||
node-version: 14.x
|
||||
|
||||
- run: yarn
|
||||
- run: yarn install --frozen-lockfile
|
||||
- name: Update versions
|
||||
run: |
|
||||
version=$(cat lerna.json \
|
||||
|
@ -51,7 +51,7 @@ jobs:
|
|||
node scripts/syncLocalDependencies.js $version
|
||||
echo "Syncing yarn workspace"
|
||||
yarn
|
||||
- run: yarn build
|
||||
- run: yarn build --configuration=production
|
||||
- run: yarn build:sdk
|
||||
|
||||
- name: Publish budibase packages to NPM
|
||||
|
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
with:
|
||||
node-version: 14.x
|
||||
|
||||
- run: yarn
|
||||
- run: yarn install --frozen-lockfile
|
||||
- name: Update versions
|
||||
run: |
|
||||
version=$(cat lerna.json \
|
||||
|
@ -57,7 +57,7 @@ jobs:
|
|||
echo "Syncing yarn workspace"
|
||||
yarn
|
||||
- run: yarn lint
|
||||
- run: yarn build
|
||||
- run: yarn build --configuration=production
|
||||
- run: yarn build:sdk
|
||||
|
||||
- name: Publish budibase packages to NPM
|
||||
|
|
|
@ -1 +1 @@
|
|||
3.10.0
|
||||
3.10.0
|
||||
|
|
|
@ -144,8 +144,6 @@ The following commands can be executed to manually get Budibase up and running (
|
|||
|
||||
`yarn` to install project dependencies
|
||||
|
||||
`yarn bootstrap` will install all budibase modules and symlink them together using lerna.
|
||||
|
||||
`yarn build` will build all budibase packages.
|
||||
|
||||
#### 4. Running
|
||||
|
@ -243,7 +241,7 @@ An overview of the CI pipelines can be found [here](../.github/workflows/README.
|
|||
|
||||
Note that only budibase maintainers will be able to access the pro repo.
|
||||
|
||||
The `yarn bootstrap` command can be used to replace the NPM supplied dependency with the local source aware version. This is achieved using the `yarn link` command. To see specifically how dependencies are linked see [scripts/link-dependencies.sh](../scripts/link-dependencies.sh). The same link script is used to link dependencies to account-portal in local dev.
|
||||
By default, NX will make sure that dependencies are replaced with local source aware version. This is achieved using the `yarn link` command. To see specifically how dependencies are linked see [scripts/link-dependencies.sh](../scripts/link-dependencies.sh). The same link script is used to link dependencies to account-portal in local dev.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
version: "3"
|
||||
|
||||
# optional ports are specified throughout for more advanced use cases.
|
||||
|
||||
services:
|
||||
app-service:
|
||||
build: ../packages/server
|
||||
container_name: build-bbapps
|
||||
environment:
|
||||
SELF_HOSTED: 1
|
||||
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
||||
WORKER_URL: http://worker-service:4003
|
||||
MINIO_URL: http://minio-service:9000
|
||||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
||||
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
|
||||
PORT: 4002
|
||||
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
LOG_LEVEL: info
|
||||
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
|
||||
ENABLE_ANALYTICS: "true"
|
||||
REDIS_URL: redis-service:6379
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
|
||||
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
|
||||
PLUGINS_DIR: ${PLUGINS_DIR}
|
||||
depends_on:
|
||||
- worker-service
|
||||
- redis-service
|
||||
# volumes:
|
||||
# - /some/path/to/plugins:/plugins
|
||||
|
||||
worker-service:
|
||||
build: ../packages/worker
|
||||
container_name: build-bbworker
|
||||
environment:
|
||||
SELF_HOSTED: 1
|
||||
PORT: 4003
|
||||
CLUSTER_PORT: ${MAIN_PORT}
|
||||
API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
||||
MINIO_URL: http://minio-service:9000
|
||||
APPS_URL: http://app-service:4002
|
||||
COUCH_DB_USERNAME: ${COUCH_DB_USER}
|
||||
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
|
||||
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
||||
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131
|
||||
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||
REDIS_URL: redis-service:6379
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
depends_on:
|
||||
- redis-service
|
||||
- minio-service
|
||||
|
||||
proxy-service-docker:
|
||||
ports:
|
||||
- "${MAIN_PORT}:10000"
|
||||
container_name: build-bbproxy
|
||||
image: budibase/proxy
|
||||
environment:
|
||||
- PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10
|
||||
- PROXY_RATE_LIMIT_API_PER_SECOND=20
|
||||
- APPS_UPSTREAM_URL=http://app-service:4002
|
||||
- WORKER_UPSTREAM_URL=http://worker-service:4003
|
||||
- MINIO_UPSTREAM_URL=http://minio-service:9000
|
||||
- COUCHDB_UPSTREAM_URL=http://couchdb-service:5984
|
||||
- WATCHTOWER_UPSTREAM_URL=http://watchtower-service:8080
|
||||
- RESOLVER=127.0.0.11
|
||||
depends_on:
|
||||
- minio-service
|
||||
- worker-service
|
||||
- app-service
|
||||
- couchdb-service
|
|
@ -55,7 +55,7 @@ http {
|
|||
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
|
||||
set $csp_object "object-src 'none'";
|
||||
set $csp_base_uri "base-uri 'self'";
|
||||
set $csp_connect "connect-src 'self' https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com";
|
||||
set $csp_connect "connect-src 'self' https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com";
|
||||
set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com";
|
||||
set $csp_frame "frame-src 'self' https:";
|
||||
set $csp_img "img-src http: https: data: blob:";
|
||||
|
@ -82,6 +82,12 @@ http {
|
|||
set $couchdb ${COUCHDB_UPSTREAM_URL};
|
||||
set $watchtower ${WATCHTOWER_UPSTREAM_URL};
|
||||
|
||||
location /health {
|
||||
access_log off;
|
||||
add_header 'Content-Type' 'application/json';
|
||||
return 200 '{ "status": "OK" }';
|
||||
}
|
||||
|
||||
location /app {
|
||||
proxy_pass $apps;
|
||||
}
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
FROM node:14-slim as build
|
||||
FROM node:16-slim as build
|
||||
|
||||
# install node-gyp dependencies
|
||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python
|
||||
|
||||
# add pin script
|
||||
WORKDIR /
|
||||
ADD scripts/pinVersions.js scripts/cleanup.sh ./
|
||||
ADD scripts/cleanup.sh ./
|
||||
RUN chmod +x /cleanup.sh
|
||||
|
||||
# build server
|
||||
WORKDIR /app
|
||||
ADD packages/server .
|
||||
RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh
|
||||
RUN yarn install --frozen-lockfile --production=true && /cleanup.sh
|
||||
|
||||
# build worker
|
||||
WORKDIR /worker
|
||||
ADD packages/worker .
|
||||
RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh
|
||||
RUN yarn install --frozen-lockfile --production=true && /cleanup.sh
|
||||
|
||||
FROM budibase/couchdb
|
||||
ARG TARGETARCH
|
||||
|
@ -31,9 +31,7 @@ COPY --from=build /worker /worker
|
|||
|
||||
# install base dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server && \
|
||||
apt-add-repository 'deb http://security.debian.org/debian-security bullseye-security/updates main' && \
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server
|
||||
|
||||
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
|
||||
WORKDIR /nodejs
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.6.8-alpha.11",
|
||||
"version": "2.6.19-alpha.33",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/backend-core",
|
||||
|
|
10
nx.json
10
nx.json
|
@ -6,5 +6,15 @@
|
|||
"cacheableOperations": ["build", "test"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"targetDefaults": {
|
||||
"dev:builder": {
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": ["@budibase/string-templates"],
|
||||
"target": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
37
package.json
37
package.json
|
@ -2,17 +2,23 @@
|
|||
"name": "root",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/node-resolve": "^0.2.2",
|
||||
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
||||
"@nx/js": "16.2.1",
|
||||
"@rollup/plugin-json": "^4.0.2",
|
||||
"@typescript-eslint/parser": "5.45.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"esbuild": "^0.17.18",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint-plugin-cypress": "^2.11.3",
|
||||
"eslint-plugin-svelte3": "^3.2.0",
|
||||
"husky": "^8.0.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"kill-port": "^1.6.1",
|
||||
"lerna": "^6.6.1",
|
||||
"lerna": "7.0.0-alpha.0",
|
||||
"madge": "^6.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"nx": "^16.2.1",
|
||||
"prettier": "^2.3.1",
|
||||
"prettier-plugin-svelte": "^2.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
|
@ -23,10 +29,11 @@
|
|||
},
|
||||
"scripts": {
|
||||
"preinstall": "node scripts/syncProPackage.js",
|
||||
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
|
||||
"bootstrap": "./scripts/bootstrap.sh && lerna link && ./scripts/link-dependencies.sh",
|
||||
"build": "lerna run --stream build",
|
||||
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev",
|
||||
"bootstrap": "./scripts/link-dependencies.sh && echo '***BOOTSTRAP ONLY REQUIRED FOR USE WITH ACCOUNT PORTAL***'",
|
||||
"build": "yarn nx run-many -t=build",
|
||||
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
|
||||
"check:types": "lerna run check:types --skip-nx-cache",
|
||||
"backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap",
|
||||
"backend:build": "./scripts/scopeBackend.sh 'lerna run --stream build'",
|
||||
"build:sdk": "lerna run --stream build:sdk",
|
||||
|
@ -41,10 +48,11 @@
|
|||
"kill-builder": "kill-port 3000",
|
||||
"kill-server": "kill-port 4001 4002",
|
||||
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
||||
"dev": "yarn run kill-all && lerna link && lerna run --stream --parallel dev:builder --concurrency 1 --stream",
|
||||
"dev:noserver": "yarn run kill-builder && lerna link && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server",
|
||||
"dev": "yarn run kill-all && lerna run --stream --parallel dev:builder --stream",
|
||||
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server",
|
||||
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
|
||||
"dev:docker": "yarn build && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
|
||||
"test": "lerna run --stream test --stream",
|
||||
"lint:eslint": "eslint packages && eslint qa-core",
|
||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
||||
|
@ -53,16 +61,16 @@
|
|||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
|
||||
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||
"build:specs": "lerna run --stream specs",
|
||||
"build:docker": "lerna run --stream build:docker && npm run build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
||||
"build:docker": "lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
||||
"build:docker:pre": "lerna run --stream build && lerna run --stream predocker",
|
||||
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
|
||||
"build:docker:selfhost": "lerna run --stream build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
|
||||
"build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && npm run build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
||||
"build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
||||
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
|
||||
"build:docker:single:multiarch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/single/Dockerfile -t budibase:latest .",
|
||||
"build:docker:single:image": "docker build -f hosting/single/Dockerfile -t budibase:latest .",
|
||||
"build:docker:single": "npm run build:docker:pre && npm run build:docker:single:image",
|
||||
"build:docker:single": "yarn build && lerna run --concurrency 1 predocker && yarn build:docker:single:image",
|
||||
"build:docker:dependencies": "docker build -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest ./hosting",
|
||||
"publish:docker:couch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/couchdb/Dockerfile -t budibase/couchdb:latest -t budibase/couchdb:v3.2.1 --push ./hosting/couchdb",
|
||||
"publish:docker:dependencies": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest -t budibase/dependencies:v3.2.1 --push ./hosting",
|
||||
|
@ -101,5 +109,12 @@
|
|||
"packages/worker",
|
||||
"packages/pro/packages/pro"
|
||||
]
|
||||
}
|
||||
},
|
||||
"resolutions": {
|
||||
"@budibase/backend-core": "0.0.0",
|
||||
"@budibase/shared-core": "0.0.0",
|
||||
"@budibase/string-templates": "0.0.0",
|
||||
"@budibase/types": "0.0.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.0",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
@ -22,7 +22,7 @@
|
|||
"dependencies": {
|
||||
"@budibase/nano": "10.1.2",
|
||||
"@budibase/pouchdb-replication-stream": "1.2.10",
|
||||
"@budibase/types": "0.0.1",
|
||||
"@budibase/types": "0.0.0",
|
||||
"@shopify/jest-koa-mocks": "5.0.1",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
"aws-cloudfront-sign": "2.2.0",
|
||||
|
@ -88,5 +88,19 @@
|
|||
"tsconfig-paths": "4.0.0",
|
||||
"typescript": "4.7.3"
|
||||
},
|
||||
"nx": {
|
||||
"targets": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": [
|
||||
"@budibase/types"
|
||||
],
|
||||
"target": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
||||
}
|
||||
|
|
|
@ -72,16 +72,12 @@ describe("writethrough", () => {
|
|||
writethrough.put({ ...current, value: 4 }),
|
||||
])
|
||||
|
||||
// with a lock, this will work
|
||||
const newRev = responses.map(x => x.rev).find(x => x !== current._rev)
|
||||
expect(newRev).toBeDefined()
|
||||
expect(responses.map(x => x.rev)).toEqual(
|
||||
expect.arrayContaining([current._rev, current._rev, newRev])
|
||||
)
|
||||
expectFunctionWasCalledTimesWith(
|
||||
mocks.alerts.logWarn,
|
||||
2,
|
||||
"Ignoring redlock conflict in write-through cache"
|
||||
)
|
||||
|
||||
const output = await db.get(current._id)
|
||||
expect(output.value).toBe(4)
|
||||
|
|
|
@ -21,7 +21,7 @@ export enum ViewName {
|
|||
AUTOMATION_LOGS = "automation_logs",
|
||||
ACCOUNT_BY_EMAIL = "account_by_email",
|
||||
PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
|
||||
USER_BY_GROUP = "by_group_user",
|
||||
USER_BY_GROUP = "user_by_group",
|
||||
APP_BACKUP_BY_TRIGGER = "by_trigger",
|
||||
}
|
||||
|
||||
|
|
|
@ -104,6 +104,22 @@ async function newContext(updates: ContextMap, task: any) {
|
|||
return Context.run(context, task)
|
||||
}
|
||||
|
||||
export async function doInAutomationContext(params: {
|
||||
appId: string
|
||||
automationId: string
|
||||
task: any
|
||||
}): Promise<any> {
|
||||
const tenantId = getTenantIDFromAppID(params.appId)
|
||||
return newContext(
|
||||
{
|
||||
tenantId,
|
||||
appId: params.appId,
|
||||
automationId: params.automationId,
|
||||
},
|
||||
params.task
|
||||
)
|
||||
}
|
||||
|
||||
export async function doInContext(appId: string, task: any): Promise<any> {
|
||||
const tenantId = getTenantIDFromAppID(appId)
|
||||
return newContext(
|
||||
|
@ -187,6 +203,11 @@ export function getTenantId(): string {
|
|||
return tenantId
|
||||
}
|
||||
|
||||
export function getAutomationId(): string | undefined {
|
||||
const context = Context.get()
|
||||
return context?.automationId
|
||||
}
|
||||
|
||||
export function getAppId(): string | undefined {
|
||||
const context = Context.get()
|
||||
const foundId = context?.appId
|
||||
|
|
|
@ -7,4 +7,5 @@ export type ContextMap = {
|
|||
identity?: IdentityContext
|
||||
environmentVariables?: Record<string, string>
|
||||
isScim?: boolean
|
||||
automationId?: string
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
isDocument,
|
||||
} from "@budibase/types"
|
||||
import { getCouchInfo } from "./connections"
|
||||
import { directCouchCall } from "./utils"
|
||||
import { directCouchUrlCall } from "./utils"
|
||||
import { getPouchDB } from "./pouchDB"
|
||||
import { WriteStream, ReadStream } from "fs"
|
||||
import { newid } from "../../docIds/newid"
|
||||
|
@ -46,6 +46,8 @@ export class DatabaseImpl implements Database {
|
|||
private readonly instanceNano?: Nano.ServerScope
|
||||
private readonly pouchOpts: DatabaseOpts
|
||||
|
||||
private readonly couchInfo = getCouchInfo()
|
||||
|
||||
constructor(dbName?: string, opts?: DatabaseOpts, connection?: string) {
|
||||
if (dbName == null) {
|
||||
throw new Error("Database name cannot be undefined.")
|
||||
|
@ -53,8 +55,8 @@ export class DatabaseImpl implements Database {
|
|||
this.name = dbName
|
||||
this.pouchOpts = opts || {}
|
||||
if (connection) {
|
||||
const couchInfo = getCouchInfo(connection)
|
||||
this.instanceNano = buildNano(couchInfo)
|
||||
this.couchInfo = getCouchInfo(connection)
|
||||
this.instanceNano = buildNano(this.couchInfo)
|
||||
}
|
||||
if (!DatabaseImpl.nano) {
|
||||
DatabaseImpl.init()
|
||||
|
@ -67,7 +69,11 @@ export class DatabaseImpl implements Database {
|
|||
}
|
||||
|
||||
async exists() {
|
||||
let response = await directCouchCall(`/${this.name}`, "HEAD")
|
||||
const response = await directCouchUrlCall({
|
||||
url: `${this.couchInfo.url}/${this.name}`,
|
||||
method: "HEAD",
|
||||
cookie: this.couchInfo.cookie,
|
||||
})
|
||||
return response.status === 200
|
||||
}
|
||||
|
||||
|
|
|
@ -4,21 +4,21 @@ export const getCouchInfo = (connection?: string) => {
|
|||
const urlInfo = getUrlInfo(connection)
|
||||
let username
|
||||
let password
|
||||
if (env.COUCH_DB_USERNAME) {
|
||||
// set from env
|
||||
username = env.COUCH_DB_USERNAME
|
||||
} else if (urlInfo.auth.username) {
|
||||
if (urlInfo.auth?.username) {
|
||||
// set from url
|
||||
username = urlInfo.auth.username
|
||||
} else if (env.COUCH_DB_USERNAME) {
|
||||
// set from env
|
||||
username = env.COUCH_DB_USERNAME
|
||||
} else if (!env.isTest()) {
|
||||
throw new Error("CouchDB username not set")
|
||||
}
|
||||
if (env.COUCH_DB_PASSWORD) {
|
||||
// set from env
|
||||
password = env.COUCH_DB_PASSWORD
|
||||
} else if (urlInfo.auth.password) {
|
||||
if (urlInfo.auth?.password) {
|
||||
// set from url
|
||||
password = urlInfo.auth.password
|
||||
} else if (env.COUCH_DB_PASSWORD) {
|
||||
// set from env
|
||||
password = env.COUCH_DB_PASSWORD
|
||||
} else if (!env.isTest()) {
|
||||
throw new Error("CouchDB password not set")
|
||||
}
|
||||
|
|
|
@ -9,6 +9,20 @@ export async function directCouchCall(
|
|||
) {
|
||||
let { url, cookie } = getCouchInfo()
|
||||
const couchUrl = `${url}/${path}`
|
||||
return await directCouchUrlCall({ url: couchUrl, cookie, method, body })
|
||||
}
|
||||
|
||||
export async function directCouchUrlCall({
|
||||
url,
|
||||
cookie,
|
||||
method,
|
||||
body,
|
||||
}: {
|
||||
url: string
|
||||
cookie: string
|
||||
method: string
|
||||
body?: any
|
||||
}) {
|
||||
const params: any = {
|
||||
method: method,
|
||||
headers: {
|
||||
|
@ -19,7 +33,7 @@ export async function directCouchCall(
|
|||
params.body = JSON.stringify(body)
|
||||
params.headers["Content-Type"] = "application/json"
|
||||
}
|
||||
return await fetch(checkSlashesInUrl(encodeURI(couchUrl)), params)
|
||||
return await fetch(checkSlashesInUrl(encodeURI(url)), params)
|
||||
}
|
||||
|
||||
export async function directCouchQuery(
|
||||
|
|
|
@ -69,10 +69,10 @@ function findVersion() {
|
|||
try {
|
||||
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
|
||||
const content = readFileSync(packageJsonFile!, "utf-8")
|
||||
const version = JSON.parse(content).version
|
||||
return version
|
||||
return JSON.parse(content).version
|
||||
} catch {
|
||||
throw new Error("Cannot find a valid version in its package.json")
|
||||
// throwing an error here is confusing/causes backend-core to be hard to import
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,7 @@ const environment = {
|
|||
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
|
||||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
||||
REDIS_URL: process.env.REDIS_URL || "localhost:6379",
|
||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD || "budibase",
|
||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||
REDIS_CLUSTERED: process.env.REDIS_CLUSTERED,
|
||||
MOCK_REDIS: process.env.MOCK_REDIS,
|
||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||
|
|
|
@ -39,6 +39,7 @@ if (!env.DISABLE_PINO_LOGGER) {
|
|||
objects?: any[]
|
||||
tenantId?: string
|
||||
appId?: string
|
||||
automationId?: string
|
||||
identityId?: string
|
||||
identityType?: IdentityType
|
||||
correlationId?: string
|
||||
|
@ -86,18 +87,45 @@ if (!env.DISABLE_PINO_LOGGER) {
|
|||
contextObject = {
|
||||
tenantId: getTenantId(),
|
||||
appId: getAppId(),
|
||||
automationId: getAutomationId(),
|
||||
identityId: identity?._id,
|
||||
identityType: identity?.type,
|
||||
correlationId: correlation.getId(),
|
||||
}
|
||||
}
|
||||
|
||||
const mergingObject = {
|
||||
objects: objects.length ? objects : undefined,
|
||||
const mergingObject: any = {
|
||||
err: error,
|
||||
pid: process.pid,
|
||||
...contextObject,
|
||||
}
|
||||
|
||||
if (objects.length) {
|
||||
// init generic data object for params supplied that don't have a
|
||||
// '_logKey' field. This prints an object using argument index as the key
|
||||
// e.g. { 0: {}, 1: {} }
|
||||
const data: any = {}
|
||||
let dataIndex = 0
|
||||
|
||||
for (let i = 0; i < objects.length; i++) {
|
||||
const object = objects[i]
|
||||
// the object has specified a log key
|
||||
// use this instead of generic key
|
||||
const logKey = object._logKey
|
||||
if (logKey) {
|
||||
delete object._logKey
|
||||
mergingObject[logKey] = object
|
||||
} else {
|
||||
data[dataIndex] = object
|
||||
dataIndex++
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(data).length) {
|
||||
mergingObject.data = data
|
||||
}
|
||||
}
|
||||
|
||||
return [mergingObject, message]
|
||||
}
|
||||
|
||||
|
@ -159,6 +187,16 @@ if (!env.DISABLE_PINO_LOGGER) {
|
|||
return appId
|
||||
}
|
||||
|
||||
const getAutomationId = () => {
|
||||
let appId
|
||||
try {
|
||||
appId = context.getAutomationId()
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
return appId
|
||||
}
|
||||
|
||||
const getIdentity = () => {
|
||||
let identity
|
||||
try {
|
||||
|
|
|
@ -128,6 +128,7 @@ class InMemoryQueue {
|
|||
|
||||
on() {
|
||||
// do nothing
|
||||
return this
|
||||
}
|
||||
|
||||
async waitForCompletion() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Job, JobId, Queue } from "bull"
|
||||
import { JobQueue } from "./constants"
|
||||
import * as context from "../context"
|
||||
|
||||
export type StalledFn = (job: Job) => Promise<void>
|
||||
|
||||
|
@ -31,77 +32,164 @@ function handleStalled(queue: Queue, removeStalledCb?: StalledFn) {
|
|||
})
|
||||
}
|
||||
|
||||
function logging(queue: Queue, jobQueue: JobQueue) {
|
||||
let eventType: string
|
||||
switch (jobQueue) {
|
||||
case JobQueue.AUTOMATION:
|
||||
eventType = "automation-event"
|
||||
break
|
||||
case JobQueue.APP_BACKUP:
|
||||
eventType = "app-backup-event"
|
||||
break
|
||||
case JobQueue.AUDIT_LOG:
|
||||
eventType = "audit-log-event"
|
||||
break
|
||||
case JobQueue.SYSTEM_EVENT_QUEUE:
|
||||
eventType = "system-event"
|
||||
break
|
||||
function getLogParams(
|
||||
eventType: QueueEventType,
|
||||
event: BullEvent,
|
||||
opts: {
|
||||
job?: Job
|
||||
jobId?: JobId
|
||||
error?: Error
|
||||
} = {},
|
||||
extra: any = {}
|
||||
) {
|
||||
const message = `[BULL] ${eventType}=${event}`
|
||||
const err = opts.error
|
||||
|
||||
const bullLog = {
|
||||
_logKey: "bull",
|
||||
eventType,
|
||||
event,
|
||||
job: opts.job,
|
||||
jobId: opts.jobId || opts.job?.id,
|
||||
...extra,
|
||||
}
|
||||
|
||||
let automationLog
|
||||
if (opts.job?.data?.automation) {
|
||||
automationLog = {
|
||||
_logKey: "automation",
|
||||
trigger: opts.job
|
||||
? opts.job.data.automation.definition.trigger.event
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
return [message, err, bullLog, automationLog]
|
||||
}
|
||||
|
||||
enum BullEvent {
|
||||
ERROR = "error",
|
||||
WAITING = "waiting",
|
||||
ACTIVE = "active",
|
||||
STALLED = "stalled",
|
||||
PROGRESS = "progress",
|
||||
COMPLETED = "completed",
|
||||
FAILED = "failed",
|
||||
PAUSED = "paused",
|
||||
RESUMED = "resumed",
|
||||
CLEANED = "cleaned",
|
||||
DRAINED = "drained",
|
||||
REMOVED = "removed",
|
||||
}
|
||||
|
||||
enum QueueEventType {
|
||||
AUTOMATION_EVENT = "automation-event",
|
||||
APP_BACKUP_EVENT = "app-backup-event",
|
||||
AUDIT_LOG_EVENT = "audit-log-event",
|
||||
SYSTEM_EVENT = "system-event",
|
||||
}
|
||||
|
||||
const EventTypeMap: { [key in JobQueue]: QueueEventType } = {
|
||||
[JobQueue.AUTOMATION]: QueueEventType.AUTOMATION_EVENT,
|
||||
[JobQueue.APP_BACKUP]: QueueEventType.APP_BACKUP_EVENT,
|
||||
[JobQueue.AUDIT_LOG]: QueueEventType.AUDIT_LOG_EVENT,
|
||||
[JobQueue.SYSTEM_EVENT_QUEUE]: QueueEventType.SYSTEM_EVENT,
|
||||
}
|
||||
|
||||
function logging(queue: Queue, jobQueue: JobQueue) {
|
||||
const eventType = EventTypeMap[jobQueue]
|
||||
|
||||
function doInJobContext(job: Job, task: any) {
|
||||
// if this is an automation job try to get the app id
|
||||
const appId = job.data.event?.appId
|
||||
if (appId) {
|
||||
return context.doInContext(appId, task)
|
||||
} else {
|
||||
task()
|
||||
}
|
||||
}
|
||||
|
||||
queue
|
||||
.on(BullEvent.STALLED, async (job: Job) => {
|
||||
// A job has been marked as stalled. This is useful for debugging job
|
||||
// workers that crash or pause the event loop.
|
||||
await doInJobContext(job, () => {
|
||||
console.error(...getLogParams(eventType, BullEvent.STALLED, { job }))
|
||||
})
|
||||
})
|
||||
.on(BullEvent.ERROR, (error: any) => {
|
||||
// An error occurred.
|
||||
console.error(...getLogParams(eventType, BullEvent.ERROR, { error }))
|
||||
})
|
||||
|
||||
if (process.env.NODE_DEBUG?.includes("bull")) {
|
||||
queue
|
||||
.on("error", (error: any) => {
|
||||
// An error occurred.
|
||||
console.error(`${eventType}=error error=${JSON.stringify(error)}`)
|
||||
})
|
||||
.on("waiting", (jobId: JobId) => {
|
||||
.on(BullEvent.WAITING, (jobId: JobId) => {
|
||||
// A Job is waiting to be processed as soon as a worker is idling.
|
||||
console.log(`${eventType}=waiting jobId=${jobId}`)
|
||||
console.info(...getLogParams(eventType, BullEvent.WAITING, { jobId }))
|
||||
})
|
||||
.on("active", (job: Job, jobPromise: any) => {
|
||||
.on(BullEvent.ACTIVE, async (job: Job, jobPromise: any) => {
|
||||
// A job has started. You can use `jobPromise.cancel()`` to abort it.
|
||||
console.log(`${eventType}=active jobId=${job.id}`)
|
||||
await doInJobContext(job, () => {
|
||||
console.info(...getLogParams(eventType, BullEvent.ACTIVE, { job }))
|
||||
})
|
||||
})
|
||||
.on("stalled", (job: Job) => {
|
||||
// A job has been marked as stalled. This is useful for debugging job
|
||||
// workers that crash or pause the event loop.
|
||||
console.error(
|
||||
`${eventType}=stalled jobId=${job.id} job=${JSON.stringify(job)}`
|
||||
)
|
||||
.on(BullEvent.PROGRESS, async (job: Job, progress: any) => {
|
||||
// A job's progress was updated
|
||||
await doInJobContext(job, () => {
|
||||
console.info(
|
||||
...getLogParams(
|
||||
eventType,
|
||||
BullEvent.PROGRESS,
|
||||
{ job },
|
||||
{ progress }
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
.on("progress", (job: Job, progress: any) => {
|
||||
// A job's progress was updated!
|
||||
console.log(
|
||||
`${eventType}=progress jobId=${job.id} progress=${progress}`
|
||||
)
|
||||
})
|
||||
.on("completed", (job: Job, result) => {
|
||||
.on(BullEvent.COMPLETED, async (job: Job, result) => {
|
||||
// A job successfully completed with a `result`.
|
||||
console.log(`${eventType}=completed jobId=${job.id} result=${result}`)
|
||||
await doInJobContext(job, () => {
|
||||
console.info(
|
||||
...getLogParams(eventType, BullEvent.COMPLETED, { job }, { result })
|
||||
)
|
||||
})
|
||||
})
|
||||
.on("failed", (job, err: any) => {
|
||||
.on(BullEvent.FAILED, async (job: Job, error: any) => {
|
||||
// A job failed with reason `err`!
|
||||
console.log(`${eventType}=failed jobId=${job.id} error=${err}`)
|
||||
await doInJobContext(job, () => {
|
||||
console.error(
|
||||
...getLogParams(eventType, BullEvent.FAILED, { job, error })
|
||||
)
|
||||
})
|
||||
})
|
||||
.on("paused", () => {
|
||||
.on(BullEvent.PAUSED, () => {
|
||||
// The queue has been paused.
|
||||
console.log(`${eventType}=paused`)
|
||||
console.info(...getLogParams(eventType, BullEvent.PAUSED))
|
||||
})
|
||||
.on("resumed", (job: Job) => {
|
||||
.on(BullEvent.RESUMED, () => {
|
||||
// The queue has been resumed.
|
||||
console.log(`${eventType}=paused jobId=${job.id}`)
|
||||
console.info(...getLogParams(eventType, BullEvent.RESUMED))
|
||||
})
|
||||
.on("cleaned", (jobs: Job[], type: string) => {
|
||||
.on(BullEvent.CLEANED, (jobs: Job[], type: string) => {
|
||||
// Old jobs have been cleaned from the queue. `jobs` is an array of cleaned
|
||||
// jobs, and `type` is the type of jobs cleaned.
|
||||
console.log(`${eventType}=cleaned length=${jobs.length} type=${type}`)
|
||||
console.info(
|
||||
...getLogParams(
|
||||
eventType,
|
||||
BullEvent.CLEANED,
|
||||
{},
|
||||
{ length: jobs.length, type }
|
||||
)
|
||||
)
|
||||
})
|
||||
.on("drained", () => {
|
||||
.on(BullEvent.DRAINED, () => {
|
||||
// Emitted every time the queue has processed all the waiting jobs (even if there can be some delayed jobs not yet processed)
|
||||
console.log(`${eventType}=drained`)
|
||||
console.info(...getLogParams(eventType, BullEvent.DRAINED))
|
||||
})
|
||||
.on("removed", (job: Job) => {
|
||||
.on(BullEvent.REMOVED, (job: Job) => {
|
||||
// A job successfully removed.
|
||||
console.log(`${eventType}=removed jobId=${job.id}`)
|
||||
console.info(...getLogParams(eventType, BullEvent.REMOVED, { job }))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,10 @@ import { LockOptions, LockType } from "@budibase/types"
|
|||
import * as context from "../context"
|
||||
import env from "../environment"
|
||||
|
||||
const getClient = async (
|
||||
async function getClient(
|
||||
type: LockType,
|
||||
opts?: Redlock.Options
|
||||
): Promise<Redlock> => {
|
||||
): Promise<Redlock> {
|
||||
if (type === LockType.CUSTOM) {
|
||||
return newRedlock(opts)
|
||||
}
|
||||
|
@ -18,6 +18,9 @@ const getClient = async (
|
|||
case LockType.TRY_ONCE: {
|
||||
return newRedlock(OPTIONS.TRY_ONCE)
|
||||
}
|
||||
case LockType.TRY_TWICE: {
|
||||
return newRedlock(OPTIONS.TRY_TWICE)
|
||||
}
|
||||
case LockType.DEFAULT: {
|
||||
return newRedlock(OPTIONS.DEFAULT)
|
||||
}
|
||||
|
@ -35,6 +38,9 @@ const OPTIONS = {
|
|||
// immediately throws an error if the lock is already held
|
||||
retryCount: 0,
|
||||
},
|
||||
TRY_TWICE: {
|
||||
retryCount: 1,
|
||||
},
|
||||
TEST: {
|
||||
// higher retry count in unit tests
|
||||
// due to high contention.
|
||||
|
@ -62,7 +68,7 @@ const OPTIONS = {
|
|||
},
|
||||
}
|
||||
|
||||
const newRedlock = async (opts: Redlock.Options = {}) => {
|
||||
export async function newRedlock(opts: Redlock.Options = {}) {
|
||||
let options = { ...OPTIONS.DEFAULT, ...opts }
|
||||
const redisWrapper = await getLockClient()
|
||||
const client = redisWrapper.getClient()
|
||||
|
@ -81,22 +87,26 @@ type RedlockExecution<T> =
|
|||
| SuccessfulRedlockExecution<T>
|
||||
| UnsuccessfulRedlockExecution
|
||||
|
||||
export const doWithLock = async <T>(
|
||||
function getLockName(opts: LockOptions) {
|
||||
// determine lock name
|
||||
// by default use the tenantId for uniqueness, unless using a system lock
|
||||
const prefix = opts.systemLock ? "system" : context.getTenantId()
|
||||
let name: string = `lock:${prefix}_${opts.name}`
|
||||
// add additional unique name if required
|
||||
if (opts.resource) {
|
||||
name = name + `_${opts.resource}`
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
export async function doWithLock<T>(
|
||||
opts: LockOptions,
|
||||
task: () => Promise<T>
|
||||
): Promise<RedlockExecution<T>> => {
|
||||
): Promise<RedlockExecution<T>> {
|
||||
const redlock = await getClient(opts.type, opts.customOptions)
|
||||
let lock
|
||||
try {
|
||||
// determine lock name
|
||||
// by default use the tenantId for uniqueness, unless using a system lock
|
||||
const prefix = opts.systemLock ? "system" : context.getTenantId()
|
||||
let name: string = `lock:${prefix}_${opts.name}`
|
||||
|
||||
// add additional unique name if required
|
||||
if (opts.resource) {
|
||||
name = name + `_${opts.resource}`
|
||||
}
|
||||
const name = getLockName(opts)
|
||||
|
||||
// create the lock
|
||||
lock = await redlock.lock(name, opts.ttl)
|
||||
|
@ -112,7 +122,6 @@ export const doWithLock = async <T>(
|
|||
if (opts.type === LockType.TRY_ONCE) {
|
||||
// don't throw for try-once locks, they will always error
|
||||
// due to retry count (0) exceeded
|
||||
console.warn(e)
|
||||
return { executed: false }
|
||||
} else {
|
||||
console.error(e)
|
||||
|
|
|
@ -27,6 +27,7 @@ export enum Databases {
|
|||
GENERIC_CACHE = "data_cache",
|
||||
WRITE_THROUGH = "writeThrough",
|
||||
LOCKS = "locks",
|
||||
SOCKET_IO = "socket_io",
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -90,6 +90,10 @@ export const useScimIntegration = () => {
|
|||
return useFeature(Feature.SCIM)
|
||||
}
|
||||
|
||||
export const useSyncAutomations = () => {
|
||||
return useFeature(Feature.SYNC_AUTOMATIONS)
|
||||
}
|
||||
|
||||
// QUOTAS
|
||||
|
||||
export const setAutomationLogsQuota = (value: number) => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.0",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,8 +38,8 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
||||
"@budibase/shared-core": "0.0.1",
|
||||
"@budibase/string-templates": "0.0.1",
|
||||
"@budibase/shared-core": "0.0.0",
|
||||
"@budibase/string-templates": "0.0.0",
|
||||
"@spectrum-css/accordion": "3.0.24",
|
||||
"@spectrum-css/actionbutton": "1.0.1",
|
||||
"@spectrum-css/actiongroup": "1.0.1",
|
||||
|
@ -84,11 +84,25 @@
|
|||
"@spectrum-css/vars": "3.0.1",
|
||||
"dayjs": "^1.10.4",
|
||||
"easymde": "^2.16.1",
|
||||
"svelte-flatpickr": "^3.3.2",
|
||||
"svelte-flatpickr": "3.2.3",
|
||||
"svelte-portal": "^1.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"loader-utils": "1.4.1"
|
||||
},
|
||||
"nx": {
|
||||
"targets": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": [
|
||||
"@budibase/string-templates"
|
||||
],
|
||||
"target": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
||||
}
|
||||
|
|
|
@ -102,7 +102,9 @@
|
|||
margin-left: 0;
|
||||
transition: color ease-out 130ms;
|
||||
}
|
||||
.is-selected:not(.spectrum-ActionButton--emphasized):not(.spectrum-ActionButton--quiet) {
|
||||
.is-selected:not(.spectrum-ActionButton--emphasized):not(
|
||||
.spectrum-ActionButton--quiet
|
||||
) {
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
border-color: var(--spectrum-global-color-gray-500);
|
||||
}
|
||||
|
|
|
@ -13,10 +13,12 @@
|
|||
export let url = ""
|
||||
export let disabled = false
|
||||
export let initials = "JD"
|
||||
export let color = null
|
||||
|
||||
const DefaultColor = "#3aab87"
|
||||
|
||||
$: color = getColor(initials)
|
||||
$: avatarColor = color || getColor(initials)
|
||||
$: style = getStyle(size, avatarColor)
|
||||
|
||||
const getColor = initials => {
|
||||
if (!initials?.length) {
|
||||
|
@ -26,6 +28,12 @@
|
|||
const hue = ((code % 26) / 26) * 360
|
||||
return `hsl(${hue}, 50%, 50%)`
|
||||
}
|
||||
|
||||
const getStyle = (sizeKey, color) => {
|
||||
const size = `var(${sizes.get(sizeKey)})`
|
||||
const fontSize = `calc(${size} / 2)`
|
||||
return `width:${size}; height:${size}; font-size:${fontSize}; background:${color};`
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if url}
|
||||
|
@ -37,13 +45,7 @@
|
|||
style="width: var({sizes.get(size)}); height: var({sizes.get(size)});"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="spectrum-Avatar"
|
||||
class:is-disabled={disabled}
|
||||
style="width: var({sizes.get(size)}); height: var({sizes.get(
|
||||
size
|
||||
)}); font-size: calc(var({sizes.get(size)}) / 2); background: {color};"
|
||||
>
|
||||
<div class="spectrum-Avatar" class:is-disabled={disabled} {style}>
|
||||
{initials || ""}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import "@spectrum-css/button/dist/index-vars.css"
|
||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||
|
||||
export let type
|
||||
export let disabled = false
|
||||
export let size = "M"
|
||||
export let cta = false
|
||||
|
@ -21,6 +22,7 @@
|
|||
|
||||
<button
|
||||
{id}
|
||||
{type}
|
||||
class:spectrum-Button--cta={cta}
|
||||
class:spectrum-Button--primary={primary}
|
||||
class:spectrum-Button--secondary={secondary}
|
||||
|
@ -73,6 +75,7 @@
|
|||
button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spectrum-Button-label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<script>
|
||||
import { slide } from "svelte/transition"
|
||||
|
||||
export let error = null
|
||||
</script>
|
||||
|
||||
<div transition:slide|local={{ duration: 130 }} class="error-message">
|
||||
{error}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.error-message {
|
||||
background: var(--spectrum-global-color-red-400);
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
padding: 6px 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { slide } from "svelte/transition"
|
||||
import ErrorMessage from "./ErrorMessage.svelte"
|
||||
|
||||
export let disabled = false
|
||||
export let error = null
|
||||
|
@ -55,9 +55,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
{#if error}
|
||||
<div transition:slide|local={{ duration: 130 }} class="error-message">
|
||||
{error}
|
||||
</div>
|
||||
<ErrorMessage {error} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
@ -110,13 +108,6 @@
|
|||
.field {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.error-message {
|
||||
background: var(--spectrum-global-color-red-400);
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
padding: 6px 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.error-icon {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
|
|
@ -4,3 +4,4 @@ export { default as FancySelect } from "./FancySelect.svelte"
|
|||
export { default as FancyButton } from "./FancyButton.svelte"
|
||||
export { default as FancyForm } from "./FancyForm.svelte"
|
||||
export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte"
|
||||
export { default as ErrorMessage } from "./ErrorMessage.svelte"
|
||||
|
|
|
@ -165,7 +165,7 @@
|
|||
{/if}
|
||||
{#if !disabled}
|
||||
<div class="delete-button" on:click={removeFile}>
|
||||
<Icon name="Close" />
|
||||
<Icon name="Delete" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -209,7 +209,7 @@
|
|||
{/if}
|
||||
{#if !disabled}
|
||||
<div class="delete-button" on:click={removeFile}>
|
||||
<Icon name="Close" />
|
||||
<Icon name="Delete" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.0",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -9,7 +9,7 @@
|
|||
"dev:builder": "routify -c dev:vite",
|
||||
"dev:vite": "vite --host 0.0.0.0",
|
||||
"rollup": "rollup -c -w",
|
||||
"test": "vitest"
|
||||
"test": "vitest run"
|
||||
},
|
||||
"jest": {
|
||||
"globals": {
|
||||
|
@ -58,10 +58,11 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "0.0.1",
|
||||
"@budibase/frontend-core": "0.0.1",
|
||||
"@budibase/shared-core": "0.0.1",
|
||||
"@budibase/string-templates": "0.0.1",
|
||||
"@budibase/bbui": "0.0.0",
|
||||
"@budibase/frontend-core": "0.0.0",
|
||||
"@budibase/shared-core": "0.0.0",
|
||||
"@budibase/string-templates": "0.0.0",
|
||||
"@budibase/types": "0.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
|
@ -116,5 +117,31 @@
|
|||
"vite": "^3.0.8",
|
||||
"vitest": "^0.29.2"
|
||||
},
|
||||
"nx": {
|
||||
"targets": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": [
|
||||
"@budibase/string-templates",
|
||||
"@budibase/shared-core"
|
||||
],
|
||||
"target": "build"
|
||||
}
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": [
|
||||
"@budibase/shared-core",
|
||||
"@budibase/string-templates"
|
||||
],
|
||||
"target": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { datasources, tables } from "../stores/backend"
|
|||
import { IntegrationNames } from "../constants/backend"
|
||||
import { get } from "svelte/store"
|
||||
import cloneDeep from "lodash/cloneDeepWith"
|
||||
import { API } from "api"
|
||||
|
||||
function prepareData(config) {
|
||||
let datasource = {}
|
||||
|
@ -37,3 +38,9 @@ export async function createRestDatasource(integration) {
|
|||
const config = cloneDeep(integration)
|
||||
return saveDatasource(config)
|
||||
}
|
||||
|
||||
export async function validateDatasourceConfig(config) {
|
||||
const datasource = prepareData(config)
|
||||
const resp = await API.validateDatasource(datasource)
|
||||
return resp
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { getFrontendStore } from "./store/frontend"
|
|||
import { getAutomationStore } from "./store/automation"
|
||||
import { getTemporalStore } from "./store/temporal"
|
||||
import { getThemeStore } from "./store/theme"
|
||||
import { getUserStore } from "./store/users"
|
||||
import { derived } from "svelte/store"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
@ -12,6 +13,7 @@ export const store = getFrontendStore()
|
|||
export const automationStore = getAutomationStore()
|
||||
export const themeStore = getThemeStore()
|
||||
export const temporalStore = getTemporalStore()
|
||||
export const userStore = getUserStore()
|
||||
|
||||
// Setup history for screens
|
||||
export const screenHistoryStore = createHistoryStore({
|
||||
|
|
|
@ -37,8 +37,10 @@ import {
|
|||
} from "builderStore/dataBinding"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
import { getComponentFieldOptions } from "helpers/formFields"
|
||||
import { createBuilderWebsocket } from "builderStore/websocket"
|
||||
|
||||
const INITIAL_FRONTEND_STATE = {
|
||||
initialised: false,
|
||||
apps: [],
|
||||
name: "",
|
||||
url: "",
|
||||
|
@ -70,6 +72,7 @@ const INITIAL_FRONTEND_STATE = {
|
|||
previewDevice: "desktop",
|
||||
highlightedSettingKey: null,
|
||||
builderSidePanel: false,
|
||||
hasLock: true,
|
||||
|
||||
// URL params
|
||||
selectedScreenId: null,
|
||||
|
@ -86,6 +89,7 @@ const INITIAL_FRONTEND_STATE = {
|
|||
|
||||
export const getFrontendStore = () => {
|
||||
const store = writable({ ...INITIAL_FRONTEND_STATE })
|
||||
let websocket
|
||||
|
||||
// This is a fake implementation of a "patch" API endpoint to try and prevent
|
||||
// 409s. All screen doc mutations (aside from creation) use this function,
|
||||
|
@ -110,10 +114,11 @@ export const getFrontendStore = () => {
|
|||
store.actions = {
|
||||
reset: () => {
|
||||
store.set({ ...INITIAL_FRONTEND_STATE })
|
||||
websocket?.disconnect()
|
||||
},
|
||||
initialise: async pkg => {
|
||||
const { layouts, screens, application, clientLibPath } = pkg
|
||||
|
||||
const { layouts, screens, application, clientLibPath, hasLock } = pkg
|
||||
websocket = createBuilderWebsocket()
|
||||
await store.actions.components.refreshDefinitions(application.appId)
|
||||
|
||||
// Reset store state
|
||||
|
@ -137,6 +142,8 @@ export const getFrontendStore = () => {
|
|||
upgradableVersion: application.upgradableVersion,
|
||||
navigation: application.navigation || {},
|
||||
usedPlugins: application.usedPlugins || [],
|
||||
hasLock,
|
||||
initialised: true,
|
||||
}))
|
||||
screenHistoryStore.reset()
|
||||
automationHistoryStore.reset()
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
|
||||
export const getUserStore = () => {
|
||||
const store = writable([])
|
||||
|
||||
const init = users => {
|
||||
store.set(users)
|
||||
}
|
||||
|
||||
const updateUser = user => {
|
||||
const $users = get(store)
|
||||
if (!$users.some(x => x.sessionId === user.sessionId)) {
|
||||
store.set([...$users, user])
|
||||
} else {
|
||||
store.update(state => {
|
||||
const index = state.findIndex(x => x.sessionId === user.sessionId)
|
||||
state[index] = user
|
||||
return state.slice()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const removeUser = user => {
|
||||
store.update(state => {
|
||||
return state.filter(x => x.sessionId !== user.sessionId)
|
||||
})
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
store.set([])
|
||||
}
|
||||
|
||||
return {
|
||||
...store,
|
||||
actions: {
|
||||
init,
|
||||
updateUser,
|
||||
removeUser,
|
||||
reset,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { ActionStepID } from "constants/backend/automations"
|
||||
import { TableNames } from "../constants"
|
||||
import {
|
||||
AUTO_COLUMN_DISPLAY_NAMES,
|
||||
|
@ -53,3 +54,9 @@ export function buildAutoColumn(tableName, name, subtype) {
|
|||
}
|
||||
return base
|
||||
}
|
||||
|
||||
export function checkForCollectStep(automation) {
|
||||
return automation.definition.steps.some(
|
||||
step => step.stepId === ActionStepID.COLLECT
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { createWebsocket } from "@budibase/frontend-core"
|
||||
import { userStore } from "builderStore"
|
||||
import { datasources, tables } from "stores/backend"
|
||||
|
||||
export const createBuilderWebsocket = () => {
|
||||
const socket = createWebsocket("/socket/builder")
|
||||
|
||||
// Connection events
|
||||
socket.on("connect", () => {
|
||||
socket.emit("get-users", null, response => {
|
||||
userStore.actions.init(response.users)
|
||||
})
|
||||
})
|
||||
socket.on("connect_error", err => {
|
||||
console.log("Failed to connect to builder websocket:", err.message)
|
||||
})
|
||||
|
||||
// User events
|
||||
socket.on("user-update", userStore.actions.updateUser)
|
||||
socket.on("user-disconnect", userStore.actions.removeUser)
|
||||
|
||||
// Table events
|
||||
socket.on("table-change", ({ id, table }) => {
|
||||
tables.replaceTable(id, table)
|
||||
})
|
||||
|
||||
// Datasource events
|
||||
socket.on("datasource-change", ({ id, datasource }) => {
|
||||
datasources.replaceDatasource(id, datasource)
|
||||
})
|
||||
|
||||
return {
|
||||
...socket,
|
||||
disconnect: () => {
|
||||
socket?.disconnect()
|
||||
userStore.actions.reset()
|
||||
},
|
||||
}
|
||||
}
|
|
@ -6,24 +6,48 @@
|
|||
Body,
|
||||
Icon,
|
||||
notifications,
|
||||
Tags,
|
||||
Tag,
|
||||
} from "@budibase/bbui"
|
||||
import { automationStore } from "builderStore"
|
||||
import { admin } from "stores/portal"
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
import { admin, licensing } from "stores/portal"
|
||||
import { externalActions } from "./ExternalActions"
|
||||
import { TriggerStepID } from "constants/backend/automations"
|
||||
import { checkForCollectStep } from "builderStore/utils"
|
||||
|
||||
export let blockIdx
|
||||
export let lastStep
|
||||
|
||||
const disabled = {
|
||||
SEND_EMAIL_SMTP: {
|
||||
disabled: !$admin.checklist.smtp.checked,
|
||||
message: "Please configure SMTP",
|
||||
},
|
||||
}
|
||||
|
||||
let syncAutomationsEnabled = $licensing.syncAutomationsEnabled
|
||||
let collectBlockAllowedSteps = [TriggerStepID.APP, TriggerStepID.WEBHOOK]
|
||||
let selectedAction
|
||||
let actionVal
|
||||
let actions = Object.entries($automationStore.blockDefinitions.ACTION)
|
||||
|
||||
$: collectBlockExists = checkForCollectStep($selectedAutomation)
|
||||
|
||||
const disabled = () => {
|
||||
return {
|
||||
SEND_EMAIL_SMTP: {
|
||||
disabled: !$admin.checklist.smtp.checked,
|
||||
message: "Please configure SMTP",
|
||||
},
|
||||
COLLECT: {
|
||||
disabled: !lastStep || !syncAutomationsEnabled || collectBlockExists,
|
||||
message: collectDisabledMessage(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const collectDisabledMessage = () => {
|
||||
if (collectBlockExists) {
|
||||
return "Only one Collect step allowed"
|
||||
}
|
||||
if (!lastStep) {
|
||||
return "Only available as the last step"
|
||||
}
|
||||
}
|
||||
|
||||
const external = actions.reduce((acc, elm) => {
|
||||
const [k, v] = elm
|
||||
if (!v.internal && !v.custom) {
|
||||
|
@ -38,6 +62,15 @@
|
|||
acc[k] = v
|
||||
}
|
||||
delete acc.LOOP
|
||||
|
||||
// Filter out Collect block if not App Action or Webhook
|
||||
if (
|
||||
!collectBlockAllowedSteps.includes(
|
||||
$selectedAutomation.definition.trigger.stepId
|
||||
)
|
||||
) {
|
||||
delete acc.COLLECT
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
|
@ -48,7 +81,6 @@
|
|||
}
|
||||
return acc
|
||||
}, {})
|
||||
console.log(plugins)
|
||||
|
||||
const selectAction = action => {
|
||||
actionVal = action
|
||||
|
@ -72,7 +104,7 @@
|
|||
<ModalContent
|
||||
title="Add automation step"
|
||||
confirmText="Save"
|
||||
size="M"
|
||||
size="L"
|
||||
disabled={!selectedAction}
|
||||
onConfirm={addBlockToAutomation}
|
||||
>
|
||||
|
@ -107,7 +139,7 @@
|
|||
<Detail size="S">Actions</Detail>
|
||||
<div class="item-list">
|
||||
{#each Object.entries(internal) as [idx, action]}
|
||||
{@const isDisabled = disabled[idx] && disabled[idx].disabled}
|
||||
{@const isDisabled = disabled()[idx] && disabled()[idx].disabled}
|
||||
<div
|
||||
class="item"
|
||||
class:disabled={isDisabled}
|
||||
|
@ -117,8 +149,14 @@
|
|||
<div class="item-body">
|
||||
<Icon name={action.icon} />
|
||||
<Body size="XS">{action.name}</Body>
|
||||
{#if isDisabled}
|
||||
<Icon name="Help" tooltip={disabled[idx].message} />
|
||||
{#if isDisabled && !syncAutomationsEnabled}
|
||||
<div class="tag-color">
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Business</Tag>
|
||||
</Tags>
|
||||
</div>
|
||||
{:else if isDisabled}
|
||||
<Icon name="Help" tooltip={disabled()[idx].message} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -152,6 +190,7 @@
|
|||
display: flex;
|
||||
margin-left: var(--spacing-m);
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
.item-list {
|
||||
display: grid;
|
||||
|
@ -181,4 +220,8 @@
|
|||
.disabled :global(.spectrum-Body) {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
.tag-color :global(.spectrum-Tags-item) {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -17,7 +17,11 @@
|
|||
import ActionModal from "./ActionModal.svelte"
|
||||
import FlowItemHeader from "./FlowItemHeader.svelte"
|
||||
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
||||
import { ActionStepID, TriggerStepID } from "constants/backend/automations"
|
||||
import {
|
||||
ActionStepID,
|
||||
TriggerStepID,
|
||||
Features,
|
||||
} from "constants/backend/automations"
|
||||
import { permissions } from "stores/backend"
|
||||
|
||||
export let block
|
||||
|
@ -31,6 +35,9 @@
|
|||
let showLooping = false
|
||||
let role
|
||||
|
||||
$: collectBlockExists = $selectedAutomation.definition.steps.some(
|
||||
step => step.stepId === ActionStepID.COLLECT
|
||||
)
|
||||
$: automationId = $selectedAutomation?._id
|
||||
$: showBindingPicker =
|
||||
block.stepId === ActionStepID.CREATE_ROW ||
|
||||
|
@ -184,7 +191,7 @@
|
|||
{#if !isTrigger}
|
||||
<div>
|
||||
<div class="block-options">
|
||||
{#if !loopBlock}
|
||||
{#if block?.features?.[Features.LOOPING] || !block.features}
|
||||
<ActionButton on:click={() => addLooping()} icon="Reuse">
|
||||
Add Looping
|
||||
</ActionButton>
|
||||
|
@ -224,21 +231,28 @@
|
|||
</Layout>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Modal bind:this={actionModal} width="30%">
|
||||
<ActionModal {blockIdx} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={webhookModal} width="30%">
|
||||
<CreateWebhookModal />
|
||||
</Modal>
|
||||
</div>
|
||||
<div class="separator" />
|
||||
<Icon on:click={() => actionModal.show()} hoverable name="AddCircle" size="S" />
|
||||
{#if isTrigger ? totalBlocks > 1 : blockIdx !== totalBlocks - 2}
|
||||
{#if !collectBlockExists || !lastStep}
|
||||
<div class="separator" />
|
||||
<Icon
|
||||
on:click={() => actionModal.show()}
|
||||
hoverable
|
||||
name="AddCircle"
|
||||
size="S"
|
||||
/>
|
||||
{#if isTrigger ? totalBlocks > 1 : blockIdx !== totalBlocks - 2}
|
||||
<div class="separator" />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<Modal bind:this={actionModal} width="30%">
|
||||
<ActionModal {lastStep} {blockIdx} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={webhookModal} width="30%">
|
||||
<CreateWebhookModal />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.delete-padding {
|
||||
padding-left: 30px;
|
||||
|
|
|
@ -16,11 +16,11 @@
|
|||
import GridEditColumnModal from "components/backend/DataTable/modals/grid/GridEditColumnModal.svelte"
|
||||
|
||||
const userSchemaOverrides = {
|
||||
firstName: { name: "First name", disabled: true },
|
||||
lastName: { name: "Last name", disabled: true },
|
||||
email: { name: "Email", disabled: true },
|
||||
roleId: { name: "Role", disabled: true },
|
||||
status: { name: "Status", disabled: true },
|
||||
firstName: { displayName: "First name", disabled: true },
|
||||
lastName: { displayName: "Last name", disabled: true },
|
||||
email: { displayName: "Email", disabled: true },
|
||||
roleId: { displayName: "Role", disabled: true },
|
||||
status: { displayName: "Status", disabled: true },
|
||||
}
|
||||
|
||||
$: id = $tables.selected?._id
|
||||
|
@ -32,10 +32,11 @@
|
|||
<Grid
|
||||
{API}
|
||||
tableId={id}
|
||||
tableType={$tables.selected?.type}
|
||||
allowAddRows={!isUsersTable}
|
||||
allowDeleteRows={!isUsersTable}
|
||||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||
on:updatetable={e => tables.updateTable(e.detail)}
|
||||
showAvatars={false}
|
||||
>
|
||||
<svelte:fragment slot="controls">
|
||||
{#if isInternal}
|
||||
|
|
|
@ -111,7 +111,7 @@
|
|||
{rowCount}
|
||||
{disableSorting}
|
||||
{customPlaceholder}
|
||||
{allowEditing}
|
||||
allowEditRows={allowEditing}
|
||||
showAutoColumns={!hideAutocolumns}
|
||||
{allowClickRows}
|
||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
{loading}
|
||||
{type}
|
||||
rowCount={10}
|
||||
allowEditing={false}
|
||||
bind:hideAutocolumns
|
||||
>
|
||||
<ViewFilterButton {view} />
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import ImportModal from "../modals/ImportModal.svelte"
|
||||
|
||||
export let tableId
|
||||
export let tableType
|
||||
export let disabled
|
||||
|
||||
let modal
|
||||
|
@ -12,5 +13,5 @@
|
|||
Import
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<ImportModal {tableId} on:importrows />
|
||||
<ImportModal {tableId} {tableType} on:importrows />
|
||||
</Modal>
|
||||
|
|
|
@ -4,11 +4,12 @@
|
|||
|
||||
export let disabled = false
|
||||
|
||||
const { rows, tableId } = getContext("grid")
|
||||
const { rows, tableId, tableType } = getContext("grid")
|
||||
</script>
|
||||
|
||||
<ImportButton
|
||||
{disabled}
|
||||
tableId={$tableId}
|
||||
{tableType}
|
||||
on:importrows={rows.actions.refreshData}
|
||||
/>
|
||||
|
|
|
@ -113,17 +113,26 @@
|
|||
})
|
||||
download(data, `export.${exportFormat}`)
|
||||
} else if (filters || sorting) {
|
||||
const data = await API.exportRows({
|
||||
tableId: view,
|
||||
format: exportFormat,
|
||||
search: {
|
||||
query: luceneFilter,
|
||||
sort: sorting?.sortColumn,
|
||||
sortOrder: sorting?.sortOrder,
|
||||
paginate: false,
|
||||
},
|
||||
})
|
||||
download(data, `export.${exportFormat}`)
|
||||
let response
|
||||
try {
|
||||
response = await API.exportRows({
|
||||
tableId: view,
|
||||
format: exportFormat,
|
||||
search: {
|
||||
query: luceneFilter,
|
||||
sort: sorting?.sortColumn,
|
||||
sortOrder: sorting?.sortOrder,
|
||||
paginate: false,
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
console.error("Failed to export", e)
|
||||
notifications.error("Export Failed")
|
||||
}
|
||||
if (response) {
|
||||
download(response, `export.${exportFormat}`)
|
||||
notifications.success("Export Successful")
|
||||
}
|
||||
} else {
|
||||
await exportView()
|
||||
}
|
||||
|
|
|
@ -13,15 +13,18 @@
|
|||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let tableId
|
||||
export let tableType
|
||||
let rows = []
|
||||
let allValid = false
|
||||
let displayColumn = null
|
||||
let identifierFields = []
|
||||
|
||||
async function importData() {
|
||||
try {
|
||||
await API.importTableData({
|
||||
tableId,
|
||||
rows,
|
||||
identifierFields,
|
||||
})
|
||||
notifications.success("Rows successfully imported")
|
||||
} catch (error) {
|
||||
|
@ -45,6 +48,13 @@
|
|||
</Body>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Label grey extraSmall>CSV or JSON file to import</Label>
|
||||
<TableDataImport {tableId} bind:rows bind:allValid bind:displayColumn />
|
||||
<TableDataImport
|
||||
{tableId}
|
||||
{tableType}
|
||||
bind:rows
|
||||
bind:allValid
|
||||
bind:displayColumn
|
||||
bind:identifierFields
|
||||
/>
|
||||
</Layout>
|
||||
</ModalContent>
|
||||
|
|
|
@ -1,254 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
ModalContent,
|
||||
Modal,
|
||||
Body,
|
||||
Layout,
|
||||
Detail,
|
||||
Heading,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import ICONS from "../icons"
|
||||
import { API } from "api"
|
||||
import { IntegrationTypes, DatasourceTypes } from "constants/backend"
|
||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||
import GoogleDatasourceConfigModal from "components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte"
|
||||
import { createRestDatasource } from "builderStore/datasource"
|
||||
import { goto } from "@roxi/routify"
|
||||
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
||||
import DatasourceCard from "../_components/DatasourceCard.svelte"
|
||||
|
||||
export let modal
|
||||
let integrations = {}
|
||||
let integration = {}
|
||||
let internalTableModal
|
||||
let externalDatasourceModal
|
||||
let importModal
|
||||
|
||||
$: showImportButton = false
|
||||
$: customIntegrations = Object.entries(integrations).filter(
|
||||
entry => entry[1].custom
|
||||
)
|
||||
$: sortedIntegrations = sortIntegrations(integrations)
|
||||
|
||||
checkShowImport()
|
||||
|
||||
onMount(() => {
|
||||
fetchIntegrations()
|
||||
})
|
||||
|
||||
function selectIntegration(integrationType) {
|
||||
const selected = integrations[integrationType]
|
||||
|
||||
// build the schema
|
||||
const config = {}
|
||||
for (let key of Object.keys(selected.datasource)) {
|
||||
config[key] = selected.datasource[key].default
|
||||
}
|
||||
integration = {
|
||||
type: integrationType,
|
||||
plus: selected.plus,
|
||||
config,
|
||||
schema: selected.datasource,
|
||||
auth: selected.auth,
|
||||
}
|
||||
if (selected.friendlyName) {
|
||||
integration.name = selected.friendlyName
|
||||
}
|
||||
checkShowImport()
|
||||
}
|
||||
|
||||
function checkShowImport() {
|
||||
showImportButton = integration.type === "REST"
|
||||
}
|
||||
|
||||
function showImportModal() {
|
||||
importModal.show()
|
||||
}
|
||||
|
||||
async function chooseNextModal() {
|
||||
if (integration.type === IntegrationTypes.INTERNAL) {
|
||||
externalDatasourceModal.hide()
|
||||
internalTableModal.show()
|
||||
} else if (integration.type === IntegrationTypes.REST) {
|
||||
try {
|
||||
// Skip modal for rest, create straight away
|
||||
const resp = await createRestDatasource(integration)
|
||||
$goto(`./datasource/${resp._id}`)
|
||||
} catch (error) {
|
||||
notifications.error("Error creating datasource")
|
||||
}
|
||||
} else {
|
||||
externalDatasourceModal.show()
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchIntegrations() {
|
||||
let newIntegrations = {
|
||||
[IntegrationTypes.INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
||||
}
|
||||
try {
|
||||
const integrationList = await API.getIntegrations()
|
||||
newIntegrations = {
|
||||
...newIntegrations,
|
||||
...integrationList,
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error fetching integrations")
|
||||
}
|
||||
integrations = newIntegrations
|
||||
}
|
||||
|
||||
function sortIntegrations(integrations) {
|
||||
let integrationsArray = Object.entries(integrations)
|
||||
function getTypeOrder(schema) {
|
||||
if (schema.type === DatasourceTypes.API) {
|
||||
return 1
|
||||
}
|
||||
if (schema.type === DatasourceTypes.RELATIONAL) {
|
||||
return 2
|
||||
}
|
||||
return schema.type?.charCodeAt(0)
|
||||
}
|
||||
|
||||
integrationsArray.sort((a, b) => {
|
||||
let typeOrderA = getTypeOrder(a[1])
|
||||
let typeOrderB = getTypeOrder(b[1])
|
||||
if (typeOrderA === typeOrderB) {
|
||||
return a[1].friendlyName?.localeCompare(b[1].friendlyName)
|
||||
}
|
||||
return typeOrderA < typeOrderB ? -1 : 1
|
||||
})
|
||||
return integrationsArray
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={internalTableModal}>
|
||||
<CreateTableModal />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={externalDatasourceModal}>
|
||||
{#if integration?.auth?.type === "google"}
|
||||
<GoogleDatasourceConfigModal {integration} {modal} />
|
||||
{:else}
|
||||
<DatasourceConfigModal {integration} {modal} />
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={importModal}>
|
||||
{#if integration.type === "REST"}
|
||||
<ImportRestQueriesModal
|
||||
navigateDatasource={true}
|
||||
createDatasource={true}
|
||||
onCancel={() => modal.show()}
|
||||
/>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<ModalContent
|
||||
disabled={!Object.keys(integration).length}
|
||||
title="Add datasource"
|
||||
confirmText="Continue"
|
||||
showSecondaryButton={showImportButton}
|
||||
secondaryButtonText="Import"
|
||||
secondaryAction={() => showImportModal()}
|
||||
showCancelButton={false}
|
||||
size="M"
|
||||
onConfirm={() => {
|
||||
chooseNextModal()
|
||||
}}
|
||||
>
|
||||
<Layout noPadding gap="XS">
|
||||
<Body size="S">Get started with Budibase DB</Body>
|
||||
<div
|
||||
class:selected={integration.type === IntegrationTypes.INTERNAL}
|
||||
on:click={() => selectIntegration(IntegrationTypes.INTERNAL)}
|
||||
class="item hoverable"
|
||||
>
|
||||
<div class="item-body with-type">
|
||||
<svelte:component this={ICONS.BUDIBASE} height="20" width="20" />
|
||||
<div class="text">
|
||||
<Heading size="XXS">Budibase DB</Heading>
|
||||
<Detail size="S" class="type">Non-relational</Detail>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<Layout noPadding gap="XS">
|
||||
<Body size="S">Connect to an external datasource</Body>
|
||||
<div class="item-list">
|
||||
{#each sortedIntegrations.filter(([key, val]) => key !== IntegrationTypes.INTERNAL && !val.custom) as [integrationType, schema]}
|
||||
<DatasourceCard
|
||||
on:selected={evt => selectIntegration(evt.detail)}
|
||||
{schema}
|
||||
bind:integrationType
|
||||
{integration}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
{#if customIntegrations.length > 0}
|
||||
<Layout noPadding gap="XS">
|
||||
<Body size="S">Custom datasource</Body>
|
||||
<div class="item-list">
|
||||
{#each customIntegrations as [integrationType, schema]}
|
||||
<DatasourceCard
|
||||
on:selected={evt => selectIntegration(evt.detail)}
|
||||
{schema}
|
||||
bind:integrationType
|
||||
{integration}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.item-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(150px, 1fr));
|
||||
grid-gap: var(--spectrum-alias-grid-baseline);
|
||||
}
|
||||
|
||||
.item {
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
||||
padding: var(--spectrum-alias-item-padding-s)
|
||||
var(--spectrum-alias-item-padding-m);
|
||||
background: var(--spectrum-alias-background-color-secondary);
|
||||
transition: background 0.13s ease-out;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
border-width: 2px;
|
||||
}
|
||||
.item:hover,
|
||||
.item.selected {
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
}
|
||||
|
||||
.item-body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.item-body.with-type {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.item-body.with-type :global(svg) {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.text :global(.spectrum-Detail) {
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
}
|
||||
</style>
|
|
@ -4,55 +4,66 @@
|
|||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||
import { IntegrationNames } from "constants/backend"
|
||||
import cloneDeep from "lodash/cloneDeepWith"
|
||||
import { saveDatasource as save } from "builderStore/datasource"
|
||||
import { onMount } from "svelte"
|
||||
import {
|
||||
saveDatasource as save,
|
||||
validateDatasourceConfig,
|
||||
} from "builderStore/datasource"
|
||||
import { DatasourceFeature } from "@budibase/types"
|
||||
|
||||
export let integration
|
||||
export let modal
|
||||
|
||||
// kill the reference so the input isn't saved
|
||||
let datasource = cloneDeep(integration)
|
||||
let skipFetch = false
|
||||
let isValid = false
|
||||
|
||||
$: name =
|
||||
IntegrationNames[datasource.type] || datasource.name || datasource.type
|
||||
|
||||
async function validateConfig() {
|
||||
const displayError = message =>
|
||||
notifications.error(message ?? "Error validating datasource")
|
||||
|
||||
let connected = false
|
||||
try {
|
||||
const resp = await validateDatasourceConfig(datasource)
|
||||
if (!resp.connected) {
|
||||
displayError(`Unable to connect - ${resp.error}`)
|
||||
}
|
||||
connected = resp.connected
|
||||
} catch (err) {
|
||||
displayError(err?.message)
|
||||
}
|
||||
return connected
|
||||
}
|
||||
|
||||
async function saveDatasource() {
|
||||
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||
const valid = await validateConfig()
|
||||
if (!valid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (!datasource.name) {
|
||||
datasource.name = name
|
||||
}
|
||||
const resp = await save(datasource, skipFetch)
|
||||
const resp = await save(datasource)
|
||||
$goto(`./datasource/${resp._id}`)
|
||||
notifications.success(`Datasource updated successfully.`)
|
||||
notifications.success(`Datasource created successfully.`)
|
||||
} catch (err) {
|
||||
notifications.error(err?.message ?? "Error saving datasource")
|
||||
// prevent the modal from closing
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
skipFetch = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={`Connect to ${name}`}
|
||||
onConfirm={() => saveDatasource()}
|
||||
onCancel={() => modal.show()}
|
||||
confirmText={datasource.plus
|
||||
? "Save and fetch tables"
|
||||
: "Save and continue to query"}
|
||||
confirmText={datasource.plus ? "Connect" : "Save and continue to query"}
|
||||
cancelText="Back"
|
||||
showSecondaryButton={datasource.plus}
|
||||
secondaryButtonText={datasource.plus ? "Skip table fetch" : undefined}
|
||||
secondaryAction={() => {
|
||||
skipFetch = true
|
||||
saveDatasource()
|
||||
return true
|
||||
}}
|
||||
size="L"
|
||||
disabled={!isValid}
|
||||
>
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { onMount } from "svelte"
|
||||
|
||||
export let integration
|
||||
export let modal
|
||||
|
||||
// kill the reference so the input isn't saved
|
||||
let datasource = cloneDeep(integration)
|
||||
|
@ -21,7 +20,6 @@
|
|||
|
||||
<ModalContent
|
||||
title={`Connect to ${IntegrationNames[datasource.type]}`}
|
||||
onCancel={() => modal.show()}
|
||||
cancelText="Back"
|
||||
size="L"
|
||||
>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { Select, Toggle, Multiselect } from "@budibase/bbui"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import { API } from "api"
|
||||
import { parseFile } from "./utils"
|
||||
|
@ -9,14 +9,17 @@
|
|||
let fileType = null
|
||||
|
||||
let loading = false
|
||||
let updateExistingRows = false
|
||||
let validation = {}
|
||||
let validateHash = ""
|
||||
let schema = null
|
||||
let invalidColumns = []
|
||||
|
||||
export let tableId = null
|
||||
export let tableType
|
||||
export let rows = []
|
||||
export let allValid = false
|
||||
export let identifierFields = []
|
||||
|
||||
const typeOptions = [
|
||||
{
|
||||
|
@ -159,6 +162,22 @@
|
|||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if tableType === "internal"}
|
||||
<br />
|
||||
<Toggle
|
||||
bind:value={updateExistingRows}
|
||||
on:change={() => (identifierFields = [])}
|
||||
thin
|
||||
text="Update existing rows"
|
||||
/>
|
||||
{#if updateExistingRows}
|
||||
<Multiselect
|
||||
label="Identifier field(s)"
|
||||
options={Object.keys(validation)}
|
||||
bind:value={identifierFields}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if invalidColumns.length > 0}
|
||||
<p class="spectrum-FieldLabel spectrum-FieldLabel--sizeM">
|
||||
The following columns are present in the data you wish to import, but do
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { API } from "api"
|
||||
import { parseFile } from "./utils"
|
||||
|
||||
let fileInput
|
||||
let error = null
|
||||
let fileName = null
|
||||
let fileType = null
|
||||
|
@ -16,6 +17,7 @@
|
|||
export let schema = {}
|
||||
export let allValid = true
|
||||
export let displayColumn = null
|
||||
export let promptUpload = false
|
||||
|
||||
const typeOptions = [
|
||||
{
|
||||
|
@ -99,10 +101,19 @@
|
|||
schema[name].type = e.detail
|
||||
schema[name].constraints = FIELDS[e.detail.toUpperCase()].constraints
|
||||
}
|
||||
|
||||
const openFileUpload = (promptUpload, fileInput) => {
|
||||
if (promptUpload && fileInput) {
|
||||
fileInput.click()
|
||||
}
|
||||
}
|
||||
|
||||
$: openFileUpload(promptUpload, fileInput)
|
||||
</script>
|
||||
|
||||
<div class="dropzone">
|
||||
<input
|
||||
bind:this={fileInput}
|
||||
disabled={loading}
|
||||
id="file-upload"
|
||||
accept="text/csv,application/json"
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
import NavItem from "components/common/NavItem.svelte"
|
||||
import { goto, isActive } from "@roxi/routify"
|
||||
|
||||
const alphabetical = (a, b) => a.name?.toLowerCase() > b.name?.toLowerCase()
|
||||
const alphabetical = (a, b) =>
|
||||
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
||||
|
||||
export let sourceId
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
? selectedSource._id
|
||||
: BUDIBASE_INTERNAL_DB_ID
|
||||
|
||||
export let promptUpload = false
|
||||
export let name
|
||||
export let beforeSave = async () => {}
|
||||
export let afterSave = async table => {
|
||||
|
@ -136,7 +137,13 @@
|
|||
<Label grey extraSmall
|
||||
>Create a Table from a CSV or JSON file (Optional)</Label
|
||||
>
|
||||
<TableDataImport bind:rows bind:schema bind:allValid bind:displayColumn />
|
||||
<TableDataImport
|
||||
{promptUpload}
|
||||
bind:rows
|
||||
bind:schema
|
||||
bind:allValid
|
||||
bind:displayColumn
|
||||
/>
|
||||
</Layout>
|
||||
</div>
|
||||
</ModalContent>
|
||||
|
|
|
@ -1,143 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ModalContent,
|
||||
Modal,
|
||||
notifications,
|
||||
ProgressCircle,
|
||||
Layout,
|
||||
Body,
|
||||
Icon,
|
||||
} from "@budibase/bbui"
|
||||
import { auth, apps } from "stores/portal"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import { API } from "api"
|
||||
|
||||
export let app
|
||||
export let buttonSize = "M"
|
||||
|
||||
let APP_DEV_LOCK_SECONDS = 600 //common area for this?
|
||||
let appLockModal
|
||||
let processing = false
|
||||
|
||||
$: lockedBy = app?.lockedBy
|
||||
$: lockedByYou = $auth.user.email === lockedBy?.email
|
||||
|
||||
$: lockIdentifer = `${
|
||||
lockedBy && lockedBy.firstName ? lockedBy?.firstName : lockedBy?.email
|
||||
}`
|
||||
|
||||
$: lockedByHeading =
|
||||
lockedBy && lockedByYou ? "Locked by you" : `Locked by ${lockIdentifer}`
|
||||
|
||||
const getExpiryDuration = app => {
|
||||
if (!app?.lockedBy?.lockedAt) {
|
||||
return -1
|
||||
}
|
||||
let expiry =
|
||||
new Date(app.lockedBy.lockedAt).getTime() + APP_DEV_LOCK_SECONDS * 1000
|
||||
return expiry - new Date().getTime()
|
||||
}
|
||||
|
||||
const releaseLock = async () => {
|
||||
processing = true
|
||||
if (app) {
|
||||
try {
|
||||
await API.releaseAppLock(app.devId)
|
||||
await apps.load()
|
||||
notifications.success("Lock released successfully")
|
||||
} catch (err) {
|
||||
notifications.error("Error releasing lock")
|
||||
}
|
||||
} else {
|
||||
notifications.error("No application is selected")
|
||||
}
|
||||
processing = false
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if lockedBy}
|
||||
<div class="lock-status">
|
||||
<Icon
|
||||
name="LockClosed"
|
||||
hoverable
|
||||
size={buttonSize}
|
||||
on:click={e => {
|
||||
e.stopPropagation()
|
||||
appLockModal.show()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Modal bind:this={appLockModal}>
|
||||
<ModalContent
|
||||
title={lockedByHeading}
|
||||
showConfirmButton={false}
|
||||
showCancelButton={false}
|
||||
>
|
||||
<Layout noPadding>
|
||||
<Body size="S">
|
||||
Apps are locked to prevent work being lost from overlapping changes
|
||||
between your team.
|
||||
</Body>
|
||||
{#if lockedByYou && getExpiryDuration(app) > 0}
|
||||
<span class="lock-expiry-body">
|
||||
{processStringSync(
|
||||
"This lock will expire in {{ duration time 'millisecond' }} from now.",
|
||||
{
|
||||
time: getExpiryDuration(app),
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
{/if}
|
||||
<div class="lock-modal-actions">
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
secondary
|
||||
quiet={lockedBy && lockedByYou}
|
||||
disabled={processing}
|
||||
on:click={() => {
|
||||
appLockModal.hide()
|
||||
}}
|
||||
>
|
||||
<span class="cancel"
|
||||
>{lockedBy && !lockedByYou ? "Done" : "Cancel"}</span
|
||||
>
|
||||
</Button>
|
||||
{#if lockedByYou}
|
||||
<Button
|
||||
cta
|
||||
disabled={processing}
|
||||
on:click={() => {
|
||||
releaseLock()
|
||||
appLockModal.hide()
|
||||
}}
|
||||
>
|
||||
{#if processing}
|
||||
<ProgressCircle overBackground={true} size="S" />
|
||||
{:else}
|
||||
<span class="unlock">Release Lock</span>
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</Layout>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.lock-modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: var(--spacing-l);
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
.lock-status {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
max-width: 175px;
|
||||
}
|
||||
</style>
|
|
@ -146,15 +146,18 @@
|
|||
|
||||
/* Override default active line highlight colour in dark theme */
|
||||
div
|
||||
:global(.CodeMirror-focused.cm-s-tomorrow-night-eighties
|
||||
.CodeMirror-activeline-background) {
|
||||
:global(
|
||||
.CodeMirror-focused.cm-s-tomorrow-night-eighties
|
||||
.CodeMirror-activeline-background
|
||||
) {
|
||||
background: rgba(255, 255, 255, 0.075);
|
||||
}
|
||||
|
||||
/* Remove active line styling when not focused */
|
||||
div
|
||||
:global(.CodeMirror:not(.CodeMirror-focused)
|
||||
.CodeMirror-activeline-background) {
|
||||
:global(
|
||||
.CodeMirror:not(.CodeMirror-focused) .CodeMirror-activeline-background
|
||||
) {
|
||||
background: unset;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
faLock,
|
||||
faFileArrowUp,
|
||||
faChevronLeft,
|
||||
faCircleInfo,
|
||||
} from "@fortawesome/free-solid-svg-icons"
|
||||
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
||||
|
||||
|
@ -20,7 +21,8 @@
|
|||
faDiscord,
|
||||
faEnvelope,
|
||||
faFileArrowUp,
|
||||
faChevronLeft
|
||||
faChevronLeft,
|
||||
faCircleInfo
|
||||
)
|
||||
dom.watch()
|
||||
</script>
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
export let highlighted = false
|
||||
export let rightAlignIcon = false
|
||||
export let id
|
||||
export let showTooltip = false
|
||||
|
||||
const scrollApi = getContext("scroll")
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -84,7 +85,7 @@
|
|||
<Icon color={iconColor} size="S" name={icon} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text">{text}</div>
|
||||
<div class="text" title={showTooltip ? text : null}>{text}</div>
|
||||
{#if withActions}
|
||||
<div class="actions">
|
||||
<slot />
|
||||
|
|
|
@ -113,109 +113,113 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<div class="action-top-nav">
|
||||
<div class="action-buttons">
|
||||
<div class="version">
|
||||
<VersionModal />
|
||||
</div>
|
||||
<RevertModal />
|
||||
|
||||
{#if isPublished}
|
||||
<div class="publish-popover">
|
||||
<div bind:this={publishPopoverAnchor}>
|
||||
<ActionButton
|
||||
quiet
|
||||
icon="Globe"
|
||||
size="M"
|
||||
tooltip="Your published app"
|
||||
on:click={publishPopover.show()}
|
||||
/>
|
||||
</div>
|
||||
<Popover
|
||||
bind:this={publishPopover}
|
||||
align="right"
|
||||
disabled={!isPublished}
|
||||
anchor={publishPopoverAnchor}
|
||||
offset={10}
|
||||
>
|
||||
<div class="popover-content">
|
||||
<Layout noPadding gap="M">
|
||||
<Heading size="XS">Your published app</Heading>
|
||||
<Body size="S">
|
||||
<span class="publish-popover-message">
|
||||
{processStringSync(
|
||||
"Last published {{ duration time 'millisecond' }} ago",
|
||||
{
|
||||
time:
|
||||
new Date().getTime() -
|
||||
new Date(latestDeployments[0].updatedAt).getTime(),
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</Body>
|
||||
<div class="buttons">
|
||||
<Button
|
||||
warning={true}
|
||||
icon="GlobeStrike"
|
||||
disabled={!isPublished}
|
||||
on:click={unpublishApp}
|
||||
>
|
||||
Unpublish
|
||||
</Button>
|
||||
<Button cta on:click={viewApp}>View app</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
</Popover>
|
||||
{#if $store.hasLock}
|
||||
<div class="action-top-nav">
|
||||
<div class="action-buttons">
|
||||
<div class="version">
|
||||
<VersionModal />
|
||||
</div>
|
||||
{/if}
|
||||
<RevertModal />
|
||||
|
||||
{#if !isPublished}
|
||||
<ActionButton
|
||||
quiet
|
||||
icon="GlobeStrike"
|
||||
size="M"
|
||||
tooltip="Your app has not been published yet"
|
||||
disabled
|
||||
/>
|
||||
{/if}
|
||||
{#if isPublished}
|
||||
<div class="publish-popover">
|
||||
<div bind:this={publishPopoverAnchor}>
|
||||
<ActionButton
|
||||
quiet
|
||||
icon="Globe"
|
||||
size="M"
|
||||
tooltip="Your published app"
|
||||
on:click={publishPopover.show()}
|
||||
/>
|
||||
</div>
|
||||
<Popover
|
||||
bind:this={publishPopover}
|
||||
align="right"
|
||||
disabled={!isPublished}
|
||||
anchor={publishPopoverAnchor}
|
||||
offset={10}
|
||||
>
|
||||
<div class="popover-content">
|
||||
<Layout noPadding gap="M">
|
||||
<Heading size="XS">Your published app</Heading>
|
||||
<Body size="S">
|
||||
<span class="publish-popover-message">
|
||||
{processStringSync(
|
||||
"Last published {{ duration time 'millisecond' }} ago",
|
||||
{
|
||||
time:
|
||||
new Date().getTime() -
|
||||
new Date(latestDeployments[0].updatedAt).getTime(),
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</Body>
|
||||
<div class="buttons">
|
||||
<Button
|
||||
warning={true}
|
||||
icon="GlobeStrike"
|
||||
disabled={!isPublished}
|
||||
on:click={unpublishApp}
|
||||
>
|
||||
Unpublish
|
||||
</Button>
|
||||
<Button cta on:click={viewApp}>View app</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<TourWrap
|
||||
tourStepKey={$store.onboarding
|
||||
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
|
||||
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
|
||||
>
|
||||
<span id="builder-app-users-button">
|
||||
{#if !isPublished}
|
||||
<ActionButton
|
||||
quiet
|
||||
icon="UserGroup"
|
||||
icon="GlobeStrike"
|
||||
size="M"
|
||||
on:click={() => {
|
||||
store.update(state => {
|
||||
state.builderSidePanel = true
|
||||
return state
|
||||
})
|
||||
}}
|
||||
>
|
||||
Users
|
||||
</ActionButton>
|
||||
</span>
|
||||
</TourWrap>
|
||||
</div>
|
||||
</div>
|
||||
tooltip="Your app has not been published yet"
|
||||
disabled
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={unpublishModal}
|
||||
title="Confirm unpublish"
|
||||
okText="Unpublish app"
|
||||
onOk={confirmUnpublishApp}
|
||||
>
|
||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||
</ConfirmDialog>
|
||||
<TourWrap
|
||||
tourStepKey={$store.onboarding
|
||||
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
|
||||
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
|
||||
>
|
||||
<span id="builder-app-users-button">
|
||||
<ActionButton
|
||||
quiet
|
||||
icon="UserGroup"
|
||||
size="M"
|
||||
on:click={() => {
|
||||
store.update(state => {
|
||||
state.builderSidePanel = true
|
||||
return state
|
||||
})
|
||||
}}
|
||||
>
|
||||
Users
|
||||
</ActionButton>
|
||||
</span>
|
||||
</TourWrap>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={unpublishModal}
|
||||
title="Confirm unpublish"
|
||||
okText="Unpublish app"
|
||||
onOk={confirmUnpublishApp}
|
||||
>
|
||||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||
</ConfirmDialog>
|
||||
{/if}
|
||||
|
||||
<div class="buttons">
|
||||
<Button on:click={previewApp} secondary>Preview</Button>
|
||||
<DeployModal onOk={completePublish} />
|
||||
{#if $store.hasLock}
|
||||
<DeployModal onOk={completePublish} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
transition: width 130ms ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
.panel.borderLeft {
|
||||
border-left: var(--border-light);
|
||||
|
|
|
@ -126,8 +126,7 @@
|
|||
}
|
||||
|
||||
const getAllBindings = (bindings, eventContextBindings, actions) => {
|
||||
let allBindings = eventContextBindings.concat(bindings)
|
||||
|
||||
let allBindings = []
|
||||
if (!actions) {
|
||||
return []
|
||||
}
|
||||
|
@ -145,14 +144,35 @@
|
|||
.forEach(action => {
|
||||
// Check we have a binding for this action, and generate one if not
|
||||
const stateBinding = makeStateBinding(action.parameters.key)
|
||||
const hasKey = allBindings.some(binding => {
|
||||
const hasKey = bindings.some(binding => {
|
||||
return binding.runtimeBinding === stateBinding.runtimeBinding
|
||||
})
|
||||
if (!hasKey) {
|
||||
allBindings.push(stateBinding)
|
||||
bindings.push(stateBinding)
|
||||
}
|
||||
})
|
||||
// Get which indexes are asynchronous automations as we want to filter them out from the bindings
|
||||
const asynchronousAutomationIndexes = actions
|
||||
.map((action, index) => {
|
||||
if (
|
||||
action[EVENT_TYPE_KEY] === "Trigger Automation" &&
|
||||
!action.parameters?.synchronous
|
||||
) {
|
||||
return index
|
||||
}
|
||||
})
|
||||
.filter(index => index !== undefined)
|
||||
|
||||
// Based on the above, filter out the asynchronous automations from the bindings
|
||||
if (asynchronousAutomationIndexes) {
|
||||
allBindings = eventContextBindings
|
||||
.filter((binding, index) => {
|
||||
return !asynchronousAutomationIndexes.includes(index)
|
||||
})
|
||||
.concat(bindings)
|
||||
} else {
|
||||
allBindings = eventContextBindings.concat(bindings)
|
||||
}
|
||||
return allBindings
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
import { Select, Label, Input, Checkbox } from "@budibase/bbui"
|
||||
import { Select, Label, Input, Checkbox, Icon } from "@budibase/bbui"
|
||||
import { automationStore } from "builderStore"
|
||||
import SaveFields from "./SaveFields.svelte"
|
||||
import { TriggerStepID } from "constants/backend/automations"
|
||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||
|
||||
export let parameters = {}
|
||||
export let bindings = []
|
||||
|
@ -16,6 +16,14 @@
|
|||
? AUTOMATION_STATUS.EXISTING
|
||||
: AUTOMATION_STATUS.NEW
|
||||
|
||||
$: {
|
||||
if (automationStatus === AUTOMATION_STATUS.NEW) {
|
||||
parameters.synchronous = false
|
||||
}
|
||||
parameters.synchronous = automations.find(
|
||||
automation => automation._id === parameters.automationId
|
||||
)?.synchronous
|
||||
}
|
||||
$: automations = $automationStore.automations
|
||||
.filter(a => a.definition.trigger?.stepId === TriggerStepID.APP)
|
||||
.map(automation => {
|
||||
|
@ -23,10 +31,15 @@
|
|||
automation.definition.trigger.inputs.fields || {}
|
||||
).map(([name, type]) => ({ name, type }))
|
||||
|
||||
let hasCollectBlock = automation.definition.steps.some(
|
||||
step => step.stepId === ActionStepID.COLLECT
|
||||
)
|
||||
|
||||
return {
|
||||
name: automation.name,
|
||||
_id: automation._id,
|
||||
schema,
|
||||
synchronous: hasCollectBlock,
|
||||
}
|
||||
})
|
||||
$: hasAutomations = automations && automations.length > 0
|
||||
|
@ -35,6 +48,8 @@
|
|||
)
|
||||
$: selectedSchema = selectedAutomation?.schema
|
||||
|
||||
$: error = parameters.timeout > 120 ? "Timeout must be less than 120s" : null
|
||||
|
||||
const onFieldsChanged = e => {
|
||||
parameters.fields = Object.entries(e.detail || {}).reduce(
|
||||
(acc, [key, value]) => {
|
||||
|
@ -57,6 +72,14 @@
|
|||
parameters.fields = {}
|
||||
parameters.automationId = automations[0]?._id
|
||||
}
|
||||
|
||||
const onChange = value => {
|
||||
let automationId = value.detail
|
||||
parameters.synchronous = automations.find(
|
||||
automation => automation._id === automationId
|
||||
)?.synchronous
|
||||
parameters.automationId = automationId
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
|
@ -85,6 +108,7 @@
|
|||
|
||||
{#if automationStatus === AUTOMATION_STATUS.EXISTING}
|
||||
<Select
|
||||
on:change={onChange}
|
||||
bind:value={parameters.automationId}
|
||||
placeholder="Choose automation"
|
||||
options={automations}
|
||||
|
@ -98,6 +122,29 @@
|
|||
/>
|
||||
{/if}
|
||||
|
||||
{#if parameters.synchronous}
|
||||
<Label small />
|
||||
|
||||
<div class="synchronous-info">
|
||||
<Icon name="Info" />
|
||||
<div>
|
||||
<i
|
||||
>This automation will run synchronously as it contains a Collect
|
||||
step</i
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<Label small />
|
||||
|
||||
<div class="timeout-width">
|
||||
<Input
|
||||
label="Timeout in seconds (120 max)"
|
||||
type="number"
|
||||
{error}
|
||||
bind:value={parameters.timeout}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<Label small />
|
||||
<Checkbox
|
||||
text="Do not display default notification"
|
||||
|
@ -133,6 +180,9 @@
|
|||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.timeout-width {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.params {
|
||||
display: grid;
|
||||
|
@ -142,6 +192,11 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.synchronous-info {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.fields {
|
||||
margin-top: var(--spacing-l);
|
||||
display: grid;
|
||||
|
|
|
@ -57,7 +57,13 @@
|
|||
{
|
||||
"name": "Trigger Automation",
|
||||
"type": "application",
|
||||
"component": "TriggerAutomation"
|
||||
"component": "TriggerAutomation",
|
||||
"context": [
|
||||
{
|
||||
"label": "Automation Result",
|
||||
"value": "result"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Update Field Value",
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<script>
|
||||
import { Modal, ModalContent, Body } from "@budibase/bbui"
|
||||
|
||||
let modal
|
||||
|
||||
export let onConfirm
|
||||
|
||||
export function show() {
|
||||
modal.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
modal.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal} on:hide={modal}>
|
||||
<ModalContent
|
||||
title="Your account is currently de-activated"
|
||||
size="S"
|
||||
showCancelButton={true}
|
||||
showCloseIcon={false}
|
||||
confirmText={"View plans"}
|
||||
{onConfirm}
|
||||
>
|
||||
<Body size="S"
|
||||
>Due to the free plan user limit being exceeded, your account has been
|
||||
de-activated. Upgrade your plan to re-activate your account.</Body
|
||||
>
|
||||
</ModalContent>
|
||||
</Modal>
|
|
@ -3,7 +3,6 @@ import { temporalStore } from "builderStore"
|
|||
import { admin, auth, licensing } from "stores/portal"
|
||||
import { get } from "svelte/store"
|
||||
import { BANNER_TYPES } from "@budibase/bbui"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
const oneDayInSeconds = 86400
|
||||
|
||||
|
@ -146,23 +145,19 @@ const buildUsersAboveLimitBanner = EXPIRY_KEY => {
|
|||
const userLicensing = get(licensing)
|
||||
return {
|
||||
key: EXPIRY_KEY,
|
||||
type: BANNER_TYPES.WARNING,
|
||||
type: BANNER_TYPES.NEGATIVE,
|
||||
onChange: () => {
|
||||
defaultCacheFn(EXPIRY_KEY)
|
||||
},
|
||||
criteria: () => {
|
||||
return userLicensing.warnUserLimit
|
||||
return userLicensing.errUserLimit
|
||||
},
|
||||
message: `${capitalise(
|
||||
userLicensing.license.plan.type
|
||||
)} plan changes - Users will be limited to ${
|
||||
userLicensing.userLimit
|
||||
} users in ${userLicensing.userLimitDays}`,
|
||||
message: "Your Budibase account is de-activated. Upgrade your plan",
|
||||
...{
|
||||
extraButtonText: "Find out more",
|
||||
extraButtonText: "View plans",
|
||||
extraButtonAction: () => {
|
||||
defaultCacheFn(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER)
|
||||
window.location.href = "/builder/portal/users/users"
|
||||
window.location.href = "https://budibase.com/pricing/"
|
||||
},
|
||||
},
|
||||
showCloseButton: true,
|
||||
|
|
|
@ -71,6 +71,9 @@
|
|||
tourStep.onComplete()
|
||||
}
|
||||
popover.hide()
|
||||
if (tourStep.endRoute) {
|
||||
$goto(tourStep.endRoute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ const getTours = () => {
|
|||
title: "Publish",
|
||||
layout: OnboardingPublish,
|
||||
route: "/builder/app/:application/design",
|
||||
endRoute: "/builder/app/:application/data",
|
||||
query: ".toprightnav #builder-app-publish-button",
|
||||
onLoad: () => {
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<script>
|
||||
import { Heading, Body, Button, Icon, notifications } from "@budibase/bbui"
|
||||
import AppLockModal from "../common/AppLockModal.svelte"
|
||||
import { Heading, Body, Button, Icon } from "@budibase/bbui"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { UserAvatar } from "@budibase/frontend-core"
|
||||
|
||||
export let app
|
||||
export let lockedAction
|
||||
|
||||
$: editing = app?.lockedBy != null
|
||||
|
||||
const handleDefaultClick = () => {
|
||||
if (window.innerWidth < 640) {
|
||||
|
@ -15,12 +18,6 @@
|
|||
}
|
||||
|
||||
const goToBuilder = () => {
|
||||
if (app.lockedOther) {
|
||||
notifications.error(
|
||||
`App locked by ${app.lockedBy.email}. Please allow lock to expire or have them unlock this app.`
|
||||
)
|
||||
return
|
||||
}
|
||||
$goto(`../../app/${app.devId}`)
|
||||
}
|
||||
|
||||
|
@ -29,7 +26,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="app-row" on:click={handleDefaultClick}>
|
||||
<div class="app-row" on:click={lockedAction || handleDefaultClick}>
|
||||
<div class="title">
|
||||
<div class="app-icon">
|
||||
<Icon size="L" name={app.icon?.name || "Apps"} color={app.icon?.color} />
|
||||
|
@ -42,7 +39,10 @@
|
|||
</div>
|
||||
|
||||
<div class="updated">
|
||||
{#if app.updatedAt}
|
||||
{#if editing}
|
||||
Currently editing
|
||||
<UserAvatar user={app.lockedBy} />
|
||||
{:else if app.updatedAt}
|
||||
{processStringSync("Updated {{ duration time 'millisecond' }} ago", {
|
||||
time: new Date().getTime() - new Date(app.updatedAt).getTime(),
|
||||
})}
|
||||
|
@ -57,9 +57,12 @@
|
|||
</div>
|
||||
|
||||
<div class="app-row-actions">
|
||||
<AppLockModal {app} buttonSize="M" />
|
||||
<Button size="S" secondary on:click={goToOverview}>Manage</Button>
|
||||
<Button size="S" primary on:click={goToBuilder}>Edit</Button>
|
||||
<Button size="S" secondary on:click={lockedAction || goToOverview}>
|
||||
Manage
|
||||
</Button>
|
||||
<Button size="S" primary on:click={lockedAction || goToBuilder}>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -82,6 +85,9 @@
|
|||
|
||||
.updated {
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.title,
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
<script>
|
||||
import { writable, get as svelteGet } from "svelte/store"
|
||||
import {
|
||||
notifications,
|
||||
Input,
|
||||
ModalContent,
|
||||
Dropzone,
|
||||
Toggle,
|
||||
} from "@budibase/bbui"
|
||||
import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui"
|
||||
import { store, automationStore } from "builderStore"
|
||||
import { API } from "api"
|
||||
import { apps, admin, auth } from "stores/portal"
|
||||
|
@ -22,7 +16,6 @@
|
|||
|
||||
let creating = false
|
||||
let defaultAppName
|
||||
let includeSampleDB = true
|
||||
|
||||
const values = writable({ name: "", url: null })
|
||||
const validation = createValidationStore()
|
||||
|
@ -117,8 +110,6 @@
|
|||
data.append("templateName", template.name)
|
||||
data.append("templateKey", template.key)
|
||||
data.append("templateFile", $values.file)
|
||||
} else {
|
||||
data.append("sampleData", includeSampleDB)
|
||||
}
|
||||
|
||||
// Create App
|
||||
|
@ -213,15 +204,6 @@
|
|||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
{#if !template && !template?.fromFile}
|
||||
<span>
|
||||
<Toggle
|
||||
text="Include sample data"
|
||||
bind:value={includeSampleDB}
|
||||
disabled={creating}
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -20,9 +20,14 @@ export const ActionStepID = {
|
|||
FILTER: "FILTER",
|
||||
QUERY_ROWS: "QUERY_ROWS",
|
||||
LOOP: "LOOP",
|
||||
COLLECT: "COLLECT",
|
||||
// these used to be lowercase step IDs, maintain for backwards compat
|
||||
discord: "discord",
|
||||
slack: "slack",
|
||||
zapier: "zapier",
|
||||
integromat: "integromat",
|
||||
}
|
||||
|
||||
export const Features = {
|
||||
LOOPING: "LOOPING",
|
||||
}
|
||||
|
|
|
@ -28,13 +28,16 @@
|
|||
let inviting = false
|
||||
let searchFocus = false
|
||||
|
||||
// Initially filter entities without app access
|
||||
// Show all when false
|
||||
let filterByAppAccess = true
|
||||
|
||||
let appInvites = []
|
||||
let filteredInvites = []
|
||||
let filteredUsers = []
|
||||
let filteredGroups = []
|
||||
let selectedGroup
|
||||
let userOnboardResponse = null
|
||||
|
||||
let userLimitReachedModal
|
||||
|
||||
$: queryIsEmail = emailValidator(query) === true
|
||||
|
@ -52,15 +55,32 @@
|
|||
}
|
||||
|
||||
const filterInvites = async query => {
|
||||
appInvites = await getInvites()
|
||||
if (!query || query == "") {
|
||||
filteredInvites = appInvites
|
||||
if (!prodAppId) {
|
||||
return
|
||||
}
|
||||
filteredInvites = appInvites.filter(invite => invite.email.includes(query))
|
||||
|
||||
appInvites = await getInvites()
|
||||
|
||||
//On Focus behaviour
|
||||
if (!filterByAppAccess && !query) {
|
||||
filteredInvites =
|
||||
appInvites.length > 100 ? appInvites.slice(0, 100) : [...appInvites]
|
||||
return
|
||||
}
|
||||
|
||||
filteredInvites = appInvites.filter(invite => {
|
||||
const inviteInfo = invite.info?.apps
|
||||
if (!query && inviteInfo && prodAppId) {
|
||||
return Object.keys(inviteInfo).includes(prodAppId)
|
||||
}
|
||||
return invite.email.includes(query)
|
||||
})
|
||||
}
|
||||
|
||||
$: filterInvites(query)
|
||||
$: filterByAppAccess, prodAppId, filterInvites(query)
|
||||
$: if (searchFocus === true) {
|
||||
filterByAppAccess = false
|
||||
}
|
||||
|
||||
const usersFetch = fetchData({
|
||||
API,
|
||||
|
@ -79,9 +99,9 @@
|
|||
}
|
||||
await usersFetch.update({
|
||||
query: {
|
||||
appId: query ? null : prodAppId,
|
||||
appId: query || !filterByAppAccess ? null : prodAppId,
|
||||
email: query,
|
||||
paginated: query ? null : false,
|
||||
paginated: query || !filterByAppAccess ? null : false,
|
||||
},
|
||||
})
|
||||
await usersFetch.refresh()
|
||||
|
@ -107,7 +127,12 @@
|
|||
}
|
||||
|
||||
const debouncedUpdateFetch = Utils.debounce(searchUsers, 250)
|
||||
$: debouncedUpdateFetch(query, $store.builderSidePanel, loaded)
|
||||
$: debouncedUpdateFetch(
|
||||
query,
|
||||
$store.builderSidePanel,
|
||||
loaded,
|
||||
filterByAppAccess
|
||||
)
|
||||
|
||||
const updateAppUser = async (user, role) => {
|
||||
if (!prodAppId) {
|
||||
|
@ -182,9 +207,10 @@
|
|||
}
|
||||
|
||||
const searchGroups = (userGroups, query) => {
|
||||
let filterGroups = query?.length
|
||||
? userGroups
|
||||
: getAppGroups(userGroups, prodAppId)
|
||||
let filterGroups =
|
||||
query?.length || !filterByAppAccess
|
||||
? userGroups
|
||||
: getAppGroups(userGroups, prodAppId)
|
||||
return filterGroups
|
||||
.filter(group => {
|
||||
if (!query?.length) {
|
||||
|
@ -214,7 +240,7 @@
|
|||
}
|
||||
|
||||
// Adds the 'role' attribute and sets it to the current app.
|
||||
$: enrichedGroups = getEnrichedGroups($groups)
|
||||
$: enrichedGroups = getEnrichedGroups($groups, filterByAppAccess)
|
||||
$: filteredGroups = searchGroups(enrichedGroups, query)
|
||||
$: groupUsers = buildGroupUsers(filteredGroups, filteredUsers)
|
||||
$: allUsers = [...filteredUsers, ...groupUsers]
|
||||
|
@ -226,7 +252,7 @@
|
|||
specific roles for the app.
|
||||
*/
|
||||
const buildGroupUsers = (userGroups, filteredUsers) => {
|
||||
if (query) {
|
||||
if (query || !filterByAppAccess) {
|
||||
return []
|
||||
}
|
||||
// Must exclude users who have explicit privileges
|
||||
|
@ -321,12 +347,12 @@
|
|||
[prodAppId]: role,
|
||||
},
|
||||
})
|
||||
await filterInvites()
|
||||
await filterInvites(query)
|
||||
}
|
||||
|
||||
const onUninviteAppUser = async invite => {
|
||||
await uninviteAppUser(invite)
|
||||
await filterInvites()
|
||||
await filterInvites(query)
|
||||
}
|
||||
|
||||
// Purge only the app from the invite or recind the invite if only 1 app remains?
|
||||
|
@ -351,7 +377,6 @@
|
|||
|
||||
onMount(() => {
|
||||
rendered = true
|
||||
searchFocus = true
|
||||
})
|
||||
|
||||
function handleKeyDown(evt) {
|
||||
|
@ -417,7 +442,6 @@
|
|||
autocomplete="off"
|
||||
disabled={inviting}
|
||||
value={query}
|
||||
autofocus
|
||||
on:input={e => {
|
||||
query = e.target.value.trim()
|
||||
}}
|
||||
|
@ -428,16 +452,20 @@
|
|||
|
||||
<span
|
||||
class="search-input-icon"
|
||||
class:searching={query}
|
||||
class:searching={query || !filterByAppAccess}
|
||||
on:click={() => {
|
||||
if (!filterByAppAccess) {
|
||||
filterByAppAccess = true
|
||||
}
|
||||
if (!query) {
|
||||
return
|
||||
}
|
||||
query = null
|
||||
userOnboardResponse = null
|
||||
filterByAppAccess = true
|
||||
}}
|
||||
>
|
||||
<Icon name={query ? "Close" : "Search"} />
|
||||
<Icon name={!filterByAppAccess || query ? "Close" : "Search"} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<script>
|
||||
import { UserAvatar } from "@budibase/frontend-core"
|
||||
|
||||
export let users = []
|
||||
|
||||
$: uniqueUsers = unique(users)
|
||||
|
||||
const unique = users => {
|
||||
let uniqueUsers = {}
|
||||
users?.forEach(user => {
|
||||
uniqueUsers[user.email] = user
|
||||
})
|
||||
return Object.values(uniqueUsers)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="avatars">
|
||||
{#each uniqueUsers as user}
|
||||
<UserAvatar {user} tooltipDirection="bottom" />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.avatars {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { store, automationStore } from "builderStore"
|
||||
import { store, automationStore, userStore } from "builderStore"
|
||||
import { roles, flags } from "stores/backend"
|
||||
import { auth } from "stores/portal"
|
||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
||||
|
@ -13,7 +13,6 @@
|
|||
Modal,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
|
||||
import AppActions from "components/deploy/AppActions.svelte"
|
||||
import { API } from "api"
|
||||
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
||||
|
@ -23,6 +22,7 @@
|
|||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||
import TourPopover from "components/portal/onboarding/TourPopover.svelte"
|
||||
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
||||
import UserAvatars from "./_components/UserAvatars.svelte"
|
||||
import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
|
||||
|
||||
export let application
|
||||
|
@ -30,7 +30,9 @@
|
|||
let promise = getPackage()
|
||||
let hasSynced = false
|
||||
let commandPaletteModal
|
||||
let loaded = false
|
||||
|
||||
$: loaded && initTour()
|
||||
$: selected = capitalise(
|
||||
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
|
||||
)
|
||||
|
@ -43,6 +45,7 @@
|
|||
await automationStore.actions.fetch()
|
||||
await roles.fetch()
|
||||
await flags.fetch()
|
||||
loaded = true
|
||||
return pkg
|
||||
} catch (error) {
|
||||
notifications.error(`Error initialising app: ${error?.message}`)
|
||||
|
@ -67,13 +70,18 @@
|
|||
|
||||
// Event handler for the command palette
|
||||
const handleKeyDown = e => {
|
||||
if (e.key === "k" && (e.ctrlKey || e.metaKey)) {
|
||||
if (e.key === "k" && (e.ctrlKey || e.metaKey) && $store.hasLock) {
|
||||
e.preventDefault()
|
||||
commandPaletteModal.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
const initTour = async () => {
|
||||
// Skip tour if we don't have the lock
|
||||
if (!$store.hasLock) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if onboarding is enabled.
|
||||
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
|
||||
if (!$auth.user?.onboardedAt) {
|
||||
|
@ -110,7 +118,6 @@
|
|||
// check if user has beta access
|
||||
// const betaResponse = await API.checkBetaAccess($auth?.user?.email)
|
||||
// betaAccess = betaResponse.access
|
||||
initTour()
|
||||
} catch (error) {
|
||||
notifications.error("Failed to sync with production database")
|
||||
}
|
||||
|
@ -119,10 +126,7 @@
|
|||
})
|
||||
|
||||
onDestroy(() => {
|
||||
store.update(state => {
|
||||
state.appId = null
|
||||
return state
|
||||
})
|
||||
store.actions.reset()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -134,74 +138,89 @@
|
|||
|
||||
<div class="root">
|
||||
<div class="top-nav">
|
||||
<div class="topleftnav">
|
||||
<ActionMenu>
|
||||
<div slot="control">
|
||||
<Icon size="M" hoverable name="ShowMenu" />
|
||||
</div>
|
||||
<MenuItem on:click={() => $goto("../../portal/apps")}>
|
||||
Exit to portal
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
on:click={() => $goto(`../../portal/overview/${application}`)}
|
||||
>
|
||||
Overview
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
on:click={() => $goto(`../../portal/overview/${application}/access`)}
|
||||
>
|
||||
Access
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
on:click={() =>
|
||||
$goto(`../../portal/overview/${application}/automation-history`)}
|
||||
>
|
||||
Automation history
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
on:click={() => $goto(`../../portal/overview/${application}/backups`)}
|
||||
>
|
||||
Backups
|
||||
</MenuItem>
|
||||
{#if $store.initialised}
|
||||
<div class="topleftnav">
|
||||
<ActionMenu>
|
||||
<div slot="control">
|
||||
<Icon size="M" hoverable name="ShowMenu" />
|
||||
</div>
|
||||
<MenuItem on:click={() => $goto("../../portal/apps")}>
|
||||
Exit to portal
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
on:click={() => $goto(`../../portal/overview/${application}`)}
|
||||
>
|
||||
Overview
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
on:click={() =>
|
||||
$goto(`../../portal/overview/${application}/access`)}
|
||||
>
|
||||
Access
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
on:click={() =>
|
||||
$goto(`../../portal/overview/${application}/automation-history`)}
|
||||
>
|
||||
Automation history
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
on:click={() =>
|
||||
$goto(`../../portal/overview/${application}/backups`)}
|
||||
>
|
||||
Backups
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
on:click={() =>
|
||||
$goto(`../../portal/overview/${application}/name-and-url`)}
|
||||
>
|
||||
Name and URL
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
on:click={() => $goto(`../../portal/overview/${application}/version`)}
|
||||
>
|
||||
Version
|
||||
</MenuItem>
|
||||
</ActionMenu>
|
||||
<Heading size="XS">{$store.name}</Heading>
|
||||
</div>
|
||||
<div class="topcenternav">
|
||||
<Tabs {selected} size="M">
|
||||
{#each $layout.children as { path, title }}
|
||||
<TourWrap tourStepKey={`builder-${title}-section`}>
|
||||
<Tab
|
||||
quiet
|
||||
selected={$isActive(path)}
|
||||
on:click={topItemNavigate(path)}
|
||||
title={capitalise(title)}
|
||||
id={`builder-${title}-tab`}
|
||||
/>
|
||||
</TourWrap>
|
||||
{/each}
|
||||
</Tabs>
|
||||
</div>
|
||||
<div class="toprightnav">
|
||||
<AppActions {application} />
|
||||
</div>
|
||||
<MenuItem
|
||||
on:click={() =>
|
||||
$goto(`../../portal/overview/${application}/name-and-url`)}
|
||||
>
|
||||
Name and URL
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
on:click={() =>
|
||||
$goto(`../../portal/overview/${application}/version`)}
|
||||
>
|
||||
Version
|
||||
</MenuItem>
|
||||
</ActionMenu>
|
||||
<Heading size="XS">{$store.name}</Heading>
|
||||
</div>
|
||||
<div class="topcenternav">
|
||||
{#if $store.hasLock}
|
||||
<Tabs {selected} size="M">
|
||||
{#each $layout.children as { path, title }}
|
||||
<TourWrap tourStepKey={`builder-${title}-section`}>
|
||||
<Tab
|
||||
quiet
|
||||
selected={$isActive(path)}
|
||||
on:click={topItemNavigate(path)}
|
||||
title={capitalise(title)}
|
||||
id={`builder-${title}-tab`}
|
||||
/>
|
||||
</TourWrap>
|
||||
{/each}
|
||||
</Tabs>
|
||||
{:else}
|
||||
<div class="secondary-editor">
|
||||
<Icon name="LockClosed" />
|
||||
Another user is currently editing your screens and automations
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="toprightnav">
|
||||
<UserAvatars users={$userStore} />
|
||||
<AppActions {application} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#await promise}
|
||||
<!-- This should probably be some kind of loading state? -->
|
||||
<div class="loading" />
|
||||
{:then _}
|
||||
<slot />
|
||||
<div class="body">
|
||||
<slot />
|
||||
</div>
|
||||
{:catch error}
|
||||
<p>Something went wrong: {error.message}</p>
|
||||
{/await}
|
||||
|
@ -237,6 +256,7 @@
|
|||
box-sizing: border-box;
|
||||
align-items: stretch;
|
||||
border-bottom: var(--border-light);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.topleftnav {
|
||||
|
@ -270,4 +290,18 @@
|
|||
align-items: center;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
.secondary-editor {
|
||||
align-self: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1 1 auto;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,6 +8,15 @@
|
|||
import { onDestroy, onMount } from "svelte"
|
||||
import { syncURLToState } from "helpers/urlStateSync"
|
||||
import * as routify from "@roxi/routify"
|
||||
import { store } from "builderStore"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
// Prevent access for other users than the lock holder
|
||||
$: {
|
||||
if (!$store.hasLock) {
|
||||
$redirect("../data")
|
||||
}
|
||||
}
|
||||
|
||||
// Keep URL and state in sync for selected screen ID
|
||||
const stopSyncing = syncURLToState({
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<script>
|
||||
import { Body, Label } from "@budibase/bbui"
|
||||
|
||||
export let title
|
||||
export let description
|
||||
export let disabled
|
||||
</script>
|
||||
|
||||
<div on:click class:disabled class="option">
|
||||
<div class="header">
|
||||
<div class="icon">
|
||||
<slot />
|
||||
</div>
|
||||
<Body>{title}</Body>
|
||||
</div>
|
||||
<Label>{description}</Label>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.option {
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--grey-4);
|
||||
padding: 10px 16px 14px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.option :global(label) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: var(--background-alt);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
|
@ -1,21 +1,20 @@
|
|||
<script>
|
||||
import { Button, Layout } from "@budibase/bbui"
|
||||
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
||||
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
|
||||
let modal
|
||||
import { isActive, goto } from "@roxi/routify"
|
||||
</script>
|
||||
|
||||
<!-- routify:options index=1 -->
|
||||
<div class="data">
|
||||
<Panel title="Sources" borderRight>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<Button cta on:click={modal.show}>Add source</Button>
|
||||
<CreateDatasourceModal bind:modal />
|
||||
<DatasourceNavigator />
|
||||
</Layout>
|
||||
</Panel>
|
||||
{#if !$isActive("./new")}
|
||||
<Panel title="Sources" borderRight>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<Button cta on:click={() => $goto("./new")}>Add source</Button>
|
||||
<DatasourceNavigator />
|
||||
</Layout>
|
||||
</Panel>
|
||||
{/if}
|
||||
|
||||
<div class="content">
|
||||
<slot />
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
import { isEqual } from "lodash"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
||||
import { API } from "api"
|
||||
import { DatasourceFeature } from "@budibase/types"
|
||||
|
||||
const querySchema = {
|
||||
name: {},
|
||||
|
@ -45,7 +47,30 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function validateConfig() {
|
||||
const displayError = message =>
|
||||
notifications.error(message ?? "Error validating datasource")
|
||||
|
||||
let connected = false
|
||||
try {
|
||||
const resp = await API.validateDatasource(datasource)
|
||||
if (!resp.connected) {
|
||||
displayError(`Unable to connect - ${resp.error}`)
|
||||
}
|
||||
connected = resp.connected
|
||||
} catch (err) {
|
||||
displayError(err?.message)
|
||||
}
|
||||
return connected
|
||||
}
|
||||
|
||||
const saveDatasource = async () => {
|
||||
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||
const valid = await validateConfig()
|
||||
if (!valid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
try {
|
||||
// Create datasource
|
||||
await datasources.save(datasource)
|
||||
|
|
|
@ -1,22 +1,17 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
import { onMount } from "svelte"
|
||||
import { admin } from "stores/portal"
|
||||
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
|
||||
import { datasources } from "stores/backend"
|
||||
|
||||
let modal
|
||||
$: setupComplete =
|
||||
$: hasData =
|
||||
$datasources.list.find(x => (x._id = "bb_internal"))?.entities?.length >
|
||||
1 || $datasources.list.length > 1
|
||||
|
||||
onMount(() => {
|
||||
if (!setupComplete && !$admin.isDev) {
|
||||
modal.show()
|
||||
if (!hasData) {
|
||||
$redirect("./new")
|
||||
} else {
|
||||
$redirect("./table")
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<CreateDatasourceModal bind:modal />
|
||||
|
|
|
@ -0,0 +1,257 @@
|
|||
<script>
|
||||
import { API } from "api"
|
||||
import { tables, datasources } from "stores/backend"
|
||||
|
||||
import { Icon, Modal, notifications, Heading, Body } from "@budibase/bbui"
|
||||
import { params, goto } from "@roxi/routify"
|
||||
import {
|
||||
IntegrationTypes,
|
||||
DatasourceTypes,
|
||||
DEFAULT_BB_DATASOURCE_ID,
|
||||
} from "constants/backend"
|
||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||
import GoogleDatasourceConfigModal from "components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte"
|
||||
import { createRestDatasource } from "builderStore/datasource"
|
||||
import DatasourceOption from "./_DatasourceOption.svelte"
|
||||
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
|
||||
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
||||
|
||||
let internalTableModal
|
||||
let externalDatasourceModal
|
||||
let integrations = []
|
||||
let integration = null
|
||||
let disabled = false
|
||||
let promptUpload = false
|
||||
|
||||
$: hasData = $datasources.list.length > 1 || $tables.list.length > 1
|
||||
$: hasDefaultData =
|
||||
$datasources.list.findIndex(
|
||||
datasource => datasource._id === DEFAULT_BB_DATASOURCE_ID
|
||||
) !== -1
|
||||
|
||||
const createSampleData = async () => {
|
||||
disabled = true
|
||||
|
||||
try {
|
||||
await API.addSampleData($params.application)
|
||||
await tables.fetch()
|
||||
await datasources.fetch()
|
||||
$goto("./table")
|
||||
} catch (e) {
|
||||
disabled = false
|
||||
notifications.error("Error creating datasource")
|
||||
}
|
||||
}
|
||||
|
||||
const handleIntegrationSelect = integrationType => {
|
||||
const selected = integrations.find(([type]) => type === integrationType)[1]
|
||||
|
||||
// build the schema
|
||||
const config = {}
|
||||
|
||||
for (let key of Object.keys(selected.datasource)) {
|
||||
config[key] = selected.datasource[key].default
|
||||
}
|
||||
|
||||
integration = {
|
||||
type: integrationType,
|
||||
plus: selected.plus,
|
||||
config,
|
||||
schema: selected.datasource,
|
||||
auth: selected.auth,
|
||||
features: selected.features || [],
|
||||
}
|
||||
|
||||
if (selected.friendlyName) {
|
||||
integration.name = selected.friendlyName
|
||||
}
|
||||
|
||||
if (integration.type === IntegrationTypes.REST) {
|
||||
disabled = true
|
||||
|
||||
// Skip modal for rest, create straight away
|
||||
createRestDatasource(integration)
|
||||
.then(response => {
|
||||
$goto(`./datasource/${response._id}`)
|
||||
})
|
||||
.catch(() => {
|
||||
disabled = false
|
||||
notifications.error("Error creating datasource")
|
||||
})
|
||||
} else {
|
||||
externalDatasourceModal.show()
|
||||
}
|
||||
}
|
||||
|
||||
const handleInternalTable = () => {
|
||||
promptUpload = false
|
||||
internalTableModal.show()
|
||||
}
|
||||
|
||||
const handleDataImport = () => {
|
||||
promptUpload = true
|
||||
internalTableModal.show()
|
||||
}
|
||||
|
||||
const handleInternalTableSave = table => {
|
||||
notifications.success(`Table created successfully.`)
|
||||
$goto(`./table/${table._id}`)
|
||||
}
|
||||
|
||||
function sortIntegrations(integrations) {
|
||||
let integrationsArray = Object.entries(integrations)
|
||||
|
||||
function getTypeOrder(schema) {
|
||||
if (schema.type === DatasourceTypes.API) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (schema.type === DatasourceTypes.RELATIONAL) {
|
||||
return 2
|
||||
}
|
||||
|
||||
return schema.type?.charCodeAt(0)
|
||||
}
|
||||
|
||||
integrationsArray.sort((a, b) => {
|
||||
let typeOrderA = getTypeOrder(a[1])
|
||||
let typeOrderB = getTypeOrder(b[1])
|
||||
|
||||
if (typeOrderA === typeOrderB) {
|
||||
return a[1].friendlyName?.localeCompare(b[1].friendlyName)
|
||||
}
|
||||
|
||||
return typeOrderA < typeOrderB ? -1 : 1
|
||||
})
|
||||
|
||||
return integrationsArray
|
||||
}
|
||||
|
||||
const fetchIntegrations = async () => {
|
||||
const unsortedIntegrations = await API.getIntegrations()
|
||||
integrations = sortIntegrations(unsortedIntegrations)
|
||||
}
|
||||
|
||||
$: fetchIntegrations()
|
||||
</script>
|
||||
|
||||
<Modal bind:this={internalTableModal}>
|
||||
<CreateTableModal {promptUpload} afterSave={handleInternalTableSave} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={externalDatasourceModal}>
|
||||
{#if integration?.auth?.type === "google"}
|
||||
<GoogleDatasourceConfigModal {integration} />
|
||||
{:else}
|
||||
<DatasourceConfigModal {integration} />
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<div class="page">
|
||||
<div class="closeButton">
|
||||
{#if hasData}
|
||||
<Icon hoverable name="Close" on:click={$goto("./table")} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="heading">
|
||||
<Heading weight="light">Add new data source</Heading>
|
||||
</div>
|
||||
|
||||
<div class="subHeading">
|
||||
<Body>Get started with our Budibase DB</Body>
|
||||
<div
|
||||
role="tooltip"
|
||||
title="Budibase DB is built with CouchDB"
|
||||
class="tooltip"
|
||||
>
|
||||
<FontAwesomeIcon name="fa-solid fa-circle-info" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options">
|
||||
<DatasourceOption
|
||||
on:click={handleInternalTable}
|
||||
title="Create new table"
|
||||
description="Non-relational"
|
||||
{disabled}
|
||||
>
|
||||
<svelte:component this={ICONS.BUDIBASE} height="20" width="20" />
|
||||
</DatasourceOption>
|
||||
<DatasourceOption
|
||||
on:click={createSampleData}
|
||||
title="Use sample data"
|
||||
description="Non-relational"
|
||||
disabled={disabled || hasDefaultData}
|
||||
>
|
||||
<svelte:component this={ICONS.BUDIBASE} height="20" width="20" />
|
||||
</DatasourceOption>
|
||||
<DatasourceOption
|
||||
on:click={handleDataImport}
|
||||
title="Upload data"
|
||||
description="Non-relational"
|
||||
{disabled}
|
||||
>
|
||||
<svelte:component this={ICONS.BUDIBASE} height="20" width="20" />
|
||||
</DatasourceOption>
|
||||
</div>
|
||||
|
||||
<div class="subHeading">
|
||||
<Body>Or connect to an external datasource</Body>
|
||||
</div>
|
||||
|
||||
<div class="options">
|
||||
{#each integrations as [key, value]}
|
||||
<DatasourceOption
|
||||
on:click={() => handleIntegrationSelect(key)}
|
||||
title={value.friendlyName}
|
||||
description={value.type}
|
||||
{disabled}
|
||||
>
|
||||
<IntegrationIcon integrationType={key} schema={value} />
|
||||
</DatasourceOption>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
height: 38px;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.subHeading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.options {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
column-gap: 24px;
|
||||
row-gap: 24px;
|
||||
grid-template-columns: repeat(auto-fit, 235px);
|
||||
justify-content: center;
|
||||
margin-bottom: 48px;
|
||||
max-width: 1050px;
|
||||
}
|
||||
</style>
|
|
@ -59,6 +59,7 @@
|
|||
text={screen.routing.route}
|
||||
on:click={() => store.actions.screens.select(screen._id)}
|
||||
rightAlignIcon
|
||||
showTooltip
|
||||
>
|
||||
<ScreenDropdownMenu screenId={screen._id} />
|
||||
<RoleIndicator slot="right" roleId={screen.routing.roleId} />
|
||||
|
|
|
@ -1,2 +1,14 @@
|
|||
<script>
|
||||
import { store } from "builderStore"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
// Prevent access for other users than the lock holder
|
||||
$: {
|
||||
if (!$store.hasLock) {
|
||||
$redirect("../data")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- routify:options index=2 -->
|
||||
<slot />
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
Divider,
|
||||
ActionMenu,
|
||||
MenuItem,
|
||||
Avatar,
|
||||
Page,
|
||||
Icon,
|
||||
Body,
|
||||
|
@ -22,6 +21,8 @@
|
|||
import { processStringSync } from "@budibase/string-templates"
|
||||
import Spaceman from "assets/bb-space-man.svg"
|
||||
import Logo from "assets/bb-emblem.svg"
|
||||
import { UserAvatar } from "@budibase/frontend-core"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
|
||||
let loaded = false
|
||||
let userInfoModal
|
||||
|
@ -96,11 +97,7 @@
|
|||
<img class="logo" alt="logo" src={$organisation.logoUrl || Logo} />
|
||||
<ActionMenu align="right">
|
||||
<div slot="control" class="avatar">
|
||||
<Avatar
|
||||
size="M"
|
||||
initials={$auth.initials}
|
||||
url={$auth.user.pictureUrl}
|
||||
/>
|
||||
<UserAvatar user={$auth.user} showTooltip={false} />
|
||||
<Icon size="XL" name="ChevronDown" />
|
||||
</div>
|
||||
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}>
|
||||
|
@ -125,7 +122,7 @@
|
|||
</div>
|
||||
<Layout noPadding gap="XS">
|
||||
<Heading size="M">
|
||||
Hey {$auth.user.firstName || $auth.user.email}
|
||||
Hey {helpers.getUserLabel($auth.user)}
|
||||
</Heading>
|
||||
<Body>
|
||||
Welcome to the {$organisation.company} portal. Below you'll find the
|
||||
|
@ -133,7 +130,7 @@
|
|||
</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
{#if $licensing.usageMetrics?.dayPasses >= 100}
|
||||
{#if $licensing.usageMetrics?.dayPasses >= 100 || $licensing.errUserLimit}
|
||||
<div>
|
||||
<Layout gap="S" justifyItems="center">
|
||||
<img class="spaceman" alt="spaceman" src={Spaceman} />
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<script>
|
||||
import { auth } from "stores/portal"
|
||||
import { ActionMenu, Avatar, MenuItem, Icon, Modal } from "@budibase/bbui"
|
||||
import { ActionMenu, MenuItem, Icon, Modal } from "@budibase/bbui"
|
||||
import { goto } from "@roxi/routify"
|
||||
import ProfileModal from "components/settings/ProfileModal.svelte"
|
||||
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
||||
import ThemeModal from "components/settings/ThemeModal.svelte"
|
||||
import APIKeyModal from "components/settings/APIKeyModal.svelte"
|
||||
import { UserAvatar } from "@budibase/frontend-core"
|
||||
|
||||
let themeModal
|
||||
let profileModal
|
||||
|
@ -23,7 +24,7 @@
|
|||
|
||||
<ActionMenu align="right">
|
||||
<div slot="control" class="user-dropdown">
|
||||
<Avatar size="M" initials={$auth.initials} url={$auth.user.pictureUrl} />
|
||||
<UserAvatar user={$auth.user} showTooltip={false} />
|
||||
<Icon size="XL" name="ChevronDown" />
|
||||
</div>
|
||||
<MenuItem icon="UserEdit" on:click={() => profileModal.show()}>
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
<script>
|
||||
import { Avatar, Tooltip } from "@budibase/bbui"
|
||||
import { Tooltip } from "@budibase/bbui"
|
||||
import { UserAvatar } from "@budibase/frontend-core"
|
||||
|
||||
export let row
|
||||
|
||||
let showTooltip
|
||||
const getInitials = user => {
|
||||
let initials = ""
|
||||
initials += user.firstName ? user.firstName[0] : ""
|
||||
initials += user.lastName ? user.lastName[0] : ""
|
||||
|
||||
return initials === "" ? user.email[0] : initials
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if row?.user?.email}
|
||||
|
@ -19,7 +14,7 @@
|
|||
on:focus={() => (showTooltip = true)}
|
||||
on:mouseleave={() => (showTooltip = false)}
|
||||
>
|
||||
<Avatar size="M" initials={getInitials(row.user)} />
|
||||
<UserAvatar user={row.user} />
|
||||
</div>
|
||||
{#if showTooltip}
|
||||
<div class="tooltip">
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
import Spinner from "components/common/Spinner.svelte"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||
import AccountLockedModal from "components/portal/licensing/AccountLockedModal.svelte"
|
||||
|
||||
import { store, automationStore } from "builderStore"
|
||||
import { API } from "api"
|
||||
|
@ -28,6 +29,7 @@
|
|||
let template
|
||||
let creationModal
|
||||
let appLimitModal
|
||||
let accountLockedModal
|
||||
let creatingApp = false
|
||||
let searchTerm = ""
|
||||
let creatingFromTemplate = false
|
||||
|
@ -48,6 +50,11 @@
|
|||
: true)
|
||||
)
|
||||
$: automationErrors = getAutomationErrors(enrichedApps)
|
||||
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||
|
||||
const usersLimitLockAction = $licensing?.errUserLimit
|
||||
? () => accountLockedModal.show()
|
||||
: null
|
||||
|
||||
const enrichApps = (apps, user, sortBy) => {
|
||||
const enrichedApps = apps.map(app => ({
|
||||
|
@ -189,6 +196,9 @@
|
|||
creatingFromTemplate = true
|
||||
createAppFromTemplateUrl(initInfo.init_template)
|
||||
}
|
||||
if (usersLimitLockAction) {
|
||||
usersLimitLockAction()
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error getting init info")
|
||||
}
|
||||
|
@ -230,20 +240,30 @@
|
|||
<Layout noPadding gap="L">
|
||||
<div class="title">
|
||||
<div class="buttons">
|
||||
<Button size="M" cta on:click={initiateAppCreation}>
|
||||
<Button
|
||||
size="M"
|
||||
cta
|
||||
on:click={usersLimitLockAction || initiateAppCreation}
|
||||
>
|
||||
Create new app
|
||||
</Button>
|
||||
{#if $apps?.length > 0}
|
||||
<Button
|
||||
size="M"
|
||||
secondary
|
||||
on:click={$goto("/builder/portal/apps/templates")}
|
||||
on:click={usersLimitLockAction ||
|
||||
$goto("/builder/portal/apps/templates")}
|
||||
>
|
||||
View templates
|
||||
</Button>
|
||||
{/if}
|
||||
{#if !$apps?.length}
|
||||
<Button size="L" quiet secondary on:click={initiateAppImport}>
|
||||
<Button
|
||||
size="L"
|
||||
quiet
|
||||
secondary
|
||||
on:click={usersLimitLockAction || initiateAppImport}
|
||||
>
|
||||
Import app
|
||||
</Button>
|
||||
{/if}
|
||||
|
@ -267,7 +287,7 @@
|
|||
|
||||
<div class="app-table">
|
||||
{#each filteredApps as app (app.appId)}
|
||||
<AppRow {app} />
|
||||
<AppRow {app} lockedAction={usersLimitLockAction} />
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
|
@ -294,6 +314,11 @@
|
|||
</Modal>
|
||||
|
||||
<AppLimitModal bind:this={appLimitModal} />
|
||||
<AccountLockedModal
|
||||
bind:this={accountLockedModal}
|
||||
onConfirm={() =>
|
||||
isOwner ? $licensing.goToUpgradePage() : $licensing.goToPricingPage()}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.title {
|
||||
|
@ -330,7 +355,6 @@
|
|||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-xl);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.empty-wrapper {
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
<script>
|
||||
import PanelHeader from "./PanelHeader.svelte"
|
||||
export let onBack = () => {}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<PanelHeader
|
||||
title="Give it some data"
|
||||
subtitle="Not ready to add yours? Get started with sample data!"
|
||||
{onBack}
|
||||
/>
|
||||
<slot />
|
||||
</div>
|
|
@ -1,120 +0,0 @@
|
|||
<script>
|
||||
import { Button, FancyForm, FancyInput, FancyCheckbox } from "@budibase/bbui"
|
||||
import GoogleButton from "components/backend/DatasourceNavigator/_components/GoogleButton.svelte"
|
||||
import { capitalise } from "helpers/helpers"
|
||||
import PanelHeader from "./PanelHeader.svelte"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
|
||||
export let title = ""
|
||||
export let onBack = null
|
||||
export let onNext = () => {}
|
||||
export let fields = {}
|
||||
export let type = ""
|
||||
|
||||
let errors = {}
|
||||
|
||||
const formatName = name => {
|
||||
if (name === "ca") {
|
||||
return "CA"
|
||||
}
|
||||
|
||||
if (name === "ssl") {
|
||||
return "SSL"
|
||||
}
|
||||
|
||||
if (name === "rejectUnauthorized") {
|
||||
return "Reject Unauthorized"
|
||||
}
|
||||
|
||||
return capitalise(name)
|
||||
}
|
||||
|
||||
const getDefaultValues = fields => {
|
||||
const newValues = {}
|
||||
|
||||
Object.entries(fields).forEach(([name, { default: defaultValue }]) => {
|
||||
if (defaultValue) {
|
||||
newValues[name] = defaultValue
|
||||
}
|
||||
})
|
||||
|
||||
return newValues
|
||||
}
|
||||
|
||||
const values = getDefaultValues(fields)
|
||||
|
||||
const validateRequired = value => {
|
||||
if (value.length < 1) {
|
||||
return "Required field"
|
||||
}
|
||||
}
|
||||
|
||||
const getIsValid = (fields, errors, values) => {
|
||||
for (const [name, { required }] of Object.entries(fields)) {
|
||||
if (required && !values[name]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(errors).every(error => !error)
|
||||
}
|
||||
|
||||
$: isValid = getIsValid(fields, errors, values)
|
||||
$: isGoogle = helpers.isGoogleSheets(type)
|
||||
|
||||
const handleNext = async () => {
|
||||
const parsedValues = {}
|
||||
|
||||
Object.entries(values).forEach(([name, value]) => {
|
||||
if (fields[name].type === "number") {
|
||||
parsedValues[name] = parseInt(value, 10)
|
||||
} else {
|
||||
parsedValues[name] = value
|
||||
}
|
||||
})
|
||||
|
||||
if (isGoogle) {
|
||||
parsedValues.isGoogle = isGoogle
|
||||
}
|
||||
return await onNext(parsedValues)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<PanelHeader
|
||||
{title}
|
||||
subtitle="Fill in the required fields to fetch your tables"
|
||||
{onBack}
|
||||
/>
|
||||
<div class="form">
|
||||
<FancyForm>
|
||||
{#each Object.entries(fields) as [name, { type, default: defaultValue, required }]}
|
||||
{#if type !== "boolean"}
|
||||
<FancyInput
|
||||
bind:value={values[name]}
|
||||
bind:error={errors[name]}
|
||||
validate={required ? validateRequired : () => {}}
|
||||
label={formatName(name)}
|
||||
{type}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{#each Object.entries(fields) as [name, { type, default: defaultValue, required }]}
|
||||
{#if type === "boolean"}
|
||||
<FancyCheckbox bind:value={values[name]} text={formatName(name)} />
|
||||
{/if}
|
||||
{/each}
|
||||
</FancyForm>
|
||||
</div>
|
||||
{#if isGoogle}
|
||||
<GoogleButton disabled={!isValid} preAuthStep={handleNext} samePage />
|
||||
{:else}
|
||||
<Button cta disabled={!isValid} on:click={handleNext}>Connect</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.form {
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
export let name = ""
|
||||
export let showData = false
|
||||
|
||||
const rows = [
|
||||
{
|
||||
|
@ -49,7 +48,7 @@
|
|||
<h1>{name}</h1>
|
||||
</div>
|
||||
<div class="nav">Home</div>
|
||||
<table class={`table ${showData ? "tableVisible" : ""}`}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>FIRST NAME</th>
|
||||
|
@ -71,7 +70,7 @@
|
|||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class={`sidePanel ${showData ? "sidePanelVisible" : ""}`}>
|
||||
<div class="sidePanel">
|
||||
<h2>{rows[0].firstName}</h2>
|
||||
<div class="field">
|
||||
<label for="exampleLastName">lastName</label>
|
||||
|
@ -199,14 +198,6 @@
|
|||
text-align: left;
|
||||
}
|
||||
|
||||
.table {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tableVisible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidePanel {
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
|
@ -216,9 +207,6 @@
|
|||
top: 0;
|
||||
right: -364px;
|
||||
padding: 42px 32px;
|
||||
}
|
||||
|
||||
.sidePanelVisible {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import PanelHeader from "./PanelHeader.svelte"
|
||||
import { APP_URL_REGEX } from "constants"
|
||||
|
||||
export let disabled
|
||||
export let name = ""
|
||||
export let url = ""
|
||||
export let onNext = () => {}
|
||||
|
@ -31,6 +32,18 @@
|
|||
return "Invalid URL"
|
||||
}
|
||||
}
|
||||
|
||||
$: urlManuallySet = false
|
||||
|
||||
const updateUrl = event => {
|
||||
const appName = event.detail
|
||||
if (urlManuallySet) {
|
||||
return
|
||||
}
|
||||
|
||||
const parsedUrl = appName.toLowerCase().replace(/\s+/g, "-")
|
||||
url = encodeURI(parsedUrl)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
@ -43,11 +56,13 @@
|
|||
bind:value={name}
|
||||
bind:error={nameError}
|
||||
validate={validateName}
|
||||
on:change={updateUrl}
|
||||
label="Name"
|
||||
/>
|
||||
<FancyInput
|
||||
bind:value={url}
|
||||
bind:error={urlError}
|
||||
on:change={() => (urlManuallySet = true)}
|
||||
validate={validateUrl}
|
||||
label="URL"
|
||||
/>
|
||||
|
@ -57,7 +72,9 @@
|
|||
{:else}
|
||||
<p></p>
|
||||
{/if}
|
||||
<Button size="L" cta disabled={!isValid} on:click={onNext}>Lets go!</Button>
|
||||
<Button size="L" cta disabled={!isValid || disabled} on:click={onNext}
|
||||
>Lets go!</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -1,100 +1,50 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import NamePanel from "./_components/NamePanel.svelte"
|
||||
import DataPanel from "./_components/DataPanel.svelte"
|
||||
import DatasourceConfigPanel from "./_components/DatasourceConfigPanel.svelte"
|
||||
import ExampleApp from "./_components/ExampleApp.svelte"
|
||||
import { FancyButton, notifications, Modal, Body } from "@budibase/bbui"
|
||||
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { SplitPage } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import { store, automationStore } from "builderStore"
|
||||
import { saveDatasource } from "builderStore/datasource"
|
||||
import { integrations } from "stores/backend"
|
||||
import { auth, admin, organisation } from "stores/portal"
|
||||
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||
import { auth, admin } from "stores/portal"
|
||||
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
||||
import { Roles } from "constants/backend"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
|
||||
let name = "My first app"
|
||||
let url = "my-first-app"
|
||||
let stage = "name"
|
||||
let appId = null
|
||||
|
||||
let plusIntegrations = {}
|
||||
let integrationsLoading = true
|
||||
let creationLoading = false
|
||||
let uploadModal
|
||||
let googleComplete = false
|
||||
let loading = false
|
||||
|
||||
$: getIntegrations()
|
||||
const createApp = async () => {
|
||||
loading = true
|
||||
|
||||
const createApp = async useSampleData => {
|
||||
creationLoading = true
|
||||
// Create form data to create app
|
||||
// This is form based and not JSON
|
||||
try {
|
||||
let data = new FormData()
|
||||
data.append("name", name.trim())
|
||||
data.append("url", url.trim())
|
||||
data.append("useTemplate", false)
|
||||
let data = new FormData()
|
||||
data.append("name", name.trim())
|
||||
data.append("url", url.trim())
|
||||
data.append("useTemplate", false)
|
||||
|
||||
if (useSampleData) {
|
||||
data.append("sampleData", true)
|
||||
}
|
||||
const createdApp = await API.createApp(data)
|
||||
|
||||
const createdApp = await API.createApp(data)
|
||||
// Select Correct Application/DB in prep for creating user
|
||||
const pkg = await API.fetchAppPackage(createdApp.instance._id)
|
||||
await store.actions.initialise(pkg)
|
||||
await automationStore.actions.fetch()
|
||||
// Update checklist - in case first app
|
||||
await admin.init()
|
||||
|
||||
// Select Correct Application/DB in prep for creating user
|
||||
const pkg = await API.fetchAppPackage(createdApp.instance._id)
|
||||
await store.actions.initialise(pkg)
|
||||
await automationStore.actions.fetch()
|
||||
// Update checklist - in case first app
|
||||
await admin.init()
|
||||
// Create user
|
||||
await auth.setInitInfo({})
|
||||
|
||||
// Create user
|
||||
await auth.setInitInfo({})
|
||||
let defaultScreenTemplate = createFromScratchScreen.create()
|
||||
defaultScreenTemplate.routing.route = "/home"
|
||||
defaultScreenTemplate.routing.roldId = Roles.BASIC
|
||||
await store.actions.screens.save(defaultScreenTemplate)
|
||||
|
||||
let defaultScreenTemplate = createFromScratchScreen.create()
|
||||
defaultScreenTemplate.routing.route = "/home"
|
||||
defaultScreenTemplate.routing.roldId = Roles.BASIC
|
||||
await store.actions.screens.save(defaultScreenTemplate)
|
||||
|
||||
appId = createdApp.instance._id
|
||||
return createdApp
|
||||
} catch (e) {
|
||||
creationLoading = false
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const getIntegrations = async () => {
|
||||
try {
|
||||
await integrations.init()
|
||||
const newPlusIntegrations = {}
|
||||
|
||||
Object.entries($integrations).forEach(([integrationType, schema]) => {
|
||||
// google sheets not available in self-host
|
||||
if (
|
||||
helpers.isGoogleSheets(integrationType) &&
|
||||
!$organisation.googleDatasourceConfigured
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (schema?.plus) {
|
||||
newPlusIntegrations[integrationType] = schema
|
||||
}
|
||||
})
|
||||
|
||||
plusIntegrations = newPlusIntegrations
|
||||
} catch (e) {
|
||||
notifications.error("There was a problem communicating with the server.")
|
||||
} finally {
|
||||
integrationsLoading = false
|
||||
}
|
||||
appId = createdApp.instance._id
|
||||
return createdApp
|
||||
}
|
||||
|
||||
const goToApp = () => {
|
||||
|
@ -102,153 +52,29 @@
|
|||
notifications.success(`App created successfully`)
|
||||
}
|
||||
|
||||
const handleCreateApp = async ({
|
||||
datasourceConfig,
|
||||
useSampleData,
|
||||
isGoogle,
|
||||
}) => {
|
||||
const handleCreateApp = async () => {
|
||||
try {
|
||||
const app = await createApp(useSampleData)
|
||||
await createApp()
|
||||
|
||||
let datasource
|
||||
if (datasourceConfig) {
|
||||
datasource = await saveDatasource({
|
||||
plus: true,
|
||||
auth: undefined,
|
||||
name: plusIntegrations[stage].friendlyName,
|
||||
schema: plusIntegrations[stage].datasource,
|
||||
config: datasourceConfig,
|
||||
type: stage,
|
||||
})
|
||||
}
|
||||
|
||||
store.set()
|
||||
|
||||
if (isGoogle) {
|
||||
googleComplete = true
|
||||
return { datasource, appId: app.appId }
|
||||
} else {
|
||||
goToApp()
|
||||
}
|
||||
goToApp()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
creationLoading = false
|
||||
loading = false
|
||||
notifications.error("There was a problem creating your app")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={uploadModal}>
|
||||
<CreateTableModal
|
||||
name="Your Data"
|
||||
beforeSave={createApp}
|
||||
afterSave={goToApp}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<SplitPage>
|
||||
{#if stage === "name"}
|
||||
<NamePanel bind:name bind:url onNext={() => (stage = "data")} />
|
||||
{:else if googleComplete}
|
||||
<div class="centered">
|
||||
<Body
|
||||
>Please login to your Google account in the new tab which as opened to
|
||||
continue.</Body
|
||||
>
|
||||
<div class="full-width">
|
||||
<SplitPage>
|
||||
<NamePanel bind:name bind:url disabled={loading} onNext={handleCreateApp} />
|
||||
<div slot="right">
|
||||
<ExampleApp {name} />
|
||||
</div>
|
||||
{:else if integrationsLoading || creationLoading}
|
||||
<div class="centered">
|
||||
<Spinner />
|
||||
</div>
|
||||
{:else if stage === "data"}
|
||||
<DataPanel onBack={() => (stage = "name")}>
|
||||
<div class="dataButton">
|
||||
<FancyButton on:click={() => handleCreateApp({ useSampleData: true })}>
|
||||
<div class="dataButtonContent">
|
||||
<div class="dataButtonIcon">
|
||||
<img
|
||||
alt="Budibase Logo"
|
||||
class="budibaseLogo"
|
||||
src={"https://i.imgur.com/Xhdt1YP.png"}
|
||||
/>
|
||||
</div>
|
||||
Budibase Sample data
|
||||
</div>
|
||||
</FancyButton>
|
||||
</div>
|
||||
<div class="dataButton">
|
||||
<FancyButton on:click={uploadModal.show}>
|
||||
<div class="dataButtonContent">
|
||||
<div class="dataButtonIcon">
|
||||
<FontAwesomeIcon name="fa-solid fa-file-arrow-up" />
|
||||
</div>
|
||||
Upload data (CSV or JSON)
|
||||
</div>
|
||||
</FancyButton>
|
||||
</div>
|
||||
{#each Object.entries(plusIntegrations) as [integrationType, schema]}
|
||||
<div class="dataButton">
|
||||
<FancyButton on:click={() => (stage = integrationType)}>
|
||||
<div class="dataButtonContent">
|
||||
<div class="dataButtonIcon">
|
||||
<IntegrationIcon {integrationType} {schema} />
|
||||
</div>
|
||||
{schema.friendlyName}
|
||||
</div>
|
||||
</FancyButton>
|
||||
</div>
|
||||
{/each}
|
||||
</DataPanel>
|
||||
{:else if stage in plusIntegrations}
|
||||
<DatasourceConfigPanel
|
||||
title={plusIntegrations[stage].friendlyName}
|
||||
fields={plusIntegrations[stage].datasource}
|
||||
type={stage}
|
||||
onBack={() => (stage = "data")}
|
||||
onNext={data => {
|
||||
const isGoogle = data.isGoogle
|
||||
delete data.isGoogle
|
||||
return handleCreateApp({ datasourceConfig: data, isGoogle })
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<p>There was an problem. Please refresh the page and try again.</p>
|
||||
{/if}
|
||||
<div slot="right">
|
||||
<ExampleApp {name} showData={stage !== "name"} />
|
||||
</div>
|
||||
</SplitPage>
|
||||
</SplitPage>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.centered {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.dataButton {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.dataButtonContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.budibaseLogo {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.dataButtonIcon {
|
||||
width: 22px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.dataButtonContent :global(svg) {
|
||||
font-size: 18px;
|
||||
color: white;
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
import { AppStatus } from "constants"
|
||||
import analytics, { Events, EventSource } from "analytics"
|
||||
import { store } from "builderStore"
|
||||
import AppLockModal from "components/common/AppLockModal.svelte"
|
||||
import EditableIcon from "components/common/EditableIcon.svelte"
|
||||
import { API } from "api"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
|
@ -80,13 +79,6 @@
|
|||
}
|
||||
|
||||
const editApp = () => {
|
||||
if (appLocked && !lockedByYou) {
|
||||
const identifier = app?.lockedBy?.firstName || app?.lockedBy?.email
|
||||
notifications.warning(
|
||||
`App locked by ${identifier}. Please allow lock to expire or have them unlock this app.`
|
||||
)
|
||||
return
|
||||
}
|
||||
$goto(`../../../app/${app.devId}`)
|
||||
}
|
||||
|
||||
|
@ -135,7 +127,6 @@
|
|||
/>
|
||||
</div>
|
||||
<div slot="buttons">
|
||||
<AppLockModal {app} />
|
||||
<span class="desktop">
|
||||
<Button
|
||||
size="M"
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
<script>
|
||||
import getUserInitials from "helpers/userInitials.js"
|
||||
import { Avatar } from "@budibase/bbui"
|
||||
import { UserAvatar } from "@budibase/frontend-core"
|
||||
|
||||
export let value
|
||||
|
||||
$: initials = getUserInitials(value)
|
||||
</script>
|
||||
|
||||
<div title={value.email} class="cell">
|
||||
<Avatar size="M" {initials} />
|
||||
<div class="cell">
|
||||
<UserAvatar user={value} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
Icon,
|
||||
Heading,
|
||||
Link,
|
||||
Avatar,
|
||||
Layout,
|
||||
Body,
|
||||
notifications,
|
||||
|
@ -15,7 +14,7 @@
|
|||
import { store } from "builderStore"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import { users, auth, apps, groups, overview } from "stores/portal"
|
||||
import { fetchData } from "@budibase/frontend-core"
|
||||
import { fetchData, UserAvatar } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import GroupIcon from "../../users/groups/_components/GroupIcon.svelte"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
|
@ -56,14 +55,6 @@
|
|||
appEditor = await users.get(editorId)
|
||||
}
|
||||
|
||||
const getInitials = user => {
|
||||
let initials = ""
|
||||
initials += user.firstName ? user.firstName[0] : ""
|
||||
initials += user.lastName ? user.lastName[0] : ""
|
||||
|
||||
return initials === "" ? user.email[0] : initials
|
||||
}
|
||||
|
||||
const confirmUnpublishApp = async () => {
|
||||
try {
|
||||
await API.unpublishApp(app.prodId)
|
||||
|
@ -140,7 +131,7 @@
|
|||
<div class="last-edited-content">
|
||||
<div class="updated-by">
|
||||
{#if appEditor}
|
||||
<Avatar size="M" initials={getInitials(appEditor)} />
|
||||
<UserAvatar user={appEditor} showTooltip={false} />
|
||||
<div class="editor-name">
|
||||
{appEditor._id === $auth.user._id ? "You" : appEditorText}
|
||||
</div>
|
||||
|
@ -201,7 +192,7 @@
|
|||
<div class="users">
|
||||
<div class="list">
|
||||
{#each appUsers.slice(0, 4) as user}
|
||||
<Avatar size="M" initials={getInitials(user)} />
|
||||
<UserAvatar {user} />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="text">
|
||||
|
|
|
@ -115,27 +115,6 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
.sso-link-icon {
|
||||
padding-top: 4px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
.sso-link {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.enforce-sso-title {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.enforce-sso-heading-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: start;
|
||||
}
|
||||
.provider-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -143,9 +122,6 @@
|
|||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.provider-title span {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.inputContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import EditUserPicker from "./EditUserPicker.svelte"
|
||||
|
||||
import { Heading, Pagination, Table } from "@budibase/bbui"
|
||||
import { Heading, Pagination, Table, Search } from "@budibase/bbui"
|
||||
import { fetchData } from "@budibase/frontend-core"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { API } from "api"
|
||||
|
@ -12,7 +12,9 @@
|
|||
|
||||
export let groupId
|
||||
|
||||
const fetchGroupUsers = fetchData({
|
||||
let emailSearch
|
||||
let fetchGroupUsers
|
||||
$: fetchGroupUsers = fetchData({
|
||||
API,
|
||||
datasource: {
|
||||
type: "groupUser",
|
||||
|
@ -20,6 +22,7 @@
|
|||
options: {
|
||||
query: {
|
||||
groupId,
|
||||
emailSearch,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
@ -59,24 +62,31 @@
|
|||
</script>
|
||||
|
||||
<div class="header">
|
||||
<Heading size="S">Users</Heading>
|
||||
{#if !scimEnabled}
|
||||
<EditUserPicker {groupId} onUsersUpdated={fetchGroupUsers.getInitialData} />
|
||||
{:else}
|
||||
<ScimBanner />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="controls-right">
|
||||
<Search bind:value={emailSearch} placeholder="Search email" />
|
||||
</div>
|
||||
</div>
|
||||
<Table
|
||||
schema={userSchema}
|
||||
data={$fetchGroupUsers?.rows}
|
||||
loading={$fetchGroupUsers.loading}
|
||||
allowEditRows={false}
|
||||
customPlaceholder
|
||||
customRenderers={customUserTableRenderers}
|
||||
on:click={e => $goto(`../users/${e.detail._id}`)}
|
||||
>
|
||||
<div class="placeholder" slot="placeholder">
|
||||
<Heading size="S">This user group doesn't have any users</Heading>
|
||||
<Heading size="S"
|
||||
>{emailSearch
|
||||
? `No users found matching the email "${emailSearch}"`
|
||||
: "This user group doesn't have any users"}</Heading
|
||||
>
|
||||
</div>
|
||||
</Table>
|
||||
|
||||
|
@ -98,7 +108,7 @@
|
|||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
@ -109,4 +119,15 @@
|
|||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.controls-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
.controls-right :global(.spectrum-Search) {
|
||||
width: 200px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import { goto, url } from "@roxi/routify"
|
||||
import {
|
||||
ActionMenu,
|
||||
Avatar,
|
||||
Button,
|
||||
Layout,
|
||||
Heading,
|
||||
|
@ -25,13 +24,14 @@
|
|||
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
||||
import DeleteUserModal from "./_components/DeleteUserModal.svelte"
|
||||
import GroupIcon from "../groups/_components/GroupIcon.svelte"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { Constants, UserAvatar } from "@budibase/frontend-core"
|
||||
import { Breadcrumbs, Breadcrumb } from "components/portal/page"
|
||||
import RemoveGroupTableRenderer from "./_components/RemoveGroupTableRenderer.svelte"
|
||||
import GroupNameTableRenderer from "../groups/_components/GroupNameTableRenderer.svelte"
|
||||
import AppNameTableRenderer from "./_components/AppNameTableRenderer.svelte"
|
||||
import AppRoleTableRenderer from "./_components/AppRoleTableRenderer.svelte"
|
||||
import ScimBanner from "../_components/SCIMBanner.svelte"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
|
||||
export let userId
|
||||
|
||||
|
@ -91,7 +91,7 @@
|
|||
$: readonly = !$auth.isAdmin || scimEnabled
|
||||
$: privileged = user?.admin?.global || user?.builder?.global
|
||||
$: nameLabel = getNameLabel(user)
|
||||
$: initials = getInitials(nameLabel)
|
||||
$: initials = helpers.getUserInitials(user)
|
||||
$: filteredGroups = getFilteredGroups($groups, searchTerm)
|
||||
$: availableApps = getAvailableApps($apps, privileged, user?.roles)
|
||||
$: userGroups = $groups.filter(x => {
|
||||
|
@ -150,17 +150,6 @@
|
|||
return label
|
||||
}
|
||||
|
||||
const getInitials = nameLabel => {
|
||||
if (!nameLabel) {
|
||||
return "?"
|
||||
}
|
||||
return nameLabel
|
||||
.split(" ")
|
||||
.slice(0, 2)
|
||||
.map(x => x[0])
|
||||
.join("")
|
||||
}
|
||||
|
||||
async function updateUserFirstName(evt) {
|
||||
try {
|
||||
await users.save({ ...user, firstName: evt.target.value })
|
||||
|
@ -238,7 +227,7 @@
|
|||
|
||||
<div class="title">
|
||||
<div class="user-info">
|
||||
<Avatar size="XXL" {initials} />
|
||||
<UserAvatar size="XXL" {user} showTooltip={false} />
|
||||
<div class="subtitle">
|
||||
<Heading size="M">{nameLabel}</Heading>
|
||||
{#if nameLabel !== user?.email}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue