merge
This commit is contained in:
commit
abce52d846
|
@ -10,4 +10,4 @@ packages/builder/.routify
|
|||
packages/builder/cypress/support/queryLevelTransformerFunction.js
|
||||
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
||||
packages/builder/cypress/reports
|
||||
packages/sdk/sdk
|
||||
packages/sdk/sdk
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"jest": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parser": "@babel/eslint-parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2019,
|
||||
"sourceType": "module",
|
||||
|
@ -16,23 +16,26 @@
|
|||
"dist",
|
||||
"public",
|
||||
"*.spec.js",
|
||||
"bundle.js",
|
||||
"packages/pro"
|
||||
"bundle.js"
|
||||
],
|
||||
"plugins": ["svelte3"],
|
||||
"extends": ["eslint:recommended"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.svelte"],
|
||||
"processor": "svelte3/svelte3"
|
||||
"files": ["**/*.svelte"],
|
||||
"extends": "plugin:svelte/recommended",
|
||||
"parser": "svelte-eslint-parser",
|
||||
"parserOptions": {
|
||||
"parser": "@babel/eslint-parser",
|
||||
"ecmaVersion": 2019,
|
||||
"sourceType": "module",
|
||||
"allowImportExportEverywhere": true
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"files": ["**/*.ts"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [],
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"extends": ["eslint:recommended"],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"no-inner-declarations": "off",
|
||||
|
@ -44,7 +47,8 @@
|
|||
}
|
||||
],
|
||||
"rules": {
|
||||
"no-self-assign": "off"
|
||||
"no-self-assign": "off",
|
||||
"no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_", "destructuredArrayIgnorePattern": "^_" }]
|
||||
},
|
||||
"globals": {
|
||||
"GeolocationPositionError": true
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
name: Budibase CI
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
# Trigger the workflow on push or pull request,
|
||||
# but only for the master branch
|
||||
|
@ -22,7 +26,16 @@ jobs:
|
|||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository == github.event.pull_request.head.repo.full_name
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
- name: Checkout repo only
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository != github.event.pull_request.head.repo.full_name
|
||||
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
@ -34,10 +47,16 @@ jobs:
|
|||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository == github.event.pull_request.head.repo.full_name
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
- name: Checkout repo only
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository != github.event.pull_request.head.repo.full_name
|
||||
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
@ -52,10 +71,16 @@ jobs:
|
|||
test-libraries:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository == github.event.pull_request.head.repo.full_name
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
- name: Checkout repo only
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository != github.event.pull_request.head.repo.full_name
|
||||
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
@ -72,10 +97,16 @@ jobs:
|
|||
test-services:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository == github.event.pull_request.head.repo.full_name
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
- name: Checkout repo only
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository != github.event.pull_request.head.repo.full_name
|
||||
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
@ -91,11 +122,14 @@ jobs:
|
|||
|
||||
test-pro:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == github.event.pull_request.head.repo.full_name
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
@ -107,10 +141,16 @@ jobs:
|
|||
integration-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository == github.event.pull_request.head.repo.full_name
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
- name: Checkout repo only
|
||||
uses: actions/checkout@v3
|
||||
if: github.repository != github.event.pull_request.head.repo.full_name
|
||||
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
@ -129,21 +169,47 @@ jobs:
|
|||
|
||||
check-pro-submodule:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == github.event.pull_request.head.repo.full_name
|
||||
steps:
|
||||
- name: Checkout code
|
||||
- name: Checkout repo and submodules
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
- name: Check submodule
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
|
||||
- name: Check pro commit
|
||||
id: get_pro_commits
|
||||
run: |
|
||||
cd packages/pro
|
||||
git fetch
|
||||
if ! git merge-base --is-ancestor $(git log -n 1 --pretty=format:%H) origin/develop; then
|
||||
echo "Current commit has not been merged to develop"
|
||||
echo "Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/develop/docs/getting_started.md"
|
||||
exit 1
|
||||
pro_commit=$(git rev-parse HEAD)
|
||||
|
||||
branch="${{ github.base_ref || github.ref_name }}"
|
||||
echo "Running on branch '$branch' (base_ref=${{ github.base_ref }}, ref_name=${{ github.head_ref }})"
|
||||
|
||||
if [[ $branch == "master" ]]; then
|
||||
base_commit=$(git rev-parse origin/master)
|
||||
else
|
||||
echo "All good, the submodule had been merged!"
|
||||
base_commit=$(git rev-parse origin/develop)
|
||||
fi
|
||||
|
||||
echo "pro_commit=$pro_commit"
|
||||
echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT"
|
||||
echo "base_commit=$base_commit"
|
||||
echo "base_commit=$base_commit" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check submodule merged to develop
|
||||
uses: actions/github-script@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const submoduleCommit = '${{ steps.get_pro_commits.outputs.pro_commit }}';
|
||||
const baseCommit = '${{ steps.get_pro_commits.outputs.base_commit }}';
|
||||
|
||||
if (submoduleCommit !== baseCommit) {
|
||||
console.error('Submodule commit does not match the latest commit on the develop branch.');
|
||||
console.error('Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/develop/docs/getting_started.md')
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('All good, the submodule had been merged and setup correctly!')
|
||||
}
|
||||
|
|
|
@ -12,31 +12,22 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# - name: Fail if not a tag
|
||||
# run: |
|
||||
# if [[ $GITHUB_REF != refs/tags/* ]]; then
|
||||
# echo "Workflow Dispatch can only be run on tags"
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
# with:
|
||||
# fetch-depth: 0
|
||||
|
||||
# - name: Fail if tag is not in master
|
||||
# run: |
|
||||
# if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
|
||||
# echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
- name: Pull values.yaml from budibase-infra
|
||||
- name: Fail if not a tag
|
||||
run: |
|
||||
curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \
|
||||
-H 'Accept: application/vnd.github.v3.raw' \
|
||||
-o values.production.yaml \
|
||||
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.yaml
|
||||
wc -l values.production.yaml
|
||||
if [[ $GITHUB_REF != refs/tags/* ]]; then
|
||||
echo "Workflow Dispatch can only be run on tags"
|
||||
exit 1
|
||||
fi
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fail if tag is not in master
|
||||
run: |
|
||||
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
|
||||
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Get the latest budibase release version
|
||||
id: version
|
||||
|
@ -48,29 +39,10 @@ jobs:
|
|||
fi
|
||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
- uses: passeidireto/trigger-external-workflow-action@main
|
||||
env:
|
||||
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: eu-west-1
|
||||
|
||||
- name: Deploy to EKS
|
||||
uses: craftech-io/eks-helm-deploy-action@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS__KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: eu-west-1
|
||||
cluster-name: budibase-eks-production
|
||||
config-files: values.production.yaml
|
||||
chart-path: charts/budibase
|
||||
namespace: budibase
|
||||
values: globals.appVersion=v${{ env.RELEASE_VERSION }},services.couchdb.url=${{ secrets.PRODUCTION_COUCHDB_URL }},services.couchdb.password=${{ secrets.PRODUCTION_COUCHDB_PASSWORD }}
|
||||
name: budibase-prod
|
||||
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v4.0.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
||||
content: "Production Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Cloud."
|
||||
embed-title: ${{ env.RELEASE_VERSION }}
|
||||
repository: budibase/budibase-deploys
|
||||
event: budicloud-prod-deploy
|
||||
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
|
|
@ -24,51 +24,18 @@ jobs:
|
|||
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
- name: Get the latest budibase release version
|
||||
id: version
|
||||
run: |
|
||||
release_version=$(cat lerna.json | jq -r '.version')
|
||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: eu-west-1
|
||||
|
||||
- name: Pull values.yaml from budibase-infra
|
||||
run: |
|
||||
curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \
|
||||
-H 'Accept: application/vnd.github.v3.raw' \
|
||||
-o values.preprod.yaml \
|
||||
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-preprod/values.yaml
|
||||
wc -l values.preprod.yaml
|
||||
- name: Deploy to Preprod Environment
|
||||
uses: budibase/helm@v1.8.0
|
||||
with:
|
||||
release: budibase-preprod
|
||||
namespace: budibase
|
||||
chart: charts/budibase
|
||||
token: ${{ github.token }}
|
||||
helm: helm3
|
||||
values: |
|
||||
globals:
|
||||
appVersion: v${{ env.RELEASE_VERSION }}
|
||||
ingress:
|
||||
enabled: true
|
||||
nginx: true
|
||||
value-files: >-
|
||||
[
|
||||
"values.preprod.yaml"
|
||||
]
|
||||
- uses: passeidireto/trigger-external-workflow-action@main
|
||||
env:
|
||||
KUBECONFIG_FILE: '${{ secrets.PREPROD_KUBECONFIG }}'
|
||||
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v4.0.0
|
||||
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
|
||||
with:
|
||||
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
||||
content: "Preprod Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Pre-prod."
|
||||
embed-title: ${{ env.RELEASE_VERSION }}
|
||||
repository: budibase/budibase-deploys
|
||||
event: budicloud-preprod-deploy
|
||||
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
||||
|
|
|
@ -34,7 +34,6 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
@ -58,9 +57,12 @@ jobs:
|
|||
echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc
|
||||
yarn release
|
||||
|
||||
- name: "Get Previous tag"
|
||||
id: previoustag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
- name: "Get Current tag"
|
||||
id: currenttag
|
||||
run: |
|
||||
version=v$(./scripts/getCurrentVersion.sh)
|
||||
echo 'Using tag $version'
|
||||
echo "::set-output name=tag::$resversionult"
|
||||
|
||||
- name: Build/release Docker images
|
||||
run: |
|
||||
|
@ -69,7 +71,7 @@ jobs:
|
|||
env:
|
||||
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}
|
||||
BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.tag }}
|
||||
|
||||
release-helm-chart:
|
||||
needs: [release-images]
|
||||
|
|
|
@ -32,10 +32,11 @@ jobs:
|
|||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
- run: yarn
|
||||
- run: cd scripts && yarn
|
||||
- name: Tag prerelease
|
||||
run: |
|
||||
cd scripts
|
||||
# setup the username and email.
|
||||
git config --global user.name "Budibase Staging Release Bot"
|
||||
git config --global user.email "<>"
|
||||
./scripts/versionCommit.sh prerelease
|
||||
./versionCommit.sh prerelease
|
||||
|
|
|
@ -42,12 +42,13 @@ jobs:
|
|||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
- run: yarn
|
||||
- run: cd scripts && yarn
|
||||
- name: Tag release
|
||||
run: |
|
||||
cd scripts
|
||||
# setup the username and email.
|
||||
git config --global user.name "Budibase Staging Release Bot"
|
||||
git config --global user.email "<>"
|
||||
BUMP_TYPE_INPUT=${{ github.event.inputs.versioning }}
|
||||
BUMP_TYPE=${BUMP_TYPE_INPUT:-"patch"}
|
||||
./scripts/versionCommit.sh $BUMP_TYPE
|
||||
./versionCommit.sh $BUMP_TYPE
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"presets": [["@babel/preset-env", { "targets": { "node": "current" } }]]
|
||||
}
|
|
@ -126,6 +126,16 @@ http {
|
|||
proxy_pass http://app-service;
|
||||
}
|
||||
|
||||
location /embed {
|
||||
rewrite /embed/(.*) /app/$1 break;
|
||||
proxy_pass http://app-service;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header x-budibase-embed "true";
|
||||
add_header x-budibase-embed "true";
|
||||
add_header Content-Security-Policy "frame-ancestors *";
|
||||
}
|
||||
|
||||
location /builder {
|
||||
proxy_read_timeout 120s;
|
||||
proxy_connect_timeout 120s;
|
||||
|
|
|
@ -92,6 +92,16 @@ http {
|
|||
proxy_pass $apps;
|
||||
}
|
||||
|
||||
location /embed {
|
||||
rewrite /embed/(.*) /app/$1 break;
|
||||
proxy_pass $apps;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header x-budibase-embed "true";
|
||||
add_header x-budibase-embed "true";
|
||||
add_header Content-Security-Policy "frame-ancestors *";
|
||||
}
|
||||
|
||||
location = / {
|
||||
proxy_pass $apps;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ const fs = require("fs")
|
|||
const { execSync } = require("child_process")
|
||||
const path = require("path")
|
||||
|
||||
const IMAGES = {
|
||||
const IS_SINGLE_IMAGE = process.env.SINGLE_IMAGE
|
||||
|
||||
let IMAGES = {
|
||||
worker: "budibase/worker",
|
||||
apps: "budibase/apps",
|
||||
proxy: "budibase/proxy",
|
||||
|
@ -10,7 +12,13 @@ const IMAGES = {
|
|||
couch: "ibmcom/couchdb3",
|
||||
curl: "curlimages/curl",
|
||||
redis: "redis",
|
||||
watchtower: "containrrr/watchtower"
|
||||
watchtower: "containrrr/watchtower",
|
||||
}
|
||||
|
||||
if (IS_SINGLE_IMAGE) {
|
||||
IMAGES = {
|
||||
budibase: "budibase/budibase"
|
||||
}
|
||||
}
|
||||
|
||||
const FILES = {
|
||||
|
@ -39,11 +47,10 @@ for (let image in IMAGES) {
|
|||
}
|
||||
|
||||
// copy config files
|
||||
copyFile(FILES.COMPOSE)
|
||||
if (!IS_SINGLE_IMAGE) {
|
||||
copyFile(FILES.COMPOSE)
|
||||
}
|
||||
copyFile(FILES.ENV)
|
||||
|
||||
// compress
|
||||
execSync(`tar -czf bb-airgapped.tar.gz hosting/scripts/bb-airgapped`)
|
||||
|
||||
// clean up
|
||||
fs.rmdirSync(OUTPUT_DIR, { recursive: true })
|
||||
execSync(`tar -czf bb-airgapped.tar.gz hosting/scripts/bb-airgapped`)
|
|
@ -37,6 +37,14 @@ COPY --from=build /worker /worker
|
|||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server
|
||||
|
||||
# Install postgres client for pg_dump utils
|
||||
RUN apt install software-properties-common apt-transport-https gpg -y \
|
||||
&& curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \
|
||||
&& echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \
|
||||
&& apt update -y \
|
||||
&& apt install postgresql-client-15 -y \
|
||||
&& apt remove software-properties-common apt-transport-https gpg -y
|
||||
|
||||
# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx
|
||||
WORKDIR /nodejs
|
||||
RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh && \
|
||||
|
|
18
lerna.json
18
lerna.json
|
@ -1,22 +1,10 @@
|
|||
{
|
||||
"version": "2.7.7-alpha.0",
|
||||
"version": "2.8.2-alpha.3",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/backend-core",
|
||||
"packages/bbui",
|
||||
"packages/builder",
|
||||
"packages/cli",
|
||||
"packages/client",
|
||||
"packages/frontend-core",
|
||||
"packages/sdk",
|
||||
"packages/server",
|
||||
"packages/shared-core",
|
||||
"packages/string-templates",
|
||||
"packages/types",
|
||||
"packages/worker",
|
||||
"packages/pro/packages/pro"
|
||||
"packages/*"
|
||||
],
|
||||
"useWorkspaces": true,
|
||||
"useNx": true,
|
||||
"command": {
|
||||
"publish": {
|
||||
"ignoreChanges": [
|
||||
|
|
47
package.json
47
package.json
|
@ -2,30 +2,31 @@
|
|||
"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",
|
||||
"esbuild-node-externals": "^1.7.0",
|
||||
"eslint": "^8.44.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": "7.0.0-alpha.0",
|
||||
"lerna": "7.0.2",
|
||||
"madge": "^6.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"nx": "^16.2.1",
|
||||
"prettier": "^2.3.1",
|
||||
"prettier": "2.8.8",
|
||||
"prettier-plugin-svelte": "^2.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup-plugin-replace": "^2.2.0",
|
||||
"semver": "^7.5.0",
|
||||
"svelte": "^3.38.2",
|
||||
"typescript": "4.7.3"
|
||||
"typescript": "4.7.3",
|
||||
"@babel/core": "^7.22.5",
|
||||
"@babel/eslint-parser": "^7.22.5",
|
||||
"@babel/preset-env": "^7.22.5",
|
||||
"eslint-plugin-svelte": "^2.32.2",
|
||||
"svelte-eslint-parser": "^0.32.0"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "node scripts/syncProPackage.js",
|
||||
|
@ -48,16 +49,16 @@
|
|||
"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 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": "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 && yarn build --projects=@budibase/client && 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:pre && 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:eslint": "eslint packages qa-core --max-warnings=0",
|
||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||
"lint:fix:eslint": "eslint --fix packages qa-core",
|
||||
"lint:fix:eslint": "eslint --fix --max-warnings=0 packages qa-core",
|
||||
"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",
|
||||
|
@ -67,6 +68,7 @@
|
|||
"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 && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
||||
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||
"build:docker:airgap:single": "SINGLE_IMAGE=1 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 .",
|
||||
|
@ -95,19 +97,7 @@
|
|||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/backend-core",
|
||||
"packages/bbui",
|
||||
"packages/builder",
|
||||
"packages/cli",
|
||||
"packages/client",
|
||||
"packages/frontend-core",
|
||||
"packages/sdk",
|
||||
"packages/server",
|
||||
"packages/shared-core",
|
||||
"packages/string-templates",
|
||||
"packages/types",
|
||||
"packages/worker",
|
||||
"packages/pro/packages/pro"
|
||||
"packages/*"
|
||||
]
|
||||
},
|
||||
"resolutions": {
|
||||
|
@ -116,5 +106,6 @@
|
|||
"@budibase/string-templates": "0.0.0",
|
||||
"@budibase/types": "0.0.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,4 +31,6 @@ const config: Config.InitialOptions = {
|
|||
coverageReporters: ["lcov", "json", "clover"],
|
||||
}
|
||||
|
||||
process.env.DISABLE_PINO_LOGGER = "1"
|
||||
|
||||
export default config
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
"aws-cloudfront-sign": "2.2.0",
|
||||
"aws-sdk": "2.1030.0",
|
||||
"bcrypt": "5.0.1",
|
||||
"bcrypt": "5.1.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bull": "4.10.1",
|
||||
"correlation-id": "4.0.0",
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
GoogleInnerConfig,
|
||||
OIDCConfig,
|
||||
OIDCInnerConfig,
|
||||
OIDCLogosConfig,
|
||||
SCIMConfig,
|
||||
SCIMInnerConfig,
|
||||
SettingsConfig,
|
||||
|
@ -191,6 +192,10 @@ export function getDefaultGoogleConfig(): GoogleInnerConfig | undefined {
|
|||
|
||||
// OIDC
|
||||
|
||||
export async function getOIDCLogosDoc(): Promise<OIDCLogosConfig | undefined> {
|
||||
return getConfig<OIDCLogosConfig>(ConfigType.OIDC_LOGOS)
|
||||
}
|
||||
|
||||
async function getOIDCConfigDoc(): Promise<OIDCConfig | undefined> {
|
||||
return getConfig<OIDCConfig>(ConfigType.OIDC)
|
||||
}
|
||||
|
|
|
@ -57,6 +57,9 @@ class Replication {
|
|||
appReplicateOpts() {
|
||||
return {
|
||||
filter: (doc: any) => {
|
||||
if (doc._id && doc._id.startsWith(DocumentType.AUTOMATION_LOG)) {
|
||||
return false
|
||||
}
|
||||
return doc._id !== DocumentType.APP_METADATA
|
||||
},
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
export function checkErrorCode(error: any, code: number) {
|
||||
const stringCode = code.toString()
|
||||
if (typeof error === "object") {
|
||||
return error.status === code || error.message?.includes(stringCode)
|
||||
} else if (typeof error === "number") {
|
||||
return error === code
|
||||
} else if (typeof error === "string") {
|
||||
return error.includes(stringCode)
|
||||
}
|
||||
}
|
||||
|
||||
export function isDocumentConflictError(error: any) {
|
||||
return checkErrorCode(error, 409)
|
||||
}
|
|
@ -9,3 +9,4 @@ export * from "../constants/db"
|
|||
export { getGlobalDBName, baseGlobalDBName } from "../context"
|
||||
export * from "./lucene"
|
||||
export * as searchIndexes from "./searchIndexes"
|
||||
export * from "./errors"
|
||||
|
|
|
@ -343,6 +343,9 @@ export class QueryBuilder<T> {
|
|||
}
|
||||
|
||||
const oneOf = (key: string, value: any) => {
|
||||
if (!value) {
|
||||
return `*:*`
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
if (typeof value === "string") {
|
||||
value = value.split(",")
|
||||
|
|
|
@ -114,6 +114,25 @@ describe("lucene", () => {
|
|||
expect(resp.rows.length).toBe(2)
|
||||
})
|
||||
|
||||
it("should return all rows when doing a one of search against falsey value", async () => {
|
||||
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||
builder.addOneOf("property", null)
|
||||
let resp = await builder.run()
|
||||
expect(resp.rows.length).toBe(3)
|
||||
|
||||
builder.addOneOf("property", undefined)
|
||||
resp = await builder.run()
|
||||
expect(resp.rows.length).toBe(3)
|
||||
|
||||
builder.addOneOf("property", "")
|
||||
resp = await builder.run()
|
||||
expect(resp.rows.length).toBe(3)
|
||||
|
||||
builder.addOneOf("property", [])
|
||||
resp = await builder.run()
|
||||
expect(resp.rows.length).toBe(0)
|
||||
})
|
||||
|
||||
it("should be able to perform a contains search", async () => {
|
||||
const builder = new QueryBuilder(dbName, INDEX_NAME)
|
||||
builder.addContains("property", ["word"])
|
||||
|
|
|
@ -81,8 +81,19 @@ export function generateAppUserID(prodAppId: string, userId: string) {
|
|||
* Generates a new role ID.
|
||||
* @returns {string} The new role ID which the role doc can be stored under.
|
||||
*/
|
||||
export function generateRoleID(id?: any) {
|
||||
return `${DocumentType.ROLE}${SEPARATOR}${id || newid()}`
|
||||
export function generateRoleID(name: string) {
|
||||
const prefix = `${DocumentType.ROLE}${SEPARATOR}`
|
||||
if (name.startsWith(prefix)) {
|
||||
return name
|
||||
}
|
||||
return `${prefix}${name}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to be more verbose.
|
||||
*/
|
||||
export function prefixRoleID(name: string) {
|
||||
return generateRoleID(name)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,10 +14,15 @@ async function servedBuilder(timezone: string) {
|
|||
await publishEvent(Event.SERVED_BUILDER, properties)
|
||||
}
|
||||
|
||||
async function servedApp(app: App, timezone: string) {
|
||||
async function servedApp(
|
||||
app: App,
|
||||
timezone: string,
|
||||
embed?: boolean | undefined
|
||||
) {
|
||||
const properties: AppServedEvent = {
|
||||
appVersion: app.version,
|
||||
timezone,
|
||||
embed: embed === true,
|
||||
}
|
||||
await publishEvent(Event.SERVED_APP, properties)
|
||||
}
|
||||
|
|
|
@ -21,6 +21,6 @@ export function logAlertWithInfo(
|
|||
logAlert(message, error)
|
||||
}
|
||||
|
||||
export function logWarn(message: string) {
|
||||
console.warn(`bb-warn: ${message}`)
|
||||
export function logWarn(message: string, e?: any) {
|
||||
console.warn(`bb-warn: ${message}`, e)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import * as google from "../sso/google"
|
||||
import { Cookie } from "../../../constants"
|
||||
import { clearCookie, getCookie } from "../../../utils"
|
||||
import * as configs from "../../../configs"
|
||||
import { BBContext, SSOProfile } from "@budibase/types"
|
||||
import * as cache from "../../../cache"
|
||||
import * as utils from "../../../utils"
|
||||
import { UserCtx, SSOProfile } from "@budibase/types"
|
||||
import { ssoSaveUserNoOp } from "../sso/sso"
|
||||
import { cache, utils } from "../../../"
|
||||
|
||||
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
||||
|
||||
type Passport = {
|
||||
|
@ -22,7 +23,7 @@ async function fetchGoogleCreds() {
|
|||
|
||||
export async function preAuth(
|
||||
passport: Passport,
|
||||
ctx: BBContext,
|
||||
ctx: UserCtx,
|
||||
next: Function
|
||||
) {
|
||||
// get the relevant config
|
||||
|
@ -49,7 +50,7 @@ export async function preAuth(
|
|||
|
||||
export async function postAuth(
|
||||
passport: Passport,
|
||||
ctx: BBContext,
|
||||
ctx: UserCtx,
|
||||
next: Function
|
||||
) {
|
||||
// get the relevant config
|
||||
|
@ -57,7 +58,7 @@ export async function postAuth(
|
|||
const platformUrl = await configs.getPlatformUrl({ tenantAware: false })
|
||||
|
||||
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
|
||||
const authStateCookie = getCookie(ctx, Cookie.DatasourceAuth)
|
||||
const authStateCookie = utils.getCookie(ctx, Cookie.DatasourceAuth)
|
||||
|
||||
return passport.authenticate(
|
||||
new GoogleStrategy(
|
||||
|
@ -72,7 +73,7 @@ export async function postAuth(
|
|||
_profile: SSOProfile,
|
||||
done: Function
|
||||
) => {
|
||||
clearCookie(ctx, Cookie.DatasourceAuth)
|
||||
utils.clearCookie(ctx, Cookie.DatasourceAuth)
|
||||
done(null, { accessToken, refreshToken })
|
||||
}
|
||||
),
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
import crypto from "crypto"
|
||||
import fs from "fs"
|
||||
import zlib from "zlib"
|
||||
import env from "../environment"
|
||||
import { join } from "path"
|
||||
|
||||
const ALGO = "aes-256-ctr"
|
||||
const SEPARATOR = "-"
|
||||
const ITERATIONS = 10000
|
||||
const RANDOM_BYTES = 16
|
||||
const STRETCH_LENGTH = 32
|
||||
|
||||
const SALT_LENGTH = 16
|
||||
const IV_LENGTH = 16
|
||||
|
||||
export enum SecretOption {
|
||||
API = "api",
|
||||
ENCRYPTION = "encryption",
|
||||
|
@ -31,15 +36,15 @@ export function getSecret(secretOption: SecretOption): string {
|
|||
return secret
|
||||
}
|
||||
|
||||
function stretchString(string: string, salt: Buffer) {
|
||||
return crypto.pbkdf2Sync(string, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
|
||||
function stretchString(secret: string, salt: Buffer) {
|
||||
return crypto.pbkdf2Sync(secret, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
|
||||
}
|
||||
|
||||
export function encrypt(
|
||||
input: string,
|
||||
secretOption: SecretOption = SecretOption.API
|
||||
) {
|
||||
const salt = crypto.randomBytes(RANDOM_BYTES)
|
||||
const salt = crypto.randomBytes(SALT_LENGTH)
|
||||
const stretched = stretchString(getSecret(secretOption), salt)
|
||||
const cipher = crypto.createCipheriv(ALGO, stretched, salt)
|
||||
const base = cipher.update(input)
|
||||
|
@ -60,3 +65,115 @@ export function decrypt(
|
|||
const final = decipher.final()
|
||||
return Buffer.concat([base, final]).toString()
|
||||
}
|
||||
|
||||
export async function encryptFile(
|
||||
{ dir, filename }: { dir: string; filename: string },
|
||||
secret: string
|
||||
) {
|
||||
const outputFileName = `${filename}.enc`
|
||||
|
||||
const filePath = join(dir, filename)
|
||||
const inputFile = fs.createReadStream(filePath)
|
||||
const outputFile = fs.createWriteStream(join(dir, outputFileName))
|
||||
|
||||
const salt = crypto.randomBytes(SALT_LENGTH)
|
||||
const iv = crypto.randomBytes(IV_LENGTH)
|
||||
const stretched = stretchString(secret, salt)
|
||||
const cipher = crypto.createCipheriv(ALGO, stretched, iv)
|
||||
|
||||
outputFile.write(salt)
|
||||
outputFile.write(iv)
|
||||
|
||||
inputFile.pipe(zlib.createGzip()).pipe(cipher).pipe(outputFile)
|
||||
|
||||
return new Promise<{ filename: string; dir: string }>(r => {
|
||||
outputFile.on("finish", () => {
|
||||
r({
|
||||
filename: outputFileName,
|
||||
dir,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function getSaltAndIV(path: string) {
|
||||
const fileStream = fs.createReadStream(path)
|
||||
|
||||
const salt = await readBytes(fileStream, SALT_LENGTH)
|
||||
const iv = await readBytes(fileStream, IV_LENGTH)
|
||||
fileStream.close()
|
||||
return { salt, iv }
|
||||
}
|
||||
|
||||
export async function decryptFile(
|
||||
inputPath: string,
|
||||
outputPath: string,
|
||||
secret: string
|
||||
) {
|
||||
const { salt, iv } = await getSaltAndIV(inputPath)
|
||||
const inputFile = fs.createReadStream(inputPath, {
|
||||
start: SALT_LENGTH + IV_LENGTH,
|
||||
})
|
||||
|
||||
const outputFile = fs.createWriteStream(outputPath)
|
||||
|
||||
const stretched = stretchString(secret, salt)
|
||||
const decipher = crypto.createDecipheriv(ALGO, stretched, iv)
|
||||
|
||||
const unzip = zlib.createGunzip()
|
||||
|
||||
inputFile.pipe(decipher).pipe(unzip).pipe(outputFile)
|
||||
|
||||
return new Promise<void>((res, rej) => {
|
||||
outputFile.on("finish", () => {
|
||||
outputFile.close()
|
||||
res()
|
||||
})
|
||||
|
||||
inputFile.on("error", e => {
|
||||
outputFile.close()
|
||||
rej(e)
|
||||
})
|
||||
|
||||
decipher.on("error", e => {
|
||||
outputFile.close()
|
||||
rej(e)
|
||||
})
|
||||
|
||||
unzip.on("error", e => {
|
||||
outputFile.close()
|
||||
rej(e)
|
||||
})
|
||||
|
||||
outputFile.on("error", e => {
|
||||
outputFile.close()
|
||||
rej(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function readBytes(stream: fs.ReadStream, length: number) {
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
let bytesRead = 0
|
||||
const data: Buffer[] = []
|
||||
|
||||
stream.on("readable", () => {
|
||||
let chunk
|
||||
|
||||
while ((chunk = stream.read(length - bytesRead)) !== null) {
|
||||
data.push(chunk)
|
||||
bytesRead += chunk.length
|
||||
}
|
||||
|
||||
resolve(Buffer.concat(data))
|
||||
})
|
||||
|
||||
stream.on("end", () => {
|
||||
reject(new Error("Insufficient data in the stream."))
|
||||
})
|
||||
|
||||
stream.on("error", error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { BuiltinPermissionID, PermissionLevel } from "./permissions"
|
||||
import { generateRoleID, getRoleParams, DocumentType, SEPARATOR } from "../db"
|
||||
import { prefixRoleID, getRoleParams, DocumentType, SEPARATOR } from "../db"
|
||||
import { getAppDB } from "../context"
|
||||
import { doWithDB } from "../db"
|
||||
import { Screen, Role as RoleDoc } from "@budibase/types"
|
||||
|
@ -25,18 +25,28 @@ const EXTERNAL_BUILTIN_ROLE_IDS = [
|
|||
BUILTIN_IDS.PUBLIC,
|
||||
]
|
||||
|
||||
export const RoleIDVersion = {
|
||||
// original version, with a UUID based ID
|
||||
UUID: undefined,
|
||||
// new version - with name based ID
|
||||
NAME: "name",
|
||||
}
|
||||
|
||||
export class Role implements RoleDoc {
|
||||
_id: string
|
||||
_rev?: string
|
||||
name: string
|
||||
permissionId: string
|
||||
inherits?: string
|
||||
version?: string
|
||||
permissions = {}
|
||||
|
||||
constructor(id: string, name: string, permissionId: string) {
|
||||
this._id = id
|
||||
this.name = name
|
||||
this.permissionId = permissionId
|
||||
// version for managing the ID - removing the role_ when responding
|
||||
this.version = RoleIDVersion.NAME
|
||||
}
|
||||
|
||||
addInheritance(inherits: string) {
|
||||
|
@ -140,9 +150,13 @@ export function lowerBuiltinRoleID(roleId1?: string, roleId2?: string): string {
|
|||
* Gets the role object, this is mainly useful for two purposes, to check if the level exists and
|
||||
* to check if the role inherits any others.
|
||||
* @param {string|null} roleId The level ID to lookup.
|
||||
* @param {object|null} opts options for the function, like whether to halt errors, instead return public.
|
||||
* @returns {Promise<Role|object|null>} The role object, which may contain an "inherits" property.
|
||||
*/
|
||||
export async function getRole(roleId?: string): Promise<RoleDoc | undefined> {
|
||||
export async function getRole(
|
||||
roleId?: string,
|
||||
opts?: { defaultPublic?: boolean }
|
||||
): Promise<RoleDoc | undefined> {
|
||||
if (!roleId) {
|
||||
return undefined
|
||||
}
|
||||
|
@ -153,14 +167,20 @@ export async function getRole(roleId?: string): Promise<RoleDoc | undefined> {
|
|||
role = cloneDeep(
|
||||
Object.values(BUILTIN_ROLES).find(role => role._id === roleId)
|
||||
)
|
||||
} else {
|
||||
// make sure has the prefix (if it has it then it won't be added)
|
||||
roleId = prefixRoleID(roleId)
|
||||
}
|
||||
try {
|
||||
const db = getAppDB()
|
||||
const dbRole = await db.get(getDBRoleID(roleId))
|
||||
role = Object.assign(role, dbRole)
|
||||
// finalise the ID
|
||||
role._id = getExternalRoleID(role._id)
|
||||
role._id = getExternalRoleID(role._id, role.version)
|
||||
} catch (err) {
|
||||
if (!isBuiltin(roleId) && opts?.defaultPublic) {
|
||||
return cloneDeep(BUILTIN_ROLES.PUBLIC)
|
||||
}
|
||||
// only throw an error if there is no role at all
|
||||
if (Object.keys(role).length === 0) {
|
||||
throw err
|
||||
|
@ -254,6 +274,9 @@ export async function getAllRoles(appId?: string) {
|
|||
})
|
||||
)
|
||||
roles = body.rows.map((row: any) => row.doc)
|
||||
roles.forEach(
|
||||
role => (role._id = getExternalRoleID(role._id!, role.version))
|
||||
)
|
||||
}
|
||||
const builtinRoles = getBuiltinRoles()
|
||||
|
||||
|
@ -261,14 +284,15 @@ export async function getAllRoles(appId?: string) {
|
|||
for (let builtinRoleId of EXTERNAL_BUILTIN_ROLE_IDS) {
|
||||
const builtinRole = builtinRoles[builtinRoleId]
|
||||
const dbBuiltin = roles.filter(
|
||||
dbRole => getExternalRoleID(dbRole._id) === builtinRoleId
|
||||
dbRole =>
|
||||
getExternalRoleID(dbRole._id!, dbRole.version) === builtinRoleId
|
||||
)[0]
|
||||
if (dbBuiltin == null) {
|
||||
roles.push(builtinRole || builtinRoles.BASIC)
|
||||
} else {
|
||||
// remove role and all back after combining with the builtin
|
||||
roles = roles.filter(role => role._id !== dbBuiltin._id)
|
||||
dbBuiltin._id = getExternalRoleID(dbBuiltin._id)
|
||||
dbBuiltin._id = getExternalRoleID(dbBuiltin._id!, dbBuiltin.version)
|
||||
roles.push(Object.assign(builtinRole, dbBuiltin))
|
||||
}
|
||||
}
|
||||
|
@ -374,19 +398,22 @@ export class AccessController {
|
|||
/**
|
||||
* Adds the "role_" for builtin role IDs which are to be written to the DB (for permissions).
|
||||
*/
|
||||
export function getDBRoleID(roleId?: string) {
|
||||
if (roleId?.startsWith(DocumentType.ROLE)) {
|
||||
return roleId
|
||||
export function getDBRoleID(roleName: string) {
|
||||
if (roleName?.startsWith(DocumentType.ROLE)) {
|
||||
return roleName
|
||||
}
|
||||
return generateRoleID(roleId)
|
||||
return prefixRoleID(roleName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the "role_" from builtin role IDs that have been written to the DB (for permissions).
|
||||
*/
|
||||
export function getExternalRoleID(roleId?: string) {
|
||||
export function getExternalRoleID(roleId: string, version?: string) {
|
||||
// for built-in roles we want to remove the DB role ID element (role_)
|
||||
if (roleId?.startsWith(DocumentType.ROLE) && isBuiltin(roleId)) {
|
||||
if (
|
||||
(roleId.startsWith(DocumentType.ROLE) && isBuiltin(roleId)) ||
|
||||
version === RoleIDVersion.NAME
|
||||
) {
|
||||
return roleId.split(`${DocumentType.ROLE}${SEPARATOR}`)[1]
|
||||
}
|
||||
return roleId
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
export let tooltip = undefined
|
||||
export let newStyles = true
|
||||
export let id
|
||||
|
||||
let showTooltip = false
|
||||
</script>
|
||||
|
||||
<button
|
||||
|
@ -35,9 +33,6 @@
|
|||
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
||||
{disabled}
|
||||
on:click|preventDefault
|
||||
on:mouseover={() => (showTooltip = true)}
|
||||
on:focus={() => (showTooltip = true)}
|
||||
on:mouseleave={() => (showTooltip = false)}
|
||||
>
|
||||
{#if icon}
|
||||
<svg
|
||||
|
@ -52,19 +47,7 @@
|
|||
{#if $$slots}
|
||||
<span class="spectrum-Button-label"><slot /></span>
|
||||
{/if}
|
||||
{#if !disabled && tooltip}
|
||||
<div class="tooltip-icon">
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-Icon--size{size.toUpperCase()}"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
aria-label="Info"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-InfoOutline" />
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
{#if showTooltip && tooltip}
|
||||
{#if tooltip}
|
||||
<div class="tooltip">
|
||||
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} />
|
||||
</div>
|
||||
|
@ -75,7 +58,6 @@
|
|||
button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.spectrum-Button-label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
@ -93,11 +75,13 @@
|
|||
text-align: center;
|
||||
transform: translateX(-50%);
|
||||
left: 50%;
|
||||
top: calc(100% - 3px);
|
||||
top: 100%;
|
||||
opacity: 0;
|
||||
transition: opacity 130ms ease-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tooltip-icon {
|
||||
padding-left: var(--spacing-m);
|
||||
line-height: 0;
|
||||
button:hover .tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
.spectrum-Button--primary.new-styles {
|
||||
background: var(--spectrum-global-color-gray-800);
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: placeholder = !value
|
||||
|
||||
const extractProperty = (value, property) => {
|
||||
if (value && typeof value === "object") {
|
||||
return value[property]
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
export let disabled = false
|
||||
export let error = null
|
||||
export let validate = null
|
||||
export let indeterminate = false
|
||||
export let compact = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -21,11 +23,19 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<FancyField {error} {value} {validate} {disabled} clickable on:click={onChange}>
|
||||
<FancyField
|
||||
{error}
|
||||
{value}
|
||||
{validate}
|
||||
{disabled}
|
||||
{compact}
|
||||
clickable
|
||||
on:click={onChange}
|
||||
>
|
||||
<span>
|
||||
<Checkbox {disabled} {value} />
|
||||
<Checkbox {disabled} {value} {indeterminate} />
|
||||
</span>
|
||||
<div class="text">
|
||||
<div class="text" class:compact>
|
||||
{#if text}
|
||||
{text}
|
||||
{/if}
|
||||
|
@ -47,6 +57,10 @@
|
|||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.text.compact {
|
||||
font-size: 13px;
|
||||
line-height: 15px;
|
||||
}
|
||||
.text > :global(*) {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
<script>
|
||||
import FancyCheckbox from "./FancyCheckbox.svelte"
|
||||
import FancyForm from "./FancyForm.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let options = []
|
||||
export let selected = []
|
||||
export let showSelectAll = true
|
||||
export let selectAllText = "Select all"
|
||||
|
||||
let selectedBooleans = options.map(x => selected.indexOf(x) > -1)
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: updateSelected(selectedBooleans)
|
||||
$: allSelected = selected?.length === options.length
|
||||
$: noneSelected = !selected?.length
|
||||
|
||||
function reset() {
|
||||
return Array(options.length).fill(true)
|
||||
}
|
||||
|
||||
function updateSelected(selectedArr) {
|
||||
const array = []
|
||||
for (let [i, isSelected] of Object.entries(selectedArr)) {
|
||||
if (isSelected) {
|
||||
array.push(options[i])
|
||||
}
|
||||
}
|
||||
selected = array
|
||||
dispatch("change", selected)
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
if (allSelected === true) {
|
||||
selectedBooleans = []
|
||||
} else {
|
||||
selectedBooleans = reset()
|
||||
}
|
||||
dispatch("change", selected)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if options && Array.isArray(options)}
|
||||
<div class="checkbox-group" class:has-select-all={showSelectAll}>
|
||||
<FancyForm on:change>
|
||||
{#if showSelectAll}
|
||||
<FancyCheckbox
|
||||
bind:value={allSelected}
|
||||
on:change={toggleSelectAll}
|
||||
text={selectAllText}
|
||||
indeterminate={!allSelected && !noneSelected}
|
||||
compact
|
||||
/>
|
||||
{/if}
|
||||
{#each options as option, i}
|
||||
<FancyCheckbox bind:value={selectedBooleans[i]} text={option} compact />
|
||||
{/each}
|
||||
</FancyForm>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.checkbox-group.has-select-all :global(.fancy-field:first-of-type) {
|
||||
background: var(--spectrum-global-color-gray-100);
|
||||
}
|
||||
.checkbox-group.has-select-all :global(.fancy-field:first-of-type:hover) {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
</style>
|
|
@ -11,6 +11,7 @@
|
|||
export let value
|
||||
export let ref
|
||||
export let autoHeight
|
||||
export let compact = false
|
||||
|
||||
const formContext = getContext("fancy-form")
|
||||
const id = Math.random()
|
||||
|
@ -42,6 +43,7 @@
|
|||
class:disabled
|
||||
class:focused
|
||||
class:clickable
|
||||
class:compact
|
||||
class:auto-height={autoHeight}
|
||||
>
|
||||
<div class="content" on:click>
|
||||
|
@ -61,7 +63,6 @@
|
|||
|
||||
<style>
|
||||
.fancy-field {
|
||||
max-width: 400px;
|
||||
background: var(--spectrum-global-color-gray-75);
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: 4px;
|
||||
|
@ -69,6 +70,12 @@
|
|||
transition: border-color 130ms ease-out, background 130ms ease-out,
|
||||
background 130ms ease-out;
|
||||
color: var(--spectrum-global-color-gray-800);
|
||||
--padding: 16px;
|
||||
--height: 64px;
|
||||
}
|
||||
.fancy-field.compact {
|
||||
--padding: 8px;
|
||||
--height: 36px;
|
||||
}
|
||||
.fancy-field:hover {
|
||||
border-color: var(--spectrum-global-color-gray-400);
|
||||
|
@ -91,8 +98,8 @@
|
|||
}
|
||||
.content {
|
||||
position: relative;
|
||||
height: 64px;
|
||||
padding: 0 16px;
|
||||
height: var(--height);
|
||||
padding: 0 var(--padding);
|
||||
}
|
||||
.fancy-field.auto-height .content {
|
||||
height: auto;
|
||||
|
@ -103,7 +110,7 @@
|
|||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
gap: var(--padding);
|
||||
}
|
||||
.field {
|
||||
flex: 1 1 auto;
|
||||
|
|
|
@ -4,4 +4,5 @@ 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 FancyCheckboxGroup } from "./FancyCheckboxGroup.svelte"
|
||||
export { default as ErrorMessage } from "./ErrorMessage.svelte"
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
export let text = null
|
||||
export let disabled = false
|
||||
export let size
|
||||
export let indeterminate = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = event => {
|
||||
|
@ -22,6 +23,7 @@
|
|||
class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}"
|
||||
class:is-invalid={!!error}
|
||||
class:checked={value}
|
||||
class:is-indeterminate={indeterminate}
|
||||
>
|
||||
<input
|
||||
checked={value}
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
timeOnly,
|
||||
enableTime,
|
||||
time24hr,
|
||||
disabled,
|
||||
}
|
||||
|
||||
const handleChange = event => {
|
||||
|
|
|
@ -150,7 +150,7 @@
|
|||
</div>
|
||||
{:else if variables.length}
|
||||
<div style="max-height: 100px">
|
||||
{#each variables as variable, idx}
|
||||
{#each variables as variable}
|
||||
<li
|
||||
class="spectrum-Menu-item"
|
||||
role="option"
|
||||
|
|
|
@ -99,9 +99,15 @@
|
|||
bind:this={button}
|
||||
>
|
||||
{#if fieldIcon}
|
||||
<span class="option-extra icon">
|
||||
<Icon size="S" name={fieldIcon} />
|
||||
</span>
|
||||
{#if !useOptionIconImage}
|
||||
<span class="option-extra icon">
|
||||
<Icon size="S" name={fieldIcon} />
|
||||
</span>
|
||||
{:else}
|
||||
<span class="option-extra icon field-icon">
|
||||
<img src={fieldIcon} alt="icon" width="15" height="15" />
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if fieldColour}
|
||||
<span class="option-extra">
|
||||
|
@ -311,4 +317,8 @@
|
|||
max-width: 170px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.option-extra.icon.field-icon {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import "@spectrum-css/link/dist/index-vars.css"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||
|
||||
export let href = "#"
|
||||
export let size = "M"
|
||||
|
@ -10,18 +11,61 @@
|
|||
export let overBackground = false
|
||||
export let target
|
||||
export let download
|
||||
export let disabled = false
|
||||
export let tooltip = null
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const onClick = e => {
|
||||
if (!disabled) {
|
||||
dispatch("click")
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<a
|
||||
on:click={e => dispatch("click") && e.stopPropagation()}
|
||||
on:click={onClick}
|
||||
{href}
|
||||
{target}
|
||||
{download}
|
||||
class:disabled
|
||||
class:spectrum-Link--primary={primary}
|
||||
class:spectrum-Link--secondary={secondary}
|
||||
class:spectrum-Link--overBackground={overBackground}
|
||||
class:spectrum-Link--quiet={quiet}
|
||||
class="spectrum-Link spectrum-Link--size{size}"><slot /></a
|
||||
class="spectrum-Link spectrum-Link--size{size}"
|
||||
>
|
||||
<slot />
|
||||
{#if tooltip}
|
||||
<div class="tooltip">
|
||||
<Tooltip textWrapping direction="bottom" text={tooltip} />
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
|
||||
<style>
|
||||
a {
|
||||
position: relative;
|
||||
}
|
||||
a.disabled {
|
||||
color: var(--spectrum-global-color-gray-500);
|
||||
}
|
||||
a.disabled:hover {
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
}
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
transform: translateX(-50%);
|
||||
opacity: 0;
|
||||
transition: 130ms ease-out;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
}
|
||||
a:hover .tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -87,7 +87,7 @@
|
|||
border-color: var(--spectrum-global-color-gray-400);
|
||||
}
|
||||
/* Toolbar button color */
|
||||
:global(.EasyMDEContainer .editor-toolbar button i) {
|
||||
:global(.EasyMDEContainer .editor-toolbar button) {
|
||||
color: var(--spectrum-global-color-gray-800);
|
||||
}
|
||||
/* Separator between toolbar buttons*/
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
export let fixed = false
|
||||
export let inline = false
|
||||
export let disableCancel = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let visible = fixed || inline
|
||||
|
@ -38,7 +39,7 @@
|
|||
}
|
||||
|
||||
export function cancel() {
|
||||
if (!visible) {
|
||||
if (!visible || disableCancel) {
|
||||
return
|
||||
}
|
||||
dispatch("cancel")
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
<script context="module">
|
||||
export const keepOpen = Symbol("keepOpen")
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import "@spectrum-css/dialog/dist/index-vars.css"
|
||||
import { getContext } from "svelte"
|
||||
|
@ -30,7 +34,7 @@
|
|||
|
||||
async function secondary(e) {
|
||||
loading = true
|
||||
if (!secondaryAction || (await secondaryAction(e)) !== false) {
|
||||
if (!secondaryAction || (await secondaryAction(e)) !== keepOpen) {
|
||||
hide()
|
||||
}
|
||||
loading = false
|
||||
|
@ -38,7 +42,7 @@
|
|||
|
||||
async function confirm() {
|
||||
loading = true
|
||||
if (!onConfirm || (await onConfirm()) !== false) {
|
||||
if (!onConfirm || (await onConfirm()) !== keepOpen) {
|
||||
hide()
|
||||
}
|
||||
loading = false
|
||||
|
@ -46,7 +50,7 @@
|
|||
|
||||
async function close() {
|
||||
loading = true
|
||||
if (!onCancel || (await onCancel()) !== false) {
|
||||
if (!onCancel || (await onCancel()) !== keepOpen) {
|
||||
cancel()
|
||||
}
|
||||
loading = false
|
||||
|
|
|
@ -90,6 +90,6 @@
|
|||
.spectrum-Popover {
|
||||
min-width: var(--spectrum-global-dimension-size-2000);
|
||||
border-color: var(--spectrum-global-color-gray-300);
|
||||
overflow: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
$: type = getType(schema)
|
||||
$: customRenderer = customRenderers?.find(x => x.column === schema?.name)
|
||||
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer
|
||||
$: width = schema?.width || "150px"
|
||||
$: cellValue = getCellValue(value, schema.template)
|
||||
|
||||
const getType = schema => {
|
||||
|
|
|
@ -204,6 +204,12 @@
|
|||
})
|
||||
return columns
|
||||
.sort((a, b) => {
|
||||
if (a.divider) {
|
||||
return a
|
||||
}
|
||||
if (b.divider) {
|
||||
return b
|
||||
}
|
||||
const orderA = a.order || Number.MAX_SAFE_INTEGER
|
||||
const orderB = b.order || Number.MAX_SAFE_INTEGER
|
||||
const nameA = getDisplayName(a)
|
||||
|
@ -373,7 +379,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
{#if sortedRows?.length}
|
||||
{#each sortedRows as row, idx}
|
||||
{#each sortedRows as row}
|
||||
<div class="spectrum-Table-row" class:clickable={allowClickRows}>
|
||||
{#if showEditColumn}
|
||||
<div
|
||||
|
|
|
@ -42,7 +42,7 @@ export { default as MenuSection } from "./Menu/Section.svelte"
|
|||
export { default as MenuSeparator } from "./Menu/Separator.svelte"
|
||||
export { default as MenuItem } from "./Menu/Item.svelte"
|
||||
export { default as Modal } from "./Modal/Modal.svelte"
|
||||
export { default as ModalContent } from "./Modal/ModalContent.svelte"
|
||||
export { default as ModalContent, keepOpen } from "./Modal/ModalContent.svelte"
|
||||
export { default as NotificationDisplay } from "./Notification/NotificationDisplay.svelte"
|
||||
export { default as Notification } from "./Notification/Notification.svelte"
|
||||
export { default as SideNavigation } from "./SideNavigation/Navigation.svelte"
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
<meta charset='utf8'>
|
||||
<meta name='viewport' content='width=device-width'>
|
||||
<title>Budibase</title>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"
|
||||
rel="stylesheet" />
|
||||
<link href="/builder/fonts/source-sans-pro/400.css" rel="stylesheet" />
|
||||
<link href="/builder/fonts/source-sans-pro/600.css" rel="stylesheet" />
|
||||
<link href="/builder/fonts/source-sans-pro/700.css" rel="stylesheet" />
|
||||
<link href="/builder/fonts/remixicon.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body id="app">
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
"dev:builder": "routify -c dev:vite",
|
||||
"dev:vite": "vite --host 0.0.0.0",
|
||||
"rollup": "rollup -c -w",
|
||||
"test": "vitest run"
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"jest": {
|
||||
"globals": {
|
||||
|
@ -70,6 +71,7 @@
|
|||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@codemirror/view": "^6.11.2",
|
||||
"@fontsource/source-sans-pro": "^5.0.3",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
|
@ -122,6 +124,7 @@
|
|||
"tsconfig-paths": "4.0.0",
|
||||
"typescript": "4.7.3",
|
||||
"vite": "^3.0.8",
|
||||
"vite-plugin-static-copy": "^0.16.0",
|
||||
"vitest": "^0.29.2"
|
||||
},
|
||||
"nx": {
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 787 B |
|
@ -1,46 +0,0 @@
|
|||
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 = {}
|
||||
let existingTypeCount = get(datasources).list.filter(
|
||||
ds => ds.source === config.type
|
||||
).length
|
||||
|
||||
let baseName = IntegrationNames[config.type] || config.name
|
||||
let name =
|
||||
existingTypeCount === 0 ? baseName : `${baseName}-${existingTypeCount + 1}`
|
||||
|
||||
datasource.type = "datasource"
|
||||
datasource.source = config.type
|
||||
datasource.config = config.config
|
||||
datasource.name = name
|
||||
datasource.plus = config.plus
|
||||
|
||||
return datasource
|
||||
}
|
||||
|
||||
export async function saveDatasource(config, skipFetch = false) {
|
||||
const datasource = prepareData(config)
|
||||
// Create datasource
|
||||
const resp = await datasources.save(datasource, !skipFetch && datasource.plus)
|
||||
|
||||
// update the tables incase datasource plus
|
||||
await tables.fetch()
|
||||
await datasources.select(resp._id)
|
||||
return resp
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -3,6 +3,7 @@ import { getAutomationStore } from "./store/automation"
|
|||
import { getTemporalStore } from "./store/temporal"
|
||||
import { getThemeStore } from "./store/theme"
|
||||
import { getUserStore } from "./store/users"
|
||||
import { getDeploymentStore } from "./store/deployments"
|
||||
import { derived } from "svelte/store"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
@ -14,6 +15,7 @@ export const automationStore = getAutomationStore()
|
|||
export const themeStore = getThemeStore()
|
||||
export const temporalStore = getTemporalStore()
|
||||
export const userStore = getUserStore()
|
||||
export const deploymentStore = getDeploymentStore()
|
||||
|
||||
// Setup history for screens
|
||||
export const screenHistoryStore = createHistoryStore({
|
||||
|
@ -118,3 +120,20 @@ export const selectedAutomation = derived(automationStore, $automationStore => {
|
|||
x => x._id === $automationStore.selectedAutomationId
|
||||
)
|
||||
})
|
||||
|
||||
// Derive map of resource IDs to other users.
|
||||
// We only ever care about a single user in each resource, so if multiple users
|
||||
// share the same datasource we can just overwrite them.
|
||||
export const userSelectedResourceMap = derived(userStore, $userStore => {
|
||||
let map = {}
|
||||
$userStore.forEach(user => {
|
||||
if (user.builderMetadata?.selectedResourceId) {
|
||||
map[user.builderMetadata?.selectedResourceId] = user
|
||||
}
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
export const isOnlyUser = derived(userStore, $userStore => {
|
||||
return $userStore.length === 1
|
||||
})
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { API } from "api"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
export const getDeploymentStore = () => {
|
||||
let store = writable([])
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
store.set(await API.getAppDeployments())
|
||||
} catch (err) {
|
||||
notifications.error("Error fetching deployments")
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
actions: {
|
||||
load,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -38,6 +38,7 @@ import {
|
|||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
import { getComponentFieldOptions } from "helpers/formFields"
|
||||
import { createBuilderWebsocket } from "builderStore/websocket"
|
||||
import { BuilderSocketEvent } from "@budibase/shared-core"
|
||||
|
||||
const INITIAL_FRONTEND_STATE = {
|
||||
initialised: false,
|
||||
|
@ -61,6 +62,9 @@ const INITIAL_FRONTEND_STATE = {
|
|||
showNotificationAction: false,
|
||||
sidePanel: false,
|
||||
},
|
||||
features: {
|
||||
componentValidation: false,
|
||||
},
|
||||
errors: [],
|
||||
hasAppPackage: false,
|
||||
libraries: null,
|
||||
|
@ -74,6 +78,7 @@ const INITIAL_FRONTEND_STATE = {
|
|||
propertyFocus: null,
|
||||
builderSidePanel: false,
|
||||
hasLock: true,
|
||||
showPreview: false,
|
||||
|
||||
// URL params
|
||||
selectedScreenId: null,
|
||||
|
@ -116,10 +121,13 @@ export const getFrontendStore = () => {
|
|||
reset: () => {
|
||||
store.set({ ...INITIAL_FRONTEND_STATE })
|
||||
websocket?.disconnect()
|
||||
websocket = null
|
||||
},
|
||||
initialise: async pkg => {
|
||||
const { layouts, screens, application, clientLibPath, hasLock } = pkg
|
||||
websocket = createBuilderWebsocket(application.appId)
|
||||
if (!websocket) {
|
||||
websocket = createBuilderWebsocket(application.appId)
|
||||
}
|
||||
await store.actions.components.refreshDefinitions(application.appId)
|
||||
|
||||
// Reset store state
|
||||
|
@ -144,6 +152,11 @@ export const getFrontendStore = () => {
|
|||
navigation: application.navigation || {},
|
||||
usedPlugins: application.usedPlugins || [],
|
||||
hasLock,
|
||||
features: {
|
||||
...INITIAL_FRONTEND_STATE.features,
|
||||
...application.features,
|
||||
},
|
||||
icon: application.icon || {},
|
||||
initialised: true,
|
||||
}))
|
||||
screenHistoryStore.reset()
|
||||
|
@ -224,6 +237,7 @@ export const getFrontendStore = () => {
|
|||
legalDirectChildren = []
|
||||
) => {
|
||||
const type = component._component
|
||||
|
||||
if (illegalChildren.includes(type)) {
|
||||
return type
|
||||
}
|
||||
|
@ -237,10 +251,13 @@ export const getFrontendStore = () => {
|
|||
return
|
||||
}
|
||||
|
||||
if (type === "@budibase/standard-components/sidepanel") {
|
||||
illegalChildren = []
|
||||
}
|
||||
|
||||
const definition = store.actions.components.getDefinition(
|
||||
component._component
|
||||
)
|
||||
|
||||
// Reset whitelist for direct children
|
||||
legalDirectChildren = []
|
||||
if (definition?.legalDirectChildren?.length) {
|
||||
|
@ -279,9 +296,12 @@ export const getFrontendStore = () => {
|
|||
}
|
||||
},
|
||||
save: async screen => {
|
||||
// Validate screen structure
|
||||
// Temporarily disabled to accommodate migration issues
|
||||
// store.actions.screens.validate(screen)
|
||||
const state = get(store)
|
||||
|
||||
// Validate screen structure if the app supports it
|
||||
if (state.features?.componentValidation) {
|
||||
store.actions.screens.validate(screen)
|
||||
}
|
||||
|
||||
// Check screen definition for any component settings which need updated
|
||||
store.actions.screens.enrichEmptySettings(screen)
|
||||
|
@ -292,7 +312,6 @@ export const getFrontendStore = () => {
|
|||
const routesResponse = await API.fetchAppRoutes()
|
||||
|
||||
// If plugins changed we need to fetch the latest app metadata
|
||||
const state = get(store)
|
||||
let usedPlugins = state.usedPlugins
|
||||
if (savedScreen.pluginAdded) {
|
||||
const { application } = await API.fetchAppPackage(state.appId)
|
||||
|
@ -335,6 +354,33 @@ export const getFrontendStore = () => {
|
|||
}
|
||||
return await sequentialScreenPatch(patchFn, screenId)
|
||||
},
|
||||
replace: async (screenId, screen) => {
|
||||
if (!screenId) {
|
||||
return
|
||||
}
|
||||
if (!screen) {
|
||||
// Screen deletion
|
||||
store.update(state => ({
|
||||
...state,
|
||||
screens: state.screens.filter(x => x._id !== screenId),
|
||||
}))
|
||||
} else {
|
||||
const index = get(store).screens.findIndex(x => x._id === screen._id)
|
||||
if (index === -1) {
|
||||
// Screen addition
|
||||
store.update(state => ({
|
||||
...state,
|
||||
screens: [...state.screens, screen],
|
||||
}))
|
||||
} else {
|
||||
// Screen update
|
||||
store.update(state => {
|
||||
state.screens[index] = screen
|
||||
return state
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
delete: async screens => {
|
||||
const screensToDelete = Array.isArray(screens) ? screens : [screens]
|
||||
|
||||
|
@ -1287,7 +1333,7 @@ export const getFrontendStore = () => {
|
|||
links: {
|
||||
save: async (url, title) => {
|
||||
const navigation = get(store).navigation
|
||||
let links = [...navigation?.links]
|
||||
let links = [...(navigation?.links ?? [])]
|
||||
|
||||
// Skip if we have an identical link
|
||||
if (links.find(link => link.url === url && link.text === title)) {
|
||||
|
@ -1347,6 +1393,21 @@ export const getFrontendStore = () => {
|
|||
})
|
||||
},
|
||||
},
|
||||
websocket: {
|
||||
selectResource: id => {
|
||||
websocket.emit(BuilderSocketEvent.SelectResource, {
|
||||
resourceId: id,
|
||||
})
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
replace: metadata => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
...metadata,
|
||||
}))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return store
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { createWebsocket } from "@budibase/frontend-core"
|
||||
import { userStore, store } from "builderStore"
|
||||
import { userStore, store, deploymentStore } from "builderStore"
|
||||
import { datasources, tables } from "stores/backend"
|
||||
import { get } from "svelte/store"
|
||||
import { auth } from "stores/portal"
|
||||
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core"
|
||||
import { apps } from "stores/portal"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
|
||||
export const createBuilderWebsocket = appId => {
|
||||
const socket = createWebsocket("/socket/builder")
|
||||
|
@ -31,7 +33,6 @@ export const createBuilderWebsocket = appId => {
|
|||
})
|
||||
socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => {
|
||||
if (userId === get(auth)?.user?._id) {
|
||||
notifications.success("You can now edit screens and automations")
|
||||
store.update(state => ({
|
||||
...state,
|
||||
hasLock: true,
|
||||
|
@ -39,15 +40,32 @@ export const createBuilderWebsocket = appId => {
|
|||
}
|
||||
})
|
||||
|
||||
// Table events
|
||||
// Data section events
|
||||
socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => {
|
||||
tables.replaceTable(id, table)
|
||||
})
|
||||
|
||||
// Datasource events
|
||||
socket.onOther(BuilderSocketEvent.DatasourceChange, ({ id, datasource }) => {
|
||||
datasources.replaceDatasource(id, datasource)
|
||||
})
|
||||
|
||||
// Design section events
|
||||
socket.onOther(BuilderSocketEvent.ScreenChange, ({ id, screen }) => {
|
||||
store.actions.screens.replace(id, screen)
|
||||
})
|
||||
socket.onOther(BuilderSocketEvent.AppMetadataChange, ({ metadata }) => {
|
||||
store.actions.metadata.replace(metadata)
|
||||
})
|
||||
socket.onOther(
|
||||
BuilderSocketEvent.AppPublishChange,
|
||||
async ({ user, published }) => {
|
||||
await apps.load()
|
||||
if (published) {
|
||||
await deploymentStore.actions.load()
|
||||
}
|
||||
const verb = published ? "published" : "unpublished"
|
||||
notifications.success(`${helpers.getUserLabel(user)} ${verb} this app`)
|
||||
}
|
||||
)
|
||||
|
||||
return socket
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
import { admin, licensing } from "stores/portal"
|
||||
import { externalActions } from "./ExternalActions"
|
||||
import { TriggerStepID } from "constants/backend/automations"
|
||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||
import { checkForCollectStep } from "builderStore/utils"
|
||||
|
||||
export let blockIdx
|
||||
|
@ -149,7 +149,7 @@
|
|||
<div class="item-body">
|
||||
<Icon name={action.icon} />
|
||||
<Body size="XS">{action.name}</Body>
|
||||
{#if isDisabled && !syncAutomationsEnabled}
|
||||
{#if isDisabled && !syncAutomationsEnabled && action.stepId === ActionStepID.COLLECT}
|
||||
<div class="tag-color">
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Business</Tag>
|
||||
|
@ -168,7 +168,7 @@
|
|||
<Layout noPadding gap="XS">
|
||||
<Detail size="S">Plugins</Detail>
|
||||
<div class="item-list">
|
||||
{#each Object.entries(plugins) as [idx, action]}
|
||||
{#each Object.entries(plugins) as [_, action]}
|
||||
<div
|
||||
class="item"
|
||||
class:selected={selectedAction === action.name}
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
</script>
|
||||
|
||||
<div>
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||
{@html html}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -78,9 +78,6 @@
|
|||
}
|
||||
|
||||
async function removeLooping() {
|
||||
let loopBlock = $selectedAutomation?.definition.steps.find(
|
||||
x => x.blockToLoop === block.id
|
||||
)
|
||||
try {
|
||||
await automationStore.actions.deleteAutomationBlock(loopBlock)
|
||||
} catch (error) {
|
||||
|
@ -89,10 +86,6 @@
|
|||
}
|
||||
|
||||
async function deleteStep() {
|
||||
let loopBlock = $selectedAutomation?.definition.steps.find(
|
||||
x => x.blockToLoop === block.id
|
||||
)
|
||||
|
||||
try {
|
||||
if (loopBlock) {
|
||||
await automationStore.actions.deleteAutomationBlock(loopBlock)
|
||||
|
@ -168,8 +161,8 @@
|
|||
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
|
||||
.properties
|
||||
)}
|
||||
block={loopBlock}
|
||||
{webhookModal}
|
||||
block={loopBlock}
|
||||
/>
|
||||
</Layout>
|
||||
</div>
|
||||
|
@ -191,7 +184,7 @@
|
|||
{#if !isTrigger}
|
||||
<div>
|
||||
<div class="block-options">
|
||||
{#if block?.features?.[Features.LOOPING] || !block.features}
|
||||
{#if !loopBlock && (block?.features?.[Features.LOOPING] || !block.features)}
|
||||
<ActionButton on:click={() => addLooping()} icon="Reuse">
|
||||
Add Looping
|
||||
</ActionButton>
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
<Layout noPadding gap="XS">
|
||||
<Label size="S">Trigger</Label>
|
||||
<div class="item-list">
|
||||
{#each triggers as [idx, trigger]}
|
||||
{#each triggers as [_, trigger]}
|
||||
<div
|
||||
class="item"
|
||||
class:selected={selectedTrigger === trigger.name}
|
||||
|
|
|
@ -32,7 +32,12 @@
|
|||
<div slot="control" class="icon">
|
||||
<Icon s hoverable name="MoreSmallList" />
|
||||
</div>
|
||||
<MenuItem icon="Duplicate" on:click={duplicateAutomation}>Duplicate</MenuItem>
|
||||
<MenuItem
|
||||
icon="Duplicate"
|
||||
on:click={duplicateAutomation}
|
||||
disabled={automation.definition.trigger.name === "Webhook"}
|
||||
>Duplicate</MenuItem
|
||||
>
|
||||
<MenuItem icon="Edit" on:click={updateAutomationDialog.show}>Edit</MenuItem>
|
||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
||||
</ActionMenu>
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
Modal,
|
||||
notifications,
|
||||
Icon,
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
} from "@budibase/bbui"
|
||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
|
@ -306,6 +308,11 @@
|
|||
drawer.hide()
|
||||
}
|
||||
|
||||
function canShowField(key, value) {
|
||||
const dependsOn = value?.dependsOn
|
||||
return !dependsOn || !!inputData[dependsOn]
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await environment.loadVariables()
|
||||
|
@ -317,210 +324,233 @@
|
|||
|
||||
<div class="fields">
|
||||
{#each deprecatedSchemaProperties as [key, value]}
|
||||
<div class="block-field">
|
||||
{#if key !== "fields"}
|
||||
<Label
|
||||
tooltip={value.title === "Binding / Value"
|
||||
? "If using the String input type, please use a comma or newline separated string"
|
||||
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
||||
>
|
||||
{/if}
|
||||
{#if value.type === "string" && value.enum}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
placeholder={false}
|
||||
options={value.enum}
|
||||
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
|
||||
/>
|
||||
{:else if value.type === "json"}
|
||||
<Editor
|
||||
editorHeight="250"
|
||||
editorWidth="448"
|
||||
mode="json"
|
||||
value={inputData[key]?.value}
|
||||
on:change={e => {
|
||||
/**
|
||||
* TODO - Remove after November 2023
|
||||
* *******************************
|
||||
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
||||
* and the new JSON body.
|
||||
*/
|
||||
delete inputData.value1
|
||||
delete inputData.value2
|
||||
delete inputData.value3
|
||||
delete inputData.value4
|
||||
delete inputData.value5
|
||||
/***********************/
|
||||
onChange(e, key)
|
||||
}}
|
||||
/>
|
||||
{:else if value.customType === "column"}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
options={Object.keys(table?.schema || {})}
|
||||
/>
|
||||
{:else if value.customType === "filters"}
|
||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||
<Drawer bind:this={drawer} {fillWidth} title="Filtering">
|
||||
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
||||
Save
|
||||
</Button>
|
||||
<FilterDrawer
|
||||
slot="body"
|
||||
{filters}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
datasource={{ type: "table", tableId }}
|
||||
panel={AutomationBindingPanel}
|
||||
fillWidth
|
||||
on:change={e => (tempFilters = e.detail)}
|
||||
/>
|
||||
</Drawer>
|
||||
{:else if value.customType === "password"}
|
||||
<Input
|
||||
type="password"
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "email"}
|
||||
{#if isTestModal}
|
||||
<ModalBindableInput
|
||||
title={value.title}
|
||||
value={inputData[key]}
|
||||
panel={AutomationBindingPanel}
|
||||
type="email"
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
fillWidth
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{:else}
|
||||
<DrawerBindableInput
|
||||
fillWidth
|
||||
title={value.title}
|
||||
panel={AutomationBindingPanel}
|
||||
type="email"
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
allowJS={false}
|
||||
updateOnChange={false}
|
||||
drawerLeft="260px"
|
||||
/>
|
||||
{#if canShowField(key, value)}
|
||||
<div class="block-field">
|
||||
{#if key !== "fields" && value.type !== "boolean"}
|
||||
<Label
|
||||
tooltip={value.title === "Binding / Value"
|
||||
? "If using the String input type, please use a comma or newline separated string"
|
||||
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
||||
>
|
||||
{/if}
|
||||
{:else if value.customType === "query"}
|
||||
<QuerySelector
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "cron"}
|
||||
<CronBuilder on:change={e => onChange(e, key)} value={inputData[key]} />
|
||||
{:else if value.customType === "queryParams"}
|
||||
<QueryParamSelector
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
{bindings}
|
||||
/>
|
||||
{:else if value.customType === "table"}
|
||||
<TableSelector
|
||||
{isTrigger}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
/>
|
||||
{:else if value.customType === "row"}
|
||||
<RowSelector
|
||||
{block}
|
||||
value={inputData[key]}
|
||||
meta={inputData["meta"] || {}}
|
||||
on:change={e => {
|
||||
if (e.detail?.key) {
|
||||
onChange(e, e.detail.key)
|
||||
} else {
|
||||
onChange(e, key)
|
||||
}
|
||||
}}
|
||||
{bindings}
|
||||
{isTestModal}
|
||||
{isUpdateRow}
|
||||
/>
|
||||
{:else if value.customType === "webhookUrl"}
|
||||
<WebhookDisplay
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "fields"}
|
||||
<FieldSelector
|
||||
{block}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
{isTestModal}
|
||||
/>
|
||||
{:else if value.customType === "triggerSchema"}
|
||||
<SchemaSetup on:change={e => onChange(e, key)} value={inputData[key]} />
|
||||
{:else if value.customType === "code"}
|
||||
<CodeEditorModal>
|
||||
<CodeEditor
|
||||
value={inputData[key]}
|
||||
on:change={e => {
|
||||
// need to pass without the value inside
|
||||
onChange({ detail: e.detail }, key)
|
||||
inputData[key] = e.detail
|
||||
}}
|
||||
completions={[
|
||||
jsAutocomplete([
|
||||
...bindingsToCompletions(bindings, EditorModes.JS),
|
||||
]),
|
||||
]}
|
||||
mode={EditorModes.JS}
|
||||
height={500}
|
||||
/>
|
||||
<div class="messaging">
|
||||
<Icon name="FlashOn" />
|
||||
<div class="messaging-wrap">
|
||||
<div>Add available bindings by typing <strong>$</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</CodeEditorModal>
|
||||
{:else if value.customType === "loopOption"}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
autoWidth
|
||||
value={inputData[key]}
|
||||
options={["Array", "String"]}
|
||||
defaultValue={"Array"}
|
||||
/>
|
||||
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
||||
{#if isTestModal}
|
||||
<ModalBindableInput
|
||||
title={value.title}
|
||||
value={inputData[key]}
|
||||
panel={AutomationBindingPanel}
|
||||
type={value.customType}
|
||||
{#if value.type === "string" && value.enum && canShowField(key, value)}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
updateOnChange={false}
|
||||
value={inputData[key]}
|
||||
placeholder={false}
|
||||
options={value.enum}
|
||||
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
|
||||
/>
|
||||
{:else}
|
||||
<div class="test">
|
||||
{:else if value.type === "json"}
|
||||
<Editor
|
||||
editorHeight="250"
|
||||
editorWidth="448"
|
||||
mode="json"
|
||||
value={inputData[key]?.value}
|
||||
on:change={e => {
|
||||
/**
|
||||
* TODO - Remove after November 2023
|
||||
* *******************************
|
||||
* Code added to provide backwards compatibility between Values 1,2,3,4,5
|
||||
* and the new JSON body.
|
||||
*/
|
||||
delete inputData.value1
|
||||
delete inputData.value2
|
||||
delete inputData.value3
|
||||
delete inputData.value4
|
||||
delete inputData.value5
|
||||
/***********************/
|
||||
onChange(e, key)
|
||||
}}
|
||||
/>
|
||||
{:else if value.type === "boolean"}
|
||||
<div style="margin-top: 10px">
|
||||
<Checkbox
|
||||
text={value.title}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
/>
|
||||
</div>
|
||||
{:else if value.type === "date"}
|
||||
<DatePicker
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
/>
|
||||
{:else if value.customType === "column"}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
options={Object.keys(table?.schema || {})}
|
||||
/>
|
||||
{:else if value.customType === "filters"}
|
||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||
<Drawer bind:this={drawer} {fillWidth} title="Filtering">
|
||||
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
||||
Save
|
||||
</Button>
|
||||
<FilterDrawer
|
||||
slot="body"
|
||||
{filters}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
datasource={{ type: "table", tableId }}
|
||||
panel={AutomationBindingPanel}
|
||||
fillWidth
|
||||
on:change={e => (tempFilters = e.detail)}
|
||||
/>
|
||||
</Drawer>
|
||||
{:else if value.customType === "password"}
|
||||
<Input
|
||||
type="password"
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "email"}
|
||||
{#if isTestModal}
|
||||
<ModalBindableInput
|
||||
title={value.title}
|
||||
value={inputData[key]}
|
||||
panel={AutomationBindingPanel}
|
||||
type="email"
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
fillWidth
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{:else}
|
||||
<DrawerBindableInput
|
||||
fillWidth={true}
|
||||
fillWidth
|
||||
title={value.title}
|
||||
panel={AutomationBindingPanel}
|
||||
type={value.customType}
|
||||
type="email"
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
allowJS={false}
|
||||
updateOnChange={false}
|
||||
placeholder={value.customType === "queryLimit" ? queryLimit : ""}
|
||||
drawerLeft="260px"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if value.customType === "query"}
|
||||
<QuerySelector
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "cron"}
|
||||
<CronBuilder
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "queryParams"}
|
||||
<QueryParamSelector
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
{bindings}
|
||||
/>
|
||||
{:else if value.customType === "table"}
|
||||
<TableSelector
|
||||
{isTrigger}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
/>
|
||||
{:else if value.customType === "row"}
|
||||
<RowSelector
|
||||
{block}
|
||||
value={inputData[key]}
|
||||
meta={inputData["meta"] || {}}
|
||||
on:change={e => {
|
||||
if (e.detail?.key) {
|
||||
onChange(e, e.detail.key)
|
||||
} else {
|
||||
onChange(e, key)
|
||||
}
|
||||
}}
|
||||
{bindings}
|
||||
{isTestModal}
|
||||
{isUpdateRow}
|
||||
/>
|
||||
{:else if value.customType === "webhookUrl"}
|
||||
<WebhookDisplay
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "fields"}
|
||||
<FieldSelector
|
||||
{block}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
{isTestModal}
|
||||
/>
|
||||
{:else if value.customType === "triggerSchema"}
|
||||
<SchemaSetup
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "code"}
|
||||
<CodeEditorModal>
|
||||
<CodeEditor
|
||||
value={inputData[key]}
|
||||
on:change={e => {
|
||||
// need to pass without the value inside
|
||||
onChange({ detail: e.detail }, key)
|
||||
inputData[key] = e.detail
|
||||
}}
|
||||
completions={[
|
||||
jsAutocomplete([
|
||||
...bindingsToCompletions(bindings, EditorModes.JS),
|
||||
]),
|
||||
]}
|
||||
mode={EditorModes.JS}
|
||||
height={500}
|
||||
/>
|
||||
<div class="messaging">
|
||||
<Icon name="FlashOn" />
|
||||
<div class="messaging-wrap">
|
||||
<div>Add available bindings by typing <strong>$</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</CodeEditorModal>
|
||||
{:else if value.customType === "loopOption"}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
autoWidth
|
||||
value={inputData[key]}
|
||||
options={["Array", "String"]}
|
||||
defaultValue={"Array"}
|
||||
/>
|
||||
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
||||
{#if isTestModal}
|
||||
<ModalBindableInput
|
||||
title={value.title}
|
||||
value={inputData[key]}
|
||||
panel={AutomationBindingPanel}
|
||||
type={value.customType}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{:else}
|
||||
<div class="test">
|
||||
<DrawerBindableInput
|
||||
fillWidth={true}
|
||||
title={value.title}
|
||||
panel={AutomationBindingPanel}
|
||||
type={value.customType}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
updateOnChange={false}
|
||||
placeholder={value.customType === "queryLimit"
|
||||
? queryLimit
|
||||
: ""}
|
||||
drawerLeft="260px"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<Modal bind:this={webhookModal} width="30%">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { tables } from "stores/backend"
|
||||
import { datasources, tables } from "stores/backend"
|
||||
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
||||
import { TableNames } from "constants"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
|
@ -26,19 +26,33 @@
|
|||
$: id = $tables.selected?._id
|
||||
$: isUsersTable = id === TableNames.USERS
|
||||
$: isInternal = $tables.selected?.type !== "external"
|
||||
|
||||
const handleGridTableUpdate = async e => {
|
||||
tables.replaceTable(id, e.detail)
|
||||
|
||||
// We need to refresh datasources when an external table changes.
|
||||
// Type "external" may exist - sometimes type is "table" and sometimes it
|
||||
// is "external" - it has different meanings in different endpoints.
|
||||
// If we check both these then we hopefully catch all external tables.
|
||||
if (e.detail?.type === "external" || e.detail?.sql) {
|
||||
await datasources.fetch()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<Grid
|
||||
{API}
|
||||
tableId={id}
|
||||
tableType={$tables.selected?.type}
|
||||
allowAddRows={!isUsersTable}
|
||||
allowDeleteRows={!isUsersTable}
|
||||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||
showAvatars={false}
|
||||
on:updatetable={e => tables.replaceTable(id, e.detail)}
|
||||
on:updatetable={handleGridTableUpdate}
|
||||
>
|
||||
<svelte:fragment slot="filter">
|
||||
<GridFilterButton />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="controls">
|
||||
{#if isInternal}
|
||||
<GridCreateViewButton />
|
||||
|
@ -53,7 +67,6 @@
|
|||
<GridImportButton />
|
||||
{/if}
|
||||
<GridExportButton />
|
||||
<GridFilterButton />
|
||||
<GridAddColumnModal />
|
||||
<GridEditColumnModal />
|
||||
{#if isUsersTable}
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
import { goto, params } from "@roxi/routify"
|
||||
import { Table, Heading, Layout } from "@budibase/bbui"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||
import CreateEditUser from "./modals/CreateEditUser.svelte"
|
||||
import {
|
||||
TableNames,
|
||||
UNEDITABLE_USER_FIELDS,
|
||||
|
@ -33,7 +31,6 @@
|
|||
$: selectedRows, dispatch("selectionUpdated", selectedRows)
|
||||
$: isUsersTable = tableId === TableNames.USERS
|
||||
$: data && resetSelectedRows()
|
||||
$: editRowComponent = isUsersTable ? CreateEditUser : CreateEditRow
|
||||
$: {
|
||||
UNSORTABLE_TYPES.forEach(type => {
|
||||
Object.values(schema || {}).forEach(col => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { ActionButton, Modal, notifications } from "@budibase/bbui"
|
||||
import CreateEditRelationship from "../../Datasources/CreateEditRelationship.svelte"
|
||||
import { ActionButton, notifications } from "@budibase/bbui"
|
||||
import CreateEditRelationshipModal from "../../Datasources/CreateEditRelationshipModal.svelte"
|
||||
import { datasources } from "../../../../stores/backend"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
|
@ -8,9 +8,7 @@
|
|||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: datasource = findDatasource(table?._id)
|
||||
$: plusTables = datasource?.plus
|
||||
? Object.values(datasource?.entities || {})
|
||||
: []
|
||||
$: tables = datasource?.plus ? Object.values(datasource?.entities || {}) : []
|
||||
|
||||
let modal
|
||||
|
||||
|
@ -24,31 +22,32 @@
|
|||
})
|
||||
}
|
||||
|
||||
async function saveRelationship() {
|
||||
try {
|
||||
// Create datasource
|
||||
await datasources.save(datasource)
|
||||
notifications.success(`Relationship information saved.`)
|
||||
dispatch("updatecolumns")
|
||||
} catch (err) {
|
||||
notifications.error(`Error saving relationship info: ${err}`)
|
||||
}
|
||||
const afterSave = ({ action }) => {
|
||||
notifications.success(`Relationship ${action} successfully`)
|
||||
dispatch("updatecolumns")
|
||||
}
|
||||
|
||||
const onError = err => {
|
||||
notifications.error(`Error saving relationship info: ${err}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if datasource}
|
||||
<div>
|
||||
<ActionButton icon="DataCorrelated" primary quiet on:click={modal.show}>
|
||||
<ActionButton
|
||||
icon="DataCorrelated"
|
||||
primary
|
||||
quiet
|
||||
on:click={() => modal.show({ fromTable: table })}
|
||||
>
|
||||
Define relationship
|
||||
</ActionButton>
|
||||
</div>
|
||||
<Modal bind:this={modal}>
|
||||
<CreateEditRelationship
|
||||
{datasource}
|
||||
save={saveRelationship}
|
||||
close={modal.hide}
|
||||
{plusTables}
|
||||
selectedFromTable={table}
|
||||
/>
|
||||
</Modal>
|
||||
<CreateEditRelationshipModal
|
||||
bind:this={modal}
|
||||
{datasource}
|
||||
{tables}
|
||||
{afterSave}
|
||||
{onError}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -14,6 +14,12 @@
|
|||
|
||||
$: tempValue = filters || []
|
||||
$: schemaFields = Object.values(schema || {})
|
||||
$: text = getText(filters)
|
||||
|
||||
const getText = filters => {
|
||||
const count = filters?.length
|
||||
return count ? `Filter (${count})` : "Filter"
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionButton
|
||||
|
@ -23,7 +29,7 @@
|
|||
on:click={modal.show}
|
||||
selected={tempValue?.length > 0}
|
||||
>
|
||||
Filter
|
||||
{text}
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<ModalContent
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
|
||||
const { columns, tableId, filter, table } = getContext("grid")
|
||||
|
||||
// Wipe filter whenever table ID changes to avoid using stale filters
|
||||
$: $tableId, filter.set([])
|
||||
|
||||
const onFilter = e => {
|
||||
filter.set(e.detail || [])
|
||||
}
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
|
||||
export let disabled = false
|
||||
|
||||
const { rows, tableId, tableType } = getContext("grid")
|
||||
const { rows, tableId, table } = getContext("grid")
|
||||
</script>
|
||||
|
||||
<ImportButton
|
||||
{disabled}
|
||||
tableId={$tableId}
|
||||
{tableType}
|
||||
tableType={$table?.type}
|
||||
on:importrows={rows.actions.refreshData}
|
||||
/>
|
||||
|
|
|
@ -76,6 +76,10 @@ export function getBindings({
|
|||
// will be replaced by the main array binding
|
||||
readableBinding: label,
|
||||
runtimeBinding: binding,
|
||||
display: {
|
||||
name: label,
|
||||
type: field.name === FIELDS.LINK.name ? "Array" : field.name,
|
||||
},
|
||||
})
|
||||
}
|
||||
return bindings
|
||||
|
|
|
@ -57,7 +57,6 @@
|
|||
|
||||
let table = $tables.selected
|
||||
let confirmDeleteDialog
|
||||
let deletion
|
||||
let savingColumn
|
||||
let deleteColName
|
||||
let jsonSchemaModal
|
||||
|
@ -182,6 +181,15 @@
|
|||
indexes,
|
||||
})
|
||||
dispatch("updatecolumns")
|
||||
if (
|
||||
saveColumn.type === LINK_TYPE &&
|
||||
saveColumn.relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||
) {
|
||||
// Fetching the new tables
|
||||
tables.fetch()
|
||||
// Fetching the new relationships
|
||||
datasources.fetch()
|
||||
}
|
||||
if (originalName) {
|
||||
notifications.success("Column updated successfully")
|
||||
} else {
|
||||
|
@ -206,7 +214,6 @@
|
|||
notifications.success(`Column ${editableColumn.name} deleted`)
|
||||
confirmDeleteDialog.hide()
|
||||
hide()
|
||||
deletion = false
|
||||
dispatch("updatecolumns")
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -257,13 +264,11 @@
|
|||
|
||||
function confirmDelete() {
|
||||
confirmDeleteDialog.show()
|
||||
deletion = true
|
||||
}
|
||||
|
||||
function hideDeleteDialog() {
|
||||
confirmDeleteDialog.hide()
|
||||
deleteColName = ""
|
||||
deletion = false
|
||||
}
|
||||
|
||||
function getRelationshipOptions(field) {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { tables } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { ModalContent, keepOpen, notifications } from "@budibase/bbui"
|
||||
import RowFieldControl from "../RowFieldControl.svelte"
|
||||
import { API } from "api"
|
||||
import { ModalContent } from "@budibase/bbui"
|
||||
import { FIELDS } from "constants/backend"
|
||||
|
||||
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||
|
@ -41,8 +40,8 @@
|
|||
} else {
|
||||
notifications.error(`Failed to save row - ${error.message}`)
|
||||
}
|
||||
// Prevent modal closing if there were errors
|
||||
return false
|
||||
|
||||
return keepOpen
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import { notifications } from "@budibase/bbui"
|
||||
import RowFieldControl from "../RowFieldControl.svelte"
|
||||
import { API } from "api"
|
||||
import { ModalContent, Select, Link } from "@budibase/bbui"
|
||||
import { keepOpen, ModalContent, Select, Link } from "@budibase/bbui"
|
||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
|
||||
|
@ -51,7 +51,7 @@
|
|||
errors = [...errors, { message: "Role is required" }]
|
||||
}
|
||||
if (errors.length) {
|
||||
return false
|
||||
return keepOpen
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -79,8 +79,8 @@
|
|||
} else {
|
||||
notifications.error("Error saving user")
|
||||
}
|
||||
// Prevent closing the modal on errors
|
||||
return false
|
||||
|
||||
return keepOpen
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { ModalContent, Select, Input, Button } from "@budibase/bbui"
|
||||
import { keepOpen, ModalContent, Select, Input, Button } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import { API } from "api"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
@ -12,15 +12,14 @@
|
|||
let selectedRole = BASE_ROLE
|
||||
let errors = []
|
||||
let builtInRoles = ["Admin", "Power", "Basic", "Public"]
|
||||
let validRegex = /^[a-zA-Z0-9_]*$/
|
||||
// Don't allow editing of public role
|
||||
$: editableRoles = $roles.filter(role => role._id !== "PUBLIC")
|
||||
$: selectedRoleId = selectedRole._id
|
||||
$: otherRoles = editableRoles.filter(role => role._id !== selectedRoleId)
|
||||
$: isCreating = selectedRoleId == null || selectedRoleId === ""
|
||||
|
||||
$: hasUniqueRoleName = !otherRoles
|
||||
?.map(role => role.name)
|
||||
?.includes(selectedRole.name)
|
||||
$: roleNameError = getRoleNameError(selectedRole.name)
|
||||
|
||||
$: valid =
|
||||
selectedRole.name &&
|
||||
|
@ -77,7 +76,7 @@
|
|||
errors.push({ message: "Please choose permissions" })
|
||||
}
|
||||
if (errors.length) {
|
||||
return false
|
||||
return keepOpen
|
||||
}
|
||||
|
||||
// Save/create the role
|
||||
|
@ -85,8 +84,8 @@
|
|||
await roles.save(selectedRole)
|
||||
notifications.success("Role saved successfully")
|
||||
} catch (error) {
|
||||
notifications.error("Error saving role")
|
||||
return false
|
||||
notifications.error(`Error saving role - ${error.message}`)
|
||||
return keepOpen
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,7 +96,20 @@
|
|||
changeRole()
|
||||
notifications.success("Role deleted successfully")
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting role")
|
||||
notifications.error(`Error deleting role - ${error.message}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const getRoleNameError = name => {
|
||||
const hasUniqueRoleName = !otherRoles
|
||||
?.map(role => role.name)
|
||||
?.includes(name)
|
||||
const invalidRoleName = !validRegex.test(name)
|
||||
if (!hasUniqueRoleName) {
|
||||
return "Select a unique role name."
|
||||
} else if (invalidRoleName) {
|
||||
return "Please enter a role name consisting of only alphanumeric symbols and underscores"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,7 +120,7 @@
|
|||
title="Edit Roles"
|
||||
confirmText={isCreating ? "Create" : "Save"}
|
||||
onConfirm={saveRole}
|
||||
disabled={!valid || !hasUniqueRoleName}
|
||||
disabled={!valid || roleNameError}
|
||||
>
|
||||
{#if errors.length}
|
||||
<ErrorsBox {errors} />
|
||||
|
@ -128,8 +140,8 @@
|
|||
<Input
|
||||
label="Name"
|
||||
bind:value={selectedRole.name}
|
||||
disabled={shouldDisableRoleInput}
|
||||
error={!hasUniqueRoleName ? "Select a unique role name." : null}
|
||||
disabled={!!selectedRoleId}
|
||||
error={roleNameError}
|
||||
/>
|
||||
<Select
|
||||
label="Inherits Role"
|
||||
|
|
|
@ -92,13 +92,20 @@
|
|||
},
|
||||
}
|
||||
|
||||
function downloadWithBlob(data, filename) {
|
||||
download(new Blob([data], { type: "text/plain" }), filename)
|
||||
}
|
||||
|
||||
async function exportView() {
|
||||
try {
|
||||
const data = await API.exportView({
|
||||
viewName: view,
|
||||
format: exportFormat,
|
||||
})
|
||||
download(data, `export.${exportFormat === "csv" ? "csv" : "json"}`)
|
||||
downloadWithBlob(
|
||||
data,
|
||||
`export.${exportFormat === "csv" ? "csv" : "json"}`
|
||||
)
|
||||
} catch (error) {
|
||||
notifications.error(`Unable to export ${exportFormat.toUpperCase()} data`)
|
||||
}
|
||||
|
@ -111,7 +118,7 @@
|
|||
rows: selectedRows.map(row => row._id),
|
||||
format: exportFormat,
|
||||
})
|
||||
download(data, `export.${exportFormat}`)
|
||||
downloadWithBlob(data, `export.${exportFormat}`)
|
||||
} else if (filters || sorting) {
|
||||
let response
|
||||
try {
|
||||
|
@ -130,7 +137,7 @@
|
|||
notifications.error("Export Failed")
|
||||
}
|
||||
if (response) {
|
||||
download(response, `export.${exportFormat}`)
|
||||
downloadWithBlob(response, `export.${exportFormat}`)
|
||||
notifications.success("Export Successful")
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
export let tableId
|
||||
export let tableType
|
||||
|
||||
let rows = []
|
||||
let allValid = false
|
||||
let displayColumn = null
|
||||
|
|
|
@ -12,8 +12,11 @@
|
|||
customQueryText,
|
||||
} from "helpers/data/utils"
|
||||
import IntegrationIcon from "./IntegrationIcon.svelte"
|
||||
import { TableNames } from "constants"
|
||||
import { userSelectedResourceMap } from "builderStore"
|
||||
|
||||
let openDataSources = []
|
||||
|
||||
$: enrichedDataSources = enrichDatasources(
|
||||
$datasources,
|
||||
$params,
|
||||
|
@ -71,6 +74,13 @@
|
|||
$goto(`./datasource/${datasource._id}`)
|
||||
}
|
||||
|
||||
const selectTable = tableId => {
|
||||
tables.select(tableId)
|
||||
if (!$isActive("./table/:tableId")) {
|
||||
$goto(`./table/${tableId}`)
|
||||
}
|
||||
}
|
||||
|
||||
function closeNode(datasource) {
|
||||
openDataSources = openDataSources.filter(id => datasource._id !== id)
|
||||
}
|
||||
|
@ -151,15 +161,24 @@
|
|||
|
||||
{#if $database?._id}
|
||||
<div class="hierarchy-items-container">
|
||||
{#each enrichedDataSources as datasource, idx}
|
||||
<NavItem
|
||||
icon="UserGroup"
|
||||
text="Users"
|
||||
selected={$isActive("./table/:tableId") &&
|
||||
$tables.selected?._id === TableNames.USERS}
|
||||
on:click={() => selectTable(TableNames.USERS)}
|
||||
selectedBy={$userSelectedResourceMap[TableNames.USERS]}
|
||||
/>
|
||||
{#each enrichedDataSources as datasource}
|
||||
<NavItem
|
||||
border={idx > 0}
|
||||
border
|
||||
text={datasource.name}
|
||||
opened={datasource.open}
|
||||
selected={$isActive("./datasource") && datasource.selected}
|
||||
withArrow={true}
|
||||
on:click={() => selectDatasource(datasource)}
|
||||
on:iconClick={() => toggleNode(datasource)}
|
||||
selectedBy={$userSelectedResourceMap[datasource._id]}
|
||||
>
|
||||
<div class="datasource-icon" slot="icon">
|
||||
<IntegrationIcon
|
||||
|
@ -174,7 +193,7 @@
|
|||
</NavItem>
|
||||
|
||||
{#if datasource.open}
|
||||
<TableNavigator sourceId={datasource._id} />
|
||||
<TableNavigator sourceId={datasource._id} {selectTable} />
|
||||
{#each $queries.list.filter(query => query.datasourceId === datasource._id) as query}
|
||||
<NavItem
|
||||
indentLevel={1}
|
||||
|
@ -185,6 +204,7 @@
|
|||
selected={$isActive("./query/:queryId") &&
|
||||
$queries.selectedQueryId === query._id}
|
||||
on:click={() => $goto(`./query/${query._id}`)}
|
||||
selectedBy={$userSelectedResourceMap[query._id]}
|
||||
>
|
||||
<EditQueryPopover {query} />
|
||||
</NavItem>
|
||||
|
@ -196,7 +216,7 @@
|
|||
|
||||
<style>
|
||||
.hierarchy-items-container {
|
||||
margin: 0 calc(-1 * var(--spacing-xl));
|
||||
margin: 0 calc(-1 * var(--spacing-l));
|
||||
}
|
||||
.datasource-icon {
|
||||
display: grid;
|
||||
|
|
|
@ -1,219 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Label,
|
||||
Input,
|
||||
Layout,
|
||||
Toggle,
|
||||
Button,
|
||||
TextArea,
|
||||
Modal,
|
||||
EnvDropdown,
|
||||
Accordion,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
import { IntegrationTypes } from "constants/backend"
|
||||
import { createValidationStore } from "helpers/validation/yup"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import { environment, licensing, auth } from "stores/portal"
|
||||
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
|
||||
|
||||
export let datasource
|
||||
export let schema
|
||||
export let creating
|
||||
|
||||
let createVariableModal
|
||||
let selectedKey
|
||||
|
||||
const validation = createValidationStore()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function filter([key, value]) {
|
||||
if (!value) {
|
||||
return false
|
||||
}
|
||||
return !(
|
||||
(datasource.source === IntegrationTypes.REST &&
|
||||
key === "defaultHeaders") ||
|
||||
value.deprecated
|
||||
)
|
||||
}
|
||||
|
||||
$: config = datasource?.config
|
||||
$: configKeys = Object.entries(schema || {})
|
||||
.filter(el => filter(el))
|
||||
.map(([key]) => key)
|
||||
|
||||
// setup the validation for each required field
|
||||
$: configKeys.forEach(key => {
|
||||
if (schema[key].required) {
|
||||
validation.addValidatorType(key, schema[key].type, schema[key].required)
|
||||
}
|
||||
})
|
||||
// run the validation whenever the config changes
|
||||
$: validation.check(config)
|
||||
// dispatch the validation result
|
||||
$: dispatch(
|
||||
"valid",
|
||||
Object.values($validation.errors).filter(val => val != null).length === 0
|
||||
)
|
||||
|
||||
let addButton
|
||||
|
||||
function getDisplayName(key, fieldKey) {
|
||||
let name
|
||||
if (fieldKey && schema[key]["fields"][fieldKey]?.display) {
|
||||
name = schema[key]["fields"][fieldKey].display
|
||||
} else if (fieldKey) {
|
||||
name = fieldKey
|
||||
} else if (schema[key]?.display) {
|
||||
name = schema[key].display
|
||||
} else {
|
||||
name = key
|
||||
}
|
||||
return capitalise(name)
|
||||
}
|
||||
|
||||
function getDisplayError(error, configKey) {
|
||||
return error?.replace(
|
||||
new RegExp(`${configKey}`, "i"),
|
||||
getDisplayName(configKey)
|
||||
)
|
||||
}
|
||||
|
||||
function getFieldGroupKeys(fieldGroup) {
|
||||
return Object.entries(schema[fieldGroup].fields || {})
|
||||
.filter(el => filter(el))
|
||||
.map(([key]) => key)
|
||||
}
|
||||
|
||||
async function save(data) {
|
||||
try {
|
||||
await environment.createVariable(data)
|
||||
config[selectedKey] = `{{ env.${data.name} }}`
|
||||
createVariableModal.hide()
|
||||
} catch (err) {
|
||||
notifications.error(`Failed to create variable: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
function showModal(configKey) {
|
||||
selectedKey = configKey
|
||||
createVariableModal.show()
|
||||
}
|
||||
|
||||
async function handleUpgradePanel() {
|
||||
await environment.upgradePanelOpened()
|
||||
$licensing.goToUpgradePage()
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await environment.loadVariables()
|
||||
if ($auth.user) {
|
||||
await licensing.init()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<form>
|
||||
<Layout noPadding gap="S">
|
||||
{#if !creating}
|
||||
<div class="form-row">
|
||||
<Label>Name</Label>
|
||||
<Input on:change bind:value={datasource.name} />
|
||||
</div>
|
||||
{/if}
|
||||
{#each configKeys as configKey}
|
||||
{#if schema[configKey].type === "object"}
|
||||
<div class="form-row ssl">
|
||||
<Label>{getDisplayName(configKey)}</Label>
|
||||
<Button secondary thin outline on:click={addButton.addEntry()}
|
||||
>Add</Button
|
||||
>
|
||||
</div>
|
||||
<KeyValueBuilder
|
||||
bind:this={addButton}
|
||||
defaults={schema[configKey].default}
|
||||
bind:object={config[configKey]}
|
||||
on:change
|
||||
noAddButton={true}
|
||||
/>
|
||||
{:else if schema[configKey].type === "boolean"}
|
||||
<div class="form-row">
|
||||
<Label>{getDisplayName(configKey)}</Label>
|
||||
<Toggle text="" bind:value={config[configKey]} />
|
||||
</div>
|
||||
{:else if schema[configKey].type === "longForm"}
|
||||
<div class="form-row">
|
||||
<Label>{getDisplayName(configKey)}</Label>
|
||||
<TextArea
|
||||
type={schema[configKey].type}
|
||||
on:change
|
||||
bind:value={config[configKey]}
|
||||
error={getDisplayError($validation.errors[configKey], configKey)}
|
||||
/>
|
||||
</div>
|
||||
{:else if schema[configKey].type === "fieldGroup"}
|
||||
<Accordion
|
||||
itemName={configKey}
|
||||
initialOpen={getFieldGroupKeys(configKey).some(
|
||||
fieldKey => !!config[fieldKey]
|
||||
)}
|
||||
header={getDisplayName(configKey)}
|
||||
>
|
||||
<Layout gap="S">
|
||||
{#each getFieldGroupKeys(configKey) as fieldKey}
|
||||
<div class="form-row">
|
||||
<Label>{getDisplayName(configKey, fieldKey)}</Label>
|
||||
<Input
|
||||
type={schema[configKey]["fields"][fieldKey]?.type}
|
||||
on:change
|
||||
bind:value={config[fieldKey]}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
</Accordion>
|
||||
{:else}
|
||||
<div class="form-row">
|
||||
<Label>{getDisplayName(configKey)}</Label>
|
||||
<EnvDropdown
|
||||
showModal={() => showModal(configKey)}
|
||||
variables={$environment.variables}
|
||||
type={configKey === "port" ? "string" : schema[configKey].type}
|
||||
on:change
|
||||
bind:value={config[configKey]}
|
||||
error={getDisplayError($validation.errors[configKey], configKey)}
|
||||
environmentVariablesEnabled={$licensing.environmentVariablesEnabled}
|
||||
{handleUpgradePanel}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</Layout>
|
||||
</form>
|
||||
|
||||
<Modal bind:this={createVariableModal}>
|
||||
<CreateEditVariableModal {save} />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-row.ssl {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 20%;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -1,249 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Heading,
|
||||
Body,
|
||||
Divider,
|
||||
InlineAlert,
|
||||
Button,
|
||||
notifications,
|
||||
Modal,
|
||||
Table,
|
||||
Toggle,
|
||||
} from "@budibase/bbui"
|
||||
import { datasources, integrations, tables } from "stores/backend"
|
||||
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
|
||||
import CreateExternalTableModal from "./CreateExternalTableModal.svelte"
|
||||
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import ValuesList from "components/common/ValuesList.svelte"
|
||||
|
||||
export let datasource
|
||||
export let save
|
||||
|
||||
let tableSchema = {
|
||||
name: {},
|
||||
primary: { displayName: "Primary Key" },
|
||||
}
|
||||
let relationshipSchema = {
|
||||
tables: {},
|
||||
columns: {},
|
||||
}
|
||||
let relationshipModal
|
||||
let createExternalTableModal
|
||||
let selectedFromRelationship, selectedToRelationship
|
||||
let confirmDialog
|
||||
let specificTables = null
|
||||
let requireSpecificTables = false
|
||||
|
||||
$: integration = datasource && $integrations[datasource.source]
|
||||
$: plusTables = datasource?.plus
|
||||
? Object.values(datasource?.entities || {})
|
||||
: []
|
||||
$: relationships = getRelationships(plusTables)
|
||||
$: schemaError = $datasources.schemaError
|
||||
$: relationshipInfo = relationshipTableData(relationships)
|
||||
|
||||
function getRelationships(tables) {
|
||||
if (!tables || !Array.isArray(tables)) {
|
||||
return {}
|
||||
}
|
||||
let pairs = {}
|
||||
for (let table of tables) {
|
||||
for (let column of Object.values(table.schema)) {
|
||||
if (column.type !== "link") {
|
||||
continue
|
||||
}
|
||||
// these relationships have an id to pair them to each other
|
||||
// one has a main for the from side
|
||||
const key = column.main ? "from" : "to"
|
||||
pairs[column._id] = {
|
||||
...pairs[column._id],
|
||||
[key]: column,
|
||||
}
|
||||
}
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
function buildRelationshipDisplayString(fromCol, toCol) {
|
||||
function getTableName(tableId) {
|
||||
if (!tableId || typeof tableId !== "string") {
|
||||
return null
|
||||
}
|
||||
return plusTables.find(table => table._id === tableId)?.name || "Unknown"
|
||||
}
|
||||
if (!toCol || !fromCol) {
|
||||
return "Cannot build name"
|
||||
}
|
||||
const fromTableName = getTableName(toCol.tableId)
|
||||
const toTableName = getTableName(fromCol.tableId)
|
||||
const throughTableName = getTableName(fromCol.through)
|
||||
|
||||
let displayString
|
||||
if (throughTableName) {
|
||||
displayString = `${fromTableName} ↔ ${toTableName}`
|
||||
} else {
|
||||
displayString = `${fromTableName} → ${toTableName}`
|
||||
}
|
||||
return displayString
|
||||
}
|
||||
|
||||
async function updateDatasourceSchema() {
|
||||
try {
|
||||
await datasources.updateSchema(datasource, specificTables)
|
||||
notifications.success(`Datasource ${name} tables updated successfully.`)
|
||||
await tables.fetch()
|
||||
} catch (error) {
|
||||
notifications.error(
|
||||
`Error updating datasource schema ${
|
||||
error?.message ? `: ${error.message}` : ""
|
||||
}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function onClickTable(table) {
|
||||
$goto(`../../table/${table._id}`)
|
||||
}
|
||||
|
||||
function openRelationshipModal(fromRelationship, toRelationship) {
|
||||
selectedFromRelationship = fromRelationship || {}
|
||||
selectedToRelationship = toRelationship || {}
|
||||
relationshipModal.show()
|
||||
}
|
||||
|
||||
function createNewTable() {
|
||||
createExternalTableModal.show()
|
||||
}
|
||||
|
||||
function relationshipTableData(relations) {
|
||||
return Object.values(relations).map(relationship => ({
|
||||
tables: buildRelationshipDisplayString(
|
||||
relationship.from,
|
||||
relationship.to
|
||||
),
|
||||
columns: `${relationship.from?.name} to ${relationship.to?.name}`,
|
||||
from: relationship.from,
|
||||
to: relationship.to,
|
||||
}))
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={relationshipModal}>
|
||||
<CreateEditRelationship
|
||||
{datasource}
|
||||
{save}
|
||||
close={relationshipModal.hide}
|
||||
{plusTables}
|
||||
fromRelationship={selectedFromRelationship}
|
||||
toRelationship={selectedToRelationship}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={createExternalTableModal}>
|
||||
<CreateExternalTableModal {datasource} />
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDialog}
|
||||
okText="Fetch tables"
|
||||
onOk={updateDatasourceSchema}
|
||||
onCancel={() => confirmDialog.hide()}
|
||||
warning={false}
|
||||
title="Confirm table fetch"
|
||||
>
|
||||
<Toggle
|
||||
bind:value={requireSpecificTables}
|
||||
on:change={e => {
|
||||
requireSpecificTables = e.detail
|
||||
specificTables = null
|
||||
}}
|
||||
thin
|
||||
text="Fetch listed tables only (one per line)"
|
||||
/>
|
||||
{#if requireSpecificTables}
|
||||
<ValuesList label="" bind:values={specificTables} />
|
||||
{/if}
|
||||
<br />
|
||||
<Body>
|
||||
If you have fetched tables from this database before, this action may
|
||||
overwrite any changes you made after your initial fetch.
|
||||
</Body>
|
||||
</ConfirmDialog>
|
||||
|
||||
<Divider />
|
||||
<div class="query-header">
|
||||
<Heading size="S">Tables</Heading>
|
||||
<div class="table-buttons">
|
||||
<Button secondary on:click={() => confirmDialog.show()}>
|
||||
Fetch tables
|
||||
</Button>
|
||||
<Button cta icon="Add" on:click={createNewTable}>New table</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Body>
|
||||
This datasource can determine tables automatically. Budibase can fetch your
|
||||
tables directly from the database and you can use them without having to write
|
||||
any queries at all.
|
||||
</Body>
|
||||
{#if schemaError}
|
||||
<InlineAlert
|
||||
type="error"
|
||||
header="Error fetching tables"
|
||||
message={schemaError}
|
||||
onConfirm={datasources.removeSchemaError}
|
||||
/>
|
||||
{/if}
|
||||
{#if plusTables && Object.values(plusTables).length > 0}
|
||||
<Table
|
||||
on:click={({ detail }) => onClickTable(detail)}
|
||||
schema={tableSchema}
|
||||
data={Object.values(plusTables)}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={false}
|
||||
customRenderers={[{ column: "primary", component: ArrayRenderer }]}
|
||||
/>
|
||||
{:else}
|
||||
<Body size="S"><i>No tables found.</i></Body>
|
||||
{/if}
|
||||
{#if integration.relationships !== false}
|
||||
<Divider />
|
||||
<div class="query-header">
|
||||
<Heading size="S">Relationships</Heading>
|
||||
<Button primary on:click={() => openRelationshipModal()}>
|
||||
Define relationship
|
||||
</Button>
|
||||
</div>
|
||||
<Body>
|
||||
Tell budibase how your tables are related to get even more smart features.
|
||||
</Body>
|
||||
{#if relationshipInfo && relationshipInfo.length > 0}
|
||||
<Table
|
||||
on:click={({ detail }) => openRelationshipModal(detail.from, detail.to)}
|
||||
schema={relationshipSchema}
|
||||
data={relationshipInfo}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={false}
|
||||
/>
|
||||
{:else}
|
||||
<Body size="S"><i>No relationships configured.</i></Body>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.query-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0 0 var(--spacing-s) 0;
|
||||
}
|
||||
|
||||
.table-buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
</style>
|
|
@ -1,86 +0,0 @@
|
|||
<script>
|
||||
import { Body, notifications } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import { API } from "api"
|
||||
import ICONS from "../icons"
|
||||
|
||||
export let integration = {}
|
||||
let integrations = []
|
||||
const INTERNAL = "BUDIBASE"
|
||||
|
||||
async function fetchIntegrations() {
|
||||
let otherIntegrations
|
||||
try {
|
||||
otherIntegrations = await API.getIntegrations()
|
||||
} catch (error) {
|
||||
otherIntegrations = {}
|
||||
notifications.error("Error getting integrations")
|
||||
}
|
||||
integrations = {
|
||||
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
||||
...otherIntegrations,
|
||||
}
|
||||
}
|
||||
|
||||
function selectIntegration(integrationType) {
|
||||
const selected = integrations[integrationType]
|
||||
|
||||
// build the schema
|
||||
const schema = {}
|
||||
for (let key of Object.keys(selected.datasource)) {
|
||||
schema[key] = selected.datasource[key].default
|
||||
}
|
||||
|
||||
integration = {
|
||||
type: integrationType,
|
||||
plus: selected.plus,
|
||||
...schema,
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchIntegrations()
|
||||
})
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<div class="integration-list">
|
||||
{#each Object.entries(integrations) as [integrationType, schema]}
|
||||
<div
|
||||
class="integration hoverable"
|
||||
class:selected={integration.type === integrationType}
|
||||
on:click={() => selectIntegration(integrationType)}
|
||||
>
|
||||
<svelte:component
|
||||
this={ICONS[integrationType]}
|
||||
height="50"
|
||||
width="50"
|
||||
/>
|
||||
<Body size="XS">{schema.name || integrationType}</Body>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.integration-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
grid-gap: var(--spectrum-alias-grid-baseline);
|
||||
}
|
||||
|
||||
.integration {
|
||||
display: grid;
|
||||
background: var(--background-alt);
|
||||
place-items: center;
|
||||
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
||||
padding: var(--spectrum-alias-item-padding-s);
|
||||
transition: 0.3s all;
|
||||
border-radius: var(--spectrum-alias-item-rounded-border-radius-s);
|
||||
}
|
||||
|
||||
.integration:hover,
|
||||
.selected {
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
}
|
||||
</style>
|
|
@ -1,123 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Divider,
|
||||
Heading,
|
||||
ActionButton,
|
||||
Badge,
|
||||
Body,
|
||||
Layout,
|
||||
} from "@budibase/bbui"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
import RestAuthenticationBuilder from "./auth/RestAuthenticationBuilder.svelte"
|
||||
import ViewDynamicVariables from "./variables/ViewDynamicVariables.svelte"
|
||||
import {
|
||||
getRestBindings,
|
||||
getEnvironmentBindings,
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableMap,
|
||||
} from "builderStore/dataBinding"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { licensing } from "stores/portal"
|
||||
|
||||
export let datasource
|
||||
export let queries
|
||||
|
||||
let addHeader
|
||||
|
||||
let parsedHeaders = runtimeToReadableMap(
|
||||
getRestBindings(),
|
||||
cloneDeep(datasource?.config?.defaultHeaders)
|
||||
)
|
||||
|
||||
const onDefaultHeaderUpdate = headers => {
|
||||
const flatHeaders = cloneDeep(headers).reduce((acc, entry) => {
|
||||
acc[entry.name] = readableToRuntimeBinding(getRestBindings(), entry.value)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
datasource.config.defaultHeaders = flatHeaders
|
||||
}
|
||||
</script>
|
||||
|
||||
<Divider />
|
||||
<div class="section-header">
|
||||
<div class="badge">
|
||||
<Heading size="S">Headers</Heading>
|
||||
<Badge quiet grey>Optional</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Body size="S">
|
||||
Headers enable you to provide additional information about the request, such
|
||||
as format.
|
||||
</Body>
|
||||
<KeyValueBuilder
|
||||
bind:this={addHeader}
|
||||
bind:object={parsedHeaders}
|
||||
on:change={evt => onDefaultHeaderUpdate(evt.detail)}
|
||||
noAddButton
|
||||
bindings={getRestBindings()}
|
||||
/>
|
||||
<div>
|
||||
<ActionButton icon="Add" on:click={() => addHeader.addEntry()}>
|
||||
Add header
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
<div class="section-header">
|
||||
<div class="badge">
|
||||
<Heading size="S">Authentication</Heading>
|
||||
<Badge quiet grey>Optional</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Body size="S">
|
||||
Create an authentication config that can be shared with queries.
|
||||
</Body>
|
||||
<RestAuthenticationBuilder bind:configs={datasource.config.authConfigs} />
|
||||
|
||||
<Divider />
|
||||
<div class="section-header">
|
||||
<div class="badge">
|
||||
<Heading size="S">Variables</Heading>
|
||||
<Badge quiet grey>Optional</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<Body size="S"
|
||||
>Variables enable you to store and re-use values in queries, with the choice
|
||||
of a static value such as a token using static variables, or a value from a
|
||||
query response using dynamic variables.</Body
|
||||
>
|
||||
<Heading size="XS">Static</Heading>
|
||||
<Layout noPadding gap="XS">
|
||||
<KeyValueBuilder
|
||||
name="Variable"
|
||||
keyPlaceholder="Name"
|
||||
headings
|
||||
bind:object={datasource.config.staticVariables}
|
||||
on:change
|
||||
bindings={$licensing.environmentVariablesEnabled
|
||||
? getEnvironmentBindings()
|
||||
: []}
|
||||
/>
|
||||
</Layout>
|
||||
<div />
|
||||
<Heading size="XS">Dynamic</Heading>
|
||||
<Body size="S">
|
||||
Dynamic variables are evaluated when a dependant query is executed. The value
|
||||
is cached for a period of time and will be refreshed if a query fails.
|
||||
</Body>
|
||||
<ViewDynamicVariables {queries} {datasource} />
|
||||
|
||||
<style>
|
||||
.section-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
</style>
|
|
@ -1,60 +0,0 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { Heading, Detail } from "@budibase/bbui"
|
||||
import IntegrationIcon from "../IntegrationIcon.svelte"
|
||||
|
||||
export let integration
|
||||
export let integrationType
|
||||
export let schema
|
||||
|
||||
let dispatcher = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div
|
||||
class:selected={integration.type === integrationType}
|
||||
on:click={() => dispatcher("selected", integrationType)}
|
||||
class="item hoverable"
|
||||
>
|
||||
<div class="item-body" class:with-type={!!schema.type}>
|
||||
<IntegrationIcon {integrationType} {schema} size="25" />
|
||||
<div class="text">
|
||||
<Heading size="XXS">{schema.friendlyName}</Heading>
|
||||
{#if schema.type}
|
||||
<Detail size="S">{schema.type || ""}</Detail>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.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;
|
||||
}
|
||||
</style>
|
|
@ -1,145 +0,0 @@
|
|||
<script>
|
||||
export let width = 100
|
||||
export let height = 100
|
||||
</script>
|
||||
|
||||
<svg
|
||||
{width}
|
||||
{height}
|
||||
viewBox="0 0 46 46"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
|
||||
>
|
||||
<!-- Generator: Sketch 3.3.3 (12081) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>btn_google_dark_normal_ios</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<filter
|
||||
x="-50%"
|
||||
y="-50%"
|
||||
width="200%"
|
||||
height="200%"
|
||||
filterUnits="objectBoundingBox"
|
||||
id="filter-1"
|
||||
>
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="0.5"
|
||||
in="shadowOffsetOuter1"
|
||||
result="shadowBlurOuter1"
|
||||
/>
|
||||
<feColorMatrix
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.168 0"
|
||||
in="shadowBlurOuter1"
|
||||
type="matrix"
|
||||
result="shadowMatrixOuter1"
|
||||
/>
|
||||
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="0.5"
|
||||
in="shadowOffsetOuter2"
|
||||
result="shadowBlurOuter2"
|
||||
/>
|
||||
<feColorMatrix
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.084 0"
|
||||
in="shadowBlurOuter2"
|
||||
type="matrix"
|
||||
result="shadowMatrixOuter2"
|
||||
/>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1" />
|
||||
<feMergeNode in="shadowMatrixOuter2" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<rect id="path-2" x="0" y="0" width="40" height="40" rx="2" />
|
||||
<rect id="path-3" x="5" y="5" width="38" height="38" rx="1" />
|
||||
</defs>
|
||||
<g
|
||||
id="Google-Button"
|
||||
stroke="none"
|
||||
stroke-width="1"
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
sketch:type="MSPage"
|
||||
>
|
||||
<g
|
||||
id="9-PATCH"
|
||||
sketch:type="MSArtboardGroup"
|
||||
transform="translate(-608.000000, -219.000000)"
|
||||
/>
|
||||
<g
|
||||
id="btn_google_dark_normal"
|
||||
sketch:type="MSArtboardGroup"
|
||||
transform="translate(-1.000000, -1.000000)"
|
||||
>
|
||||
<g
|
||||
id="button"
|
||||
sketch:type="MSLayerGroup"
|
||||
transform="translate(4.000000, 4.000000)"
|
||||
filter="url(#filter-1)"
|
||||
>
|
||||
<g id="button-bg">
|
||||
<use
|
||||
fill="#4285F4"
|
||||
fill-rule="evenodd"
|
||||
sketch:type="MSShapeGroup"
|
||||
xlink:href="#path-2"
|
||||
/>
|
||||
<use fill="none" xlink:href="#path-2" />
|
||||
<use fill="none" xlink:href="#path-2" />
|
||||
<use fill="none" xlink:href="#path-2" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="button-bg-copy">
|
||||
<use
|
||||
fill="#FFFFFF"
|
||||
fill-rule="evenodd"
|
||||
sketch:type="MSShapeGroup"
|
||||
xlink:href="#path-3"
|
||||
/>
|
||||
<use fill="none" xlink:href="#path-3" />
|
||||
<use fill="none" xlink:href="#path-3" />
|
||||
<use fill="none" xlink:href="#path-3" />
|
||||
</g>
|
||||
<g
|
||||
id="logo_googleg_48dp"
|
||||
sketch:type="MSLayerGroup"
|
||||
transform="translate(15.000000, 15.000000)"
|
||||
>
|
||||
<path
|
||||
d="M17.64,9.20454545 C17.64,8.56636364 17.5827273,7.95272727 17.4763636,7.36363636 L9,7.36363636 L9,10.845 L13.8436364,10.845 C13.635,11.97 13.0009091,12.9231818 12.0477273,13.5613636 L12.0477273,15.8195455 L14.9563636,15.8195455 C16.6581818,14.2527273 17.64,11.9454545 17.64,9.20454545 L17.64,9.20454545 Z"
|
||||
id="Shape"
|
||||
fill="#4285F4"
|
||||
sketch:type="MSShapeGroup"
|
||||
/>
|
||||
<path
|
||||
d="M9,18 C11.43,18 13.4672727,17.1940909 14.9563636,15.8195455 L12.0477273,13.5613636 C11.2418182,14.1013636 10.2109091,14.4204545 9,14.4204545 C6.65590909,14.4204545 4.67181818,12.8372727 3.96409091,10.71 L0.957272727,10.71 L0.957272727,13.0418182 C2.43818182,15.9831818 5.48181818,18 9,18 L9,18 Z"
|
||||
id="Shape"
|
||||
fill="#34A853"
|
||||
sketch:type="MSShapeGroup"
|
||||
/>
|
||||
<path
|
||||
d="M3.96409091,10.71 C3.78409091,10.17 3.68181818,9.59318182 3.68181818,9 C3.68181818,8.40681818 3.78409091,7.83 3.96409091,7.29 L3.96409091,4.95818182 L0.957272727,4.95818182 C0.347727273,6.17318182 0,7.54772727 0,9 C0,10.4522727 0.347727273,11.8268182 0.957272727,13.0418182 L3.96409091,10.71 L3.96409091,10.71 Z"
|
||||
id="Shape"
|
||||
fill="#FBBC05"
|
||||
sketch:type="MSShapeGroup"
|
||||
/>
|
||||
<path
|
||||
d="M9,3.57954545 C10.3213636,3.57954545 11.5077273,4.03363636 12.4404545,4.92545455 L15.0218182,2.34409091 C13.4631818,0.891818182 11.4259091,0 9,0 C5.48181818,0 2.43818182,2.01681818 0.957272727,4.95818182 L3.96409091,7.29 C4.67181818,5.16272727 6.65590909,3.57954545 9,3.57954545 L9,3.57954545 Z"
|
||||
id="Shape"
|
||||
fill="#EA4335"
|
||||
sketch:type="MSShapeGroup"
|
||||
/>
|
||||
<path
|
||||
d="M0,0 L18,0 L18,18 L0,18 L0,0 Z"
|
||||
id="Shape"
|
||||
sketch:type="MSShapeGroup"
|
||||
/>
|
||||
</g>
|
||||
<g id="handles_square" sketch:type="MSLayerGroup" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
|
@ -31,65 +31,65 @@
|
|||
<path
|
||||
class="st1"
|
||||
d="M-83.19,48h-41.79c-1.76,0-3.19-1.43-3.19-3.19V3.02c0-1.76,1.43-3.19,3.19-3.19h41.79
|
||||
c1.76,0,3.19,1.43,3.19,3.19v41.79C-80,46.57-81.43,48-83.19,48z"
|
||||
c1.76,0,3.19,1.43,3.19,3.19v41.79C-80,46.57-81.43,48-83.19,48z"
|
||||
/>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
class="st0"
|
||||
d="M-99.62,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
|
||||
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
||||
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35h-4.89V12.57H-99.62z
|
||||
M-93.46,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
||||
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
||||
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
||||
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
||||
C-93.55,28.92-93.46,28.52-93.46,28.11z"
|
||||
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
||||
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35h-4.89V12.57H-99.62z
|
||||
M-93.46,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
||||
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
||||
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
||||
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
||||
C-93.55,28.92-93.46,28.52-93.46,28.11z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
class="st0"
|
||||
d="M-114.76,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58
|
||||
c0.86,0.39,1.59,0.91,2.19,1.57c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89
|
||||
c-0.35,0.9-0.84,1.68-1.47,2.35c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35
|
||||
h-4.89V12.57H-114.76z M-108.6,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
||||
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
||||
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
||||
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
||||
C-108.68,28.92-108.6,28.52-108.6,28.11z"
|
||||
c0.86,0.39,1.59,0.91,2.19,1.57c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89
|
||||
c-0.35,0.9-0.84,1.68-1.47,2.35c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35
|
||||
h-4.89V12.57H-114.76z M-108.6,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
||||
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
||||
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
||||
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
||||
C-108.68,28.92-108.6,28.52-108.6,28.11z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
class="st2"
|
||||
d="M44.81,159H3.02c-1.76,0-3.19-1.43-3.19-3.19v-41.79c0-1.76,1.43-3.19,3.19-3.19h41.79
|
||||
c1.76,0,3.19,1.43,3.19,3.19v41.79C48,157.57,46.57,159,44.81,159z"
|
||||
c1.76,0,3.19,1.43,3.19,3.19v41.79C48,157.57,46.57,159,44.81,159z"
|
||||
/>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
class="st1"
|
||||
d="M28.38,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
|
||||
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
||||
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146h-4.89v-22.43H28.38z
|
||||
M34.54,139.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
||||
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
||||
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
||||
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
||||
C34.45,139.92,34.54,139.52,34.54,139.11z"
|
||||
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
||||
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146h-4.89v-22.43H28.38z
|
||||
M34.54,139.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
||||
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
||||
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
||||
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
||||
C34.45,139.92,34.54,139.52,34.54,139.11z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
class="st1"
|
||||
d="M13.24,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
|
||||
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
||||
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146H8.35v-22.43H13.24z M19.4,139.11
|
||||
c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69c-0.38-0.17-0.79-0.26-1.24-0.26
|
||||
c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01c-0.17,0.39-0.26,0.8-0.26,1.23
|
||||
c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68c0.39,0.17,0.8,0.26,1.23,0.26
|
||||
c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1C19.32,139.92,19.4,139.52,19.4,139.11z"
|
||||
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
||||
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146H8.35v-22.43H13.24z M19.4,139.11
|
||||
c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69c-0.38-0.17-0.79-0.26-1.24-0.26
|
||||
c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01c-0.17,0.39-0.26,0.8-0.26,1.23
|
||||
c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68c0.39,0.17,0.8,0.26,1.23,0.26
|
||||
c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1C19.32,139.92,19.4,139.52,19.4,139.11z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
|
@ -102,24 +102,24 @@
|
|||
<path
|
||||
class="st1"
|
||||
d="M28.48,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
|
||||
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
|
||||
c-0.64,0.7-1.4,1.25-2.28,1.66C34.8,35.8,33.86,36,32.84,36c-1.84,0-3.3-0.69-4.37-2.07v1.62h-5V12H28.48z M34.78,28.31
|
||||
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
|
||||
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
|
||||
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
|
||||
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C34.69,29.16,34.78,28.75,34.78,28.31z"
|
||||
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
|
||||
c-0.64,0.7-1.4,1.25-2.28,1.66C34.8,35.8,33.86,36,32.84,36c-1.84,0-3.3-0.69-4.37-2.07v1.62h-5V12H28.48z M34.78,28.31
|
||||
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
|
||||
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
|
||||
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
|
||||
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C34.69,29.16,34.78,28.75,34.78,28.31z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
class="st1"
|
||||
d="M13,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
|
||||
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
|
||||
c-0.64,0.7-1.4,1.25-2.28,1.66C19.32,35.8,18.38,36,17.37,36c-1.84,0-3.3-0.69-4.37-2.07v1.62H8V12H13z M19.3,28.31
|
||||
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
|
||||
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
|
||||
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
|
||||
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C19.21,29.16,19.3,28.75,19.3,28.31z"
|
||||
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
|
||||
c-0.64,0.7-1.4,1.25-2.28,1.66C19.32,35.8,18.38,36,17.37,36c-1.84,0-3.3-0.69-4.37-2.07v1.62H8V12H13z M19.3,28.31
|
||||
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
|
||||
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
|
||||
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
|
||||
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C19.21,29.16,19.3,28.75,19.3,28.31z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
|
|
|
@ -44,6 +44,9 @@ export default ICONS
|
|||
|
||||
export function getIcon(integrationType, schema) {
|
||||
const integrationList = get(integrations)
|
||||
if (!integrationList) {
|
||||
return
|
||||
}
|
||||
if (integrationList[integrationType]?.iconUrl) {
|
||||
return { url: integrationList[integrationType].iconUrl }
|
||||
} else if (schema?.custom || !ICONS[integrationType]) {
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { ModalContent, notifications, Body, Layout } from "@budibase/bbui"
|
||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||
import { IntegrationNames } from "constants/backend"
|
||||
import cloneDeep from "lodash/cloneDeepWith"
|
||||
import {
|
||||
saveDatasource as save,
|
||||
validateDatasourceConfig,
|
||||
} from "builderStore/datasource"
|
||||
import { DatasourceFeature } from "@budibase/types"
|
||||
|
||||
export let integration
|
||||
|
||||
// kill the reference so the input isn't saved
|
||||
let datasource = cloneDeep(integration)
|
||||
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)
|
||||
$goto(`./datasource/${resp._id}`)
|
||||
notifications.success(`Datasource created successfully.`)
|
||||
} catch (err) {
|
||||
notifications.error(err?.message ?? "Error saving datasource")
|
||||
// prevent the modal from closing
|
||||
return false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={`Connect to ${name}`}
|
||||
onConfirm={() => saveDatasource()}
|
||||
confirmText={datasource.plus ? "Connect" : "Save and continue to query"}
|
||||
cancelText="Back"
|
||||
showSecondaryButton={datasource.plus}
|
||||
size="L"
|
||||
disabled={!isValid}
|
||||
>
|
||||
<Layout noPadding>
|
||||
<Body size="XS"
|
||||
>Connect your database to Budibase using the config below.
|
||||
</Body>
|
||||
</Layout>
|
||||
<IntegrationConfigForm
|
||||
schema={datasource.schema}
|
||||
bind:datasource
|
||||
creating={true}
|
||||
on:valid={e => (isValid = e.detail)}
|
||||
/>
|
||||
</ModalContent>
|
|
@ -1,110 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
ModalContent,
|
||||
Body,
|
||||
Layout,
|
||||
Link,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { IntegrationNames, IntegrationTypes } from "constants/backend"
|
||||
import GoogleButton from "../_components/GoogleButton.svelte"
|
||||
import { organisation } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
import { validateDatasourceConfig } from "builderStore/datasource"
|
||||
import cloneDeep from "lodash/cloneDeepWith"
|
||||
import IntegrationConfigForm from "../TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
|
||||
import { saveDatasource } from "builderStore/datasource"
|
||||
import { DatasourceFeature } from "@budibase/types"
|
||||
|
||||
export let integration
|
||||
export let continueSetupId = false
|
||||
|
||||
let datasource = cloneDeep(integration)
|
||||
datasource.config.continueSetupId = continueSetupId
|
||||
|
||||
$: isGoogleConfigured = !!$organisation.googleDatasourceConfigured
|
||||
|
||||
onMount(async () => {
|
||||
await organisation.init()
|
||||
})
|
||||
const integrationName = IntegrationNames[IntegrationTypes.GOOGLE_SHEETS]
|
||||
|
||||
export const GoogleDatasouceConfigStep = {
|
||||
AUTH: "Auth",
|
||||
SET_URL: "Set_url",
|
||||
}
|
||||
|
||||
let step = continueSetupId
|
||||
? GoogleDatasouceConfigStep.SET_URL
|
||||
: GoogleDatasouceConfigStep.AUTH
|
||||
|
||||
let isValid = false
|
||||
|
||||
const modalConfig = {
|
||||
[GoogleDatasouceConfigStep.AUTH]: {},
|
||||
[GoogleDatasouceConfigStep.SET_URL]: {
|
||||
confirmButtonText: "Connect",
|
||||
onConfirm: async () => {
|
||||
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||
const resp = await validateDatasourceConfig(datasource)
|
||||
if (!resp.connected) {
|
||||
notifications.error(`Unable to connect - ${resp.error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await saveDatasource(datasource)
|
||||
$goto(`./datasource/${resp._id}`)
|
||||
notifications.success(`Datasource created successfully.`)
|
||||
} catch (err) {
|
||||
notifications.error(err?.message ?? "Error saving datasource")
|
||||
// prevent the modal from closing
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={`Connect to ${integrationName}`}
|
||||
cancelText="Cancel"
|
||||
size="L"
|
||||
confirmText={modalConfig[step].confirmButtonText}
|
||||
showConfirmButton={!!modalConfig[step].onConfirm}
|
||||
onConfirm={modalConfig[step].onConfirm}
|
||||
disabled={!isValid}
|
||||
>
|
||||
{#if step === GoogleDatasouceConfigStep.AUTH}
|
||||
<!-- check true and false directly, don't render until flag is set -->
|
||||
{#if isGoogleConfigured === true}
|
||||
<Layout noPadding>
|
||||
<Body size="S"
|
||||
>Authenticate with your google account to use the {integrationName} integration.</Body
|
||||
>
|
||||
</Layout>
|
||||
<GoogleButton samePage />
|
||||
{:else if isGoogleConfigured === false}
|
||||
<Body size="S"
|
||||
>Google authentication is not enabled, please complete Google SSO
|
||||
configuration.</Body
|
||||
>
|
||||
<Link href="/builder/portal/settings/auth">Configure Google SSO</Link>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if step === GoogleDatasouceConfigStep.SET_URL}
|
||||
<Layout noPadding no>
|
||||
<Body size="S">Add the URL of the sheet you want to connect.</Body>
|
||||
|
||||
<IntegrationConfigForm
|
||||
schema={datasource.schema}
|
||||
bind:datasource
|
||||
creating={true}
|
||||
on:valid={e => (isValid = e.detail)}
|
||||
/>
|
||||
</Layout>
|
||||
{/if}
|
||||
</ModalContent>
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import {
|
||||
keepOpen,
|
||||
ModalContent,
|
||||
notifications,
|
||||
Body,
|
||||
|
@ -70,10 +71,9 @@
|
|||
}
|
||||
|
||||
notifications.success(`Imported successfully.`)
|
||||
return true
|
||||
} catch (error) {
|
||||
notifications.error("Error importing queries")
|
||||
return false
|
||||
return keepOpen
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
<script>
|
||||
import { datasources } from "stores/backend"
|
||||
import { get } from "svelte/store"
|
||||
import { datasources, integrations } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { Input, ModalContent, Modal } from "@budibase/bbui"
|
||||
import { integrationForDatasource } from "stores/selectors"
|
||||
|
||||
let error = ""
|
||||
let modal
|
||||
|
@ -32,7 +34,10 @@
|
|||
...datasource,
|
||||
name,
|
||||
}
|
||||
await datasources.save(updatedDatasource)
|
||||
await datasources.update({
|
||||
datasource: updatedDatasource,
|
||||
integration: integrationForDatasource(get(integrations), datasource),
|
||||
})
|
||||
notifications.success(`Datasource ${name} updated successfully.`)
|
||||
hide()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<script>
|
||||
import ObjectField from "./fields/Object.svelte"
|
||||
import BooleanField from "./fields/Boolean.svelte"
|
||||
import LongFormField from "./fields/LongForm.svelte"
|
||||
import FieldGroupField from "./fields/FieldGroup.svelte"
|
||||
import StringField from "./fields/String.svelte"
|
||||
import SelectField from "./fields/Select.svelte"
|
||||
|
||||
export let type
|
||||
export let value
|
||||
export let error
|
||||
export let name
|
||||
export let config
|
||||
export let showModal = () => {}
|
||||
|
||||
const selectComponent = type => {
|
||||
if (type === "object") {
|
||||
return ObjectField
|
||||
} else if (type === "boolean") {
|
||||
return BooleanField
|
||||
} else if (type === "longForm") {
|
||||
return LongFormField
|
||||
} else if (type === "fieldGroup") {
|
||||
return FieldGroupField
|
||||
} else if (type === "select") {
|
||||
return SelectField
|
||||
} else {
|
||||
return StringField
|
||||
}
|
||||
}
|
||||
|
||||
$: component = selectComponent(type)
|
||||
</script>
|
||||
|
||||
<svelte:component
|
||||
this={component}
|
||||
{type}
|
||||
{value}
|
||||
{error}
|
||||
{name}
|
||||
{config}
|
||||
{showModal}
|
||||
on:blur
|
||||
on:change
|
||||
/>
|
|
@ -0,0 +1,20 @@
|
|||
<script>
|
||||
import { Label, Toggle } from "@budibase/bbui"
|
||||
|
||||
export let value
|
||||
export let name
|
||||
</script>
|
||||
|
||||
<div class="form-row">
|
||||
<Label>{name}</Label>
|
||||
<Toggle on:blur on:change text="" {value} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { Layout, Accordion } from "@budibase/bbui"
|
||||
import ConfigInput from "../ConfigInput.svelte"
|
||||
|
||||
export let value
|
||||
export let name
|
||||
export let config
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
|
||||
const handleChange = (updatedFieldKey, updatedFieldValue) => {
|
||||
const updatedValue = value.map(field => {
|
||||
return {
|
||||
key: field.key,
|
||||
value: field.key === updatedFieldKey ? updatedFieldValue : field.value,
|
||||
}
|
||||
})
|
||||
|
||||
dispatch("change", updatedValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Accordion
|
||||
initialOpen={config?.openByDefault ||
|
||||
Object.values(value).some(properties => !!properties.value)}
|
||||
header={name}
|
||||
>
|
||||
<Layout gap="S">
|
||||
{#each value as field}
|
||||
<ConfigInput
|
||||
{...field}
|
||||
on:change={e => handleChange(field.key, e.detail)}
|
||||
/>
|
||||
{/each}
|
||||
</Layout>
|
||||
</Accordion>
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
import { Label, TextArea } from "@budibase/bbui"
|
||||
|
||||
export let type
|
||||
export let name
|
||||
export let value
|
||||
export let error
|
||||
</script>
|
||||
|
||||
<div class="form-row">
|
||||
<Label>{name}</Label>
|
||||
<TextArea on:blur on:change {type} {value} {error} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
import { Label, Button } from "@budibase/bbui"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
|
||||
export let name
|
||||
export let value
|
||||
|
||||
let addButton
|
||||
</script>
|
||||
|
||||
<div class="form-row ssl">
|
||||
<Label>{name}</Label>
|
||||
<Button secondary thin outline on:click={addButton.addEntry()}>Add</Button>
|
||||
</div>
|
||||
<KeyValueBuilder
|
||||
on:change
|
||||
on:blur
|
||||
bind:this={addButton}
|
||||
defaults={value}
|
||||
noAddButton={true}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-row.ssl {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 20%;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,30 @@
|
|||
<script>
|
||||
import { Label, Select } from "@budibase/bbui"
|
||||
|
||||
export let type
|
||||
export let name
|
||||
export let value
|
||||
export let error
|
||||
export let config
|
||||
</script>
|
||||
|
||||
<div class="form-row">
|
||||
<Label>{name}</Label>
|
||||
<Select
|
||||
on:blur
|
||||
on:change
|
||||
options={config.options}
|
||||
{type}
|
||||
value={value || undefined}
|
||||
{error}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,39 @@
|
|||
<script>
|
||||
import { Label, EnvDropdown } from "@budibase/bbui"
|
||||
import { environment, licensing } from "stores/portal"
|
||||
|
||||
export let type
|
||||
export let name
|
||||
export let value
|
||||
export let error
|
||||
export let showModal = () => {}
|
||||
|
||||
async function handleUpgradePanel() {
|
||||
await environment.upgradePanelOpened()
|
||||
$licensing.goToUpgradePage()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="form-row">
|
||||
<Label>{name}</Label>
|
||||
<EnvDropdown
|
||||
on:change
|
||||
on:blur
|
||||
type={type === "port" ? "string" : type}
|
||||
{value}
|
||||
{error}
|
||||
variables={$environment.variables}
|
||||
environmentVariablesEnabled={$licensing.environmentVariablesEnabled}
|
||||
{showModal}
|
||||
{handleUpgradePanel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,107 @@
|
|||
<script>
|
||||
import {
|
||||
keepOpen,
|
||||
Modal,
|
||||
notifications,
|
||||
Body,
|
||||
Layout,
|
||||
ModalContent,
|
||||
} from "@budibase/bbui"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
|
||||
import ConfigInput from "./ConfigInput.svelte"
|
||||
import { createValidatedConfigStore } from "./stores/validatedConfig"
|
||||
import { createValidatedNameStore } from "./stores/validatedName"
|
||||
import { get } from "svelte/store"
|
||||
import { environment } from "stores/portal"
|
||||
|
||||
export let integration
|
||||
export let config
|
||||
export let onSubmit = () => {}
|
||||
export let showNameField = false
|
||||
export let nameFieldValue = ""
|
||||
|
||||
$: configStore = createValidatedConfigStore(integration, config)
|
||||
$: nameStore = createValidatedNameStore(nameFieldValue, showNameField)
|
||||
|
||||
const handleConfirm = async () => {
|
||||
configStore.markAllFieldsActive()
|
||||
nameStore.markActive()
|
||||
|
||||
if ((await configStore.validate()) && (await nameStore.validate())) {
|
||||
const { config } = get(configStore)
|
||||
const { name } = get(nameStore)
|
||||
return onSubmit({
|
||||
config,
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
return keepOpen
|
||||
}
|
||||
|
||||
let createVariableModal
|
||||
let configValueSetterCallback = () => {}
|
||||
|
||||
const showModal = setter => {
|
||||
configValueSetterCallback = setter
|
||||
createVariableModal.show()
|
||||
}
|
||||
|
||||
async function saveVariable(data) {
|
||||
try {
|
||||
await environment.createVariable(data)
|
||||
configValueSetterCallback(`{{ env.${data.name} }}`)
|
||||
createVariableModal.hide()
|
||||
} catch (err) {
|
||||
notifications.error(`Failed to create variable: ${err.message}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={`Connect to ${integration.friendlyName}`}
|
||||
onConfirm={handleConfirm}
|
||||
confirmText={integration.plus ? "Connect" : "Save and continue to query"}
|
||||
cancelText="Back"
|
||||
disabled={$configStore.preventSubmit || $nameStore.preventSubmit}
|
||||
size="L"
|
||||
>
|
||||
<Layout noPadding>
|
||||
<Body size="XS">
|
||||
Connect your database to Budibase using the config below.
|
||||
</Body>
|
||||
</Layout>
|
||||
|
||||
{#if showNameField}
|
||||
<ConfigInput
|
||||
type="string"
|
||||
value={$nameStore.name}
|
||||
error={$nameStore.error}
|
||||
name="Name"
|
||||
showModal={() => showModal(nameStore.updateValue)}
|
||||
on:blur={nameStore.markActive}
|
||||
on:change={e => nameStore.updateValue(e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#each $configStore.validatedConfig as { type, key, value, error, name, hidden, config }}
|
||||
{#if hidden === undefined || !eval(processStringSync(hidden, $configStore.config))}
|
||||
<ConfigInput
|
||||
{type}
|
||||
{value}
|
||||
{error}
|
||||
{name}
|
||||
{config}
|
||||
showModal={() =>
|
||||
showModal(newValue => configStore.updateFieldValue(key, newValue))}
|
||||
on:blur={() => configStore.markFieldActive(key)}
|
||||
on:change={e => configStore.updateFieldValue(key, e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</ModalContent>
|
||||
|
||||
<Modal bind:this={createVariableModal}>
|
||||
<CreateEditVariableModal save={saveVariable} />
|
||||
</Modal>
|
|
@ -0,0 +1,144 @@
|
|||
import { derived, writable, get } from "svelte/store"
|
||||
import { getValidatorFields } from "./validation"
|
||||
import { capitalise } from "helpers"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { object } from "yup"
|
||||
|
||||
export const createValidatedConfigStore = (integration, config) => {
|
||||
const configStore = writable(config)
|
||||
const allValidators = getValidatorFields(integration)
|
||||
const selectedValidatorsStore = writable({})
|
||||
const errorsStore = writable({})
|
||||
|
||||
const validate = async () => {
|
||||
try {
|
||||
await object()
|
||||
.shape(get(selectedValidatorsStore))
|
||||
.validate(get(configStore), { abortEarly: false })
|
||||
|
||||
errorsStore.set({})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
// Yup error
|
||||
if (error.inner) {
|
||||
const errors = {}
|
||||
|
||||
error.inner.forEach(innerError => {
|
||||
errors[innerError.path] = capitalise(innerError.message)
|
||||
})
|
||||
|
||||
errorsStore.set(errors)
|
||||
} else {
|
||||
// Non-yup error
|
||||
notifications.error("Unexpected validation error")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const updateFieldValue = (key, value) => {
|
||||
configStore.update($configStore => {
|
||||
const newStore = { ...$configStore }
|
||||
|
||||
if (integration.datasource[key].type === "fieldGroup") {
|
||||
value.forEach(field => {
|
||||
newStore[field.key] = field.value
|
||||
})
|
||||
if (!integration.datasource[key].config?.nestedFields) {
|
||||
value.forEach(field => {
|
||||
newStore[field.key] = field.value
|
||||
})
|
||||
} else {
|
||||
newStore[key] = value.reduce(
|
||||
(p, field) => ({
|
||||
...p,
|
||||
[field.key]: field.value,
|
||||
}),
|
||||
{}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
newStore[key] = value
|
||||
}
|
||||
|
||||
return newStore
|
||||
})
|
||||
validate()
|
||||
}
|
||||
|
||||
const markAllFieldsActive = () => {
|
||||
selectedValidatorsStore.set(allValidators)
|
||||
validate()
|
||||
}
|
||||
|
||||
const markFieldActive = key => {
|
||||
selectedValidatorsStore.update($validatorsStore => ({
|
||||
...$validatorsStore,
|
||||
[key]: allValidators[key],
|
||||
}))
|
||||
validate()
|
||||
}
|
||||
|
||||
const combined = derived(
|
||||
[configStore, errorsStore, selectedValidatorsStore],
|
||||
([$configStore, $errorsStore, $selectedValidatorsStore]) => {
|
||||
const validatedConfig = []
|
||||
|
||||
Object.entries(integration.datasource).forEach(([key, properties]) => {
|
||||
if (integration.name === "REST" && key !== "rejectUnauthorized") {
|
||||
return
|
||||
}
|
||||
|
||||
const getValue = () => {
|
||||
if (properties.type === "fieldGroup") {
|
||||
return Object.entries(properties.fields).map(
|
||||
([fieldKey, fieldProperties]) => {
|
||||
return {
|
||||
key: fieldKey,
|
||||
name: capitalise(fieldProperties.display || fieldKey),
|
||||
type: fieldProperties.type,
|
||||
value: $configStore[fieldKey],
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return $configStore[key]
|
||||
}
|
||||
|
||||
validatedConfig.push({
|
||||
key,
|
||||
value: getValue(),
|
||||
error: $errorsStore[key],
|
||||
name: capitalise(properties.display || key),
|
||||
type: properties.type,
|
||||
hidden: properties.hidden,
|
||||
config: properties.config,
|
||||
})
|
||||
})
|
||||
|
||||
const allFieldsActive =
|
||||
Object.keys($selectedValidatorsStore).length ===
|
||||
Object.keys(allValidators).length
|
||||
|
||||
const hasErrors = Object.keys($errorsStore).length > 0
|
||||
|
||||
return {
|
||||
validatedConfig,
|
||||
config: $configStore,
|
||||
errors: $errorsStore,
|
||||
preventSubmit: allFieldsActive && hasErrors,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
subscribe: combined.subscribe,
|
||||
updateFieldValue,
|
||||
markAllFieldsActive,
|
||||
markFieldActive,
|
||||
validate,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import { derived, get, writable } from "svelte/store"
|
||||
import { capitalise } from "helpers"
|
||||
import { string } from "yup"
|
||||
|
||||
export const createValidatedNameStore = (name, isVisible) => {
|
||||
const nameStore = writable(name)
|
||||
const isActiveStore = writable(false)
|
||||
const errorStore = writable(null)
|
||||
|
||||
const validate = async () => {
|
||||
if (!isVisible || !get(isActiveStore)) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
await string().required().validate(get(nameStore), { abortEarly: false })
|
||||
|
||||
errorStore.set(null)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
errorStore.set(capitalise(error.message))
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const updateValue = value => {
|
||||
nameStore.set(value)
|
||||
validate()
|
||||
}
|
||||
|
||||
const markActive = () => {
|
||||
isActiveStore.set(true)
|
||||
validate()
|
||||
}
|
||||
|
||||
const combined = derived(
|
||||
[nameStore, errorStore, isActiveStore],
|
||||
([$nameStore, $errorStore, $isActiveStore]) => ({
|
||||
name: $nameStore,
|
||||
error: $errorStore,
|
||||
preventSubmit: $errorStore !== null && $isActiveStore,
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
subscribe: combined.subscribe,
|
||||
updateValue,
|
||||
markActive,
|
||||
validate,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { string, number } from "yup"
|
||||
|
||||
const propertyValidator = type => {
|
||||
if (type === "number") {
|
||||
return number().nullable()
|
||||
}
|
||||
|
||||
if (type === "email") {
|
||||
return string().email().nullable()
|
||||
}
|
||||
|
||||
return string().nullable()
|
||||
}
|
||||
|
||||
export const getValidatorFields = integration => {
|
||||
const validatorFields = {}
|
||||
|
||||
Object.entries(integration?.datasource || {}).forEach(([key, properties]) => {
|
||||
if (properties.required) {
|
||||
validatorFields[key] = propertyValidator(properties.type).required()
|
||||
} else {
|
||||
validatorFields[key] = propertyValidator(properties.type).notRequired()
|
||||
}
|
||||
})
|
||||
|
||||
return validatorFields
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { RelationshipTypes } from "constants/backend"
|
||||
import {
|
||||
keepOpen,
|
||||
Button,
|
||||
Input,
|
||||
ModalContent,
|
||||
|
@ -59,7 +60,6 @@
|
|||
$: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet()
|
||||
$: isManyToMany = relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||
$: isManyToOne = relationshipType === RelationshipTypes.MANY_TO_ONE
|
||||
$: toRelationship.relationshipType = fromRelationship?.relationshipType
|
||||
|
||||
function getTable(id) {
|
||||
return plusTables.find(table => table._id === id)
|
||||
|
@ -180,6 +180,16 @@
|
|||
return getErrorCount(errors) === 0
|
||||
}
|
||||
|
||||
function otherRelationshipType(type) {
|
||||
if (type === RelationshipTypes.MANY_TO_ONE) {
|
||||
return RelationshipTypes.ONE_TO_MANY
|
||||
} else if (type === RelationshipTypes.ONE_TO_MANY) {
|
||||
return RelationshipTypes.MANY_TO_ONE
|
||||
} else if (type === RelationshipTypes.MANY_TO_MANY) {
|
||||
return RelationshipTypes.MANY_TO_MANY
|
||||
}
|
||||
}
|
||||
|
||||
function buildRelationships() {
|
||||
const id = Helpers.uuid()
|
||||
//Map temporary variables
|
||||
|
@ -200,6 +210,7 @@
|
|||
...toRelationship,
|
||||
tableId: fromId,
|
||||
name: fromColumn,
|
||||
relationshipType: otherRelationshipType(relationshipType),
|
||||
through: throughId,
|
||||
type: "link",
|
||||
_id: id,
|
||||
|
@ -267,7 +278,7 @@
|
|||
|
||||
async function saveRelationship() {
|
||||
if (!validate()) {
|
||||
return false
|
||||
return keepOpen
|
||||
}
|
||||
buildRelationships()
|
||||
removeExistingRelationship()
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<script>
|
||||
import { Modal } from "@budibase/bbui"
|
||||
import { get } from "svelte/store"
|
||||
import CreateEditRelationship from "./CreateEditRelationship.svelte"
|
||||
import { integrations, datasources } from "stores/backend"
|
||||
import { integrationForDatasource } from "stores/selectors"
|
||||
|
||||
export let datasource
|
||||
export let tables
|
||||
export let beforeSave = async () => {}
|
||||
export let afterSave = async () => {}
|
||||
export let onError = async () => {}
|
||||
|
||||
let relationshipModal
|
||||
let fromRelationship = {}
|
||||
let toRelationship = {}
|
||||
let fromTable = null
|
||||
|
||||
export function show({
|
||||
fromRelationship: selectedFromRelationship = {},
|
||||
toRelationship: selectedToRelationship = {},
|
||||
fromTable: selectedFromTable = null,
|
||||
}) {
|
||||
fromRelationship = selectedFromRelationship
|
||||
toRelationship = selectedToRelationship
|
||||
fromTable = selectedFromTable
|
||||
|
||||
relationshipModal.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
relationshipModal.hide()
|
||||
}
|
||||
|
||||
// action is one of 'created', 'updated' or 'deleted'
|
||||
async function saveRelationship(action) {
|
||||
try {
|
||||
await beforeSave({ action, datasource })
|
||||
|
||||
const integration = integrationForDatasource(
|
||||
get(integrations),
|
||||
datasource
|
||||
)
|
||||
await datasources.update({ datasource, integration })
|
||||
|
||||
await afterSave({ datasource, action })
|
||||
} catch (err) {
|
||||
await onError({ err, datasource, action })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={relationshipModal}>
|
||||
<CreateEditRelationship
|
||||
save={saveRelationship}
|
||||
close={relationshipModal.hide}
|
||||
selectedFromTable={fromTable}
|
||||
{datasource}
|
||||
plusTables={tables}
|
||||
{fromRelationship}
|
||||
{toRelationship}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue