diff --git a/README.md b/README.md index a1d9f62f0..692c6b710 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,6 @@ GitHub release (latest by date) - - Discord - Follow @budibase @@ -52,8 +49,6 @@ Report a bug ยท Support: Discussions - & - Discord @@ -61,7 +56,7 @@ - **Build and ship real software.** Unlike other platforms, with Budibase you build and ship single page applications. Budibase applications have performance baked in and can be designed responsively, providing your users with a great experience. -- **Open source and extensable.** Budibase is open-source - licensed as GPL v3. This should fill you with confidence that Budibase will always be around. You can also code against Budibase or fork it and make changes as you please, providing a developer-friendly experience. +- **Open source and extensible.** Budibase is open-source - licensed as GPL v3. This should fill you with confidence that Budibase will always be around. You can also code against Budibase or fork it and make changes as you please, providing a developer-friendly experience. - **Load data or start from scratch.** Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, mySQL, Airtable, S3, DyanmoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). @@ -79,28 +74,6 @@ --- -## โŒ› 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 [get started](https://docs.budibase.com/getting-started). -- [ ] Official Launch - -Watch "releases" of this repo to get notified of major updates, and give the star button a click whilst you're there. - -

- -

- -### Stargazers over time - -[![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase) - -If you are having issues between updates of the builder, please use the guide [here](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting) to clear down your environment. - - ---- - - ## ๐Ÿ Getting Started with Budibase in 5 minutes To get started, you must have docker and docker compose installed on your machine. @@ -141,21 +114,40 @@ Done! You are now ready to build powerful internal tools in minutes. For additio The Budibase [documentation lives here](https://docs.budibase.com). -You can also follow a quick tutorial on [how to build a CRM with Budibase](https://docs.budibase.com/tutorial/tutorial-introduction) + +--- + +## โŒ› 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 [get started](https://docs.budibase.com/getting-started). +- [ ] Official Launch + +Watch "releases" of this repo to get notified of major updates, and give the star button a click whilst you're there. + +

+ +

--- +## Stargazers over time + +[![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase) -## Roadmap +If you are having issues between updates of the builder, please use the guide [here](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting) to clear down your environment. -Checkout our [Public Roadmap](https://github.com/Budibase/budibase/projects/10). If you would like to discuss some of the items on the roadmap, please feel to reach out on [Discord](https://discord.gg/rCYayfe), or via [Github discussions](https://github.com/Budibase/budibase/discussions) + +--- ## โ— Code of Conduct Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). Please read it. +--- + ## ๐Ÿ™Œ Contributing to Budibase From opening a bug report to creating a pull request: every contribution is appreciated and welcomed. If you're planning to implement a new feature or change the API please create an issue first. This way we can ensure your work is not in vain. @@ -188,11 +180,7 @@ Budibase is open-source. The builder is licensed [AGPL v3](https://www.gnu.org/l ## ๐Ÿ’ฌ Get in touch -If you have a question or would like to talk with other Budibase users, please hop over to [Github discussions](https://github.com/Budibase/budibase/discussions) or join our Discord server: - -[Discord chatroom](https://discord.gg/rCYayfe) - -![Discord Shield](https://discordapp.com/api/guilds/733030666647765003/widget.png?style=shield) +If you have a question or would like to talk with other Budibase users and join our community, please hop over to [Github discussions](https://github.com/Budibase/budibase/discussions) --- diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml index 0cd7bc92b..b03b433ce 100644 --- a/hosting/docker-compose.yaml +++ b/hosting/docker-compose.yaml @@ -24,6 +24,8 @@ services: ENABLE_ANALYTICS: "true" REDIS_URL: redis-service:6379 REDIS_PASSWORD: ${REDIS_PASSWORD} + volumes: + - ./logs:/logs depends_on: - worker-service @@ -46,6 +48,8 @@ services: INTERNAL_API_KEY: ${INTERNAL_API_KEY} REDIS_URL: redis-service:6379 REDIS_PASSWORD: ${REDIS_PASSWORD} + volumes: + - ./logs:/logs depends_on: - redis-service - minio-service @@ -109,6 +113,21 @@ services: - "${REDIS_PORT}:6379" volumes: - redis_data:/data + + watchtower-service: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + command: --debug --http-api-update budibase/apps budibase/worker + environment: + - WATCHTOWER_HTTP_API=true + - WATCHTOWER_HTTP_API_TOKEN=budibase + - WATCHTOWER_CLEANUP=true + labels: + - "com.centurylinklabs.watchtower.enable=false" + ports: + - 6161:8080 + volumes: couchdb3_data: diff --git a/hosting/envoy.dev.yaml.hbs b/hosting/envoy.dev.yaml.hbs index 054ceb9d3..080311ee4 100644 --- a/hosting/envoy.dev.yaml.hbs +++ b/hosting/envoy.dev.yaml.hbs @@ -133,4 +133,3 @@ static_resources: socket_address: address: {{ address }} port_value: 4002 - diff --git a/hosting/envoy.yaml b/hosting/envoy.yaml index 463b32ab6..20e912434 100644 --- a/hosting/envoy.yaml +++ b/hosting/envoy.yaml @@ -21,6 +21,10 @@ static_resources: cluster: app-service prefix_rewrite: "/" + - match: { path: "/v1/update" } + route: + cluster: watchtower-service + - match: { prefix: "/builder/" } route: cluster: app-service @@ -123,3 +127,17 @@ static_resources: address: couchdb-service port_value: 5984 + - name: watchtower-service + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + load_assignment: + cluster_name: watchtower-service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: watchtower-service + port_value: 6161 + diff --git a/lerna.json b/lerna.json index 301a2ed2d..105efb7cd 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.9.21", + "version": "0.9.27", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/auth/package.json b/packages/auth/package.json index c4be809e0..d2704c861 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/auth", - "version": "0.9.21", + "version": "0.9.27", "description": "Authentication middlewares for budibase builder and apps", "main": "src/index.js", "author": "Budibase", diff --git a/packages/auth/src/index.js b/packages/auth/src/index.js index d2ba86a52..9582d6ffd 100644 --- a/packages/auth/src/index.js +++ b/packages/auth/src/index.js @@ -2,7 +2,7 @@ const passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy const { StaticDatabases } = require("./db/utils") -const { jwt, local, authenticated, google } = require("./middleware") +const { jwt, local, authenticated, google, auditLog } = require("./middleware") const { setDB, getDB } = require("./db") // Strategies @@ -45,6 +45,7 @@ module.exports = { passport, google, jwt: require("jsonwebtoken"), + auditLog, }, StaticDatabases, constants: require("./constants"), diff --git a/packages/auth/src/middleware/auditLog.js b/packages/auth/src/middleware/auditLog.js new file mode 100644 index 000000000..c9063ae2e --- /dev/null +++ b/packages/auth/src/middleware/auditLog.js @@ -0,0 +1,4 @@ +module.exports = async (ctx, next) => { + // Placeholder for audit log middleware + return next() +} diff --git a/packages/auth/src/middleware/index.js b/packages/auth/src/middleware/index.js index 519233eda..2a249ce0f 100644 --- a/packages/auth/src/middleware/index.js +++ b/packages/auth/src/middleware/index.js @@ -2,10 +2,12 @@ const jwt = require("./passport/jwt") const local = require("./passport/local") const google = require("./passport/google") const authenticated = require("./authenticated") +const auditLog = require("./auditLog") module.exports = { google, jwt, local, authenticated, + auditLog, } diff --git a/packages/auth/src/objectStore/index.js b/packages/auth/src/objectStore/index.js index a157332ae..80875fdfe 100644 --- a/packages/auth/src/objectStore/index.js +++ b/packages/auth/src/objectStore/index.js @@ -159,7 +159,7 @@ exports.upload = async ({ * Similar to the upload function but can be used to send a file stream * through to the object store. */ -exports.streamUpload = async (bucketName, filename, stream) => { +exports.streamUpload = async (bucketName, filename, stream, extra = {}) => { const objectStore = exports.ObjectStore(bucketName) await exports.makeSureBucketExists(objectStore, bucketName) @@ -167,6 +167,7 @@ exports.streamUpload = async (bucketName, filename, stream) => { Bucket: sanitizeBucket(bucketName), Key: sanitizeKey(filename), Body: stream, + ...extra, } return objectStore.upload(params).promise() } diff --git a/packages/auth/src/security/roles.js b/packages/auth/src/security/roles.js index d652c25b0..53e1b90d7 100644 --- a/packages/auth/src/security/roles.js +++ b/packages/auth/src/security/roles.js @@ -13,7 +13,6 @@ const BUILTIN_IDS = { POWER: "POWER", BASIC: "BASIC", PUBLIC: "PUBLIC", - BUILDER: "BUILDER", } // exclude internal roles like builder diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 48ea168f9..834e9dcd6 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "0.9.21", + "version": "0.9.27", "license": "AGPL-3.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", diff --git a/packages/bbui/src/Form/Core/RadioGroup.svelte b/packages/bbui/src/Form/Core/RadioGroup.svelte index 4ead9ed31..d7941b251 100644 --- a/packages/bbui/src/Form/Core/RadioGroup.svelte +++ b/packages/bbui/src/Form/Core/RadioGroup.svelte @@ -37,3 +37,9 @@ {/each} {/if} + + diff --git a/packages/builder/package.json b/packages/builder/package.json index a78d61956..5c19608da 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "0.9.21", + "version": "0.9.27", "license": "AGPL-3.0", "private": true, "scripts": { @@ -65,10 +65,10 @@ } }, "dependencies": { - "@budibase/bbui": "^0.9.21", - "@budibase/client": "^0.9.21", + "@budibase/bbui": "^0.9.27", + "@budibase/client": "^0.9.27", "@budibase/colorpicker": "1.1.2", - "@budibase/string-templates": "^0.9.21", + "@budibase/string-templates": "^0.9.27", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 89532e3bc..5ce8e407c 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -136,7 +136,7 @@ const getContextBindings = (asset, componentId) => { if (!datasource) { return } - const info = getSchemaForDatasource(datasource) + const info = getSchemaForDatasource(asset, datasource) schema = info.schema readablePrefix = info.table?.name } @@ -191,7 +191,7 @@ const getContextBindings = (asset, componentId) => { */ const getUserBindings = () => { let bindings = [] - const { schema } = getSchemaForDatasource({ + const { schema } = getSchemaForDatasource(null, { type: "table", tableId: TableNames.USERS, }) @@ -244,11 +244,15 @@ const getUrlBindings = asset => { /** * Gets a schema for a datasource object. */ -export const getSchemaForDatasource = (datasource, isForm = false) => { +export const getSchemaForDatasource = (asset, datasource, isForm = false) => { let schema, table if (datasource) { const { type } = datasource - if (type === "query") { + if (type === "provider") { + const component = findComponent(asset.props, datasource.providerId) + const source = getDatasourceForProvider(asset, component) + return getSchemaForDatasource(asset, source, isForm) + } else if (type === "query") { const queries = get(queriesStores).list table = queries.find(query => query._id === datasource._id) } else { diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js index 02b3c20a2..e234a1a77 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js @@ -174,7 +174,7 @@ const fieldTypeToComponentMap = { } export function makeDatasourceFormComponents(datasource) { - const { schema } = getSchemaForDatasource(datasource, true) + const { schema } = getSchemaForDatasource(null, datasource, true) let components = [] let fields = Object.keys(schema || {}) fields.forEach(field => { diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index f68cc95e2..152f2e72c 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -43,6 +43,7 @@ } let originalName = field.name + const linkEditDisabled = originalName != null let primaryDisplay = $tables.selected.primaryDisplay == null || $tables.selected.primaryDisplay === field.name @@ -198,7 +199,7 @@ + {:else if field.type === FORMULA_TYPE} - import { getBindableProperties } from "builderStore/dataBinding" + import { + getBindableProperties, + getDataProviderComponents, + } from "builderStore/dataBinding" import { Button, Popover, @@ -61,6 +64,17 @@ $currentAsset, $store.selectedComponentId ) + $: dataProviders = getDataProviderComponents( + $currentAsset, + $store.selectedComponentId + ).map(provider => ({ + label: provider._instanceName, + name: provider._instanceName, + providerId: provider._id, + value: `{{ literal [${provider._id}] }}`, + type: "provider", + schema: provider.schema, + })) $: queryBindableProperties = bindableProperties.map(property => ({ ...property, category: property.type === "instance" ? "Component" : "Table", @@ -182,7 +196,20 @@ {/each} - + +
+ Data Providers +
+ {#if otherSources?.length}
diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveRow.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveRow.svelte index 70e44ea83..664129ee0 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveRow.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/SaveRow.svelte @@ -14,11 +14,11 @@ $currentAsset, $store.selectedComponentId ) - $: schemaFields = getSchemaFields(parameters?.tableId) + $: schemaFields = getSchemaFields($currentAsset, parameters?.tableId) $: tableOptions = $tables.list || [] - const getSchemaFields = tableId => { - const { schema } = getSchemaForDatasource({ type: "table", tableId }) + const getSchemaFields = (asset, tableId) => { + const { schema } = getSchemaForDatasource(asset, { type: "table", tableId }) return Object.values(schema || {}) } diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js index e851bdb4b..a3e43cb04 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js @@ -5,11 +5,13 @@ import ExecuteQuery from "./ExecuteQuery.svelte" import TriggerAutomation from "./TriggerAutomation.svelte" import ValidateForm from "./ValidateForm.svelte" -// defines what actions are available, when adding a new one -// the component is the setup panel for the action -// NOTE that the "name" is used by the client library, -// so if you want to change it, you must change it client lib too - +// Defines which actions are available to configure in the front end. +// Unfortunately the "name" property is used as the identifier so please don't +// change them. +// The client library removes any spaces when processing actions, so they can +// be considered as camel case too. +// There is technical debt here to sanitize all these and standardise them +// across the packages but it's a breaking change to existing apps. export default [ { name: "Save Row", diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FieldSelect.svelte index dd5436c1f..9b6aec458 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FieldSelect.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FieldSelect.svelte @@ -14,7 +14,7 @@ const dispatch = createEventDispatcher() $: datasource = getDatasourceForProvider($currentAsset, componentInstance) - $: schema = getSchemaForDatasource(datasource).schema + $: schema = getSchemaForDatasource($currentAsset, datasource).schema $: options = Object.keys(schema || {}) $: boundValue = getValidValue(value, options) diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte index 166233740..20a7b9d9f 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterEditor.svelte @@ -27,19 +27,16 @@ ? tempValue.length : Object.keys(tempValue || {}).length $: dataSource = getDatasourceForProvider($currentAsset, componentInstance) - $: schema = getSchemaForDatasource(dataSource)?.schema + $: schema = getSchemaForDatasource($currentAsset, dataSource)?.schema $: schemaFields = Object.values(schema || {}) $: internalTable = dataSource?.type === "table" // Reset value if value is wrong type for the datasource. // Lucene editor needs an array, and simple editor needs an object. $: { - if (internalTable && !Array.isArray(value)) { + if (!Array.isArray(value)) { tempValue = [] dispatch("change", []) - } else if (!internalTable && Array.isArray(value)) { - tempValue = {} - dispatch("change", {}) } } @@ -63,28 +60,7 @@ constaints. {/if} - {#if internalTable} - - {:else} -
- (tempValue = e.detail)} - /> -
- {/if} + - - diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte index 1d7f5367d..1824bf7f5 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte @@ -18,7 +18,7 @@ component => component._component === "@budibase/standard-components/form" ) $: datasource = getDatasourceForProvider($currentAsset, form) - $: schema = getSchemaForDatasource(datasource, true).schema + $: schema = getSchemaForDatasource($currentAsset, datasource, true).schema $: options = getOptions(schema, type) const getOptions = (schema, fieldType) => { diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/MultiFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/MultiFieldSelect.svelte index 44cea7a62..e3f38d88d 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/MultiFieldSelect.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/MultiFieldSelect.svelte @@ -14,7 +14,7 @@ const dispatch = createEventDispatcher() $: datasource = getDatasourceForProvider($currentAsset, componentInstance) - $: schema = getSchemaForDatasource(datasource).schema + $: schema = getSchemaForDatasource($currentAsset, datasource).schema $: options = Object.keys(schema || {}) $: boundValue = getValidOptions(value, options) diff --git a/packages/builder/src/pages/builder/portal/_layout.svelte b/packages/builder/src/pages/builder/portal/_layout.svelte index 0f691a33b..4bc0b4616 100644 --- a/packages/builder/src/pages/builder/portal/_layout.svelte +++ b/packages/builder/src/pages/builder/portal/_layout.svelte @@ -43,6 +43,10 @@ title: "Theming", href: "/builder/portal/settings/theming", }, + { + title: "Updates", + href: "/builder/portal/settings/update", + }, ]) } else { menu = menu.concat([ diff --git a/packages/builder/src/pages/builder/portal/settings/organisation.svelte b/packages/builder/src/pages/builder/portal/settings/organisation.svelte index 046f55615..f8f4c8a7d 100644 --- a/packages/builder/src/pages/builder/portal/settings/organisation.svelte +++ b/packages/builder/src/pages/builder/portal/settings/organisation.svelte @@ -12,7 +12,7 @@ notifications, } from "@budibase/bbui" import { auth, organisation } from "stores/portal" - import { post } from "builderStore/api" + import { post, get } from "builderStore/api" import analytics from "analytics" import { writable } from "svelte/store" import { redirect } from "@roxi/routify" @@ -130,10 +130,10 @@
+
+ +
-
- -
{/if} diff --git a/packages/builder/src/pages/builder/portal/settings/update.svelte b/packages/builder/src/pages/builder/portal/settings/update.svelte new file mode 100644 index 000000000..5835f9d0d --- /dev/null +++ b/packages/builder/src/pages/builder/portal/settings/update.svelte @@ -0,0 +1,70 @@ + + +{#if $auth.isAdmin} + + + Update + + Keep your budibase installation up to date to take advantage of the + latest features, security updates and much more. + + + +
+
+ +
+
+
+{/if} + + diff --git a/packages/cli/package.json b/packages/cli/package.json index 2243e135f..519db352f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "0.9.21", + "version": "0.9.27", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { diff --git a/packages/client/package.json b/packages/client/package.json index fdcc1d830..ee022f655 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "0.9.21", + "version": "0.9.27", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -18,13 +18,13 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/string-templates": "^0.9.21", + "@budibase/string-templates": "^0.9.27", "regexparam": "^1.3.0", "shortid": "^2.2.15", "svelte-spa-router": "^3.0.5" }, "devDependencies": { - "@budibase/standard-components": "^0.9.21", + "@budibase/standard-components": "^0.9.27", "@rollup/plugin-commonjs": "^18.0.0", "@rollup/plugin-node-resolve": "^11.2.1", "fs-extra": "^8.1.0", diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index b32222ce2..4d2dddffc 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -55,13 +55,13 @@ } // Enriches any string component props using handlebars - const updateComponentProps = async (definition, context) => { + const updateComponentProps = (definition, context) => { // Record the timestamp so we can reference it after enrichment latestUpdateTime = Date.now() const enrichmentTime = latestUpdateTime // Enrich props with context - const enrichedProps = await enrichProps(definition, context) + const enrichedProps = enrichProps(definition, context) // Abandon this update if a newer update has started if (enrichmentTime !== latestUpdateTime) { diff --git a/packages/client/src/constants.js b/packages/client/src/constants.js index 3aa302bec..31ac4b285 100644 --- a/packages/client/src/constants.js +++ b/packages/client/src/constants.js @@ -5,4 +5,5 @@ export const TableNames = { export const ActionTypes = { ValidateForm: "ValidateForm", RefreshDatasource: "RefreshDatasource", + SetDataProviderQuery: "SetDataProviderQuery", } diff --git a/packages/client/src/sdk.js b/packages/client/src/sdk.js index 7eef69441..bf467e141 100644 --- a/packages/client/src/sdk.js +++ b/packages/client/src/sdk.js @@ -9,6 +9,7 @@ import { import { styleable } from "./utils/styleable" import transition from "./utils/transition" import { linkable } from "./utils/linkable" +import { getAction } from "./utils/getAction" import Provider from "./components/Provider.svelte" import { ActionTypes } from "./constants" @@ -22,6 +23,7 @@ export default { styleable, transition, linkable, + getAction, Provider, ActionTypes, } diff --git a/packages/client/src/utils/componentProps.js b/packages/client/src/utils/componentProps.js index 559fc5448..14516fdb4 100644 --- a/packages/client/src/utils/componentProps.js +++ b/packages/client/src/utils/componentProps.js @@ -21,7 +21,7 @@ export const propsAreSame = (a, b) => { * Enriches component props. * Data bindings are enriched, and button actions are enriched. */ -export const enrichProps = async (props, context) => { +export const enrichProps = (props, context) => { // Exclude all private props that start with an underscore let validProps = {} Object.entries(props) @@ -41,7 +41,7 @@ export const enrichProps = async (props, context) => { } // Enrich all data bindings in top level props - let enrichedProps = await enrichDataBindings(validProps, totalContext) + let enrichedProps = enrichDataBindings(validProps, totalContext) // Enrich click actions if they exist if (enrichedProps.onClick) { diff --git a/packages/client/src/utils/enrichDataBinding.js b/packages/client/src/utils/enrichDataBinding.js index b8c5020c7..34bfb7853 100644 --- a/packages/client/src/utils/enrichDataBinding.js +++ b/packages/client/src/utils/enrichDataBinding.js @@ -1,5 +1,5 @@ import { cloneDeep } from "lodash/fp" -import { processString, processObject } from "@budibase/string-templates" +import { processString, processObjectSync } from "@budibase/string-templates" // Regex to test inputs with to see if they are likely candidates for template strings const looksLikeTemplate = /{{.*}}/ @@ -23,6 +23,6 @@ export const enrichDataBinding = async (input, context) => { * Recursively enriches all props in a props object and returns the new props. * Props are deeply cloned so that no mutation is done to the source object. */ -export const enrichDataBindings = async (props, context) => { - return await processObject(cloneDeep(props), context) +export const enrichDataBindings = (props, context) => { + return processObjectSync(cloneDeep(props), context) } diff --git a/packages/client/src/utils/getAction.js b/packages/client/src/utils/getAction.js new file mode 100644 index 000000000..cd97fcf07 --- /dev/null +++ b/packages/client/src/utils/getAction.js @@ -0,0 +1,19 @@ +import { get } from "svelte/store" +import { getContext } from "svelte" + +/** + * Gets a component action. + * @param id The component ID that provides the action + * @param type The action type to get + * @returns {null|*} The action function + */ +export const getAction = (id, type) => { + if (!id || !type) { + return null + } + const context = getContext("context") + if (!context) { + return null + } + return get(context)?.[`${id}_${type}`] +} diff --git a/packages/server/package.json b/packages/server/package.json index 38f4865bd..4d1691d37 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "0.9.21", + "version": "0.9.27", "description": "Budibase Web Server", "main": "src/electron.js", "repository": { @@ -55,9 +55,9 @@ "author": "Budibase", "license": "AGPL-3.0-or-later", "dependencies": { - "@budibase/auth": "^0.9.21", - "@budibase/client": "^0.9.21", - "@budibase/string-templates": "^0.9.21", + "@budibase/auth": "^0.9.27", + "@budibase/client": "^0.9.27", + "@budibase/string-templates": "^0.9.27", "@elastic/elasticsearch": "7.10.0", "@koa/router": "8.0.0", "@sendgrid/mail": "7.1.1", @@ -109,7 +109,7 @@ "devDependencies": { "@babel/core": "^7.14.3", "@babel/preset-env": "^7.14.4", - "@budibase/standard-components": "^0.9.21", + "@budibase/standard-components": "^0.9.27", "@jest/test-sequencer": "^24.8.0", "babel-jest": "^27.0.2", "docker-compose": "^0.23.6", diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index 30d6e76fb..36dc39999 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -99,12 +99,18 @@ async function createInstance(template) { // replicate the template data to the instance DB // this is currently very hard to test, downloading and importing template files /* istanbul ignore next */ + let _rev if (template && template.useTemplate === "true") { const { ok } = await db.load(await getTemplateStream(template)) if (!ok) { throw "Error loading database dump from template." } - var { _rev } = await db.get(DocumentTypes.APP_METADATA) + try { + const response = await db.get(DocumentTypes.APP_METADATA) + _rev = response._rev + } catch (err) { + _rev = null + } } else { // create the users table await db.put(USERS_TABLE_SCHEMA) diff --git a/packages/server/src/api/controllers/auth.js b/packages/server/src/api/controllers/auth.js index 92d731cfb..da863f549 100644 --- a/packages/server/src/api/controllers/auth.js +++ b/packages/server/src/api/controllers/auth.js @@ -5,7 +5,7 @@ const { getFullUser } = require("../../utilities/users") exports.fetchSelf = async ctx => { const appId = ctx.appId - const { userId } = ctx.user + let userId = ctx.user.userId || ctx.user._id /* istanbul ignore next */ if (!userId) { ctx.body = {} diff --git a/packages/server/src/api/controllers/routing.js b/packages/server/src/api/controllers/routing.js index 1bbb521ea..d281b92fe 100644 --- a/packages/server/src/api/controllers/routing.js +++ b/packages/server/src/api/controllers/routing.js @@ -63,10 +63,6 @@ exports.fetch = async ctx => { exports.clientFetch = async ctx => { const routing = await getRoutingStructure(ctx.appId) let roleId = ctx.user.role._id - // builder is a special case, always return the full routing structure - if (roleId === BUILTIN_ROLE_IDS.BUILDER) { - roleId = BUILTIN_ROLE_IDS.ADMIN - } const roleIds = await getUserRoleHierarchy(ctx.appId, roleId) for (let topLevel of Object.values(routing.routes)) { for (let subpathKey of Object.keys(topLevel.subpaths)) { diff --git a/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte b/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte index b73cfc6a3..f73f79c09 100644 --- a/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte +++ b/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte @@ -31,7 +31,6 @@ margin: 0; padding: 0; } - *, *:before, *:after { @@ -41,9 +40,9 @@ - - diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js index 73ba56943..fc207c479 100644 --- a/packages/server/src/api/controllers/user.js +++ b/packages/server/src/api/controllers/user.js @@ -4,7 +4,6 @@ const { getUserMetadataParams, } = require("../../db/utils") const { InternalTables } = require("../../db/utils") -const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") const { getGlobalUsers, addAppRoleToUser, @@ -47,10 +46,6 @@ exports.fetchMetadata = async function (ctx) { exports.updateSelfMetadata = async function (ctx) { // overwrite the ID with current users ctx.request.body._id = ctx.user._id - if (ctx.user.builder && ctx.user.builder.global) { - // specific case, update self role in global user - await addAppRoleToUser(ctx, ctx.appId, BUILTIN_ROLE_IDS.ADMIN) - } // make sure no stale rev delete ctx.request.body._rev await exports.updateMetadata(ctx) diff --git a/packages/server/src/api/index.js b/packages/server/src/api/index.js index 332b917a7..6c4188a5d 100644 --- a/packages/server/src/api/index.js +++ b/packages/server/src/api/index.js @@ -1,5 +1,5 @@ const Router = require("@koa/router") -const { buildAuthMiddleware } = require("@budibase/auth").auth +const { buildAuthMiddleware, auditLog } = require("@budibase/auth").auth const currentApp = require("../middleware/currentapp") const compress = require("koa-compress") const zlib = require("zlib") @@ -37,6 +37,7 @@ router }) ) .use(currentApp) + .use(auditLog) // error handling middleware router.use(async (ctx, next) => { diff --git a/packages/server/src/api/routes/tests/routing.spec.js b/packages/server/src/api/routes/tests/routing.spec.js index 622552c77..38cd62ae7 100644 --- a/packages/server/src/api/routes/tests/routing.spec.js +++ b/packages/server/src/api/routes/tests/routing.spec.js @@ -28,9 +28,7 @@ describe("/routing", () => { it("returns the correct routing for basic user", async () => { workerRequests.getGlobalUsers.mockImplementationOnce((ctx, appId) => { return { - roles: { - [appId]: BUILTIN_ROLE_IDS.BASIC, - } + roleId: BUILTIN_ROLE_IDS.BASIC, } }) const res = await request diff --git a/packages/server/src/api/routes/tests/utilities/TestFunctions.js b/packages/server/src/api/routes/tests/utilities/TestFunctions.js index c49e44c94..dfd77eec7 100644 --- a/packages/server/src/api/routes/tests/utilities/TestFunctions.js +++ b/packages/server/src/api/routes/tests/utilities/TestFunctions.js @@ -2,6 +2,7 @@ const rowController = require("../../../controllers/row") const appController = require("../../../controllers/application") const CouchDB = require("../../../../db") const { AppStatus } = require("../../../../db/utils") +const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") function Request(appId, params) { this.appId = appId @@ -77,11 +78,17 @@ exports.checkPermissionsEndpoint = async ({ .set(passHeader) .expect(200) - user = await config.createUser("fail@budibase.com", password, failRole) - const failHeader = await config.login("fail@budibase.com", password, { - roleId: failRole, - userId: user.globalId, - }) + let failHeader + if (failRole === BUILTIN_ROLE_IDS.PUBLIC) { + failHeader = config.publicHeaders() + } else { + user = await config.createUser("fail@budibase.com", password, failRole) + failHeader = await config.login("fail@budibase.com", password, { + roleId: failRole, + userId: user.globalId, + builder: false, + }) + } await exports .createRequest(config.request, method, url, body) diff --git a/packages/server/src/app.js b/packages/server/src/app.js index 50df056b2..d7fc248bb 100644 --- a/packages/server/src/app.js +++ b/packages/server/src/app.js @@ -5,7 +5,7 @@ require("@budibase/auth").init(CouchDB) const Koa = require("koa") const destroyable = require("server-destroy") const koaBody = require("koa-body") -const logger = require("koa-pino-logger") +const pino = require("koa-pino-logger") const http = require("http") const api = require("./api") const eventEmitter = require("./events") @@ -29,7 +29,7 @@ app.use( ) app.use( - logger({ + pino({ prettyPrint: { levelFirst: true, }, diff --git a/packages/server/src/automations/actions.js b/packages/server/src/automations/actions.js index ad102e7b6..762ce677a 100644 --- a/packages/server/src/automations/actions.js +++ b/packages/server/src/automations/actions.js @@ -4,6 +4,7 @@ const createRow = require("./steps/createRow") const updateRow = require("./steps/updateRow") const deleteRow = require("./steps/deleteRow") const executeScript = require("./steps/executeScript") +const bash = require("./steps/bash") const executeQuery = require("./steps/executeQuery") const outgoingWebhook = require("./steps/outgoingWebhook") const env = require("../environment") @@ -21,6 +22,7 @@ const BUILTIN_ACTIONS = { DELETE_ROW: deleteRow.run, OUTGOING_WEBHOOK: outgoingWebhook.run, EXECUTE_SCRIPT: executeScript.run, + EXECUTE_BASH: bash.run, EXECUTE_QUERY: executeQuery.run, } const BUILTIN_DEFINITIONS = { @@ -32,6 +34,7 @@ const BUILTIN_DEFINITIONS = { OUTGOING_WEBHOOK: outgoingWebhook.definition, EXECUTE_SCRIPT: executeScript.definition, EXECUTE_QUERY: executeQuery.definition, + EXECUTE_BASH: bash.definition, } let MANIFEST = null diff --git a/packages/server/src/automations/steps/bash.js b/packages/server/src/automations/steps/bash.js new file mode 100644 index 000000000..76d6713c5 --- /dev/null +++ b/packages/server/src/automations/steps/bash.js @@ -0,0 +1,62 @@ +const { execSync } = require("child_process") +const { processStringSync } = require("@budibase/string-templates") + +module.exports.definition = { + name: "Bash Scripting", + tagline: "Execute a bash command", + icon: "ri-terminal-box-line", + description: "Run a bash script", + type: "ACTION", + stepId: "EXECUTE_BASH", + inputs: {}, + schema: { + inputs: { + properties: { + code: { + type: "string", + customType: "code", + title: "Code", + }, + }, + required: ["code"], + }, + outputs: { + properties: { + stdout: { + type: "string", + description: "Standard output of your bash command or script.", + }, + }, + }, + required: ["stdout"], + }, +} + +module.exports.run = async function ({ inputs, context }) { + if (inputs.code == null) { + return { + stdout: "Budibase bash automation failed: Invalid inputs", + } + } + + try { + const command = processStringSync(inputs.code, context) + + let stdout + try { + stdout = execSync(command, { timeout: 500 }) + } catch (err) { + stdout = err.message + } + + return { + stdout, + } + } catch (err) { + console.error(err) + return { + success: false, + response: err, + } + } +} diff --git a/packages/server/src/automations/steps/executeScript.js b/packages/server/src/automations/steps/executeScript.js index 5bea1ab15..33ffd3ee8 100644 --- a/packages/server/src/automations/steps/executeScript.js +++ b/packages/server/src/automations/steps/executeScript.js @@ -1,7 +1,7 @@ const scriptController = require("../../api/controllers/script") module.exports.definition = { - name: "Scripting", + name: "JS Scripting", tagline: "Execute JavaScript Code", icon: "ri-terminal-box-line", description: "Run a piece of JavaScript code in your automation", diff --git a/packages/server/src/middleware/currentapp.js b/packages/server/src/middleware/currentapp.js index ae83da8ba..e47c9894f 100644 --- a/packages/server/src/middleware/currentapp.js +++ b/packages/server/src/middleware/currentapp.js @@ -33,7 +33,7 @@ module.exports = async (ctx, next) => { updateCookie = true appId = requestAppId // retrieving global user gets the right role - roleId = globalUser.roleId + roleId = globalUser.roleId || BUILTIN_ROLE_IDS.PUBLIC } else if (appCookie != null) { appId = appCookie.appId roleId = appCookie.roleId || BUILTIN_ROLE_IDS.PUBLIC diff --git a/packages/server/src/tests/utilities/TestConfiguration.js b/packages/server/src/tests/utilities/TestConfiguration.js index 60e503c12..4ba5abbb5 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.js +++ b/packages/server/src/tests/utilities/TestConfiguration.js @@ -101,7 +101,7 @@ class TestConfiguration { userId: GLOBAL_USER_ID, } const app = { - roleId: BUILTIN_ROLE_IDS.BUILDER, + roleId: BUILTIN_ROLE_IDS.ADMIN, appId: this.appId, } const authToken = jwt.sign(auth, env.JWT_SECRET) @@ -306,12 +306,9 @@ class TestConfiguration { return await this._req(config, null, controllers.layout.save) } - async createUser(roleId = BUILTIN_ROLE_IDS.POWER) { + async createUser() { const globalId = `us_${Math.random()}` - const resp = await this.globalUser( - globalId, - roleId === BUILTIN_ROLE_IDS.BUILDER - ) + const resp = await this.globalUser(globalId) return { ...resp, globalId, @@ -319,7 +316,6 @@ class TestConfiguration { } async login(email, password, { roleId, userId, builder } = {}) { - roleId = !roleId ? BUILTIN_ROLE_IDS.BUILDER : roleId userId = !userId ? `us_uuid1` : userId if (!this.request) { throw "Server has not been opened, cannot login." diff --git a/packages/server/src/utilities/fileSystem/newApp.js b/packages/server/src/utilities/fileSystem/newApp.js index 22113757c..735f0d523 100644 --- a/packages/server/src/utilities/fileSystem/newApp.js +++ b/packages/server/src/utilities/fileSystem/newApp.js @@ -30,5 +30,7 @@ exports.uploadClientLibrary = async appId => { const sourcepath = require.resolve("@budibase/client") const destPath = join(appId, "budibase-client.js") - await streamUpload(BUCKET_NAME, destPath, fs.createReadStream(sourcepath)) + await streamUpload(BUCKET_NAME, destPath, fs.createReadStream(sourcepath), { + ContentType: "application/javascript", + }) } diff --git a/packages/server/src/utilities/redis.js b/packages/server/src/utilities/redis.js index 18acb1aaa..e1fa63200 100644 --- a/packages/server/src/utilities/redis.js +++ b/packages/server/src/utilities/redis.js @@ -12,12 +12,8 @@ exports.init = async () => { } exports.shutdown = async () => { - if (devAppClient != null) { - await devAppClient.finish() - } - if (debounceClient != null) { - await debounceClient.finish() - } + if (devAppClient) await devAppClient.finish() + if (debounceClient) await debounceClient.finish() } exports.doesUserHaveLock = async (devAppId, user) => { diff --git a/packages/server/src/utilities/rowProcessor.js b/packages/server/src/utilities/rowProcessor.js index 2267c9e98..807563d47 100644 --- a/packages/server/src/utilities/rowProcessor.js +++ b/packages/server/src/utilities/rowProcessor.js @@ -169,9 +169,14 @@ exports.inputProcessing = (user = {}, table, row) => { let clonedRow = cloneDeep(row) // need to copy the table so it can be differenced on way out const copiedTable = cloneDeep(table) + const dontCleanseKeys = ["type", "_id", "_rev", "tableId"] for (let [key, value] of Object.entries(clonedRow)) { const field = table.schema[key] + // cleanse fields that aren't in the schema if (!field) { + if (dontCleanseKeys.indexOf(key) === -1) { + delete clonedRow[key] + } continue } clonedRow[key] = exports.coerce(value, field.type) diff --git a/packages/server/src/utilities/workerRequests.js b/packages/server/src/utilities/workerRequests.js index 99d9a1c3e..59ab2c296 100644 --- a/packages/server/src/utilities/workerRequests.js +++ b/packages/server/src/utilities/workerRequests.js @@ -9,19 +9,26 @@ function getAppRole(appId, user) { if (!user.roles) { return user } - // always use the deployed app - user.roleId = user.roles[getDeployedAppID(appId)] - if (!user.roleId) { - user.roleId = BUILTIN_ROLE_IDS.PUBLIC + if (user.builder && user.builder.global) { + user.roleId = BUILTIN_ROLE_IDS.ADMIN + } else { + // always use the deployed app + user.roleId = user.roles[getDeployedAppID(appId)] + if (!user.roleId) { + user.roleId = BUILTIN_ROLE_IDS.PUBLIC + } } delete user.roles return user } -function request(ctx, request) { +function request(ctx, request, noApiKey) { if (!request.headers) { request.headers = {} } + if (!noApiKey) { + request.headers["x-budibase-api-key"] = env.INTERNAL_API_KEY + } if (request.body && Object.keys(request.body).length > 0) { request.headers["Content-Type"] = "application/json" request.body = @@ -44,9 +51,6 @@ exports.sendSmtpEmail = async (to, from, subject, contents) => { checkSlashesInUrl(env.WORKER_URL + `/api/admin/email/send`), request(null, { method: "POST", - headers: { - "x-budibase-api-key": env.INTERNAL_API_KEY, - }, body: { email: to, from, @@ -86,16 +90,6 @@ exports.getDeployedApps = async ctx => { } } -exports.deleteGlobalUser = async (ctx, globalId) => { - const endpoint = `/api/admin/users/${globalId}` - const reqCfg = { method: "DELETE" } - const response = await fetch( - checkSlashesInUrl(env.WORKER_URL + endpoint), - request(ctx, reqCfg) - ) - return response.json() -} - exports.getGlobalUsers = async (ctx, appId = null, globalId = null) => { const endpoint = globalId ? `/api/admin/users/${globalId}` @@ -121,7 +115,8 @@ exports.getGlobalSelf = async (ctx, appId = null) => { const endpoint = `/api/admin/users/self` const response = await fetch( checkSlashesInUrl(env.WORKER_URL + endpoint), - request(ctx, { method: "GET" }) + // we don't want to use API key when getting self + request(ctx, { method: "GET" }, true) ) if (response.status !== 200) { ctx.throw(400, "Unable to get self globally.") @@ -172,9 +167,6 @@ exports.removeAppFromUserRoles = async appId => { checkSlashesInUrl(env.WORKER_URL + `/api/admin/roles/${deployedAppId}`), request(null, { method: "DELETE", - headers: { - "x-budibase-api-key": env.INTERNAL_API_KEY, - }, }) ) if (response.status !== 200) { diff --git a/packages/standard-components/manifest.json b/packages/standard-components/manifest.json index 9f67af648..8f0f63f68 100644 --- a/packages/standard-components/manifest.json +++ b/packages/standard-components/manifest.json @@ -1505,5 +1505,38 @@ "context": { "type": "schema" } + }, + "daterangepicker": { + "name": "Date Range", + "icon": "Date", + "styleable": true, + "hasChildren": false, + "info": "Your data provider will be automatically filtered to the given date range.", + "settings": [ + { + "type": "dataProvider", + "label": "Provider", + "key": "dataProvider" + }, + { + "type": "field", + "label": "Date field", + "key": "field" + }, + { + "type": "select", + "label": "Default range", + "key": "defaultValue", + "options": [ + "Last 1 day", + "Last 7 days", + "Last 30 days", + "Last 3 months", + "Last 6 months", + "Last 1 year" + ], + "defaultValue": "Last 30 days" + } + ] } } diff --git a/packages/standard-components/package.json b/packages/standard-components/package.json index 5261bb723..7b9b683ed 100644 --- a/packages/standard-components/package.json +++ b/packages/standard-components/package.json @@ -29,14 +29,15 @@ "keywords": [ "svelte" ], - "version": "0.9.21", + "version": "0.9.27", "license": "MIT", "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc", "dependencies": { - "@budibase/bbui": "^0.9.21", + "@budibase/bbui": "^0.9.27", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", "apexcharts": "^3.22.1", + "dayjs": "^1.10.5", "svelte-apexcharts": "^1.0.2", "svelte-flatpickr": "^3.1.0" } diff --git a/packages/standard-components/src/DataProvider.svelte b/packages/standard-components/src/DataProvider.svelte index e0b2ad859..7b759b534 100644 --- a/packages/standard-components/src/DataProvider.svelte +++ b/packages/standard-components/src/DataProvider.svelte @@ -1,6 +1,12 @@ + +
+