diff --git a/lerna.json b/lerna.json index dd49bd32bb..b327205215 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.8.22-alpha.1", + "version": "2.8.22-alpha.4", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/nx.json b/nx.json index 3df61886c2..c2f44ef70d 100644 --- a/nx.json +++ b/nx.json @@ -3,11 +3,8 @@ "default": { "runner": "nx-cloud", "options": { - "cacheableOperations": [ - "build", - "test" - ], - "accessToken": "YWNiYzc5NTEtMzMzZC00NDhjLTgyNjktZTllMjI1MzM4OGQxfHJlYWQtd3JpdGU=" + "cacheableOperations": ["build", "test"], + "accessToken": "MmM4OGYxNzItMDBlYy00ZmE3LTk4MTYtNmJhYWMyZjBjZTUyfHJlYWQ=" } } }, @@ -15,9 +12,7 @@ "dev:builder": { "dependsOn": [ { - "projects": [ - "@budibase/string-templates" - ], + "projects": ["@budibase/string-templates"], "target": "build" } ] diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 5076b7569b..b8d2eb2a54 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -163,6 +163,7 @@ const environment = { : false, ...getPackageJsonFields(), DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER, + OFFLINE_MODE: process.env.OFFLINE_MODE, _set(key: any, value: any) { process.env[key] = value // @ts-ignore diff --git a/packages/backend-core/src/errors/errors.ts b/packages/backend-core/src/errors/errors.ts index 4e1f1abbb5..7d55d25e89 100644 --- a/packages/backend-core/src/errors/errors.ts +++ b/packages/backend-core/src/errors/errors.ts @@ -55,6 +55,18 @@ export class HTTPError extends BudibaseError { } } +export class NotFoundError extends HTTPError { + constructor(message: string) { + super(message, 404) + } +} + +export class BadRequestError extends HTTPError { + constructor(message: string) { + super(message, 400) + } +} + // LICENSING export class UsageLimitError extends HTTPError { diff --git a/packages/backend-core/src/events/identification.ts b/packages/backend-core/src/events/identification.ts index 5eb11d1354..948d3b692b 100644 --- a/packages/backend-core/src/events/identification.ts +++ b/packages/backend-core/src/events/identification.ts @@ -264,7 +264,7 @@ const getEventTenantId = async (tenantId: string): Promise => { } } -const getUniqueTenantId = async (tenantId: string): Promise => { +export const getUniqueTenantId = async (tenantId: string): Promise => { // make sure this tenantId always matches the tenantId in context return context.doInTenant(tenantId, () => { return withCache(CacheKey.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => { diff --git a/packages/backend-core/tests/core/utilities/structures/accounts.ts b/packages/backend-core/tests/core/utilities/structures/accounts.ts index 807153cd09..8476399aa3 100644 --- a/packages/backend-core/tests/core/utilities/structures/accounts.ts +++ b/packages/backend-core/tests/core/utilities/structures/accounts.ts @@ -13,7 +13,7 @@ import { } from "@budibase/types" import _ from "lodash" -export const account = (): Account => { +export const account = (partial: Partial = {}): Account => { return { accountId: uuid(), tenantId: generator.word(), @@ -29,6 +29,7 @@ export const account = (): Account => { size: "10+", profession: "Software Engineer", quotaUsage: quotas.usage(), + ...partial, } } diff --git a/packages/backend-core/tests/core/utilities/structures/db.ts b/packages/backend-core/tests/core/utilities/structures/db.ts index 31a52dce8b..87325573eb 100644 --- a/packages/backend-core/tests/core/utilities/structures/db.ts +++ b/packages/backend-core/tests/core/utilities/structures/db.ts @@ -1,4 +1,4 @@ -import { structures } from ".." +import { generator } from "./generator" import { newid } from "../../../../src/docIds/newid" export function id() { @@ -6,7 +6,7 @@ export function id() { } export function rev() { - return `${structures.generator.character({ + return `${generator.character({ numeric: true, - })}-${structures.uuid().replace(/-/, "")}` + })}-${generator.guid().replace(/-/, "")}` } diff --git a/packages/backend-core/tests/core/utilities/structures/documents/index.ts b/packages/backend-core/tests/core/utilities/structures/documents/index.ts new file mode 100644 index 0000000000..c3bfba3597 --- /dev/null +++ b/packages/backend-core/tests/core/utilities/structures/documents/index.ts @@ -0,0 +1 @@ +export * from "./platform" diff --git a/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts b/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts new file mode 100644 index 0000000000..98a6314999 --- /dev/null +++ b/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts @@ -0,0 +1 @@ +export * as installation from "./installation" diff --git a/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts b/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts new file mode 100644 index 0000000000..711c6cf14f --- /dev/null +++ b/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts @@ -0,0 +1,12 @@ +import { generator } from "../../generator" +import { Installation } from "@budibase/types" +import * as db from "../../db" + +export function install(): Installation { + return { + _id: "install", + _rev: db.rev(), + installId: generator.guid(), + version: generator.string(), + } +} diff --git a/packages/backend-core/tests/core/utilities/structures/index.ts b/packages/backend-core/tests/core/utilities/structures/index.ts index 2c094f43a7..1a49e912fc 100644 --- a/packages/backend-core/tests/core/utilities/structures/index.ts +++ b/packages/backend-core/tests/core/utilities/structures/index.ts @@ -2,6 +2,7 @@ export * from "./common" export * as accounts from "./accounts" export * as apps from "./apps" export * as db from "./db" +export * as docs from "./documents" export * as koa from "./koa" export * as licenses from "./licenses" export * as plugins from "./plugins" diff --git a/packages/backend-core/tests/core/utilities/structures/licenses.ts b/packages/backend-core/tests/core/utilities/structures/licenses.ts index 22e73f2871..5cce84edfd 100644 --- a/packages/backend-core/tests/core/utilities/structures/licenses.ts +++ b/packages/backend-core/tests/core/utilities/structures/licenses.ts @@ -3,6 +3,8 @@ import { Customer, Feature, License, + OfflineIdentifier, + OfflineLicense, PlanModel, PlanType, PriceDuration, @@ -11,6 +13,7 @@ import { Quotas, Subscription, } from "@budibase/types" +import { generator } from "./generator" export function price(): PurchasedPrice { return { @@ -127,15 +130,15 @@ export function subscription(): Subscription { } } -export const license = ( - opts: { - quotas?: Quotas - plan?: PurchasedPlan - planType?: PlanType - features?: Feature[] - billing?: Billing - } = {} -): License => { +interface GenerateLicenseOpts { + quotas?: Quotas + plan?: PurchasedPlan + planType?: PlanType + features?: Feature[] + billing?: Billing +} + +export const license = (opts: GenerateLicenseOpts = {}): License => { return { features: opts.features || [], quotas: opts.quotas || quotas(), @@ -143,3 +146,22 @@ export const license = ( billing: opts.billing || billing(), } } + +export function offlineLicense(opts: GenerateLicenseOpts = {}): OfflineLicense { + const base = license(opts) + return { + ...base, + expireAt: new Date().toISOString(), + identifier: offlineIdentifier(), + } +} + +export function offlineIdentifier( + installId: string = generator.guid(), + tenantId: string = generator.guid() +): OfflineIdentifier { + return { + installId, + tenantId, + } +} diff --git a/packages/bbui/package.json b/packages/bbui/package.json index b03c83d71b..4d39f6330b 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -96,7 +96,8 @@ "dependsOn": [ { "projects": [ - "@budibase/string-templates" + "@budibase/string-templates", + "@budibase/shared-core" ], "target": "build" } diff --git a/packages/builder/src/components/design/settings/componentSettings.js b/packages/builder/src/components/design/settings/componentSettings.js index 314391e77c..8b151564a1 100644 --- a/packages/builder/src/components/design/settings/componentSettings.js +++ b/packages/builder/src/components/design/settings/componentSettings.js @@ -23,6 +23,7 @@ import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte" import GridColumnEditor from "./controls/ColumnEditor/GridColumnEditor.svelte" import BarButtonList from "./controls/BarButtonList.svelte" import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte" +import RelationshipFilterEditor from "./controls/RelationshipFilterEditor.svelte" const componentMap = { text: DrawerBindableInput, @@ -44,6 +45,7 @@ const componentMap = { schema: SchemaSelect, section: SectionSelect, filter: FilterEditor, + "filter/relationship": RelationshipFilterEditor, url: URLSelect, fieldConfiguration: FieldConfiguration, columns: ColumnEditor, diff --git a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte index 88c3842f54..828d189850 100644 --- a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterEditor.svelte @@ -13,13 +13,14 @@ export let value = [] export let componentInstance export let bindings = [] + export let schema = null let drawer $: tempValue = value $: datasource = getDatasourceForProvider($currentAsset, componentInstance) - $: schema = getSchemaForDatasource($currentAsset, datasource)?.schema - $: schemaFields = Object.values(schema || {}) + $: dsSchema = getSchemaForDatasource($currentAsset, datasource)?.schema + $: schemaFields = Object.values(schema || dsSchema || {}) $: text = getText(value?.filter(filter => filter.field)) async function saveFilter() { diff --git a/packages/builder/src/components/design/settings/controls/RelationshipFilterEditor.svelte b/packages/builder/src/components/design/settings/controls/RelationshipFilterEditor.svelte new file mode 100644 index 0000000000..0010a22d15 --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/RelationshipFilterEditor.svelte @@ -0,0 +1,35 @@ + + + diff --git a/packages/builder/src/components/design/settings/controls/ValidationEditor/ValidationEditor.svelte b/packages/builder/src/components/design/settings/controls/ValidationEditor/ValidationEditor.svelte index 6c62c9f5af..6db24e8d69 100644 --- a/packages/builder/src/components/design/settings/controls/ValidationEditor/ValidationEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/ValidationEditor/ValidationEditor.svelte @@ -8,16 +8,29 @@ export let componentDefinition export let type + const dispatch = createEventDispatcher() let drawer - const dispatch = createEventDispatcher() + $: text = getText(value) + const save = () => { dispatch("change", value) drawer.hide() } + + const getText = rules => { + if (!rules?.length) { + return "No rules set" + } else { + return `${rules.length} rule${rules.length === 1 ? "" : "s"} set` + } + } -Configure validation +
+ {text} +
+ Configure validation rules for this field. @@ -31,3 +44,9 @@ {componentDefinition} /> + + diff --git a/packages/builder/src/pages/builder/portal/account/upgrade.svelte b/packages/builder/src/pages/builder/portal/account/upgrade.svelte index f0ee87bde5..b9ce143728 100644 --- a/packages/builder/src/pages/builder/portal/account/upgrade.svelte +++ b/packages/builder/src/pages/builder/portal/account/upgrade.svelte @@ -10,6 +10,8 @@ Label, ButtonGroup, notifications, + CopyInput, + File, } from "@budibase/bbui" import { auth, admin } from "stores/portal" import { redirect } from "@roxi/routify" @@ -21,15 +23,20 @@ $: license = $auth.user.license $: upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade` + // LICENSE KEY + $: activateDisabled = !licenseKey || licenseKeyDisabled - - let licenseInfo - let licenseKeyDisabled = false let licenseKeyType = "text" let licenseKey = "" let deleteLicenseKeyModal + // OFFLINE + + let offlineLicenseIdentifier = "" + let offlineLicense = undefined + const offlineLicenseExtensions = [".txt"] + // Make sure page can't be visited directly in cloud $: { if ($admin.cloud) { @@ -37,28 +44,115 @@ } } - const activate = async () => { + // LICENSE KEY + + const getLicenseKey = async () => { try { - await API.activateLicenseKey({ licenseKey }) - await auth.getSelf() - await setLicenseInfo() - notifications.success("Successfully activated") + licenseKey = await API.getLicenseKey() + if (licenseKey) { + licenseKey = "**********************************************" + licenseKeyType = "password" + licenseKeyDisabled = true + activateDisabled = true + } } catch (e) { - notifications.error(e.message) + console.error(e) + notifications.error("Error retrieving license key") } } - const destroy = async () => { + const activateLicenseKey = async () => { + try { + await API.activateLicenseKey({ licenseKey }) + await auth.getSelf() + await getLicenseKey() + notifications.success("Successfully activated") + } catch (e) { + console.error(e) + notifications.error("Error activating license key") + } + } + + const deleteLicenseKey = async () => { try { await API.deleteLicenseKey({ licenseKey }) await auth.getSelf() - await setLicenseInfo() + await getLicenseKey() // reset the form licenseKey = "" licenseKeyDisabled = false - notifications.success("Successfully deleted") + notifications.success("Offline license removed") } catch (e) { - notifications.error(e.message) + console.error(e) + notifications.error("Error deleting license key") + } + } + + // OFFLINE LICENSE + + const getOfflineLicense = async () => { + try { + const license = await API.getOfflineLicense() + if (license) { + offlineLicense = { + name: "license", + } + } else { + offlineLicense = undefined + } + } catch (e) { + console.error(e) + notifications.error("Error loading offline license") + } + } + + const getOfflineLicenseIdentifier = async () => { + try { + const res = await API.getOfflineLicenseIdentifier() + offlineLicenseIdentifier = res.identifierBase64 + } catch (e) { + console.error(e) + notifications.error("Error loading installation identifier") + } + } + + async function activateOfflineLicense(offlineLicenseToken) { + try { + await API.activateOfflineLicense({ offlineLicenseToken }) + await auth.getSelf() + await getOfflineLicense() + notifications.success("Successfully activated") + } catch (e) { + console.error(e) + notifications.error("Error activating offline license") + } + } + + async function deleteOfflineLicense() { + try { + await API.deleteOfflineLicense() + await auth.getSelf() + await getOfflineLicense() + notifications.success("Successfully removed ofline license") + } catch (e) { + console.error(e) + notifications.error("Error upload offline license") + } + } + + async function onOfflineLicenseChange(event) { + if (event.detail) { + // prevent file preview jitter by assigning constant + // as soon as possible + offlineLicense = { + name: "license", + } + const reader = new FileReader() + reader.readAsText(event.detail) + reader.onload = () => activateOfflineLicense(reader.result) + } else { + offlineLicense = undefined + await deleteOfflineLicense() } } @@ -73,29 +167,19 @@ } } - // deactivate the license key field if there is a license key set - $: { - if (licenseInfo?.licenseKey) { - licenseKey = "**********************************************" - licenseKeyType = "password" - licenseKeyDisabled = true - activateDisabled = true - } - } - - const setLicenseInfo = async () => { - licenseInfo = await API.getLicenseInfo() - } - onMount(async () => { - await setLicenseInfo() + if ($admin.offlineMode) { + await Promise.all([getOfflineLicense(), getOfflineLicenseIdentifier()]) + } else { + await getLicenseKey() + } }) {#if $auth.isAdmin} @@ -108,42 +192,82 @@ {:else} To manage your plan visit your account +
 
{/if}
- - Activate - Enter your license key below to activate your plan - - -
-
- - + Installation identifier + Share this with support@budibase.com to obtain your offline license + + +
+ +
+
+ + + License + Upload your license to activate your plan + + +
+
-
- - - {#if licenseInfo?.licenseKey} - - {/if} - - + {#if licenseKey} + + {/if} + + + {/if} - Plan - + Plan + You are currently on the {license.plan.type} plan +
+ If you purchase or update your plan on the account + portal, click the refresh button to sync those changes +
{processStringSync("Updated {{ duration time 'millisecond' }} ago", { time: @@ -169,4 +293,7 @@ grid-gap: var(--spacing-l); align-items: center; } + .identifier-input { + width: 300px; + } diff --git a/packages/builder/src/stores/portal/admin.js b/packages/builder/src/stores/portal/admin.js index b9467fd037..2106acac27 100644 --- a/packages/builder/src/stores/portal/admin.js +++ b/packages/builder/src/stores/portal/admin.js @@ -17,6 +17,7 @@ export const DEFAULT_CONFIG = { adminUser: { checked: false }, sso: { checked: false }, }, + offlineMode: false, } export function createAdminStore() { diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 285f045d08..1e4c443f06 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -3485,6 +3485,16 @@ } ] }, + { + "type": "validation/link", + "label": "Validation", + "key": "validation" + }, + { + "type": "filter/relationship", + "label": "Filtering", + "key": "filter" + }, { "type": "boolean", "label": "Autocomplete", @@ -3496,11 +3506,6 @@ "label": "Disabled", "key": "disabled", "defaultValue": false - }, - { - "type": "validation/link", - "label": "Validation", - "key": "validation" } ] }, diff --git a/packages/client/src/components/app/forms/RelationshipField.svelte b/packages/client/src/components/app/forms/RelationshipField.svelte index 735b44b9ae..0c8b076a67 100644 --- a/packages/client/src/components/app/forms/RelationshipField.svelte +++ b/packages/client/src/components/app/forms/RelationshipField.svelte @@ -1,5 +1,6 @@