diff --git a/README.md b/README.md index a1d9f62f05..692c6b7100 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) + +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. --- -## Roadmap - -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 0cd7bc92bf..b03b433cef 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 054ceb9d3f..080311ee49 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 463b32ab60..20e912434a 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 301a2ed2d7..105efb7cd5 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 c4be809e08..d2704c8618 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 d2ba86a524..9582d6ffd6 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 0000000000..c9063ae2e0 --- /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 519233eda4..2a249ce0f9 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 a157332ae5..80875fdfee 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 d652c25b00..53e1b90d73 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 48ea168f97..834e9dcd60 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 4ead9ed311..d7941b2518 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 a78d619569..5c19608dab 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 89532e3bc3..5ce8e407c1 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 02b3c20a2f..e234a1a770 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 f68cc95e24..152f2e72c8 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 70e44ea83a..664129ee02 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 e851bdb4be..a3e43cb045 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 dd5436c1f7..9b6aec4584 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 1662337409..20a7b9d9ff 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 1d7f5367df..1824bf7f58 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 44cea7a627..e3f38d88d8 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 0f691a33b2..4bc0b46167 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 046f55615b..f8f4c8a7dd 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 0000000000..5835f9d0dc --- /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 2243e135f2..519db352f3 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 fdcc1d8305..ee022f6558 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 b32222ce2f..4d2dddffcc 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 3aa302bec9..31ac4b285e 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 7eef69441d..bf467e141f 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 559fc54486..14516fdb4c 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 b8c5020c74..34bfb78539 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 0000000000..cd97fcf079 --- /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 38f4865bdf..4d1691d37e 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 30d6e76fb0..36dc399996 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 92d731cfbb..da863f5493 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 1bbb521eab..d281b92fe2 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 b73cfc6a3e..f73f79c096 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 73ba56943a..fc207c479f 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 332b917a76..6c4188a5dc 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 622552c77f..38cd62ae76 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 c49e44c949..dfd77eec7a 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 50df056b2a..d7fc248bba 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 ad102e7b67..762ce677a7 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 0000000000..76d6713c5b --- /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 5bea1ab151..33ffd3ee8e 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 ae83da8ba6..e47c9894fa 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 60e503c128..4ba5abbb59 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 22113757c9..735f0d523e 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 18acb1aaa5..e1fa632003 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 2267c9e986..807563d47e 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 99d9a1c3e2..59ab2c296c 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 9f67af6489..8f0f63f681 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 5261bb723b..7b9b683edf 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 e0b2ad859a..7b759b5348 100644 --- a/packages/standard-components/src/DataProvider.svelte +++ b/packages/standard-components/src/DataProvider.svelte @@ -1,6 +1,12 @@ + +
+