Merge branch 'develop' into feature/day-pass-pricing
This commit is contained in:
commit
8cfd6a9faa
|
@ -65,7 +65,7 @@ Budibase is open-source - licensed as GPL v3. This should fill you with confiden
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
### Load data or start from scratch
|
### Load data or start from scratch
|
||||||
Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no datasources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png">
|
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png">
|
||||||
|
|
|
@ -348,7 +348,7 @@ export interface paths {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
responses: {
|
responses: {
|
||||||
/** Returns the created table, including the ID which has been generated for it. This can be internal or external data sources. */
|
/** Returns the created table, including the ID which has been generated for it. This can be internal or external datasources. */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["tableOutput"]
|
"application/json": components["schemas"]["tableOutput"]
|
||||||
|
@ -959,7 +959,7 @@ export interface components {
|
||||||
query: {
|
query: {
|
||||||
/** @description The ID of the query. */
|
/** @description The ID of the query. */
|
||||||
_id: string
|
_id: string
|
||||||
/** @description The ID of the data source the query belongs to. */
|
/** @description The ID of the datasource the query belongs to. */
|
||||||
datasourceId?: string
|
datasourceId?: string
|
||||||
/** @description The bindings which are required to perform this query. */
|
/** @description The bindings which are required to perform this query. */
|
||||||
parameters?: string[]
|
parameters?: string[]
|
||||||
|
@ -983,7 +983,7 @@ export interface components {
|
||||||
data: {
|
data: {
|
||||||
/** @description The ID of the query. */
|
/** @description The ID of the query. */
|
||||||
_id: string
|
_id: string
|
||||||
/** @description The ID of the data source the query belongs to. */
|
/** @description The ID of the datasource the query belongs to. */
|
||||||
datasourceId?: string
|
datasourceId?: string
|
||||||
/** @description The bindings which are required to perform this query. */
|
/** @description The bindings which are required to perform this query. */
|
||||||
parameters?: string[]
|
parameters?: string[]
|
||||||
|
|
|
@ -65,10 +65,6 @@ http {
|
||||||
proxy_pass http://{{ address }}:4001;
|
proxy_pass http://{{ address }}:4001;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /preview {
|
|
||||||
proxy_pass http://{{ address }}:4001;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /builder {
|
location /builder {
|
||||||
proxy_pass http://{{ address }}:3000;
|
proxy_pass http://{{ address }}:3000;
|
||||||
rewrite ^/builder(.*)$ /builder/$1 break;
|
rewrite ^/builder(.*)$ /builder/$1 break;
|
||||||
|
@ -84,9 +80,18 @@ http {
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /vite {
|
location /vite/ {
|
||||||
proxy_pass http://{{ address }}:3000;
|
proxy_pass http://{{ address }}:3000;
|
||||||
rewrite ^/vite(.*)$ /$1 break;
|
rewrite ^/vite(.*)$ /$1 break;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /socket/ {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_pass http://{{ address }}:4001;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
|
|
@ -88,10 +88,6 @@ http {
|
||||||
proxy_pass http://$apps:4002;
|
proxy_pass http://$apps:4002;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /preview {
|
|
||||||
proxy_pass http://$apps:4002;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = / {
|
location = / {
|
||||||
proxy_pass http://$apps:4002;
|
proxy_pass http://$apps:4002;
|
||||||
}
|
}
|
||||||
|
@ -162,6 +158,15 @@ http {
|
||||||
rewrite ^/db/(.*)$ /$1 break;
|
rewrite ^/db/(.*)$ /$1 break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /socket/ {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_pass http://$apps:4002;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
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;
|
||||||
|
|
|
@ -4,9 +4,9 @@ echo ${TARGETBUILD} > /buildtarget.txt
|
||||||
if [[ "${TARGETBUILD}" = "aas" ]]; then
|
if [[ "${TARGETBUILD}" = "aas" ]]; then
|
||||||
# Azure AppService uses /home for persisent data & SSH on port 2222
|
# Azure AppService uses /home for persisent data & SSH on port 2222
|
||||||
DATA_DIR=/home
|
DATA_DIR=/home
|
||||||
mkdir -p $DATA_DIR/{search,minio,couchdb}
|
mkdir -p $DATA_DIR/{search,minio,couch}
|
||||||
mkdir -p $DATA_DIR/couchdb/{dbs,views}
|
mkdir -p $DATA_DIR/couch/{dbs,views}
|
||||||
chown -R couchdb:couchdb $DATA_DIR/couchdb/
|
chown -R couchdb:couchdb $DATA_DIR/couch/
|
||||||
apt update
|
apt update
|
||||||
apt-get install -y openssh-server
|
apt-get install -y openssh-server
|
||||||
sed -i "s/#Port 22/Port 2222/" /etc/ssh/sshd_config
|
sed -i "s/#Port 22/Port 2222/" /etc/ssh/sshd_config
|
||||||
|
@ -16,5 +16,4 @@ if [[ "${TARGETBUILD}" = "aas" ]]; then
|
||||||
else
|
else
|
||||||
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
|
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
|
||||||
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
|
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
|
||||||
|
|
||||||
fi
|
fi
|
|
@ -1,5 +1,5 @@
|
||||||
; CouchDB Configuration Settings
|
; CouchDB Configuration Settings
|
||||||
|
|
||||||
[couchdb]
|
[couchdb]
|
||||||
database_dir = DATA_DIR/couchdb/dbs
|
database_dir = DATA_DIR/couch/dbs
|
||||||
view_index_dir = DATA_DIR/couchdb/views
|
view_index_dir = DATA_DIR/couch/views
|
||||||
|
|
|
@ -66,6 +66,15 @@ server {
|
||||||
rewrite ^/db/(.*)$ /$1 break;
|
rewrite ^/db/(.*)$ /$1 break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /socket/ {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_pass http://127.0.0.1:4001;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
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;
|
||||||
|
|
|
@ -36,10 +36,10 @@ fi
|
||||||
export COUCH_DB_URL=http://$COUCHDB_USER:$COUCHDB_PASSWORD@localhost:5984
|
export COUCH_DB_URL=http://$COUCHDB_USER:$COUCHDB_PASSWORD@localhost:5984
|
||||||
|
|
||||||
# make these directories in runner, incase of mount
|
# make these directories in runner, incase of mount
|
||||||
mkdir -p ${DATA_DIR}/couchdb/{dbs,views}
|
mkdir -p ${DATA_DIR}/couch/{dbs,views}
|
||||||
mkdir -p ${DATA_DIR}/minio
|
mkdir -p ${DATA_DIR}/minio
|
||||||
mkdir -p ${DATA_DIR}/search
|
mkdir -p ${DATA_DIR}/search
|
||||||
chown -R couchdb:couchdb ${DATA_DIR}/couchdb
|
chown -R couchdb:couchdb ${DATA_DIR}/couch
|
||||||
redis-server --requirepass $REDIS_PASSWORD &
|
redis-server --requirepass $REDIS_PASSWORD &
|
||||||
/opt/clouseau/bin/clouseau &
|
/opt/clouseau/bin/clouseau &
|
||||||
/minio/minio server ${DATA_DIR}/minio &
|
/minio/minio server ${DATA_DIR}/minio &
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.3.12-alpha.3",
|
"version": "1.3.15-alpha.9",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "1.3.12-alpha.3",
|
"version": "1.3.15-alpha.9",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
|
@ -20,11 +20,12 @@
|
||||||
"test:watch": "jest --watchAll"
|
"test:watch": "jest --watchAll"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/types": "1.3.12-alpha.3",
|
"@budibase/types": "1.3.15-alpha.9",
|
||||||
"@shopify/jest-koa-mocks": "5.0.1",
|
"@shopify/jest-koa-mocks": "5.0.1",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
"aws-sdk": "2.1030.0",
|
"aws-sdk": "2.1030.0",
|
||||||
"bcrypt": "5.0.1",
|
"bcrypt": "5.0.1",
|
||||||
|
"bcryptjs": "2.4.3",
|
||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
"emitter-listener": "1.1.2",
|
"emitter-listener": "1.1.2",
|
||||||
"ioredis": "4.28.0",
|
"ioredis": "4.28.0",
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
...require("./src/plugin"),
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import { dangerousGetDB, closeDB } from "."
|
import { dangerousGetDB, closeDB } from "."
|
||||||
|
import { DocumentType } from "./constants"
|
||||||
|
|
||||||
class Replication {
|
class Replication {
|
||||||
source: any
|
source: any
|
||||||
|
@ -53,6 +54,14 @@ class Replication {
|
||||||
return this.replication
|
return this.replication
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appReplicateOpts() {
|
||||||
|
return {
|
||||||
|
filter: (doc: any) => {
|
||||||
|
return doc._id !== DocumentType.APP_METADATA
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rollback the target DB back to the state of the source DB
|
* Rollback the target DB back to the state of the source DB
|
||||||
*/
|
*/
|
||||||
|
@ -60,6 +69,7 @@ class Replication {
|
||||||
await this.target.destroy()
|
await this.target.destroy()
|
||||||
// Recreate the DB again
|
// Recreate the DB again
|
||||||
this.target = dangerousGetDB(this.target.name)
|
this.target = dangerousGetDB(this.target.name)
|
||||||
|
// take the opportunity to remove deleted tombstones
|
||||||
await this.replicate()
|
await this.replicate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -254,7 +254,16 @@ export async function getAllApps({ dev, all, idsOnly, efficient }: any = {}) {
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
if (idsOnly) {
|
if (idsOnly) {
|
||||||
return appDbNames
|
const devAppIds = appDbNames.filter(appId => isDevAppID(appId))
|
||||||
|
const prodAppIds = appDbNames.filter(appId => !isDevAppID(appId))
|
||||||
|
switch (dev) {
|
||||||
|
case true:
|
||||||
|
return devAppIds
|
||||||
|
case false:
|
||||||
|
return prodAppIds
|
||||||
|
default:
|
||||||
|
return appDbNames
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const appPromises = appDbNames.map((app: any) =>
|
const appPromises = appDbNames.map((app: any) =>
|
||||||
// skip setup otherwise databases could be re-created
|
// skip setup otherwise databases could be re-created
|
||||||
|
|
|
@ -19,6 +19,7 @@ if (!LOADED && isDev() && !isTest()) {
|
||||||
const env = {
|
const env = {
|
||||||
isTest,
|
isTest,
|
||||||
isDev,
|
isDev,
|
||||||
|
JS_BCRYPT: process.env.JS_BCRYPT,
|
||||||
JWT_SECRET: process.env.JWT_SECRET,
|
JWT_SECRET: process.env.JWT_SECRET,
|
||||||
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
||||||
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const bcrypt = require("bcrypt")
|
|
||||||
const env = require("./environment")
|
const env = require("./environment")
|
||||||
|
const bcrypt = env.JS_BCRYPT ? require("bcryptjs") : require("bcrypt")
|
||||||
const { v4 } = require("uuid")
|
const { v4 } = require("uuid")
|
||||||
|
|
||||||
const SALT_ROUNDS = env.SALT_ROUNDS || 10
|
const SALT_ROUNDS = env.SALT_ROUNDS || 10
|
||||||
|
|
|
@ -17,6 +17,7 @@ import * as dbConstants from "./db/constants"
|
||||||
import * as logging from "./logging"
|
import * as logging from "./logging"
|
||||||
import pino from "./pino"
|
import pino from "./pino"
|
||||||
import * as middleware from "./middleware"
|
import * as middleware from "./middleware"
|
||||||
|
import plugins from "./plugin"
|
||||||
|
|
||||||
// mimic the outer package exports
|
// mimic the outer package exports
|
||||||
import * as db from "./pkg/db"
|
import * as db from "./pkg/db"
|
||||||
|
@ -55,6 +56,7 @@ const core = {
|
||||||
errors,
|
errors,
|
||||||
logging,
|
logging,
|
||||||
roles,
|
roles,
|
||||||
|
plugins,
|
||||||
...pino,
|
...pino,
|
||||||
...errorClasses,
|
...errorClasses,
|
||||||
middleware,
|
middleware,
|
||||||
|
|
|
@ -307,9 +307,13 @@ export const uploadDirectory = async (
|
||||||
return files
|
return files
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.downloadTarballDirect = async (url: string, path: string) => {
|
exports.downloadTarballDirect = async (
|
||||||
|
url: string,
|
||||||
|
path: string,
|
||||||
|
headers = {}
|
||||||
|
) => {
|
||||||
path = sanitizeKey(path)
|
path = sanitizeKey(path)
|
||||||
const response = await fetch(url)
|
const response = await fetch(url, { headers })
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`unexpected response ${response.statusText}`)
|
throw new Error(`unexpected response ${response.statusText}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import * as utils from "./utils"
|
||||||
|
|
||||||
|
const pkg = {
|
||||||
|
...utils,
|
||||||
|
}
|
||||||
|
|
||||||
|
export = pkg
|
|
@ -1,5 +1,8 @@
|
||||||
const { PluginTypes } = require("./constants")
|
const {
|
||||||
const { DatasourceFieldType, QueryType } = require("@budibase/types")
|
DatasourceFieldType,
|
||||||
|
QueryType,
|
||||||
|
PluginType,
|
||||||
|
} = require("@budibase/types")
|
||||||
const joi = require("joi")
|
const joi = require("joi")
|
||||||
|
|
||||||
const DATASOURCE_TYPES = [
|
const DATASOURCE_TYPES = [
|
||||||
|
@ -78,11 +81,11 @@ function validateDatasource(schema) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.validate = schema => {
|
exports.validate = schema => {
|
||||||
switch (schema.type) {
|
switch (schema?.type) {
|
||||||
case PluginTypes.COMPONENT:
|
case PluginType.COMPONENT:
|
||||||
validateComponent(schema)
|
validateComponent(schema)
|
||||||
break
|
break
|
||||||
case PluginTypes.DATASOURCE:
|
case PluginType.DATASOURCE:
|
||||||
validateDatasource(schema)
|
validateDatasource(schema)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
|
@ -1377,6 +1377,11 @@ bcrypt@5.0.1:
|
||||||
"@mapbox/node-pre-gyp" "^1.0.0"
|
"@mapbox/node-pre-gyp" "^1.0.0"
|
||||||
node-addon-api "^3.1.0"
|
node-addon-api "^3.1.0"
|
||||||
|
|
||||||
|
bcryptjs@2.4.3:
|
||||||
|
version "2.4.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
|
||||||
|
integrity sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==
|
||||||
|
|
||||||
binary-extensions@^2.0.0:
|
binary-extensions@^2.0.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||||
|
|
|
@ -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.3.12-alpha.3",
|
"version": "1.3.15-alpha.9",
|
||||||
"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.3.12-alpha.3",
|
"@budibase/string-templates": "1.3.15-alpha.9",
|
||||||
"@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",
|
||||||
|
|
|
@ -4,10 +4,15 @@
|
||||||
|
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
export let tooltip = ""
|
export let tooltip = ""
|
||||||
|
export let muted
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TooltipWrapper {tooltip} {size}>
|
<TooltipWrapper {tooltip} {size}>
|
||||||
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}>
|
<label
|
||||||
|
class:muted
|
||||||
|
for=""
|
||||||
|
class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</label>
|
</label>
|
||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
|
@ -17,4 +22,8 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -24,7 +24,6 @@
|
||||||
export let secondaryAction = undefined
|
export let secondaryAction = undefined
|
||||||
export let secondaryButtonWarning = false
|
export let secondaryButtonWarning = false
|
||||||
export let dataCy = null
|
export let dataCy = null
|
||||||
|
|
||||||
const { hide, cancel } = getContext(Context.Modal)
|
const { hide, cancel } = getContext(Context.Modal)
|
||||||
let loading = false
|
let loading = false
|
||||||
$: confirmDisabled = disabled || loading
|
$: confirmDisabled = disabled || loading
|
||||||
|
@ -88,12 +87,11 @@
|
||||||
<section class="spectrum-Dialog-content content-grid">
|
<section class="spectrum-Dialog-content content-grid">
|
||||||
<slot />
|
<slot />
|
||||||
</section>
|
</section>
|
||||||
{#if showCancelButton || showConfirmButton}
|
{#if showCancelButton || showConfirmButton || $$slots.footer}
|
||||||
<div
|
<div
|
||||||
class="spectrum-ButtonGroup spectrum-Dialog-buttonGroup spectrum-Dialog-buttonGroup--noFooter"
|
class="spectrum-ButtonGroup spectrum-Dialog-buttonGroup spectrum-Dialog-buttonGroup--noFooter"
|
||||||
>
|
>
|
||||||
<slot name="footer" />
|
<slot name="footer" />
|
||||||
|
|
||||||
{#if showSecondaryButton && secondaryButtonText && secondaryAction}
|
{#if showSecondaryButton && secondaryButtonText && secondaryAction}
|
||||||
<div class="secondary-action">
|
<div class="secondary-action">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
export let noHorizPadding = false
|
export let noHorizPadding = false
|
||||||
export let quiet = false
|
export let quiet = false
|
||||||
export let emphasized = false
|
export let emphasized = false
|
||||||
|
export let onTop = false
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
|
|
||||||
let thisSelected = undefined
|
let thisSelected = undefined
|
||||||
|
@ -75,6 +76,7 @@
|
||||||
bind:this={container}
|
bind:this={container}
|
||||||
class:spectrum-Tabs--quiet={quiet}
|
class:spectrum-Tabs--quiet={quiet}
|
||||||
class:noHorizPadding
|
class:noHorizPadding
|
||||||
|
class:onTop
|
||||||
class:spectrum-Tabs--vertical={vertical}
|
class:spectrum-Tabs--vertical={vertical}
|
||||||
class:spectrum-Tabs--horizontal={!vertical}
|
class:spectrum-Tabs--horizontal={!vertical}
|
||||||
class="spectrum-Tabs spectrum-Tabs--size{size}"
|
class="spectrum-Tabs spectrum-Tabs--size{size}"
|
||||||
|
@ -122,4 +124,7 @@
|
||||||
.noPadding {
|
.noPadding {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.onTop {
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -82,10 +82,10 @@ filterTests(['smoke', 'all'], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (Cypress.env("TEST_ENV")) {
|
if (Cypress.env("TEST_ENV")) {
|
||||||
it("should generate data source screens", () => {
|
it("should generate datasource screens", () => {
|
||||||
// Using MySQL data source for testing this
|
// Using MySQL datasource for testing this
|
||||||
const datasource = "MySQL"
|
const datasource = "MySQL"
|
||||||
// Select & configure MySQL data source
|
// Select & configure MySQL datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
cy.addDatasourceConfig(datasource)
|
cy.addDatasourceConfig(datasource)
|
||||||
// Create Autogenerated screens from a MySQL table - MySQL contains books table
|
// Create Autogenerated screens from a MySQL table - MySQL contains books table
|
||||||
|
|
|
@ -11,8 +11,8 @@ filterTests(["all"], () => {
|
||||||
const queryName = "Cypress Test Query"
|
const queryName = "Cypress Test Query"
|
||||||
const queryRename = "CT Query Rename"
|
const queryRename = "CT Query Rename"
|
||||||
|
|
||||||
it("Should add MySQL data source without configuration", () => {
|
it("Should add MySQL datasource without configuration", () => {
|
||||||
// Select MySQL data source
|
// Select MySQL datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
// Attempt to fetch tables without applying configuration
|
// Attempt to fetch tables without applying configuration
|
||||||
cy.intercept("**/datasources").as("datasource")
|
cy.intercept("**/datasources").as("datasource")
|
||||||
|
@ -35,8 +35,8 @@ filterTests(["all"], () => {
|
||||||
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should add MySQL data source and fetch tables", () => {
|
it("should add MySQL datasource and fetch tables", () => {
|
||||||
// Add & configure MySQL data source
|
// Add & configure MySQL datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
cy.intercept("**/datasources").as("datasource")
|
cy.intercept("**/datasources").as("datasource")
|
||||||
cy.addDatasourceConfig(datasource)
|
cy.addDatasourceConfig(datasource)
|
||||||
|
@ -52,7 +52,7 @@ filterTests(["all"], () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should check table fetching error", () => {
|
it("should check table fetching error", () => {
|
||||||
// MySQL test data source contains tables without primary keys
|
// MySQL test datasource contains tables without primary keys
|
||||||
cy.get(".spectrum-InLineAlert")
|
cy.get(".spectrum-InLineAlert")
|
||||||
.should("contain", "Error fetching tables")
|
.should("contain", "Error fetching tables")
|
||||||
.and("contain", "No primary key constraint found")
|
.and("contain", "No primary key constraint found")
|
||||||
|
|
|
@ -11,8 +11,8 @@ filterTests(["all"], () => {
|
||||||
const queryName = "Cypress Test Query"
|
const queryName = "Cypress Test Query"
|
||||||
const queryRename = "CT Query Rename"
|
const queryRename = "CT Query Rename"
|
||||||
|
|
||||||
it("Should add Oracle data source and skip table fetch", () => {
|
it("Should add Oracle datasource and skip table fetch", () => {
|
||||||
// Select Oracle data source
|
// Select Oracle datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
// Skip table fetch - no config added
|
// Skip table fetch - no config added
|
||||||
cy.get(".spectrum-Button")
|
cy.get(".spectrum-Button")
|
||||||
|
@ -23,7 +23,7 @@ filterTests(["all"], () => {
|
||||||
cy.get(".spectrum-Textfield-input", { timeout: 500 })
|
cy.get(".spectrum-Textfield-input", { timeout: 500 })
|
||||||
.eq(1)
|
.eq(1)
|
||||||
.should("have.value", "localhost")
|
.should("have.value", "localhost")
|
||||||
// Add another Oracle data source, configure & skip table fetch
|
// Add another Oracle datasource, configure & skip table fetch
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
cy.addDatasourceConfig(datasource, true)
|
cy.addDatasourceConfig(datasource, true)
|
||||||
// Confirm config and no tables
|
// Confirm config and no tables
|
||||||
|
@ -33,8 +33,8 @@ filterTests(["all"], () => {
|
||||||
cy.get(".spectrum-Body").eq(2).should("contain", "No tables found.")
|
cy.get(".spectrum-Body").eq(2).should("contain", "No tables found.")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Should add Oracle data source and fetch tables without configuration", () => {
|
it("Should add Oracle datasource and fetch tables without configuration", () => {
|
||||||
// Select Oracle data source
|
// Select Oracle datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
// Attempt to fetch tables without applying configuration
|
// Attempt to fetch tables without applying configuration
|
||||||
cy.intercept("**/datasources").as("datasource")
|
cy.intercept("**/datasources").as("datasource")
|
||||||
|
@ -49,8 +49,8 @@ filterTests(["all"], () => {
|
||||||
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
xit("should add Oracle data source and fetch tables", () => {
|
xit("should add Oracle datasource and fetch tables", () => {
|
||||||
// Add & configure Oracle data source
|
// Add & configure Oracle datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
cy.intercept("**/datasources").as("datasource")
|
cy.intercept("**/datasources").as("datasource")
|
||||||
cy.addDatasourceConfig(datasource)
|
cy.addDatasourceConfig(datasource)
|
||||||
|
|
|
@ -11,8 +11,8 @@ filterTests(["all"], () => {
|
||||||
const queryName = "Cypress Test Query"
|
const queryName = "Cypress Test Query"
|
||||||
const queryRename = "CT Query Rename"
|
const queryRename = "CT Query Rename"
|
||||||
|
|
||||||
xit("Should add PostgreSQL data source without configuration", () => {
|
xit("Should add PostgreSQL datasource without configuration", () => {
|
||||||
// Select PostgreSQL data source
|
// Select PostgreSQL datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
// Attempt to fetch tables without applying configuration
|
// Attempt to fetch tables without applying configuration
|
||||||
cy.intercept("**/datasources").as("datasource")
|
cy.intercept("**/datasources").as("datasource")
|
||||||
|
@ -27,8 +27,8 @@ filterTests(["all"], () => {
|
||||||
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should add PostgreSQL data source and fetch tables", () => {
|
it("should add PostgreSQL datasource and fetch tables", () => {
|
||||||
// Add & configure PostgreSQL data source
|
// Add & configure PostgreSQL datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
cy.intercept("**/datasources").as("datasource")
|
cy.intercept("**/datasources").as("datasource")
|
||||||
cy.addDatasourceConfig(datasource)
|
cy.addDatasourceConfig(datasource)
|
||||||
|
|
|
@ -10,8 +10,8 @@ filterTests(["smoke", "all"], () => {
|
||||||
const datasource = "REST"
|
const datasource = "REST"
|
||||||
const restUrl = "https://api.openbrewerydb.org/breweries"
|
const restUrl = "https://api.openbrewerydb.org/breweries"
|
||||||
|
|
||||||
it("Should add REST data source with incorrect API", () => {
|
it("Should add REST datasource with incorrect API", () => {
|
||||||
// Select REST data source
|
// Select REST datasource
|
||||||
cy.selectExternalDatasource(datasource)
|
cy.selectExternalDatasource(datasource)
|
||||||
// Enter incorrect api & attempt to send query
|
// Enter incorrect api & attempt to send query
|
||||||
cy.get(".query-buttons", { timeout: 1000 }).contains("Add query").click({ force: true })
|
cy.get(".query-buttons", { timeout: 1000 }).contains("Add query").click({ force: true })
|
||||||
|
|
|
@ -763,7 +763,7 @@ Cypress.Commands.add("navigateToDataSection", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("navigateToAutogeneratedModal", () => {
|
Cypress.Commands.add("navigateToAutogeneratedModal", () => {
|
||||||
// Screen name must already exist within data source
|
// Screen name must already exist within datasource
|
||||||
cy.contains("Design").click()
|
cy.contains("Design").click()
|
||||||
cy.get(".spectrum-Button").contains("Add screen").click({ force: true })
|
cy.get(".spectrum-Button").contains("Add screen").click({ force: true })
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
@ -779,7 +779,7 @@ Cypress.Commands.add("navigateToAutogeneratedModal", () => {
|
||||||
Cypress.Commands.add("selectExternalDatasource", datasourceName => {
|
Cypress.Commands.add("selectExternalDatasource", datasourceName => {
|
||||||
// Navigates to Data Section
|
// Navigates to Data Section
|
||||||
cy.navigateToDataSection()
|
cy.navigateToDataSection()
|
||||||
// Open Data Source modal
|
// Open Datasource modal
|
||||||
cy.get(".nav").within(() => {
|
cy.get(".nav").within(() => {
|
||||||
cy.get(".add-button").click()
|
cy.get(".add-button").click()
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.3.12-alpha.3",
|
"version": "1.3.15-alpha.9",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -69,10 +69,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "1.3.12-alpha.3",
|
"@budibase/bbui": "1.3.15-alpha.9",
|
||||||
"@budibase/client": "1.3.12-alpha.3",
|
"@budibase/client": "1.3.15-alpha.9",
|
||||||
"@budibase/frontend-core": "1.3.12-alpha.3",
|
"@budibase/frontend-core": "1.3.15-alpha.9",
|
||||||
"@budibase/string-templates": "1.3.12-alpha.3",
|
"@budibase/string-templates": "1.3.15-alpha.9",
|
||||||
"@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",
|
||||||
|
|
|
@ -27,7 +27,7 @@ export async function saveDatasource(config, skipFetch = false) {
|
||||||
// Create datasource
|
// Create datasource
|
||||||
const resp = await datasources.save(datasource, !skipFetch && datasource.plus)
|
const resp = await datasources.save(datasource, !skipFetch && datasource.plus)
|
||||||
|
|
||||||
// update the tables incase data source plus
|
// update the tables incase datasource plus
|
||||||
await tables.fetch()
|
await tables.fetch()
|
||||||
await datasources.select(resp._id)
|
await datasources.select(resp._id)
|
||||||
return resp
|
return resp
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button, Select, Input, Label } from "@budibase/bbui"
|
import { Button, Select, Input, Label } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { onMount, createEventDispatcher } from "svelte"
|
||||||
|
import { flags } from "stores/backend"
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
@ -29,11 +30,16 @@
|
||||||
label: "Every Night at Midnight",
|
label: "Every Night at Midnight",
|
||||||
value: "0 0 * * *",
|
value: "0 0 * * *",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "Every Budibase Reboot",
|
|
||||||
value: "@reboot",
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!$flags.cloud) {
|
||||||
|
CRON_EXPRESSIONS.push({
|
||||||
|
label: "Every Budibase Reboot",
|
||||||
|
value: "@reboot",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="block-field">
|
<div class="block-field">
|
||||||
|
|
|
@ -32,8 +32,8 @@
|
||||||
: []
|
: []
|
||||||
$: openDataSource = enrichedDataSources.find(x => x.open)
|
$: openDataSource = enrichedDataSources.find(x => x.open)
|
||||||
$: {
|
$: {
|
||||||
// Ensure the open data source is always included in the list of open
|
// Ensure the open datasource is always included in the list of open
|
||||||
// data sources
|
// datasources
|
||||||
if (openDataSource) {
|
if (openDataSource) {
|
||||||
openNode(openDataSource)
|
openNode(openDataSource)
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,7 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
const containsActiveEntity = datasource => {
|
const containsActiveEntity = datasource => {
|
||||||
// If we're view a query then the data source ID is in the URL
|
// If we're view a query then the datasource ID is in the URL
|
||||||
if ($params.selectedDatasource === datasource._id) {
|
if ($params.selectedDatasource === datasource._id) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
notifications,
|
notifications,
|
||||||
Modal,
|
Modal,
|
||||||
Table,
|
Table,
|
||||||
|
Toggle,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { datasources, integrations, tables } from "stores/backend"
|
import { datasources, integrations, tables } from "stores/backend"
|
||||||
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
|
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
|
||||||
|
@ -15,6 +16,7 @@
|
||||||
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
import ValuesList from "components/common/ValuesList.svelte"
|
||||||
|
|
||||||
export let datasource
|
export let datasource
|
||||||
export let save
|
export let save
|
||||||
|
@ -31,6 +33,8 @@
|
||||||
let createExternalTableModal
|
let createExternalTableModal
|
||||||
let selectedFromRelationship, selectedToRelationship
|
let selectedFromRelationship, selectedToRelationship
|
||||||
let confirmDialog
|
let confirmDialog
|
||||||
|
let specificTables = null
|
||||||
|
let requireSpecificTables = false
|
||||||
|
|
||||||
$: integration = datasource && $integrations[datasource.source]
|
$: integration = datasource && $integrations[datasource.source]
|
||||||
$: plusTables = datasource?.plus
|
$: plusTables = datasource?.plus
|
||||||
|
@ -87,7 +91,7 @@
|
||||||
|
|
||||||
async function updateDatasourceSchema() {
|
async function updateDatasourceSchema() {
|
||||||
try {
|
try {
|
||||||
await datasources.updateSchema(datasource)
|
await datasources.updateSchema(datasource, specificTables)
|
||||||
notifications.success(`Datasource ${name} tables updated successfully.`)
|
notifications.success(`Datasource ${name} tables updated successfully.`)
|
||||||
await tables.fetch()
|
await tables.fetch()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -150,6 +154,19 @@
|
||||||
warning={false}
|
warning={false}
|
||||||
title="Confirm table fetch"
|
title="Confirm table fetch"
|
||||||
>
|
>
|
||||||
|
<Toggle
|
||||||
|
bind:value={requireSpecificTables}
|
||||||
|
on:change={e => {
|
||||||
|
requireSpecificTables = e.detail
|
||||||
|
specificTables = null
|
||||||
|
}}
|
||||||
|
thin
|
||||||
|
text="Fetch listed tables only (one per line)"
|
||||||
|
/>
|
||||||
|
{#if requireSpecificTables}
|
||||||
|
<ValuesList label="" bind:values={specificTables} />
|
||||||
|
{/if}
|
||||||
|
<br />
|
||||||
<Body>
|
<Body>
|
||||||
If you have fetched tables from this database before, this action may
|
If you have fetched tables from this database before, this action may
|
||||||
overwrite any changes you made after your initial fetch.
|
overwrite any changes you made after your initial fetch.
|
||||||
|
|
|
@ -126,7 +126,7 @@
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
disabled={!Object.keys(integration).length}
|
disabled={!Object.keys(integration).length}
|
||||||
title="Add data source"
|
title="Add datasource"
|
||||||
confirmText="Continue"
|
confirmText="Continue"
|
||||||
showSecondaryButton={showImportButton}
|
showSecondaryButton={showImportButton}
|
||||||
secondaryButtonText="Import"
|
secondaryButtonText="Import"
|
||||||
|
@ -155,7 +155,7 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Body size="S">Connect to an external data source</Body>
|
<Body size="S">Connect to an external datasource</Body>
|
||||||
<div class="item-list">
|
<div class="item-list">
|
||||||
{#each Object.entries(integrations).filter(([key, val]) => key !== IntegrationTypes.INTERNAL && !val.custom) as [integrationType, schema]}
|
{#each Object.entries(integrations).filter(([key, val]) => key !== IntegrationTypes.INTERNAL && !val.custom) as [integrationType, schema]}
|
||||||
<DatasourceCard
|
<DatasourceCard
|
||||||
|
@ -170,7 +170,7 @@
|
||||||
|
|
||||||
{#if customIntegrations.length > 0}
|
{#if customIntegrations.length > 0}
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Body size="S">Custom data source</Body>
|
<Body size="S">Custom datasource</Body>
|
||||||
<div class="item-list">
|
<div class="item-list">
|
||||||
{#each customIntegrations as [integrationType, schema]}
|
{#each customIntegrations as [integrationType, schema]}
|
||||||
<DatasourceCard
|
<DatasourceCard
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let bindingDrawer
|
let bindingDrawer
|
||||||
let valid = true
|
let valid = true
|
||||||
|
let currentVal = value
|
||||||
|
|
||||||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||||
$: tempValue = readableValue
|
$: tempValue = readableValue
|
||||||
|
@ -30,11 +31,17 @@
|
||||||
|
|
||||||
const saveBinding = () => {
|
const saveBinding = () => {
|
||||||
onChange(tempValue)
|
onChange(tempValue)
|
||||||
|
onBlur()
|
||||||
bindingDrawer.hide()
|
bindingDrawer.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChange = value => {
|
const onChange = value => {
|
||||||
dispatch("change", readableToRuntimeBinding(bindings, value))
|
currentVal = readableToRuntimeBinding(bindings, value)
|
||||||
|
dispatch("change", currentVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBlur = () => {
|
||||||
|
dispatch("blur", currentVal)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -45,6 +52,7 @@
|
||||||
readonly={isJS}
|
readonly={isJS}
|
||||||
value={isJS ? "(JavaScript function)" : readableValue}
|
value={isJS ? "(JavaScript function)" : readableValue}
|
||||||
on:change={event => onChange(event.detail)}
|
on:change={event => onChange(event.detail)}
|
||||||
|
on:blur={onBlur}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
{updateOnChange}
|
{updateOnChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
newBlock.inputs = {
|
newBlock.inputs = {
|
||||||
fields: Object.keys(parameters.fields).reduce((fields, key) => {
|
fields: Object.keys(parameters.fields ?? {}).reduce((fields, key) => {
|
||||||
fields[key] = "string"
|
fields[key] = "string"
|
||||||
return fields
|
return fields
|
||||||
}, {}),
|
}, {}),
|
||||||
|
|
|
@ -73,13 +73,13 @@
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Choose the data source that provides the row you would like to duplicate.
|
Choose the datasource that provides the row you would like to duplicate.
|
||||||
<br />
|
<br />
|
||||||
You can always add or override fields manually.
|
You can always add or override fields manually.
|
||||||
</Body>
|
</Body>
|
||||||
|
|
||||||
<div class="params">
|
<div class="params">
|
||||||
<Label small>Data Source</Label>
|
<Label small>Datasource</Label>
|
||||||
<Select
|
<Select
|
||||||
bind:value={parameters.providerId}
|
bind:value={parameters.providerId}
|
||||||
options={providerOptions}
|
options={providerOptions}
|
||||||
|
|
|
@ -71,13 +71,13 @@
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Choosing a Data Source will automatically use the data it provides, but it's
|
Choosing a Datasource will automatically use the data it provides, but it's
|
||||||
optional.<br />
|
optional.<br />
|
||||||
You can always add or override fields manually.
|
You can always add or override fields manually.
|
||||||
</Body>
|
</Body>
|
||||||
|
|
||||||
<div class="params">
|
<div class="params">
|
||||||
<Label small>Data Source</Label>
|
<Label small>Datasource</Label>
|
||||||
<Select
|
<Select
|
||||||
bind:value={parameters.providerId}
|
bind:value={parameters.providerId}
|
||||||
options={providerOptions}
|
options={providerOptions}
|
||||||
|
|
|
@ -107,7 +107,7 @@
|
||||||
placeholder={keyPlaceholder}
|
placeholder={keyPlaceholder}
|
||||||
readonly={readOnly}
|
readonly={readOnly}
|
||||||
bind:value={field.name}
|
bind:value={field.name}
|
||||||
on:change={changed}
|
on:blur={changed}
|
||||||
/>
|
/>
|
||||||
{#if options}
|
{#if options}
|
||||||
<Select bind:value={field.value} on:change={changed} {options} />
|
<Select bind:value={field.value} on:change={changed} {options} />
|
||||||
|
@ -115,7 +115,10 @@
|
||||||
<DrawerBindableInput
|
<DrawerBindableInput
|
||||||
{bindings}
|
{bindings}
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
on:change={e => (field.value = e.detail)}
|
on:blur={e => {
|
||||||
|
field.value = e.detail
|
||||||
|
changed()
|
||||||
|
}}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
allowJS={false}
|
allowJS={false}
|
||||||
|
@ -127,7 +130,7 @@
|
||||||
placeholder={valuePlaceholder}
|
placeholder={valuePlaceholder}
|
||||||
readonly={readOnly}
|
readonly={readOnly}
|
||||||
bind:value={field.value}
|
bind:value={field.value}
|
||||||
on:change={changed}
|
on:blur={changed}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if toggle}
|
{#if toggle}
|
||||||
|
|
|
@ -1,16 +1,24 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Toggle } from "@budibase/bbui"
|
import { ModalContent, Toggle, Body } from "@budibase/bbui"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
|
export let published
|
||||||
let excludeRows = false
|
let excludeRows = false
|
||||||
|
|
||||||
|
$: title = published ? "Export published app" : "Export latest app"
|
||||||
|
$: confirmText = published ? "Export published" : "Export latest"
|
||||||
|
|
||||||
const exportApp = () => {
|
const exportApp = () => {
|
||||||
const id = app.deployed ? app.prodId : app.devId
|
const id = published ? app.prodId : app.devId
|
||||||
const appName = encodeURIComponent(app.name)
|
const appName = encodeURIComponent(app.name)
|
||||||
window.location = `/api/backups/export?appId=${id}&appname=${appName}&excludeRows=${excludeRows}`
|
window.location = `/api/backups/export?appId=${id}&appname=${appName}&excludeRows=${excludeRows}`
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent title={"Export"} confirmText={"Export"} onConfirm={exportApp}>
|
<ModalContent {title} {confirmText} onConfirm={exportApp}>
|
||||||
|
<Body
|
||||||
|
>Apps can be exported with or without data that is within internal tables -
|
||||||
|
select this below.</Body
|
||||||
|
>
|
||||||
<Toggle text="Exclude Rows" bind:value={excludeRows} />
|
<Toggle text="Exclude Rows" bind:value={excludeRows} />
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -64,3 +64,10 @@ export const PlanType = {
|
||||||
BUSINESS: "business",
|
BUSINESS: "business",
|
||||||
ENTERPRISE: "enterprise",
|
ENTERPRISE: "enterprise",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PluginSource = {
|
||||||
|
URL: "URL",
|
||||||
|
NPM: "NPM",
|
||||||
|
GITHUB: "Github",
|
||||||
|
FILE: "File Upload",
|
||||||
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ export function buildQueryString(obj) {
|
||||||
if (str !== "") {
|
if (str !== "") {
|
||||||
str += "&"
|
str += "&"
|
||||||
}
|
}
|
||||||
str += `${key}=${value || ""}`
|
str += `${key}=${encodeURIComponent(value || "")}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return str
|
return str
|
||||||
|
|
|
@ -28,25 +28,25 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import restUtils from "helpers/data/utils"
|
import restUtils from "helpers/data/utils"
|
||||||
import {
|
import {
|
||||||
RestBodyTypes as bodyTypes,
|
|
||||||
SchemaTypeOptions,
|
|
||||||
PaginationLocations,
|
PaginationLocations,
|
||||||
PaginationTypes,
|
PaginationTypes,
|
||||||
|
RawRestBodyTypes,
|
||||||
|
RestBodyTypes as bodyTypes,
|
||||||
|
SchemaTypeOptions,
|
||||||
} from "constants/backend"
|
} from "constants/backend"
|
||||||
import JSONPreview from "components/integration/JSONPreview.svelte"
|
import JSONPreview from "components/integration/JSONPreview.svelte"
|
||||||
import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte"
|
import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte"
|
||||||
import DynamicVariableModal from "../../_components/DynamicVariableModal.svelte"
|
import DynamicVariableModal from "../../_components/DynamicVariableModal.svelte"
|
||||||
import Placeholder from "assets/bb-spaceship.svg"
|
import Placeholder from "assets/bb-spaceship.svg"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { RawRestBodyTypes } from "constants/backend"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getRestBindings,
|
getRestBindings,
|
||||||
toBindingsArray,
|
|
||||||
runtimeToReadableBinding,
|
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
runtimeToReadableMap,
|
|
||||||
readableToRuntimeMap,
|
readableToRuntimeMap,
|
||||||
|
runtimeToReadableBinding,
|
||||||
|
runtimeToReadableMap,
|
||||||
|
toBindingsArray,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
|
|
||||||
let query, datasource
|
let query, datasource
|
||||||
|
@ -95,7 +95,7 @@
|
||||||
$: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs)
|
$: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs)
|
||||||
|
|
||||||
function getSelectedQuery() {
|
function getSelectedQuery() {
|
||||||
const cloneQuery = cloneDeep(
|
return cloneDeep(
|
||||||
$queries.list.find(q => q._id === $queries.selected) || {
|
$queries.list.find(q => q._id === $queries.selected) || {
|
||||||
datasourceId: $params.selectedDatasource,
|
datasourceId: $params.selectedDatasource,
|
||||||
parameters: [],
|
parameters: [],
|
||||||
|
@ -107,7 +107,6 @@
|
||||||
queryVerb: "read",
|
queryVerb: "read",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return cloneQuery
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkQueryName(inputUrl = null) {
|
function checkQueryName(inputUrl = null) {
|
||||||
|
@ -121,14 +120,15 @@
|
||||||
if (!base) {
|
if (!base) {
|
||||||
return base
|
return base
|
||||||
}
|
}
|
||||||
const qs = restUtils.buildQueryString(
|
let qs = restUtils.buildQueryString(
|
||||||
runtimeToReadableMap(mergedBindings, qsObj)
|
runtimeToReadableMap(mergedBindings, qsObj)
|
||||||
)
|
)
|
||||||
let newUrl = base
|
let newUrl = base
|
||||||
if (base.includes("?")) {
|
if (base.includes("?")) {
|
||||||
newUrl = base.split("?")[0]
|
const split = base.split("?")
|
||||||
|
newUrl = split[0]
|
||||||
}
|
}
|
||||||
return qs.length > 0 ? `${newUrl}?${qs}` : newUrl
|
return qs.length === 0 ? newUrl : `${newUrl}?${qs}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildQuery() {
|
function buildQuery() {
|
||||||
|
@ -314,6 +314,25 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const paramsChanged = evt => {
|
||||||
|
breakQs = {}
|
||||||
|
for (let param of evt.detail) {
|
||||||
|
breakQs[param.name] = param.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlChanged = evt => {
|
||||||
|
breakQs = {}
|
||||||
|
const qs = evt.target.value.split("?")[1]
|
||||||
|
if (qs && qs.length > 0) {
|
||||||
|
const parts = qs.split("&")
|
||||||
|
for (let part of parts) {
|
||||||
|
const [key, value] = part.split("=")
|
||||||
|
breakQs[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
query = getSelectedQuery()
|
query = getSelectedQuery()
|
||||||
|
|
||||||
|
@ -426,7 +445,11 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="url">
|
<div class="url">
|
||||||
<Input bind:value={url} placeholder="http://www.api.com/endpoint" />
|
<Input
|
||||||
|
on:blur={urlChanged}
|
||||||
|
bind:value={url}
|
||||||
|
placeholder="http://www.api.com/endpoint"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button primary disabled={!url} on:click={runQuery}>Send</Button>
|
<Button primary disabled={!url} on:click={runQuery}>Send</Button>
|
||||||
<Button
|
<Button
|
||||||
|
@ -456,13 +479,16 @@
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Params">
|
<Tab title="Params">
|
||||||
<KeyValueBuilder
|
{#key breakQs}
|
||||||
bind:object={breakQs}
|
<KeyValueBuilder
|
||||||
name="param"
|
on:change={paramsChanged}
|
||||||
headings
|
object={breakQs}
|
||||||
bindings={mergedBindings}
|
name="param"
|
||||||
bindingDrawerLeft="260px"
|
headings
|
||||||
/>
|
bindings={mergedBindings}
|
||||||
|
bindingDrawerLeft="260px"
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Headers">
|
<Tab title="Headers">
|
||||||
<KeyValueBuilder
|
<KeyValueBuilder
|
||||||
|
|
|
@ -86,6 +86,7 @@
|
||||||
: [],
|
: [],
|
||||||
isBudibaseEvent: true,
|
isBudibaseEvent: true,
|
||||||
usedPlugins: $store.usedPlugins,
|
usedPlugins: $store.usedPlugins,
|
||||||
|
location: window.location,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh the preview when required
|
// Refresh the preview when required
|
||||||
|
@ -291,7 +292,7 @@
|
||||||
<iframe
|
<iframe
|
||||||
title="componentPreview"
|
title="componentPreview"
|
||||||
bind:this={iframe}
|
bind:this={iframe}
|
||||||
src="/preview"
|
src="/app/preview"
|
||||||
class:hidden={loading || error}
|
class:hidden={loading || error}
|
||||||
class:tablet={$store.previewDevice === "tablet"}
|
class:tablet={$store.previewDevice === "tablet"}
|
||||||
class:mobile={$store.previewDevice === "mobile"}
|
class:mobile={$store.previewDevice === "mobile"}
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
size="L"
|
size="L"
|
||||||
>
|
>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Select which data source you would like to use to create your screens
|
Select which datasource you would like to use to create your screens
|
||||||
</Body>
|
</Body>
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
{#each filteredSources as datasource}
|
{#each filteredSources as datasource}
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
<Heading size="XS">Autogenerated screens</Heading>
|
<Heading size="XS">Autogenerated screens</Heading>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Add autogenerated screens with CRUD functionality to get a working
|
Add autogenerated screens with CRUD functionality to get a working
|
||||||
app quickly! (Requires a data source)
|
app quickly! (Requires a datasource)
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -54,6 +54,8 @@
|
||||||
: undefined,
|
: undefined,
|
||||||
{ title: "Auth", href: "/builder/portal/manage/auth" },
|
{ title: "Auth", href: "/builder/portal/manage/auth" },
|
||||||
{ title: "Email", href: "/builder/portal/manage/email" },
|
{ title: "Email", href: "/builder/portal/manage/email" },
|
||||||
|
{ title: "Plugins", href: "/builder/portal/manage/plugins" },
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "Organisation",
|
title: "Organisation",
|
||||||
href: "/builder/portal/settings/organisation",
|
href: "/builder/portal/settings/organisation",
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||||
import ExportAppModal from "components/start/ExportAppModal.svelte"
|
|
||||||
|
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore } from "builderStore"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
@ -33,7 +32,6 @@
|
||||||
let selectedApp
|
let selectedApp
|
||||||
let creationModal
|
let creationModal
|
||||||
let updatingModal
|
let updatingModal
|
||||||
let exportModal
|
|
||||||
let creatingApp = false
|
let creatingApp = false
|
||||||
let loaded = $apps?.length || $templates?.length
|
let loaded = $apps?.length || $templates?.length
|
||||||
let searchTerm = ""
|
let searchTerm = ""
|
||||||
|
@ -407,10 +405,6 @@
|
||||||
<UpdateAppModal app={selectedApp} />
|
<UpdateAppModal app={selectedApp} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={exportModal} padding={false} width="600px">
|
|
||||||
<ExportAppModal app={selectedApp} />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.appTable {
|
.appTable {
|
||||||
border-top: var(--border-light);
|
border-top: var(--border-light);
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
ModalContent,
|
||||||
|
Label,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Dropzone,
|
||||||
|
Body,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
|
import { plugins } from "stores/portal"
|
||||||
|
import { PluginSource } from "constants"
|
||||||
|
|
||||||
|
function opt(name, optional) {
|
||||||
|
if (optional) {
|
||||||
|
return { name, optional }
|
||||||
|
}
|
||||||
|
return { name }
|
||||||
|
}
|
||||||
|
|
||||||
|
let authOptions = {
|
||||||
|
[PluginSource.URL]: [opt("URL"), opt("Headers", true)],
|
||||||
|
[PluginSource.NPM]: [opt("URL")],
|
||||||
|
[PluginSource.GITHUB]: [opt("URL"), opt("Github Token", true)],
|
||||||
|
[PluginSource.FILE]: [opt("File Upload")],
|
||||||
|
}
|
||||||
|
let file
|
||||||
|
let source = PluginSource.URL
|
||||||
|
let dynamicValues = {}
|
||||||
|
|
||||||
|
let validation
|
||||||
|
$: validation = source === "File Upload" ? file : dynamicValues["URL"]
|
||||||
|
|
||||||
|
function infoMessage(optionName) {
|
||||||
|
switch (optionName) {
|
||||||
|
case PluginSource.URL:
|
||||||
|
return "Please specify a URL which directs to a built plugin TAR archive. You can provide headers if authentication is required."
|
||||||
|
case PluginSource.NPM:
|
||||||
|
return "Please specify the URL to a public NPM package which contains the built version of the plugin you wish to install."
|
||||||
|
case PluginSource.GITHUB:
|
||||||
|
return "Please specify the URL to a Github repository which contains built plugin releases. If this is a private repo you can provide a token to access it."
|
||||||
|
case PluginSource.FILE:
|
||||||
|
return "Please provide a built plugin TAR archive. You can build a plugin locally using the Budibase CLI."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
try {
|
||||||
|
if (source === PluginSource.FILE) {
|
||||||
|
await plugins.uploadPlugin(file)
|
||||||
|
} else {
|
||||||
|
const url = dynamicValues["URL"]
|
||||||
|
let auth =
|
||||||
|
source === PluginSource.GITHUB
|
||||||
|
? dynamicValues["Github Token"]
|
||||||
|
: source === PluginSource.URL
|
||||||
|
? dynamicValues["Headers"]
|
||||||
|
: undefined
|
||||||
|
await plugins.createPlugin(source, url, auth)
|
||||||
|
}
|
||||||
|
notifications.success("Plugin added successfully.")
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err?.message ? err.message : JSON.stringify(err)
|
||||||
|
notifications.error(`Failed to add plugin: ${msg}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
confirmText={"Save"}
|
||||||
|
onConfirm={save}
|
||||||
|
disabled={!validation}
|
||||||
|
size="M"
|
||||||
|
title="Add new plugin"
|
||||||
|
>
|
||||||
|
<div class="form-row">
|
||||||
|
<Label size="M">Source</Label>
|
||||||
|
<Select
|
||||||
|
placeholder={null}
|
||||||
|
bind:value={source}
|
||||||
|
options={Object.values(PluginSource)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Body size="S">{infoMessage(source)}</Body>
|
||||||
|
{#each authOptions[source] as option}
|
||||||
|
{#if option.name === PluginSource.FILE}
|
||||||
|
<div class="form-row">
|
||||||
|
<Label size="M">{option.name}</Label>
|
||||||
|
<Dropzone
|
||||||
|
gallery={false}
|
||||||
|
value={[file]}
|
||||||
|
on:change={e => {
|
||||||
|
if (!e.detail || e.detail.length === 0) {
|
||||||
|
file = null
|
||||||
|
} else {
|
||||||
|
file = e.detail[0]
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="form-row">
|
||||||
|
<div>
|
||||||
|
<Label size="M">{option.name}</Label>
|
||||||
|
{#if option.optional}
|
||||||
|
<Label size="S" muted><i>Optional</i></Label>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if option.name === "Headers"}
|
||||||
|
<KeyValueBuilder bind:object={dynamicValues[option.name]} />
|
||||||
|
{:else}
|
||||||
|
<Input bind:value={dynamicValues[option.name]} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 60px 1fr;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script>
|
||||||
|
import { Body, ModalContent, notifications } from "@budibase/bbui"
|
||||||
|
import { plugins } from "stores/portal"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let plugin
|
||||||
|
|
||||||
|
let dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
async function deletePlugin() {
|
||||||
|
try {
|
||||||
|
await plugins.deletePlugin(plugin._id)
|
||||||
|
notifications.success(`Plugin ${plugin?.name} deleted`)
|
||||||
|
dispatch("deleted")
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error?.message ? error.message : JSON.stringify(error)
|
||||||
|
notifications.error(`Error deleting plugin: ${msg}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
warning
|
||||||
|
onConfirm={deletePlugin}
|
||||||
|
title="Delete Plugin"
|
||||||
|
confirmText="Delete plugin"
|
||||||
|
cancelText="Cancel"
|
||||||
|
showCloseIcon={false}
|
||||||
|
>
|
||||||
|
<Body>
|
||||||
|
Are you sure you want to delete <strong>{plugin?.name}</strong>
|
||||||
|
</Body>
|
||||||
|
</ModalContent>
|
|
@ -0,0 +1,155 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Icon,
|
||||||
|
Body,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Button,
|
||||||
|
Label,
|
||||||
|
Input,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import DeletePluginModal from "../_components/DeletePluginModal.svelte"
|
||||||
|
|
||||||
|
export let plugin
|
||||||
|
|
||||||
|
let detailsModal
|
||||||
|
let deleteModal
|
||||||
|
|
||||||
|
let icon =
|
||||||
|
plugin.schema.type === "component"
|
||||||
|
? plugin.schema.schema.icon || "Book"
|
||||||
|
: plugin.schema.schema.icon || "Beaker"
|
||||||
|
|
||||||
|
$: friendlyName = plugin?.schema?.schema?.friendlyName
|
||||||
|
|
||||||
|
function pluginDeleted() {
|
||||||
|
if (detailsModal) {
|
||||||
|
detailsModal.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="row" on:click={() => detailsModal.show()}>
|
||||||
|
<div class="title">
|
||||||
|
<div class="name">
|
||||||
|
<div>
|
||||||
|
<Icon size="M" name={icon} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Body
|
||||||
|
size="S"
|
||||||
|
color="var(--spectrum-global-color-gray-900)"
|
||||||
|
weight="800"
|
||||||
|
>
|
||||||
|
{plugin.name}
|
||||||
|
</Body>
|
||||||
|
<Body size="XS" color="var(--spectrum-global-color-gray-900)">
|
||||||
|
{friendlyName}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="desktop">{plugin.version}</div>
|
||||||
|
<div class="desktop">
|
||||||
|
{plugin.schema.type.charAt(0).toUpperCase() + plugin.schema.type.slice(1)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Icon name="ChevronRight" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal bind:this={detailsModal}>
|
||||||
|
<ModalContent
|
||||||
|
size="M"
|
||||||
|
title="Plugin details"
|
||||||
|
showConfirmButton={false}
|
||||||
|
showCancelButton={false}
|
||||||
|
>
|
||||||
|
<div class="details-row">
|
||||||
|
<Label size="M">Name</Label>
|
||||||
|
<Input disabled value={plugin.name} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details-row">
|
||||||
|
<Label size="M">Friendly name</Label>
|
||||||
|
<Input disabled value={friendlyName} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details-row">
|
||||||
|
<Label size="M">Type</Label>
|
||||||
|
<Input
|
||||||
|
disabled
|
||||||
|
value={plugin.schema.type.charAt(0).toUpperCase() +
|
||||||
|
plugin.schema.type.slice(1)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="details-row">
|
||||||
|
<Label size="M">Source</Label>
|
||||||
|
<Input disabled value={plugin.source || "N/A"} />
|
||||||
|
</div>
|
||||||
|
<div class="details-row">
|
||||||
|
<Label size="M">Version</Label>
|
||||||
|
<Input disabled value={plugin.version} />
|
||||||
|
</div>
|
||||||
|
<div class="details-row">
|
||||||
|
<Label size="M">License</Label>
|
||||||
|
<Input disabled value={plugin.package.license} />
|
||||||
|
</div>
|
||||||
|
<div class="details-row">
|
||||||
|
<Label size="M">Author</Label>
|
||||||
|
<Input disabled value={plugin.package.author || "N/A"} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer" slot="footer">
|
||||||
|
<Button newStyles on:click={deleteModal.show()} warning>Delete</Button>
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<Modal bind:this={deleteModal}>
|
||||||
|
<DeletePluginModal {plugin} on:deleted={pluginDeleted} />
|
||||||
|
</Modal>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 110px 140px 20px;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--background);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 56px;
|
||||||
|
background: var(--spectrum-global-color-gray-50);
|
||||||
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
transition: background 130ms ease-out;
|
||||||
|
}
|
||||||
|
.row:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--spectrum-global-color-gray-75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
grid-gap: var(--spacing-m);
|
||||||
|
grid-template-columns: 75px 75px;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 70px 1fr;
|
||||||
|
grid-gap: var(--spacing-l) var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.desktop {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,95 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Select,
|
||||||
|
Divider,
|
||||||
|
Modal,
|
||||||
|
Search,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { plugins } from "stores/portal"
|
||||||
|
import PluginRow from "./_components/PluginRow.svelte"
|
||||||
|
import AddPluginModal from "./_components/AddPluginModal.svelte"
|
||||||
|
|
||||||
|
let modal
|
||||||
|
let searchTerm = ""
|
||||||
|
let filter = "all"
|
||||||
|
let filterOptions = [
|
||||||
|
{ label: "All plugins", value: "all" },
|
||||||
|
{ label: "Components", value: "component" },
|
||||||
|
{ label: "Datasources", value: "datasource" },
|
||||||
|
]
|
||||||
|
|
||||||
|
$: filteredPlugins = $plugins
|
||||||
|
.filter(plugin => {
|
||||||
|
return filter === "all" || plugin.schema.type === filter
|
||||||
|
})
|
||||||
|
.filter(plugin => {
|
||||||
|
return (
|
||||||
|
!searchTerm ||
|
||||||
|
plugin?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await plugins.load()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Layout noPadding>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="M">Plugins</Heading>
|
||||||
|
<Body>Add your own custom datasources and components</Body>
|
||||||
|
</Layout>
|
||||||
|
<Divider size="S" />
|
||||||
|
<Layout noPadding>
|
||||||
|
<div class="controls">
|
||||||
|
<div>
|
||||||
|
<Button on:click={modal.show} newStyles cta icon={"Add"}>
|
||||||
|
Add plugin
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="filters">
|
||||||
|
<div class="select">
|
||||||
|
<Select
|
||||||
|
bind:value={filter}
|
||||||
|
placeholder={null}
|
||||||
|
options={filterOptions}
|
||||||
|
autoWidth
|
||||||
|
quiet
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Search bind:value={searchTerm} placeholder="Search plugins" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if filteredPlugins?.length}
|
||||||
|
<Layout noPadding gap="S">
|
||||||
|
{#each filteredPlugins as plugin (plugin._id)}
|
||||||
|
<PluginRow {plugin} />
|
||||||
|
{/each}
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<AddPluginModal />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.controls :global(.spectrum-Search) {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -16,6 +16,7 @@
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Icon,
|
Icon,
|
||||||
Helpers,
|
Helpers,
|
||||||
|
Modal,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import OverviewTab from "../_components/OverviewTab.svelte"
|
import OverviewTab from "../_components/OverviewTab.svelte"
|
||||||
import SettingsTab from "../_components/SettingsTab.svelte"
|
import SettingsTab from "../_components/SettingsTab.svelte"
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
import EditableIcon from "components/common/EditableIcon.svelte"
|
import EditableIcon from "components/common/EditableIcon.svelte"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import HistoryTab from "components/portal/overview/automation/HistoryTab.svelte"
|
import HistoryTab from "components/portal/overview/automation/HistoryTab.svelte"
|
||||||
|
import ExportAppModal from "components/start/ExportAppModal.svelte"
|
||||||
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
||||||
import { onDestroy, onMount } from "svelte"
|
import { onDestroy, onMount } from "svelte"
|
||||||
|
|
||||||
|
@ -38,7 +40,9 @@
|
||||||
let loaded = false
|
let loaded = false
|
||||||
let deletionModal
|
let deletionModal
|
||||||
let unpublishModal
|
let unpublishModal
|
||||||
|
let exportModal
|
||||||
let appName = ""
|
let appName = ""
|
||||||
|
let published
|
||||||
|
|
||||||
// App
|
// App
|
||||||
$: filteredApps = $apps.filter(app => app.devId === application)
|
$: filteredApps = $apps.filter(app => app.devId === application)
|
||||||
|
@ -140,11 +144,9 @@
|
||||||
notifications.success("App ID copied to clipboard.")
|
notifications.success("App ID copied to clipboard.")
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportApp = (app, opts = { published: false }) => {
|
const exportApp = opts => {
|
||||||
const appName = encodeURIComponent(app.name)
|
published = opts.published
|
||||||
const id = opts?.published ? app.prodId : app.devId
|
exportModal.show()
|
||||||
// always export the development version
|
|
||||||
window.location = `/api/backups/export?appId=${id}&appname=${appName}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const unpublishApp = app => {
|
const unpublishApp = app => {
|
||||||
|
@ -206,6 +208,10 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={exportModal} padding={false} width="600px">
|
||||||
|
<ExportAppModal app={selectedApp} {published} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<span class="overview-wrap">
|
<span class="overview-wrap">
|
||||||
<Page wide noPadding>
|
<Page wide noPadding>
|
||||||
{#await promise}
|
{#await promise}
|
||||||
|
@ -269,14 +275,14 @@
|
||||||
<Icon hoverable name="More" />
|
<Icon hoverable name="More" />
|
||||||
</span>
|
</span>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
on:click={() => exportApp(selectedApp, { published: false })}
|
on:click={() => exportApp({ published: false })}
|
||||||
icon="DownloadFromCloud"
|
icon="DownloadFromCloud"
|
||||||
>
|
>
|
||||||
Export latest
|
Export latest
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{#if isPublished}
|
{#if isPublished}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
on:click={() => exportApp(selectedApp, { published: true })}
|
on:click={() => exportApp({ published: true })}
|
||||||
icon="DownloadFromCloudOutline"
|
icon="DownloadFromCloudOutline"
|
||||||
>
|
>
|
||||||
Export published
|
Export published
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
import { onMount } from "svelte"
|
|
||||||
|
|
||||||
// Only admins allowed here
|
// Only admins allowed here
|
||||||
$: {
|
$: {
|
||||||
|
@ -34,12 +33,11 @@
|
||||||
})
|
})
|
||||||
let loading = false
|
let loading = false
|
||||||
|
|
||||||
async function uploadLogo() {
|
async function uploadLogo(file) {
|
||||||
try {
|
try {
|
||||||
let data = new FormData()
|
let data = new FormData()
|
||||||
data.append("file", $values.logo)
|
data.append("file", file)
|
||||||
await API.uploadPlugin(data)
|
await API.uploadLogo(data)
|
||||||
notifications.success("Plugin uploaded successfully")
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error uploading logo")
|
notifications.error("Error uploading logo")
|
||||||
}
|
}
|
||||||
|
@ -73,11 +71,6 @@
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
const plugins = await API.getPlugins()
|
|
||||||
console.log(plugins)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $auth.isAdmin}
|
{#if $auth.isAdmin}
|
||||||
|
@ -95,14 +88,14 @@
|
||||||
<Heading size="S">Information</Heading>
|
<Heading size="S">Information</Heading>
|
||||||
<Body size="S">Here you can update your logo and organization name.</Body>
|
<Body size="S">Here you can update your logo and organization name.</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<div class="fields">
|
<div className="fields">
|
||||||
<div class="field">
|
<div className="field">
|
||||||
<Label size="L">Org. name</Label>
|
<Label size="L">Org. name</Label>
|
||||||
<Input thin bind:value={$values.company} />
|
<Input thin bind:value={$values.company} />
|
||||||
</div>
|
</div>
|
||||||
<div class="field logo">
|
<div className="field logo">
|
||||||
<Label size="L">Logo</Label>
|
<Label size="L">Logo</Label>
|
||||||
<div class="file">
|
<div className="file">
|
||||||
<Dropzone
|
<Dropzone
|
||||||
value={[$values.logo]}
|
value={[$values.logo]}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
|
@ -113,7 +106,6 @@
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button on:click={uploadLogo}>Upload</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -123,8 +115,8 @@
|
||||||
<Heading size="S">Platform</Heading>
|
<Heading size="S">Platform</Heading>
|
||||||
<Body size="S">Here you can set up general platform settings.</Body>
|
<Body size="S">Here you can set up general platform settings.</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<div class="fields">
|
<div className="fields">
|
||||||
<div class="field">
|
<div className="field">
|
||||||
<Label
|
<Label
|
||||||
size="L"
|
size="L"
|
||||||
tooltip={"Update the Platform URL to match your Budibase web URL. This keeps email templates and authentication configs up to date."}
|
tooltip={"Update the Platform URL to match your Budibase web URL. This keeps email templates and authentication configs up to date."}
|
||||||
|
@ -158,15 +150,18 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: var(--spacing-m);
|
grid-gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 100px 1fr;
|
grid-template-columns: 100px 1fr;
|
||||||
grid-gap: var(--spacing-l);
|
grid-gap: var(--spacing-l);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file {
|
.file {
|
||||||
max-width: 30ch;
|
max-width: 30ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,8 +62,11 @@ export function createDatasourcesStore() {
|
||||||
unselect: () => {
|
unselect: () => {
|
||||||
update(state => ({ ...state, selected: null }))
|
update(state => ({ ...state, selected: null }))
|
||||||
},
|
},
|
||||||
updateSchema: async datasource => {
|
updateSchema: async (datasource, tablesFilter) => {
|
||||||
const response = await API.buildDatasourceSchema(datasource?._id)
|
const response = await API.buildDatasourceSchema({
|
||||||
|
datasourceId: datasource?._id,
|
||||||
|
tablesFilter,
|
||||||
|
})
|
||||||
return await updateDatasource(response)
|
return await updateDatasource(response)
|
||||||
},
|
},
|
||||||
save: async (body, fetchSchema = false) => {
|
save: async (body, fetchSchema = false) => {
|
||||||
|
|
|
@ -8,3 +8,4 @@ export { oidc } from "./oidc"
|
||||||
export { templates } from "./templates"
|
export { templates } from "./templates"
|
||||||
export { licensing } from "./licensing"
|
export { licensing } from "./licensing"
|
||||||
export { groups } from "./groups"
|
export { groups } from "./groups"
|
||||||
|
export { plugins } from "./plugins"
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { API } from "api"
|
||||||
|
import { PluginSource } from "constants"
|
||||||
|
|
||||||
|
export function createPluginsStore() {
|
||||||
|
const { subscribe, set, update } = writable([])
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const plugins = await API.getPlugins()
|
||||||
|
set(plugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePlugin(pluginId) {
|
||||||
|
await API.deletePlugin(pluginId)
|
||||||
|
update(state => {
|
||||||
|
state = state.filter(existing => existing._id !== pluginId)
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createPlugin(source, url, auth = null) {
|
||||||
|
let pluginData = {
|
||||||
|
source,
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (source) {
|
||||||
|
case PluginSource.URL:
|
||||||
|
pluginData.headers = auth
|
||||||
|
break
|
||||||
|
case PluginSource.GITHUB:
|
||||||
|
pluginData.githubToken = auth
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = await API.createPlugin(pluginData)
|
||||||
|
|
||||||
|
let newPlugin = res.plugins[0]
|
||||||
|
update(state => {
|
||||||
|
const currentIdx = state.findIndex(plugin => plugin._id === newPlugin._id)
|
||||||
|
if (currentIdx >= 0) {
|
||||||
|
state.splice(currentIdx, 1, newPlugin)
|
||||||
|
} else {
|
||||||
|
state.push(newPlugin)
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadPlugin(file) {
|
||||||
|
if (!file) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let data = new FormData()
|
||||||
|
data.append("file", file)
|
||||||
|
let resp = await API.uploadPlugin(data)
|
||||||
|
let newPlugin = resp.plugins[0]
|
||||||
|
update(state => {
|
||||||
|
const currentIdx = state.findIndex(plugin => plugin._id === newPlugin._id)
|
||||||
|
if (currentIdx >= 0) {
|
||||||
|
state.splice(currentIdx, 1, newPlugin)
|
||||||
|
} else {
|
||||||
|
state.push(newPlugin)
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
load,
|
||||||
|
createPlugin,
|
||||||
|
deletePlugin,
|
||||||
|
uploadPlugin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const plugins = createPluginsStore()
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "1.3.12-alpha.3",
|
"version": "1.3.15-alpha.9",
|
||||||
"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": {
|
||||||
|
@ -26,18 +26,18 @@
|
||||||
"outputPath": "build"
|
"outputPath": "build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "1.3.12-alpha.3",
|
"@budibase/backend-core": "1.3.15-alpha.9",
|
||||||
"@budibase/string-templates": "1.3.12-alpha.3",
|
"@budibase/string-templates": "1.3.15-alpha.9",
|
||||||
"@budibase/types": "1.3.12-alpha.3",
|
"@budibase/types": "1.3.15-alpha.9",
|
||||||
"axios": "0.21.2",
|
"axios": "0.21.2",
|
||||||
"chalk": "4.1.0",
|
"chalk": "4.1.0",
|
||||||
"cli-progress": "3.11.2",
|
"cli-progress": "3.11.2",
|
||||||
"commander": "7.1.0",
|
"commander": "7.1.0",
|
||||||
"docker-compose": "0.23.6",
|
"docker-compose": "0.23.6",
|
||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
"download": "^8.0.0",
|
"download": "8.0.0",
|
||||||
"inquirer": "8.0.0",
|
"inquirer": "8.0.0",
|
||||||
"joi": "^17.6.0",
|
"joi": "17.6.0",
|
||||||
"lookpath": "1.1.0",
|
"lookpath": "1.1.0",
|
||||||
"node-fetch": "2",
|
"node-fetch": "2",
|
||||||
"pkg": "5.7.0",
|
"pkg": "5.7.0",
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
process.env.NO_JS = "1"
|
process.env.NO_JS = "1"
|
||||||
|
process.env.JS_BCRYPT = "1"
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
exports.PluginTypes = {
|
|
||||||
COMPONENT: "component",
|
|
||||||
DATASOURCE: "datasource",
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.PLUGIN_TYPES_ARR = Object.values(exports.PluginTypes)
|
|
|
@ -3,11 +3,11 @@ const { CommandWords } = require("../constants")
|
||||||
const { getSkeleton, fleshOutSkeleton } = require("./skeleton")
|
const { getSkeleton, fleshOutSkeleton } = require("./skeleton")
|
||||||
const questions = require("../questions")
|
const questions = require("../questions")
|
||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
const { PLUGIN_TYPES_ARR } = require("./constants")
|
const { PLUGIN_TYPE_ARR } = require("@budibase/types")
|
||||||
const { validate } = require("./validate")
|
const { validate } = require("@budibase/backend-core/plugins")
|
||||||
const { runPkgCommand } = require("../exec")
|
const { runPkgCommand } = require("../exec")
|
||||||
const { join } = require("path")
|
const { join } = require("path")
|
||||||
const { success, error, info } = require("../utils")
|
const { success, error, info, moveDirectory } = require("../utils")
|
||||||
|
|
||||||
function checkInPlugin() {
|
function checkInPlugin() {
|
||||||
if (!fs.existsSync("package.json")) {
|
if (!fs.existsSync("package.json")) {
|
||||||
|
@ -22,9 +22,27 @@ function checkInPlugin() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function askAboutTopLevel(name) {
|
||||||
|
const files = fs.readdirSync(process.cwd())
|
||||||
|
// we are in an empty git repo, don't ask
|
||||||
|
if (files.find(file => file === ".git")) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
info(`By default the plugin will be created in the directory "${name}"`)
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
info(
|
||||||
|
"if you are already in an empty directory, such as a new Git repo, you can disable this functionality."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return questions.confirmation("Create top level directory?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function init(opts) {
|
async function init(opts) {
|
||||||
const type = opts["init"] || opts
|
const type = opts["init"] || opts
|
||||||
if (!type || !PLUGIN_TYPES_ARR.includes(type)) {
|
if (!type || !PLUGIN_TYPE_ARR.includes(type)) {
|
||||||
console.log(
|
console.log(
|
||||||
error(
|
error(
|
||||||
"Please provide a type to init, either 'component' or 'datasource'."
|
"Please provide a type to init, either 'component' or 'datasource'."
|
||||||
|
@ -45,13 +63,20 @@ async function init(opts) {
|
||||||
`An amazing Budibase ${type}!`
|
`An amazing Budibase ${type}!`
|
||||||
)
|
)
|
||||||
const version = await questions.string("Version", "1.0.0")
|
const version = await questions.string("Version", "1.0.0")
|
||||||
|
const topLevel = await askAboutTopLevel(name)
|
||||||
// get the skeleton
|
// get the skeleton
|
||||||
console.log(info("Retrieving project..."))
|
console.log(info("Retrieving project..."))
|
||||||
await getSkeleton(type, name)
|
await getSkeleton(type, name)
|
||||||
await fleshOutSkeleton(type, name, desc, version)
|
await fleshOutSkeleton(type, name, desc, version)
|
||||||
console.log(info("Installing dependencies..."))
|
console.log(info("Installing dependencies..."))
|
||||||
await runPkgCommand("install", join(process.cwd(), name))
|
await runPkgCommand("install", join(process.cwd(), name))
|
||||||
console.log(info(`Plugin created in directory "${name}"`))
|
// if no parent directory desired move to cwd
|
||||||
|
if (!topLevel) {
|
||||||
|
moveDirectory(name, process.cwd())
|
||||||
|
console.log(info(`Plugin created in current directory.`))
|
||||||
|
} else {
|
||||||
|
console.log(info(`Plugin created in directory "${name}"`))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verify() {
|
async function verify() {
|
||||||
|
|
|
@ -27,7 +27,10 @@ function checkForBinaries() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanup(evt) {
|
function cleanup(evt) {
|
||||||
if (evt && evt.errno) {
|
if (!isNaN(evt)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (evt) {
|
||||||
console.error(
|
console.error(
|
||||||
error(
|
error(
|
||||||
"Failed to run CLI command - please report with the following message:"
|
"Failed to run CLI command - please report with the following message:"
|
||||||
|
|
|
@ -3,6 +3,7 @@ const fs = require("fs")
|
||||||
const axios = require("axios")
|
const axios = require("axios")
|
||||||
const path = require("path")
|
const path = require("path")
|
||||||
const progress = require("cli-progress")
|
const progress = require("cli-progress")
|
||||||
|
const { join } = require("path")
|
||||||
|
|
||||||
exports.downloadFile = async (url, filePath) => {
|
exports.downloadFile = async (url, filePath) => {
|
||||||
filePath = path.resolve(filePath)
|
filePath = path.resolve(filePath)
|
||||||
|
@ -67,3 +68,19 @@ exports.progressBar = total => {
|
||||||
exports.checkSlashesInUrl = url => {
|
exports.checkSlashesInUrl = url => {
|
||||||
return url.replace(/(https?:\/\/)|(\/)+/g, "$1$2")
|
return url.replace(/(https?:\/\/)|(\/)+/g, "$1$2")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.moveDirectory = (oldPath, newPath) => {
|
||||||
|
const files = fs.readdirSync(oldPath)
|
||||||
|
// check any file exists already
|
||||||
|
for (let file of files) {
|
||||||
|
if (fs.existsSync(join(newPath, file))) {
|
||||||
|
throw new Error(
|
||||||
|
"Unable to remove top level directory - some skeleton files already exist."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let file of files) {
|
||||||
|
fs.renameSync(join(oldPath, file), join(newPath, file))
|
||||||
|
}
|
||||||
|
fs.rmdirSync(oldPath)
|
||||||
|
}
|
||||||
|
|
|
@ -753,7 +753,7 @@ double-ended-queue@2.1.0-0:
|
||||||
resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c"
|
resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c"
|
||||||
integrity sha512-+BNfZ+deCo8hMNpDqDnvT+c0XpJ5cUa6mqYq89bho2Ifze4URTqRkcwR399hWoTrTkbZ/XJYDgP6rc7pRgffEQ==
|
integrity sha512-+BNfZ+deCo8hMNpDqDnvT+c0XpJ5cUa6mqYq89bho2Ifze4URTqRkcwR399hWoTrTkbZ/XJYDgP6rc7pRgffEQ==
|
||||||
|
|
||||||
download@^8.0.0:
|
download@8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/download/-/download-8.0.0.tgz#afc0b309730811731aae9f5371c9f46be73e51b1"
|
resolved "https://registry.yarnpkg.com/download/-/download-8.0.0.tgz#afc0b309730811731aae9f5371c9f46be73e51b1"
|
||||||
integrity sha512-ASRY5QhDk7FK+XrQtQyvhpDKanLluEEQtWl/J7Lxuf/b+i8RYh997QeXvL85xitrmRKVlx9c7eTrcRdq2GS4eA==
|
integrity sha512-ASRY5QhDk7FK+XrQtQyvhpDKanLluEEQtWl/J7Lxuf/b+i8RYh997QeXvL85xitrmRKVlx9c7eTrcRdq2GS4eA==
|
||||||
|
@ -1551,7 +1551,7 @@ isurl@^1.0.0-alpha5:
|
||||||
has-to-string-tag-x "^1.2.0"
|
has-to-string-tag-x "^1.2.0"
|
||||||
is-object "^1.0.1"
|
is-object "^1.0.1"
|
||||||
|
|
||||||
joi@^17.6.0:
|
joi@17.6.0:
|
||||||
version "17.6.0"
|
version "17.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/joi/-/joi-17.6.0.tgz#0bb54f2f006c09a96e75ce687957bd04290054b2"
|
resolved "https://registry.yarnpkg.com/joi/-/joi-17.6.0.tgz#0bb54f2f006c09a96e75ce687957bd04290054b2"
|
||||||
integrity sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==
|
integrity sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "1.3.12-alpha.3",
|
"version": "1.3.15-alpha.9",
|
||||||
"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,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "1.3.12-alpha.3",
|
"@budibase/bbui": "1.3.15-alpha.9",
|
||||||
"@budibase/frontend-core": "1.3.12-alpha.3",
|
"@budibase/frontend-core": "1.3.15-alpha.9",
|
||||||
"@budibase/string-templates": "1.3.12-alpha.3",
|
"@budibase/string-templates": "1.3.15-alpha.9",
|
||||||
"@spectrum-css/button": "^3.0.3",
|
"@spectrum-css/button": "^3.0.3",
|
||||||
"@spectrum-css/card": "^3.0.3",
|
"@spectrum-css/card": "^3.0.3",
|
||||||
"@spectrum-css/divider": "^1.0.3",
|
"@spectrum-css/divider": "^1.0.3",
|
||||||
|
@ -39,6 +39,7 @@
|
||||||
"sanitize-html": "^2.7.0",
|
"sanitize-html": "^2.7.0",
|
||||||
"screenfull": "^6.0.1",
|
"screenfull": "^6.0.1",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
|
"socket.io-client": "^4.5.1",
|
||||||
"svelte": "^3.49.0",
|
"svelte": "^3.49.0",
|
||||||
"svelte-apexcharts": "^1.0.2",
|
"svelte-apexcharts": "^1.0.2",
|
||||||
"svelte-flatpickr": "^3.1.0",
|
"svelte-flatpickr": "^3.1.0",
|
||||||
|
|
|
@ -91,8 +91,8 @@
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
{#if $builderStore.usedPlugins?.length}
|
{#if $builderStore.usedPlugins?.length}
|
||||||
{#each $builderStore.usedPlugins as plugin}
|
{#each $builderStore.usedPlugins as plugin (plugin.hash)}
|
||||||
<script src={`/plugins/${plugin.jsUrl}`}></script>
|
<script src={`/plugins/${plugin.jsUrl}?r=${plugin.hash || ""}`}></script>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
|
@ -125,7 +125,9 @@
|
||||||
// Empty components are those which accept children but do not have any.
|
// Empty components are those which accept children but do not have any.
|
||||||
// Empty states can be shown for these components, but can be disabled
|
// Empty states can be shown for these components, but can be disabled
|
||||||
// in the component manifest.
|
// in the component manifest.
|
||||||
$: empty = interactive && !children.length && hasChildren
|
$: empty =
|
||||||
|
(interactive && !children.length && hasChildren) ||
|
||||||
|
hasMissingRequiredSettings
|
||||||
$: emptyState = empty && showEmptyState
|
$: emptyState = empty && showEmptyState
|
||||||
|
|
||||||
// Enrich component settings
|
// Enrich component settings
|
||||||
|
|
|
@ -2,28 +2,25 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { builderStore } from "stores"
|
import { builderStore } from "stores"
|
||||||
|
|
||||||
const { styleable } = getContext("sdk")
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
|
||||||
$: requiredSetting = $component.missingRequiredSettings?.[0]
|
$: requiredSetting = $component.missingRequiredSettings?.[0]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $builderStore.inBuilder && requiredSetting}
|
{#if $builderStore.inBuilder && requiredSetting}
|
||||||
<div use:styleable={$component.styles}>
|
<div class="component-placeholder">
|
||||||
<div class="component-placeholder">
|
<span>
|
||||||
<span>
|
Add the <mark>{requiredSetting.label}</mark> setting to start using your component
|
||||||
Add the <mark>{requiredSetting.label}</mark> setting to start using your
|
-
|
||||||
component -
|
</span>
|
||||||
</span>
|
<span
|
||||||
<span
|
class="spectrum-Link"
|
||||||
class="spectrum-Link"
|
on:click={() => {
|
||||||
on:click={() => {
|
builderStore.actions.highlightSetting(requiredSetting.key)
|
||||||
builderStore.actions.highlightSetting(requiredSetting.key)
|
}}
|
||||||
}}
|
>
|
||||||
>
|
Show me
|
||||||
Show me
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { ProgressCircle, Pagination } from "@budibase/bbui"
|
import { ProgressCircle, Pagination } from "@budibase/bbui"
|
||||||
import Placeholder from "./Placeholder.svelte"
|
|
||||||
import { fetchData, LuceneUtils } from "@budibase/frontend-core"
|
import { fetchData, LuceneUtils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let dataSource
|
export let dataSource
|
||||||
|
@ -133,11 +132,7 @@
|
||||||
<ProgressCircle />
|
<ProgressCircle />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#if $component.emptyState}
|
<slot />
|
||||||
<Placeholder />
|
|
||||||
{:else}
|
|
||||||
<slot />
|
|
||||||
{/if}
|
|
||||||
{#if paginate && $fetch.supportsPagination}
|
{#if paginate && $fetch.supportsPagination}
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
if (["user", "url"].includes(context.closestComponentId)) {
|
if (["user", "url"].includes(context.closestComponentId)) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
// Always inherit the closest data source
|
// Always inherit the closest datasource
|
||||||
const closestContext = context[`${context.closestComponentId}`] || {}
|
const closestContext = context[`${context.closestComponentId}`] || {}
|
||||||
return closestContext || {}
|
return closestContext || {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -372,7 +372,7 @@
|
||||||
formState,
|
formState,
|
||||||
formApi,
|
formApi,
|
||||||
|
|
||||||
// Data source is needed by attachment fields to be able to upload files
|
// Datasource is needed by attachment fields to be able to upload files
|
||||||
// to the correct table ID
|
// to the correct table ID
|
||||||
dataSource,
|
dataSource,
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,6 +2,7 @@ import ClientApp from "./components/ClientApp.svelte"
|
||||||
import { componentStore, builderStore, appStore, devToolsStore } from "./stores"
|
import { componentStore, builderStore, appStore, devToolsStore } from "./stores"
|
||||||
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js"
|
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
import { initWebsocket } from "./websocket.js"
|
||||||
|
|
||||||
// Provide svelte and svelte/internal as globals for custom components
|
// Provide svelte and svelte/internal as globals for custom components
|
||||||
import * as svelte from "svelte"
|
import * as svelte from "svelte"
|
||||||
|
@ -28,6 +29,7 @@ const loadBudibase = () => {
|
||||||
navigation: window["##BUDIBASE_PREVIEW_NAVIGATION##"],
|
navigation: window["##BUDIBASE_PREVIEW_NAVIGATION##"],
|
||||||
hiddenComponentIds: window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"],
|
hiddenComponentIds: window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"],
|
||||||
usedPlugins: window["##BUDIBASE_USED_PLUGINS##"],
|
usedPlugins: window["##BUDIBASE_USED_PLUGINS##"],
|
||||||
|
location: window["##BUDIBASE_LOCATION##"],
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set app ID - this window flag is set by both the preview and the real
|
// Set app ID - this window flag is set by both the preview and the real
|
||||||
|
@ -51,6 +53,9 @@ const loadBudibase = () => {
|
||||||
window.registerCustomComponent =
|
window.registerCustomComponent =
|
||||||
componentStore.actions.registerCustomComponent
|
componentStore.actions.registerCustomComponent
|
||||||
|
|
||||||
|
// Initialise websocket
|
||||||
|
initWebsocket()
|
||||||
|
|
||||||
// Create app if one hasn't been created yet
|
// Create app if one hasn't been created yet
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = new ClientApp({
|
app = new ClientApp({
|
||||||
|
|
|
@ -19,6 +19,7 @@ const createBuilderStore = () => {
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
navigation: null,
|
navigation: null,
|
||||||
hiddenComponentIds: [],
|
hiddenComponentIds: [],
|
||||||
|
usedPlugins: null,
|
||||||
|
|
||||||
// Legacy - allow the builder to specify a layout
|
// Legacy - allow the builder to specify a layout
|
||||||
layout: null,
|
layout: null,
|
||||||
|
@ -84,6 +85,20 @@ const createBuilderStore = () => {
|
||||||
highlightSetting: setting => {
|
highlightSetting: setting => {
|
||||||
dispatchEvent("highlight-setting", { setting })
|
dispatchEvent("highlight-setting", { setting })
|
||||||
},
|
},
|
||||||
|
updateUsedPlugin: (name, hash) => {
|
||||||
|
// Check if we used this plugin
|
||||||
|
const used = get(store)?.usedPlugins?.find(x => x.name === name)
|
||||||
|
if (used) {
|
||||||
|
store.update(state => {
|
||||||
|
state.usedPlugins = state.usedPlugins.filter(x => x.name !== name)
|
||||||
|
state.usedPlugins.push({
|
||||||
|
...used,
|
||||||
|
hash,
|
||||||
|
})
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...store,
|
...store,
|
||||||
|
|
|
@ -141,7 +141,7 @@ const createComponentStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const registerCustomComponent = ({ Component, schema, version }) => {
|
const registerCustomComponent = ({ Component, schema, version }) => {
|
||||||
if (!Component || !schema?.schema?.name) {
|
if (!Component || !schema?.schema?.name || !version) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const component = `plugin/${schema.schema.name}`
|
const component = `plugin/${schema.schema.name}`
|
||||||
|
@ -149,7 +149,6 @@ const createComponentStore = () => {
|
||||||
state.customComponentManifest[component] = {
|
state.customComponentManifest[component] = {
|
||||||
Component,
|
Component,
|
||||||
schema,
|
schema,
|
||||||
version,
|
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,7 +13,7 @@ const schemaComponentMap = {
|
||||||
/**
|
/**
|
||||||
* Determine data types for search fields and only use those that are valid
|
* Determine data types for search fields and only use those that are valid
|
||||||
* @param searchColumns the search columns to use
|
* @param searchColumns the search columns to use
|
||||||
* @param schema the data source schema
|
* @param schema the datasource schema
|
||||||
*/
|
*/
|
||||||
export const enrichSearchColumns = (searchColumns, schema) => {
|
export const enrichSearchColumns = (searchColumns, schema) => {
|
||||||
let enrichedColumns = []
|
let enrichedColumns = []
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { builderStore } from "./stores/index.js"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { io } from "socket.io-client"
|
||||||
|
|
||||||
|
export const initWebsocket = () => {
|
||||||
|
const { inBuilder, location } = get(builderStore)
|
||||||
|
|
||||||
|
// Only connect when we're inside the builder preview, for now
|
||||||
|
if (!inBuilder || !location) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise connection
|
||||||
|
const tls = location.protocol === "https:"
|
||||||
|
const proto = tls ? "wss:" : "ws:"
|
||||||
|
const host = location.hostname
|
||||||
|
const port = location.port || (tls ? 443 : 80)
|
||||||
|
const socket = io(`${proto}//${host}:${port}`, {
|
||||||
|
path: "/socket/client",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
socket.on("plugin-update", data => {
|
||||||
|
builderStore.actions.updateUsedPlugin(data.name, data.hash)
|
||||||
|
})
|
||||||
|
}
|
|
@ -113,6 +113,11 @@
|
||||||
estree-walker "^1.0.1"
|
estree-walker "^1.0.1"
|
||||||
picomatch "^2.2.2"
|
picomatch "^2.2.2"
|
||||||
|
|
||||||
|
"@socket.io/component-emitter@~3.1.0":
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
|
||||||
|
integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
|
||||||
|
|
||||||
"@spectrum-css/button@^3.0.3":
|
"@spectrum-css/button@^3.0.3":
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@spectrum-css/button/-/button-3.0.3.tgz#2df1efaab6c7e0b3b06cb4b59e1eae59c7f1fc84"
|
resolved "https://registry.yarnpkg.com/@spectrum-css/button/-/button-3.0.3.tgz#2df1efaab6c7e0b3b06cb4b59e1eae59c7f1fc84"
|
||||||
|
@ -469,6 +474,13 @@ dayjs@^1.10.5:
|
||||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.8.tgz#267df4bc6276fcb33c04a6735287e3f429abec41"
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.8.tgz#267df4bc6276fcb33c04a6735287e3f429abec41"
|
||||||
integrity sha512-wbNwDfBHHur9UOzNUjeKUOJ0fCb0a52Wx0xInmQ7Y8FstyajiV1NmK1e00cxsr9YrE9r7yAChE0VvpuY5Rnlow==
|
integrity sha512-wbNwDfBHHur9UOzNUjeKUOJ0fCb0a52Wx0xInmQ7Y8FstyajiV1NmK1e00cxsr9YrE9r7yAChE0VvpuY5Rnlow==
|
||||||
|
|
||||||
|
debug@~4.3.1, debug@~4.3.2:
|
||||||
|
version "4.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||||
|
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||||
|
dependencies:
|
||||||
|
ms "2.1.2"
|
||||||
|
|
||||||
deepmerge@^4.2.2:
|
deepmerge@^4.2.2:
|
||||||
version "4.2.2"
|
version "4.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
|
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
|
||||||
|
@ -536,6 +548,22 @@ emojis-list@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
||||||
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
||||||
|
|
||||||
|
engine.io-client@~6.2.1:
|
||||||
|
version "6.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.2.2.tgz#c6c5243167f5943dcd9c4abee1bfc634aa2cbdd0"
|
||||||
|
integrity sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==
|
||||||
|
dependencies:
|
||||||
|
"@socket.io/component-emitter" "~3.1.0"
|
||||||
|
debug "~4.3.1"
|
||||||
|
engine.io-parser "~5.0.3"
|
||||||
|
ws "~8.2.3"
|
||||||
|
xmlhttprequest-ssl "~2.0.0"
|
||||||
|
|
||||||
|
engine.io-parser@~5.0.3:
|
||||||
|
version "5.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0"
|
||||||
|
integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==
|
||||||
|
|
||||||
entities@^2.0.0:
|
entities@^2.0.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
|
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
|
||||||
|
@ -824,6 +852,11 @@ minimist@^1.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||||
|
|
||||||
|
ms@2.1.2:
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||||
|
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||||
|
|
||||||
nanoid@^2.1.0:
|
nanoid@^2.1.0:
|
||||||
version "2.1.11"
|
version "2.1.11"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
|
||||||
|
@ -1389,6 +1422,24 @@ slash@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
||||||
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
||||||
|
|
||||||
|
socket.io-client@^4.5.1:
|
||||||
|
version "4.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.5.1.tgz#cab8da71976a300d3090414e28c2203a47884d84"
|
||||||
|
integrity sha512-e6nLVgiRYatS+AHXnOnGi4ocOpubvOUCGhyWw8v+/FxW8saHkinG6Dfhi9TU0Kt/8mwJIAASxvw6eujQmjdZVA==
|
||||||
|
dependencies:
|
||||||
|
"@socket.io/component-emitter" "~3.1.0"
|
||||||
|
debug "~4.3.2"
|
||||||
|
engine.io-client "~6.2.1"
|
||||||
|
socket.io-parser "~4.2.0"
|
||||||
|
|
||||||
|
socket.io-parser@~4.2.0:
|
||||||
|
version "4.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5"
|
||||||
|
integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==
|
||||||
|
dependencies:
|
||||||
|
"@socket.io/component-emitter" "~3.1.0"
|
||||||
|
debug "~4.3.1"
|
||||||
|
|
||||||
source-map-js@^1.0.1, source-map-js@^1.0.2:
|
source-map-js@^1.0.1, source-map-js@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||||
|
@ -1598,6 +1649,16 @@ wrappy@1:
|
||||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||||
|
|
||||||
|
ws@~8.2.3:
|
||||||
|
version "8.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
|
||||||
|
integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
|
||||||
|
|
||||||
|
xmlhttprequest-ssl@~2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
|
||||||
|
integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==
|
||||||
|
|
||||||
y18n@^5.0.5:
|
y18n@^5.0.5:
|
||||||
version "5.0.8"
|
version "5.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/frontend-core",
|
"name": "@budibase/frontend-core",
|
||||||
"version": "1.3.12-alpha.3",
|
"version": "1.3.15-alpha.9",
|
||||||
"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.3.12-alpha.3",
|
"@budibase/bbui": "1.3.15-alpha.9",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"svelte": "^3.46.2"
|
"svelte": "^3.46.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,14 @@ export const buildDatasourceEndpoints = API => ({
|
||||||
/**
|
/**
|
||||||
* Prompts the server to build the schema for a datasource.
|
* Prompts the server to build the schema for a datasource.
|
||||||
* @param datasourceId the datasource ID to build the schema for
|
* @param datasourceId the datasource ID to build the schema for
|
||||||
|
* @param tablesFilter list of specific table names to be build the schema
|
||||||
*/
|
*/
|
||||||
buildDatasourceSchema: async datasourceId => {
|
buildDatasourceSchema: async ({ datasourceId, tablesFilter }) => {
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: `/api/datasources/${datasourceId}/schema`,
|
url: `/api/datasources/${datasourceId}/schema`,
|
||||||
|
body: {
|
||||||
|
tablesFilter,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,22 @@ export const buildPluginEndpoints = API => ({
|
||||||
*/
|
*/
|
||||||
uploadPlugin: async data => {
|
uploadPlugin: async data => {
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: "/api/plugin/upload",
|
url: `/api/plugin/upload`,
|
||||||
body: data,
|
body: data,
|
||||||
json: false,
|
json: false,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a plugin from URL, Github or NPM
|
||||||
|
*/
|
||||||
|
createPlugin: async data => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/plugin`,
|
||||||
|
body: data,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a list of all plugins
|
* Gets a list of all plugins
|
||||||
*/
|
*/
|
||||||
|
@ -19,4 +29,16 @@ export const buildPluginEndpoints = API => ({
|
||||||
url: "/api/plugin",
|
url: "/api/plugin",
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a plugin.
|
||||||
|
* @param pluginId the ID of the plugin to delete
|
||||||
|
*
|
||||||
|
* * @param pluginId the revision of the plugin to delete
|
||||||
|
*/
|
||||||
|
deletePlugin: async pluginId => {
|
||||||
|
return await API.delete({
|
||||||
|
url: `/api/plugin/${pluginId}`,
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -80,6 +80,19 @@ const cleanupQuery = query => {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a numeric prefix on field names designed to give fields uniqueness
|
||||||
|
*/
|
||||||
|
const removeKeyNumbering = key => {
|
||||||
|
if (typeof key === "string" && key.match(/\d[0-9]*:/g) != null) {
|
||||||
|
const parts = key.split(":")
|
||||||
|
parts.shift()
|
||||||
|
return parts.join(":")
|
||||||
|
} else {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a lucene JSON query from the filter structure generated in the builder
|
* Builds a lucene JSON query from the filter structure generated in the builder
|
||||||
* @param filter the builder filter structure
|
* @param filter the builder filter structure
|
||||||
|
@ -108,7 +121,7 @@ export const buildLuceneQuery = filter => {
|
||||||
query.allOr = true
|
query.allOr = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (type === "datetime") {
|
if (type === "datetime" && !isHbs) {
|
||||||
// Ensure date value is a valid date and parse into correct format
|
// Ensure date value is a valid date and parse into correct format
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return
|
return
|
||||||
|
@ -194,7 +207,7 @@ export const runLuceneQuery = (docs, query) => {
|
||||||
const filters = Object.entries(query[type] || {})
|
const filters = Object.entries(query[type] || {})
|
||||||
for (let i = 0; i < filters.length; i++) {
|
for (let i = 0; i < filters.length; i++) {
|
||||||
const [key, testValue] = filters[i]
|
const [key, testValue] = filters[i]
|
||||||
const docValue = Helpers.deepGet(doc, key)
|
const docValue = Helpers.deepGet(doc, removeKeyNumbering(key))
|
||||||
if (failFn(docValue, testValue)) {
|
if (failFn(docValue, testValue)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "1.3.12-alpha.3",
|
"version": "1.3.15-alpha.9",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -77,11 +77,11 @@
|
||||||
"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.3.12-alpha.3",
|
"@budibase/backend-core": "1.3.15-alpha.9",
|
||||||
"@budibase/client": "1.3.12-alpha.3",
|
"@budibase/client": "1.3.15-alpha.9",
|
||||||
"@budibase/pro": "1.3.12-alpha.3",
|
"@budibase/pro": "1.3.15-alpha.9",
|
||||||
"@budibase/string-templates": "1.3.12-alpha.3",
|
"@budibase/string-templates": "1.3.15-alpha.9",
|
||||||
"@budibase/types": "1.3.12-alpha.3",
|
"@budibase/types": "1.3.15-alpha.9",
|
||||||
"@bull-board/api": "3.7.0",
|
"@bull-board/api": "3.7.0",
|
||||||
"@bull-board/koa": "3.9.4",
|
"@bull-board/koa": "3.9.4",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
@ -95,7 +95,7 @@
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"bull": "4.8.5",
|
"bull": "4.8.5",
|
||||||
"chmodr": "1.2.0",
|
"chmodr": "1.2.0",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "3.5.3",
|
||||||
"csvtojson": "2.0.10",
|
"csvtojson": "2.0.10",
|
||||||
"curlconverter": "3.21.0",
|
"curlconverter": "3.21.0",
|
||||||
"dotenv": "8.2.0",
|
"dotenv": "8.2.0",
|
||||||
|
@ -139,9 +139,10 @@
|
||||||
"redis": "4",
|
"redis": "4",
|
||||||
"server-destroy": "1.0.1",
|
"server-destroy": "1.0.1",
|
||||||
"snowflake-promise": "^4.5.0",
|
"snowflake-promise": "^4.5.0",
|
||||||
|
"socket.io": "^4.5.1",
|
||||||
"svelte": "3.49.0",
|
"svelte": "3.49.0",
|
||||||
"swagger-parser": "10.0.3",
|
"swagger-parser": "10.0.3",
|
||||||
"tar": "^6.1.11",
|
"tar": "6.1.11",
|
||||||
"to-json-schema": "0.2.5",
|
"to-json-schema": "0.2.5",
|
||||||
"uuid": "3.3.2",
|
"uuid": "3.3.2",
|
||||||
"validate.js": "0.13.1",
|
"validate.js": "0.13.1",
|
||||||
|
|
|
@ -1327,7 +1327,7 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"datasourceId": {
|
"datasourceId": {
|
||||||
"description": "The ID of the data source the query belongs to.",
|
"description": "The ID of the datasource the query belongs to.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
@ -1386,7 +1386,7 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"datasourceId": {
|
"datasourceId": {
|
||||||
"description": "The ID of the data source the query belongs to.",
|
"description": "The ID of the datasource the query belongs to.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"parameters": {
|
"parameters": {
|
||||||
|
@ -2289,7 +2289,7 @@
|
||||||
},
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Returns the created table, including the ID which has been generated for it. This can be internal or external data sources.",
|
"description": "Returns the created table, including the ID which has been generated for it. This can be internal or external datasources.",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
|
|
|
@ -1004,7 +1004,7 @@ components:
|
||||||
description: The ID of the query.
|
description: The ID of the query.
|
||||||
type: string
|
type: string
|
||||||
datasourceId:
|
datasourceId:
|
||||||
description: The ID of the data source the query belongs to.
|
description: The ID of the datasource the query belongs to.
|
||||||
type: string
|
type: string
|
||||||
parameters:
|
parameters:
|
||||||
description: The bindings which are required to perform this query.
|
description: The bindings which are required to perform this query.
|
||||||
|
@ -1051,7 +1051,7 @@ components:
|
||||||
description: The ID of the query.
|
description: The ID of the query.
|
||||||
type: string
|
type: string
|
||||||
datasourceId:
|
datasourceId:
|
||||||
description: The ID of the data source the query belongs to.
|
description: The ID of the datasource the query belongs to.
|
||||||
type: string
|
type: string
|
||||||
parameters:
|
parameters:
|
||||||
description: The bindings which are required to perform this query.
|
description: The bindings which are required to perform this query.
|
||||||
|
@ -1665,7 +1665,7 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Returns the created table, including the ID which has been
|
description: Returns the created table, including the ID which has been
|
||||||
generated for it. This can be internal or external data sources.
|
generated for it. This can be internal or external datasources.
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
|
|
|
@ -82,7 +82,7 @@ const querySchema = object(
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
datasourceId: {
|
datasourceId: {
|
||||||
description: "The ID of the data source the query belongs to.",
|
description: "The ID of the datasource the query belongs to.",
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
|
|
|
@ -552,11 +552,7 @@ export const sync = async (ctx: any, next: any) => {
|
||||||
})
|
})
|
||||||
let error
|
let error
|
||||||
try {
|
try {
|
||||||
await replication.replicate({
|
await replication.replicate(replication.appReplicateOpts())
|
||||||
filter: function (doc: any) {
|
|
||||||
return doc._id !== DocumentType.APP_METADATA
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err
|
error = err
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
const { streamBackup } = require("../../utilities/fileSystem")
|
const { streamBackup } = require("../../utilities/fileSystem")
|
||||||
const { events, context } = require("@budibase/backend-core")
|
const { events, context } = require("@budibase/backend-core")
|
||||||
const { DocumentType } = require("../../db/utils")
|
const { DocumentType } = require("../../db/utils")
|
||||||
|
const { isQsTrue } = require("../../utilities")
|
||||||
|
|
||||||
exports.exportAppDump = async function (ctx) {
|
exports.exportAppDump = async function (ctx) {
|
||||||
let { appId, excludeRows } = ctx.query
|
let { appId, excludeRows } = ctx.query
|
||||||
const appName = decodeURI(ctx.query.appname)
|
const appName = decodeURI(ctx.query.appname)
|
||||||
excludeRows = excludeRows === "true"
|
excludeRows = isQsTrue(excludeRows)
|
||||||
const backupIdentifier = `${appName}-export-${new Date().getTime()}.txt`
|
const backupIdentifier = `${appName}-export-${new Date().getTime()}.txt`
|
||||||
ctx.attachment(backupIdentifier)
|
ctx.attachment(backupIdentifier)
|
||||||
ctx.body = await streamBackup(appId, excludeRows)
|
ctx.body = await streamBackup(appId, excludeRows)
|
||||||
|
|
|
@ -2,7 +2,6 @@ const { DocumentType, getPluginParams } = require("../../db/utils")
|
||||||
const { getComponentLibraryManifest } = require("../../utilities/fileSystem")
|
const { getComponentLibraryManifest } = require("../../utilities/fileSystem")
|
||||||
const { getAppDB } = require("@budibase/backend-core/context")
|
const { getAppDB } = require("@budibase/backend-core/context")
|
||||||
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
|
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
|
||||||
const env = require("../../environment")
|
|
||||||
|
|
||||||
exports.fetchAppComponentDefinitions = async function (ctx) {
|
exports.fetchAppComponentDefinitions = async function (ctx) {
|
||||||
try {
|
try {
|
||||||
|
@ -33,26 +32,23 @@ exports.fetchAppComponentDefinitions = async function (ctx) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// for now custom components only supported in self-host
|
// Add custom components
|
||||||
if (env.SELF_HOSTED) {
|
const globalDB = getGlobalDB()
|
||||||
// Add custom components
|
const response = await globalDB.allDocs(
|
||||||
const globalDB = getGlobalDB()
|
getPluginParams(null, {
|
||||||
const response = await globalDB.allDocs(
|
include_docs: true,
|
||||||
getPluginParams(null, {
|
})
|
||||||
include_docs: true,
|
)
|
||||||
})
|
response.rows
|
||||||
)
|
.map(row => row.doc)
|
||||||
response.rows
|
.filter(plugin => plugin.schema.type === "component")
|
||||||
.map(row => row.doc)
|
.forEach(plugin => {
|
||||||
.filter(plugin => plugin.schema.type === "component")
|
const fullComponentName = `plugin/${plugin.name}`
|
||||||
.forEach(plugin => {
|
definitions[fullComponentName] = {
|
||||||
const fullComponentName = `plugin/${plugin.name}`
|
component: fullComponentName,
|
||||||
definitions[fullComponentName] = {
|
...plugin.schema.schema,
|
||||||
component: fullComponentName,
|
}
|
||||||
...plugin.schema.schema,
|
})
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.body = definitions
|
ctx.body = definitions
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -50,9 +50,23 @@ exports.fetch = async function (ctx) {
|
||||||
exports.buildSchemaFromDb = async function (ctx) {
|
exports.buildSchemaFromDb = async function (ctx) {
|
||||||
const db = getAppDB()
|
const db = getAppDB()
|
||||||
const datasource = await db.get(ctx.params.datasourceId)
|
const datasource = await db.get(ctx.params.datasourceId)
|
||||||
|
const tablesFilter = ctx.request.body.tablesFilter
|
||||||
|
|
||||||
const { tables, error } = await buildSchemaHelper(datasource)
|
let { tables, error } = await buildSchemaHelper(datasource)
|
||||||
datasource.entities = tables
|
if (tablesFilter) {
|
||||||
|
if (!datasource.entities) {
|
||||||
|
datasource.entities = {}
|
||||||
|
}
|
||||||
|
for (let key in tables) {
|
||||||
|
if (
|
||||||
|
tablesFilter.some(filter => filter.toLowerCase() === key.toLowerCase())
|
||||||
|
) {
|
||||||
|
datasource.entities[key] = tables[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
datasource.entities = tables
|
||||||
|
}
|
||||||
|
|
||||||
const dbResp = await db.put(datasource)
|
const dbResp = await db.put(datasource)
|
||||||
datasource._rev = dbResp.rev
|
datasource._rev = dbResp.rev
|
||||||
|
@ -223,10 +237,9 @@ const buildSchemaHelper = async datasource => {
|
||||||
// Connect to the DB and build the schema
|
// Connect to the DB and build the schema
|
||||||
const connector = new Connector(datasource.config)
|
const connector = new Connector(datasource.config)
|
||||||
await connector.buildSchema(datasource._id, datasource.entities)
|
await connector.buildSchema(datasource._id, datasource.entities)
|
||||||
datasource.entities = connector.tables
|
|
||||||
|
|
||||||
// make sure they all have a display name selected
|
// make sure they all have a display name selected
|
||||||
for (let entity of Object.values(datasource.entities)) {
|
for (let entity of Object.values(datasource.entities ?? {})) {
|
||||||
if (entity.primaryDisplay) {
|
if (entity.primaryDisplay) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
getAppId,
|
getAppId,
|
||||||
getAppDB,
|
getAppDB,
|
||||||
getProdAppDB,
|
getProdAppDB,
|
||||||
|
getDevAppDB,
|
||||||
} from "@budibase/backend-core/context"
|
} from "@budibase/backend-core/context"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import { events } from "@budibase/backend-core"
|
import { events } from "@budibase/backend-core"
|
||||||
|
@ -110,17 +111,29 @@ async function deployApp(deployment: any) {
|
||||||
target: productionAppId,
|
target: productionAppId,
|
||||||
}
|
}
|
||||||
replication = new Replication(config)
|
replication = new Replication(config)
|
||||||
|
const devDb = getDevAppDB()
|
||||||
|
console.log("Compacting development DB")
|
||||||
|
await devDb.compact()
|
||||||
console.log("Replication object created")
|
console.log("Replication object created")
|
||||||
await replication.replicate()
|
await replication.replicate(replication.appReplicateOpts())
|
||||||
console.log("replication complete.. replacing app meta doc")
|
console.log("replication complete.. replacing app meta doc")
|
||||||
|
// app metadata is excluded as it is likely to be in conflict
|
||||||
|
// replicate the app metadata document manually
|
||||||
const db = getProdAppDB()
|
const db = getProdAppDB()
|
||||||
const appDoc = await db.get(DocumentType.APP_METADATA)
|
const appDoc = await devDb.get(DocumentType.APP_METADATA)
|
||||||
|
try {
|
||||||
|
const prodAppDoc = await db.get(DocumentType.APP_METADATA)
|
||||||
|
appDoc._rev = prodAppDoc._rev
|
||||||
|
} catch (err) {
|
||||||
|
delete appDoc._rev
|
||||||
|
}
|
||||||
|
|
||||||
|
// switch to production app ID
|
||||||
deployment.appUrl = appDoc.url
|
deployment.appUrl = appDoc.url
|
||||||
|
|
||||||
appDoc.appId = productionAppId
|
appDoc.appId = productionAppId
|
||||||
appDoc.instance._id = productionAppId
|
appDoc.instance._id = productionAppId
|
||||||
|
// remove automation errors if they exist
|
||||||
|
delete appDoc.automationErrors
|
||||||
await db.put(appDoc)
|
await db.put(appDoc)
|
||||||
await appCache.invalidateAppMetadata(productionAppId)
|
await appCache.invalidateAppMetadata(productionAppId)
|
||||||
console.log("New app doc written successfully.")
|
console.log("New app doc written successfully.")
|
||||||
|
|
|
@ -1,110 +0,0 @@
|
||||||
import { ObjectStoreBuckets } from "../../constants"
|
|
||||||
import { extractPluginTarball, loadJSFile } from "../../utilities/fileSystem"
|
|
||||||
import { getGlobalDB } from "@budibase/backend-core/tenancy"
|
|
||||||
import { generatePluginID, getPluginParams } from "../../db/utils"
|
|
||||||
import { uploadDirectory } from "@budibase/backend-core/objectStore"
|
|
||||||
import { PluginType, FileType } from "@budibase/types"
|
|
||||||
import env from "../../environment"
|
|
||||||
|
|
||||||
export async function getPlugins(type?: PluginType) {
|
|
||||||
const db = getGlobalDB()
|
|
||||||
const response = await db.allDocs(
|
|
||||||
getPluginParams(null, {
|
|
||||||
include_docs: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const plugins = response.rows.map((row: any) => row.doc)
|
|
||||||
if (type) {
|
|
||||||
return plugins.filter((plugin: any) => plugin.schema?.type === type)
|
|
||||||
} else {
|
|
||||||
return plugins
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function upload(ctx: any) {
|
|
||||||
const plugins: FileType[] =
|
|
||||||
ctx.request.files.file.length > 1
|
|
||||||
? Array.from(ctx.request.files.file)
|
|
||||||
: [ctx.request.files.file]
|
|
||||||
try {
|
|
||||||
let docs = []
|
|
||||||
// can do single or multiple plugins
|
|
||||||
for (let plugin of plugins) {
|
|
||||||
const doc = await processPlugin(plugin)
|
|
||||||
docs.push(doc)
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
message: "Plugin(s) uploaded successfully",
|
|
||||||
plugins: docs,
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
const errMsg = err?.message ? err?.message : err
|
|
||||||
ctx.throw(400, `Failed to import plugin: ${errMsg}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetch(ctx: any) {
|
|
||||||
ctx.body = await getPlugins()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function destroy(ctx: any) {}
|
|
||||||
|
|
||||||
export async function processPlugin(plugin: FileType) {
|
|
||||||
if (!env.SELF_HOSTED) {
|
|
||||||
throw new Error("Plugins not supported outside of self-host.")
|
|
||||||
}
|
|
||||||
const db = getGlobalDB()
|
|
||||||
const { metadata, directory } = await extractPluginTarball(plugin)
|
|
||||||
const version = metadata.package.version,
|
|
||||||
name = metadata.package.name,
|
|
||||||
description = metadata.package.description
|
|
||||||
|
|
||||||
// first open the tarball into tmp directory
|
|
||||||
const bucketPath = `${name}/`
|
|
||||||
const files = await uploadDirectory(
|
|
||||||
ObjectStoreBuckets.PLUGINS,
|
|
||||||
directory,
|
|
||||||
bucketPath
|
|
||||||
)
|
|
||||||
const jsFile = files.find((file: any) => file.name.endsWith(".js"))
|
|
||||||
if (!jsFile) {
|
|
||||||
throw new Error(`Plugin missing .js file.`)
|
|
||||||
}
|
|
||||||
// validate the JS for a datasource
|
|
||||||
if (metadata.schema.type === PluginType.DATASOURCE) {
|
|
||||||
const js = loadJSFile(directory, jsFile.name)
|
|
||||||
// TODO: this isn't safe - but we need full node environment
|
|
||||||
// in future we should do this in a thread for safety
|
|
||||||
try {
|
|
||||||
eval(js)
|
|
||||||
} catch (err: any) {
|
|
||||||
const message = err?.message ? err.message : JSON.stringify(err)
|
|
||||||
throw new Error(`JS invalid: ${message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const jsFileName = jsFile.name
|
|
||||||
const pluginId = generatePluginID(name)
|
|
||||||
|
|
||||||
// overwrite existing docs entirely if they exist
|
|
||||||
let rev
|
|
||||||
try {
|
|
||||||
const existing = await db.get(pluginId)
|
|
||||||
rev = existing._rev
|
|
||||||
} catch (err) {
|
|
||||||
rev = undefined
|
|
||||||
}
|
|
||||||
const doc = {
|
|
||||||
_id: pluginId,
|
|
||||||
_rev: rev,
|
|
||||||
...metadata,
|
|
||||||
name,
|
|
||||||
version,
|
|
||||||
description,
|
|
||||||
jsUrl: `${bucketPath}${jsFileName}`,
|
|
||||||
}
|
|
||||||
const response = await db.put(doc)
|
|
||||||
return {
|
|
||||||
...doc,
|
|
||||||
_rev: response.rev,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import {
|
||||||
|
createTempFolder,
|
||||||
|
getPluginMetadata,
|
||||||
|
extractTarball,
|
||||||
|
} from "../../../utilities/fileSystem"
|
||||||
|
|
||||||
|
export async function fileUpload(file: { name: string; path: string }) {
|
||||||
|
if (!file.name.endsWith(".tar.gz")) {
|
||||||
|
throw new Error("Plugin must be compressed into a gzipped tarball.")
|
||||||
|
}
|
||||||
|
const path = createTempFolder(file.name.split(".tar.gz")[0])
|
||||||
|
await extractTarball(file.path, path)
|
||||||
|
|
||||||
|
return await getPluginMetadata(path)
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { getPluginMetadata } from "../../../utilities/fileSystem"
|
||||||
|
import fetch from "node-fetch"
|
||||||
|
import { downloadUnzipTarball } from "./utils"
|
||||||
|
|
||||||
|
export async function request(
|
||||||
|
url: string,
|
||||||
|
headers: { [key: string]: string },
|
||||||
|
err: string
|
||||||
|
) {
|
||||||
|
const response = await fetch(url, { headers })
|
||||||
|
if (response.status >= 300) {
|
||||||
|
const respErr = await response.text()
|
||||||
|
throw new Error(`Error: ${err} - ${respErr}`)
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function githubUpload(url: string, name = "", token = "") {
|
||||||
|
let githubUrl = url
|
||||||
|
|
||||||
|
if (!githubUrl.includes("https://github.com/")) {
|
||||||
|
throw new Error("The plugin origin must be from Github")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.includes(".git")) {
|
||||||
|
githubUrl = url.replace(".git", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
const githubApiUrl = githubUrl.replace(
|
||||||
|
"https://github.com/",
|
||||||
|
"https://api.github.com/repos/"
|
||||||
|
)
|
||||||
|
const headers: any = token ? { Authorization: `Bearer ${token}` } : {}
|
||||||
|
const pluginDetails = await request(
|
||||||
|
githubApiUrl,
|
||||||
|
headers,
|
||||||
|
"Repository not found"
|
||||||
|
)
|
||||||
|
const pluginName = pluginDetails.name || name
|
||||||
|
const pluginLatestReleaseUrl = pluginDetails?.["releases_url"]
|
||||||
|
? pluginDetails?.["releases_url"].replace("{/id}", "/latest")
|
||||||
|
: undefined
|
||||||
|
if (!pluginLatestReleaseUrl) {
|
||||||
|
throw new Error("Github release not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginReleaseDetails = await request(
|
||||||
|
pluginLatestReleaseUrl,
|
||||||
|
headers,
|
||||||
|
"Github latest release not found"
|
||||||
|
)
|
||||||
|
const pluginReleaseTarballAsset = pluginReleaseDetails?.assets?.find(
|
||||||
|
(x: any) => x?.["content_type"] === "application/gzip"
|
||||||
|
)
|
||||||
|
const pluginLastReleaseTarballUrl =
|
||||||
|
pluginReleaseTarballAsset?.["browser_download_url"]
|
||||||
|
if (!pluginLastReleaseTarballUrl) {
|
||||||
|
throw new Error("Github latest release url not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const path = await downloadUnzipTarball(
|
||||||
|
pluginLastReleaseTarballUrl,
|
||||||
|
pluginName,
|
||||||
|
headers
|
||||||
|
)
|
||||||
|
return await getPluginMetadata(path)
|
||||||
|
} catch (err: any) {
|
||||||
|
let errMsg = err?.message || err
|
||||||
|
if (errMsg === "unexpected response Not Found") {
|
||||||
|
errMsg = "Github release tarball not found"
|
||||||
|
}
|
||||||
|
throw new Error(errMsg)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,212 @@
|
||||||
|
import { ObjectStoreBuckets } from "../../../constants"
|
||||||
|
import { loadJSFile } from "../../../utilities/fileSystem"
|
||||||
|
import { npmUpload, urlUpload, githubUpload, fileUpload } from "./uploaders"
|
||||||
|
import { getGlobalDB } from "@budibase/backend-core/tenancy"
|
||||||
|
import { validate } from "@budibase/backend-core/plugins"
|
||||||
|
import { generatePluginID, getPluginParams } from "../../../db/utils"
|
||||||
|
import {
|
||||||
|
uploadDirectory,
|
||||||
|
deleteFolder,
|
||||||
|
} from "@budibase/backend-core/objectStore"
|
||||||
|
import { PluginType, FileType, PluginSource } from "@budibase/types"
|
||||||
|
import env from "../../../environment"
|
||||||
|
import { ClientAppSocket } from "../../../websocket"
|
||||||
|
|
||||||
|
export async function getPlugins(type?: PluginType) {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
const response = await db.allDocs(
|
||||||
|
getPluginParams(null, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const plugins = response.rows.map((row: any) => row.doc)
|
||||||
|
if (type) {
|
||||||
|
return plugins.filter((plugin: any) => plugin.schema?.type === type)
|
||||||
|
} else {
|
||||||
|
return plugins
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upload(ctx: any) {
|
||||||
|
const plugins: FileType[] =
|
||||||
|
ctx.request.files.file.length > 1
|
||||||
|
? Array.from(ctx.request.files.file)
|
||||||
|
: [ctx.request.files.file]
|
||||||
|
try {
|
||||||
|
let docs = []
|
||||||
|
// can do single or multiple plugins
|
||||||
|
for (let plugin of plugins) {
|
||||||
|
const doc = await processPlugin(plugin, PluginSource.FILE)
|
||||||
|
docs.push(doc)
|
||||||
|
}
|
||||||
|
ctx.body = {
|
||||||
|
message: "Plugin(s) uploaded successfully",
|
||||||
|
plugins: docs,
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
const errMsg = err?.message ? err?.message : err
|
||||||
|
|
||||||
|
ctx.throw(400, `Failed to import plugin: ${errMsg}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create(ctx: any) {
|
||||||
|
const { source, url, headers, githubToken } = ctx.request.body
|
||||||
|
|
||||||
|
try {
|
||||||
|
let metadata
|
||||||
|
let directory
|
||||||
|
// Generating random name as a backup and needed for url
|
||||||
|
let name = "PLUGIN_" + Math.floor(100000 + Math.random() * 900000)
|
||||||
|
|
||||||
|
switch (source) {
|
||||||
|
case PluginSource.NPM:
|
||||||
|
const { metadata: metadataNpm, directory: directoryNpm } =
|
||||||
|
await npmUpload(url, name)
|
||||||
|
metadata = metadataNpm
|
||||||
|
directory = directoryNpm
|
||||||
|
break
|
||||||
|
case PluginSource.GITHUB:
|
||||||
|
const { metadata: metadataGithub, directory: directoryGithub } =
|
||||||
|
await githubUpload(url, name, githubToken)
|
||||||
|
metadata = metadataGithub
|
||||||
|
directory = directoryGithub
|
||||||
|
break
|
||||||
|
case PluginSource.URL:
|
||||||
|
const headersObj = headers || {}
|
||||||
|
const { metadata: metadataUrl, directory: directoryUrl } =
|
||||||
|
await urlUpload(url, name, headersObj)
|
||||||
|
metadata = metadataUrl
|
||||||
|
directory = directoryUrl
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(metadata?.schema)
|
||||||
|
|
||||||
|
// Only allow components in cloud
|
||||||
|
if (!env.SELF_HOSTED && metadata?.schema?.type !== PluginType.COMPONENT) {
|
||||||
|
throw new Error(
|
||||||
|
"Only component plugins are supported outside of self-host"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = await storePlugin(metadata, directory, source)
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
message: "Plugin uploaded successfully",
|
||||||
|
plugins: [doc],
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
const errMsg = err?.message ? err?.message : err
|
||||||
|
|
||||||
|
ctx.throw(400, `Failed to import plugin: ${errMsg}`)
|
||||||
|
}
|
||||||
|
ctx.status = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetch(ctx: any) {
|
||||||
|
ctx.body = await getPlugins()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function destroy(ctx: any) {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
const { pluginId } = ctx.params
|
||||||
|
|
||||||
|
try {
|
||||||
|
const plugin = await db.get(pluginId)
|
||||||
|
const bucketPath = `${plugin.name}/`
|
||||||
|
await deleteFolder(ObjectStoreBuckets.PLUGINS, bucketPath)
|
||||||
|
|
||||||
|
await db.remove(pluginId, plugin._rev)
|
||||||
|
} catch (err: any) {
|
||||||
|
const errMsg = err?.message ? err?.message : err
|
||||||
|
|
||||||
|
ctx.throw(400, `Failed to delete plugin: ${errMsg}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.message = `Plugin ${ctx.params.pluginId} deleted.`
|
||||||
|
ctx.status = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function storePlugin(
|
||||||
|
metadata: any,
|
||||||
|
directory: any,
|
||||||
|
source?: string
|
||||||
|
) {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
const version = metadata.package.version,
|
||||||
|
name = metadata.package.name,
|
||||||
|
description = metadata.package.description,
|
||||||
|
hash = metadata.schema.hash
|
||||||
|
|
||||||
|
// first open the tarball into tmp directory
|
||||||
|
const bucketPath = `${name}/`
|
||||||
|
const files = await uploadDirectory(
|
||||||
|
ObjectStoreBuckets.PLUGINS,
|
||||||
|
directory,
|
||||||
|
bucketPath
|
||||||
|
)
|
||||||
|
const jsFile = files.find((file: any) => file.name.endsWith(".js"))
|
||||||
|
if (!jsFile) {
|
||||||
|
throw new Error(`Plugin missing .js file.`)
|
||||||
|
}
|
||||||
|
// validate the JS for a datasource
|
||||||
|
if (metadata.schema.type === PluginType.DATASOURCE) {
|
||||||
|
const js = loadJSFile(directory, jsFile.name)
|
||||||
|
// TODO: this isn't safe - but we need full node environment
|
||||||
|
// in future we should do this in a thread for safety
|
||||||
|
try {
|
||||||
|
eval(js)
|
||||||
|
} catch (err: any) {
|
||||||
|
const message = err?.message ? err.message : JSON.stringify(err)
|
||||||
|
throw new Error(`JS invalid: ${message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const jsFileName = jsFile.name
|
||||||
|
const pluginId = generatePluginID(name)
|
||||||
|
|
||||||
|
// overwrite existing docs entirely if they exist
|
||||||
|
let rev
|
||||||
|
try {
|
||||||
|
const existing = await db.get(pluginId)
|
||||||
|
rev = existing._rev
|
||||||
|
} catch (err) {
|
||||||
|
rev = undefined
|
||||||
|
}
|
||||||
|
let doc = {
|
||||||
|
_id: pluginId,
|
||||||
|
_rev: rev,
|
||||||
|
...metadata,
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
hash,
|
||||||
|
description,
|
||||||
|
jsUrl: `${bucketPath}${jsFileName}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source) {
|
||||||
|
doc = {
|
||||||
|
...doc,
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await db.put(doc)
|
||||||
|
ClientAppSocket.emit("plugin-update", { name, hash })
|
||||||
|
return {
|
||||||
|
...doc,
|
||||||
|
_rev: response.rev,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processPlugin(plugin: FileType, source?: string) {
|
||||||
|
const { metadata, directory } = await fileUpload(plugin)
|
||||||
|
validate(metadata?.schema)
|
||||||
|
|
||||||
|
// Only allow components in cloud
|
||||||
|
if (!env.SELF_HOSTED && metadata?.schema?.type !== PluginType.COMPONENT) {
|
||||||
|
throw new Error("Only component plugins are supported outside of self-host")
|
||||||
|
}
|
||||||
|
|
||||||
|
return await storePlugin(metadata, directory, source)
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
import {
|
||||||
|
getPluginMetadata,
|
||||||
|
findFileRec,
|
||||||
|
extractTarball,
|
||||||
|
deleteFolderFileSystem,
|
||||||
|
} from "../../../utilities/fileSystem"
|
||||||
|
import fetch from "node-fetch"
|
||||||
|
import { join } from "path"
|
||||||
|
import { downloadUnzipTarball } from "./utils"
|
||||||
|
|
||||||
|
export async function npmUpload(url: string, name: string, headers = {}) {
|
||||||
|
let npmTarballUrl = url
|
||||||
|
let pluginName = name
|
||||||
|
|
||||||
|
if (
|
||||||
|
!npmTarballUrl.includes("https://www.npmjs.com") &&
|
||||||
|
!npmTarballUrl.includes("https://registry.npmjs.org")
|
||||||
|
) {
|
||||||
|
throw new Error("The plugin origin must be from NPM")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!npmTarballUrl.includes(".tgz")) {
|
||||||
|
const npmPackageURl = url.replace(
|
||||||
|
"https://www.npmjs.com/package/",
|
||||||
|
"https://registry.npmjs.org/"
|
||||||
|
)
|
||||||
|
const response = await fetch(npmPackageURl)
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error("NPM Package not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
let npmDetails = await response.json()
|
||||||
|
pluginName = npmDetails.name
|
||||||
|
const npmVersion = npmDetails["dist-tags"].latest
|
||||||
|
npmTarballUrl = npmDetails?.versions?.[npmVersion]?.dist?.tarball
|
||||||
|
|
||||||
|
if (!npmTarballUrl) {
|
||||||
|
throw new Error("NPM tarball url not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = await downloadUnzipTarball(npmTarballUrl, pluginName, headers)
|
||||||
|
const tarballPluginFile = findFileRec(path, ".tar.gz")
|
||||||
|
if (!tarballPluginFile) {
|
||||||
|
throw new Error("Tarball plugin file not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await extractTarball(tarballPluginFile, path)
|
||||||
|
deleteFolderFileSystem(join(path, "package"))
|
||||||
|
} catch (err: any) {
|
||||||
|
throw new Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getPluginMetadata(path)
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export { fileUpload } from "./file"
|
||||||
|
export { githubUpload } from "./github"
|
||||||
|
export { npmUpload } from "./npm"
|
||||||
|
export { urlUpload } from "./url"
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { downloadUnzipTarball } from "./utils"
|
||||||
|
import { getPluginMetadata } from "../../../utilities/fileSystem"
|
||||||
|
|
||||||
|
export async function urlUpload(url: string, name = "", headers = {}) {
|
||||||
|
if (!url.includes(".tar.gz")) {
|
||||||
|
throw new Error("Plugin must be compressed into a gzipped tarball.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = await downloadUnzipTarball(url, name, headers)
|
||||||
|
|
||||||
|
return await getPluginMetadata(path)
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import {
|
||||||
|
createTempFolder,
|
||||||
|
downloadTarballDirect,
|
||||||
|
} from "../../../utilities/fileSystem"
|
||||||
|
|
||||||
|
export async function downloadUnzipTarball(
|
||||||
|
url: string,
|
||||||
|
name: string,
|
||||||
|
headers = {}
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const path = createTempFolder(name)
|
||||||
|
await downloadTarballDirect(url, path, headers)
|
||||||
|
|
||||||
|
return path
|
||||||
|
} catch (e: any) {
|
||||||
|
throw new Error(e.message)
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue