Merge branch 'master' of github.com:Budibase/budibase into form-builder

This commit is contained in:
Andrew Kingston 2021-01-20 11:14:36 +00:00
commit 47dabc16ce
127 changed files with 8781 additions and 6960 deletions

3
.gitignore vendored
View File

@ -81,3 +81,6 @@ typings/
# Mac files
.DS_Store
# Nova Editor
.nova

View File

@ -8,10 +8,10 @@
</h1>
<h3 align="center">
Build business apps 50x faster
Build internal tools 50x faster
</h3>
<p align="center">
Budibase is an open-source low-code platform that helps developers and IT professionals design, build, and ship business apps 50x faster.
Budibase is an open-source low-code platform that helps developers and IT professionals build, automate and ship internal tools 50x faster.
</p>
<h3 align="center">
@ -67,18 +67,26 @@ When other platforms chose the closed source route, we decided to go open source
- **Automate processes, integrate with other tools, and connect to webhooks.** Save time by automating manual processes and workflows. From connecting to webhooks, to automating emails, simply tell Budibase what to do and let it work for you. You can easily [create new automations for Budibase here](https://github.com/Budibase/automations) or [request new integrations here](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
- **Cloud hosting (available) and self-hosting (coming soon).** Users will soon have the option to host with Budibase in AWS (available now) or self-host (coming very soon). From the very beginning, we wanted our users to have the option to self-host. We understand the importance of having full control over data. This is why we are working incredibly hard to offer an easy path to self-hosting. If you are interested in self-hosting, please [join the conversation and add your thoughts](https://github.com/Budibase/budibase/discussions/648).
- **Cloud hosting and self-hosting.** Users can self-host (see below), or host their apps with Budibase. Currently, our cloud hosting offering is limited to the free tier but we aim to change this in the future. For heavy usage, we advise users to self-host.
## 🤖 Self-hosting
<p align="center">
<img src="https://i.imgur.com/Z52cEvT.png?1" />
</p>
Budibase wants to make sure anyone can use the tools we develop and we know a lot of people need to be able to host the apps they make on their own systems - that is why we've decided to try and make self hosting as easy as possible!
Currently, you can host your apps using Docker. The documentation for self-hosting can be found [here](https://docs.budibase.com/self-hosting/introduction-to-self-hosting).
## ⌛ Status
- [x] Alpha: We are demoing Budibase to users and receiving feedback
- [x] Private Beta: We are testing Budibase with a closed set of customers
- [x] Public Beta: Anyone can [sign-up and use Budibase](https://portal.budi.live/signup) but it's not production ready. We cannot ensure backwards compatibility
- [x] Public Beta: Anyone can [sign-up and use Budibase](https://portal.budi.live/signup).
- [ ] Official Launch: Production-ready
We are currently in Public Beta. Until our official launch, we cannot ensure backwards compatibility for your Budibase applications between versions. Issues may arise when trying to edit apps created with old versions of the Budibase builder.
Watch "releases" of this repo to get notified of major updates, and give the star button a click whilst you're there.
<p align="center">

View File

@ -7,7 +7,10 @@ services:
- "${APP_PORT}:4002"
environment:
SELF_HOSTED: 1
CLOUD: 1
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
WORKER_URL: http://worker-service:4003
HOSTING_KEY: ${HOSTING_KEY}
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
PORT: 4002
JWT_SECRET: ${JWT_SECRET}
@ -20,14 +23,13 @@ services:
- "${WORKER_PORT}:4003"
environment:
SELF_HOSTED: 1,
DEPLOYMENT_API_KEY: ${WORKER_API_KEY}
PORT: 4003
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
RAW_MINIO_URL: http://minio-service:9000
COUCH_DB_USERNAME: ${COUCH_DB_USER}
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
RAW_COUCH_DB_URL: http://couchdb-service:5984
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
SELF_HOST_KEY: ${HOSTING_KEY}
depends_on:
- minio-service

View File

@ -12,7 +12,6 @@ MINIO_ACCESS_KEY=budibase
MINIO_SECRET_KEY=budibase
COUCH_DB_PASSWORD=budibase
COUCH_DB_USER=budibase
WORKER_API_KEY=budibase
# This section contains variables that do not need to be altered under normal circumstances
APP_PORT=4002

146
package-lock.json generated
View File

@ -615,6 +615,17 @@
"ssri": "^6.0.1",
"unique-filename": "^1.1.1",
"y18n": "^4.0.0"
},
"dependencies": {
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
}
}
},
"make-fetch-happen": {
@ -1004,6 +1015,17 @@
"ssri": "^6.0.1",
"unique-filename": "^1.1.1",
"y18n": "^4.0.0"
},
"dependencies": {
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
}
}
},
"make-fetch-happen": {
@ -1087,6 +1109,17 @@
"npmlog": "^4.1.2",
"path-exists": "^3.0.0",
"rimraf": "^2.6.2"
},
"dependencies": {
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
}
}
},
"@lerna/run": {
@ -1900,6 +1933,17 @@
"ssri": "^6.0.1",
"unique-filename": "^1.1.1",
"y18n": "^4.0.0"
},
"dependencies": {
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
}
}
},
"cache-base": {
@ -2494,6 +2538,17 @@
"mkdirp": "^0.5.1",
"rimraf": "^2.5.4",
"run-queue": "^1.0.0"
},
"dependencies": {
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
}
}
},
"copy-descriptor": {
@ -3394,6 +3449,17 @@
"flatted": "^2.0.0",
"rimraf": "2.6.3",
"write": "1.0.3"
},
"dependencies": {
"rimraf": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
}
}
},
"flatted": {
@ -3767,6 +3833,12 @@
"pump": "^3.0.0"
}
},
"get-them-args": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/get-them-args/-/get-them-args-1.3.2.tgz",
"integrity": "sha1-dKILqKSr7OWuGZrQPyvMaP38m6U=",
"dev": true
},
"get-value": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
@ -4354,9 +4426,9 @@
"dev": true
},
"ini": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true
},
"init-package-json": {
@ -4829,6 +4901,16 @@
"verror": "1.10.0"
}
},
"kill-port": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/kill-port/-/kill-port-1.6.1.tgz",
"integrity": "sha512-un0Y55cOM7JKGaLnGja28T38tDDop0AQ8N0KlAdyh+B1nmMoX8AnNmqPNZbS3mUMgiST51DCVqmbFT1gNJpVNw==",
"dev": true,
"requires": {
"get-them-args": "1.3.2",
"shell-exec": "1.0.2"
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@ -5370,6 +5452,17 @@
"mkdirp": "^0.5.1",
"rimraf": "^2.5.4",
"run-queue": "^1.0.3"
},
"dependencies": {
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
}
}
},
"ms": {
@ -5469,6 +5562,15 @@
"which": "1"
},
"dependencies": {
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
},
"semver": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
@ -5966,6 +6068,15 @@
"which": "^1.3.1"
},
"dependencies": {
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@ -6192,13 +6303,10 @@
}
},
"prettier-plugin-svelte": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-0.7.0.tgz",
"integrity": "sha512-SuZSeMh48rx42kCFEpI/xE1XgjxQcS3r22Yo7jIhBYRhwbAa8laNxiIHsfeWWkX8BdyELkEayaTQp4ricckwTQ==",
"dev": true,
"requires": {
"tslib": "^1.9.3"
}
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-1.4.2.tgz",
"integrity": "sha512-O9VsNwII+raTG8QPoQWouk5ABQy/hmLm4dZ2eqJ7DPnbO35A+BxMSjlfqkw0cNP+UcbykHFYU8zNXm93ytWP9g==",
"dev": true
},
"process-nextick-args": {
"version": "2.0.1",
@ -6603,9 +6711,9 @@
"dev": true
},
"rimraf": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"requires": {
"glob": "^7.1.3"
@ -6733,6 +6841,12 @@
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
"dev": true
},
"shell-exec": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/shell-exec/-/shell-exec-1.0.2.tgz",
"integrity": "sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg==",
"dev": true
},
"signal-exit": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
@ -7213,9 +7327,9 @@
}
},
"svelte": {
"version": "3.29.0",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.29.0.tgz",
"integrity": "sha512-f+A65eyOQ5ujETLy+igNXtlr6AEjAQLYd1yJE1VwNiXMQO5Z/Vmiy3rL+zblV/9jd7rtTTWqO1IcuXsP2Qv0OA==",
"version": "3.31.2",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.31.2.tgz",
"integrity": "sha512-TxZGrXzX2ggFH3BIKY5fmbeMdJuZrMIMDYPMX6R9255bueuYIuVaBQSLUeY2oD7W4IdeqRZiAVGCjDw2POKBRA==",
"dev": true
},
"table": {

View File

@ -34,8 +34,5 @@
"test:e2e": "lerna run cy:test",
"test:e2e:ci": "lerna run cy:ci",
"build:docker": "cd hosting/scripts/linux/ && ./release-to-docker-hub.sh && cd -"
},
"dependencies": {
"@fortawesome/fontawesome": "^1.1.8"
}
}

View File

@ -63,14 +63,14 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.52.4",
"@budibase/bbui": "^1.54.0",
"@budibase/client": "^0.5.3",
"@budibase/colorpicker": "^1.0.1",
"@budibase/svelte-ag-grid": "^0.0.16",
"@fortawesome/fontawesome-free": "^5.14.0",
"@sentry/browser": "5.19.1",
"@svelteschool/svelte-forms": "^0.7.0",
"britecharts": "^2.16.0",
"codemirror": "^5.59.0",
"d3-selection": "^1.4.1",
"deepmerge": "^4.2.2",
"fast-sort": "^2.2.0",

View File

@ -10,9 +10,10 @@ const CAPTURE_VAR_INSIDE_MUSTACHE = /{{([^}]+)}}/g
* Gets all bindable data context fields and instance fields.
*/
export const getBindableProperties = (rootComponent, componentId) => {
const bindableContexts = getBindableContexts(rootComponent, componentId)
const bindableComponents = getBindableComponents(rootComponent)
return [...bindableContexts, ...bindableComponents]
const contextBindings = getContextBindings(rootComponent, componentId)
const queryBindings = getQueryBindings(rootComponent, componentId)
const componentBindings = getComponentBindings(rootComponent)
return [...contextBindings, ...queryBindings, componentBindings]
}
/**
@ -71,11 +72,10 @@ export const getDatasourceForProvider = component => {
* Gets all bindable data contexts. These are fields of schemas of data contexts
* provided by data provider components, such as lists or row detail components.
*/
export const getBindableContexts = (rootComponent, componentId) => {
const dataProviders = getDataProviderComponents(rootComponent, componentId)
export const getContextBindings = (rootComponent, componentId) => {
// Extract any components which provide data contexts
let contexts = []
const dataProviders = getDataProviderComponents(rootComponent, componentId)
let contextBindings = []
dataProviders.forEach(component => {
const datasource = getDatasourceForProvider(component)
if (!datasource) {
@ -99,7 +99,7 @@ export const getBindableContexts = (rootComponent, componentId) => {
runtimeBoundKey = `${key}_first`
}
contexts.push({
contextBindings.push({
type: "context",
runtimeBinding: `${component._id}.${runtimeBoundKey}`,
readableBinding: `${component._instanceName}.${table.name}.${key}`,
@ -110,14 +110,14 @@ export const getBindableContexts = (rootComponent, componentId) => {
})
})
})
return contexts
return contextBindings
}
/**
* Gets all bindable components. These are form components which allow their
* values to be bound to.
*/
export const getBindableComponents = rootComponent => {
export const getComponentBindings = rootComponent => {
if (!rootComponent) {
return []
}
@ -137,6 +137,47 @@ export const getBindableComponents = rootComponent => {
})
}
/**
* Gets all bindable query fields. These are fields of schemas of data contexts
* provided by data provider components, such as lists or row detail components.
*/
export const getQueryBindings = (rootComponent, componentId) => {
// Extract any components which provide data contexts
const dataProviders = getDataProviderComponents(rootComponent, componentId)
const queries = get(backendUiStore).queries
let queryBindings = []
dataProviders.forEach(component => {
const datasource = getDatasourceForProvider(component)
if (!datasource) {
return
}
// Find a query for this table ID
const queryId = datasource.tableId
const query = queries.find(query => query._id === queryId)
const schema = query?.schema
if (!schema) {
return
}
// Add all schema fields as bindable values
const keys = Object.keys(schema).sort()
keys.forEach(key => {
const fieldSchema = schema[key]
queryBindings.push({
type: "context",
fieldSchema,
runtimeBinding: `${component._id}.${key}`,
readableBinding: `${component._instanceName}.${query.name}.${key}`,
providerId: component._id,
tableId: datasource.tableId,
field: key,
})
})
})
return queryBindings
}
/**
* Gets a schema for a datasource object.
*/

View File

@ -7,6 +7,9 @@ const INITIAL_BACKEND_UI_STATE = {
views: [],
users: [],
roles: [],
datasources: [],
queries: [],
integrations: {},
selectedDatabase: {},
selectedTable: {},
draftTable: {},
@ -21,9 +24,19 @@ export const getBackendUiStore = () => {
select: async db => {
const tablesResponse = await api.get(`/api/tables`)
const tables = await tablesResponse.json()
const datasourcesResponse = await api.get(`/api/datasources`)
const datasources = await datasourcesResponse.json()
const queriesResponse = await api.get(`/api/queries`)
const queries = await queriesResponse.json()
const integrationsResponse = await api.get("/api/integrations")
const integrations = await integrationsResponse.json()
store.update(state => {
state.selectedDatabase = db
state.tables = tables
state.datasources = datasources
state.queries = queries
state.integrations = integrations
return state
})
},
@ -45,6 +58,107 @@ export const getBackendUiStore = () => {
return state
}),
},
datasources: {
fetch: async () => {
const response = await api.get(`/api/datasources`)
const json = await response.json()
store.update(state => {
state.datasources = json
return state
})
return json
},
select: async datasourceId => {
store.update(state => {
state.selectedDatasourceId = datasourceId
state.selectedQueryId = null
return state
})
},
save: async datasource => {
const response = await api.post("/api/datasources", datasource)
const json = await response.json()
store.update(state => {
const currentIdx = state.datasources.findIndex(
ds => ds._id === json._id
)
if (currentIdx >= 0) {
state.datasources.splice(currentIdx, 1, json)
} else {
state.datasources.push(json)
}
state.datasources = state.datasources
state.selectedDatasourceId = json._id
return state
})
return json
},
delete: async datasource => {
await api.delete(
`/api/datasources/${datasource._id}/${datasource._rev}`
)
store.update(state => {
state.datasources = state.datasources.filter(
existing => existing._id !== datasource._id
)
state.selectedDatasourceId = null
return state
})
},
},
queries: {
fetch: async () => {
const response = await api.get(`/api/queries`)
const json = await response.json()
store.update(state => {
state.queries = json
return state
})
return json
},
save: async (datasourceId, query) => {
query.datasourceId = datasourceId
const response = await api.post(`/api/queries`, query)
const json = await response.json()
store.update(state => {
const currentIdx = state.queries.findIndex(
query => query._id === json._id
)
if (currentIdx >= 0) {
state.queries.splice(currentIdx, 1, json)
} else {
state.queries.push(json)
}
state.queries = state.queries
state.selectedQueryId = json._id
return state
})
return json
},
select: query =>
store.update(state => {
state.selectedDatasourceId = query.datasourceId
state.selectedQueryId = query._id
return state
}),
delete: async query => {
await api.delete(`/api/queries/${query._id}/${query._rev}`)
store.update(state => {
state.queries = state.queries.filter(
existing => existing._id !== query._id
)
if (state.selectedQueryId === query._id) {
state.selectedQueryId = null
}
return state
})
},
},
tables: {
fetch: async () => {
const tablesResponse = await api.get(`/api/tables`)

View File

@ -19,6 +19,7 @@ import { uuid } from "../uuid"
const INITIAL_FRONTEND_STATE = {
apps: [],
name: "",
url: "",
description: "",
layouts: [],
screens: [],
@ -48,6 +49,7 @@ export const getFrontendStore = () => {
name: application.name,
description: application.description,
appId: application._id,
url: application.url,
layouts,
screens,
hasAppPackage: true,

View File

@ -1,13 +1,16 @@
import { writable } from "svelte/store"
import api from "../api"
import api, { get } from "../api"
const INITIAL_BACKEND_UI_STATE = {
const INITIAL_HOSTING_UI_STATE = {
hostingInfo: {},
appUrl: "",
deployedApps: {},
deployedAppNames: [],
deployedAppUrls: [],
}
export const getHostingStore = () => {
const store = writable({ ...INITIAL_BACKEND_UI_STATE })
const store = writable({ ...INITIAL_HOSTING_UI_STATE })
store.actions = {
fetch: async () => {
const responses = await Promise.all([
@ -33,6 +36,16 @@ export const getHostingStore = () => {
return state
})
},
fetchDeployedApps: async () => {
let deployments = await (await get("/api/hosting/apps")).json()
store.update(state => {
state.deployedApps = deployments
state.deployedAppNames = Object.values(deployments).map(app => app.name)
state.deployedAppUrls = Object.values(deployments).map(app => app.url)
return state
})
return deployments
},
}
return store
}

View File

@ -50,7 +50,10 @@
function addBlockToAutomation(stepId, blockDefinition) {
const newBlock = $automationStore.selectedAutomation.constructBlock(
selectedTab, stepId, blockDefinition)
selectedTab,
stepId,
blockDefinition
)
automationStore.actions.addBlockToAutomation(newBlock)
closePopover()
if (stepId === "WEBHOOK") {

View File

@ -1,9 +1,13 @@
<script>
import { createEventDispatcher } from "svelte"
import GenericBindingPopover from "./GenericBindingPopover.svelte"
import { Input, Icon } from "@budibase/bbui"
const dispatch = createEventDispatcher()
export let bindings = []
export let value
let anchor
let popover = undefined
let enrichedValue
@ -14,6 +18,7 @@
let { bindings, ...otherProps } = $$props
inputProps = otherProps
}
$: value && dispatch("change", value)
</script>
<div class="container" bind:this={anchor}>

View File

@ -50,7 +50,9 @@
<span class="binding__label">{binding.label}</span>
<span class="binding__type">{binding.type}</span>
<br />
<div class="binding__description">{binding.description}</div>
<div class="binding__description">
{binding.description || ''}
</div>
</div>
{/each}
{/each}

View File

@ -36,9 +36,7 @@
</script>
<div class="root">
<div class="add-field">
<i class="ri-add-line" on:click={addField} />
</div>
<div class="add-field"><i class="ri-add-line" on:click={addField} /></div>
<div class="spacer" />
{#each fieldsArray as field}
<div class="field">
@ -57,13 +55,11 @@
<option>datetime</option>
</Select>
<i class="remove-field ri-delete-bin-line"
<i
class="remove-field ri-delete-bin-line"
on:click={() => removeField(field.name)} />
</div>
{/each}
</div>
<style>
@ -105,7 +101,8 @@
}
.field :global(input) {
padding: var(--spacing-m) var(--spacing-xl) var(--spacing-xs) var(--spacing-m);
padding: var(--spacing-m) var(--spacing-xl) var(--spacing-xs)
var(--spacing-m);
font-size: var(--font-size-s);
font-weight: bold;
}
@ -129,5 +126,4 @@
.add-field > i {
cursor: pointer;
}
</style>

View File

@ -0,0 +1,28 @@
<script>
import { params } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import * as api from "./api"
import Table from "./Table.svelte"
export let query = {}
export let data = []
let loading = false
let error = false
</script>
{#if error}
<div class="errors">{error}</div>
{/if}
<Table title={''} schema={query.schema} {data} {loading} />
<style>
.errors {
color: var(--red);
background: var(--red-light);
padding: var(--spacing-m);
border-radius: var(--border-radius-m);
margin-bottom: var(--spacing-m);
}
</style>

View File

@ -199,7 +199,6 @@
align-items: stretch;
}
.grid-wrapper :global(> *) {
height: auto;
flex: 1 1 auto;
}
:global(.grid-wrapper) {
@ -236,6 +235,7 @@
}
:global(.ag-filter) {
background: var(--background);
padding: var(--spacing-s);
outline: none;
box-sizing: border-box;

View File

@ -23,5 +23,10 @@ export async function fetchDataForView(view) {
const FETCH_ROWS_URL = `/api/views/${view.name}`
const response = await api.get(FETCH_ROWS_URL)
return await response.json()
const json = await response.json()
if (response.status !== 200) {
throw new Error(json.message)
}
return json
}

View File

@ -0,0 +1,63 @@
<script>
import { onMount } from "svelte"
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import CreateDatasourceModal from "./modals/CreateDatasourceModal.svelte"
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte"
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
import { Modal, Switcher } from "@budibase/bbui"
import NavItem from "components/common/NavItem.svelte"
import ICONS from "./icons"
$: selectedView =
$backendUiStore.selectedView && $backendUiStore.selectedView.name
function selectDatasource(datasource) {
backendUiStore.actions.datasources.select(datasource._id)
$goto(`./datasource/${datasource._id}`)
}
function onClickQuery(query) {
if ($backendUiStore.selectedQueryId === query._id) {
return
}
backendUiStore.actions.queries.select(query)
$goto(`./datasource/${query.datasourceId}/${query._id}`)
}
onMount(() => {
backendUiStore.actions.datasources.fetch()
backendUiStore.actions.queries.fetch()
})
</script>
{#if $backendUiStore.selectedDatabase && $backendUiStore.selectedDatabase._id}
<div class="hierarchy-items-container">
{#each $backendUiStore.datasources as datasource, idx}
<NavItem
border={idx > 0}
text={datasource.name}
selected={$backendUiStore.selectedDatasourceId === datasource._id}
on:click={() => selectDatasource(datasource)}>
<div class="datasource-icon" slot="icon">
<svelte:component
this={ICONS[datasource.source]}
height="15"
width="15" />
</div>
<EditDatasourcePopover {datasource} />
</NavItem>
{#each $backendUiStore.queries.filter(query => query.datasourceId === datasource._id) as query}
<NavItem
indentLevel={1}
icon="ri-eye-line"
text={query.name}
selected={$backendUiStore.selectedQueryId === query._id}
on:click={() => onClickQuery(query)}>
<EditQueryPopover {query} />
</NavItem>
{/each}
{/each}
</div>
{/if}

View File

@ -0,0 +1,55 @@
<script>
export let icon
export let className
export let title
export let selected
export let indented
</script>
<div
data-cy="table-nav-item"
class:indented
class:selected
on:click
class={className}>
<i class={icon} />
<span>{title}</span>
<slot />
</div>
<style>
.indented {
grid-template-columns: 46px 1fr 20px;
}
.indented i {
justify-self: end;
}
div {
padding: var(--spacing-s) var(--spacing-m);
border-radius: var(--border-radius-m);
display: grid;
grid-template-columns: 20px 1fr 20px;
align-items: center;
transition: 0.3s background-color;
color: var(--ink);
font-weight: 400;
font-size: 14px;
margin-bottom: var(--spacing-xs);
grid-gap: var(--spacing-s);
}
.selected {
background-color: var(--grey-2);
}
div:hover {
background-color: var(--grey-1);
cursor: pointer;
}
i {
color: var(--grey-7);
font-size: 20px;
}
</style>

View File

@ -0,0 +1,16 @@
<script>
import { Input, TextArea, Spacer } from "@budibase/bbui"
export let integration
</script>
<form>
{#each Object.keys(integration) as configKey}
<Input
thin
type={integration[configKey].type}
label={configKey}
bind:value={integration[configKey]} />
<Spacer medium />
{/each}
</form>

View File

@ -0,0 +1,97 @@
<script>
import { onMount } from "svelte"
import { backendUiStore } from "builderStore"
import api from "builderStore/api"
import { Input, TextArea, Spacer } from "@budibase/bbui"
import ICONS from "../icons"
export let integration = {}
let schema
let integrations = []
async function fetchIntegrations() {
const response = await api.get("/api/integrations")
const json = await response.json()
integrations = json
return json
}
function selectIntegration(integrationType) {
schema = integrations[integrationType].datasource
integration = {
type: integrationType,
...Object.keys(schema).reduce(
(acc, next) => ({ ...acc, [next]: schema[next].default }),
{}
),
}
}
onMount(() => {
fetchIntegrations()
})
</script>
<section>
<div class="integration-list">
{#each Object.keys(integrations) as integrationType}
<div
class="integration hoverable"
class:selected={integration.type === integrationType}
on:click={() => selectIntegration(integrationType)}>
<svelte:component
this={ICONS[integrationType]}
height="100"
width="100" />
<span>{integrationType}</span>
</div>
{/each}
</div>
{#if schema}
{#each Object.keys(schema) as configKey}
<Input
thin
type={schema[configKey].type}
label={configKey}
bind:value={integration[configKey]} />
<Spacer medium />
{/each}
{/if}
</section>
<style>
section {
display: grid;
}
.integration-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: var(--spacing-m);
}
.integration {
display: flex;
align-items: center;
flex-direction: column;
padding: 5px;
transition: 0.3s all;
border-radius: var(--border-radius-s);
height: 75px;
width: 200px;
}
span {
font-size: var(--font-size-xs);
margin-top: var(--spacing-m);
margin-bottom: var(--spacing-xs);
}
.integration:hover,
.selected {
background-color: var(--grey-3);
}
</style>

View File

@ -0,0 +1,46 @@
<script>
export let width = "100"
export let height = "100"
</script>
<svg
{width}
{height}
viewBox="0 0 256 215"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path
d="M114.259 2.701L18.86 42.176C13.555 44.371 13.61 51.906 18.949
54.023L114.746 92.012C123.163 95.3503 132.537 95.3503 140.954
92.012L236.753 54.022C242.09 51.907 242.146 44.372 236.839 42.176L141.442
2.7C132.739 -0.901099 122.962 -0.901099 114.259 2.7"
fill="#FFBF00" />
<path
d="M136.35 112.757V207.659C136.35 212.173 140.9 215.264 145.096
213.601L251.844 172.166C253.034 171.694 254.056 170.875 254.775
169.816C255.495 168.757 255.879 167.506 255.879 166.225V71.322C255.879
66.808 251.328 63.718 247.132 65.381L140.384 106.815C139.194 107.287
138.172 108.106 137.453 109.166C136.734 110.225 136.349 111.476 136.349
112.757"
fill="#26B5F8" />
<path
d="M111.423 117.654L79.743 132.95L76.526 134.505L9.65 166.548C5.411
168.593 0 165.504 0 160.795V71.72C0 70.016 0.874 68.545 2.046
67.437C2.52616 66.9592 3.07076 66.5509 3.664 66.224C5.262 65.265 7.542
65.009 9.48 65.776L110.89 105.956C116.045 108.001 116.45 115.224 111.423
117.653"
fill="#ED3049" />
<path
d="M111.423 117.654L79.743 132.95L2.045 67.438C2.52516 66.9602 3.06976
66.5519 3.663 66.225C5.261 65.266 7.541 65.01 9.479 65.777L110.889
105.957C116.044 108.002 116.449 115.225 111.422 117.654"
fill="black"
fill-opacity="0.25" />
</g>
<defs>
<clipPath id="clip0">
<rect width="256" height="215" fill="white" />
</clipPath>
</defs>
</svg>

View File

@ -0,0 +1,35 @@
<script>
export let width = "100"
export let height = "100"
</script>
<svg
{width}
{height}
viewBox="0 0 175 115"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M142.187 76.6667C142.187 83.9288 138.367 87.4876 131.249
87.6136V87.6184H43.7507V87.6136C36.6331 87.4876 32.8132 83.9288 32.8132
76.666C32.8132 69.4052 36.6331 65.845 43.7507
65.7204V65.7136H131.249V65.7204C138.367 65.845 142.187 69.4052 142.187
76.666V76.6667ZM131.249 93.1014V93.0952H43.7507V93.1021C36.6331 93.2267
32.8132 96.7869 32.8132 104.05C32.8132 111.312 36.6331 114.869 43.7507
114.995V115H131.249V114.993C138.367 114.869 142.187 111.31 142.187
104.048C142.187 96.7848 138.367 93.2267 131.249 93.1021V93.1014ZM158.593
32.8695V32.8633C151.477 32.9893 147.656 36.5481 147.656
43.8109V104.048C147.656 111.31 151.477 114.867 158.593
114.993V114.982C169.269 114.606 175 103.929 175 82.1429V54.7619C175 40.2384
169.269 33.1214 158.594 32.8688L158.593 32.8695ZM16.4062
32.8626V32.8695C5.73193 33.1207 0 40.2377 0 54.7619V82.1429C0 103.929
5.73125 114.605 16.4062 114.982V114.993C23.5238 114.869 27.3438 111.31
27.3438 104.048V43.8095C27.3438 36.5474 23.5238 32.9886 16.4062
32.8626ZM158.594 27.381C158.594 9.22533 149.042 0.328571 131.25
0.014375V0H43.75V0.014375C25.9595 0.329256 16.4062 9.22533 16.4062
27.3816V27.3905C27.0819 27.5788 32.8132 32.9167 32.8132 43.8102C32.8132
54.703 38.5444 60.0403 49.2194 60.2292V60.2381H125.782V60.2299C136.456
60.0416 142.187 54.7037 142.187 43.8109C142.187 32.9174 147.918 27.5795
158.593 27.3912V27.381H158.594Z"
fill="#E42528" />
</svg>

View File

@ -0,0 +1,58 @@
<script>
export let width = "100"
export let height = "100"
</script>
<svg
{width}
{height}
viewBox="0 0 256 289"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path
d="M165.258 288.501H168.766L226.027 259.867L226.98 258.52V29.964L226.027
28.61L168.766 0H165.215L165.258 288.501Z"
fill="#5294CF" />
<path
d="M90.741 288.501H87.184L29.972 259.867L28.811 257.87L28.222
31.128L29.972 28.61L87.184 0H90.785L90.741 288.501Z"
fill="#1F5B98" />
<path d="M87.285 0H168.711V288.501H87.285V0Z" fill="#2D72B8" />
<path
d="M256 137.769L254.065 137.34L226.437 134.764L226.027 134.968L168.715
132.676H87.285L29.972 134.968V91.264L29.912 91.296L29.972 91.168L87.285
77.888H168.715L226.027 91.168L247.096 102.367V95.167L256 94.193L255.078
92.395L226.886 72.236L226.027 72.515L168.715 54.756H87.285L29.972
72.515V28.61L0 63.723V94.389L0.232 94.221L8.904 95.167V102.515L0
107.28V137.793L0.232 137.769L8.904 137.897V150.704L1.422 150.816L0
150.68V181.205L8.904 185.993V193.426L0.373 194.368L0
194.088V224.749L29.972 259.867V215.966L87.285 233.725H168.715L226.196
215.914L226.96 216.249L254.781 196.387L256 194.408L247.096
193.426V186.142L245.929 185.676L226.886 195.941L226.196 197.381L168.715
210.584V210.6H87.285V210.584L29.972 197.325V153.461L87.285
155.745V155.801H168.715L226.027 153.461L227.332 154.061L254.111
151.755L256 150.832L247.096 150.704V137.897L256 137.769"
fill="#1A476F" />
<path
d="M226.027 215.966V259.867L256 224.749V194.288L226.2 215.914L226.027
215.966Z"
fill="#2D72B8" />
<path
d="M226.027 197.421L226.2 197.381L256 181.353V150.704L226.027
153.461V197.421"
fill="#2D72B8" />
<path
d="M226.2 91.208L226.027 91.168V134.968L256 137.769V107.135L226.2 91.208Z"
fill="#2D72B8" />
<path
d="M226.2 72.687L256 94.193V63.731L226.027 28.61V72.515L226.2
72.575V72.687Z"
fill="#2D72B8" />
</g>
<defs>
<clipPath id="clip0">
<rect width="256" height="289" fill="white" />
</clipPath>
</defs>
</svg>

View File

@ -0,0 +1,47 @@
<script>
export let width = "100"
export let height = "100"
</script>
<svg
{width}
{height}
viewBox="0 0 128 143"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path
d="M7.172 40.0865H101.775C112.201 40.0865 121.51 35.3065 127.757
27.843C114.727 10.918 94.2765 0 71.265 0C43.089 0 18.738 16.3555 7.172
40.0865Z"
fill="#F0BF1A" />
<path
d="M93.7575 51.222H2.8775C0.969102 57.732 0.000154178 64.4811 0 71.265C0
78.227 1.0165 84.9485 2.8775 91.3085H93.7575C104.67 91.3085 113.801
82.4005 113.801 71.265C113.801 60.13 104.893 51.222 93.7575 51.222"
fill="#07A5DE" />
<path
d="M128 114.378C121.793 107.082 112.559 102.443 102.22
102.443H7.173C18.7385 126.175 43.0895 142.53 71.265 142.53C94.418 142.53
114.983 131.482 128 114.378Z"
fill="#3EBEB0" />
<path
d="M2.87749 51.222C-0.959651 64.3087 -0.959651 78.2218 2.87749
91.3085H62.357C63.916 85.2955 64.8065 78.6145 64.8065 71.265C64.8065
63.916 63.916 57.235 62.3565 51.222H2.87849H2.87749Z"
fill="#231F20" />
<path
d="M35.41 9.5765C23.3835 16.703 13.362 27.3925 7.1265
40.0865H59.6845C54.3395 27.838 45.877 17.594 35.4095 9.5765"
fill="#D7A229" />
<path
d="M37.637 134.068C47.881 125.828 56.121 114.915 61.2435
102.443H7.1265C13.8075 115.806 24.4975 126.941 37.637 134.068Z"
fill="#019B8F" />
</g>
<defs>
<clipPath id="clip0">
<rect width="128" height="143" fill="white" />
</clipPath>
</defs>
</svg>

View File

@ -0,0 +1,77 @@
<script>
export let width = "100"
export let height = "100"
</script>
<svg
{width}
{height}
viewBox="0 0 179 179"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M153.828 30.7656H101.806L90.0594
54.2594H24.6125V148.234H165.575V30.7656H153.828ZM153.828
54.2594H107.959L114.112 42.5125H153.828V54.2594Z"
fill="#458248" />
<path
d="M149.773 173.406L146.623 172.36C146.623 172.36 147.026 156.412 141.276
155.299C137.483 150.903 141.835 -32.4046 155.635 154.673C153.015 156 151.011
158.289 150.041 161.061C149.521 165.155 149.431 169.293 149.773
173.406V173.406Z"
fill="url(#paint0_linear)" />
<path
d="M151.473 158.007C159.86 151.69 166.283 143.124 169.994 133.302C173.705
123.48 174.553 112.807 172.439 102.522C166.285 75.3142 151.68 66.3698
150.108 62.9577C148.75 60.8316 147.591 58.5845 146.646 56.2452L147.809
132.052C147.809 132.052 145.398 155.215 151.473 158.007Z"
fill="url(#paint1_linear)" />
<path
d="M145.018 159.014C145.018 159.014 119.337 141.522 120.825 110.583C120.964
101.255 123.102 92.0662 127.096 83.636C131.091 75.2058 136.847 67.7308
143.978 61.7158C144.809 61.0059 145.469 60.1177 145.91 59.1173C146.351
58.1169 146.56 57.03 146.523 55.9375C148.123 59.3777 147.86 107.299 148.027
112.904C148.682 134.709 146.819 154.897 145.018 159.014V159.014Z"
fill="url(#paint2_linear)" />
<defs>
<linearGradient
id="paint0_linear"
x1="128.589"
y1="115.729"
x2="164.805"
y2="128.016"
gradientUnits="userSpaceOnUse">
<stop offset="0.231" stop-color="#999875" />
<stop offset="0.563" stop-color="#9B9977" />
<stop offset="0.683" stop-color="#A09F7E" />
<stop offset="0.768" stop-color="#A9A889" />
<stop offset="0.837" stop-color="#B7B69A" />
<stop offset="0.896" stop-color="#C9C7B0" />
<stop offset="0.948" stop-color="#DEDDCB" />
<stop offset="0.994" stop-color="#F8F6EB" />
<stop offset="1" stop-color="#FBF9EF" />
</linearGradient>
<linearGradient
id="paint1_linear"
x1="141.872"
y1="55.7038"
x2="157.883"
y2="155.673"
gradientUnits="userSpaceOnUse">
<stop stop-color="#48A547" />
<stop offset="1" stop-color="#3F9143" />
</linearGradient>
<linearGradient
id="paint2_linear"
x1="118.614"
y1="113.645"
x2="161.577"
y2="99.2572"
gradientUnits="userSpaceOnUse">
<stop stop-color="#41A247" />
<stop offset="0.352" stop-color="#4BA74B" />
<stop offset="0.956" stop-color="#67B554" />
<stop offset="1" stop-color="#69B655" />
</linearGradient>
</defs>
</svg>

View File

@ -0,0 +1,149 @@
<script>
export let width = "100"
export let height = "100"
</script>
<svg
{width}
{height}
viewBox="0 0 170 175"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M169.341 104.792C168.322 101.71 165.652 99.5637 162.199 99.0487C160.57
98.8061 158.706 98.9095 156.499 99.3635C152.653 100.156 149.799 100.457
147.717 100.516C155.577 87.268 161.969 72.161 165.648 57.9402C171.598
34.9456 168.419 24.4702 164.704 19.7319C154.871 7.19025 140.525 0.452754
123.218 0.246599C113.986 0.13391 105.881 1.95351 101.653 3.26203C97.7162
2.56866 93.4835 2.18154 89.0422 2.10995C80.7169 1.97738 73.361 3.78902
67.0757 7.51241C63.596 6.33779 58.0112 4.68192 51.5619 3.62529C36.3933
1.1395 24.1679 3.07643 15.2257 9.38239C4.39748 17.0174 -0.622171 30.283
0.306188 48.8104C0.601032 54.6928 3.89744 72.5905 9.08775 89.5642C12.0707
99.3205 15.2516 107.422 18.542 113.647C23.2084 122.473 28.2015 127.67
33.8068 129.537C36.9485 130.582 42.6568 131.314 48.6606 126.322C49.4216
127.242 50.4369 128.156 51.7843 129.005C53.4956 130.083 55.5881 130.963
57.6779 131.485C65.209 133.364 72.2633 132.894 78.2817 130.26C78.3189
131.328 78.3475 132.349 78.3714 133.231C78.4112 134.661 78.4511 136.063
78.5035 137.374C78.8601 146.239 79.4644 153.132 81.2548 157.955C81.353
158.221 81.4852 158.625 81.6246 159.054C82.5178 161.784 84.0113 166.353
87.811 169.932C91.7456 173.64 96.5049 174.777 100.864 174.777C103.05 174.777
105.136 174.49 106.965 174.099C113.486 172.704 120.893 170.579 126.25
162.963C131.316 155.765 133.778 144.922 134.224 127.838C134.281 127.354
134.335 126.893 134.386 126.453L134.492 125.551L135.685 125.655L135.993
125.676C142.635 125.978 150.756 124.572 155.744 122.259C159.685 120.434
172.315 113.778 169.341 104.792"
fill="black" />
<path
d="M157.984 106.539C138.235 110.606 136.877 103.931 136.877 103.931C157.729
73.046 166.446 33.8413 158.924 24.2462C138.402 -1.92761 102.878 10.451
102.285 10.7718L102.094 10.8063C98.1923 9.99759 93.8261 9.51633 88.918
9.43613C79.9824 9.29029 73.2036 11.7748 68.0598 15.6685C68.0598 15.6685
4.68831 -10.3919 7.63609 48.4439C8.26296 60.9604 25.6083 143.151 46.2965
118.326C53.8582 109.248 61.1648 101.572 61.1648 101.572C64.7933 103.979
69.1376 105.206 73.6917 104.765L74.0457 104.466C73.9354 105.593 73.9859
106.695 74.1871 108C68.8573 113.944 70.4239 114.988 59.7696 117.177C48.9886
119.395 55.3218 123.343 59.4569 124.376C64.4699 125.627 76.0671 127.399
83.903 116.449L83.5909 117.699C85.6787 119.369 87.1449 128.559 86.8992
136.89C86.6535 145.222 86.4895 150.942 88.1344 155.409C89.7806 159.877
91.4202 169.93 105.427 166.934C117.129 164.43 123.194 157.943 124.038
147.121C124.637 139.427 125.992 140.565 126.077 133.686L127.164
130.43C128.417 120.001 127.363 116.636 134.573 118.201L136.326
118.355C141.632 118.596 148.578 117.503 152.655 115.611C161.432 111.545
166.639 104.755 157.983 106.539H157.984"
fill="#336791" />
<path
d="M71.7692 54.0412C69.9895 53.7939 68.3778 54.0226 67.5623 54.6391C67.1041
54.9858 66.962 55.3875 66.9235 55.6639C66.8212 56.3964 67.3352 57.2064
67.6513 57.624C68.5452 58.8066 69.8507 59.6193 71.143 59.7983C71.3302
59.8248 71.5168 59.8367 71.7021 59.8367C73.857 59.8367 75.8167 58.1616
75.9893 56.9254C76.2051 55.3769 73.9533 54.3448 71.7692 54.0418"
fill="white" />
<path
d="M130.727 54.0902C130.557 52.8765 128.394 52.5305 126.341 52.8155C124.29
53.1006 122.302 54.0246 122.468 55.241C122.601 56.1869 124.312 57.8017
126.337 57.8017C126.509 57.8017 126.681 57.7904 126.855 57.7659C128.207
57.579 129.199 56.7219 129.671 56.228C130.388 55.475 130.804 54.6358 130.727
54.0902"
fill="white" />
<path
d="M164.556 106.077C163.803 103.804 161.379 103.073 157.352 103.903C145.396
106.366 141.114 104.66 139.708 103.627C149.002 89.494 156.647 72.4103
160.771 56.4713C162.725 48.9212 163.804 41.9092 163.893 36.1939C163.99
29.9211 162.92 25.3114 160.712 22.4955C151.813 11.1437 138.751 5.05449
122.939 4.88744C112.069 4.76547 102.885 7.54227 101.105 8.32314C97.3555
7.39246 93.2682 6.82106 88.8183 6.74814C80.6583 6.61623 73.6046 8.56642
67.7649 12.5417C65.2282 11.5991 58.6725 9.35259 50.6553 8.06329C36.795
5.83602 25.7809 7.52371 17.921 13.0819C8.54246 19.7147 4.21277 31.571
5.05148 48.3206C5.33371 53.9557 8.55043 71.2913 13.6265 87.8931C20.3083
109.744 27.5718 122.114 35.2139 124.659C36.1084 124.957 37.1396 125.165
38.2772 125.165C41.0649 125.165 44.4829 123.911 48.0389 119.643C52.3763
114.449 56.863 109.381 61.4935 104.446C64.4977 106.055 67.7981 106.954
71.1735 107.044C71.1802 107.133 71.1888 107.221 71.1968 107.308C70.6157 108
70.0468 108.701 69.4901 109.413C67.1513 112.377 66.6645 112.994 59.1361
114.541C56.9945 114.982 51.3068 116.152 51.2231 120.132C51.1328 124.481
57.9467 126.307 58.723 126.501C61.4284 127.177 64.0348 127.51 66.5204
127.51C72.5654 127.51 77.8852 125.527 82.1365 121.689C82.0057 137.191
82.6532 152.467 84.5179 157.121C86.0452 160.931 89.7759 170.243 101.56
170.242C103.29 170.242 105.193 170.041 107.287 169.593C119.586 166.961
124.927 161.535 126.993 149.572C128.098 143.179 129.996 127.912 130.888
119.723C132.771 120.309 135.196 120.578 137.817 120.577C143.283 120.577
149.591 119.418 153.547 117.585C157.991 115.525 166.01 110.469 164.556
106.077V106.077ZM135.267 50.7401C135.226 53.1576 134.893 55.3524 134.54
57.6433C134.159 60.1072 133.766 62.6547 133.667 65.747C133.569 68.7565
133.946 71.8853 134.31 74.9113C135.046 81.023 135.801 87.3151 132.878
93.5236C132.393 92.665 131.96 91.7775 131.583 90.8661C131.22 89.9872 130.431
88.5752 129.339 86.6211C125.089 79.0139 115.138 61.1997 120.232
53.9305C121.75 51.7669 125.601 49.5423 135.267 50.7401V50.7401ZM123.55
9.7828C137.717 10.095 148.924 15.3855 156.858 25.5063C162.944 33.2693
156.243 68.5921 136.843 99.0653C136.648 98.8177 136.452 98.5708 136.255
98.3248L136.009 98.0186C141.022 89.7545 140.042 81.5779 139.17
74.3286C138.811 71.3536 138.472 68.5437 138.559 65.9041C138.648 63.1068
139.018 60.7071 139.377 58.3871C139.817 55.5281 140.265 52.5696 140.142
49.0822C140.234 48.7163 140.271 48.2841 140.223 47.7711C139.907 44.4268
136.082 34.418 128.286 25.3591C124.022 20.4041 117.803 14.8591 109.311
11.1192C112.964 10.3635 117.958 9.65884 123.55 9.7828V9.7828ZM44.2757
116.52C40.3577 121.222 37.6523 120.32 36.7625 120.025C30.9652 118.095
24.2382 105.863 18.3075 86.4673C13.1756 69.6845 10.1767 52.8083 9.93898
48.076C9.18992 33.1095 12.8243 22.6791 20.742 17.0738C33.6274 7.95259
54.8123 13.4121 63.325 16.1809C63.2028 16.3016 63.0753 16.4142 62.9544
16.5369C48.9852 30.6191 49.3166 54.6789 49.3511 56.1498C49.3498 56.7173
49.3976 57.5207 49.4627 58.6257C49.703 62.6726 50.1506 70.2042 48.9553
78.7335C47.845 86.6595 50.2927 94.4172 55.6696 100.018C56.2215 100.592
56.8015 101.138 57.4075 101.654C55.0142 104.213 49.8126 109.871 44.2757
116.52V116.52ZM59.2031 96.6378C54.8695 92.1236 52.9012 85.8448 53.8023
79.4096C55.064 70.3998 54.5985 62.5526 54.3482 58.3367C54.313 57.7467
54.2818 57.2297 54.2638 56.822C56.3045 55.0157 65.7614 49.9579 72.5056
51.5004C75.5829 52.2037 77.4582 54.2958 78.2378 57.8939C82.2727 76.5228
78.7717 84.2871 75.9587 90.5267C75.379 91.8121 74.8312 93.0271 74.3637
94.2839L74.0011 95.2557C73.0834 97.7123 72.2294 99.9966 71.7001
102.166C67.0929 102.152 62.6111 100.188 59.2031 96.6372V96.6378ZM59.9104
121.761C58.565 121.426 57.355 120.843 56.6452 120.36C57.2382 120.081 58.2934
119.702 60.1235 119.326C68.9801 117.506 70.3481 116.221 73.335
112.436C74.0197 111.567 74.796 110.583 75.8711 109.385L75.8724
109.384C77.4735 107.594 78.2059 107.897 79.5341 108.448C80.6105 108.892
81.6591 110.239 82.0841 111.721C82.2853 112.421 82.5111 113.749 81.772
114.783C75.5324 123.504 66.4401 123.392 59.9104 121.761V121.761ZM106.261
164.816C95.4264 167.134 91.5901 161.615 89.062 155.307C87.4304 151.234
86.6282 132.868 87.1973 112.586C87.2047 112.316 87.1661 112.056 87.0918
111.81C87.0252 111.326 86.924 110.847 86.7889 110.377C85.9429 107.426 83.881
104.958 81.4074 103.934C80.4246 103.528 78.621 102.782 76.4535
103.336C76.9157 101.434 77.7172 99.2873 78.5864 96.9626L78.951
95.9849C79.3614 94.8825 79.8767 93.7404 80.4213 92.5313C83.365 86.0033
87.3966 77.0617 83.0211 56.8618C81.3821 49.2957 75.9089 45.6008 67.6121
46.4592C62.6383 46.973 58.0875 48.9762 55.8177 50.1249C55.3296 50.3715
54.8834 50.6102 54.4664 50.8422C55.0999 43.2191 57.4932 28.9725 66.4461
19.9587C72.0833 14.2844 79.5905 11.4818 88.7373 11.6329C106.76 11.9272
118.317 21.1598 124.84 28.8538C130.46 35.4833 133.504 42.1618 134.718
45.7639C125.584 44.8365 119.372 46.6362 116.223 51.1305C109.372 60.9067
119.971 79.8809 125.065 89.0001C125.999 90.6712 126.805 92.1157 127.059
92.7295C128.718 96.7426 130.865 99.4219 132.433 101.377C132.914 101.977
133.38 102.558 133.735 103.066C130.968 103.862 125.999 105.701 126.452
114.896C126.087 119.51 123.49 141.11 122.17 148.742C120.428 158.824 116.712
162.58 106.261 164.817V164.816ZM151.487 113.154C148.658 114.465 143.924
115.448 139.427 115.659C134.459 115.891 131.931 115.104 131.336
114.619C131.057 108.889 133.193 108.291 135.454 107.657C135.809 107.557
136.156 107.46 136.491 107.343C136.699 107.512 136.926 107.68 137.176
107.844C141.168 110.475 148.289 110.758 158.342 108.687L158.453
108.665C157.097 109.931 154.776 111.629 151.487 113.154Z"
fill="white" />
</svg>

View File

@ -0,0 +1,55 @@
<script>
export let width = "100"
export let height = "100"
</script>
<svg
{width}
{height}
viewBox="0 0 128 155"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M10.312 26.843L0 32V122.51L10.312 127.637L10.374 127.562V26.914L10.312
26.843Z"
fill="#8C3123" />
<path
d="M65.5 114.5L10.312 127.637V26.843L65.5 39.6935V114.5Z"
fill="#E05243" />
<path
d="M40.589 93.933L63.998 96.913L64.145 96.574L64.2765 58.189L63.998
57.889L40.589 60.826V93.933"
fill="#8C3123" />
<path
d="M63.998 114.647L117.683 127.665L117.768 127.53L117.766 26.933L117.681
26.843L63.998 39.841V114.647"
fill="#8C3123" />
<path
d="M87.4135 93.933L63.998 96.913V57.889L87.4135 60.826V93.933Z"
fill="#E05243" />
<path
d="M87.4135 44.8155L63.998 49.083L40.589 44.8155L63.9685 38.6875L87.4135
44.8155Z"
fill="#5E1F18" />
<path
d="M87.4135 109.901L63.998 105.605L40.589 109.901L63.9695 116.427L87.4135
109.901Z"
fill="#F2B0A9" />
<path
d="M40.589 44.8155L63.998 39.0225L64.1875 38.964V0.1565L63.998 0L40.589
11.7065V44.8155Z"
fill="#8C3123" />
<path
d="M87.4135 44.8155L63.998 39.0225V0L87.4135 11.7065V44.8155Z"
fill="#E05243" />
<path
d="M63.998 154.714L40.5865 143.012V109.903L63.998 115.694L64.3425
116.086L64.249 154.039L63.998 154.714Z"
fill="#8C3123" />
<path
d="M63.998 154.714L87.4115 143.012V109.903L63.998 115.694V154.714"
fill="#E05243" />
<path
d="M117.684 26.843L128 32V122.51L117.684 127.665V26.843Z"
fill="#E05243" />
</svg>

View File

@ -0,0 +1,391 @@
<script>
export let width = "100"
export let height = "100"
</script>
<svg
{width}
{height}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1478.201 1195.111">
<g transform="matrix(.569 0 0 .569 199.451 -82.735)">
<linearGradient
id="a"
gradientUnits="userSpaceOnUse"
x1="-2901.952"
y1="923.573"
x2="-2061.249"
y2="1420.331"
gradientTransform="matrix(.1234 0 0 -.1234 1158.33 1550.273)">
<stop offset="0" stop-color="#909ca9" />
<stop offset="1" stop-color="#ededee" />
</linearGradient>
<path
fill="url(#a)"
d="M1410.773 814.195l-286.9 93.683-249.599 110.161-69.829 18.435c-17.784
16.916-36.431 34.049-56.599 51.397-22.119 19.082-42.72 36.433-58.553
49.008-17.564 13.88-43.587 39.902-56.814 56.38-19.735 24.721-35.348
50.96-42.071 71.13-11.928 36.433-6.07 73.297 16.916 107.346 29.492 43.369
88.261 87.606 156.785 117.749 34.916 15.4 93.683 35.132 137.92 46.19
73.512 18.651 215.771 38.819 294.054 41.857 15.828.65 37.082.65 37.947 0
1.737-1.088 13.881-24.289 27.979-53.129 48.142-98.238 82.838-190.402
101.703-269.119 11.276-47.706 20.166-111.246 26.019-186.492 1.521-21.036
2.169-91.514.868-115.37-1.953-39.033-5.423-70.692-10.84-101.703-.868-4.555-1.088-8.676-.652-8.892.865-.65
3.467-1.517 38.815-11.712l-7.153-16.912v-.005h.004zm-65.49 38.386c2.602 0
9.539 66.573 11.273 108.646.436 8.89.216 14.745-.216 14.745-1.733
0-36.649-20.599-61.583-36.212-21.687-13.663-62.888-40.988-69.393-46.192-2.173-1.517-1.957-1.733
15.828-7.807 30.14-10.194 101.706-33.18 104.091-33.18zm-146.161
48.143c1.953 0 6.937 2.816 18.865 10.191 44.671 27.974 105.393 61.805
131.415 73.083 8.022 3.469 8.887 2.166-9.542 14.746-39.468 26.889-88.697
53.344-148.983 80.018-10.624 4.771-19.514 8.456-19.73 8.456-.432 0
.865-5.418 2.598-11.925 14.53-54.001 22.772-108.647
23.208-152.452.216-21.687.216-21.687 2.169-22.334-.436.217-.22.217 0
.217zm-30.142 11.492c1.297 1.299.432 49.877-1.304 63.104-3.903
31.662-9.975 61.153-19.947 94.335-2.386 8.018-4.558 14.745-4.987
15.177-.872
1.083-30.581-27.975-40.339-39.251-16.916-19.518-30.141-39.035-39.9-58.117-4.988-9.759-12.793-28.84-12.144-29.492
3.469-2.385 117.753-46.622 118.621-45.756zm-141.826 55.731c.216 0 .432 0
.652.216.432.434 1.953 3.905 3.254 7.807 6.937 18.867 22.548 46.624 35.997
64.407 14.746 19.518 34.048 40.334 50.091 53.996 5.207 4.337 9.975 8.456
10.624 9.108 1.304 1.302 1.737 1.083-33.612 14.53-40.981 15.613-85.656
31.226-136.835 47.706a6825.474 6825.474 0 0 0-36.643
11.928c-1.955.652-1.303-.434 4.335-9.323 25.371-39.686 63.97-117.536
85.657-172.618 3.687-9.542 7.373-19.082 8.025-21.251.868-3.038 1.95-4.121
4.768-5.64 1.518-.43 3.038-.866 3.687-.866zm-43.367 17.999c.649.436-10.411
23.637-21.254 44.889-21.036 40.985-44.022 81.323-74.815 130.331-5.204
8.456-10.19 16.265-10.842 17.132-1.083 1.519-1.519
1.083-4.988-5.638-7.373-14.53-13.447-33.181-16.699-50.313-3.254-16.916-2.602-46.406
1.086-64.621 2.816-13.444 2.602-13.227 9.107-16.481 27.757-14.095
117.537-56.166 118.405-55.299zm374.073 15.182v9.107c0 48.359-5.204
114.716-12.797 163.077-1.301 8.456-2.389 15.393-2.602 15.613 0
0-6.288-1.733-13.661-3.905-32.527-10.193-67.875-25.156-99.754-42.718-21.038-11.494-51.612-30.363-50.743-31.231.213-.215
9.323-4.986 19.947-10.625 42.509-22.118 83.274-45.972 118.622-69.609
13.229-8.892 33.176-23.202 37.518-27.107l3.47-2.602zm-537.802 64.185c.867
0 .65 1.735-.651 9.542-.868 5.64-1.951 16.049-2.382 23.202-1.739 31.662
3.469 55.084 19.082 87.177 4.337 8.892 7.809 16.265 7.589 16.48-1.519
1.303-145.074 43.375-190.183 55.734-13.444 3.685-25.152 6.939-26.024
7.153-1.515.436-1.733.22-1.083-3.47 4.987-31.875 29.276-73.512
63.104-108.644 22.554-23.419 40.554-37.08 71.347-54.648 22.119-12.575
56.165-31.439 58.767-32.309.002-.217.218-.217.434-.217zm338.295
60.503c.216-.216 5.42 2.605 11.708 6.29 46.408 26.891 111.03 51.83 166.108
64.623l4.991 1.086-6.941 3.899c-28.84 16.049-123.606 55.515-220.538
91.732-14.098 5.202-27.975 10.409-30.581 11.492-2.602 1.083-4.988
1.735-4.988 1.519 0-.22 3.906-7.809 8.89-17.132 27.107-50.744
54.433-112.547 68.311-155.485 1.739-4.12 2.82-7.805 3.04-8.024zm-34.48
11.278c.22.221-1.517 4.771-3.687 9.975-18.865 45.756-43.59 95.636-75.249
151.583-8.022 14.314-14.746 25.808-14.966 25.808-.213
0-6.721-3.906-14.527-8.676-45.976-28.192-86.743-62.888-113.414-96.501l-3.905-4.771
19.732-5.422c70.696-19.298 130.762-40.116 190.4-65.704 8.459-3.471
15.4-6.292 15.616-6.292zm214.253 74.815s.217.217 0 0c.216 4.988-10.844
49.661-19.953 81.969-7.589 27.107-14.098 48.361-26.022 85.874-5.204
16.485-9.755 30.143-9.975 30.143-.216
0-1.517-.216-2.818-.647-64.405-11.714-122.089-27.977-176.303-49.661-15.182-6.074-36.866-15.833-38.167-16.916-.432-.438
12.58-6.506 29.06-13.663 98.669-43.154 201.024-92.164 236.153-113.196
4.119-2.603 7.373-3.903 8.025-3.903zm-494.646 16.916c.434.432-27.107
40.118-65.709 94.114-13.444 18.867-29.057 40.985-34.911 49.225-5.856
8.241-14.746 21.253-19.734 29.06l-9.112
14.096-9.759-8.24c-11.494-9.544-31.442-29.927-40.333-41.204-18.651-23.201-31.226-47.706-36.214-70.04-2.386-10.411-2.386-15.618-.22-16.265
3.252-.867 61.153-14.53 115.37-27.11 30.143-6.937 65.054-15.177
77.632-18.213 12.579-3.041 22.774-5.423 22.99-5.423zm27.756 10.626l6.937
7.806c31.231 34.914 63.108 60.724 101.708 83.272 6.941 3.906 12.144 7.373
11.708 7.594-1.514 1.083-134.016 48.136-195.385 69.389-34.478
12.143-62.888 21.901-63.102 21.901-.216
0-2.169-1.299-4.341-2.818l-3.901-2.82 6.288-9.106c20.383-29.493
45.976-61.803 101.707-129.028l38.381-46.19zm173.053 123.822c.213-.215
9.755 3.252 21.464 7.594 28.195 10.624 50.527 17.345 80.456 24.936 36.866
9.326 90.211 18.434 121.657 21.035 4.771.432 7.373.868 6.505
1.519-1.521.868-33.395 11.494-56.816 18.867-37.302 11.708-151.149
45.32-243.962 71.995-17.132 4.987-31.879 9.108-32.746
9.323-2.166.436-9.325-1.519-9.325-2.386 0-.431 5.204-7.153 11.494-14.527
31.225-37.3 62.238-78.935 88.044-118.403 7.154-10.846 13.229-19.736
13.229-19.953zm-38.17 1.087c.216.216-15.179 24.936-42.066 67.439-11.496
17.999-24.291 38.383-28.846 45.54-4.337 6.939-10.842 17.784-14.527
23.854l-6.29
11.061-3.252-.868c-7.809-2.169-62.672-21.471-77.202-27.325-18-7.157-36.649-15.829-50.529-23.202-17.346-9.326-39.03-23.206-37.297-23.637.433-.216
30.143-8.243 65.922-17.999 94.984-25.809 147.678-40.77 182.161-51.612
6.29-1.952 11.71-3.471 11.926-3.251zm269.985 63.318h.216c.868 2.171-34.26
99.755-47.06 130.547-2.815 6.939-3.896 8.677-5.417
8.456-3.687-.213-54.646-7.37-85.66-11.925-53.994-8.24-144.641-24.073-167.409-29.275l-5.204-1.083
32.307-7.378c69.396-15.613 102.791-24.069 136.619-34.478 42.722-13.011
85.011-29.276 127.729-49.225 6.722-3.037 12.361-5.422 13.879-5.639z" />
<linearGradient
id="b"
gradientUnits="userSpaceOnUse"
x1="-2882.7"
y1="10288.81"
x2="-2206.249"
y2="10288.81"
gradientTransform="matrix(.1234 0 0 -.1234 1158.33 1550.273)">
<stop offset="0" stop-color="#939fab" />
<stop offset="1" stop-color="#dcdee1" />
</linearGradient>
<path
fill="url(#b)"
d="M1114.983 145.414c-4.771-.647-81.757 27.11-131.415 47.275-67.01
27.327-119.052 53.351-151.148 75.899-11.925 8.461-26.891 23.422-29.273
29.276-.867 2.169-1.303 4.771-1.303 7.373l29.06 27.541 69.175 22.119
164.594 29.493 188.228 32.312 1.953-16.264c-.649
0-1.085-.216-1.73-.216l-24.728-3.905-4.984-8.89c-25.59-45.107-53.781-101.056-70.261-138.789-12.793-29.276-24.938-63.102-31.662-87.391-3.687-14.746-4.119-15.613-6.501-15.829v-.005h-.005zm-3.474
11.063h.223c.213.214 1.081 6.29 1.95 13.442 3.683 30.364 10.411 59.635
21.035 91.297 8.022 23.855 8.022 22.555-1.301
19.734-22.119-6.07-121.221-23.202-193-33.177-11.494-1.519-21.253-3.036-21.253-3.252-.867-.867
51.827-28.41 75.031-39.25 29.709-13.665 111.246-47.711
117.315-48.794zm-209.047 97.15l8.461 2.816c45.97 15.616 161.551 37.736
225.31 42.94 7.154.651 13.229 1.303 13.442 1.303.216.216-5.852
3.469-13.661 7.154-30.79 15.397-64.621 34.264-88.042 48.794-6.937
4.335-13.229 7.807-14.094 7.807-.868
0-5.42-.868-10.191-1.519l-8.674-1.303-21.683-21.253c-38.167-37.08-68.094-65.704-79.588-76.549l-11.28-10.19zm-8.671
6.721l30.576 38.168c16.696 21.035 33.611 41.635 37.301 46.187 3.683 4.557
6.721 8.245 6.505
8.461-.868.65-44.236-7.809-67.226-13.011-23.637-5.423-33.395-8.025-47.924-12.577l-11.928-3.905v-3.038c.216-14.53
18.651-36.214 49.877-58.331l2.819-1.954zm259.791 52.046c.869 0 1.95 1.951
4.552 7.806 7.373 16.263 30.364 60.07 35.997 68.526 1.74 2.822 4.771
3.038-25.802-1.95-73.512-11.93-97.152-15.829-97.152-16.263 0-.216
2.169-1.735 4.988-3.254 22.771-12.575 45.756-28.624 66.142-45.756
4.988-4.121 9.542-8.024 10.407-8.676.216-.433.652-.649.868-.433z" />
<radialGradient
id="c"
cx="-14217.448"
cy="7277.705"
r="898.12"
gradientTransform="matrix(-.1185 -.0178 -.036 .237 -198.955 -1314.415)"
gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#ee352c" />
<stop offset="1" stop-color="#a91d22" />
</radialGradient>
<path
fill="url(#c)"
d="M804.66 294.828s-4.768 7.593-.215 18.87c2.822 6.937 11.061 15.393
20.384 24.069 0 0 96.5 94.114 108.211 107.561 53.344 61.585 76.549 122.305
78.718 206.012 1.301 53.78-8.894 101.054-34.264 155.919-45.106
98.453-140.307 207.098-287.117 327.67l21.472-7.157c13.878-10.411
32.745-21.467 76.982-45.756 102.137-55.952 217.071-107.346 358.028-160.258
202.971-76.335 536.715-165.681
726.676-194.736l19.737-3.038-3.038-4.771c-17.345-26.891-29.276-43.587-43.59-61.369-41.633-51.612-92.157-93.463-153.964-128.161-85.007-47.489-194.956-84.571-334.173-112.112-26.239-5.207-83.923-15.181-130.763-22.337-99.321-15.393-163.51-26.021-234.203-38.165-25.37-4.339-63.323-10.843-88.478-16.263-13.011-2.822-37.947-8.676-57.464-15.398-15.613-6.075-38.168-12.147-42.939-30.58zm55.952
54.216c.214-.214 3.683 1.083 8.24 2.602 8.24 2.816 18.865 6.07 31.446
9.542a1599.47 1599.47 0 0 0 28.624 7.589c13.011 3.251 23.852 6.288 24.068
6.288 1.521 1.519 23.424 71.558 30.797 98.449 2.815 10.195 4.988 18.867
4.771
18.867-.223.22-2.605-3.469-5.423-8.456-25.373-44.673-65.491-89.995-111.899-126.428-6.069-4.333-10.624-8.237-10.624-8.453zm106.692
29.492c1.085 0 5.856.651 11.708 1.951 36.866 8.24 103.008 20.818 145.293
27.975 7.157 1.083 12.797 2.387 12.797 2.818 0 .436-2.605 1.951-5.859
3.688-7.153 3.685-35.997 20.815-45.536 27.322-24.073 16.047-45.756
33.395-61.371 49.008-6.288 6.29-11.712 11.494-11.712
11.494s-1.297-3.685-2.386-8.242c-7.802-30.143-24.069-74.816-38.815-106.258-2.386-4.986-4.339-9.541-4.339-9.973
0 .433 0 .217.22.217zm187.795 35.781c1.301.432 3.47 7.806 7.806 24.069
8.025 31.446 11.712 66.576 10.411 99.321-.436 9.108-.868 17.564-1.304
18.651l-.649
2.166-11.276-3.685c-23.204-7.373-60.935-18.435-93.245-27.541-18.436-4.988-33.395-9.542-33.395-9.975
0-1.303 26.891-28.192 38.383-38.383 21.898-19.303 81.316-65.275
83.269-64.623zm14.963 2.166c.652-.647 89.779 14.746 130.331 22.554 30.145
5.854 73.948 14.963 76.549 16.049 1.301.432-3.254 3.034-17.784
9.539-57.248 25.808-99.754 49.008-142.036 77.202-11.06 7.373-20.386
13.444-20.602 13.444-.216 0-.433-6.287-.433-13.878
0-41.201-8.241-82.838-23.424-117.968-1.517-3.47-2.818-6.722-2.601-6.942zm230.516
45.542c.652.65-2.169 18.217-4.771 28.624-7.806 32.312-28.84 80.24-54.643
125.343-4.558 8.024-8.677 14.53-9.114
14.746-.429.216-6.285-3.038-13.009-6.941-25.154-14.746-53.778-28.624-85.007-41.637-8.671-3.685-16.263-6.723-16.48-7.153-1.521-1.303
68.308-47.493 105.174-69.612 29.276-17.781 76.982-44.239
77.85-43.37zm16.48 2.601c1.953 0 41.421 10.844 62.019 16.916 50.963 15.181
109.512 36.648 147.679 53.996l15.828 7.159-11.056 2.6c-93.245
21.467-173.049 46.192-250.034 77.418-6.289 2.602-11.928 4.771-12.357
4.771-.436 0 1.733-4.987 4.552-11.061 23.204-49.225 38.167-100.62
41.85-144.427.221-4.121.867-7.372 1.519-7.372zm-392.938 90.213c.649-.652
30.793 6.506 47.057 11.056 24.721 6.942 77.198 24.505 77.198 25.808 0
.216-5.853 5.204-12.79 11.278-28.408 23.637-55.734 48.572-88.481
80.234-9.759 9.328-17.997 16.917-18.429 16.917-.436
0-.649-1.304-.436-3.038 4.987-36.433
3.906-83.272-3.034-130.763-.653-6.074-1.302-11.276-1.085-11.492zm633.433.652c.429.431-13.881
22.984-22.988 35.777-13.009 18.649-32.098 43.375-75.252 97.588-22.765
28.622-48.358 60.936-56.812 71.778-8.678 10.842-15.831 19.948-16.051
19.948-.216
0-3.031-3.901-6.069-8.671-24.289-36.433-53.349-68.311-87.829-96.935-6.505-5.423-13.658-11.278-16.044-13.013-2.386-1.734-4.339-3.469-4.339-3.685
0-.649 36.862-16.483 64.841-27.757 49.01-19.952 115.794-43.805
165.892-59.203 26.24-8.239 54.215-16.263 54.651-15.827zm16.696
4.334c.865-.215 6.072 2.387 12.361 6.07 52.697 30.143 104.305 68.962
145.077 108.864 11.492 11.278 39.9 40.77 39.464 40.986 0
0-9.975.867-21.683 1.733-91.296 6.942-208.178 26.239-320.511 53.345-7.589
1.733-14.31 3.252-14.746 3.252-.429 0 8.025-8.456 18.653-18.647
65.922-63.538 96.067-103.656 131.628-175.22 4.986-10.623 9.325-19.731
9.757-20.383-.216 0-.216 0 0 0zm-482.936 49.446c3.038.647 31.229 13.88
52.48 24.503 19.517 9.755 48.794 25.372 50.311 26.671.216.216-10.195
5.638-22.984 11.928-40.772 20.384-75.684 39.682-112.118 61.802-10.408
6.29-19.082 11.497-19.298 11.497-.868 0-.652-.872 5.204-11.497
19.518-35.561 35.129-78.065 44.023-119.486.864-3.252 1.733-5.418
2.382-5.418zm-28.192 5.202c.652.652-6.721 27.323-11.273 41.853-8.894
27.541-23.856 62.02-38.383 88.043-3.474 6.069-8.677 14.961-11.496
19.948l-5.42
8.674-12.144-11.707c-14.094-13.663-25.59-22.12-40.333-29.712-5.859-3.033-10.411-5.638-10.411-6.069
0-1.735 37.082-35.347 65.49-59.635 20.383-17.566 63.321-52.045
63.97-51.395zm172.404 70.913l10.627 6.937c24.282 15.833 52.906 36.866
74.813 55.298 12.357 10.19 36.21 31.662 40.985 36.866l2.598 2.822-17.561
4.986c-99.321 27.538-176.087 52.043-265.649 85.007-9.975 3.685-18.433
6.721-19.085 6.721-1.297 0-2.385 1.083 19.954-19.519 57.251-52.691
107.992-110.812 145.726-167.411l7.592-11.707zm-45.324
11.276c.432.432-29.276 42.284-47.06 65.922-21.251 28.192-58.985
75.465-85.007 106.256-10.84 12.797-20.163 23.422-20.599
23.64-.652.216-.868-3.036-.868-8.024
0-26.242-6.721-54.216-18.433-78.068-4.988-9.975-5.856-12.361-4.768-13.444
4.119-3.688 67.223-39.686 107.123-61.153 26.89-14.312 68.956-35.563
69.612-35.129zm-274.107 67.225c.652 0 5.64 2.6 11.279 5.638 13.878 7.589
26.239 16.046 37.298 25.156.432.432-5.204 4.988-12.577 10.406-20.602
14.746-51.828 38.385-70.041 52.915-19.088 15.18-19.734 15.613-17.568
12.361 14.314-21.903 21.467-34.264 29.06-50.093 6.721-14.094 13.442-30.793
18.213-45.323 1.734-6.289 3.904-11.06 4.336-11.06zm73.083
57.248c1.081-.214 2.386 1.735 8.238 10.411 12.361 18.429 21.903 43.154
24.292 63.104l.429 4.339-29.705 11.494c-53.133 20.599-102.139
40.985-135.322 56.162-9.322 4.339-25.587 12.144-36.211 17.352-10.627
5.418-19.301 9.539-19.301 9.323s6.721-5.204 14.961-11.278c64.844-47.055
121.007-98.669 163.076-150.279 4.555-5.423 8.677-10.411
9.107-10.627l.436-.001zm-33.612 8.242c.868.867-23.853 28.84-40.768
45.971-41.853 42.723-83.273 76.12-134.669 108.649-6.505 4.119-12.359
7.804-13.011 8.24-1.519.867.432-1.303 22.986-25.808 14.314-15.397
25.155-28.408 37.516-44.453 8.24-10.624 9.759-12.143 21.688-20.604
31.878-22.987 105.39-72.864 106.258-71.995z" />
</g>
<path
fill="#231F1F"
d="M265.747 900.102c-2.276 0-4.553.217-6.809.217-45.975 2.45-76.983
22.683-95.113 62.195-15.506 35.735-13.813 82.446.174 118.4 16.265 35.131
42.547 53.672 86.416 60.675 9.282 1.52 15.506 6.616 33.483 27.606l22.12
25.915h40.118l-26.676-26.892c-14.746-14.745-26.673-27.584-26.673-28.712
0-1.127 5.641-3.599 12.469-5.68 22.51-6.812 41.203-24.202 54.279-50.854
10.583-21.402 12.102-28.018 13.619-54.646
3.969-79.26-37.82-128.813-107.409-128.247l.002.023zm35.173 207.27c-19.517
9.453-47.857 11.34-66.356
4.553-19.127-7.025-37.646-26.889-45.975-49.377-9.259-24.591-7.937-69.956
2.646-90.386 17.023-32.528 39.534-47.49 72.43-47.49 48.792 0 76.549 29.884
80.171 86.048 2.863 46.885-12.838 82.058-42.895
96.632l-.021.02zm693.025-139.568c-16.828 0-29.709 6.811-38.385 20.231l-6.809
10.627v-27.628h-29.123v165.678h29.104v-52.956c0-48.424.604-54.084
7.371-67.335 9.326-18.172 25.371-27.234 40.879-22.897l10.408
3.036v-28.712h-13.445v-.044zm-171.098-1.519c-5.705 0-11.756.76-17.781
2.084-38.971 10.19-60.938 47.489-59.594 85.873 0 32.139 6.244 48.206 21.752
65.057 31.77 26.065 60.502 28.146 99.275 14.161 6.615-2.819 13.814-6.072
13.814-6.072v-26.065l-13.814 7.156c-31.379 13.661-55.016
13.661-73.949-2.43-12.076-12.296-17.391-27.042-19.84-43.868h117.426v-22.339c0-45.539-27.41-74.294-67.313-73.557h.024zm-47.492
72.647s4.338-28.407 20.428-39.554c7.744-5.466 16.633-8.11 25.328-8.11 8.719
0 17.414 2.818 24.592 8.306 14.748 11.341 17.219 39.143 17.219
39.143h-87.566v.215h-.001zm-702.111-29.881c-31.573-19.128-45.582-32.921-43.869-49.185
4.9-44.997 60.503-38.773
91.295-21.749l.219-30.272s-17.024-7.373-41.421-7.764c-37.429-.564-61.63
11.709-72.97 36.691-16.656 36.865-1.908 64.665 51.396 95.677 29.925 17.412
43.152 32.528 43.152 49.008 0 34.047-41.05 45.931-83.401
24.57-8.716-4.337-16.09-7.959-16.48-7.959-1.519 9.651-.736 32.745-.736
32.745s13.012 5.466 32.527 9.236c48.4 9.65 92.445-13.054 96.608-49.919
3.622-34.609-8.893-52.761-56.318-81.104l-.002.025zm1178.454-43.155c-5.682
0-11.711.78-18 2.103-38.924 10.192-60.85 47.492-59.354 85.876 0 32.095 6.225
48.011 21.729 64.838 31.771 26.089 60.504 28.191 99.473 14.184 6.592-2.818
13.77-6.026 13.77-6.026v-26.109l-13.791 7.197c-31.443 13.619-55.082
13.619-73.947-2.471-12.145-12.274-17.414-26.847-19.865-43.871h117.232v-22.336c0-45.321-27.412-74.099-67.313-73.339l.066-.046zm-47.492
72.646s4.381-28.365 20.449-39.729c7.721-5.485 16.611-8.132 25.307-8.132
8.674 0 17.414 2.819 24.594 8.327 14.746 11.342 17.219 39.338 17.219
39.338h-87.545l-.024.196zm-533.809-29.123c-31.573-19.083-45.54-32.92-43.848-49.185
4.9-45.02 60.504-38.773
91.296-21.749l.218-30.272s-17.024-7.374-41.421-7.722c-37.429-.563-61.63
11.711-72.991 36.692-16.633 36.864-1.692 64.666 51.437 95.677 29.884 17.393
43.111 32.312 43.111 48.792 0 34.047-41.029 46.126-83.381
24.569-8.674-4.337-16.046-7.916-16.48-7.916-1.519 9.649-.736 32.746-.736
32.746s12.858 5.27 32.31 9.237c48.445 9.672 92.51-13.012 96.653-49.877
3.6-34.437-8.891-52.587-56.167-80.952v-.04zm752.421-42.005c-16.828 0-29.859
6.829-38.383 20.254l-6.811
10.582v-27.583h-29.123V1136.3h29.102v-52.954c0-48.403.584-54.085
7.375-67.313 9.324-18.15 25.369-27.235 40.875-22.878l10.408
3.035v-28.775h-13.443zm-984.021
41.05V902.941h-29.361v233.728h123.478v-27.604h-94.116v-100.601zm679.015
32.896l-24.201 62.975-23.27-63.322-23.637-70.173h-30.055c19.475 55.212
40.658 111.376 62.02 165.829 9.26.216 18.541 0 27.799 0l32.682-82.058
33.287-83.75h-28.732s-12.688 33.266-25.914 70.521l.021-.022zM506.455
839.251c4.728 0 8.674-1.516 11.927-4.769 3.208-3.211 4.9-6.984 4.9-11.711
0-4.728-1.692-8.675-4.9-11.711-3.253-3.035-7.005-4.555-11.711-4.555-4.769
0-8.717 1.52-11.927 4.728-3.252 3.211-4.727 7.158-4.727 11.712 0 4.771 1.519
8.716 4.727 11.711 3.037 3.034 6.984 4.553 11.711
4.553v.042zm-10.408-26.889c2.818-2.818 6.245-4.121 10.625-4.121 4.121 0
7.548 1.303 10.411 4.121 2.819 2.819 4.337 6.245 4.337 10.409 0 4.163-1.518
7.764-4.337 10.582-2.862 2.817-6.29 4.163-10.411 4.163-4.185
0-7.59-1.301-10.408-4.163-2.819-2.818-4.337-6.419-4.337-10.582 0-4.164
1.301-7.589 4.12-10.409zm7.003 11.928h1.908c1.346 0 2.668 1.3 3.795
3.773l2.279 5.116h3.577l-2.818-5.683c-1.149-2.275-2.276-3.598-3.6-3.969
1.67-.39 2.992-.953 3.947-2.082.952-.974 1.3-2.298 1.3-3.795
0-1.734-.542-3.034-1.69-3.989-1.302-1.084-3.384-1.669-6.074-1.669h-6.026v21.187h3.035v-8.891l.367.002zm0-9.846h2.647c1.908
0 3.253.39 3.99.953.716.564.911 1.303.911 2.646 0 2.45-1.52 3.601-4.337
3.601h-3.252v-7.2h.041zm-485.018
7.958c0-7.373-.216-12.858-.39-16.09h.174c.758 3.814 1.691 6.657 2.45
8.543l28.19 62.975h4.728l28.19-63.538c.761-1.733 1.52-4.337
2.452-7.959h.216c-.563 6.29-.758 11.754-.758
16.112v55.581h9.648v-82.622h-12.1L54.919 852.87c-.955 2.276-2.278
5.683-3.969
10.193h-.392c-.563-2.234-1.886-5.639-3.772-9.803l-25.33-58.053H8.598v82.621h9.281v-55.385l.153-.041zm96.045.154h8.329v51.458h-8.329v-51.458zm4.164-18.868c1.736
0 3.21-.587 4.337-1.734 1.15-1.129 1.91-2.603 1.91-4.337
0-1.692-.565-3.211-1.887-4.337-1.171-1.15-2.668-1.737-4.381-1.737-1.69
0-3.208.587-4.338 1.737-1.146 1.126-1.907 2.645-1.907 4.337 0 1.887.586
3.208 1.907 4.337 1.304 1.147 2.647 1.734 4.338 1.734h.021zm63.54
71.455v-9.066c-4.555 3.405-9.456 5.098-14.53 5.098-6.07
0-10.995-2.081-14.595-6.07-3.577-3.947-5.485-9.436-5.485-16.266 0-7.156
1.908-12.84 5.854-17.177 3.795-4.163 8.719-6.245 14.748-6.245 4.922 0 9.647
1.52 14.009 4.557v-9.65c-3.968-2.082-8.5-3.037-13.619-3.037-9.456 0-16.827
3.037-22.335 8.894-5.466 5.854-8.285 13.813-8.285 23.42 0 8.543 2.45 15.722
7.548 21.209 5.312 5.637 12.102 8.5 20.428 8.5 6.438-.178 11.707-1.523
16.262-4.167zm23.831-27.433c0-6.788 1.518-12.273 4.337-16.049 2.647-3.403
5.855-5.116 9.65-5.116 3.21 0 5.486.585 7.155
1.908v-9.846c-1.3-.563-3.187-.758-5.637-.758-3.405 0-6.439 1.146-9.107
3.253-2.819 2.231-5.074 5.638-6.397
9.975h-.216v-12.08h-9.433v58.985h9.454V847.71h.194zm54.279 31.443c8.892 0
16.048-2.863 21.36-8.543 5.29-5.641 7.936-13.229 7.936-22.686
0-9.647-2.427-17.021-7.372-22.51-4.9-5.483-11.711-8.132-20.603-8.132s-16.048
2.647-21.36 7.764c-5.681 5.641-8.674 13.599-8.674 23.813 0 8.891 2.429
16.265 7.548 21.751 5.29 5.68 12.295 8.521 21.165
8.521v.022zm-13.445-48.055c3.6-3.795 8.329-5.683 14.182-5.683 6.074 0 10.627
1.888 14.01 5.683 3.404 3.969 5.097 9.63 5.097 17.197 0 7.198-1.519
12.859-4.729 16.654-3.208 3.969-7.936 6.071-14.183 6.071-6.071
0-10.777-2.104-14.377-6.071-3.577-3.99-5.291-9.456-5.291-16.654-.368-7.156
1.519-13.01 5.291-17.197zm84.141 42.916c3.599-3.208 5.509-7.155 5.509-12.102
0-4.337-1.52-7.936-4.338-10.777-2.3-2.275-5.854-4.337-10.994-6.419-4.556-1.906-7.374-3.6-8.893-4.923-1.517-1.517-2.45-3.402-2.45-6.071
0-2.45.955-4.337 2.821-5.855 1.908-1.516 4.337-2.253 7.59-2.253 5.096 0
9.454 1.343 13.443 4.185v-9.456c-3.816-1.906-7.958-2.817-12.686-2.817-6.071
0-11.189 1.671-14.964 4.899-3.969 3.212-5.854 7.375-5.854 12.274 0 4.337 1.3
7.938 3.771 10.582 2.082 2.256 5.641 4.556 10.583 6.614 4.729 2.083 7.938
3.968 9.65 5.485 1.691 1.52 2.45 3.405 2.45 5.641 0 5.506-3.772 8.349-11.146
8.349-5.682 0-10.776-1.866-15.333-5.638v10.189c4.121 2.475 9.066 3.601 14.53
3.601 7.005-.368 12.49-2.081 16.264-5.486l.047-.022zm45.019-56.73c-8.893
0-16.048 2.647-21.361 7.764-5.638 5.641-8.674 13.599-8.674 23.813 0 8.891
2.452 16.265 7.547 21.751 5.313 5.68 12.295 8.521 21.187 8.521 9.107 0
16.048-2.861 21.36-8.545 5.313-5.637 7.958-13.227 7.958-22.683
0-9.65-2.472-17.022-7.374-22.509-5.115-5.487-11.927-8.133-20.601-8.133l-.042.021zm18.345
31.012c0 7.198-1.518 12.859-4.727 16.654-3.21 3.969-7.938 6.071-14.184
6.071-6.074 0-10.778-2.104-14.379-6.071-3.577-3.99-5.29-9.456-5.29-16.654
0-7.59 1.888-13.444 5.683-17.393 3.576-3.773 8.306-5.682 14.182-5.682 5.854
0 10.561 1.907 13.964 5.682 3.037 4.163 4.729 9.824 4.729
17.393h.022zm25.547 29.513h9.433v-51.068h13.813v-7.938H428.93v-9.108c0-8.282
3.208-12.446 9.845-12.446 2.234 0 4.511.563 6.203
1.518v-8.521c-1.692-.759-3.969-.932-6.812-.932-5.095 0-9.258 1.519-12.664
4.727-3.969 3.773-6.071 8.674-6.071
15.312v9.672h-9.978v7.936h9.978v50.876l.067-.028zm38.75-16.091c0 11.538
5.098 17.414 15.506 17.414 3.774 0 6.614-.606 8.891-1.951v-8.11c-1.734
1.302-3.795 1.91-6.071 1.91-3.208
0-5.464-.762-6.788-2.475-1.345-1.689-2.103-4.554-2.103-8.501v-33.286h14.961v-7.938h-14.961v-17.39c-3.253
1.127-6.44 2.082-9.456
3.034v14.355h-10.192v7.938h10.192v34.979l.021.021zm1014.88
108.73c-3.209-3.034-7.004-4.553-11.709-4.553-4.77 0-8.719 1.519-11.928
4.771-3.209 3.188-4.729 7.155-4.729 11.711 0 4.728 1.52 8.675 4.705 11.709
3.211 3.036 7.156 4.556 11.928 4.556 4.705 0 8.674-1.52 11.928-4.729
3.188-3.253 4.879-7.004
4.879-11.709-.174-4.771-1.887-8.719-5.096-11.754l.022-.002zm-1.517
22.338c-2.82 2.818-6.246 4.119-10.41 4.119-4.119
0-7.545-1.301-10.408-4.119-2.818-2.863-4.338-6.441-4.338-10.627 0-4.121
1.301-7.545 4.164-10.408 2.818-2.817 6.225-4.121 10.582-4.121 4.121 0 7.549
1.304 10.41 4.121 2.818 2.863 4.336 6.287 4.336 10.408 0 4.382-1.301
7.764-4.336 10.627zm-8.502-9.651c1.691-.39 3.037-1.149 3.969-2.081.955-.977
1.303-2.301 1.303-3.815
0-1.692-.543-3.037-1.691-3.969-1.301-1.085-3.404-1.671-6.07-1.671h-6.029v21.164h3.037v-8.891h1.885c1.303
0 2.604 1.3 3.773 3.773l2.254
5.096h3.602l-2.818-5.683c-.977-2.472-2.105-3.601-3.252-3.97l.037.047zm-2.082-1.907h-3.252v-7.155h2.668c1.887
0 3.209.345 3.969.932.758.563.932 1.301.932 2.646 0 2.45-1.518 3.579-4.336
3.579l.019-.002zM933.443
816.353h2.646v-21.187h7.002v-2.646h-16.652v2.646h7.006v21.187h-.002zm16.047-15.917c0-2.062
0-3.753-.152-4.705.174 1.126.564 1.887.738 2.45l8.133
18.172h1.301l8.152-18.347c.219-.563.393-1.301.76-2.275-.174 1.887-.174
3.401-.174 4.553v16.048h2.82V792.52h-3.406l-7.371 16.438c-.174.587-.762
1.734-1.129
3.037h-.217c-.152-.761-.541-1.519-1.084-2.818l-7.373-16.655h-3.816v23.854h2.666v-15.917l.152-.023z" />
</svg>

View File

@ -0,0 +1,19 @@
import Postgres from "./Postgres.svelte"
import DynamoDB from "./DynamoDB.svelte"
import Elasticsearch from "./Elasticsearch.svelte"
import MongoDB from "./MongoDB.svelte"
import CouchDB from "./CouchDB.svelte"
import S3 from "./S3.svelte"
import Airtable from "./Airtable.svelte"
import SqlServer from "./SQLServer.svelte"
export default {
POSTGRES: Postgres,
DYNAMODB: DynamoDB,
MONGODB: MongoDB,
ELASTICSEARCH: Elasticsearch,
COUCHDB: CouchDB,
SQL_SERVER: SqlServer,
S3: S3,
AIRTABLE: Airtable,
}

View File

@ -0,0 +1,61 @@
<script>
import { goto, params } from "@sveltech/routify"
import { backendUiStore, store } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { Input, Label, ModalContent, Button, Spacer } from "@budibase/bbui"
import TableIntegrationMenu from "../TableIntegrationMenu/index.svelte"
import analytics from "analytics"
let modal
let error = ""
let name
let source
let integration
let datasource
function checkValid(evt) {
const datasourceName = evt.target.value
if (
$backendUiStore.datasources?.some(
datasource => datasource.name === datasourceName
)
) {
error = `Datasource with name ${datasourceName} already exists. Please choose another name.`
return
}
error = ""
}
async function saveDatasource() {
const { type, ...config } = integration
// Create datasource
const response = await backendUiStore.actions.datasources.save({
name,
source: type,
config,
})
notifier.success(`Datasource ${name} created successfully.`)
analytics.captureEvent("Datasource Created", { name })
// Navigate to new datasource
$goto(`./datasource/${response._id}`)
}
</script>
<ModalContent
title="Create Datasource"
confirmText="Create"
onConfirm={saveDatasource}
disabled={error || !name}>
<Input
data-cy="datasource-name-input"
thin
label="Datasource Name"
on:input={checkValid}
bind:value={name}
{error} />
<Label grey extraSmall>Source</Label>
<TableIntegrationMenu bind:integration />
</ModalContent>

View File

@ -0,0 +1,61 @@
<script>
import { goto, params } from "@sveltech/routify"
import { backendUiStore, store } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { Input, Label, ModalContent, Button, Spacer } from "@budibase/bbui"
import TableIntegrationMenu from "../TableIntegrationMenu/index.svelte"
import analytics from "analytics"
let modal
let error = ""
let name
let source
let integration
let datasource
function checkValid(evt) {
const datasourceName = evt.target.value
if (
$backendUiStore.datasources?.some(
datasource => datasource.name === datasourceName
)
) {
error = `Datasource with name ${tableName} already exists. Please choose another name.`
return
}
error = ""
}
async function saveDatasource() {
const { type, ...config } = integration
// Create datasource
await backendUiStore.actions.datasources.save({
name,
source: type,
config,
})
notifier.success(`Datasource ${name} created successfully.`)
analytics.captureEvent("Datasource Created", { name })
// Navigate to new datasource
$goto(`./datasource/${datasource._id}`)
}
</script>
<ModalContent
title="Create Datasource"
confirmText="Create"
onConfirm={saveDatasource}
disabled={error || !name}>
<Input
data-cy="datasource-name-input"
thin
label="Datasource Name"
on:input={checkValid}
bind:value={name}
{error} />
<Label grey extraSmall>Create Integrated Table from External Source</Label>
<TableIntegrationMenu bind:integration />
</ModalContent>

View File

@ -0,0 +1,87 @@
<script>
import { backendUiStore, store, allScreens } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Input } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let datasource
let anchor
let dropdown
let confirmDeleteDialog
let error = ""
let originalName = datasource.name
let willBeDeleted
function hideEditor() {
dropdown?.hide()
}
function showModal() {
hideEditor()
confirmDeleteDialog.show()
}
async function deleteDatasource() {
await backendUiStore.actions.datasources.delete(datasource)
notifier.success("Datasource deleted")
hideEditor()
}
</script>
<div on:click|stopPropagation>
<div bind:this={anchor} class="icon" on:click={dropdown.show}>
<i class="ri-more-line" />
</div>
<DropdownMenu align="left" {anchor} bind:this={dropdown}>
<DropdownContainer>
<DropdownItem
icon="ri-delete-bin-line"
title="Delete"
on:click={showModal}
data-cy="delete-datasource" />
</DropdownContainer>
</DropdownMenu>
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
okText="Delete Datasource"
onOk={deleteDatasource}
title="Confirm Deletion">
Are you sure you wish to delete the datasource
<i>{datasource.name}?</i>
This action cannot be undone.
</ConfirmDialog>
<style>
div.icon {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
div.icon i {
font-size: 16px;
}
.actions {
padding: var(--spacing-xl);
display: grid;
grid-gap: var(--spacing-xl);
min-width: 400px;
}
h5 {
margin: 0;
font-weight: 500;
}
footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
}
</style>

View File

@ -0,0 +1,84 @@
<script>
import { backendUiStore, store, allScreens } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Input } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let query
let anchor
let dropdown
let confirmDeleteDialog
let error = ""
let willBeDeleted
function hideEditor() {
dropdown?.hide()
}
function showModal() {
hideEditor()
confirmDeleteDialog.show()
}
async function deleteQuery() {
await backendUiStore.actions.queries.delete(query)
notifier.success("Query deleted")
hideEditor()
}
</script>
<div on:click|stopPropagation>
<div bind:this={anchor} class="icon" on:click={dropdown.show}>
<i class="ri-more-line" />
</div>
<DropdownMenu align="left" {anchor} bind:this={dropdown}>
<DropdownContainer>
<DropdownItem
icon="ri-delete-bin-line"
title="Delete"
on:click={showModal}
data-cy="delete-datasource" />
</DropdownContainer>
</DropdownMenu>
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
okText="Delete Query"
onOk={deleteQuery}
title="Confirm Deletion">
Are you sure you wish to delete this query? This action cannot be undone.
</ConfirmDialog>
<style>
div.icon {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
div.icon i {
font-size: 16px;
}
.actions {
padding: var(--spacing-xl);
display: grid;
grid-gap: var(--spacing-xl);
min-width: 400px;
}
h5 {
margin: 0;
font-weight: 500;
}
footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
}
</style>

View File

@ -5,7 +5,7 @@
import api from "builderStore/api"
const BYTES_IN_MB = 1000000
const FILE_SIZE_LIMIT = BYTES_IN_MB * 1
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
export let files = []
export let dataImport = {

View File

@ -2,13 +2,11 @@
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import CreateTableModal from "./modals/CreateTableModal.svelte"
import EditTablePopover from "./popovers/EditTablePopover.svelte"
import EditViewPopover from "./popovers/EditViewPopover.svelte"
import { Modal } from "@budibase/bbui"
import { Switcher } from "@budibase/bbui"
import NavItem from "components/common/NavItem.svelte"
let modal
$: selectedView =
$backendUiStore.selectedView && $backendUiStore.selectedView.name
@ -34,10 +32,6 @@
</script>
{#if $backendUiStore.selectedDatabase && $backendUiStore.selectedDatabase._id}
<div class="title">
<h1>Tables</h1>
<i data-cy="new-table" on:click={modal.show} class="ri-add-circle-fill" />
</div>
<div class="hierarchy-items-container">
{#each $backendUiStore.tables as table, idx}
<NavItem
@ -64,27 +58,3 @@
{/each}
</div>
{/if}
<Modal bind:this={modal}>
<CreateTableModal />
</Modal>
<style>
.title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.title h1 {
font-size: var(--font-size-m);
font-weight: 500;
margin: 0;
}
.title i {
font-size: 20px;
}
.title i:hover {
cursor: pointer;
color: var(--blue);
}
</style>

View File

@ -2,7 +2,14 @@
import { goto } from "@sveltech/routify"
import { backendUiStore, store } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { Input, Label, ModalContent, Toggle } from "@budibase/bbui"
import {
Input,
Label,
ModalContent,
Button,
Spacer,
Toggle,
} from "@budibase/bbui"
import TableDataImport from "../TableDataImport.svelte"
import analytics from "analytics"
import screenTemplates from "builderStore/store/screenTemplates"

View File

@ -29,6 +29,8 @@
<i class="ri-arrow-right-s-line" />
</div>
{/if}
<slot name="icon" />
{#if icon}
<div class="icon"><i class={icon} /></div>
{/if}

View File

@ -6,7 +6,7 @@
import api from "builderStore/api"
import { notifier } from "builderStore/store/notifications"
import CreateWebhookDeploymentModal from "./CreateWebhookDeploymentModal.svelte"
import { hostingStore } from "builderStore"
import { store, hostingStore } from "builderStore"
const DeploymentStatus = {
SUCCESS: "SUCCESS",
@ -36,7 +36,9 @@
let errorReason
let poll
let deployments = []
let deploymentUrl = `${$hostingStore.appUrl}/${appId}`
let urlComponent =
$hostingStore.hostingInfo.type === "self" ? $store.url : `/${appId}`
let deploymentUrl = `${$hostingStore.appUrl}${urlComponent}`
const formatDate = (date, format) =>
Intl.DateTimeFormat("en-GB", DATE_OPTIONS[format]).format(date)

View File

@ -1,6 +1,7 @@
<html>
<head>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<link href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css" rel="stylesheet">
<style>
body, html {
height: 100% !important;

View File

@ -0,0 +1,113 @@
<script>
import groupBy from "lodash/fp/groupBy"
import {
Button,
TextArea,
Drawer,
Heading,
Spacer,
} from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let bindableProperties
export let value = ""
export let bindingDrawer
function addToText(readableBinding) {
value = value + `{{ ${readableBinding} }}`
}
let originalValue = value
$: dispatch("update", value)
export function cancel() {
dispatch("update", originalValue)
bindingDrawer.close()
}
$: ({ instance, context } = groupBy("type", bindableProperties))
</script>
<div class="drawer-contents">
<div class="container" data-cy="binding-dropdown-modal">
<div class="list">
<Heading extraSmall>Objects</Heading>
<Spacer medium />
{#if context}
<Heading extraSmall>Tables</Heading>
<ul>
{#each context as { readableBinding }}
<li on:click={() => addToText(readableBinding)}>{readableBinding}</li>
{/each}
</ul>
{/if}
{#if instance}
<Heading extraSmall>Components</Heading>
<ul>
{#each instance as { readableBinding }}
<li on:click={() => addToText(readableBinding)}>{readableBinding}</li>
{/each}
</ul>
{/if}
</div>
<div class="text">
<TextArea
thin
bind:value
placeholder="Add text, or click the objects on the left to add them to the
textbox." />
</div>
</div>
</div>
<style>
.container {
height: 100%;
display: grid;
grid-template-columns: auto 1fr;
}
.list {
width: 150px;
border-right: 1.5px solid var(--grey-4);
padding: var(--spacing-s);
}
.text {
padding: var(--spacing-s);
font-family: var(--font-sans);
}
.text :global(p) {
margin: 0;
}
ul {
list-style: none;
padding-left: 0;
margin: 0;
padding: 0;
}
li {
display: flex;
font-family: var(--font-sans);
font-size: var(--font-size-xs);
color: var(--grey-7);
padding: var(--spacing-s) 0;
margin: auto 0px;
align-items: center;
cursor: pointer;
}
li:hover {
color: var(--ink);
font-weight: 500;
}
li:active {
color: var(--blue);
}
.drawer-contents {
height: 40vh;
overflow-y: auto;
}
</style>

View File

@ -1,142 +0,0 @@
<script>
import groupBy from "lodash/fp/groupBy"
import { Button, TextArea, Body, Heading, Spacer } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let bindableProperties
export let value = ""
export let close
function addToText(readableBinding) {
value = value + `{{ ${readableBinding} }}`
}
let originalValue = value
$: dispatch("update", value)
function cancel() {
dispatch("update", originalValue)
close()
}
$: ({ instance, context } = groupBy("type", bindableProperties))
$: hasBindableProperties = !!bindableProperties?.length
</script>
<div class="container" data-cy="binding-dropdown-modal">
<div class="list">
{#if hasBindableProperties}
{#if context}
<Heading extraSmall>Datasources</Heading>
<Spacer small />
<ul>
{#each context as { readableBinding }}
<li on:click={() => addToText(readableBinding)}>
{readableBinding}
</li>
{/each}
</ul>
{/if}
{#if instance}
<Spacer medium />
<Heading extraSmall>Components</Heading>
<Spacer small />
<ul>
{#each instance as { readableBinding }}
<li on:click={() => addToText(readableBinding)}>
{readableBinding}
</li>
{/each}
</ul>
{/if}
{:else}
<div class="empty">There aren't any bindable properties available.</div>
{/if}
</div>
<div class="text">
<Heading extraSmall>Data Binding</Heading>
<Spacer small />
<Body extraSmall lh>
Binding connects one piece of data to another and makes it dynamic. Click
the objects on the left to add them to the extbox.
</Body>
<Spacer large />
<TextArea
thin
bind:value
placeholder="Add text, or click the objects on the left to add them to the textbox." />
<div class="controls">
<a href="https://docs.budibase.com/design/binding">
<Body small grey>Learn more about binding</Body>
</a>
<Button on:click={cancel} secondary>Cancel</Button>
<Button on:click={close} primary>Done</Button>
</div>
</div>
</div>
<style>
.container {
display: grid;
grid-template-columns: auto auto;
}
.list,
.text {
padding: var(--spacing-m);
}
.controls {
margin-top: var(--spacing-m);
display: grid;
align-items: center;
grid-gap: var(--spacing-l);
grid-template-columns: 1fr auto auto;
}
.list {
width: 200px;
border-right: 1.5px solid var(--grey-4);
padding: var(--spacing-xl);
max-height: 300px;
overflow: auto;
}
.text {
width: 600px;
padding: var(--spacing-xl);
font-family: var(--font-sans);
}
.text :global(p) {
margin: 0;
}
.text :global(textarea) {
min-height: 50px;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
li {
display: flex;
font-family: var(--font-sans);
font-size: var(--font-size-xs);
color: var(--grey-7);
padding: var(--spacing-xs) 0;
margin: auto 0px;
align-items: center;
cursor: pointer;
}
li:hover {
color: var(--ink);
}
li:active {
color: var(--blue);
}
div.empty {
font-size: var(--font-size-xs);
color: var(--grey-5);
}
</style>

View File

@ -0,0 +1,168 @@
<script>
import {
Button,
TextButton,
Body,
DropdownMenu,
ModalContent,
} from "@budibase/bbui"
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
import actionTypes from "./actions"
import { createEventDispatcher } from "svelte"
import { automationStore } from "builderStore"
const EVENT_TYPE_KEY = "##eventHandlerType"
export let event
let addActionButton
let addActionDropdown
let selectedAction
$: actions = event || []
$: selectedActionComponent =
selectedAction &&
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY]).component
const deleteAction = index => {
actions.splice(index, 1)
actions = actions
}
const addAction = actionType => () => {
const newAction = {
parameters: {},
[EVENT_TYPE_KEY]: actionType.name,
}
actions.push(newAction)
selectedAction = newAction
actions = actions
addActionDropdown.hide()
}
const selectAction = action => () => {
selectedAction = action
}
</script>
<div class="actions-container">
<div class="actions-list">
<div>
<div bind:this={addActionButton}>
<TextButton text small blue on:click={addActionDropdown.show}>
<div style="height: 20px; width: 20px;">
<AddIcon />
</div>
Add Action
</TextButton>
</div>
<DropdownMenu
bind:this={addActionDropdown}
anchor={addActionButton}
align="right">
<div class="available-actions-container">
{#each actionTypes as actionType}
<div class="available-action" on:click={addAction(actionType)}>
<span>{actionType.name}</span>
</div>
{/each}
</div>
</DropdownMenu>
</div>
{#if actions && actions.length > 0}
{#each actions as action, index}
<div class="action-container">
<div class="action-header" on:click={selectAction(action)}>
<span class:selected={action === selectedAction}>
{index + 1}.
{action[EVENT_TYPE_KEY]}
</span>
</div>
<i
class="ri-close-fill"
style="margin-left: var(--spacing-m);"
on:click={() => deleteAction(index)} />
</div>
{/each}
{/if}
</div>
<div class="action-config">
{#if selectedAction}
<div class="selected-action-container">
<svelte:component
this={selectedActionComponent}
parameters={selectedAction.parameters} />
</div>
{/if}
</div>
</div>
<style>
.action-header {
display: flex;
flex-direction: row;
align-items: center;
margin-top: var(--spacing-m);
}
.action-header > span {
margin-bottom: var(--spacing-m);
font-size: var(--font-size-s);
}
.action-header > span:hover,
.selected {
cursor: pointer;
font-weight: 500;
}
.actions-list {
border: var(--border-light);
padding: var(--spacing-s);
}
.available-action {
padding: var(--spacing-s);
font-size: var(--font-size-m);
cursor: pointer;
}
.available-action:hover {
background: var(--grey-2);
}
.actions-container {
height: 40vh;
display: grid;
grid-gap: var(--spacing-m);
grid-template-columns: 15% 1fr;
grid-auto-flow: column;
min-height: 0;
padding-top: 0;
overflow-y: auto;
}
.action-container {
border: var(--border-light);
border-width: 1px 0 0 0;
display: flex;
align-items: center;
}
.selected-action-container {
padding-bottom: var(--spacing-s);
padding-top: var(--spacing-s);
}
a {
flex: 1;
color: var(--grey-5);
font-size: var(--font-size-s);
text-decoration: none;
}
a:hover {
color: var(--blue);
}
</style>

View File

@ -54,25 +54,29 @@
const createAutomation = async parameters => {
if (parameters.automationId || !parameters.newAutomationName) return
await automationStore.actions.create({name: parameters.newAutomationName})
await automationStore.actions.create({ name: parameters.newAutomationName })
const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP
const newBlock = $automationStore.selectedAutomation.constructBlock(
"TRIGGER", "APP", appActionDefinition)
"TRIGGER",
"APP",
appActionDefinition
)
newBlock.inputs = {
fields: Object.entries(parameters.fields).reduce((fields, [key, value]) => {
fields: Object.entries(parameters.fields).reduce(
(fields, [key, value]) => {
fields[key] = value.type
return fields
}, {})
},
{}
),
}
automationStore.actions.addBlockToAutomation(newBlock)
await automationStore.actions.save(
$automationStore.selectedAutomation)
await automationStore.actions.save($automationStore.selectedAutomation)
parameters.automationId = $automationStore.selectedAutomation.automation._id
delete parameters.newAutomationName

View File

@ -1,17 +1,68 @@
<script>
import { Button, Modal } from "@budibase/bbui"
import EventEditorModal from "./EventEditorModal.svelte"
import { Button, Drawer } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { notifier } from "builderStore/store/notifications"
import EventEditor from "./EventEditor.svelte"
import { automationStore } from "builderStore"
const dispatch = createEventDispatcher()
export let value
export let name
let modal
let drawer
const saveEventData = async () => {
// any automations that need created from event triggers
const automationsToCreate = value.filter(
action => action["##eventHandlerType"] === "Trigger Automation"
)
automationsToCreate.forEach(action => createAutomation(action.parameters))
dispatch("change", value)
notifier.success("Component actions saved.")
drawer.hide()
}
// called by the parent modal when actions are saved
const createAutomation = async parameters => {
if (parameters.automationId || !parameters.newAutomationName) return
await automationStore.actions.create({ name: parameters.newAutomationName })
const appActionDefinition = $automationStore.blockDefinitions.TRIGGER.APP
const newBlock = $automationStore.selectedAutomation.constructBlock(
"TRIGGER",
"APP",
appActionDefinition
)
newBlock.inputs = {
fields: Object.entries(parameters.fields).reduce(
(fields, [key, value]) => {
fields[key] = value.type
return fields
},
{}
),
}
automationStore.actions.addBlockToAutomation(newBlock)
await automationStore.actions.save($automationStore.selectedAutomation)
parameters.automationId = $automationStore.selectedAutomation.automation._id
delete parameters.newAutomationName
}
</script>
<Button secondary small on:click={modal.show}>Define Actions</Button>
<Modal bind:this={modal} width="600px">
<EventEditorModal event={value} eventType={name} on:change />
</Modal>
<Button secondary small on:click={drawer.show}>Define Actions</Button>
<Drawer bind:this={drawer} title={'Actions'}>
<heading slot="buttons">
<Button thin blue on:click={saveEventData}>Save</Button>
</heading>
<div slot="body">
<EventEditor event={value} eventType={name} />
</div>
</Drawer>

View File

@ -0,0 +1,69 @@
<script>
import { Select, Label, Spacer } from "@budibase/bbui"
import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import ParameterBuilder from "../../../../../integration/QueryParameterBuilder.svelte"
export let parameters
$: datasource = $backendUiStore.datasources.find(
ds => ds._id === parameters.datasourceId
)
// TODO: binding needs to be centralised
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
queries: $backendUiStore.queries,
}).map(property => ({
...property,
category: property.type === "instance" ? "Component" : "Table",
label: property.readableBinding,
path: property.readableBinding,
}))
$: query =
parameters.queryId &&
$backendUiStore.queries.find(query => query._id === parameters.queryId)
</script>
<div class="root">
<Label size="m" color="dark">Datasource</Label>
<Select thin secondary bind:value={parameters.datasourceId}>
<option value="" />
{#each $backendUiStore.datasources as datasource}
<option value={datasource._id}>{datasource.name}</option>
{/each}
</Select>
<Spacer medium />
{#if parameters.datasourceId}
<Label size="m" color="dark">Query</Label>
<Select thin secondary bind:value={parameters.queryId}>
<option value="" />
{#each $backendUiStore.queries.filter(query => query.datasourceId === datasource._id) as query}
<option value={query._id}>{query.name}</option>
{/each}
</Select>
{/if}
<Spacer medium />
{#if query?.parameters?.length > 0}
<ParameterBuilder
bind:customParams={parameters.queryParams}
parameters={query.parameters}
bindings={bindableProperties} />
{#if query.fields.sql}
<pre>{query.fields.queryString}</pre>
{/if}
{/if}
</div>
<style>
.root {
padding: var(--spacing-m);
}
</style>

View File

@ -3,9 +3,16 @@
import { automationStore } from "builderStore"
import SaveFields from "./SaveFields.svelte"
const AUTOMATION_STATUS = {
NEW: "new",
EXISTING: "existing",
}
export let parameters = {}
let newOrExisting = parameters.automationId ? "existing" : "new"
let automationStatus = parameters.automationId
? AUTOMATION_STATUS.EXISTING
: AUTOMATION_STATUS.NEW
$: automations = $automationStore.automations
.filter(a => a.definition.trigger?.stepId === "APP")
@ -33,12 +40,12 @@
}
const setNew = () => {
newOrExisting = "new"
automationStatus = AUTOMATION_STATUS.NEW
parameters.automationId = undefined
}
const setExisting = () => {
newOrExisting = "existing"
automationStatus = AUTOMATION_STATUS.EXISTING
parameters.newAutomationName = ""
}
</script>
@ -47,8 +54,8 @@
<div class="radio-container" on:click={setNew}>
<input
type="radio"
value="new"
bind:group={newOrExisting}
value={AUTOMATION_STATUS.NEW}
bind:group={automationStatus}
disabled={!hasAutomations} />
<Label disabled={!hasAutomations}>Create a new automation</Label>
@ -57,8 +64,8 @@
<div class="radio-container" on:click={setExisting}>
<input
type="radio"
value="existing"
bind:group={newOrExisting}
value={AUTOMATION_STATUS.EXISTING}
bind:group={automationStatus}
disabled={!hasAutomations} />
<Label disabled={!hasAutomations}>Use an existing automation</Label>
@ -66,7 +73,7 @@
<Label size="m" color="dark">Automation</Label>
{#if newOrExisting === 'existing'}
{#if automationStatus === AUTOMATION_STATUS.EXISTING}
<Select
secondary
bind:value={parameters.automationId}
@ -84,8 +91,7 @@
{/if}
<SaveFields
parameterFields={parameters.fields}
schemaFields={newOrExisting === 'existing' && selectedAutomation && selectedAutomation.schema}
schemaFields={automationStatus === AUTOMATION_STATUS.EXISTING && selectedAutomation && selectedAutomation.schema}
fieldLabel="Field"
on:fieldschanged={onFieldsChanged} />
</div>

View File

@ -1,6 +1,7 @@
import NavigateTo from "./NavigateTo.svelte"
import SaveRow from "./SaveRow.svelte"
import DeleteRow from "./DeleteRow.svelte"
import ExecuteQuery from "./ExecuteQuery.svelte"
import TriggerAutomation from "./TriggerAutomation.svelte"
// defines what actions are available, when adding a new one
@ -21,6 +22,10 @@ export default [
name: "Navigate To",
component: NavigateTo,
},
{
name: "Execute Query",
component: ExecuteQuery,
},
{
name: "Trigger Automation",
component: TriggerAutomation,

View File

@ -1,9 +1,15 @@
<script context="module">
import iconData from "./icons.js"
const categories = Object.keys(iconData)
const icons = Object.keys(iconData).reduce((acc, cat) => [...acc, ...Object.keys(iconData[cat])], [])
</script>
<script>
import { DropdownMenu, Button, Input } from "@budibase/bbui"
import { createEventDispatcher, tick } from "svelte"
import icons from "./icons.js"
const dispatch = createEventDispatcher()
export let value = ""
@ -45,12 +51,13 @@
"Y",
"Z",
]
let buttonAnchor, dropdown
let loading = false
function findIconByTerm(term) {
const r = new RegExp(`\^${term}`, "i")
return icons.filter(i => r.test(i.label))
return icons.filter(i => r.test(i))
}
async function switchLetter(letter) {
@ -62,7 +69,6 @@
await tick() //svg icons do not update without tick
loading = false
}
async function findIconOnPage() {
loading = true
const iconIdx = filteredIcons.findIndex(i => i.value === value)
@ -100,7 +106,7 @@
loading = false
}
$: displayValue = value ? value.substring(7) : "Pick Icon"
$: displayValue = value ? value.substring(3) : "Pick Icon"
$: totalPages = Math.ceil(filteredIcons.length / maxIconsPerPage)
$: pageEndIdx = maxIconsPerPage * currentPage
@ -138,11 +144,11 @@
<div class="page-area">
<div class="pager">
<span on:click={() => pageClick(false)}>
<i class="page-btn fas fa-chevron-left" />
<i class="page-btn ri-arrow-left-line ri-sm" />
</span>
<span>{pagerText}</span>
<span on:click={() => pageClick(true)}>
<i class="page-btn fas fa-chevron-right" />
<i class="page-btn ri-arrow-right-line ri-sm" />
</span>
</div>
</div>
@ -153,12 +159,21 @@
{#each pagedIcons as icon}
<div
class="icon-container"
class:selected={value === icon.value}
on:click={() => (value = icon.value)}>
class:selected={value === `ri-${icon}-fill`}
on:click={() => (value = `ri-${icon}-fill`)}>
<div class="icon-preview">
<i class={`${icon.value} fa-3x`} />
<i class={`ri-${icon}-fill ri-xl`} />
</div>
<div class="icon-label">{icon.label}</div>
<div class="icon-label">{icon}-fill</div>
</div>
<div
class="icon-container"
class:selected={value === `ri-${icon}-line`}
on:click={() => (value = `ri-${icon}-line`)}>
<div class="icon-preview">
<i class={`ri-${icon}-line ri-xl`} />
</div>
<div class="icon-label">{icon}-line</div>
</div>
{/each}
{/if}
@ -182,13 +197,11 @@
padding: 10px 0px 10px 15px;
overflow-x: hidden;
}
.search-area {
flex: 0 0 80px;
display: flex;
flex-direction: column;
}
.icon-area {
flex: 1;
display: grid;
@ -199,13 +212,11 @@
overflow-x: hidden;
padding-right: 10px;
}
.no-icons {
display: flex;
justify-content: center;
align-items: center;
}
.alphabet-area {
display: flex;
flex-flow: row wrap;
@ -213,44 +224,36 @@
padding-right: 15px;
justify-content: space-around;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
}
.search-input {
display: flex;
flex-flow: row nowrap;
width: 100%;
padding-right: 15px;
}
.input-wrapper {
width: 510px;
margin-right: 5px;
}
.page-area {
padding: 10px;
display: flex;
justify-content: center;
}
.letter {
color: var(--blue);
}
.letter:hover {
cursor: pointer;
text-decoration: underline;
}
.letter-selected {
text-decoration: underline;
}
.icon-container {
height: 100px;
display: flex;
@ -258,33 +261,27 @@
flex-direction: column;
border: var(--border-dark);
}
.icon-container:hover {
cursor: pointer;
background: var(--grey-2);
}
.selected {
background: var(--grey-3);
}
.icon-preview {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.icon-label {
flex: 0 0 20px;
text-align: center;
font-size: 12px;
}
.page-btn {
color: var(--blue);
}
.page-btn:hover {
cursor: pointer;
}

View File

@ -1,4 +1 @@
import "@fortawesome/fontawesome-free/js/all.js"
import IconSelect from "./IconSelect.svelte"
export default IconSelect
export { default as IconSelect } from "./IconSelect.svelte"

View File

@ -1,13 +1,12 @@
<script>
import { Icon } from "@budibase/bbui"
import { Button, Icon, Drawer, Body } from "@budibase/bbui"
import { store, currentAsset } from "builderStore"
import {
getBindableProperties,
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import { DropdownMenu } from "@budibase/bbui"
import BindingDropdown from "./BindingDropdown.svelte"
import BindingPanel from "components/design/PropertiesPanel/BindingPanel.svelte"
export let label = ""
export let bindable = true
@ -19,9 +18,9 @@
export let props = {}
export let onChange = () => {}
let bindingDrawer
let temporaryBindableValue = value
let anchor
let dropdown
$: bindableProperties = getBindableProperties(
$currentAsset.props,
@ -30,6 +29,11 @@
$: safeValue = getSafeValue(value, props.defaultValue, bindableProperties)
$: replaceBindings = val => readableToRuntimeBinding(bindableProperties, val)
const handleClose = () => {
handleChange(temporaryBindableValue)
bindingDrawer.hide()
}
// Handle a value change of any type
// String values have any bindings handled
const handleChange = value => {
@ -74,24 +78,28 @@
<div
class="icon"
data-cy={`${key}-binding-button`}
on:click={dropdown.show}>
on:click={bindingDrawer.show}>
<Icon name="edit" />
</div>
{/if}
</div>
{#if type === 'text'}
<DropdownMenu
on:close={() => handleChange(temporaryBindableValue)}
bind:this={dropdown}
{anchor}
align="right">
<BindingDropdown
<Drawer bind:this={bindingDrawer} title="Bindings">
<div slot="description">
<Body extraSmall grey>
Add the objects on the left to enrich your text.
</Body>
</div>
<heading slot="buttons">
<Button thin blue on:click={handleClose}>Save</Button>
</heading>
<div slot="body">
<BindingPanel
value={safeValue}
close={dropdown.hide}
close={handleClose}
on:update={e => (temporaryBindableValue = e.detail)}
{bindableProperties} />
</DropdownMenu>
{/if}
</div>
</Drawer>
<style>
.property-control {
@ -100,6 +108,7 @@
flex-flow: row;
align-items: center;
}
.label {
display: flex;
align-items: center;

View File

@ -9,17 +9,20 @@
export let multiselect = false
const tables = $backendUiStore.tables
const queries = $backendUiStore.queries
let options = []
$: table = componentInstance.datasource
$: table =
componentInstance.datasource?.type === "table"
? tables.find(m => m._id === componentInstance.datasource.tableId)
: null
: queries.find(query => query._id === componentInstance.datasource._id)
$: type = componentInstance.datasource.type
$: if (table) {
options =
type === "table" || type === "link"
type === "table" || type === "link" || type === "query"
? Object.keys(table.schema)
: Object.keys(table.views[componentInstance.datasource.name].schema)
}

View File

@ -1,25 +1,30 @@
<script>
import { Icon, DropdownMenu, Heading } from "@budibase/bbui"
import { getBindableProperties } from "builderStore/dataBinding"
import {
Button,
Icon,
DropdownMenu,
Spacer,
Heading,
Drawer,
} from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { store, backendUiStore, currentAsset } from "builderStore"
import { getBindableProperties } from "builderStore/dataBinding"
import { notifier } from "builderStore/store/notifications"
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
import IntegrationQueryEditor from "components/integration/index.svelte"
const dispatch = createEventDispatcher()
let anchorRight, dropdownRight
let drawer
export let value = {}
function handleSelected(selected) {
dispatch("change", selected)
dropdownRight.hide()
}
$: tables = $backendUiStore.tables.map(m => ({
label: m.name,
tableId: m._id,
type: "table",
}))
$: views = $backendUiStore.tables.reduce((acc, cur) => {
let viewsArr = Object.entries(cur.views).map(([key, value]) => ({
label: key,
@ -29,12 +34,24 @@
}))
return [...acc, ...viewsArr]
}, [])
$: queries = $backendUiStore.queries.map(query => ({
label: query.name,
name: query.name,
...query,
schema: query.schema,
parameters: query.parameters,
type: "query",
}))
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$store.selectedComponentId
)
$: queryBindableProperties = bindableProperties.map(property => ({
...property,
category: property.type === "instance" ? "Component" : "Table",
label: property.readableBinding,
path: property.readableBinding,
}))
$: links = bindableProperties
.filter(x => x.fieldSchema?.type === "link")
.map(property => {
@ -47,15 +64,59 @@
type: "link",
}
})
function handleSelected(selected) {
dispatch("change", selected)
dropdownRight.hide()
}
function fetchDatasourceSchema(query) {
const source = $backendUiStore.datasources.find(
ds => ds._id === query.datasourceId
).source
return $backendUiStore.integrations[source].query[query.queryVerb][
query.queryType
]
}
</script>
<div
class="dropdownbutton"
bind:this={anchorRight}
on:click={dropdownRight.show}>
<span>{value && value.label ? value.label : 'Table / View'}</span>
<span>{value.label ? value.label : 'Table / View / Query'}</span>
<Icon name="arrowdown" />
</div>
{#if value.type === 'query'}
<i class="ri-settings-5-line" on:click={drawer.show} />
<Drawer title={'Query'}>
<div slot="buttons">
<Button
blue
thin
on:click={() => {
notifier.success('Query parameters saved.')
handleSelected(value)
drawer.hide()
}}>
Save
</Button>
</div>
<div class="drawer-contents" slot="body">
<IntegrationQueryEditor
query={value}
schema={fetchDatasourceSchema(value)}
editable={false} />
<Spacer large />
{#if value.parameters.length > 0}
<ParameterBuilder
bind:customParams={value.queryParams}
parameters={queries.find(query => query._id === value._id).parameters}
bindings={queryBindableProperties} />
{/if}
</div>
</Drawer>
{/if}
<DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
<div class="dropdown">
<div class="title">
@ -96,6 +157,20 @@
</li>
{/each}
</ul>
<hr />
<div class="title">
<Heading extraSmall>Queries</Heading>
</div>
<ul>
{#each queries as query}
<li
class:selected={value === query}
on:click={() => handleSelected(query)}>
{query.label}
</li>
{/each}
</ul>
</div>
</DropdownMenu>
@ -160,4 +235,23 @@
li:hover {
background-color: var(--grey-4);
}
.drawer-contents {
padding: var(--spacing-xl);
height: 40vh;
overflow-y: auto;
}
i {
margin-left: 5px;
display: flex;
align-items: center;
transition: all 0.2s;
}
i:hover {
transform: scale(1.1);
font-weight: 500;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,162 @@
<script>
import CodeMirror from "./codemirror"
import { onMount, createEventDispatcher } from "svelte"
import { themeStore } from "builderStore"
const dispatch = createEventDispatcher()
const THEMES = {
DARK: "tomorrow-night-eighties",
LIGHT: "default",
}
export let value = ""
export let readOnly = false
export let lineNumbers = true
export let tab = true
export let mode
let width
let height
// We have to expose set and update methods, rather
// than making this state-driven through props,
// because it's difficult to update an editor
// without resetting scroll otherwise
export async function set(new_value, new_mode) {
if (new_mode !== mode) {
await createEditor((mode = new_mode))
}
value = new_value
updating_externally = true
if (editor) editor.setValue(value)
updating_externally = false
}
export function update(new_value) {
value = new_value
if (editor) {
const { left, top } = editor.getScrollInfo()
editor.setValue((value = new_value))
editor.scrollTo(left, top)
}
}
export function resize() {
editor.refresh()
}
export function focus() {
editor.focus()
}
const modes = {
js: {
name: "javascript",
json: false,
},
json: {
name: "javascript",
json: true,
},
sql: {
name: "sql",
},
svelte: {
name: "handlebars",
base: "text/html",
},
}
const refs = {}
let editor
let updating_externally = false
let marker
let error_line
let destroyed = false
$: if (editor && width && height) {
editor.refresh()
}
onMount(() => {
createEditor(mode).then(() => {
if (editor) editor.setValue(value || "")
})
return () => {
destroyed = true
if (editor) editor.toTextArea()
}
})
let first = true
async function createEditor(mode) {
if (destroyed || !CodeMirror) return
if (editor) editor.toTextArea()
const opts = {
lineNumbers,
lineWrapping: true,
indentWithTabs: true,
indentUnit: 2,
tabSize: 2,
value: "",
mode: modes[mode] || {
name: mode,
},
readOnly,
autoCloseBrackets: true,
autoCloseTags: true,
theme: $themeStore.darkMode ? THEMES.DARK : THEMES.LIGHT,
}
if (!tab)
opts.extraKeys = {
Tab: tab,
"Shift-Tab": tab,
}
// Creating a text editor is a lot of work, so we yield
// the main thread for a moment. This helps reduce jank
if (first) await sleep(50)
if (destroyed) return
editor = CodeMirror.fromTextArea(refs.editor, opts)
editor.on("change", instance => {
if (!updating_externally) {
const value = instance.getValue()
dispatch("change", { value })
}
})
if (first) await sleep(50)
editor.refresh()
first = false
}
function sleep(ms) {
return new Promise(fulfil => setTimeout(fulfil, ms))
}
</script>
<textarea tabindex="0" bind:this={refs.editor} readonly {value} />
<style>
textarea {
visibility: hidden;
}
:global(.CodeMirror) {
height: auto !important;
border-radius: var(--border-radius-m);
font-family: var(--font-sans) !important;
}
</style>

View File

@ -0,0 +1,56 @@
<script>
import {
Button,
TextArea,
Label,
Input,
Heading,
Select,
} from "@budibase/bbui"
import Editor from "./QueryEditor.svelte"
export let fields = {}
export let schema
export let editable
let draftField = {}
$: fieldKeys = Object.keys(fields)
$: schemaKeys = Object.keys(schema.fields)
function updateCustomFields({ detail }) {
fields.customData = detail.value
}
</script>
<form on:submit|preventDefault>
{#each schemaKeys as field}
<Label extraSmall grey>{field}</Label>
<div class="field">
<Input
disabled={!editable}
type={schema.fields[field]?.type}
required={schema.fields[field]?.required}
bind:value={fields[field]} />
</div>
{/each}
</form>
<Label extraSmall grey>Data</Label>
{#if schema.customisable}
<Editor
label="Query"
mode="json"
on:change={updateCustomFields}
readOnly={!editable}
value={fields.customData} />
{/if}
<style>
.field {
margin-bottom: var(--spacing-m);
display: grid;
grid-template-columns: 1fr 2%;
grid-gap: var(--spacing-m);
align-items: center;
}
</style>

View File

@ -0,0 +1,88 @@
<script>
import { Button, TextArea, Label, Input, Heading } from "@budibase/bbui"
import BindableInput from "components/userInterface/BindableInput.svelte"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/replaceBindings"
export let bindable = true
export let parameters = []
export let bindings = []
export let customParams = {}
function newQueryParameter() {
parameters = [...parameters, {}]
}
function deleteQueryParameter(idx) {
parameters.splice(idx, 1)
parameters = parameters
}
// This is necessary due to the way readable and writable bindings are stored.
// The readable binding in the UI gets converted to a UUID value that the client understands
// for parsing, then converted back so we can display it the readable form in the UI
function onBindingChange(param, valueToParse) {
const parsedBindingValue = readableToRuntimeBinding(bindings, valueToParse)
customParams[param] = parsedBindingValue
}
</script>
<section>
<Heading extraSmall black>Parameters</Heading>
<div class="parameters" class:bindable>
<Label extraSmall grey>Parameter Name</Label>
<Label extraSmall grey>Default</Label>
{#if bindable}
<Label extraSmall grey>Value</Label>
{:else}
<div />
{/if}
{#each parameters as parameter, idx}
<Input thin disabled={bindable} bind:value={parameter.name} />
<Input thin disabled={bindable} bind:value={parameter.default} />
{#if bindable}
<BindableInput
type="string"
thin
on:change={evt => onBindingChange(parameter.name, evt.detail)}
value={runtimeToReadableBinding(bindings, customParams[parameter.name])}
{bindings} />
{:else}
<i
class="ri-close-circle-line delete"
on:click={() => deleteQueryParameter(idx)} />
{/if}
{/each}
</div>
{#if !bindable}
<Button thin secondary small on:click={newQueryParameter}>
Add Parameter
</Button>
{/if}
</section>
<style>
.parameters.bindable {
grid-template-columns: 1fr 1fr 1fr;
}
.parameters {
display: grid;
grid-template-columns: 1fr 1fr 5%;
grid-gap: 10px;
align-items: center;
margin-bottom: var(--spacing-xl);
}
.delete {
transition: all 0.2s;
}
.delete:hover {
transform: scale(1.1);
font-weight: 500;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,284 @@
<script>
import { onMount } from "svelte"
import { goto } from "@sveltech/routify"
import {
Select,
Button,
Label,
Input,
TextArea,
Heading,
Spacer,
Switcher,
} from "@budibase/bbui"
import { notifier } from "builderStore/store/notifications"
import api from "builderStore/api"
import { FIELDS } from "constants/backend"
import IntegrationQueryEditor from "components/integration/index.svelte"
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
import { backendUiStore } from "builderStore"
const PREVIEW_HEADINGS = [
{
title: "JSON",
key: "JSON",
},
{
title: "Schema",
key: "SCHEMA",
},
{
title: "Preview",
key: "PREVIEW",
},
]
export let query
export let fields = []
let config
let tab = "JSON"
let parameters
let data = []
$: datasource = $backendUiStore.datasources.find(
ds => ds._id === query.datasourceId
)
$: query.schema = fields.reduce(
(acc, next) => ({
...acc,
[next.name]: {
name: next.name,
type: "string",
},
}),
{}
)
$: datasourceType = datasource?.source
$: config = $backendUiStore.integrations[datasourceType]?.query
$: docsLink = $backendUiStore.integrations[datasourceType]?.docs
$: shouldShowQueryConfig = config && query.queryVerb && query.queryType
function newField() {
fields = [...fields, {}]
}
function deleteField(idx) {
fields.splice(idx, 1)
fields = fields
}
async function previewQuery() {
try {
const response = await api.post(`/api/queries/preview`, {
fields: query.fields,
queryVerb: query.queryVerb,
parameters: query.parameters.reduce(
(acc, next) => ({
...acc,
[next.name]: next.default,
}),
{}
),
datasourceId: datasource._id,
})
const json = await response.json()
if (response.status !== 200) throw new Error(json.message)
data = json || []
if (data.length === 0) {
notifier.info(
"Query results empty. Please execute a query with results to create your schema."
)
return
}
notifier.success("Query executed successfully.")
// Assume all the fields are strings and create a basic schema
// from the first record returned by the query
fields = Object.keys(json[0]).map(field => ({
name: field,
type: "STRING",
}))
} catch (err) {
notifier.danger(`Query Error: ${err.message}`)
console.error(err)
}
}
async function saveQuery() {
try {
const { _id } = await backendUiStore.actions.queries.save(
query.datasourceId,
query
)
notifier.success(`Query saved successfully.`)
$goto(`../../${_id}`)
} catch (err) {
console.error(err)
notifier.danger(`Error creating query. ${err.message}`)
}
}
</script>
<header>
<Heading small>{query.name}</Heading>
{#if config}
<div class="queryVerbs">
{#each Object.keys(config) as queryVerb}
<div
class="queryVerb"
class:selected={queryVerb === query.queryVerb}
on:click={() => {
query.queryVerb = queryVerb
}}>
{queryVerb}
</div>
{/each}
</div>
{#if query.queryVerb}
<Select thin secondary bind:value={query.queryType}>
<option value={''}>Select an option</option>
{#each Object.keys(config[query.queryVerb]) as queryType}
<option value={queryType}>{queryType}</option>
{/each}
</Select>
{/if}
<Spacer medium />
<Button primary href={docsLink} target="_blank">
<i class="ri-book-2-line" />
</Button>
{/if}
</header>
<Spacer large />
{#if shouldShowQueryConfig}
<section>
<div class="config">
<Label extraSmall grey>Query Name</Label>
<Input thin bind:value={query.name} />
<Spacer medium />
<IntegrationQueryEditor
{query}
schema={config[query.queryVerb][query.queryType]}
bind:parameters />
<Spacer medium />
<div class="viewer-controls">
<Button
wide
thin
blue
disabled={data.length === 0}
on:click={saveQuery}>
Save
</Button>
<Button wide thin primary on:click={previewQuery}>Run</Button>
</div>
<section class="viewer">
{#if data}
<Switcher headings={PREVIEW_HEADINGS} bind:value={tab}>
{#if tab === 'JSON'}
<pre class="preview">{JSON.stringify(data[0], undefined, 2)}</pre>
{:else if tab === 'PREVIEW'}
<ExternalDataSourceTable {query} {data} />
{:else if tab === 'SCHEMA'}
{#each fields as field, idx}
<div class="field">
<Input thin type={'text'} bind:value={field.name} />
<Select secondary thin bind:value={field.type}>
<option value={''}>Select an option</option>
<option value={'STRING'}>Text</option>
<option value={'NUMBER'}>Number</option>
<option value={'BOOLEAN'}>Boolean</option>
<option value={'DATETIME'}>Datetime</option>
</Select>
<i
class="ri-close-circle-line delete"
on:click={() => deleteField(idx)} />
</div>
{/each}
<Button thin secondary on:click={newField}>Add Field</Button>
{/if}
</Switcher>
{/if}
</section>
</div>
</section>
{/if}
<style>
.field {
display: grid;
grid-gap: 10px;
grid-template-columns: 1fr 1fr 50px;
margin-bottom: var(--spacing-m);
}
a {
font-size: var(--font-size-s);
}
.config {
margin-bottom: var(--spacing-s);
}
.delete {
align-self: center;
cursor: pointer;
}
.preview {
width: 800px;
height: 100%;
overflow-y: auto;
overflow-wrap: break-word;
white-space: pre-wrap;
}
header {
display: flex;
align-items: center;
}
.queryVerbs {
display: flex;
flex: 1;
font-size: var(--font-size-m);
align-items: center;
margin-left: var(--spacing-l);
}
.queryVerb {
text-transform: capitalize;
margin-right: var(--spacing-m);
color: var(--grey-5);
cursor: pointer;
}
.selected {
color: var(--white);
font-weight: 500;
}
.viewer-controls {
display: grid;
grid-gap: var(--spacing-m);
grid-auto-flow: column;
direction: rtl;
grid-template-columns: 10% 10% 1fr;
margin-bottom: var(--spacing-m);
}
</style>

View File

@ -0,0 +1,10 @@
import CodeMirror from "codemirror"
import "codemirror/lib/codemirror.css"
import "codemirror/theme/tomorrow-night-eighties.css"
import "codemirror/theme/neo.css"
import "codemirror/mode/sql/sql"
import "codemirror/mode/css/css"
import "codemirror/mode/handlebars/handlebars"
import "codemirror/mode/javascript/javascript"
export default CodeMirror

View File

@ -0,0 +1,52 @@
<script>
import { onMount } from "svelte"
import { TextArea, Label, Input, Heading, Spacer } from "@budibase/bbui"
import Editor from "./QueryEditor.svelte"
import ParameterBuilder from "./QueryParameterBuilder.svelte"
import FieldsBuilder from "./QueryFieldsBuilder.svelte"
const QueryTypes = {
SQL: "sql",
JSON: "json",
FIELDS: "fields",
}
export let query
export let schema
export let editable = true
function updateQuery({ detail }) {
query.fields[schema.type] = detail.value
}
</script>
{#if editable}
<ParameterBuilder bind:parameters={query.parameters} bindable={false} />
<Spacer large />
{/if}
<Heading extraSmall black>Query</Heading>
<Spacer medium />
{#if schema}
{#key query._id}
{#if schema.type === QueryTypes.SQL}
<Editor
label="Query"
mode="sql"
on:change={updateQuery}
readOnly={!editable}
value={query.fields.sql} />
{:else if schema.type === QueryTypes.JSON}
<Spacer large />
<Editor
label="Query"
mode="json"
on:change={updateQuery}
readOnly={!editable}
value={query.fields.json} />
{:else if schema.type === QueryTypes.FIELDS}
<FieldsBuilder bind:fields={query.fields} {schema} {editable} />
{/if}
{/key}
{/if}

View File

@ -1,11 +1,20 @@
<script>
import { Input, TextArea, Button } from "@budibase/bbui"
import { store } from "builderStore"
import { Input, TextArea } from "@budibase/bbui"
import { store, hostingStore } from "builderStore"
import api from "builderStore/api"
import { object, string } from "yup"
import { onMount } from "svelte"
import { get } from "svelte/store"
let nameValidation, nameError
let urlValidation, urlError
$: checkName($store.name)
$: checkUrl($store.url)
async function updateApplication(data) {
const response = await api.put(`/api/applications/${$store.appId}`, data)
const app = await response.json()
await response.json()
store.update(state => {
state = {
...state,
@ -14,6 +23,59 @@
return state
})
}
async function checkValidation(input, validation) {
if (!input || !validation) {
return
}
try {
await object(validation).validate(input, { abortEarly: false })
} catch (error) {
if (!error || !error.inner) return ""
return error.inner.reduce((acc, err) => {
return acc + err.message
}, "")
}
}
async function checkName(name) {
nameError = await checkValidation({ name }, nameValidation)
}
async function checkUrl(url) {
urlError = await checkValidation({ url: url.toLowerCase() }, urlValidation)
}
onMount(async () => {
const nameError = "Your application must have a name.",
urlError = "Your application must have a URL."
let hostingInfo = await hostingStore.actions.fetch()
if (hostingInfo.type === "self") {
await hostingStore.actions.fetchDeployedApps()
const existingAppNames = get(hostingStore).deployedAppNames
const existingAppUrls = get(hostingStore).deployedAppUrls
const nameIdx = existingAppNames.indexOf(get(store).name)
const urlIdx = existingAppUrls.indexOf(get(store).url)
if (nameIdx !== -1) {
existingAppNames.splice(nameIdx, 1)
}
if (urlIdx !== -1) {
existingAppUrls.splice(urlIdx, 1)
}
nameValidation = {
name: string()
.required(nameError)
.notOneOf(existingAppNames),
}
urlValidation = {
url: string()
.required(urlError)
.notOneOf(existingAppUrls),
}
} else {
nameValidation = { name: string.required(nameError) }
}
})
</script>
<div class="container">
@ -21,8 +83,18 @@
on:save={e => updateApplication({ name: e.detail })}
thin
edit
value={$store.name}
bind:value={$store.name}
bind:error={nameError}
label="App Name" />
{#if $hostingStore.hostingInfo.type === 'self'}
<Input
on:save={e => updateApplication({ url: e.detail })}
thin
edit
bind:value={$store.url}
bind:error={urlError}
label="App URL" />
{/if}
<TextArea
on:save={e => updateApplication({ description: e.detail })}
thin

View File

@ -1,5 +1,5 @@
<script>
import { writable } from "svelte/store"
import { writable, get as svelteGet } from "svelte/store"
import {
store,
automationStore,
@ -28,6 +28,7 @@
let isApiKeyValid
let lastApiKey
let fetchApiKeyPromise
const validateApiKey = async apiKey => {
if (isApiKeyValid) return true
if (!apiKey) return false
@ -81,6 +82,11 @@
let hostingInfo = await hostingStore.actions.fetch()
// re-init the steps based on whether self hosting or cloud hosted
if (hostingInfo.type === "self") {
await hostingStore.actions.fetchDeployedApps()
const existingAppNames = svelteGet(hostingStore).deployedAppNames
infoValidation.applicationName = string()
.required("Your application must have a name.")
.notOneOf(existingAppNames)
isApiKeyValid = true
steps = [buildStep(Info), buildStep(User)]
validationSchemas = [infoValidation, userValidation]

View File

@ -1,11 +1,49 @@
<script>
import { params } from "@sveltech/routify"
import { Switcher, Modal } from "@budibase/bbui"
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
const tabs = [
{
title: "Tables",
key: "table",
},
{
title: "Data Sources",
key: "datasource",
},
]
let tab = $params.selectedDatasource ? "datasource" : "table"
let modal
</script>
<!-- routify:options index=0 -->
<div class="root">
<div class="nav">
<Switcher headings={tabs} bind:value={tab}>
<div class="title">
<i
data-cy={`new-${tab}`}
class="ri-add-circle-fill"
on:click={modal.show} />
</div>
{#if tab === 'table'}
<TableNavigator />
<Modal bind:this={modal}>
<CreateTableModal />
</Modal>
{:else if tab === 'datasource'}
<DatasourceNavigator />
<Modal bind:this={modal}>
<CreateDatasourceModal />
</Modal>
{/if}
</Switcher>
</div>
<div class="content">
<slot />
@ -19,6 +57,7 @@
grid-template-columns: 260px minmax(0, 1fr);
background: var(--grey-2);
}
.content {
flex: 1 1 auto;
padding: var(--spacing-l) 40px;
@ -29,6 +68,7 @@
align-items: stretch;
gap: var(--spacing-l);
}
.nav {
overflow-y: auto;
background: var(--background);
@ -38,5 +78,18 @@
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-l);
position: relative;
}
i {
font-size: 20px;
position: absolute;
top: var(--spacing-l);
right: var(--spacing-xl);
}
i:hover {
cursor: pointer;
color: var(--blue);
}
</style>

View File

@ -0,0 +1,42 @@
<script>
import { params } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { Switcher } from "@budibase/bbui"
import QueryInterface from "components/integration/QueryViewer.svelte"
let query
async function fetchQueryConfig() {
try {
const response = await api.get(`/api/integrations/${datasource.source}`)
const json = await response.json()
config = json.query
} catch (err) {
notifier.danger("Error fetching datasource configuration options.")
console.error(err)
}
}
$: selectedQuery = $backendUiStore.queries.find(
query => query._id === $backendUiStore.selectedQueryId
) || {
datasourceId: $params.selectedDatasource,
name: "New Query",
parameters: [],
fields: {},
}
</script>
<section>
{#if $backendUiStore.selectedDatabase._id && selectedQuery}
<QueryInterface query={selectedQuery} />
{/if}
</section>
<style>
section {
background: var(--background);
padding: var(--spacing-xl);
border-radius: var(--border-radius-m);
}
</style>

View File

@ -0,0 +1,15 @@
<script>
import { params } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
if ($params.selectedDatasourceId) {
const datasource = $backendUiStore.datasources.find(
m => m._id === $params.selectedDatasource
)
if (datasource) {
backendUiStore.actions.datasources.select(datasource)
}
}
</script>
<slot />

View File

@ -0,0 +1,44 @@
<script>
import { goto } from "@sveltech/routify"
import { Button, Spacer, Icon, TextButton } from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
$: datasource = $backendUiStore.datasources.find(
ds => ds._id === $backendUiStore.selectedDatasourceId
)
async function saveDatasource() {
// Create datasource
await backendUiStore.actions.datasources.save(datasource)
notifier.success(`Datasource ${name} saved successfully.`)
}
</script>
{#if datasource}
<TextButton text small on:click={() => $goto('../new')}>
<Icon name="filter" />
Create Query
</TextButton>
<section>
<h4>{datasource.name}: Configuration</h4>
<IntegrationConfigForm integration={datasource.config} />
<Spacer medium />
<footer>
<Button blue wide on:click={saveDatasource}>Save</Button>
</footer>
</section>
{/if}
<style>
h4 {
margin-top: var(--spacing-xl);
margin-bottom: var(--spacing-s);
}
section {
background: var(--background);
border-radius: var(--border-radius-m);
padding: var(--spacing-xl);
}
</style>

View File

@ -0,0 +1,18 @@
<script>
import { backendUiStore } from "builderStore"
import { goto, leftover } from "@sveltech/routify"
import { onMount } from "svelte"
onMount(async () => {
// navigate to first datasource in list, if not already selected
if (
!$leftover &&
$backendUiStore.datasources.length > 0 &&
!$backendUiStore.selectedDatasourceId
) {
$goto(`./${$backendUiStore.datasources[0]._id}`)
}
})
</script>
<slot />

View File

@ -89,6 +89,15 @@
padding: var(--spacing-l) var(--spacing-xl);
}
.binding-drawer-container {
height: 50vh;
position: absolute;
bottom: 0;
width: 100%;
background: var(--background);
padding: var(--spacing-xl);
}
.nav-group-header > div:nth-child(1) {
padding: 0rem 0.5rem 0rem 0rem;
vertical-align: bottom;

View File

@ -842,10 +842,10 @@
lodash "^4.17.19"
to-fast-properties "^2.0.0"
"@budibase/bbui@^1.52.4":
version "1.52.4"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.52.4.tgz#ae3c17e1f49f14e65831703958bcddc6e64afd24"
integrity sha512-/wiv5dSyvXLgy2/zGEslnCsjwE8qqng1D8k5ScSOPEyMab8tzzd1XxfZAN9rp84zIMgAXeH6s5a4j4riR+jVkg==
"@budibase/bbui@^1.54.0":
version "1.54.0"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.54.0.tgz#60e6c0faa3d8f1781c503e74f8b8990f75ba2c40"
integrity sha512-98koXkueqda6oQT6q0NPNvdL878ETRevtmmm34aSz9C6B4Oz68VVCsiFzRWuHvP/7wiNaAxMgY1nsEsCwP3LpQ==
dependencies:
markdown-it "^12.0.2"
quill "^1.3.7"
@ -918,11 +918,6 @@
debug "^3.1.0"
lodash.once "^4.1.1"
"@fortawesome/fontawesome-free@^5.14.0":
version "5.15.1"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.1.tgz#ccfef6ddbe59f8fe8f694783e1d3eb88902dc5eb"
integrity sha512-OEdH7SyC1suTdhBGW91/zBfR6qaIhThbcN8PUXtXilY4GYnSBbVqOntdHbC1vXwsDnX0Qix2m2+DSU1J51ybOQ==
"@hapi/hoek@^9.0.0":
version "9.1.0"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6"
@ -2233,6 +2228,11 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
codemirror@^5.59.0:
version "5.59.0"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.59.0.tgz#6d8132055459aabf21d04cae5cf5c430e5c57bb9"
integrity sha512-UGzSkCacY9z0rSpQ3wnTWRN2nvRE6foDXnJltWW8pazInR/R+3gXHrao4IFQMv/bSBvFBxt8/HPpkpKAS54x5Q==
collection-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
@ -3911,9 +3911,9 @@ inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1,
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ini@^1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
invariant@^2.2.4:
version "2.2.4"

View File

@ -1,7 +1,11 @@
import { get } from "svelte/store"
import { fetchTableData } from "./tables"
import { fetchViewData } from "./views"
import { fetchRelationshipData } from "./relationships"
import { executeQuery } from "./queries"
import { enrichRows } from "./rows"
import { enrichDataBindings } from "../utils/enrichDataBinding"
import { bindingStore } from "../store/binding"
/**
* Fetches all rows for a particular Budibase data source.
@ -18,6 +22,21 @@ export const fetchDatasource = async (datasource, dataContext) => {
rows = await fetchTableData(tableId)
} else if (type === "view") {
rows = await fetchViewData(datasource)
} else if (type === "query") {
const bindings = get(bindingStore)
// Set the default query params
let queryParams = datasource.queryParams || {}
for (let param of datasource.parameters) {
if (!queryParams[param.name]) {
queryParams[param.name] = param.default
}
}
const parameters = enrichDataBindings(queryParams, {
...bindings,
...dataContext,
})
return await executeQuery({ queryId: datasource._id, parameters })
} else if (type === "link") {
const row = dataContext[datasource.providerId]
rows = await fetchRelationshipData({
@ -26,7 +45,6 @@ export const fetchDatasource = async (datasource, dataContext) => {
fieldName,
})
}
// Enrich rows
return await enrichRows(rows, tableId)
}

View File

@ -6,5 +6,6 @@ export * from "./attachments"
export * from "./views"
export * from "./relationships"
export * from "./routes"
export * from "./queries"
export * from "./app"
export * from "./automations"

View File

@ -0,0 +1,14 @@
import API from "./api"
/**
* Executes a query against an external data connector.
*/
export const executeQuery = async ({ queryId, parameters }) => {
const response = await API.post({
url: `/api/queries/${queryId}`,
body: {
parameters,
},
})
return response
}

View File

@ -1,7 +1,7 @@
import { get } from "svelte/store"
import { enrichDataBinding } from "./enrichDataBinding"
import { enrichDataBinding, enrichDataBindings } from "./enrichDataBinding"
import { routeStore, builderStore } from "../store"
import { saveRow, deleteRow, triggerAutomation } from "../api"
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "../api"
const saveRowHandler = async (action, context) => {
const { fields, providerId } = action.parameters
@ -46,10 +46,21 @@ const navigationHandler = action => {
}
}
const queryExecutionHandler = async (action, context) => {
const { datasourceId, queryId, queryParams } = action.parameters
const enrichedQueryParameters = enrichDataBindings(queryParams, context)
await executeQuery({
datasourceId,
queryId,
parameters: enrichedQueryParameters,
})
}
const handlerMap = {
["Save Row"]: saveRowHandler,
["Delete Row"]: deleteRowHandler,
["Navigate To"]: navigationHandler,
["Execute Query"]: queryExecutionHandler,
["Trigger Automation"]: triggerAutomationHandler,
}

View File

@ -49,10 +49,12 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@elastic/elasticsearch": "^7.10.0",
"@budibase/client": "^0.5.3",
"@koa/router": "^8.0.0",
"@sendgrid/mail": "^7.1.1",
"@sentry/node": "^5.19.2",
"airtable": "^0.10.1",
"aws-sdk": "^2.767.0",
"bcryptjs": "^2.4.3",
"chmodr": "^1.2.0",
@ -78,9 +80,12 @@
"koa-session": "^5.12.0",
"koa-static": "^5.0.0",
"lodash": "^4.17.13",
"mongodb": "^3.6.3",
"mssql": "^6.2.3",
"mustache": "^4.0.1",
"node-fetch": "^2.6.0",
"open": "^7.3.0",
"pg": "^8.5.1",
"pino-pretty": "^4.0.0",
"pouchdb": "^7.2.1",
"pouchdb-all-dbs": "^1.0.2",

View File

@ -3,7 +3,7 @@ const { exportTemplateFromApp } = require("../src/utilities/templates")
const yargs = require("yargs")
// Script to export a chosen budibase app into a package
// Usage: ./scripts/exportAppTemplate.js export --name=Funky --appId=someInstanceId --appId=appId
// Usage: ./scripts/exportAppTemplate.js export --name=Funky --appId=appId
yargs
.command(

View File

@ -12,8 +12,6 @@ const { createRoutingView } = require("../../utilities/routing")
const { downloadTemplate } = require("../../utilities/templates")
const {
generateAppID,
DocumentTypes,
SEPARATOR,
getLayoutParams,
getScreenParams,
generateScreenID,
@ -32,9 +30,15 @@ const {
} = require("../../constants/screens")
const { cloneDeep } = require("lodash/fp")
const { recurseMustache } = require("../../utilities/mustache")
const { getAllApps } = require("../../utilities")
const { USERS_TABLE_SCHEMA } = require("../../constants")
const {
getDeployedApps,
getHostingInfo,
HostingTypes,
} = require("../../utilities/builder/hosting")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
const URL_REGEX_SLASH = /\/|\\/g
// utility function, need to do away with this
async function getLayouts(db) {
@ -63,6 +67,28 @@ function getUserRoleId(ctx) {
: ctx.user.role._id
}
async function getAppUrlIfNotInUse(ctx) {
let url
if (ctx.request.body.url) {
url = encodeURI(ctx.request.body.url)
} else {
url = encodeURI(`${ctx.request.body.name}`)
}
url = `/${url.replace(URL_REGEX_SLASH, "")}`.toLowerCase()
const hostingInfo = await getHostingInfo()
if (hostingInfo.type === HostingTypes.CLOUD) {
return url
}
const deployedApps = await getDeployedApps()
if (
deployedApps[url] != null &&
deployedApps[url].appId !== ctx.params.appId
) {
ctx.throw(400, "App name/URL is already in use.")
}
return url
}
async function createInstance(template) {
const appId = generateAppID()
@ -96,17 +122,7 @@ async function createInstance(template) {
}
exports.fetch = async function(ctx) {
let allDbs = await CouchDB.allDbs()
const appDbNames = allDbs.filter(dbName => dbName.startsWith(APP_PREFIX))
const apps = appDbNames.map(db => new CouchDB(db).get(db))
if (apps.length === 0) {
ctx.body = []
} else {
const response = await Promise.allSettled(apps)
ctx.body = response
.filter(result => result.status === "fulfilled")
.map(({ value }) => value)
}
ctx.body = await getAllApps()
}
exports.fetchAppDefinition = async function(ctx) {
@ -139,6 +155,7 @@ exports.fetchAppPackage = async function(ctx) {
}
exports.create = async function(ctx) {
const url = await getAppUrlIfNotInUse(ctx)
const instance = await createInstance(ctx.request.body.template)
const appId = instance._id
const version = packageJson.version
@ -148,6 +165,7 @@ exports.create = async function(ctx) {
version: packageJson.version,
componentLibraries: ["@budibase/standard-components"],
name: ctx.request.body.name,
url: url,
template: ctx.request.body.template,
instance: instance,
deployment: {
@ -169,11 +187,12 @@ exports.create = async function(ctx) {
}
exports.update = async function(ctx) {
const url = await getAppUrlIfNotInUse(ctx)
const db = new CouchDB(ctx.params.appId)
const application = await db.get(ctx.params.appId)
const data = ctx.request.body
const newData = { ...application, ...data }
const newData = { ...application, ...data, url }
const response = await db.put(newData)
data._rev = response.rev

View File

@ -0,0 +1,79 @@
const CouchDB = require("../../db")
const bcrypt = require("../../utilities/bcrypt")
const {
generateDatasourceID,
getDatasourceParams,
getQueryParams,
} = require("../../db/utils")
exports.fetch = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
const datasources = (
await database.allDocs(
getDatasourceParams(null, {
include_docs: true,
})
)
).rows.map(row => row.doc)
ctx.body = datasources
}
exports.save = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const datasource = {
_id: generateDatasourceID(),
type: "datasource",
...ctx.request.body,
}
try {
const response = await db.post(datasource)
datasource._rev = response.rev
ctx.status = 200
ctx.message = "Datasource saved successfully."
ctx.body = datasource
} catch (err) {
ctx.throw(err.status, err)
}
}
exports.update = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const user = ctx.request.body
const dbUser = await db.get(ctx.request.body._id)
if (user.password) {
user.password = await bcrypt.hash(user.password)
} else {
delete user.password
}
const newData = { ...dbUser, ...user }
const response = await db.put(newData)
user._rev = response.rev
ctx.status = 200
ctx.message = `User ${ctx.request.body.email} updated successfully.`
ctx.body = response
}
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
// Delete all queries for the datasource
const rows = await db.allDocs(getQueryParams(ctx.params.datasourceId, null))
await db.bulkDocs(rows.rows.map(row => ({ ...row.doc, _deleted: true })))
// delete the datasource
await db.remove(ctx.params.datasourceId, ctx.params.revId)
ctx.message = `Datasource deleted.`
ctx.status = 200
}
exports.find = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
const datasource = await database.get(ctx.params.datasourceId)
ctx.body = datasource
}

View File

@ -2,6 +2,7 @@ const CouchDB = require("../../db")
const { BUILDER_CONFIG_DB, HOSTING_DOC } = require("../../constants")
const {
getHostingInfo,
getDeployedApps,
HostingTypes,
getAppUrl,
} = require("../../utilities/builder/hosting")
@ -37,3 +38,7 @@ exports.fetchUrls = async ctx => {
app: await getAppUrl(ctx.appId),
}
}
exports.getDeployedApps = async ctx => {
ctx.body = await getDeployedApps()
}

View File

@ -0,0 +1,13 @@
const { definitions } = require("../../integrations")
exports.fetch = async function(ctx) {
// TODO: fetch these from a github repo etc
console.log(definitions)
ctx.status = 200
ctx.body = definitions
}
exports.find = async function(ctx) {
ctx.status = 200
ctx.body = definitions[ctx.params.type]
}

View File

@ -0,0 +1,101 @@
const handlebars = require("handlebars")
const CouchDB = require("../../db")
const { generateQueryID, getQueryParams } = require("../../db/utils")
const { integrations } = require("../../integrations")
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const body = await db.allDocs(
getQueryParams(null, {
include_docs: true,
})
)
ctx.body = body.rows.map(row => row.doc)
}
exports.save = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const query = ctx.request.body
if (!query._id) {
query._id = generateQueryID(query.datasourceId)
}
const response = await db.put(query)
query._rev = response.rev
ctx.body = query
ctx.message = `Query ${query.name} saved successfully.`
}
function enrichQueryFields(fields, parameters) {
const enrichedQuery = {}
// enrich the fields with dynamic parameters
for (let key in fields) {
const template = handlebars.compile(fields[key])
enrichedQuery[key] = template(parameters)
}
if (enrichedQuery.json || enrichedQuery.customData) {
enrichedQuery.json = JSON.parse(
enrichedQuery.json || enrichedQuery.customData
)
delete enrichedQuery.customData
}
return enrichedQuery
}
exports.preview = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const datasource = await db.get(ctx.request.body.datasourceId)
const Integration = integrations[datasource.source]
if (!Integration) {
ctx.throw(400, "Integration type does not exist.")
return
}
const { fields, parameters, queryVerb } = ctx.request.body
const enrichedQuery = enrichQueryFields(fields, parameters)
ctx.body = await new Integration(datasource.config)[queryVerb](enrichedQuery)
}
exports.execute = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const query = await db.get(ctx.params.queryId)
const datasource = await db.get(query.datasourceId)
const Integration = integrations[datasource.source]
if (!Integration) {
ctx.throw(400, "Integration type does not exist.")
return
}
const enrichedQuery = enrichQueryFields(
query.fields,
ctx.request.body.parameters
)
// call the relevant CRUD method on the integration class
const response = await new Integration(datasource.config)[query.queryVerb](
enrichedQuery
)
ctx.body = response
}
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
await db.remove(ctx.params.queryId, ctx.params.revId)
ctx.message = `Query deleted.`
ctx.status = 200
}

View File

@ -175,7 +175,7 @@ exports.fetchView = async function(ctx) {
const viewName = ctx.params.viewName
// if this is a table view being looked for just transfer to that
if (viewName.indexOf(TABLE_VIEW_BEGINS_WITH) === 0) {
if (viewName.startsWith(TABLE_VIEW_BEGINS_WITH)) {
ctx.params.tableId = viewName.substring(4)
await exports.fetchTableRows(ctx)
return
@ -217,6 +217,7 @@ exports.fetchView = async function(ctx) {
exports.fetchTableRows = async function(ctx) {
const appId = ctx.user.appId
// special case for users, fetch through the user controller
let rows
if (ctx.params.tableId === ViewNames.USERS) {

View File

@ -12,6 +12,7 @@ const {
budibaseAppsDir,
budibaseTempDir,
} = require("../../../utilities/budibaseDir")
const { getDeployedApps } = require("../../../utilities/builder/hosting")
const CouchDB = require("../../../db")
const setBuilderToken = require("../../../utilities/builder/setBuilderToken")
const fileProcessor = require("../../../utilities/fileProcessor")
@ -26,6 +27,17 @@ function objectStoreUrl() {
}
}
async function checkForSelfHostedURL(ctx) {
// the "appId" component of the URL may actually be a specific self hosted URL
let possibleAppUrl = `/${encodeURI(ctx.params.appId).toLowerCase()}`
const apps = await getDeployedApps()
if (apps[possibleAppUrl] && apps[possibleAppUrl].appId) {
return apps[possibleAppUrl].appId
} else {
return ctx.params.appId
}
}
// this was the version before we started versioning the component library
const COMP_LIB_BASE_APP_VERSION = "0.2.5"
@ -149,14 +161,18 @@ exports.performLocalFileProcessing = async function(ctx) {
}
exports.serveApp = async function(ctx) {
let appId = ctx.params.appId
if (env.SELF_HOSTED) {
appId = await checkForSelfHostedURL(ctx)
}
const App = require("./templates/BudibaseApp.svelte").default
const db = new CouchDB(ctx.params.appId)
const appInfo = await db.get(ctx.params.appId)
const db = new CouchDB(appId, { skip_setup: true })
const appInfo = await db.get(appId)
const { head, html, css } = App.render({
title: appInfo.name,
production: env.CLOUD,
appId: ctx.params.appId,
appId,
objectStoreUrl: objectStoreUrl(),
})
@ -168,7 +184,7 @@ exports.serveApp = async function(ctx) {
head,
body: html,
style: css.code,
appId: ctx.params.appId,
appId,
})
}
@ -176,8 +192,7 @@ exports.serveAttachment = async function(ctx) {
const appId = ctx.user.appId
const attachmentsPath = resolve(budibaseAppsDir(), appId, "attachments")
// Serve from CloudFront
// TODO: need to replace this with link to self hosted object store
// Serve from object store
if (env.CLOUD) {
const S3_URL = join(objectStoreUrl(), appId, "attachments", ctx.file)
const response = await fetch(S3_URL)

View File

@ -22,6 +22,7 @@
<title>{title}</title>
<link rel="icon" type="image/png" href={favicon} />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
<link href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css" rel="stylesheet">
<style>
html,
body {

View File

@ -0,0 +1,26 @@
const Router = require("@koa/router")
const datasourceController = require("../controllers/datasource")
const authorized = require("../../middleware/authorized")
const {
BUILDER,
PermissionLevels,
PermissionTypes,
} = require("../../utilities/security/permissions")
const router = Router()
router
.get("/api/datasources", authorized(BUILDER), datasourceController.fetch)
.get(
"/api/datasources/:id",
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
datasourceController.find
)
.post("/api/datasources", authorized(BUILDER), datasourceController.save)
.delete(
"/api/datasources/:datasourceId/:revId",
authorized(BUILDER),
datasourceController.destroy
)
module.exports = router

View File

@ -1,6 +1,7 @@
const Router = require("@koa/router")
const controller = require("../controllers/hosting")
const authorized = require("../../middleware/authorized")
const selfhost = require("../../middleware/selfhost")
const { BUILDER } = require("../../utilities/security/permissions")
const router = Router()
@ -10,5 +11,11 @@ router
.get("/api/hosting/urls", authorized(BUILDER), controller.fetchUrls)
.get("/api/hosting", authorized(BUILDER), controller.fetch)
.post("/api/hosting", authorized(BUILDER), controller.save)
.get(
"/api/hosting/apps",
authorized(BUILDER),
selfhost,
controller.getDeployedApps
)
module.exports = router

View File

@ -16,7 +16,10 @@ const apiKeysRoutes = require("./apikeys")
const templatesRoutes = require("./templates")
const analyticsRoutes = require("./analytics")
const routingRoutes = require("./routing")
const integrationRoutes = require("./integration")
const permissionRoutes = require("./permission")
const datasourceRoutes = require("./datasource")
const queryRoutes = require("./query")
const hostingRoutes = require("./hosting")
exports.mainRoutes = [
@ -34,7 +37,10 @@ exports.mainRoutes = [
analyticsRoutes,
webhookRoutes,
routingRoutes,
integrationRoutes,
permissionRoutes,
datasourceRoutes,
queryRoutes,
hostingRoutes,
// these need to be handled last as they still use /api/:tableId
// this could be breaking as koa may recognise other routes as this

View File

@ -0,0 +1,12 @@
const Router = require("@koa/router")
const controller = require("../controllers/integration")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions")
const router = Router()
router
.get("/api/integrations", authorized(BUILDER), controller.fetch)
.get("/api/integrations/:type", authorized(BUILDER), controller.find)
module.exports = router

View File

@ -0,0 +1,74 @@
const Router = require("@koa/router")
const queryController = require("../controllers/query")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions")
const Joi = require("joi")
const {
PermissionLevels,
PermissionTypes,
} = require("../../utilities/security/permissions")
const joiValidator = require("../../middleware/joi-validator")
const router = Router()
const QueryVerb = {
Create: "create",
Read: "read",
Update: "update",
Delete: "delete",
}
function generateQueryValidation() {
// prettier-ignore
return joiValidator.body(Joi.object({
_id: Joi.string(),
_rev: Joi.string(),
name: Joi.string().required(),
fields: Joi.object().required(),
datasourceId: Joi.string().required(),
parameters: Joi.array().items(Joi.object({
name: Joi.string(),
default: Joi.string()
})),
queryVerb: Joi.string().allow(...Object.values(QueryVerb)).required(),
queryType: Joi.string().required(),
schema: Joi.object({}).required().unknown(true)
}))
}
function generateQueryPreviewValidation() {
// prettier-ignore
return joiValidator.body(Joi.object({
fields: Joi.object().required(),
queryVerb: Joi.string().allow(...Object.values(QueryVerb)).required(),
datasourceId: Joi.string().required(),
parameters: Joi.object({}).required().unknown(true)
}))
}
router
.get("/api/queries", authorized(BUILDER), queryController.fetch)
.post(
"/api/queries",
authorized(BUILDER),
generateQueryValidation(),
queryController.save
)
.post(
"/api/queries/preview",
authorized(BUILDER),
generateQueryPreviewValidation(),
queryController.preview
)
.post(
"/api/queries/:queryId",
authorized(PermissionTypes.QUERY, PermissionLevels.WRITE),
queryController.execute
)
.delete(
"/api/queries/:queryId/:revId",
authorized(BUILDER),
queryController.destroy
)
module.exports = router

View File

@ -0,0 +1,149 @@
const {
supertest,
createApplication,
defaultHeaders,
builderEndpointShouldBlockNormalUsers,
getDocument,
insertDocument
} = require("./couchTestUtils")
let { generateDatasourceID, generateQueryID } = require("../../../db/utils")
const DATASOURCE_ID = generateDatasourceID()
const TEST_DATASOURCE = {
_id: DATASOURCE_ID,
type: "datasource",
name: "Test",
source: "POSTGRES",
config: {},
type: "datasource",
}
const TEST_QUERY = {
_id: generateQueryID(DATASOURCE_ID),
datasourceId: DATASOURCE_ID,
name:"New Query",
parameters:[],
fields:{},
schema:{},
queryVerb:"read",
queryType:"Table",
}
describe("/datasources", () => {
let request
let server
let app
let appId
let datasource
beforeAll(async () => {
({ request, server } = await supertest())
});
afterAll(() => {
server.close()
})
beforeEach(async () => {
app = await createApplication(request)
appId = app.instance._id
});
async function createDatasource() {
return await insertDocument(appId, TEST_DATASOURCE)
}
async function createQuery() {
return await insertDocument(appId, TEST_QUERY)
}
describe("create", () => {
it("should create a new datasource", async () => {
const res = await request
.post(`/api/datasources`)
.send(TEST_DATASOURCE)
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
expect(res.res.statusMessage).toEqual("Datasource saved successfully.");
expect(res.body.name).toEqual("Test");
})
});
describe("fetch", () => {
let datasource
beforeEach(async () => {
datasource = await createDatasource()
});
afterEach(() => {
delete datasource._rev
});
it("returns all the datasources from the server", async () => {
const res = await request
.get(`/api/datasources`)
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
const datasources = res.body;
expect(datasources).toEqual([
{
"_rev": datasources[0]._rev,
...TEST_DATASOURCE
}
]);
})
it("should apply authorization to endpoint", async () => {
await builderEndpointShouldBlockNormalUsers({
request,
method: "GET",
url: `/api/datasources`,
appId: appId,
})
})
});
describe("destroy", () => {
let datasource;
beforeEach(async () => {
datasource = await createDatasource()
});
afterEach(() => {
delete datasource._rev
});
it("deletes queries for the datasource after deletion and returns a success message", async () => {
await createQuery(datasource.id)
await request
.delete(`/api/datasources/${datasource.id}/${datasource.rev}`)
.set(defaultHeaders(appId))
.expect(200)
const res = await request
.get(`/api/datasources`)
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
expect(res.body).toEqual([])
})
it("should apply authorization to endpoint", async () => {
await builderEndpointShouldBlockNormalUsers({
request,
method: "DELETE",
url: `/api/datasources/${datasource._id}/${datasource._rev}`,
appId: appId,
})
})
});
});

View File

@ -0,0 +1,153 @@
const {
supertest,
createApplication,
defaultHeaders,
builderEndpointShouldBlockNormalUsers,
getDocument,
insertDocument
} = require("./couchTestUtils")
let { generateDatasourceID, generateQueryID } = require("../../../db/utils")
const DATASOURCE_ID = generateDatasourceID()
const TEST_DATASOURCE = {
_id: DATASOURCE_ID,
type: "datasource",
name: "Test",
source: "POSTGRES",
config: {},
type: "datasource",
}
const TEST_QUERY = {
_id: generateQueryID(DATASOURCE_ID),
datasourceId: DATASOURCE_ID,
name:"New Query",
parameters:[],
fields:{},
schema:{},
queryVerb:"read",
queryType:"Table",
}
describe("/queries", () => {
let request
let server
let app
let appId
let datasource
let query
beforeAll(async () => {
({ request, server } = await supertest())
});
afterAll(() => {
server.close()
})
beforeEach(async () => {
app = await createApplication(request)
appId = app.instance._id
});
async function createDatasource() {
return await insertDocument(appId, TEST_DATASOURCE)
}
async function createQuery() {
return await insertDocument(appId, TEST_QUERY)
}
describe("create", () => {
it("should create a new query", async () => {
const res = await request
.post(`/api/queries`)
.send(TEST_QUERY)
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
expect(res.res.statusMessage).toEqual(`Query ${TEST_QUERY.name} saved successfully.`);
expect(res.body).toEqual({
_rev: res.body._rev,
...TEST_QUERY,
});
})
});
describe("fetch", () => {
let datasource
beforeEach(async () => {
datasource = await createDatasource()
});
afterEach(() => {
delete datasource._rev
});
it("returns all the queries from the server", async () => {
const query = await createQuery()
const res = await request
.get(`/api/queries`)
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
const queries = res.body;
expect(queries).toEqual([
{
"_rev": query.rev,
...TEST_QUERY
}
]);
})
it("should apply authorization to endpoint", async () => {
await builderEndpointShouldBlockNormalUsers({
request,
method: "GET",
url: `/api/datasources`,
appId: appId,
})
})
});
describe("destroy", () => {
let datasource;
beforeEach(async () => {
datasource = await createDatasource()
});
afterEach(() => {
delete datasource._rev
});
it("deletes a query and returns a success message", async () => {
const query = await createQuery()
await request
.delete(`/api/queries/${query.id}/${query.rev}`)
.set(defaultHeaders(appId))
.expect(200)
const res = await request
.get(`/api/queries`)
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect(200)
expect(res.body).toEqual([])
})
it("should apply authorization to endpoint", async () => {
await builderEndpointShouldBlockNormalUsers({
request,
method: "DELETE",
url: `/api/datasources/${datasource._id}/${datasource._rev}`,
appId: appId,
})
})
});
});

View File

@ -12,6 +12,7 @@ PouchDB.adapter("writableStream", replicationStream.adapters.writableStream)
let POUCH_DB_DEFAULTS = {
prefix: COUCH_DB_URL,
skip_setup: !!env.CLOUD,
}
if (isInMemory) {

View File

@ -15,6 +15,8 @@ const DocumentTypes = {
INSTANCE: "inst",
LAYOUT: "layout",
SCREEN: "screen",
DATASOURCE: "datasource",
QUERY: "query",
}
const ViewNames = {
@ -223,3 +225,43 @@ exports.generateWebhookID = () => {
exports.getWebhookParams = (webhookId = null, otherProps = {}) => {
return getDocParams(DocumentTypes.WEBHOOK, webhookId, otherProps)
}
/**
* Generates a new datasource ID.
* @returns {string} The new datasource ID which the webhook doc can be stored under.
*/
exports.generateDatasourceID = () => {
return `${DocumentTypes.DATASOURCE}${SEPARATOR}${newid()}`
}
/**
* Gets parameters for retrieving a datasource, this is a utility function for the getDocParams function.
*/
exports.getDatasourceParams = (datasourceId = null, otherProps = {}) => {
return getDocParams(DocumentTypes.DATASOURCE, datasourceId, otherProps)
}
/**
* Generates a new query ID.
* @returns {string} The new query ID which the query doc can be stored under.
*/
exports.generateQueryID = datasourceId => {
return `${
DocumentTypes.QUERY
}${SEPARATOR}${datasourceId}${SEPARATOR}${newid()}`
}
/**
* Gets parameters for retrieving a query, this is a utility function for the getDocParams function.
*/
exports.getQueryParams = (datasourceId = null, otherProps = {}) => {
if (datasourceId == null) {
return getDocParams(DocumentTypes.QUERY, null, otherProps)
}
return getDocParams(
DocumentTypes.QUERY,
`${datasourceId}${SEPARATOR}`,
otherProps
)
}

View File

@ -28,6 +28,8 @@ module.exports = {
SENDGRID_API_KEY: process.env.SENDGRID_API_KEY,
CLOUD: process.env.CLOUD,
SELF_HOSTED: process.env.SELF_HOSTED,
WORKER_URL: process.env.WORKER_URL,
HOSTING_KEY: process.env.HOSTING_KEY,
DYNAMO_ENDPOINT: process.env.DYNAMO_ENDPOINT,
AWS_REGION: process.env.AWS_REGION,
DEPLOYMENT_CREDENTIALS_URL: process.env.DEPLOYMENT_CREDENTIALS_URL,

View File

@ -0,0 +1,12 @@
exports.QUERY_TYPES = {
SQL: "sql",
JSON: "json",
FIELDS: "fields",
}
exports.FIELD_TYPES = {
STRING: "string",
NUMBER: "number",
PASSWORD: "password",
LIST: "list",
}

View File

@ -0,0 +1,131 @@
const Airtable = require("airtable")
const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
const SCHEMA = {
docs: "https://airtable.com/api",
datasource: {
apiKey: {
type: FIELD_TYPES.STRING,
default: "enter api key",
required: true,
},
base: {
type: FIELD_TYPES.STRING,
default: "mybase",
required: true,
},
},
query: {
create: {
"Airtable Record": {
type: QUERY_TYPES.FIELDS,
customisable: true,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},
},
read: {
Table: {
type: QUERY_TYPES.FIELDS,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
view: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},
},
update: {
Fields: {
type: QUERY_TYPES.FIELDS,
customisable: true,
fields: {
id: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},
},
delete: {
"Airtable Ids": {
type: FIELD_TYPES.JSON,
},
},
},
}
class AirtableIntegration {
constructor(config) {
this.config = config
this.client = new Airtable(config).base(config.base)
}
async create(query) {
const { table, json } = query
try {
const records = await this.client(table).create([
{
fields: json,
},
])
return records
} catch (err) {
console.error("Error writing to airtable", err)
throw err
}
}
async read(query) {
try {
const records = await this.client(query.table)
.select({ maxRecords: query.numRecords || 10, view: query.view })
.firstPage()
return records.map(({ fields }) => fields)
} catch (err) {
console.error("Error writing to airtable", err)
return []
}
}
async update(query) {
const { table, id, json } = query
try {
const records = await this.client(table).update([
{
id,
fields: json,
},
])
return records
} catch (err) {
console.error("Error writing to airtable", err)
throw err
}
}
async delete(query) {
try {
const records = await this.client(query.table).destroy(query.ids)
return records
} catch (err) {
console.error("Error writing to airtable", err)
throw err
}
}
}
module.exports = {
schema: SCHEMA,
integration: AirtableIntegration,
}

View File

@ -0,0 +1,100 @@
const PouchDB = require("pouchdb")
const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
const SCHEMA = {
docs: "https://docs.couchdb.org/en/stable/",
datasource: {
url: {
type: FIELD_TYPES.STRING,
required: true,
default: "http://localhost:5984",
},
database: {
type: FIELD_TYPES.STRING,
required: true,
},
},
query: {
create: {
"CouchDB DSL": {
type: QUERY_TYPES.JSON,
},
},
read: {
"CouchDB DSL": {
type: QUERY_TYPES.JSON,
},
},
update: {
"CouchDB Document": {
type: QUERY_TYPES.JSON,
},
},
delete: {
"Document ID": {
type: QUERY_TYPES.FIELDS,
fields: {
id: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},
},
},
}
class CouchDBIntegration {
constructor(config) {
this.config = config
this.client = new PouchDB(`${config.url}/${config.database}`)
}
async create(query) {
try {
const result = await this.client.post(query.json)
return result
} catch (err) {
console.error("Error writing to couchDB", err)
throw err
}
}
async read(query) {
try {
const result = await this.client.allDocs({
include_docs: true,
...query.json,
})
return result.rows.map(row => row.doc)
} catch (err) {
console.error("Error querying couchDB", err)
throw err
}
}
async update(query) {
try {
const result = await this.client.put(query.json)
return result
} catch (err) {
console.error("Error updating couchDB document", err)
throw err
}
}
async delete(query) {
try {
const result = await this.client.remove(query.id)
return result
} catch (err) {
console.error("Error deleting couchDB document", err)
throw err
}
}
}
module.exports = {
schema: SCHEMA,
integration: CouchDBIntegration,
}

View File

@ -0,0 +1,128 @@
const AWS = require("aws-sdk")
const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
const SCHEMA = {
docs: "https://github.com/dabit3/dynamodb-documentclient-cheat-sheet",
datasource: {
region: {
type: FIELD_TYPES.STRING,
required: true,
default: "us-east-1",
},
accessKeyId: {
type: FIELD_TYPES.PASSWORD,
required: true,
},
secretKey: {
type: FIELD_TYPES.PASSWORD,
required: true,
},
},
query: {
create: {
DynamoConfig: {
type: QUERY_TYPES.FIELDS,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
customisable: true,
},
},
},
read: {
DynamoConfig: {
type: QUERY_TYPES.FIELDS,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
index: {
type: FIELD_TYPES.STRING,
},
customisable: true,
},
},
},
update: {
DynamoConfig: {
type: QUERY_TYPES.FIELDS,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
customisable: true,
},
},
},
delete: {
"Dynamo Partition Key": {
type: QUERY_TYPES.FIELDS,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
key: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},
},
},
}
class DynamoDBIntegration {
constructor(config) {
this.config = config
this.connect()
this.client = new AWS.DynamoDB.DocumentClient()
}
async connect() {
AWS.config.update(this.config)
}
async create(query) {
const response = await this.client.query({
TableName: query.table,
Item: query.json,
})
return response
}
async read(query) {
const response = await this.client.query({
TableName: query.Table,
...query.json,
})
return response
}
async update(query) {
const response = await this.client.query({
TableName: query.Table,
...query.json,
})
return response
}
async delete(query) {
const response = await this.client.query({
TableName: query.Table,
Key: {
id: query.key,
},
})
return response
}
}
module.exports = {
schema: SCHEMA,
integration: DynamoDBIntegration,
}

View File

@ -0,0 +1,145 @@
const { Client } = require("@elastic/elasticsearch")
const { QUERY_TYPES, FIELD_TYPES } = require("./Integration")
const SCHEMA = {
docs:
"https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html",
datasource: {
url: {
type: "string",
required: true,
default: "http://localhost:9200",
},
},
query: {
create: {
"ES Query DSL": {
type: QUERY_TYPES.FIELDS,
customisable: true,
fields: {
index: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},
},
read: {
"ES Query DSL": {
type: QUERY_TYPES.FIELDS,
customisable: true,
fields: {
index: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},
},
update: {
"ES Query DSL": {
type: QUERY_TYPES.FIELDS,
customisable: true,
fields: {
id: {
type: FIELD_TYPES.STRING,
required: true,
},
index: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},
},
delete: {
"Document ID": {
type: QUERY_TYPES.FIELDS,
fields: {
index: {
type: FIELD_TYPES.STRING,
required: true,
},
id: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},
},
},
}
class ElasticSearchIntegration {
constructor(config) {
this.config = config
this.client = new Client({ node: config.url })
}
async create(query) {
const { index, json } = query
try {
const result = await this.client.index({
index,
body: json,
})
return result.body
} catch (err) {
console.error("Error writing to elasticsearch", err)
throw err
} finally {
await this.client.close()
}
}
async read(query) {
const { index, json } = query
try {
const result = await this.client.search({
index: index,
body: json,
})
return result.body.hits.hits.map(({ _source }) => _source)
} catch (err) {
console.error("Error querying elasticsearch", err)
throw err
} finally {
await this.client.close()
}
}
async update(query) {
const { id, index, json } = query
try {
const result = await this.client.update({
id,
index,
body: json,
})
return result.body
} catch (err) {
console.error("Error querying elasticsearch", err)
throw err
} finally {
await this.client.close()
}
}
async delete(query) {
try {
const result = await this.client.delete(query)
return result.body
} catch (err) {
console.error("Error deleting from elasticsearch", err)
throw err
} finally {
await this.client.close()
}
}
}
module.exports = {
schema: SCHEMA,
integration: ElasticSearchIntegration,
}

View File

@ -0,0 +1,36 @@
const postgres = require("./postgres")
const dynamodb = require("./dynamodb")
const mongodb = require("./mongodb")
const elasticsearch = require("./elasticsearch")
const couchdb = require("./couchdb")
// const redis = require("./redis")
const sqlServer = require("./microsoftSqlServer")
const s3 = require("./s3")
const airtable = require("./airtable")
const DEFINITIONS = {
POSTGRES: postgres.schema,
DYNAMODB: dynamodb.schema,
MONGODB: mongodb.schema,
ELASTICSEARCH: elasticsearch.schema,
COUCHDB: couchdb.schema,
SQL_SERVER: sqlServer.schema,
S3: s3.schema,
AIRTABLE: airtable.schema,
}
const INTEGRATIONS = {
POSTGRES: postgres.integration,
DYNAMODB: dynamodb.integration,
MONGODB: mongodb.integration,
ELASTICSEARCH: elasticsearch.integration,
COUCHDB: couchdb.integration,
S3: s3.integration,
SQL_SERVER: sqlServer.integration,
AIRTABLE: airtable.integration,
}
module.exports = {
definitions: DEFINITIONS,
integrations: INTEGRATIONS,
}

Some files were not shown because too many files have changed in this diff Show More