Merge branch 'develop' into labday/status-banner
This commit is contained in:
commit
7151c86224
|
@ -38,6 +38,17 @@ jobs:
|
||||||
fi
|
fi
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Tag and release Proxy service docker image
|
||||||
|
run: |
|
||||||
|
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||||
|
yarn build:docker:proxy:prod
|
||||||
|
docker tag budibase/proxy:$release_tag budibase/proxy:$PROD_TAG
|
||||||
|
docker push budibase/proxy:$PROD_TAG
|
||||||
|
env:
|
||||||
|
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||||
|
PROD_TAG: k8s
|
||||||
|
|
||||||
- name: Configure AWS Credentials
|
- name: Configure AWS Credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v1
|
uses: aws-actions/configure-aws-credentials@v1
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -23,12 +23,24 @@ jobs:
|
||||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
aws-region: eu-west-1
|
aws-region: eu-west-1
|
||||||
|
|
||||||
|
|
||||||
- name: Get the latest budibase release version
|
- name: Get the latest budibase release version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
release_version=$(cat lerna.json | jq -r '.version')
|
release_version=$(cat lerna.json | jq -r '.version')
|
||||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Tag and release Proxy service docker image
|
||||||
|
run: |
|
||||||
|
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||||
|
yarn build:docker:proxy:preprod
|
||||||
|
docker tag budibase/proxy:$release_tag budibase/proxy:$PREPROD_TAG
|
||||||
|
docker push budibase/proxy:$PREPROD_TAG
|
||||||
|
env:
|
||||||
|
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||||
|
PREPROD_TAG: k8s-preprod
|
||||||
|
|
||||||
- name: Pull values.yaml from budibase-infra
|
- name: Pull values.yaml from budibase-infra
|
||||||
run: |
|
run: |
|
||||||
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \
|
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \
|
||||||
|
|
|
@ -48,6 +48,13 @@ jobs:
|
||||||
yarn build
|
yarn build
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
- name: Build OpenAPI sepc
|
||||||
|
run: |
|
||||||
|
pushd packages/server
|
||||||
|
yarn
|
||||||
|
yarn specs
|
||||||
|
popd
|
||||||
|
|
||||||
- name: Setup Helm
|
- name: Setup Helm
|
||||||
uses: azure/setup-helm@v1
|
uses: azure/setup-helm@v1
|
||||||
id: helm-install
|
id: helm-install
|
||||||
|
@ -77,3 +84,5 @@ jobs:
|
||||||
packages/cli/build/cli-win.exe
|
packages/cli/build/cli-win.exe
|
||||||
packages/cli/build/cli-linux
|
packages/cli/build/cli-linux
|
||||||
packages/cli/build/cli-macos
|
packages/cli/build/cli-macos
|
||||||
|
packages/server/specs/openapi.yaml
|
||||||
|
packages/server/specs/openapi.json
|
||||||
|
|
|
@ -96,4 +96,5 @@ hosting/proxy/.generated-nginx.prod.conf
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
|
|
||||||
bin/
|
bin/
|
||||||
|
hosting/.generated*
|
||||||
packages/builder/cypress.env.json
|
packages/builder/cypress.env.json
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
node_modules
|
node_modules
|
||||||
public
|
|
||||||
dist
|
dist
|
||||||
*.spec.js
|
*.spec.js
|
||||||
packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
|
packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
|
||||||
|
|
|
@ -76,6 +76,7 @@ http {
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
|
||||||
proxy_connect_timeout 300;
|
proxy_connect_timeout 300;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
|
@ -19,13 +19,7 @@ http {
|
||||||
tcp_nodelay on;
|
tcp_nodelay on;
|
||||||
server_tokens off;
|
server_tokens off;
|
||||||
types_hash_max_size 2048;
|
types_hash_max_size 2048;
|
||||||
{{#if compose}}
|
resolver {{ resolver }} valid=10s ipv6=off;
|
||||||
resolver 127.0.0.11 ipv6=off;
|
|
||||||
{{/if}}
|
|
||||||
{{#if k8s}}
|
|
||||||
resolver kube-dns.kube-system.svc.cluster.local valid=10s;
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
|
|
||||||
# buffering
|
# buffering
|
||||||
client_body_buffer_size 1K;
|
client_body_buffer_size 1K;
|
||||||
|
@ -55,7 +49,7 @@ http {
|
||||||
add_header X-Frame-Options SAMEORIGIN always;
|
add_header X-Frame-Options SAMEORIGIN always;
|
||||||
add_header X-Content-Type-Options nosniff always;
|
add_header X-Content-Type-Options nosniff always;
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com; object-src 'none'; base-uri 'self'; connect-src 'self' https://api-iam.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io; font-src 'self' data https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com; frame-src 'self'; img-src http: https: data; manifest-src 'self'; media-src 'self'; worker-src 'none';" always;
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com; object-src 'none'; base-uri 'self'; connect-src 'self' https://api-iam.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io ; font-src 'self' data https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com; frame-src 'self' https:; img-src http: https: data; manifest-src 'self'; media-src 'self'; worker-src 'none';" always;
|
||||||
|
|
||||||
# upstreams
|
# upstreams
|
||||||
set $apps {{ apps }};
|
set $apps {{ apps }};
|
||||||
|
|
|
@ -1,145 +0,0 @@
|
||||||
user nginx;
|
|
||||||
error_log /var/log/nginx/error.log debug;
|
|
||||||
pid /var/run/nginx.pid;
|
|
||||||
worker_processes auto;
|
|
||||||
worker_rlimit_nofile 33282;
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s;
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
charset utf-8;
|
|
||||||
sendfile on;
|
|
||||||
tcp_nopush on;
|
|
||||||
tcp_nodelay on;
|
|
||||||
server_tokens off;
|
|
||||||
types_hash_max_size 2048;
|
|
||||||
|
|
||||||
# buffering
|
|
||||||
client_body_buffer_size 1K;
|
|
||||||
client_header_buffer_size 1k;
|
|
||||||
client_max_body_size 1k;
|
|
||||||
ignore_invalid_headers off;
|
|
||||||
|
|
||||||
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
||||||
|
|
||||||
map $http_upgrade $connection_upgrade {
|
|
||||||
default "upgrade";
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 10000 default_server;
|
|
||||||
listen [::]:10000 default_server;
|
|
||||||
server_name _;
|
|
||||||
client_max_body_size 1000m;
|
|
||||||
ignore_invalid_headers off;
|
|
||||||
proxy_buffering off;
|
|
||||||
port_in_redirect off;
|
|
||||||
|
|
||||||
# Security Headers
|
|
||||||
add_header X-Frame-Options SAMEORIGIN always;
|
|
||||||
add_header X-Content-Type-Options nosniff always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me; object-src 'none'; base-uri 'self'; connect-src 'self' https://api-iam.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io; font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me; frame-src 'self'; img-src http: https: data:; manifest-src 'self'; media-src 'self'; worker-src 'none';" always;
|
|
||||||
|
|
||||||
location /app {
|
|
||||||
proxy_pass http://app-service:4002;
|
|
||||||
rewrite ^/app/(.*)$ /$1 break;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = / {
|
|
||||||
port_in_redirect off;
|
|
||||||
proxy_pass http://app-service:4002;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /v1/update {
|
|
||||||
proxy_pass http://watchtower-service:8080;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /builder/ {
|
|
||||||
port_in_redirect off;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Connection $connection_upgrade;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_pass http://app-service:4002;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/(builder|app_) {
|
|
||||||
port_in_redirect off;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Connection $connection_upgrade;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_pass http://app-service:4002;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/api/(system|admin|global)/ {
|
|
||||||
proxy_pass http://worker-service:4003;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /worker/ {
|
|
||||||
proxy_pass http://worker-service:4003;
|
|
||||||
rewrite ^/worker/(.*)$ /$1 break;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api/ {
|
|
||||||
# calls to the API are rate limited with bursting
|
|
||||||
limit_req zone=ratelimit burst=20 nodelay;
|
|
||||||
|
|
||||||
# 120s timeout on API requests
|
|
||||||
proxy_read_timeout 120s;
|
|
||||||
proxy_connect_timeout 120s;
|
|
||||||
proxy_send_timeout 120s;
|
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Connection $connection_upgrade;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
|
|
||||||
proxy_pass http://app-service:4002;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /db/ {
|
|
||||||
proxy_pass http://couchdb-service:5984;
|
|
||||||
rewrite ^/db/(.*)$ /$1 break;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header Host $http_host;
|
|
||||||
|
|
||||||
proxy_connect_timeout 300;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Connection "";
|
|
||||||
chunked_transfer_encoding off;
|
|
||||||
proxy_pass http://minio-service:9000;
|
|
||||||
}
|
|
||||||
|
|
||||||
client_header_timeout 60;
|
|
||||||
client_body_timeout 60;
|
|
||||||
keepalive_timeout 60;
|
|
||||||
|
|
||||||
# gzip
|
|
||||||
gzip on;
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_proxied any;
|
|
||||||
gzip_comp_level 6;
|
|
||||||
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.76-alpha.2",
|
"version": "1.0.81-alpha.0",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
11
package.json
11
package.json
|
@ -42,13 +42,14 @@
|
||||||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\"",
|
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\"",
|
||||||
"lint:fix:ts": "lerna run lint:fix",
|
"lint:fix:ts": "lerna run lint:fix",
|
||||||
"lint:fix": "yarn run lint:fix:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
"lint:fix": "yarn run lint:fix:ts && yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||||
"test:e2e": "lerna run cy:test",
|
"test:e2e": "lerna run cy:test --stream",
|
||||||
"test:e2e:ci": "lerna run cy:ci",
|
"test:e2e:ci": "lerna run cy:ci --stream",
|
||||||
|
"build:specs": "lerna run specs",
|
||||||
"build:docker": "lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
"build:docker": "lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
||||||
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
|
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
|
||||||
"build:docker:proxy:compose": "lerna run generate:proxy:compose && npm run build:docker:proxy",
|
"build:docker:proxy:compose": "node scripts/proxy/generateProxyConfig compose && npm run build:docker:proxy",
|
||||||
"build:docker:proxy:preprod": "lerna run generate:proxy:preprod && npm run build:docker:proxy",
|
"build:docker:proxy:preprod": "node scripts/proxy/generateProxyConfig preprod && npm run build:docker:proxy",
|
||||||
"build:docker:proxy:prod": "lerna run generate:proxy:prod && npm run build:docker:proxy",
|
"build:docker:proxy:prod": "node scripts/proxy/generateProxyConfig prod && npm run build:docker:proxy",
|
||||||
"build:docker:selfhost": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
|
"build:docker:selfhost": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
|
||||||
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
||||||
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require("./src/security/encryption")
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.0.76-alpha.2",
|
"version": "1.0.81-alpha.0",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
|
|
@ -32,11 +32,10 @@ const populateFromDB = async (userId, tenantId) => {
|
||||||
* @param {*} populateUser function to provide the user for re-caching. default to couch db
|
* @param {*} populateUser function to provide the user for re-caching. default to couch db
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
exports.getUser = async (
|
exports.getUser = async (userId, tenantId = null, populateUser = null) => {
|
||||||
userId,
|
if (!populateUser) {
|
||||||
tenantId = null,
|
populateUser = populateFromDB
|
||||||
populateUser = populateFromDB
|
}
|
||||||
) => {
|
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
try {
|
try {
|
||||||
tenantId = getTenantId()
|
tenantId = getTenantId()
|
||||||
|
|
|
@ -14,6 +14,7 @@ exports.DocumentTypes = {
|
||||||
APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`,
|
APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`,
|
||||||
ROLE: "role",
|
ROLE: "role",
|
||||||
MIGRATIONS: "migrations",
|
MIGRATIONS: "migrations",
|
||||||
|
DEV_INFO: "devinfo",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.StaticDatabases = {
|
exports.StaticDatabases = {
|
||||||
|
|
|
@ -30,6 +30,7 @@ const UNICODE_MAX = "\ufff0"
|
||||||
|
|
||||||
exports.ViewNames = {
|
exports.ViewNames = {
|
||||||
USER_BY_EMAIL: "by_email",
|
USER_BY_EMAIL: "by_email",
|
||||||
|
BY_API_KEY: "by_api_key",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.StaticDatabases = StaticDatabases
|
exports.StaticDatabases = StaticDatabases
|
||||||
|
@ -67,6 +68,7 @@ function getDocParams(docType, docId = null, otherProps = {}) {
|
||||||
endkey: `${docType}${SEPARATOR}${docId}${UNICODE_MAX}`,
|
endkey: `${docType}${SEPARATOR}${docId}${UNICODE_MAX}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
exports.getDocParams = getDocParams
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new workspace ID.
|
* Generates a new workspace ID.
|
||||||
|
@ -339,6 +341,14 @@ const getConfigParams = ({ type, workspace, user }, otherProps = {}) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new dev info document ID - this is scoped to a user.
|
||||||
|
* @returns {string} The new dev info ID which info for dev (like api key) can be stored under.
|
||||||
|
*/
|
||||||
|
const generateDevInfoID = userId => {
|
||||||
|
return `${DocumentTypes.DEV_INFO}${SEPARATOR}${userId}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the most granular configuration document from the DB based on the type, workspace and userID passed.
|
* Returns the most granular configuration document from the DB based on the type, workspace and userID passed.
|
||||||
* @param {Object} db - db instance to query
|
* @param {Object} db - db instance to query
|
||||||
|
@ -454,3 +464,4 @@ exports.generateConfigID = generateConfigID
|
||||||
exports.getConfigParams = getConfigParams
|
exports.getConfigParams = getConfigParams
|
||||||
exports.getScopedFullConfig = getScopedFullConfig
|
exports.getScopedFullConfig = getScopedFullConfig
|
||||||
exports.generateNewUsageQuotaDoc = generateNewUsageQuotaDoc
|
exports.generateNewUsageQuotaDoc = generateNewUsageQuotaDoc
|
||||||
|
exports.generateDevInfoID = generateDevInfoID
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const { DocumentTypes, ViewNames } = require("./utils")
|
const { DocumentTypes, ViewNames } = require("./utils")
|
||||||
|
const { getGlobalDB } = require("../tenancy")
|
||||||
|
|
||||||
function DesignDoc() {
|
function DesignDoc() {
|
||||||
return {
|
return {
|
||||||
|
@ -9,7 +10,8 @@ function DesignDoc() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createUserEmailView = async db => {
|
exports.createUserEmailView = async () => {
|
||||||
|
const db = getGlobalDB()
|
||||||
let designDoc
|
let designDoc
|
||||||
try {
|
try {
|
||||||
designDoc = await db.get("_design/database")
|
designDoc = await db.get("_design/database")
|
||||||
|
@ -31,3 +33,51 @@ exports.createUserEmailView = async db => {
|
||||||
}
|
}
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.createApiKeyView = async () => {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
let designDoc
|
||||||
|
try {
|
||||||
|
designDoc = await db.get("_design/database")
|
||||||
|
} catch (err) {
|
||||||
|
designDoc = DesignDoc()
|
||||||
|
}
|
||||||
|
const view = {
|
||||||
|
map: `function(doc) {
|
||||||
|
if (doc._id.startsWith("${DocumentTypes.DEV_INFO}") && doc.apiKey) {
|
||||||
|
emit(doc.apiKey, doc.userId)
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
}
|
||||||
|
designDoc.views = {
|
||||||
|
...designDoc.views,
|
||||||
|
[ViewNames.BY_API_KEY]: view,
|
||||||
|
}
|
||||||
|
await db.put(designDoc)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.queryGlobalView = async (viewName, params, db = null) => {
|
||||||
|
const CreateFuncByName = {
|
||||||
|
[ViewNames.USER_BY_EMAIL]: exports.createUserEmailView,
|
||||||
|
[ViewNames.BY_API_KEY]: exports.createApiKeyView,
|
||||||
|
}
|
||||||
|
// can pass DB in if working with something specific
|
||||||
|
if (!db) {
|
||||||
|
db = getGlobalDB()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let response = (await db.query(`database/${viewName}`, params)).rows
|
||||||
|
response = response.map(resp =>
|
||||||
|
params.include_docs ? resp.doc : resp.value
|
||||||
|
)
|
||||||
|
return response.length <= 1 ? response[0] : response
|
||||||
|
} catch (err) {
|
||||||
|
if (err != null && err.name === "not_found") {
|
||||||
|
const createFunc = CreateFuncByName[viewName]
|
||||||
|
await createFunc()
|
||||||
|
return exports.queryGlobalView(viewName, params)
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,9 @@ const { getUser } = require("../cache/user")
|
||||||
const { getSession, updateSessionTTL } = require("../security/sessions")
|
const { getSession, updateSessionTTL } = require("../security/sessions")
|
||||||
const { buildMatcherRegex, matches } = require("./matchers")
|
const { buildMatcherRegex, matches } = require("./matchers")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
const { SEPARATOR, ViewNames, queryGlobalView } = require("../../db")
|
||||||
|
const { getGlobalDB } = require("../tenancy")
|
||||||
|
const { decrypt } = require("../security/encryption")
|
||||||
|
|
||||||
function finalise(
|
function finalise(
|
||||||
ctx,
|
ctx,
|
||||||
|
@ -16,6 +19,28 @@ function finalise(
|
||||||
ctx.version = version
|
ctx.version = version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkApiKey(apiKey, populateUser) {
|
||||||
|
if (apiKey === env.INTERNAL_API_KEY) {
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
const decrypted = decrypt(apiKey)
|
||||||
|
const tenantId = decrypted.split(SEPARATOR)[0]
|
||||||
|
const db = getGlobalDB(tenantId)
|
||||||
|
// api key is encrypted in the database
|
||||||
|
const userId = await queryGlobalView(
|
||||||
|
ViewNames.BY_API_KEY,
|
||||||
|
{
|
||||||
|
key: apiKey,
|
||||||
|
},
|
||||||
|
db
|
||||||
|
)
|
||||||
|
if (userId) {
|
||||||
|
return { valid: true, user: await getUser(userId, tenantId, populateUser) }
|
||||||
|
} else {
|
||||||
|
throw "Invalid API key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This middleware is tenancy aware, so that it does not depend on other middlewares being used.
|
* This middleware is tenancy aware, so that it does not depend on other middlewares being used.
|
||||||
* The tenancy modules should not be used here and it should be assumed that the tenancy context
|
* The tenancy modules should not be used here and it should be assumed that the tenancy context
|
||||||
|
@ -79,9 +104,19 @@ module.exports = (
|
||||||
const apiKey = ctx.request.headers[Headers.API_KEY]
|
const apiKey = ctx.request.headers[Headers.API_KEY]
|
||||||
const tenantId = ctx.request.headers[Headers.TENANT_ID]
|
const tenantId = ctx.request.headers[Headers.TENANT_ID]
|
||||||
// this is an internal request, no user made it
|
// this is an internal request, no user made it
|
||||||
if (!authenticated && apiKey && apiKey === env.INTERNAL_API_KEY) {
|
if (!authenticated && apiKey) {
|
||||||
authenticated = true
|
const populateUser = opts.populateUser ? opts.populateUser(ctx) : null
|
||||||
internal = true
|
const { valid, user: foundUser } = await checkApiKey(
|
||||||
|
apiKey,
|
||||||
|
populateUser
|
||||||
|
)
|
||||||
|
if (valid && foundUser) {
|
||||||
|
authenticated = true
|
||||||
|
user = foundUser
|
||||||
|
} else if (valid) {
|
||||||
|
authenticated = true
|
||||||
|
internal = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!user && tenantId) {
|
if (!user && tenantId) {
|
||||||
user = { tenantId }
|
user = { tenantId }
|
||||||
|
@ -101,6 +136,7 @@ module.exports = (
|
||||||
// allow configuring for public access
|
// allow configuring for public access
|
||||||
if ((opts && opts.publicAllowed) || publicEndpoint) {
|
if ((opts && opts.publicAllowed) || publicEndpoint) {
|
||||||
finalise(ctx, { authenticated: false, version, publicEndpoint })
|
finalise(ctx, { authenticated: false, version, publicEndpoint })
|
||||||
|
return next()
|
||||||
} else {
|
} else {
|
||||||
ctx.throw(err.status || 403, err)
|
ctx.throw(err.status || 403, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,7 @@ exports.ObjectStore = bucket => {
|
||||||
const config = {
|
const config = {
|
||||||
s3ForcePathStyle: true,
|
s3ForcePathStyle: true,
|
||||||
signatureVersion: "v4",
|
signatureVersion: "v4",
|
||||||
|
apiVersion: "2006-03-01",
|
||||||
params: {
|
params: {
|
||||||
Bucket: sanitizeBucket(bucket),
|
Bucket: sanitizeBucket(bucket),
|
||||||
},
|
},
|
||||||
|
@ -102,17 +103,21 @@ exports.makeSureBucketExists = async (client, bucketName) => {
|
||||||
.promise()
|
.promise()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const promises = STATE.bucketCreationPromises
|
const promises = STATE.bucketCreationPromises
|
||||||
|
const doesntExist = err.statusCode === 404,
|
||||||
|
noAccess = err.statusCode === 403
|
||||||
if (promises[bucketName]) {
|
if (promises[bucketName]) {
|
||||||
await promises[bucketName]
|
await promises[bucketName]
|
||||||
} else if (err.statusCode === 404) {
|
} else if (doesntExist || noAccess) {
|
||||||
// bucket doesn't exist create it
|
if (doesntExist) {
|
||||||
promises[bucketName] = client
|
// bucket doesn't exist create it
|
||||||
.createBucket({
|
promises[bucketName] = client
|
||||||
Bucket: bucketName,
|
.createBucket({
|
||||||
})
|
Bucket: bucketName,
|
||||||
.promise()
|
})
|
||||||
await promises[bucketName]
|
.promise()
|
||||||
delete promises[bucketName]
|
await promises[bucketName]
|
||||||
|
delete promises[bucketName]
|
||||||
|
}
|
||||||
// public buckets are quite hidden in the system, make sure
|
// public buckets are quite hidden in the system, make sure
|
||||||
// no bucket is set accidentally
|
// no bucket is set accidentally
|
||||||
if (PUBLIC_BUCKETS.includes(bucketName)) {
|
if (PUBLIC_BUCKETS.includes(bucketName)) {
|
||||||
|
@ -124,7 +129,7 @@ exports.makeSureBucketExists = async (client, bucketName) => {
|
||||||
.promise()
|
.promise()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw err
|
throw new Error("Unable to write to object store bucket.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
exports.lookupApiKey = async () => {}
|
|
@ -0,0 +1,33 @@
|
||||||
|
const crypto = require("crypto")
|
||||||
|
const env = require("../environment")
|
||||||
|
|
||||||
|
const ALGO = "aes-256-ctr"
|
||||||
|
const SECRET = env.JWT_SECRET
|
||||||
|
const SEPARATOR = "-"
|
||||||
|
const ITERATIONS = 10000
|
||||||
|
const RANDOM_BYTES = 16
|
||||||
|
const STRETCH_LENGTH = 32
|
||||||
|
|
||||||
|
function stretchString(string, salt) {
|
||||||
|
return crypto.pbkdf2Sync(string, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.encrypt = input => {
|
||||||
|
const salt = crypto.randomBytes(RANDOM_BYTES)
|
||||||
|
const stretched = stretchString(SECRET, salt)
|
||||||
|
const cipher = crypto.createCipheriv(ALGO, stretched, salt)
|
||||||
|
const base = cipher.update(input)
|
||||||
|
const final = cipher.final()
|
||||||
|
const encrypted = Buffer.concat([base, final]).toString("hex")
|
||||||
|
return `${salt.toString("hex")}${SEPARATOR}${encrypted}`
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.decrypt = input => {
|
||||||
|
const [salt, encrypted] = input.split(SEPARATOR)
|
||||||
|
const saltBuffer = Buffer.from(salt, "hex")
|
||||||
|
const stretched = stretchString(SECRET, saltBuffer)
|
||||||
|
const decipher = crypto.createDecipheriv(ALGO, stretched, saltBuffer)
|
||||||
|
const base = decipher.update(Buffer.from(encrypted, "hex"))
|
||||||
|
const final = decipher.final()
|
||||||
|
return Buffer.concat([base, final]).toString()
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ const PermissionLevels = {
|
||||||
|
|
||||||
// these are the global types, that govern the underlying default behaviour
|
// these are the global types, that govern the underlying default behaviour
|
||||||
const PermissionTypes = {
|
const PermissionTypes = {
|
||||||
|
APP: "app",
|
||||||
TABLE: "table",
|
TABLE: "table",
|
||||||
USER: "user",
|
USER: "user",
|
||||||
AUTOMATION: "automation",
|
AUTOMATION: "automation",
|
||||||
|
|
|
@ -6,7 +6,7 @@ const {
|
||||||
} = require("./db/utils")
|
} = require("./db/utils")
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
const { options } = require("./middleware/passport/jwt")
|
const { options } = require("./middleware/passport/jwt")
|
||||||
const { createUserEmailView } = require("./db/views")
|
const { queryGlobalView } = require("./db/views")
|
||||||
const { Headers, UserStatus, Cookies, MAX_VALID_DATE } = require("./constants")
|
const { Headers, UserStatus, Cookies, MAX_VALID_DATE } = require("./constants")
|
||||||
const {
|
const {
|
||||||
getGlobalDB,
|
getGlobalDB,
|
||||||
|
@ -139,25 +139,11 @@ exports.getGlobalUserByEmail = async email => {
|
||||||
if (email == null) {
|
if (email == null) {
|
||||||
throw "Must supply an email address to view"
|
throw "Must supply an email address to view"
|
||||||
}
|
}
|
||||||
const db = getGlobalDB()
|
|
||||||
|
|
||||||
try {
|
return queryGlobalView(ViewNames.USER_BY_EMAIL, {
|
||||||
let users = (
|
key: email.toLowerCase(),
|
||||||
await db.query(`database/${ViewNames.USER_BY_EMAIL}`, {
|
include_docs: true,
|
||||||
key: email.toLowerCase(),
|
})
|
||||||
include_docs: true,
|
|
||||||
})
|
|
||||||
).rows
|
|
||||||
users = users.map(user => user.doc)
|
|
||||||
return users.length <= 1 ? users[0] : users
|
|
||||||
} catch (err) {
|
|
||||||
if (err != null && err.name === "not_found") {
|
|
||||||
await createUserEmailView(db)
|
|
||||||
return exports.getGlobalUserByEmail(email)
|
|
||||||
} else {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.saveUser = async (
|
exports.saveUser = async (
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "1.0.76-alpha.2",
|
"version": "1.0.81-alpha.0",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||||
"@budibase/string-templates": "^1.0.72-alpha.0",
|
"@budibase/string-templates": "^1.0.81-alpha.0",
|
||||||
"@spectrum-css/actionbutton": "^1.0.1",
|
"@spectrum-css/actionbutton": "^1.0.1",
|
||||||
"@spectrum-css/actiongroup": "^1.0.1",
|
"@spectrum-css/actiongroup": "^1.0.1",
|
||||||
"@spectrum-css/avatar": "^3.0.2",
|
"@spectrum-css/avatar": "^3.0.2",
|
||||||
|
|
|
@ -61,5 +61,6 @@
|
||||||
<style>
|
<style>
|
||||||
.spectrum-Toast {
|
.spectrum-Toast {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Portal target=".modal-container">
|
<Portal target=".banner-container">
|
||||||
<div class="banner">
|
<div class="banner">
|
||||||
{#if $banner.message}
|
{#if $banner.message}
|
||||||
<div transition:fly={{ y: -30 }}>
|
<div transition:fly={{ y: -30 }}>
|
||||||
|
@ -25,18 +25,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.banner {
|
.banner {
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0;
|
|
||||||
z-index: 9999;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
gap: 10px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -47,7 +47,9 @@
|
||||||
<use xlink:href="#spectrum-css-icon-Dash100" />
|
<use xlink:href="#spectrum-css-icon-Dash100" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span class="spectrum-Checkbox-label">{text || ""}</span>
|
{#if text}
|
||||||
|
<span class="spectrum-Checkbox-label">{text}</span>
|
||||||
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -54,34 +54,43 @@
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKey} />
|
<svelte:window on:keydown={handleKey} />
|
||||||
|
|
||||||
<!-- These svelte if statements need to be defined like this. -->
|
{#if inline}
|
||||||
<!-- The modal transitions do not work if nested inside more than one "if" -->
|
{#if visible}
|
||||||
{#if visible && inline}
|
<div use:focusFirstInput class="spectrum-Modal inline is-open">
|
||||||
<div use:focusFirstInput class="spectrum-Modal inline is-open">
|
<slot />
|
||||||
<slot />
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{:else if visible}
|
{:else}
|
||||||
|
<!--
|
||||||
|
We cannot conditionally render the portal as this leads to a missing
|
||||||
|
insertion point when using nested modals. Therefore we just conditionally
|
||||||
|
render the content of the portal.
|
||||||
|
It still breaks the modal animation, but its better than soft bricking the
|
||||||
|
screen.
|
||||||
|
-->
|
||||||
<Portal target=".modal-container">
|
<Portal target=".modal-container">
|
||||||
<div
|
{#if visible}
|
||||||
class="spectrum-Underlay is-open"
|
<div
|
||||||
in:fade={{ duration: 200 }}
|
class="spectrum-Underlay is-open"
|
||||||
out:fade|local={{ duration: 200 }}
|
in:fade={{ duration: 200 }}
|
||||||
on:mousedown|self={cancel}
|
out:fade|local={{ duration: 200 }}
|
||||||
>
|
on:mousedown|self={cancel}
|
||||||
<div class="modal-wrapper" on:mousedown|self={cancel}>
|
>
|
||||||
<div class="modal-inner-wrapper" on:mousedown|self={cancel}>
|
<div class="modal-wrapper" on:mousedown|self={cancel}>
|
||||||
<slot name="outside" />
|
<div class="modal-inner-wrapper" on:mousedown|self={cancel}>
|
||||||
<div
|
<slot name="outside" />
|
||||||
use:focusFirstInput
|
<div
|
||||||
class="spectrum-Modal is-open"
|
use:focusFirstInput
|
||||||
in:fly={{ y: 30, duration: 200 }}
|
class="spectrum-Modal is-open"
|
||||||
out:fly|local={{ y: 30, duration: 200 }}
|
in:fly={{ y: 30, duration: 200 }}
|
||||||
>
|
out:fly|local={{ y: 30, duration: 200 }}
|
||||||
<slot />
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</Portal>
|
</Portal>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
@ -165,4 +165,8 @@
|
||||||
.secondary-action {
|
.secondary-action {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spectrum-Dialog-buttonGroup {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,9 +8,21 @@
|
||||||
export let allowEditRows = false
|
export let allowEditRows = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if allowSelectRows}
|
<div>
|
||||||
<Checkbox value={selected} />
|
{#if allowSelectRows}
|
||||||
{/if}
|
<Checkbox value={selected} />
|
||||||
{#if allowEditRows}
|
{/if}
|
||||||
<ActionButton size="S" on:click={onEdit}>Edit</ActionButton>
|
{#if allowEditRows}
|
||||||
{/if}
|
<ActionButton size="S" on:click={onEdit}>Edit</ActionButton>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
import SelectEditRenderer from "./SelectEditRenderer.svelte"
|
import SelectEditRenderer from "./SelectEditRenderer.svelte"
|
||||||
import { cloneDeep, deepGet } from "../helpers"
|
import { cloneDeep, deepGet } from "../helpers"
|
||||||
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
|
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
|
||||||
|
import Checkbox from "../Form/Checkbox.svelte"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The expected schema is our normal couch schemas for our tables.
|
* The expected schema is our normal couch schemas for our tables.
|
||||||
|
@ -31,7 +32,6 @@
|
||||||
export let allowEditRows = true
|
export let allowEditRows = true
|
||||||
export let allowEditColumns = true
|
export let allowEditColumns = true
|
||||||
export let selectedRows = []
|
export let selectedRows = []
|
||||||
export let editColumnTitle = "Edit"
|
|
||||||
export let customRenderers = []
|
export let customRenderers = []
|
||||||
export let disableSorting = false
|
export let disableSorting = false
|
||||||
export let autoSortColumns = true
|
export let autoSortColumns = true
|
||||||
|
@ -50,6 +50,8 @@
|
||||||
// Table state
|
// Table state
|
||||||
let height = 0
|
let height = 0
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
let checkboxStatus = false
|
||||||
|
|
||||||
$: schema = fixSchema(schema)
|
$: schema = fixSchema(schema)
|
||||||
$: if (!loading) loaded = true
|
$: if (!loading) loaded = true
|
||||||
$: fields = getFields(schema, showAutoColumns, autoSortColumns)
|
$: fields = getFields(schema, showAutoColumns, autoSortColumns)
|
||||||
|
@ -67,6 +69,16 @@
|
||||||
$: showEditColumn = allowEditRows || allowSelectRows
|
$: showEditColumn = allowEditRows || allowSelectRows
|
||||||
$: cellStyles = computeCellStyles(schema)
|
$: cellStyles = computeCellStyles(schema)
|
||||||
|
|
||||||
|
// Deselect the "select all" checkbox when the user navigates to a new page
|
||||||
|
$: {
|
||||||
|
let checkRowCount = rows.filter(o1 =>
|
||||||
|
selectedRows.some(o2 => o1._id === o2._id)
|
||||||
|
)
|
||||||
|
if (checkRowCount.length === 0) {
|
||||||
|
checkboxStatus = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fixSchema = schema => {
|
const fixSchema = schema => {
|
||||||
let fixedSchema = {}
|
let fixedSchema = {}
|
||||||
Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => {
|
Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => {
|
||||||
|
@ -197,13 +209,32 @@
|
||||||
if (!allowSelectRows) {
|
if (!allowSelectRows) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (selectedRows.includes(row)) {
|
if (selectedRows.some(selectedRow => selectedRow._id === row._id)) {
|
||||||
selectedRows = selectedRows.filter(selectedRow => selectedRow !== row)
|
selectedRows = selectedRows.filter(
|
||||||
|
selectedRow => selectedRow._id !== row._id
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
selectedRows = [...selectedRows, row]
|
selectedRows = [...selectedRows, row]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleSelectAll = e => {
|
||||||
|
const select = !!e.detail
|
||||||
|
if (select) {
|
||||||
|
// Add any rows which are not already in selected rows
|
||||||
|
rows.forEach(row => {
|
||||||
|
if (selectedRows.findIndex(x => x._id === row._id) === -1) {
|
||||||
|
selectedRows.push(row)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Remove any rows from selected rows that are in the current data set
|
||||||
|
selectedRows = selectedRows.filter(el =>
|
||||||
|
rows.every(f => f._id !== el._id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const computeCellStyles = schema => {
|
const computeCellStyles = schema => {
|
||||||
let styles = {}
|
let styles = {}
|
||||||
Object.keys(schema || {}).forEach(field => {
|
Object.keys(schema || {}).forEach(field => {
|
||||||
|
@ -244,7 +275,14 @@
|
||||||
<div
|
<div
|
||||||
class="spectrum-Table-headCell spectrum-Table-headCell--divider spectrum-Table-headCell--edit"
|
class="spectrum-Table-headCell spectrum-Table-headCell--divider spectrum-Table-headCell--edit"
|
||||||
>
|
>
|
||||||
{editColumnTitle || ""}
|
{#if allowSelectRows}
|
||||||
|
<Checkbox
|
||||||
|
bind:value={checkboxStatus}
|
||||||
|
on:change={toggleSelectAll}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
Edit
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
|
@ -302,11 +340,16 @@
|
||||||
{#if showEditColumn}
|
{#if showEditColumn}
|
||||||
<div
|
<div
|
||||||
class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
|
class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
|
||||||
|
on:click={e => {
|
||||||
|
toggleSelectRow(row)
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SelectEditRenderer
|
<SelectEditRenderer
|
||||||
data={row}
|
data={row}
|
||||||
selected={selectedRows.includes(row)}
|
selected={selectedRows.findIndex(
|
||||||
onToggleSelection={() => toggleSelectRow(row)}
|
selectedRow => selectedRow._id === row._id
|
||||||
|
) !== -1}
|
||||||
onEdit={e => editRow(e, row)}
|
onEdit={e => editRow(e, row)}
|
||||||
{allowSelectRows}
|
{allowSelectRows}
|
||||||
{allowEditRows}
|
{allowEditRows}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
{
|
{
|
||||||
"baseUrl": "http://localhost:10001",
|
"baseUrl": "http://localhost:4100",
|
||||||
"video": false,
|
"video": false,
|
||||||
"projectId": "bmbemn",
|
"projectId": "bmbemn",
|
||||||
"env": {
|
"env": {
|
||||||
"PORT": "10001",
|
"PORT": "4100",
|
||||||
|
"WORKER_PORT": "4200",
|
||||||
"JWT_SECRET": "test",
|
"JWT_SECRET": "test",
|
||||||
"HOST_IP": ""
|
"HOST_IP": ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ filterTests(['smoke', 'all'], () => {
|
||||||
cy.get(".spectrum-Button--cta").click()
|
cy.get(".spectrum-Button--cta").click()
|
||||||
})
|
})
|
||||||
cy.contains("Setup").click()
|
cy.contains("Setup").click()
|
||||||
cy.get(".spectrum-Picker-label").click()
|
cy.get(".spectrum-Picker-label").eq(1).click()
|
||||||
cy.contains("dog").click()
|
cy.contains("dog").click()
|
||||||
cy.get(".spectrum-Textfield-input")
|
cy.get(".spectrum-Textfield-input")
|
||||||
.first()
|
.first()
|
||||||
|
|
|
@ -27,10 +27,13 @@ filterTests(["smoke", "all"], () => {
|
||||||
it("updates a column on the table", () => {
|
it("updates a column on the table", () => {
|
||||||
cy.get(".title").click()
|
cy.get(".title").click()
|
||||||
cy.get(".spectrum-Table-editIcon > use").click()
|
cy.get(".spectrum-Table-editIcon > use").click()
|
||||||
cy.get("input").eq(1).type("updated", { force: true })
|
cy.get(".modal-inner-wrapper").within(() => {
|
||||||
|
|
||||||
|
cy.get("input").eq(0).type("updated", { force: true })
|
||||||
// Unset table display column
|
// Unset table display column
|
||||||
cy.get(".spectrum-Switch-input").eq(1).click()
|
cy.get(".spectrum-Switch-input").eq(1).click()
|
||||||
cy.contains("Save Column").click()
|
cy.contains("Save Column").click()
|
||||||
|
})
|
||||||
cy.contains("nameupdated ").should("contain", "nameupdated")
|
cy.contains("nameupdated ").should("contain", "nameupdated")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,43 +1,45 @@
|
||||||
import filterTests from "../../support/filterTests"
|
import filterTests from "../../support/filterTests"
|
||||||
|
|
||||||
filterTests(['smoke', 'all'], () => {
|
filterTests(["smoke", "all"], () => {
|
||||||
context("REST Datasource Testing", () => {
|
context("REST Datasource Testing", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
cy.createTestApp()
|
cy.createTestApp()
|
||||||
})
|
|
||||||
|
|
||||||
const datasource = "REST"
|
|
||||||
const restUrl = "https://api.openbrewerydb.org/breweries"
|
|
||||||
|
|
||||||
it("Should add REST data source with incorrect API", () => {
|
|
||||||
// Select REST data source
|
|
||||||
cy.selectExternalDatasource(datasource)
|
|
||||||
// Enter incorrect api & attempt to send query
|
|
||||||
cy.wait(500)
|
|
||||||
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
|
|
||||||
cy.intercept('**/preview').as('queryError')
|
|
||||||
cy.get("input").clear().type("random text")
|
|
||||||
cy.get(".spectrum-Button").contains("Send").click({ force: true })
|
|
||||||
// Intercept Request after button click & apply assertions
|
|
||||||
cy.wait("@queryError")
|
|
||||||
cy.get("@queryError").its('response.body')
|
|
||||||
.should('have.property', 'message', 'Invalid URL: http://random text?')
|
|
||||||
cy.get("@queryError").its('response.body')
|
|
||||||
.should('have.property', 'status', 400)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should add and configure a REST datasource", () => {
|
|
||||||
// Select REST datasource and create query
|
|
||||||
cy.selectExternalDatasource(datasource)
|
|
||||||
cy.wait(500)
|
|
||||||
// createRestQuery confirms query creation
|
|
||||||
cy.createRestQuery("GET", restUrl)
|
|
||||||
// Confirm status code response within REST datasource
|
|
||||||
cy.get(".spectrum-FieldLabel")
|
|
||||||
.contains("Status")
|
|
||||||
.children()
|
|
||||||
.should('contain', 200)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const datasource = "REST"
|
||||||
|
const restUrl = "https://api.openbrewerydb.org/breweries"
|
||||||
|
|
||||||
|
it("Should add REST data source with incorrect API", () => {
|
||||||
|
// Select REST data source
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
// Enter incorrect api & attempt to send query
|
||||||
|
cy.wait(500)
|
||||||
|
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
|
||||||
|
cy.intercept("**/preview").as("queryError")
|
||||||
|
cy.get("input").clear().type("random text")
|
||||||
|
cy.get(".spectrum-Button").contains("Send").click({ force: true })
|
||||||
|
// Intercept Request after button click & apply assertions
|
||||||
|
cy.wait("@queryError")
|
||||||
|
cy.get("@queryError")
|
||||||
|
.its("response.body")
|
||||||
|
.should("have.property", "message", "Invalid URL: http://random text?")
|
||||||
|
cy.get("@queryError")
|
||||||
|
.its("response.body")
|
||||||
|
.should("have.property", "status", 400)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add and configure a REST datasource", () => {
|
||||||
|
// Select REST datasource and create query
|
||||||
|
cy.selectExternalDatasource(datasource)
|
||||||
|
cy.wait(500)
|
||||||
|
// createRestQuery confirms query creation
|
||||||
|
cy.createRestQuery("GET", restUrl, "/breweries")
|
||||||
|
// Confirm status code response within REST datasource
|
||||||
|
cy.get(".spectrum-FieldLabel")
|
||||||
|
.contains("Status")
|
||||||
|
.children()
|
||||||
|
.should("contain", 200)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import filterTests from "../support/filterTests"
|
import filterTests from "../support/filterTests"
|
||||||
|
|
||||||
filterTests(['smoke', 'all'], () => {
|
filterTests(["smoke", "all"], () => {
|
||||||
context("Query Level Transformers", () => {
|
context("Query Level Transformers", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.login()
|
cy.login()
|
||||||
|
@ -9,54 +9,60 @@ filterTests(['smoke', 'all'], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should write a transformer function", () => {
|
it("should write a transformer function", () => {
|
||||||
// Add REST datasource - contains API for breweries
|
// Add REST datasource - contains API for breweries
|
||||||
const datasource = "REST"
|
const datasource = "REST"
|
||||||
const restUrl = "https://api.openbrewerydb.org/breweries"
|
const restUrl = "https://api.openbrewerydb.org/breweries"
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
cy.createRestQuery("GET", restUrl)
|
cy.createRestQuery("GET", restUrl, "/breweries")
|
||||||
cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click()
|
cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click()
|
||||||
// Get Transformer Function from file
|
// Get Transformer Function from file
|
||||||
cy.readFile("cypress/support/queryLevelTransformerFunction.js").then((transformerFunction) => {
|
cy.readFile("cypress/support/queryLevelTransformerFunction.js").then(
|
||||||
|
transformerFunction => {
|
||||||
cy.get(".CodeMirror textarea")
|
cy.get(".CodeMirror textarea")
|
||||||
// Highlight current text and overwrite with file contents
|
// Highlight current text and overwrite with file contents
|
||||||
.type(Cypress.platform === 'darwin' ? '{cmd}a' : '{ctrl}a', { force: true })
|
.type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", {
|
||||||
.type(transformerFunction, { parseSpecialCharSequences: false })
|
force: true,
|
||||||
})
|
})
|
||||||
// Send Query
|
.type(transformerFunction, { parseSpecialCharSequences: false })
|
||||||
cy.intercept('**/queries/preview').as('query')
|
}
|
||||||
cy.get(".spectrum-Button").contains("Send").click({ force: true })
|
)
|
||||||
cy.wait("@query")
|
// Send Query
|
||||||
// Assert against Status Code, body, & body rows
|
cy.intercept("**/queries/preview").as("query")
|
||||||
cy.get("@query").its('response.statusCode')
|
cy.get(".spectrum-Button").contains("Send").click({ force: true })
|
||||||
.should('eq', 200)
|
cy.wait("@query")
|
||||||
cy.get("@query").its('response.body').should('not.be.empty')
|
// Assert against Status Code, body, & body rows
|
||||||
cy.get("@query").its('response.body.rows').should('not.be.empty')
|
cy.get("@query").its("response.statusCode").should("eq", 200)
|
||||||
})
|
cy.get("@query").its("response.body").should("not.be.empty")
|
||||||
|
cy.get("@query").its("response.body.rows").should("not.be.empty")
|
||||||
|
})
|
||||||
|
|
||||||
it("should add data to the previous query", () => {
|
it("should add data to the previous query", () => {
|
||||||
// Add REST datasource - contains API for breweries
|
// Add REST datasource - contains API for breweries
|
||||||
const datasource = "REST"
|
const datasource = "REST"
|
||||||
const restUrl = "https://api.openbrewerydb.org/breweries"
|
const restUrl = "https://api.openbrewerydb.org/breweries"
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
cy.createRestQuery("GET", restUrl)
|
cy.createRestQuery("GET", restUrl, "/breweries")
|
||||||
cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click()
|
cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click()
|
||||||
// Get Transformer Function with Data from file
|
// Get Transformer Function with Data from file
|
||||||
cy.readFile("cypress/support/queryLevelTransformerFunctionWithData.js").then((transformerFunction) => {
|
cy.readFile(
|
||||||
|
"cypress/support/queryLevelTransformerFunctionWithData.js"
|
||||||
|
).then(transformerFunction => {
|
||||||
//console.log(transformerFunction[1])
|
//console.log(transformerFunction[1])
|
||||||
cy.get(".CodeMirror textarea")
|
cy.get(".CodeMirror textarea")
|
||||||
// Highlight current text and overwrite with file contents
|
// Highlight current text and overwrite with file contents
|
||||||
.type(Cypress.platform === 'darwin' ? '{cmd}a' : '{ctrl}a', { force: true })
|
.type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", {
|
||||||
.type(transformerFunction, { parseSpecialCharSequences: false })
|
force: true,
|
||||||
|
})
|
||||||
|
.type(transformerFunction, { parseSpecialCharSequences: false })
|
||||||
})
|
})
|
||||||
// Send Query
|
// Send Query
|
||||||
cy.intercept('**/queries/preview').as('query')
|
cy.intercept("**/queries/preview").as("query")
|
||||||
cy.get(".spectrum-Button").contains("Send").click({ force: true })
|
cy.get(".spectrum-Button").contains("Send").click({ force: true })
|
||||||
cy.wait("@query")
|
cy.wait("@query")
|
||||||
// Assert against Status Code, body, & body rows
|
// Assert against Status Code, body, & body rows
|
||||||
cy.get("@query").its('response.statusCode')
|
cy.get("@query").its("response.statusCode").should("eq", 200)
|
||||||
.should('eq', 200)
|
cy.get("@query").its("response.body").should("not.be.empty")
|
||||||
cy.get("@query").its('response.body').should('not.be.empty')
|
cy.get("@query").its("response.body.rows").should("not.be.empty")
|
||||||
cy.get("@query").its('response.body.rows').should('not.be.empty')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should run an invalid query within the transformer section", () => {
|
it("should run an invalid query within the transformer section", () => {
|
||||||
|
@ -64,52 +70,70 @@ filterTests(['smoke', 'all'], () => {
|
||||||
const datasource = "REST"
|
const datasource = "REST"
|
||||||
const restUrl = "https://api.openbrewerydb.org/breweries"
|
const restUrl = "https://api.openbrewerydb.org/breweries"
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
cy.createRestQuery("GET", restUrl)
|
cy.createRestQuery("GET", restUrl, "/breweries")
|
||||||
cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click()
|
cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click()
|
||||||
// Clear the code box and add "test"
|
// Clear the code box and add "test"
|
||||||
cy.get(".CodeMirror textarea")
|
cy.get(".CodeMirror textarea")
|
||||||
.type(Cypress.platform === 'darwin' ? '{cmd}a' : '{ctrl}a', { force: true })
|
.type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", {
|
||||||
.type("test")
|
force: true,
|
||||||
|
})
|
||||||
|
.type("test")
|
||||||
// Run Query and intercept
|
// Run Query and intercept
|
||||||
cy.intercept('**/preview').as('queryError')
|
cy.intercept("**/preview").as("queryError")
|
||||||
cy.get(".spectrum-Button").contains("Send").click({ force: true })
|
cy.get(".spectrum-Button").contains("Send").click({ force: true })
|
||||||
cy.wait("@queryError")
|
cy.wait("@queryError")
|
||||||
cy.wait(500)
|
cy.wait(500)
|
||||||
// Assert against message and status for the query error
|
// Assert against message and status for the query error
|
||||||
cy.get("@queryError").its('response.body').should('have.property', 'message', "test is not defined")
|
cy.get("@queryError")
|
||||||
cy.get("@queryError").its('response.body').should('have.property', 'status', 400)
|
.its("response.body")
|
||||||
|
.should("have.property", "message", "test is not defined")
|
||||||
|
cy.get("@queryError")
|
||||||
|
.its("response.body")
|
||||||
|
.should("have.property", "status", 400)
|
||||||
})
|
})
|
||||||
|
|
||||||
xit("should run an invalid query via POST request", () => {
|
xit("should run an invalid query via POST request", () => {
|
||||||
// POST request with transformer as null
|
// POST request with transformer as null
|
||||||
cy.request({method: 'POST',
|
cy.request({
|
||||||
url: `${Cypress.config().baseUrl}/api/queries/`,
|
method: "POST",
|
||||||
body: {fields : {"headers":{},"queryString":null,"path":null},
|
url: `${Cypress.config().baseUrl}/api/queries/`,
|
||||||
parameters : [],
|
body: {
|
||||||
schema : {},
|
fields: { headers: {}, queryString: null, path: null },
|
||||||
name : "test",
|
parameters: [],
|
||||||
queryVerb : "read",
|
schema: {},
|
||||||
transformer : null,
|
name: "test",
|
||||||
datasourceId: "test"},
|
queryVerb: "read",
|
||||||
// Expected 400 error - Transformer must be a string
|
transformer: null,
|
||||||
failOnStatusCode: false}).then((response) => {
|
datasourceId: "test",
|
||||||
|
},
|
||||||
|
// Expected 400 error - Transformer must be a string
|
||||||
|
failOnStatusCode: false,
|
||||||
|
}).then(response => {
|
||||||
expect(response.status).to.equal(400)
|
expect(response.status).to.equal(400)
|
||||||
expect(response.body.message).to.include('Invalid body - "transformer" must be a string')
|
expect(response.body.message).to.include(
|
||||||
|
'Invalid body - "transformer" must be a string'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
xit("should run an empty query", () => {
|
xit("should run an empty query", () => {
|
||||||
// POST request with Transformer as an empty string
|
// POST request with Transformer as an empty string
|
||||||
cy.request({method: 'POST',
|
cy.request({
|
||||||
url: `${Cypress.config().baseUrl}/api/queries/preview`,
|
method: "POST",
|
||||||
body: {fields : {"headers":{},"queryString":null,"path":null},
|
url: `${Cypress.config().baseUrl}/api/queries/preview`,
|
||||||
queryVerb : "read",
|
body: {
|
||||||
transformer : "",
|
fields: { headers: {}, queryString: null, path: null },
|
||||||
datasourceId: "test"},
|
queryVerb: "read",
|
||||||
// Expected 400 error - Transformer is not allowed to be empty
|
transformer: "",
|
||||||
failOnStatusCode: false}).then((response) => {
|
datasourceId: "test",
|
||||||
|
},
|
||||||
|
// Expected 400 error - Transformer is not allowed to be empty
|
||||||
|
failOnStatusCode: false,
|
||||||
|
}).then(response => {
|
||||||
expect(response.status).to.equal(400)
|
expect(response.status).to.equal(400)
|
||||||
expect(response.body.message).to.include('Invalid body - "transformer" is not allowed to be empty')
|
expect(response.body.message).to.include(
|
||||||
|
'Invalid body - "transformer" is not allowed to be empty'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,17 +4,17 @@ const path = require("path")
|
||||||
const tmpdir = path.join(require("os").tmpdir(), ".budibase")
|
const tmpdir = path.join(require("os").tmpdir(), ".budibase")
|
||||||
|
|
||||||
// normal development system
|
// normal development system
|
||||||
const WORKER_PORT = "10002"
|
const SERVER_PORT = cypressConfig.env.PORT
|
||||||
const MAIN_PORT = cypressConfig.env.PORT
|
const WORKER_PORT = cypressConfig.env.WORKER_PORT
|
||||||
|
|
||||||
process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE"
|
process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE"
|
||||||
process.env.NODE_ENV = "cypress"
|
process.env.NODE_ENV = "cypress"
|
||||||
process.env.ENABLE_ANALYTICS = "false"
|
process.env.ENABLE_ANALYTICS = "false"
|
||||||
process.env.PORT = MAIN_PORT
|
|
||||||
process.env.JWT_SECRET = cypressConfig.env.JWT_SECRET
|
process.env.JWT_SECRET = cypressConfig.env.JWT_SECRET
|
||||||
process.env.COUCH_URL = `leveldb://${tmpdir}/.data/`
|
process.env.COUCH_URL = `leveldb://${tmpdir}/.data/`
|
||||||
process.env.SELF_HOSTED = 1
|
process.env.SELF_HOSTED = 1
|
||||||
process.env.WORKER_URL = "http://localhost:10002/"
|
process.env.WORKER_URL = `http://localhost:${WORKER_PORT}/`
|
||||||
process.env.APPS_URL = `http://localhost:${MAIN_PORT}/`
|
process.env.APPS_URL = `http://localhost:${SERVER_PORT}/`
|
||||||
process.env.MINIO_URL = `http://localhost:4004`
|
process.env.MINIO_URL = `http://localhost:4004`
|
||||||
process.env.MINIO_ACCESS_KEY = "budibase"
|
process.env.MINIO_ACCESS_KEY = "budibase"
|
||||||
process.env.MINIO_SECRET_KEY = "budibase"
|
process.env.MINIO_SECRET_KEY = "budibase"
|
||||||
|
@ -33,11 +33,14 @@ exports.run = (
|
||||||
// require("dotenv").config({ path: resolve(dir, ".env") })
|
// require("dotenv").config({ path: resolve(dir, ".env") })
|
||||||
// don't make this a variable or top level require
|
// don't make this a variable or top level require
|
||||||
// it will cause environment module to be loaded prematurely
|
// it will cause environment module to be loaded prematurely
|
||||||
require(serverLoc)
|
|
||||||
|
// override the port with the worker port temporarily
|
||||||
process.env.PORT = WORKER_PORT
|
process.env.PORT = WORKER_PORT
|
||||||
require(workerLoc)
|
require(workerLoc)
|
||||||
// reload main port for rest of system
|
|
||||||
process.env.PORT = MAIN_PORT
|
// override the port with the server port
|
||||||
|
process.env.PORT = SERVER_PORT
|
||||||
|
require(serverLoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
|
|
|
@ -39,7 +39,7 @@ Cypress.Commands.add("createApp", name => {
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
||||||
cy.wait(5000)
|
cy.wait(10000)
|
||||||
})
|
})
|
||||||
cy.createTable("Cypress Tests", true)
|
cy.createTable("Cypress Tests", true)
|
||||||
})
|
})
|
||||||
|
@ -116,10 +116,10 @@ Cypress.Commands.add("createTestTableWithData", () => {
|
||||||
Cypress.Commands.add("createTable", (tableName, initialTable) => {
|
Cypress.Commands.add("createTable", (tableName, initialTable) => {
|
||||||
if (!initialTable) {
|
if (!initialTable) {
|
||||||
cy.navigateToDataSection()
|
cy.navigateToDataSection()
|
||||||
cy.get(".add-button").click()
|
cy.get(`[data-cy="new-table"]`).click()
|
||||||
}
|
}
|
||||||
cy.wait(7000)
|
cy.wait(5000)
|
||||||
cy.get(".spectrum-Modal")
|
cy.get(".spectrum-Dialog-grid")
|
||||||
.contains("Budibase DB")
|
.contains("Budibase DB")
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -172,17 +172,19 @@ Cypress.Commands.add("addRow", values => {
|
||||||
|
|
||||||
Cypress.Commands.add("addRowMultiValue", values => {
|
Cypress.Commands.add("addRowMultiValue", values => {
|
||||||
cy.contains("Create row").click()
|
cy.contains("Create row").click()
|
||||||
cy.get(".spectrum-Form-itemField")
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
.click()
|
cy.get(".spectrum-Form-itemField")
|
||||||
.then(() => {
|
.click()
|
||||||
cy.get(".spectrum-Popover").within(() => {
|
.then(() => {
|
||||||
for (let i = 0; i < values.length; i++) {
|
cy.get(".spectrum-Popover").within(() => {
|
||||||
cy.get(".spectrum-Menu-item").eq(i).click()
|
for (let i = 0; i < values.length; i++) {
|
||||||
}
|
cy.get(".spectrum-Menu-item").eq(i).click()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
cy.get(".spectrum-Dialog-grid").click("top")
|
||||||
|
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
||||||
})
|
})
|
||||||
cy.get(".spectrum-Dialog-grid").click("top")
|
})
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createUser", email => {
|
Cypress.Commands.add("createUser", email => {
|
||||||
|
@ -435,7 +437,7 @@ Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createRestQuery", (method, restUrl) => {
|
Cypress.Commands.add("createRestQuery", (method, restUrl, queryPrettyName) => {
|
||||||
// addExternalDatasource should be called prior to this
|
// addExternalDatasource should be called prior to this
|
||||||
// Configures REST datasource & sends query
|
// Configures REST datasource & sends query
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
|
@ -450,5 +452,5 @@ Cypress.Commands.add("createRestQuery", (method, restUrl) => {
|
||||||
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||||
cy.get(".hierarchy-items-container")
|
cy.get(".hierarchy-items-container")
|
||||||
.should("contain", method)
|
.should("contain", method)
|
||||||
.and("contain", restUrl)
|
.and("contain", queryPrettyName)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.0.76-alpha.2",
|
"version": "1.0.81-alpha.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -11,12 +11,13 @@
|
||||||
"rollup": "rollup -c -w",
|
"rollup": "rollup -c -w",
|
||||||
"cy:setup": "ts-node ./cypress/ts/setup.ts",
|
"cy:setup": "ts-node ./cypress/ts/setup.ts",
|
||||||
"cy:setup:ci": "node ./cypress/setup.js",
|
"cy:setup:ci": "node ./cypress/setup.js",
|
||||||
"cy:run": "xvfb-run cypress run --headed --browser chrome",
|
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
"cy:run:ci": "cypress run --record",
|
"cy:run": "cypress run",
|
||||||
"cy:test": "start-server-and-test cy:setup http://localhost:10001/builder cy:run",
|
"cy:run:ci": "xvfb-run cypress run --headed --browser chrome",
|
||||||
"cy:ci": "start-server-and-test cy:setup:ci http://localhost:10001/builder cy:run",
|
"cy:test": "start-server-and-test cy:setup http://localhost:4100/builder cy:run",
|
||||||
"cy:debug": "start-server-and-test cy:setup http://localhost:10001/builder cy:open"
|
"cy:ci": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci",
|
||||||
|
"cy:debug": "start-server-and-test cy:setup http://localhost:4100/builder cy:open",
|
||||||
|
"cy:debug:ci": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:open"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"globals": {
|
"globals": {
|
||||||
|
@ -64,10 +65,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.76-alpha.2",
|
"@budibase/bbui": "^1.0.81-alpha.0",
|
||||||
"@budibase/client": "^1.0.76-alpha.2",
|
"@budibase/client": "^1.0.81-alpha.0",
|
||||||
"@budibase/frontend-core": "^1.0.76-alpha.2",
|
"@budibase/frontend-core": "^1.0.81-alpha.0",
|
||||||
"@budibase/string-templates": "^1.0.76-alpha.2",
|
"@budibase/string-templates": "^1.0.81-alpha.0",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
|
|
@ -8,8 +8,10 @@
|
||||||
const queryHandler = { parse, stringify }
|
const queryHandler = { parse, stringify }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<NotificationDisplay />
|
<div class="banner-container" />
|
||||||
<BannerDisplay />
|
<BannerDisplay />
|
||||||
|
|
||||||
|
<NotificationDisplay />
|
||||||
<Router {routes} config={{ queryHandler }} />
|
<Router {routes} config={{ queryHandler }} />
|
||||||
<div class="modal-container" />
|
<div class="modal-container" />
|
||||||
<HelpIcon />
|
<HelpIcon />
|
||||||
|
|
|
@ -32,12 +32,14 @@ export const getBindableProperties = (asset, componentId) => {
|
||||||
const urlBindings = getUrlBindings(asset)
|
const urlBindings = getUrlBindings(asset)
|
||||||
const deviceBindings = getDeviceBindings()
|
const deviceBindings = getDeviceBindings()
|
||||||
const stateBindings = getStateBindings()
|
const stateBindings = getStateBindings()
|
||||||
|
const selectedRowsBindings = getSelectedRowsBindings(asset)
|
||||||
return [
|
return [
|
||||||
...contextBindings,
|
...contextBindings,
|
||||||
...urlBindings,
|
...urlBindings,
|
||||||
...stateBindings,
|
...stateBindings,
|
||||||
...userBindings,
|
...userBindings,
|
||||||
...deviceBindings,
|
...deviceBindings,
|
||||||
|
...selectedRowsBindings,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,6 +317,44 @@ const getDeviceBindings = () => {
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all selected rows bindings for tables in the current asset.
|
||||||
|
*/
|
||||||
|
const getSelectedRowsBindings = asset => {
|
||||||
|
let bindings = []
|
||||||
|
if (get(store).clientFeatures?.rowSelection) {
|
||||||
|
// Add bindings for table components
|
||||||
|
let tables = findAllMatchingComponents(asset?.props, component =>
|
||||||
|
component._component.endsWith("table")
|
||||||
|
)
|
||||||
|
const safeState = makePropSafe("rowSelection")
|
||||||
|
bindings = bindings.concat(
|
||||||
|
tables.map(table => ({
|
||||||
|
type: "context",
|
||||||
|
runtimeBinding: `${safeState}.${makePropSafe(table._id)}.${makePropSafe(
|
||||||
|
"selectedRows"
|
||||||
|
)}`,
|
||||||
|
readableBinding: `${table._instanceName}.Selected rows`,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add bindings for table blocks
|
||||||
|
let tableBlocks = findAllMatchingComponents(asset?.props, component =>
|
||||||
|
component._component.endsWith("tableblock")
|
||||||
|
)
|
||||||
|
bindings = bindings.concat(
|
||||||
|
tableBlocks.map(block => ({
|
||||||
|
type: "context",
|
||||||
|
runtimeBinding: `${safeState}.${makePropSafe(
|
||||||
|
block._id + "-table"
|
||||||
|
)}.${makePropSafe("selectedRows")}`,
|
||||||
|
readableBinding: `${block._instanceName}.Selected rows`,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all state bindings that are globally available.
|
* Gets all state bindings that are globally available.
|
||||||
*/
|
*/
|
||||||
|
@ -597,14 +637,9 @@ const buildFormSchema = component => {
|
||||||
* in the app.
|
* in the app.
|
||||||
*/
|
*/
|
||||||
export const getAllStateVariables = () => {
|
export const getAllStateVariables = () => {
|
||||||
// Get all component containing assets
|
|
||||||
let allAssets = []
|
|
||||||
allAssets = allAssets.concat(get(store).layouts || [])
|
|
||||||
allAssets = allAssets.concat(get(store).screens || [])
|
|
||||||
|
|
||||||
// Find all button action settings in all components
|
// Find all button action settings in all components
|
||||||
let eventSettings = []
|
let eventSettings = []
|
||||||
allAssets.forEach(asset => {
|
getAllAssets().forEach(asset => {
|
||||||
findAllMatchingComponents(asset.props, component => {
|
findAllMatchingComponents(asset.props, component => {
|
||||||
const settings = getComponentSettings(component._component)
|
const settings = getComponentSettings(component._component)
|
||||||
settings
|
settings
|
||||||
|
@ -635,6 +670,15 @@ export const getAllStateVariables = () => {
|
||||||
return Array.from(bindingSet)
|
return Array.from(bindingSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getAllAssets = () => {
|
||||||
|
// Get all component containing assets
|
||||||
|
let allAssets = []
|
||||||
|
allAssets = allAssets.concat(get(store).layouts || [])
|
||||||
|
allAssets = allAssets.concat(get(store).screens || [])
|
||||||
|
|
||||||
|
return allAssets
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recurses the input object to remove any instances of bindings.
|
* Recurses the input object to remove any instances of bindings.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -57,6 +57,7 @@ const automationActions = store => ({
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
save: async automation => {
|
save: async automation => {
|
||||||
const response = await API.updateAutomation(automation)
|
const response = await API.updateAutomation(automation)
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
|
@ -130,6 +131,12 @@ const automationActions = store => ({
|
||||||
name: block.name,
|
name: block.name,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
toggleFieldControl: value => {
|
||||||
|
store.update(state => {
|
||||||
|
state.selectedBlock.rowControl = value
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
},
|
||||||
deleteAutomationBlock: block => {
|
deleteAutomationBlock: block => {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
const idx =
|
const idx =
|
||||||
|
|
|
@ -41,6 +41,7 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
intelligentLoading: false,
|
intelligentLoading: false,
|
||||||
deviceAwareness: false,
|
deviceAwareness: false,
|
||||||
state: false,
|
state: false,
|
||||||
|
rowSelection: false,
|
||||||
customThemes: false,
|
customThemes: false,
|
||||||
devicePreview: false,
|
devicePreview: false,
|
||||||
messagePassing: false,
|
messagePassing: false,
|
||||||
|
|
|
@ -3,14 +3,8 @@
|
||||||
import Flowchart from "./FlowChart/FlowChart.svelte"
|
import Flowchart from "./FlowChart/FlowChart.svelte"
|
||||||
|
|
||||||
$: automation = $automationStore.selectedAutomation?.automation
|
$: automation = $automationStore.selectedAutomation?.automation
|
||||||
function onSelect(block) {
|
|
||||||
automationStore.update(state => {
|
|
||||||
state.selectedBlock = block
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if automation}
|
{#if automation}
|
||||||
<Flowchart {automation} {onSelect} />
|
<Flowchart {automation} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
|
||||||
export let automation
|
export let automation
|
||||||
export let onSelect
|
|
||||||
let testDataModal
|
let testDataModal
|
||||||
let blocks
|
let blocks
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<div class="subtitle">
|
<div class="subtitle">
|
||||||
<Heading size="S">{automation.name}</Heading>
|
<Heading size="S">{automation.name}</Heading>
|
||||||
<div style="display:flex;">
|
<div style="display:flex; align-items: center;">
|
||||||
<div class="iconPadding">
|
<div class="iconPadding">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -72,7 +72,7 @@
|
||||||
animate:flip={{ duration: 500 }}
|
animate:flip={{ duration: 500 }}
|
||||||
in:fly|local={{ x: 500, duration: 1500 }}
|
in:fly|local={{ x: 500, duration: 1500 }}
|
||||||
>
|
>
|
||||||
<FlowItem {testDataModal} {onSelect} {block} />
|
<FlowItem {testDataModal} {block} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
Button,
|
Button,
|
||||||
StatusLight,
|
StatusLight,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
|
Select,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||||
|
@ -18,7 +19,6 @@
|
||||||
import ActionModal from "./ActionModal.svelte"
|
import ActionModal from "./ActionModal.svelte"
|
||||||
import { externalActions } from "./ExternalActions"
|
import { externalActions } from "./ExternalActions"
|
||||||
|
|
||||||
export let onSelect
|
|
||||||
export let block
|
export let block
|
||||||
export let testDataModal
|
export let testDataModal
|
||||||
let selected
|
let selected
|
||||||
|
@ -28,6 +28,10 @@
|
||||||
let setupToggled
|
let setupToggled
|
||||||
let blockComplete
|
let blockComplete
|
||||||
|
|
||||||
|
$: rowControl = $automationStore.selectedAutomation.automation.rowControl
|
||||||
|
$: showBindingPicker =
|
||||||
|
block.stepId === "CREATE_ROW" || block.stepId === "UPDATE_ROW"
|
||||||
|
|
||||||
$: testResult = $automationStore.selectedAutomation.testResults?.steps.filter(
|
$: testResult = $automationStore.selectedAutomation.testResults?.steps.filter(
|
||||||
step => (block.id ? step.id === block.id : step.stepId === block.stepId)
|
step => (block.id ? step.id === block.id : step.stepId === block.stepId)
|
||||||
)
|
)
|
||||||
|
@ -44,12 +48,6 @@
|
||||||
$automationStore.selectedAutomation?.automation?.definition?.steps.length +
|
$automationStore.selectedAutomation?.automation?.definition?.steps.length +
|
||||||
1
|
1
|
||||||
|
|
||||||
// Logic for hiding / showing the add button.first we check if it has a child
|
|
||||||
// then we check to see whether its inputs have been commpleted
|
|
||||||
$: disableAddButton = isTrigger
|
|
||||||
? $automationStore.selectedAutomation?.automation?.definition?.steps
|
|
||||||
.length > 0
|
|
||||||
: !isTrigger && steps.length - blockIdx > 1
|
|
||||||
$: hasCompletedInputs = Object.keys(
|
$: hasCompletedInputs = Object.keys(
|
||||||
block.schema?.inputs?.properties || {}
|
block.schema?.inputs?.properties || {}
|
||||||
).every(x => block?.inputs[x])
|
).every(x => block?.inputs[x])
|
||||||
|
@ -64,6 +62,26 @@
|
||||||
notifications.error("Error saving notification")
|
notifications.error("Error saving notification")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function toggleFieldControl(evt) {
|
||||||
|
onSelect(block)
|
||||||
|
let rowControl
|
||||||
|
if (evt.detail === "Use values") {
|
||||||
|
rowControl = false
|
||||||
|
} else {
|
||||||
|
rowControl = true
|
||||||
|
}
|
||||||
|
automationStore.actions.toggleFieldControl(rowControl)
|
||||||
|
automationStore.actions.save(
|
||||||
|
$automationStore.selectedAutomation?.automation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSelect(block) {
|
||||||
|
await automationStore.update(state => {
|
||||||
|
state.selectedBlock = block
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -126,15 +144,33 @@
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
<div class="splitHeader">
|
<div class="splitHeader">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
on:click={() => (setupToggled = !setupToggled)}
|
on:click={() => {
|
||||||
|
onSelect(block)
|
||||||
|
setupToggled = !setupToggled
|
||||||
|
}}
|
||||||
quiet
|
quiet
|
||||||
icon={setupToggled ? "ChevronDown" : "ChevronRight"}
|
icon={setupToggled ? "ChevronDown" : "ChevronRight"}
|
||||||
>
|
>
|
||||||
<Detail size="S">Setup</Detail>
|
<Detail size="S">Setup</Detail>
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
{#if !isTrigger}
|
{#if !isTrigger}
|
||||||
<div on:click={() => deleteStep()}>
|
<div class="block-options">
|
||||||
<Icon name="DeleteOutline" />
|
{#if showBindingPicker}
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
on:change={toggleFieldControl}
|
||||||
|
quiet
|
||||||
|
defaultValue="Use values"
|
||||||
|
autoWidth
|
||||||
|
value={rowControl ? "Use bindings" : "Use values"}
|
||||||
|
options={["Use values", "Use bindings"]}
|
||||||
|
placeholder={null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="delete-padding" on:click={() => deleteStep()}>
|
||||||
|
<Icon name="DeleteOutline" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -180,6 +216,13 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.delete-padding {
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
.block-options {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
.center-items {
|
.center-items {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -227,6 +227,7 @@
|
||||||
/>
|
/>
|
||||||
{:else if value.customType === "row"}
|
{:else if value.customType === "row"}
|
||||||
<RowSelector
|
<RowSelector
|
||||||
|
{block}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
|
|
@ -1,26 +1,31 @@
|
||||||
<script>
|
<script>
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import {
|
import { Select } from "@budibase/bbui"
|
||||||
Select,
|
|
||||||
Toggle,
|
|
||||||
DatePicker,
|
|
||||||
Multiselect,
|
|
||||||
TextArea,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
|
||||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
|
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let bindings
|
export let bindings
|
||||||
|
export let block
|
||||||
|
|
||||||
let table
|
let table
|
||||||
let schemaFields
|
let schemaFields
|
||||||
|
|
||||||
|
let placeholders = {
|
||||||
|
number: 10,
|
||||||
|
boolean: "true",
|
||||||
|
datetime: "2022-02-16T12:00:00.000Z ",
|
||||||
|
options: "1",
|
||||||
|
array: "1 2 3 4",
|
||||||
|
link: "ro_ta_123_456",
|
||||||
|
longform: "long form text",
|
||||||
|
}
|
||||||
|
$: rowControl = block.rowControl
|
||||||
$: {
|
$: {
|
||||||
table = $tables.list.find(table => table._id === value?.tableId)
|
table = $tables.list.find(table => table._id === value?.tableId)
|
||||||
schemaFields = Object.entries(table?.schema ?? {})
|
schemaFields = Object.entries(table?.schema ?? {})
|
||||||
|
@ -37,18 +42,48 @@
|
||||||
dispatch("change", value)
|
dispatch("change", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChange = (e, field) => {
|
const coerce = (value, type) => {
|
||||||
value[field] = e.detail
|
if (type === "boolean") {
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return value === "true"
|
||||||
|
}
|
||||||
|
if (type === "number") {
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return Number(value)
|
||||||
|
}
|
||||||
|
if (type === "options") {
|
||||||
|
return [value]
|
||||||
|
}
|
||||||
|
if (type === "array") {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return value.split(",").map(x => x.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "link") {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return [value]
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = (e, field, type) => {
|
||||||
|
value[field] = coerce(e.detail, type)
|
||||||
dispatch("change", value)
|
dispatch("change", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure any nullish tableId values get set to empty string so
|
// Ensure any nullish tableId values get set to empty string so
|
||||||
// that the select works
|
// that the select works
|
||||||
$: if (value?.tableId == null) value = { tableId: "" }
|
$: if (value?.tableId == null) value = { tableId: "" }
|
||||||
|
|
||||||
function schemaHasOptions(schema) {
|
|
||||||
return !!schema.constraints?.inclusion?.length
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
|
@ -62,55 +97,46 @@
|
||||||
<div class="schema-fields">
|
<div class="schema-fields">
|
||||||
{#each schemaFields as [field, schema]}
|
{#each schemaFields as [field, schema]}
|
||||||
{#if !schema.autocolumn}
|
{#if !schema.autocolumn}
|
||||||
{#if schemaHasOptions(schema) && schema.type !== "array"}
|
{#if schema.type !== "attachment"}
|
||||||
<Select
|
|
||||||
on:change={e => onChange(e, field)}
|
|
||||||
label={field}
|
|
||||||
value={value[field]}
|
|
||||||
options={schema.constraints.inclusion}
|
|
||||||
/>
|
|
||||||
{:else if schema.type === "datetime"}
|
|
||||||
<DatePicker
|
|
||||||
label={field}
|
|
||||||
value={value[field]}
|
|
||||||
on:change={e => onChange(e, field)}
|
|
||||||
/>
|
|
||||||
{:else if schema.type === "boolean"}
|
|
||||||
<Toggle
|
|
||||||
text={field}
|
|
||||||
value={value[field]}
|
|
||||||
on:change={e => onChange(e, field)}
|
|
||||||
/>
|
|
||||||
{:else if schema.type === "array"}
|
|
||||||
<Multiselect
|
|
||||||
bind:value={value[field]}
|
|
||||||
label={field}
|
|
||||||
options={schema.constraints.inclusion}
|
|
||||||
/>
|
|
||||||
{:else if schema.type === "longform"}
|
|
||||||
<TextArea label={field} bind:value={value[field]} />
|
|
||||||
{:else if schema.type === "link"}
|
|
||||||
<LinkedRowSelector bind:linkedRows={value[field]} {schema} />
|
|
||||||
{:else if schema.type === "string" || schema.type === "number"}
|
|
||||||
{#if $automationStore.selectedAutomation.automation.testData}
|
{#if $automationStore.selectedAutomation.automation.testData}
|
||||||
<ModalBindableInput
|
{#if !rowControl}
|
||||||
value={value[field]}
|
<RowSelectorTypes
|
||||||
panel={AutomationBindingPanel}
|
{field}
|
||||||
label={field}
|
{schema}
|
||||||
type={value.customType}
|
{bindings}
|
||||||
on:change={e => onChange(e, field)}
|
{value}
|
||||||
{bindings}
|
{onChange}
|
||||||
/>
|
/>
|
||||||
|
{:else}
|
||||||
|
<DrawerBindableInput
|
||||||
|
placeholder={placeholders[schema.type]}
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
value={Array.isArray(value[field])
|
||||||
|
? value[field].join(" ")
|
||||||
|
: value[field]}
|
||||||
|
on:change={e => onChange(e, field, schema.type)}
|
||||||
|
label={field}
|
||||||
|
type="string"
|
||||||
|
{bindings}
|
||||||
|
fillWidth={true}
|
||||||
|
allowJS={true}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{:else if !rowControl}
|
||||||
|
<RowSelectorTypes {field} {schema} {bindings} {value} {onChange} />
|
||||||
{:else}
|
{:else}
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
|
placeholder={placeholders[schema.type]}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
value={value[field]}
|
value={Array.isArray(value[field])
|
||||||
on:change={e => onChange(e, field)}
|
? value[field].join(" ")
|
||||||
|
: value[field]}
|
||||||
|
on:change={e => onChange(e, field, schema.type)}
|
||||||
label={field}
|
label={field}
|
||||||
type="string"
|
type="string"
|
||||||
{bindings}
|
{bindings}
|
||||||
fillWidth={true}
|
fillWidth={true}
|
||||||
allowJS={false}
|
allowJS={true}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
Toggle,
|
||||||
|
DatePicker,
|
||||||
|
Multiselect,
|
||||||
|
TextArea,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||||
|
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||||
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
|
|
||||||
|
export let onChange
|
||||||
|
export let field
|
||||||
|
export let schema
|
||||||
|
export let value
|
||||||
|
export let bindings
|
||||||
|
|
||||||
|
function schemaHasOptions(schema) {
|
||||||
|
return !!schema.constraints?.inclusion?.length
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if schemaHasOptions(schema) && schema.type !== "array"}
|
||||||
|
<Select
|
||||||
|
on:change={e => onChange(e, field)}
|
||||||
|
label={field}
|
||||||
|
value={value[field]}
|
||||||
|
options={schema.constraints.inclusion}
|
||||||
|
/>
|
||||||
|
{:else if schema.type === "datetime"}
|
||||||
|
<DatePicker
|
||||||
|
label={field}
|
||||||
|
value={value[field]}
|
||||||
|
on:change={e => onChange(e, field)}
|
||||||
|
/>
|
||||||
|
{:else if schema.type === "boolean"}
|
||||||
|
<Toggle
|
||||||
|
text={field}
|
||||||
|
value={value[field]}
|
||||||
|
on:change={e => onChange(e, field)}
|
||||||
|
/>
|
||||||
|
{:else if schema.type === "array"}
|
||||||
|
<Multiselect
|
||||||
|
bind:value={value[field]}
|
||||||
|
label={field}
|
||||||
|
options={schema.constraints.inclusion}
|
||||||
|
/>
|
||||||
|
{:else if schema.type === "longform"}
|
||||||
|
<TextArea label={field} bind:value={value[field]} />
|
||||||
|
{:else if schema.type === "link"}
|
||||||
|
<LinkedRowSelector bind:linkedRows={value[field]} {schema} />
|
||||||
|
{:else if schema.type === "string" || schema.type === "number"}
|
||||||
|
<DrawerBindableInput
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
value={value[field]}
|
||||||
|
on:change={e => onChange(e, field)}
|
||||||
|
label={field}
|
||||||
|
type="string"
|
||||||
|
{bindings}
|
||||||
|
fillWidth={true}
|
||||||
|
allowJS={true}
|
||||||
|
/>
|
||||||
|
{/if}
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Input, Icon, notifications } from "@budibase/bbui"
|
import CopyInput from "components/common/inputs/CopyInput.svelte"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
|
@ -10,55 +10,6 @@
|
||||||
|
|
||||||
return `${window.location.origin}/${uri}`
|
return `${window.location.origin}/${uri}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyToClipboard() {
|
|
||||||
const dummy = document.createElement("textarea")
|
|
||||||
document.body.appendChild(dummy)
|
|
||||||
dummy.value = fullWebhookURL(value)
|
|
||||||
dummy.select()
|
|
||||||
document.execCommand("copy")
|
|
||||||
document.body.removeChild(dummy)
|
|
||||||
notifications.success(`URL copied to clipboard`)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<CopyInput {value} copyValue={fullWebhookURL(value)} />
|
||||||
<Input readonly value={fullWebhookURL(value)} />
|
|
||||||
<div class="icon" on:click={() => copyToClipboard()}>
|
|
||||||
<Icon size="S" name="Copy" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
div {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
right: 1px;
|
|
||||||
bottom: 1px;
|
|
||||||
position: absolute;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-left: 1px solid var(--spectrum-alias-border-color);
|
|
||||||
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
|
|
||||||
border-bottom-right-radius: var(--spectrum-alias-border-radius-regular);
|
|
||||||
width: 31px;
|
|
||||||
color: var(--spectrum-alias-text-color);
|
|
||||||
background-color: var(--spectrum-global-color-gray-75);
|
|
||||||
transition: background-color
|
|
||||||
var(--spectrum-global-animation-duration-100, 130ms),
|
|
||||||
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
|
|
||||||
border-color var(--spectrum-global-animation-duration-100, 130ms);
|
|
||||||
height: calc(var(--spectrum-alias-item-height-m) - 2px);
|
|
||||||
}
|
|
||||||
.icon:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--spectrum-alias-text-color-hover);
|
|
||||||
background-color: var(--spectrum-global-color-gray-50);
|
|
||||||
border-color: var(--spectrum-alias-border-color-hover);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -57,7 +57,8 @@
|
||||||
{data}
|
{data}
|
||||||
{loading}
|
{loading}
|
||||||
{type}
|
{type}
|
||||||
allowEditing={!view?.calculation}
|
allowEditing={false}
|
||||||
|
rowCount={10}
|
||||||
bind:hideAutocolumns
|
bind:hideAutocolumns
|
||||||
>
|
>
|
||||||
<ViewFilterButton {view} />
|
<ViewFilterButton {view} />
|
||||||
|
|
|
@ -8,7 +8,11 @@
|
||||||
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
|
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
|
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
|
||||||
import { customQueryIconText, customQueryIconColor } from "helpers/data/utils"
|
import {
|
||||||
|
customQueryIconText,
|
||||||
|
customQueryIconColor,
|
||||||
|
customQueryText,
|
||||||
|
} from "helpers/data/utils"
|
||||||
import ICONS from "./icons"
|
import ICONS from "./icons"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
|
@ -137,7 +141,7 @@
|
||||||
icon="SQLQuery"
|
icon="SQLQuery"
|
||||||
iconText={customQueryIconText(datasource, query)}
|
iconText={customQueryIconText(datasource, query)}
|
||||||
iconColor={customQueryIconColor(datasource, query)}
|
iconColor={customQueryIconColor(datasource, query)}
|
||||||
text={query.name}
|
text={customQueryText(datasource, query)}
|
||||||
opened={$queries.selected === query._id}
|
opened={$queries.selected === query._id}
|
||||||
selected={$queries.selected === query._id}
|
selected={$queries.selected === query._id}
|
||||||
on:click={() => onClickQuery(query)}
|
on:click={() => onClickQuery(query)}
|
||||||
|
|
|
@ -20,7 +20,9 @@
|
||||||
$goto(`./datasource/${resp._id}`)
|
$goto(`./datasource/${resp._id}`)
|
||||||
notifications.success(`Datasource updated successfully.`)
|
notifications.success(`Datasource updated successfully.`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error("Error saving datasource")
|
notifications.error(err?.message ?? "Error saving datasource")
|
||||||
|
// prevent the modal from closing
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
<script>
|
||||||
|
import { Input, Icon, notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let label = null
|
||||||
|
export let value
|
||||||
|
export let copyValue
|
||||||
|
|
||||||
|
const copyToClipboard = val => {
|
||||||
|
const dummy = document.createElement("textarea")
|
||||||
|
document.body.appendChild(dummy)
|
||||||
|
dummy.value = val
|
||||||
|
dummy.select()
|
||||||
|
document.execCommand("copy")
|
||||||
|
document.body.removeChild(dummy)
|
||||||
|
notifications.success(`URL copied to clipboard`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Input readonly {value} {label} />
|
||||||
|
<div class="icon" on:click={() => copyToClipboard(value || copyValue)}>
|
||||||
|
<Icon size="S" name="Copy" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
right: 1px;
|
||||||
|
bottom: 1px;
|
||||||
|
position: absolute;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-left: 1px solid var(--spectrum-alias-border-color);
|
||||||
|
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
|
||||||
|
border-bottom-right-radius: var(--spectrum-alias-border-radius-regular);
|
||||||
|
width: 31px;
|
||||||
|
color: var(--spectrum-alias-text-color);
|
||||||
|
background-color: var(--spectrum-global-color-gray-75);
|
||||||
|
transition: background-color
|
||||||
|
var(--spectrum-global-animation-duration-100, 130ms),
|
||||||
|
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
|
||||||
|
border-color var(--spectrum-global-animation-duration-100, 130ms);
|
||||||
|
height: calc(var(--spectrum-alias-item-height-m) - 2px);
|
||||||
|
}
|
||||||
|
.icon:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--spectrum-alias-text-color-hover);
|
||||||
|
background-color: var(--spectrum-global-color-gray-50);
|
||||||
|
border-color: var(--spectrum-alias-border-color-hover);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -134,8 +134,9 @@
|
||||||
|
|
||||||
// Remove all iframe event listeners on component destroy
|
// Remove all iframe event listeners on component destroy
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
window.removeEventListener("message", receiveMessage)
|
||||||
|
|
||||||
if (iframe.contentWindow) {
|
if (iframe.contentWindow) {
|
||||||
window.removeEventListener("message", receiveMessage)
|
|
||||||
if (!$store.clientFeatures.messagePassing) {
|
if (!$store.clientFeatures.messagePassing) {
|
||||||
// Legacy - remove in later versions of BB
|
// Legacy - remove in later versions of BB
|
||||||
iframe.contentWindow.removeEventListener(
|
iframe.contentWindow.removeEventListener(
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
|
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
|
||||||
import ResetFieldsButton from "./PropertyControls/ResetFieldsButton.svelte"
|
import ResetFieldsButton from "./PropertyControls/ResetFieldsButton.svelte"
|
||||||
import { getComponentForSettingType } from "./PropertyControls/componentSettings"
|
import { getComponentForSettingType } from "./PropertyControls/componentSettings"
|
||||||
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let componentDefinition
|
export let componentDefinition
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
|
@ -40,13 +41,13 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateProp = async (key, value) => {
|
const updateProp = Utils.sequential(async (key, value) => {
|
||||||
try {
|
try {
|
||||||
await store.actions.components.updateProp(key, value)
|
await store.actions.components.updateProp(key, value)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error updating component prop")
|
notifications.error("Error updating component prop")
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
const canRenderControl = setting => {
|
const canRenderControl = setting => {
|
||||||
const control = getComponentForSettingType(setting?.type)
|
const control = getComponentForSettingType(setting?.type)
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
<script>
|
||||||
|
import { Label, Select, Body } from "@budibase/bbui"
|
||||||
|
import { tables } from "stores/backend"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
export let parameters
|
||||||
|
$: tableOptions = $tables.list || []
|
||||||
|
|
||||||
|
const FORMATS = [
|
||||||
|
{
|
||||||
|
label: "CSV",
|
||||||
|
value: "csv",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "JSON",
|
||||||
|
value: "json",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!parameters.type) {
|
||||||
|
parameters.type = "csv"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
<Body size="S">
|
||||||
|
Choose the table that you would like to export your row selection from.
|
||||||
|
<br />
|
||||||
|
Please ensure you have enabled row selection in the table settings
|
||||||
|
</Body>
|
||||||
|
|
||||||
|
<div class="params">
|
||||||
|
<Label small>Table</Label>
|
||||||
|
<Select
|
||||||
|
bind:value={parameters.tableId}
|
||||||
|
options={tableOptions}
|
||||||
|
getOptionLabel={option => option.name}
|
||||||
|
getOptionValue={option => option._id}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Label small>Type</Label>
|
||||||
|
<Select bind:value={parameters.type} options={FORMATS} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(p) {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.params {
|
||||||
|
display: grid;
|
||||||
|
column-gap: var(--spacing-l);
|
||||||
|
row-gap: var(--spacing-s);
|
||||||
|
grid-template-columns: 100px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -12,3 +12,4 @@ export { default as UpdateState } from "./UpdateState.svelte"
|
||||||
export { default as RefreshDataProvider } from "./RefreshDataProvider.svelte"
|
export { default as RefreshDataProvider } from "./RefreshDataProvider.svelte"
|
||||||
export { default as DuplicateRow } from "./DuplicateRow.svelte"
|
export { default as DuplicateRow } from "./DuplicateRow.svelte"
|
||||||
export { default as S3Upload } from "./S3Upload.svelte"
|
export { default as S3Upload } from "./S3Upload.svelte"
|
||||||
|
export { default as ExportData } from "./ExportData.svelte"
|
||||||
|
|
|
@ -80,6 +80,10 @@
|
||||||
"value": "publicUrl"
|
"value": "publicUrl"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Export Data",
|
||||||
|
"component": "ExportData"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script>
|
||||||
|
import { ModalContent, Body, notifications } from "@budibase/bbui"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import CopyInput from "components/common/inputs/CopyInput.svelte"
|
||||||
|
|
||||||
|
let apiKey = null
|
||||||
|
|
||||||
|
async function generateAPIKey() {
|
||||||
|
try {
|
||||||
|
apiKey = await auth.generateAPIKey()
|
||||||
|
notifications.success("New API key generated")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Unable to generate new API key")
|
||||||
|
}
|
||||||
|
// need to return false to keep modal open
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
apiKey = await auth.fetchAPIKey()
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Unable to fetch API key")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
title="Developer information"
|
||||||
|
showConfirmButton={false}
|
||||||
|
showSecondaryButton={true}
|
||||||
|
secondaryButtonText="Re-generate key"
|
||||||
|
secondaryAction={generateAPIKey}
|
||||||
|
>
|
||||||
|
<Body size="S">
|
||||||
|
You can find information about your developer account here, such as the API
|
||||||
|
key used to access the Budibase API.
|
||||||
|
</Body>
|
||||||
|
<CopyInput bind:value={apiKey} label="API key" />
|
||||||
|
</ModalContent>
|
|
@ -30,8 +30,8 @@ export function breakQueryString(qs) {
|
||||||
const params = qs.split("&")
|
const params = qs.split("&")
|
||||||
let paramObj = {}
|
let paramObj = {}
|
||||||
for (let param of params) {
|
for (let param of params) {
|
||||||
const [key, value] = param.split("=")
|
const split = param.split("=")
|
||||||
paramObj[key] = value
|
paramObj[split[0]] = split.slice(1).join("=")
|
||||||
}
|
}
|
||||||
return paramObj
|
return paramObj
|
||||||
}
|
}
|
||||||
|
@ -109,6 +109,36 @@ export function customQueryIconColor(datasource, query) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function customQueryText(datasource, query) {
|
||||||
|
if (!query.name || datasource.source !== IntegrationTypes.REST) {
|
||||||
|
return query.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove protocol
|
||||||
|
let name = query.name
|
||||||
|
if (name.includes("//")) {
|
||||||
|
name = name.split("//")[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no path, return the full name
|
||||||
|
if (!name.includes("/")) {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing slash
|
||||||
|
if (name.endsWith("/")) {
|
||||||
|
name = name.slice(0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only use path
|
||||||
|
const split = name.split("/")
|
||||||
|
if (split[1]) {
|
||||||
|
return `/${split.slice(1).join("/")}`
|
||||||
|
} else {
|
||||||
|
return split[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function flipHeaderState(headersActivity) {
|
export function flipHeaderState(headersActivity) {
|
||||||
if (!headersActivity) {
|
if (!headersActivity) {
|
||||||
return {}
|
return {}
|
||||||
|
|
|
@ -45,6 +45,14 @@
|
||||||
store.actions.screens.select(id)
|
store.actions.screens.select(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we didn't find a valid asset, just update the preview type
|
||||||
|
if (!id) {
|
||||||
|
store.update(state => {
|
||||||
|
state.currentFrontEndType = assetType
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -18,11 +18,13 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte"
|
import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte"
|
||||||
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
||||||
|
import UpdateAPIKeyModal from "components/settings/UpdateAPIKeyModal.svelte"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
let userInfoModal
|
let userInfoModal
|
||||||
let changePasswordModal
|
let changePasswordModal
|
||||||
|
let apiKeyModal
|
||||||
let mobileMenuVisible = false
|
let mobileMenuVisible = false
|
||||||
|
|
||||||
$: menu = buildMenu($auth.isAdmin)
|
$: menu = buildMenu($auth.isAdmin)
|
||||||
|
@ -162,6 +164,11 @@
|
||||||
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}>
|
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}>
|
||||||
Update user information
|
Update user information
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{#if $auth.isBuilder}
|
||||||
|
<MenuItem icon="Key" on:click={() => apiKeyModal.show()}>
|
||||||
|
View API key
|
||||||
|
</MenuItem>
|
||||||
|
{/if}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="LockClosed"
|
icon="LockClosed"
|
||||||
on:click={() => changePasswordModal.show()}
|
on:click={() => changePasswordModal.show()}
|
||||||
|
@ -186,6 +193,9 @@
|
||||||
<Modal bind:this={changePasswordModal}>
|
<Modal bind:this={changePasswordModal}>
|
||||||
<ChangePasswordModal />
|
<ChangePasswordModal />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Modal bind:this={apiKeyModal}>
|
||||||
|
<UpdateAPIKeyModal />
|
||||||
|
</Modal>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -60,7 +60,7 @@ export function createQueriesStore() {
|
||||||
})
|
})
|
||||||
return savedQuery
|
return savedQuery
|
||||||
},
|
},
|
||||||
import: async (data, datasourceId) => {
|
import: async ({ data, datasourceId }) => {
|
||||||
return await API.importQueries({
|
return await API.importQueries({
|
||||||
datasourceId,
|
datasourceId,
|
||||||
data,
|
data,
|
||||||
|
|
|
@ -172,6 +172,13 @@ export function createAuthStore() {
|
||||||
resetCode,
|
resetCode,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
generateAPIKey: async () => {
|
||||||
|
return API.generateAPIKey()
|
||||||
|
},
|
||||||
|
fetchAPIKey: async () => {
|
||||||
|
const info = await API.fetchDeveloperInfo()
|
||||||
|
return info?.apiKey
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -3,3 +3,4 @@ docker-compose.yaml
|
||||||
nginx.conf
|
nginx.conf
|
||||||
build/
|
build/
|
||||||
docker-error.log
|
docker-error.log
|
||||||
|
envoy.yaml
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "1.0.76-alpha.2",
|
"version": "1.0.81-alpha.0",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
"state": true,
|
"state": true,
|
||||||
"customThemes": true,
|
"customThemes": true,
|
||||||
"devicePreview": true,
|
"devicePreview": true,
|
||||||
"messagePassing": true
|
"messagePassing": true,
|
||||||
|
"rowSelection": true
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
"name": "Layout",
|
"name": "Layout",
|
||||||
|
@ -2714,6 +2715,13 @@
|
||||||
"key": "showAutoColumns",
|
"key": "showAutoColumns",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Allow row selection",
|
||||||
|
"key": "allowSelectRows",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Link table rows",
|
"label": "Link table rows",
|
||||||
|
@ -2973,6 +2981,11 @@
|
||||||
"label": "Show auto columns",
|
"label": "Show auto columns",
|
||||||
"key": "showAutoColumns"
|
"key": "showAutoColumns"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Allow row selection",
|
||||||
|
"key": "allowSelectRows"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Link table rows",
|
"label": "Link table rows",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "1.0.76-alpha.2",
|
"version": "1.0.81-alpha.0",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,25 +19,12 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.76-alpha.2",
|
"@budibase/bbui": "^1.0.81-alpha.0",
|
||||||
"@budibase/frontend-core": "^1.0.76-alpha.2",
|
"@budibase/frontend-core": "^1.0.81-alpha.0",
|
||||||
"@budibase/string-templates": "^1.0.76-alpha.2",
|
"@budibase/string-templates": "^1.0.81-alpha.0",
|
||||||
"@spectrum-css/button": "^3.0.3",
|
|
||||||
"@spectrum-css/card": "^3.0.3",
|
|
||||||
"@spectrum-css/divider": "^1.0.3",
|
|
||||||
"@spectrum-css/link": "^3.1.3",
|
|
||||||
"@spectrum-css/page": "^3.0.1",
|
|
||||||
"@spectrum-css/tag": "^3.1.4",
|
|
||||||
"@spectrum-css/typography": "^3.0.2",
|
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
|
||||||
"apexcharts": "^3.22.1",
|
|
||||||
"dayjs": "^1.10.5",
|
|
||||||
"regexparam": "^1.3.0",
|
"regexparam": "^1.3.0",
|
||||||
"rollup-plugin-polyfill-node": "^0.8.0",
|
"rollup-plugin-polyfill-node": "^0.8.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte": "^3.38.2",
|
|
||||||
"svelte-apexcharts": "^1.0.2",
|
|
||||||
"svelte-flatpickr": "^3.1.0",
|
|
||||||
"svelte-spa-router": "^3.0.5"
|
"svelte-spa-router": "^3.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
import UserBindingsProvider from "components/context/UserBindingsProvider.svelte"
|
import UserBindingsProvider from "components/context/UserBindingsProvider.svelte"
|
||||||
import DeviceBindingsProvider from "components/context/DeviceBindingsProvider.svelte"
|
import DeviceBindingsProvider from "components/context/DeviceBindingsProvider.svelte"
|
||||||
import StateBindingsProvider from "components/context/StateBindingsProvider.svelte"
|
import StateBindingsProvider from "components/context/StateBindingsProvider.svelte"
|
||||||
|
import RowSelectionProvider from "components/context/RowSelectionProvider.svelte"
|
||||||
import SettingsBar from "components/preview/SettingsBar.svelte"
|
import SettingsBar from "components/preview/SettingsBar.svelte"
|
||||||
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
|
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
|
||||||
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
||||||
|
@ -90,59 +91,61 @@
|
||||||
<UserBindingsProvider>
|
<UserBindingsProvider>
|
||||||
<DeviceBindingsProvider>
|
<DeviceBindingsProvider>
|
||||||
<StateBindingsProvider>
|
<StateBindingsProvider>
|
||||||
<!-- Settings bar can be rendered outside of device preview -->
|
<RowSelectionProvider>
|
||||||
<!-- Key block needs to be outside the if statement or it breaks -->
|
<!-- Settings bar can be rendered outside of device preview -->
|
||||||
{#key $builderStore.selectedComponentId}
|
<!-- Key block needs to be outside the if statement or it breaks -->
|
||||||
{#if $builderStore.inBuilder}
|
{#key $builderStore.selectedComponentId}
|
||||||
<SettingsBar />
|
{#if $builderStore.inBuilder}
|
||||||
{/if}
|
<SettingsBar />
|
||||||
{/key}
|
{/if}
|
||||||
|
{/key}
|
||||||
|
|
||||||
<!-- Clip boundary for selection indicators -->
|
<!-- Clip boundary for selection indicators -->
|
||||||
<div
|
<div
|
||||||
id="clip-root"
|
id="clip-root"
|
||||||
class:preview={$builderStore.inBuilder}
|
class:preview={$builderStore.inBuilder}
|
||||||
class:tablet-preview={$builderStore.previewDevice === "tablet"}
|
class:tablet-preview={$builderStore.previewDevice === "tablet"}
|
||||||
class:mobile-preview={$builderStore.previewDevice === "mobile"}
|
class:mobile-preview={$builderStore.previewDevice === "mobile"}
|
||||||
>
|
>
|
||||||
<!-- Actual app -->
|
<!-- Actual app -->
|
||||||
<div id="app-root">
|
<div id="app-root">
|
||||||
<CustomThemeWrapper>
|
<CustomThemeWrapper>
|
||||||
{#key $screenStore.activeLayout._id}
|
{#key `${$screenStore.activeLayout._id}-${$builderStore.previewType}`}
|
||||||
<Component
|
<Component
|
||||||
isLayout
|
isLayout
|
||||||
instance={$screenStore.activeLayout.props}
|
instance={$screenStore.activeLayout.props}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Flatpickr needs to be inside the theme wrapper.
|
Flatpickr needs to be inside the theme wrapper.
|
||||||
It also needs its own container because otherwise it hijacks
|
It also needs its own container because otherwise it hijacks
|
||||||
key events on the whole page. It is painful to work with.
|
key events on the whole page. It is painful to work with.
|
||||||
-->
|
-->
|
||||||
<div id="flatpickr-root" />
|
<div id="flatpickr-root" />
|
||||||
|
|
||||||
<!-- Modal container to ensure they sit on top -->
|
<!-- Modal container to ensure they sit on top -->
|
||||||
<div class="modal-container" />
|
<div class="modal-container" />
|
||||||
|
|
||||||
<!-- Layers on top of app -->
|
<!-- Layers on top of app -->
|
||||||
<NotificationDisplay />
|
<NotificationDisplay />
|
||||||
<ConfirmationDisplay />
|
<ConfirmationDisplay />
|
||||||
<PeekScreenDisplay />
|
<PeekScreenDisplay />
|
||||||
</CustomThemeWrapper>
|
</CustomThemeWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Selection indicators should be bounded by device -->
|
<!-- Selection indicators should be bounded by device -->
|
||||||
<!--
|
<!--
|
||||||
We don't want to key these by componentID as they control their own
|
We don't want to key these by componentID as they control their own
|
||||||
re-mounting to avoid flashes.
|
re-mounting to avoid flashes.
|
||||||
-->
|
-->
|
||||||
{#if $builderStore.inBuilder}
|
{#if $builderStore.inBuilder}
|
||||||
<SelectionIndicator />
|
<SelectionIndicator />
|
||||||
<HoverIndicator />
|
<HoverIndicator />
|
||||||
<DNDHandler />
|
<DNDHandler />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</RowSelectionProvider>
|
||||||
</StateBindingsProvider>
|
</StateBindingsProvider>
|
||||||
</DeviceBindingsProvider>
|
</DeviceBindingsProvider>
|
||||||
</UserBindingsProvider>
|
</UserBindingsProvider>
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
export let quiet
|
export let quiet
|
||||||
export let compact
|
export let compact
|
||||||
export let size
|
export let size
|
||||||
|
export let allowSelectRows
|
||||||
export let linkRows
|
export let linkRows
|
||||||
export let linkURL
|
export let linkURL
|
||||||
export let linkColumn
|
export let linkColumn
|
||||||
|
@ -157,6 +158,7 @@
|
||||||
>
|
>
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type="table"
|
type="table"
|
||||||
|
context="table"
|
||||||
props={{
|
props={{
|
||||||
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
|
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
|
||||||
columns: tableColumns,
|
columns: tableColumns,
|
||||||
|
@ -164,6 +166,7 @@
|
||||||
rowCount,
|
rowCount,
|
||||||
quiet,
|
quiet,
|
||||||
compact,
|
compact,
|
||||||
|
allowSelectRows,
|
||||||
size,
|
size,
|
||||||
linkRows,
|
linkRows,
|
||||||
linkURL,
|
linkURL,
|
||||||
|
|
|
@ -81,7 +81,10 @@
|
||||||
loading = false
|
loading = false
|
||||||
return res
|
return res
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notificationStore.actions.error(`Error uploading file: ${error}`)
|
notificationStore.actions.error(
|
||||||
|
`Error uploading file: ${error?.message || error}`
|
||||||
|
)
|
||||||
|
loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ export const getOptions = (
|
||||||
|
|
||||||
dataProvider?.rows?.forEach(row => {
|
dataProvider?.rows?.forEach(row => {
|
||||||
const value = row?.[valueColumn]
|
const value = row?.[valueColumn]
|
||||||
if (value) {
|
if (value != null) {
|
||||||
const label = row[labelColumn] || value
|
const label = row[labelColumn] || value
|
||||||
optionsSet[value] = { value, label }
|
optionsSet[value] = { value, label }
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ export const getOptions = (
|
||||||
let optionsSet = {}
|
let optionsSet = {}
|
||||||
dataProvider?.rows?.forEach(row => {
|
dataProvider?.rows?.forEach(row => {
|
||||||
const value = row?.[valueColumn]
|
const value = row?.[valueColumn]
|
||||||
if (value) {
|
if (value != null) {
|
||||||
const label = row[labelColumn] || value
|
const label = row[labelColumn] || value
|
||||||
optionsSet[value] = { value, label }
|
optionsSet[value] = { value, label }
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { Table } from "@budibase/bbui"
|
import { Table } from "@budibase/bbui"
|
||||||
import SlotRenderer from "./SlotRenderer.svelte"
|
import SlotRenderer from "./SlotRenderer.svelte"
|
||||||
import { UnsortableTypes } from "../../../constants"
|
import { UnsortableTypes } from "../../../constants"
|
||||||
|
import { onDestroy } from "svelte"
|
||||||
|
|
||||||
export let dataProvider
|
export let dataProvider
|
||||||
export let columns
|
export let columns
|
||||||
|
@ -14,10 +15,12 @@
|
||||||
export let linkURL
|
export let linkURL
|
||||||
export let linkColumn
|
export let linkColumn
|
||||||
export let linkPeek
|
export let linkPeek
|
||||||
|
export let allowSelectRows
|
||||||
export let compact
|
export let compact
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable, getAction, ActionTypes, routeStore } = getContext("sdk")
|
const { styleable, getAction, ActionTypes, routeStore, rowSelectionStore } =
|
||||||
|
getContext("sdk")
|
||||||
const customColumnKey = `custom-${Math.random()}`
|
const customColumnKey = `custom-${Math.random()}`
|
||||||
const customRenderers = [
|
const customRenderers = [
|
||||||
{
|
{
|
||||||
|
@ -25,7 +28,7 @@
|
||||||
component: SlotRenderer,
|
component: SlotRenderer,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
let selectedRows = []
|
||||||
$: hasChildren = $component.children
|
$: hasChildren = $component.children
|
||||||
$: loading = dataProvider?.loading ?? false
|
$: loading = dataProvider?.loading ?? false
|
||||||
$: data = dataProvider?.rows || []
|
$: data = dataProvider?.rows || []
|
||||||
|
@ -36,6 +39,13 @@
|
||||||
dataProvider?.id,
|
dataProvider?.id,
|
||||||
ActionTypes.SetDataProviderSorting
|
ActionTypes.SetDataProviderSorting
|
||||||
)
|
)
|
||||||
|
$: {
|
||||||
|
rowSelectionStore.actions.updateSelection(
|
||||||
|
$component.id,
|
||||||
|
selectedRows.length ? selectedRows[0].tableId : "",
|
||||||
|
selectedRows.map(row => row._id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const getFields = (schema, customColumns, showAutoColumns) => {
|
const getFields = (schema, customColumns, showAutoColumns) => {
|
||||||
// Check for an invalid column selection
|
// Check for an invalid column selection
|
||||||
|
@ -117,6 +127,10 @@
|
||||||
const split = linkURL.split("/:")
|
const split = linkURL.split("/:")
|
||||||
routeStore.actions.navigate(`${split[0]}/${id}`, linkPeek)
|
routeStore.actions.navigate(`${split[0]}/${id}`, linkPeek)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
rowSelectionStore.actions.updateSelection($component.id, [])
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:styleable={$component.styles} class={size}>
|
<div use:styleable={$component.styles} class={size}>
|
||||||
|
@ -128,7 +142,8 @@
|
||||||
{quiet}
|
{quiet}
|
||||||
{compact}
|
{compact}
|
||||||
{customRenderers}
|
{customRenderers}
|
||||||
allowSelectRows={false}
|
allowSelectRows={!!allowSelectRows}
|
||||||
|
bind:selectedRows
|
||||||
allowEditRows={false}
|
allowEditRows={false}
|
||||||
allowEditColumns={false}
|
allowEditColumns={false}
|
||||||
showAutoColumns={true}
|
showAutoColumns={true}
|
||||||
|
@ -139,10 +154,19 @@
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</Table>
|
</Table>
|
||||||
|
{#if allowSelectRows && selectedRows.length}
|
||||||
|
<div class="row-count">
|
||||||
|
{selectedRows.length} row{selectedRows.length === 1 ? "" : "s"} selected
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
background-color: var(--spectrum-alias-background-color-secondary);
|
background-color: var(--spectrum-alias-background-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row-count {
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
<script>
|
||||||
|
import Provider from "./Provider.svelte"
|
||||||
|
import { rowSelectionStore } from "stores"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Provider key="rowSelection" data={$rowSelectionStore}>
|
||||||
|
<slot />
|
||||||
|
</Provider>
|
|
@ -6,6 +6,7 @@ import {
|
||||||
screenStore,
|
screenStore,
|
||||||
builderStore,
|
builderStore,
|
||||||
uploadStore,
|
uploadStore,
|
||||||
|
rowSelectionStore,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import { styleable } from "utils/styleable"
|
import { styleable } from "utils/styleable"
|
||||||
import { linkable } from "utils/linkable"
|
import { linkable } from "utils/linkable"
|
||||||
|
@ -19,6 +20,7 @@ export default {
|
||||||
authStore,
|
authStore,
|
||||||
notificationStore,
|
notificationStore,
|
||||||
routeStore,
|
routeStore,
|
||||||
|
rowSelectionStore,
|
||||||
screenStore,
|
screenStore,
|
||||||
builderStore,
|
builderStore,
|
||||||
uploadStore,
|
uploadStore,
|
||||||
|
|
|
@ -10,7 +10,7 @@ export { peekStore } from "./peek"
|
||||||
export { stateStore } from "./state"
|
export { stateStore } from "./state"
|
||||||
export { themeStore } from "./theme"
|
export { themeStore } from "./theme"
|
||||||
export { uploadStore } from "./uploads.js"
|
export { uploadStore } from "./uploads.js"
|
||||||
|
export { rowSelectionStore } from "./rowSelection.js"
|
||||||
// Context stores are layered and duplicated, so it is not a singleton
|
// Context stores are layered and duplicated, so it is not a singleton
|
||||||
export { createContextStore } from "./context"
|
export { createContextStore } from "./context"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { get, writable } from "svelte/store"
|
||||||
|
|
||||||
|
const createRowSelectionStore = () => {
|
||||||
|
const store = writable({})
|
||||||
|
|
||||||
|
function updateSelection(componentId, tableId, selectedRows) {
|
||||||
|
store.update(state => {
|
||||||
|
state[componentId] = { tableId: tableId, selectedRows: selectedRows }
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelection(tableId) {
|
||||||
|
const selection = get(store)
|
||||||
|
const componentId = Object.keys(selection).find(
|
||||||
|
componentId => selection[componentId].tableId === tableId
|
||||||
|
)
|
||||||
|
return componentId ? selection[componentId] : {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
set: store.set,
|
||||||
|
actions: {
|
||||||
|
updateSelection,
|
||||||
|
getSelection,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rowSelectionStore = createRowSelectionStore()
|
|
@ -1,4 +1,5 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
import download from "downloadjs"
|
||||||
import {
|
import {
|
||||||
routeStore,
|
routeStore,
|
||||||
builderStore,
|
builderStore,
|
||||||
|
@ -8,6 +9,7 @@ import {
|
||||||
notificationStore,
|
notificationStore,
|
||||||
dataSourceStore,
|
dataSourceStore,
|
||||||
uploadStore,
|
uploadStore,
|
||||||
|
rowSelectionStore,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { ActionTypes } from "constants"
|
import { ActionTypes } from "constants"
|
||||||
|
@ -127,12 +129,16 @@ const queryExecutionHandler = async action => {
|
||||||
// Trigger a notification and invalidate the datasource as long as this
|
// Trigger a notification and invalidate the datasource as long as this
|
||||||
// was not a readable query
|
// was not a readable query
|
||||||
if (!query.readable) {
|
if (!query.readable) {
|
||||||
API.notifications.error.success("Query executed successfully")
|
notificationStore.actions.success("Query executed successfully")
|
||||||
await dataSourceStore.actions.invalidateDataSource(query.datasourceId)
|
await dataSourceStore.actions.invalidateDataSource(query.datasourceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { result }
|
return { result }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
notificationStore.actions.error(
|
||||||
|
"An error occurred while executing the query"
|
||||||
|
)
|
||||||
|
|
||||||
// Abort next actions
|
// Abort next actions
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -235,6 +241,26 @@ const s3UploadHandler = async action => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exportDataHandler = async action => {
|
||||||
|
let selection = rowSelectionStore.actions.getSelection(
|
||||||
|
action.parameters.tableId
|
||||||
|
)
|
||||||
|
if (selection.selectedRows && selection.selectedRows.length > 0) {
|
||||||
|
try {
|
||||||
|
const data = await API.exportRows({
|
||||||
|
tableId: selection.tableId,
|
||||||
|
rows: selection.selectedRows,
|
||||||
|
})
|
||||||
|
|
||||||
|
download(JSON.stringify(data), `export.${action.parameters.type}`)
|
||||||
|
} catch (error) {
|
||||||
|
notificationStore.actions.error("There was an error exporting the data")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notificationStore.actions.error("Please select at least one row")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handlerMap = {
|
const handlerMap = {
|
||||||
["Save Row"]: saveRowHandler,
|
["Save Row"]: saveRowHandler,
|
||||||
["Duplicate Row"]: duplicateRowHandler,
|
["Duplicate Row"]: duplicateRowHandler,
|
||||||
|
@ -250,6 +276,7 @@ const handlerMap = {
|
||||||
["Change Form Step"]: changeFormStepHandler,
|
["Change Form Step"]: changeFormStepHandler,
|
||||||
["Update State"]: updateStateHandler,
|
["Update State"]: updateStateHandler,
|
||||||
["Upload File to S3"]: s3UploadHandler,
|
["Upload File to S3"]: s3UploadHandler,
|
||||||
|
["Export Data"]: exportDataHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmTextMap = {
|
const confirmTextMap = {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -84,28 +84,28 @@
|
||||||
integrity sha512-+oKLUI2a0QmQP9EzySeq/G4FpUkkdaDNbuEbqCj2IkPMc/2v/nwzsPhh1fj2UIghGAiiUwXfPpzax1e8fyhQUg==
|
integrity sha512-+oKLUI2a0QmQP9EzySeq/G4FpUkkdaDNbuEbqCj2IkPMc/2v/nwzsPhh1fj2UIghGAiiUwXfPpzax1e8fyhQUg==
|
||||||
|
|
||||||
"@spectrum-css/divider@^1.0.3":
|
"@spectrum-css/divider@^1.0.3":
|
||||||
version "1.0.9"
|
version "1.0.17"
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/divider/-/divider-1.0.9.tgz#00246bd453981c4696149d26f5bcfeefd29b4b53"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/divider/-/divider-1.0.17.tgz#cae86fdcb5eb6dae95798ae19ec962e5735fc27f"
|
||||||
integrity sha512-kmSMSXbm56FR0/OAGwT6tlsHuy1OpOve2DBggjND+AVWk6i3TpoTjvbVppy/f8fuLfbMDS5D3MPD27wTEj8wDA==
|
integrity sha512-wuijKLQ+hwZC/aN8x7fVY18KPoFrxnhohheoDB99OTH44nt5+jEggzcPtwMHiUyb5iWMZx045OrG2DdJHiGl1A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@spectrum-css/vars" "^4.3.0"
|
"@spectrum-css/vars" "^7.0.0"
|
||||||
|
|
||||||
"@spectrum-css/link@^3.1.3":
|
"@spectrum-css/link@^3.1.3":
|
||||||
version "3.1.9"
|
version "3.1.15"
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/link/-/link-3.1.9.tgz#fe40db561c98bf2987489541ef39dcc71416908f"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/link/-/link-3.1.15.tgz#08bcb78e3fe3e816968ba96399597f54e2a7d62d"
|
||||||
integrity sha512-/DpmLIbQGDBNZl+Fnf5VDQ34uC6E6Bz393CAYkzYFyadtvzVEy+PGCgUkT3Tgrwu833IW9fZOh7rkKjw1o/Zng==
|
integrity sha512-LKyI/zr8HXY/PGHCyQxT1Uv1zUzvg7Kuy8E1Itzp+yeCs82hg91aUYkXdQzeWm2eXB/w9cBfjr4NoCl9RWb5bQ==
|
||||||
|
|
||||||
"@spectrum-css/page@^3.0.1":
|
"@spectrum-css/page@^3.0.1":
|
||||||
version "3.0.8"
|
version "3.0.9"
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/page/-/page-3.0.8.tgz#001efa9e4c10095df9b2b37cf7d7d6eb60140190"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/page/-/page-3.0.9.tgz#f8a705dee90af958e2ee20307218e4f82a018c36"
|
||||||
integrity sha512-naEGOyDv9zeK05oa8mZKdwenPILmHG9OTLyKcE8RwuYQDvb0EHcMGC54DOKtGJ5SMNMGCMdC4RwmYUYYKAhkNA==
|
integrity sha512-zxbzJHDHgbc6fq6DpgWPtQa73kCl/3bukMOV2l784jyEWfXx62nuhFYxFVUq8olvyHw2MNZEXF//P7y+W5axVw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@spectrum-css/vars" "^4.3.0"
|
"@spectrum-css/vars" "^4.3.1"
|
||||||
|
|
||||||
"@spectrum-css/tag@^3.1.4":
|
"@spectrum-css/tag@^3.1.4":
|
||||||
version "3.1.4"
|
version "3.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/tag/-/tag-3.1.4.tgz#334384dd789ddf0562679cae62ef763883480ac5"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/tag/-/tag-3.3.3.tgz#826bf03525d10f1ae034681095337973bd43f4af"
|
||||||
integrity sha512-9dYBMhCEkjy+p75XJIfCA2/zU4JAqsJrL7fkYIDXakS6/BzeVtIvAW/6JaIHtLIA9lrj0Sn4m+ZjceKnZNIv1w==
|
integrity sha512-sWcopo4Pgl5VMOF0TuP6on3KmnrcGcaYfBt1/LDAin8+pUoqv2NgLv5BkO7maaPsd9pCLU4K9Y8NPXbujDOefQ==
|
||||||
|
|
||||||
"@spectrum-css/typography@^3.0.2":
|
"@spectrum-css/typography@^3.0.2":
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
|
@ -117,10 +117,15 @@
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-3.0.2.tgz#ea9062c3c98dfc6ba59e5df14a03025ad8969999"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-3.0.2.tgz#ea9062c3c98dfc6ba59e5df14a03025ad8969999"
|
||||||
integrity sha512-vzS9KqYXot4J3AEER/u618MXWAS+IoMvYMNrOoscKiLLKYQWenaueakUWulFonToPd/9vIpqtdbwxznqrK5qDw==
|
integrity sha512-vzS9KqYXot4J3AEER/u618MXWAS+IoMvYMNrOoscKiLLKYQWenaueakUWulFonToPd/9vIpqtdbwxznqrK5qDw==
|
||||||
|
|
||||||
"@spectrum-css/vars@^4.3.0":
|
"@spectrum-css/vars@^4.3.1":
|
||||||
version "4.3.0"
|
version "4.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-4.3.0.tgz#03ddf67d3aa8a9a4cb0edbbd259465c9ced7e70d"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-4.3.1.tgz#d333fa41909f691c8750b5c15ad9ba029df2248e"
|
||||||
integrity sha512-ZQ2XAhgu4G9yBeXQNDAz07Z8oZNnMt5o9vzf/mpBA7Teb/JI+8qXp2wt8D245SzmtNlFkG/bzRYvQc0scgZeCQ==
|
integrity sha512-rX6Iasu9BsFMVgEN0vGRPm9dmSxva+IK/uqQAa9HM0lliwqUiFrJxrFXHHpiAgNuux/U4srEJwbSpGzfF+CegQ==
|
||||||
|
|
||||||
|
"@spectrum-css/vars@^7.0.0":
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@spectrum-css/vars/-/vars-7.0.0.tgz#61a10028f3eeb69d8e488cfeb6251ec75107076d"
|
||||||
|
integrity sha512-0c7i7B/OPrLjrSIQVAxhnDDmDs9tA46gMuuiiaqpRNqEjfS1pRv5eY06qxNMXO5TWL75bHxaiiTQy8NdGA/v6A==
|
||||||
|
|
||||||
"@trysound/sax@0.2.0":
|
"@trysound/sax@0.2.0":
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
|
@ -169,9 +174,9 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
|
||||||
color-convert "^2.0.1"
|
color-convert "^2.0.1"
|
||||||
|
|
||||||
apexcharts@^3.19.2, apexcharts@^3.22.1:
|
apexcharts@^3.19.2, apexcharts@^3.22.1:
|
||||||
version "3.30.0"
|
version "3.33.1"
|
||||||
resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.30.0.tgz#09b008d0a58bb303904bed33b09b260e8fa5e283"
|
resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.33.1.tgz#7159f45e7d726a548e5135a327c03e7894d0bf13"
|
||||||
integrity sha512-NHhFjkd4sqoQqHi+ECN/duVCRvqVZMdXX/UBzCs1xriq8NbNLvs+nIM8OXH1Siv+W50FrK1uTDZrW2cLsKWhBQ==
|
integrity sha512-5aVzrgJefd8EH4w7oRmuOhA3+cxJxQg27cYg3ANVGvPCOB4AY3mVVNtFHRFaIq7bv8ws4GRaA9MWfzoWQw3MPQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
svg.draggable.js "^2.2.2"
|
svg.draggable.js "^2.2.2"
|
||||||
svg.easing.js "^2.0.0"
|
svg.easing.js "^2.0.0"
|
||||||
|
@ -459,6 +464,11 @@ domutils@^2.6.0:
|
||||||
domelementtype "^2.2.0"
|
domelementtype "^2.2.0"
|
||||||
domhandler "^4.2.0"
|
domhandler "^4.2.0"
|
||||||
|
|
||||||
|
downloadjs@1.4.7:
|
||||||
|
version "1.4.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/downloadjs/-/downloadjs-1.4.7.tgz#f69f96f940e0d0553dac291139865a3cd0101e3c"
|
||||||
|
integrity sha1-9p+W+UDg0FU9rCkROYZaPNAQHjw=
|
||||||
|
|
||||||
electron-to-chromium@^1.3.896:
|
electron-to-chromium@^1.3.896:
|
||||||
version "1.3.900"
|
version "1.3.900"
|
||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.900.tgz#5be2c5818a2a012c511b4b43e87b6ab7a296d4f5"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.900.tgz#5be2c5818a2a012c511b4b43e87b6ab7a296d4f5"
|
||||||
|
@ -1355,9 +1365,9 @@ svelte-apexcharts@^1.0.2:
|
||||||
apexcharts "^3.19.2"
|
apexcharts "^3.19.2"
|
||||||
|
|
||||||
svelte-flatpickr@^3.1.0:
|
svelte-flatpickr@^3.1.0:
|
||||||
version "3.2.4"
|
version "3.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.2.4.tgz#1824e26a5dc151d14906cfc7dfd100aefd1b072d"
|
resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.2.6.tgz#595a97b2f25a669e61fe743f90a10dce783bbd49"
|
||||||
integrity sha512-EE2wbFfpZ3iCBOXRRW52w436Jv5lqFoJkd/1vB8XmkfASJgF9HrrZ6Er11NWSmmpaV1nPywwDYFXdWHCB+Wi9Q==
|
integrity sha512-0ePUyE9OjInYFqQwRKOxnFSu4dQX9+/rzFMynq2fKYXx406ZUThzSx72gebtjr0DoAQbsH2///BBZa5qk4qZXg==
|
||||||
dependencies:
|
dependencies:
|
||||||
flatpickr "^4.5.2"
|
flatpickr "^4.5.2"
|
||||||
|
|
||||||
|
@ -1369,9 +1379,9 @@ svelte-spa-router@^3.0.5:
|
||||||
regexparam "2.0.0"
|
regexparam "2.0.0"
|
||||||
|
|
||||||
svelte@^3.38.2:
|
svelte@^3.38.2:
|
||||||
version "3.44.1"
|
version "3.46.4"
|
||||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.44.1.tgz#5cc772a8340f4519a4ecd1ac1a842325466b1a63"
|
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.46.4.tgz#0c46bc4a3e20a2617a1b7dc43a722f9d6c084a38"
|
||||||
integrity sha512-4DrCEJoBvdR689efHNSxIQn2pnFwB7E7j2yLEJtHE/P8hxwZWIphCtJ8are7bjl/iVMlcEf5uh5pJ68IwR09vQ==
|
integrity sha512-qKJzw6DpA33CIa+C/rGp4AUdSfii0DOTCzj/2YpSKKayw5WGSS624Et9L1nU1k2OVRS9vaENQXp2CVZNU+xvIg==
|
||||||
|
|
||||||
svg.draggable.js@^2.2.2:
|
svg.draggable.js@^2.2.2:
|
||||||
version "2.2.2"
|
version "2.2.2"
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/frontend-core",
|
"name": "@budibase/frontend-core",
|
||||||
"version": "1.0.76-alpha.2",
|
"version": "1.0.81-alpha.0",
|
||||||
"description": "Budibase frontend core libraries used in builder and client",
|
"description": "Budibase frontend core libraries used in builder and client",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.76-alpha.2",
|
"@budibase/bbui": "^1.0.81-alpha.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"svelte": "^3.46.2"
|
"svelte": "^3.46.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,61 +1,66 @@
|
||||||
export const buildAttachmentEndpoints = API => ({
|
export const buildAttachmentEndpoints = API => {
|
||||||
/**
|
|
||||||
* Uploads an attachment to the server.
|
|
||||||
* @param data the attachment to upload
|
|
||||||
* @param tableId the table ID to upload to
|
|
||||||
*/
|
|
||||||
uploadAttachment: async ({ data, tableId }) => {
|
|
||||||
return await API.post({
|
|
||||||
url: `/api/attachments/${tableId}/upload`,
|
|
||||||
body: data,
|
|
||||||
json: false,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uploads an attachment to the server as a builder user from the builder.
|
|
||||||
* @param data the data to upload
|
|
||||||
*/
|
|
||||||
uploadBuilderAttachment: async data => {
|
|
||||||
return await API.post({
|
|
||||||
url: "/api/attachments/process",
|
|
||||||
body: data,
|
|
||||||
json: false,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a signed URL to upload a file to an external datasource.
|
* Generates a signed URL to upload a file to an external datasource.
|
||||||
* @param datasourceId the ID of the datasource to upload to
|
* @param datasourceId the ID of the datasource to upload to
|
||||||
* @param bucket the name of the bucket to upload to
|
* @param bucket the name of the bucket to upload to
|
||||||
* @param key the name of the file to upload to
|
* @param key the name of the file to upload to
|
||||||
*/
|
*/
|
||||||
getSignedDatasourceURL: async ({ datasourceId, bucket, key }) => {
|
const getSignedDatasourceURL = async ({ datasourceId, bucket, key }) => {
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: `/api/attachments/${datasourceId}/url`,
|
url: `/api/attachments/${datasourceId}/url`,
|
||||||
body: { bucket, key },
|
body: { bucket, key },
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
return {
|
||||||
* Uploads a file to an external datasource.
|
getSignedDatasourceURL,
|
||||||
* @param datasourceId the ID of the datasource to upload to
|
|
||||||
* @param bucket the name of the bucket to upload to
|
/**
|
||||||
* @param key the name of the file to upload to
|
* Uploads an attachment to the server.
|
||||||
* @param data the file to upload
|
* @param data the attachment to upload
|
||||||
*/
|
* @param tableId the table ID to upload to
|
||||||
externalUpload: async ({ datasourceId, bucket, key, data }) => {
|
*/
|
||||||
const { signedUrl, publicUrl } = await API.getSignedDatasourceURL({
|
uploadAttachment: async ({ data, tableId }) => {
|
||||||
datasourceId,
|
return await API.post({
|
||||||
bucket,
|
url: `/api/attachments/${tableId}/upload`,
|
||||||
key,
|
body: data,
|
||||||
})
|
json: false,
|
||||||
await API.put({
|
})
|
||||||
url: signedUrl,
|
},
|
||||||
body: data,
|
|
||||||
json: false,
|
/**
|
||||||
external: true,
|
* Uploads an attachment to the server as a builder user from the builder.
|
||||||
})
|
* @param data the data to upload
|
||||||
return { publicUrl }
|
*/
|
||||||
},
|
uploadBuilderAttachment: async data => {
|
||||||
})
|
return await API.post({
|
||||||
|
url: "/api/attachments/process",
|
||||||
|
body: data,
|
||||||
|
json: false,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a file to an external datasource.
|
||||||
|
* @param datasourceId the ID of the datasource to upload to
|
||||||
|
* @param bucket the name of the bucket to upload to
|
||||||
|
* @param key the name of the file to upload to
|
||||||
|
* @param data the file to upload
|
||||||
|
*/
|
||||||
|
externalUpload: async ({ datasourceId, bucket, key, data }) => {
|
||||||
|
console.log(API)
|
||||||
|
const { signedUrl, publicUrl } = await getSignedDatasourceURL({
|
||||||
|
datasourceId,
|
||||||
|
bucket,
|
||||||
|
key,
|
||||||
|
})
|
||||||
|
await API.put({
|
||||||
|
url: signedUrl,
|
||||||
|
body: data,
|
||||||
|
json: false,
|
||||||
|
external: true,
|
||||||
|
})
|
||||||
|
return { publicUrl }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { buildScreenEndpoints } from "./screens"
|
||||||
import { buildTableEndpoints } from "./tables"
|
import { buildTableEndpoints } from "./tables"
|
||||||
import { buildTemplateEndpoints } from "./templates"
|
import { buildTemplateEndpoints } from "./templates"
|
||||||
import { buildUserEndpoints } from "./user"
|
import { buildUserEndpoints } from "./user"
|
||||||
|
import { buildSelfEndpoints } from "./self"
|
||||||
import { buildViewEndpoints } from "./views"
|
import { buildViewEndpoints } from "./views"
|
||||||
|
|
||||||
const defaultAPIClientConfig = {
|
const defaultAPIClientConfig = {
|
||||||
|
@ -231,5 +232,6 @@ export const createAPIClient = config => {
|
||||||
...buildTemplateEndpoints(API),
|
...buildTemplateEndpoints(API),
|
||||||
...buildUserEndpoints(API),
|
...buildUserEndpoints(API),
|
||||||
...buildViewEndpoints(API),
|
...buildViewEndpoints(API),
|
||||||
|
...buildSelfEndpoints(API),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,9 +39,11 @@ export const buildOtherEndpoints = API => ({
|
||||||
* Gets the version of the installed Budibase environment.
|
* Gets the version of the installed Budibase environment.
|
||||||
*/
|
*/
|
||||||
getBudibaseVersion: async () => {
|
getBudibaseVersion: async () => {
|
||||||
return await API.get({
|
return (
|
||||||
url: "/api/dev/version",
|
await API.get({
|
||||||
}).version
|
url: "/api/dev/version",
|
||||||
|
})
|
||||||
|
).version
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -60,4 +60,18 @@ export const buildRowEndpoints = API => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports rows.
|
||||||
|
* @param tableId the table ID to export the rows from
|
||||||
|
* @param rows the array of rows to export
|
||||||
|
*/
|
||||||
|
exportRows: async ({ tableId, rows }) => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/${tableId}/rows/exportRows`,
|
||||||
|
body: {
|
||||||
|
rows,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
export const buildSelfEndpoints = API => ({
|
||||||
|
/**
|
||||||
|
* Using the logged in user, this will generate a new API key,
|
||||||
|
* assuming the user is a builder.
|
||||||
|
* @return {Promise<object>} returns the API response, including an API key.
|
||||||
|
*/
|
||||||
|
generateAPIKey: async () => {
|
||||||
|
const response = await API.post({
|
||||||
|
url: "/api/global/self/api_key",
|
||||||
|
})
|
||||||
|
return response?.apiKey
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* retrieves the API key for the logged in user.
|
||||||
|
* @return {Promise<object>} An object containing the user developer information.
|
||||||
|
*/
|
||||||
|
fetchDeveloperInfo: async () => {
|
||||||
|
return API.get({
|
||||||
|
url: "/api/global/self/api_key",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the currently logged-in user object.
|
||||||
|
* Used in client apps.
|
||||||
|
*/
|
||||||
|
fetchSelf: async () => {
|
||||||
|
return await API.get({
|
||||||
|
url: "/api/self",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the currently logged-in user object.
|
||||||
|
* Used in the builder.
|
||||||
|
*/
|
||||||
|
fetchBuilderSelf: async () => {
|
||||||
|
return await API.get({
|
||||||
|
url: "/api/global/self",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the current logged-in user.
|
||||||
|
* @param user the new user object to save
|
||||||
|
*/
|
||||||
|
updateSelf: async user => {
|
||||||
|
return await API.post({
|
||||||
|
url: "/api/global/self",
|
||||||
|
body: user,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
|
@ -1,24 +1,4 @@
|
||||||
export const buildUserEndpoints = API => ({
|
export const buildUserEndpoints = API => ({
|
||||||
/**
|
|
||||||
* Fetches the currently logged-in user object.
|
|
||||||
* Used in client apps.
|
|
||||||
*/
|
|
||||||
fetchSelf: async () => {
|
|
||||||
return await API.get({
|
|
||||||
url: "/api/self",
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the currently logged-in user object.
|
|
||||||
* Used in the builder.
|
|
||||||
*/
|
|
||||||
fetchBuilderSelf: async () => {
|
|
||||||
return await API.get({
|
|
||||||
url: "/api/global/users/self",
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a list of users in the current tenant.
|
* Gets a list of users in the current tenant.
|
||||||
*/
|
*/
|
||||||
|
@ -61,17 +41,6 @@ export const buildUserEndpoints = API => ({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the current logged-in user.
|
|
||||||
* @param user the new user object to save
|
|
||||||
*/
|
|
||||||
updateSelf: async user => {
|
|
||||||
return await API.post({
|
|
||||||
url: "/api/global/users/self",
|
|
||||||
body: user,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates or updates a user in the current tenant.
|
* Creates or updates a user in the current tenant.
|
||||||
* @param user the new user to create
|
* @param user the new user to create
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
export { createAPIClient } from "./api"
|
export { createAPIClient } from "./api"
|
||||||
export { createLocalStorageStore } from "./stores/localStorage"
|
|
||||||
export { fetchData } from "./fetch/fetchData"
|
export { fetchData } from "./fetch/fetchData"
|
||||||
export * as Constants from "./constants"
|
export * as Constants from "./constants"
|
||||||
export * as LuceneUtils from "./utils/lucene"
|
export * from "./stores"
|
||||||
export * as JSONUtils from "./utils/json"
|
export * from "./utils"
|
||||||
export * as CookieUtils from "./utils/cookies"
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export { createLocalStorageStore } from "./localStorage"
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * as LuceneUtils from "./lucene"
|
||||||
|
export * as JSONUtils from "./json"
|
||||||
|
export * as CookieUtils from "./cookies"
|
||||||
|
export * as Utils from "./utils"
|
|
@ -0,0 +1,17 @@
|
||||||
|
/**
|
||||||
|
* Utility to wrap an async function and ensure all invocations happen
|
||||||
|
* sequentially.
|
||||||
|
* @param fn the async function to run
|
||||||
|
* @return {Promise} a sequential version of the function
|
||||||
|
*/
|
||||||
|
export const sequential = fn => {
|
||||||
|
let promise
|
||||||
|
return async (...params) => {
|
||||||
|
if (promise) {
|
||||||
|
await promise
|
||||||
|
}
|
||||||
|
promise = fn(...params)
|
||||||
|
await promise
|
||||||
|
promise = null
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ myapps/
|
||||||
.env
|
.env
|
||||||
builder/*
|
builder/*
|
||||||
client/*
|
client/*
|
||||||
public/
|
|
||||||
db/dev.db/
|
db/dev.db/
|
||||||
dist
|
dist
|
||||||
coverage/
|
coverage/
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
module MySQLMock {
|
||||||
|
const mysql: any = {}
|
||||||
|
|
||||||
|
const client = {
|
||||||
|
connect: jest.fn(),
|
||||||
|
end: jest.fn(),
|
||||||
|
query: jest.fn(async () => {
|
||||||
|
return [[]]
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
mysql.createConnection = jest.fn(async () => {
|
||||||
|
return client
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = mysql
|
||||||
|
}
|
|
@ -53,6 +53,7 @@ module FetchMock {
|
||||||
{
|
{
|
||||||
doc: {
|
doc: {
|
||||||
_id: "test",
|
_id: "test",
|
||||||
|
tableId: opts.body.split("tableId:")[1].split('"')[0],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "1.0.76-alpha.2",
|
"version": "1.0.81-alpha.0",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -21,10 +21,8 @@
|
||||||
"dev:stack:down": "node scripts/dev/manage.js down",
|
"dev:stack:down": "node scripts/dev/manage.js down",
|
||||||
"dev:stack:nuke": "node scripts/dev/manage.js nuke",
|
"dev:stack:nuke": "node scripts/dev/manage.js nuke",
|
||||||
"dev:builder": "yarn run dev:stack:up && nodemon",
|
"dev:builder": "yarn run dev:stack:up && nodemon",
|
||||||
"generate:proxy:compose": "node scripts/proxy/generateProxyConfig compose",
|
|
||||||
"generate:proxy:preprod": "node scripts/proxy/generateProxyConfig preprod",
|
|
||||||
"generate:proxy:prod": "node scripts/proxy/generateProxyConfig prod",
|
|
||||||
"format": "prettier --config ../../.prettierrc.json 'src/**/*.ts' --write",
|
"format": "prettier --config ../../.prettierrc.json 'src/**/*.ts' --write",
|
||||||
|
"specs": "node specs/generate.js && openapi-typescript specs/openapi.yaml --output src/definitions/openapi.ts",
|
||||||
"lint": "eslint --fix src/",
|
"lint": "eslint --fix src/",
|
||||||
"lint:fix": "yarn run format && yarn run lint",
|
"lint:fix": "yarn run format && yarn run lint",
|
||||||
"initialise": "node scripts/initialise.js",
|
"initialise": "node scripts/initialise.js",
|
||||||
|
@ -73,9 +71,9 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "^10.0.3",
|
"@apidevtools/swagger-parser": "^10.0.3",
|
||||||
"@budibase/backend-core": "^1.0.76-alpha.2",
|
"@budibase/backend-core": "^1.0.81-alpha.0",
|
||||||
"@budibase/client": "^1.0.76-alpha.2",
|
"@budibase/client": "^1.0.81-alpha.0",
|
||||||
"@budibase/string-templates": "^1.0.76-alpha.2",
|
"@budibase/string-templates": "^1.0.81-alpha.0",
|
||||||
"@bull-board/api": "^3.7.0",
|
"@bull-board/api": "^3.7.0",
|
||||||
"@bull-board/koa": "^3.7.0",
|
"@bull-board/koa": "^3.7.0",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
@ -110,6 +108,7 @@
|
||||||
"koa-send": "5.0.0",
|
"koa-send": "5.0.0",
|
||||||
"koa-session": "5.12.0",
|
"koa-session": "5.12.0",
|
||||||
"koa-static": "5.0.0",
|
"koa-static": "5.0.0",
|
||||||
|
"koa2-ratelimit": "^1.1.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"memorystream": "^0.3.1",
|
"memorystream": "^0.3.1",
|
||||||
"mongodb": "3.6.3",
|
"mongodb": "3.6.3",
|
||||||
|
@ -125,6 +124,7 @@
|
||||||
"pouchdb-all-dbs": "1.0.2",
|
"pouchdb-all-dbs": "1.0.2",
|
||||||
"pouchdb-find": "^7.2.2",
|
"pouchdb-find": "^7.2.2",
|
||||||
"pouchdb-replication-stream": "1.2.9",
|
"pouchdb-replication-stream": "1.2.9",
|
||||||
|
"redis": "4",
|
||||||
"server-destroy": "1.0.1",
|
"server-destroy": "1.0.1",
|
||||||
"svelte": "^3.38.2",
|
"svelte": "^3.38.2",
|
||||||
"swagger-parser": "^10.0.3",
|
"swagger-parser": "^10.0.3",
|
||||||
|
@ -150,6 +150,7 @@
|
||||||
"@types/koa-router": "^7.4.2",
|
"@types/koa-router": "^7.4.2",
|
||||||
"@types/node": "^15.12.4",
|
"@types/node": "^15.12.4",
|
||||||
"@types/oracledb": "^5.2.1",
|
"@types/oracledb": "^5.2.1",
|
||||||
|
"@types/redis": "^4.0.11",
|
||||||
"@typescript-eslint/parser": "4.28.0",
|
"@typescript-eslint/parser": "4.28.0",
|
||||||
"apidoc": "^0.50.2",
|
"apidoc": "^0.50.2",
|
||||||
"babel-jest": "^27.0.2",
|
"babel-jest": "^27.0.2",
|
||||||
|
@ -157,12 +158,15 @@
|
||||||
"docker-compose": "^0.23.6",
|
"docker-compose": "^0.23.6",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"jest": "^27.0.5",
|
"jest": "^27.0.5",
|
||||||
|
"jest-openapi": "^0.14.2",
|
||||||
"nodemon": "^2.0.4",
|
"nodemon": "^2.0.4",
|
||||||
"openapi-types": "^9.3.1",
|
"openapi-types": "^9.3.1",
|
||||||
|
"openapi-typescript": "^5.2.0",
|
||||||
"path-to-regexp": "^6.2.0",
|
"path-to-regexp": "^6.2.0",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"supertest": "^4.0.2",
|
"supertest": "^4.0.2",
|
||||||
|
"swagger-jsdoc": "^6.1.0",
|
||||||
"ts-jest": "^27.0.3",
|
"ts-jest": "^27.0.3",
|
||||||
"ts-node": "^10.0.0",
|
"ts-node": "^10.0.0",
|
||||||
"typescript": "^4.3.5",
|
"typescript": "^4.3.5",
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
USE master;
|
USE master;
|
||||||
|
|
||||||
|
IF NOT EXISTS(SELECT 1 FROM sys.schemas WHERE name = 'Chains')
|
||||||
|
BEGIN
|
||||||
|
EXEC sys.sp_executesql N'CREATE SCHEMA Chains;'
|
||||||
|
END
|
||||||
|
|
||||||
IF OBJECT_ID ('dbo.products', 'U') IS NOT NULL
|
IF OBJECT_ID ('dbo.products', 'U') IS NOT NULL
|
||||||
DROP TABLE products;
|
DROP TABLE products;
|
||||||
GO
|
GO
|
||||||
|
@ -61,3 +66,15 @@ VALUES ('Bob', '30'),
|
||||||
('Bobert', '99'),
|
('Bobert', '99'),
|
||||||
('Jan', '22'),
|
('Jan', '22'),
|
||||||
('Megan', '11');
|
('Megan', '11');
|
||||||
|
|
||||||
|
|
||||||
|
IF OBJECT_ID ('Chains.sizes', 'U') IS NOT NULL
|
||||||
|
DROP TABLE Chains.sizes;
|
||||||
|
GO
|
||||||
|
CREATE TABLE Chains.sizes
|
||||||
|
(
|
||||||
|
sizeid int IDENTITY(1, 1),
|
||||||
|
name varchar(30),
|
||||||
|
CONSTRAINT pk_size PRIMARY KEY NONCLUSTERED (sizeid)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,9 @@
|
||||||
|
|
||||||
To install oracle express edition simply run `docker-compose up`
|
To install oracle express edition simply run `docker-compose up`
|
||||||
|
|
||||||
- A single instance pluggable database (PDB) will be created named `xepdb`
|
- A single instance pluggable database (PDB) will be created named `xepdb1`
|
||||||
- The default password is configured in the compose file as `oracle`
|
- The default password is configured in the compose file as `oracle`
|
||||||
- The `system`, `sys` and `pdbadmin` users all share this password
|
- The `system` and `pdbadmin` users share this password
|
||||||
|
|
||||||
## Instant Client
|
## Instant Client
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
SELECT 'CREATE DATABASE main'
|
SELECT 'CREATE DATABASE main'
|
||||||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'main')\gexec
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'main')\gexec
|
||||||
|
CREATE SCHEMA test;
|
||||||
|
CREATE TYPE person_job AS ENUM ('qa', 'programmer', 'designer');
|
||||||
CREATE TABLE Persons (
|
CREATE TABLE Persons (
|
||||||
PersonID SERIAL PRIMARY KEY,
|
PersonID SERIAL PRIMARY KEY,
|
||||||
LastName varchar(255),
|
LastName varchar(255),
|
||||||
FirstName varchar(255),
|
FirstName varchar(255),
|
||||||
Address varchar(255),
|
Address varchar(255),
|
||||||
City varchar(255) DEFAULT 'Belfast'
|
City varchar(255) DEFAULT 'Belfast',
|
||||||
|
Type person_job
|
||||||
);
|
);
|
||||||
CREATE TABLE Tasks (
|
CREATE TABLE Tasks (
|
||||||
TaskID SERIAL PRIMARY KEY,
|
TaskID SERIAL PRIMARY KEY,
|
||||||
|
@ -35,8 +38,12 @@ CREATE TABLE Products_Tasks (
|
||||||
REFERENCES Tasks(TaskID),
|
REFERENCES Tasks(TaskID),
|
||||||
PRIMARY KEY (ProductID, TaskID)
|
PRIMARY KEY (ProductID, TaskID)
|
||||||
);
|
);
|
||||||
INSERT INTO Persons (FirstName, LastName, Address, City) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast');
|
CREATE TABLE test.table1 (
|
||||||
INSERT INTO Persons (FirstName, LastName, Address, City) Values ('John', 'Smith', '64 Updown Road', 'Dublin');
|
id SERIAL PRIMARY KEY,
|
||||||
|
Name varchar(255)
|
||||||
|
);
|
||||||
|
INSERT INTO Persons (FirstName, LastName, Address, City, Type) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast', 'qa');
|
||||||
|
INSERT INTO Persons (FirstName, LastName, Address, City, Type) VALUES ('John', 'Smith', '64 Updown Road', 'Dublin', 'programmer');
|
||||||
INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed) VALUES (1, 2, 'assembling', TRUE);
|
INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed) VALUES (1, 2, 'assembling', TRUE);
|
||||||
INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed) VALUES (2, 1, 'processing', FALSE);
|
INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed) VALUES (2, 1, 'processing', FALSE);
|
||||||
INSERT INTO Products (ProductName) VALUES ('Computers');
|
INSERT INTO Products (ProductName) VALUES ('Computers');
|
||||||
|
@ -46,3 +53,4 @@ INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (1, 1);
|
||||||
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (2, 1);
|
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (2, 1);
|
||||||
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (3, 1);
|
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (3, 1);
|
||||||
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (1, 2);
|
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (1, 2);
|
||||||
|
INSERT INTO test.table1 (Name) VALUES ('Test');
|
||||||
|
|
|
@ -2,14 +2,13 @@
|
||||||
* This script just makes it easy to re-create *
|
* This script just makes it easy to re-create *
|
||||||
* a cypress like environment for testing the backend *
|
* a cypress like environment for testing the backend *
|
||||||
******************************************************/
|
******************************************************/
|
||||||
const path = require("path")
|
import path from "path"
|
||||||
const tmpdir = path.join(require("os").tmpdir(), ".budibase")
|
const tmpdir = path.join(require("os").tmpdir(), ".budibase")
|
||||||
|
|
||||||
const MAIN_PORT = "10001"
|
const SERVER_PORT = "4100"
|
||||||
const WORKER_PORT = "10002"
|
const WORKER_PORT = "4200"
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
process.env.PORT = MAIN_PORT
|
|
||||||
process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE"
|
process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE"
|
||||||
process.env.NODE_ENV = "cypress"
|
process.env.NODE_ENV = "cypress"
|
||||||
process.env.ENABLE_ANALYTICS = "false"
|
process.env.ENABLE_ANALYTICS = "false"
|
||||||
|
@ -27,7 +26,11 @@ process.env.ALLOW_DEV_AUTOMATIONS = "1"
|
||||||
|
|
||||||
// don't make this a variable or top level require
|
// don't make this a variable or top level require
|
||||||
// it will cause environment module to be loaded prematurely
|
// it will cause environment module to be loaded prematurely
|
||||||
const server = require("../src/app")
|
|
||||||
|
// override the port with the worker port temporarily
|
||||||
process.env.PORT = WORKER_PORT
|
process.env.PORT = WORKER_PORT
|
||||||
const worker = require("../../worker/src/index")
|
const worker = require("../../worker/src/index")
|
||||||
process.env.PORT = MAIN_PORT
|
|
||||||
|
// override the port with the server port
|
||||||
|
process.env.PORT = SERVER_PORT
|
||||||
|
const server = require("../src/app")
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
const swaggerJsdoc = require("swagger-jsdoc")
|
||||||
|
const { join } = require("path")
|
||||||
|
const { writeFileSync } = require("fs")
|
||||||
|
const { examples, schemas } = require("./resources")
|
||||||
|
const parameters = require("./parameters")
|
||||||
|
const security = require("./security")
|
||||||
|
|
||||||
|
const VARIABLES = {}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
definition: {
|
||||||
|
openapi: "3.0.0",
|
||||||
|
info: {
|
||||||
|
title: "Budibase API",
|
||||||
|
description: "The public API for Budibase apps and its services.",
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: "https://budibase.app/api/public/v1",
|
||||||
|
description: "Budibase Cloud API",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "{protocol}://{hostname}/api/public/v1",
|
||||||
|
description: "Budibase self hosted API",
|
||||||
|
variables: {
|
||||||
|
protocol: {
|
||||||
|
default: "http",
|
||||||
|
description:
|
||||||
|
"Whether HTTP or HTTPS should be used to communicate with your Budibase instance.",
|
||||||
|
},
|
||||||
|
hostname: {
|
||||||
|
default: "localhost:10000",
|
||||||
|
description: "The URL of your Budibase instance.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
parameters: {
|
||||||
|
...parameters,
|
||||||
|
},
|
||||||
|
examples: {
|
||||||
|
...examples,
|
||||||
|
},
|
||||||
|
securitySchemes: {
|
||||||
|
...security,
|
||||||
|
},
|
||||||
|
schemas: {
|
||||||
|
...schemas,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
ApiKeyAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
format: ".json",
|
||||||
|
apis: [join(__dirname, "..", "src", "api", "routes", "public", "*.ts")],
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFile(output, filename) {
|
||||||
|
try {
|
||||||
|
const path = join(__dirname, filename)
|
||||||
|
let spec = output
|
||||||
|
if (filename.endsWith("json")) {
|
||||||
|
spec = JSON.stringify(output, null, 2)
|
||||||
|
}
|
||||||
|
// input the static variables
|
||||||
|
for (let [key, replacement] of Object.entries(VARIABLES)) {
|
||||||
|
spec = spec.replace(new RegExp(`{${key}}`, "g"), replacement)
|
||||||
|
}
|
||||||
|
writeFileSync(path, spec)
|
||||||
|
console.log(`Wrote spec to ${path}`)
|
||||||
|
return path
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
const outputJSON = swaggerJsdoc(options)
|
||||||
|
options.format = ".yaml"
|
||||||
|
const outputYAML = swaggerJsdoc(options)
|
||||||
|
writeFile(outputJSON, "openapi.json")
|
||||||
|
return writeFile(outputYAML, "openapi.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = run
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue