diff --git a/.github/workflows/force-release.yml b/.github/workflows/force-release.yml index 8a9d444f51..3d96d51484 100644 --- a/.github/workflows/force-release.yml +++ b/.github/workflows/force-release.yml @@ -9,7 +9,7 @@ on: jobs: ensure-is-master-tag: name: Ensure is a master tag - runs-on: qa-arc-runner-set + runs-on: ubuntu-latest steps: - name: Checkout monorepo uses: actions/checkout@v4 diff --git a/charts/budibase/Chart.yaml b/charts/budibase/Chart.yaml index e2c9378f2c..83a72d203f 100644 --- a/charts/budibase/Chart.yaml +++ b/charts/budibase/Chart.yaml @@ -17,6 +17,6 @@ version: 0.0.0 appVersion: 0.0.0 dependencies: - name: couchdb - version: 4.3.0 + version: 4.5.3 repository: https://apache.github.io/couchdb-helm condition: services.couchdb.enabled diff --git a/hosting/proxy/nginx.prod.conf b/hosting/proxy/nginx.prod.conf index 12b8df049f..59722dac5c 100644 --- a/hosting/proxy/nginx.prod.conf +++ b/hosting/proxy/nginx.prod.conf @@ -74,6 +74,7 @@ http { add_header X-Content-Type-Options nosniff always; add_header X-XSS-Protection "1; mode=block" always; add_header Content-Security-Policy "${csp_default}; ${csp_script}; ${csp_style}; ${csp_object}; ${csp_base_uri}; ${csp_connect}; ${csp_font}; ${csp_frame}; ${csp_img}; ${csp_manifest}; ${csp_media}; ${csp_worker};" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; # upstreams set $apps ${APPS_UPSTREAM_URL}; diff --git a/lerna.json b/lerna.json index 36d217c1bb..d90f7732a2 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.27.4", + "version": "2.27.6", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/account-portal b/packages/account-portal index 39acfff42a..a03225549e 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit 39acfff42a063e5a8a7d58d36721ec3103e16348 +Subproject commit a03225549e3ce61f43d0da878da162e08941b939 diff --git a/packages/backend-core/src/objectStore/objectStore.ts b/packages/backend-core/src/objectStore/objectStore.ts index 0ac2c35179..de94e3968b 100644 --- a/packages/backend-core/src/objectStore/objectStore.ts +++ b/packages/backend-core/src/objectStore/objectStore.ts @@ -14,6 +14,7 @@ import { v4 } from "uuid" import { APP_PREFIX, APP_DEV_PREFIX } from "../db" import fsp from "fs/promises" import { HeadObjectOutput } from "aws-sdk/clients/s3" +import { ReadableStream } from "stream/web" const streamPipeline = promisify(stream.pipeline) // use this as a temporary store of buckets that are being created @@ -41,10 +42,7 @@ type UploadParams = BaseUploadParams & { path?: string | PathLike } -export type StreamTypes = - | ReadStream - | NodeJS.ReadableStream - | ReadableStream +export type StreamTypes = ReadStream | NodeJS.ReadableStream export type StreamUploadParams = BaseUploadParams & { stream?: StreamTypes @@ -222,6 +220,9 @@ export async function streamUpload({ extra, ttl, }: StreamUploadParams) { + if (!stream) { + throw new Error("Stream to upload is invalid/undefined") + } const extension = filename.split(".").pop() const objectStore = ObjectStore(bucketName) const bucketCreated = await createBucketIfNotExists(objectStore, bucketName) @@ -251,14 +252,27 @@ export async function streamUpload({ : CONTENT_TYPE_MAP.txt } + const bucket = sanitizeBucket(bucketName), + objKey = sanitizeKey(filename) const params = { - Bucket: sanitizeBucket(bucketName), - Key: sanitizeKey(filename), + Bucket: bucket, + Key: objKey, Body: stream, ContentType: contentType, ...extra, } - return objectStore.upload(params).promise() + + const details = await objectStore.upload(params).promise() + const headDetails = await objectStore + .headObject({ + Bucket: bucket, + Key: objKey, + }) + .promise() + return { + ...details, + ContentLength: headDetails.ContentLength, + } } /** diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte index c346e34d54..d3cec0f307 100644 --- a/packages/bbui/src/ActionButton/ActionButton.svelte +++ b/packages/bbui/src/ActionButton/ActionButton.svelte @@ -57,6 +57,7 @@ class:fullWidth class="spectrum-ActionButton spectrum-ActionButton--size{size}" class:active + class:disabled {disabled} on:longPress on:click|preventDefault @@ -109,19 +110,22 @@ background: var(--spectrum-global-color-gray-300); border-color: var(--spectrum-global-color-gray-500); } - .noPadding { - padding: 0; - min-width: 0; - } .spectrum-ActionButton--quiet { padding: 0 8px; } .spectrum-ActionButton--quiet.is-selected { color: var(--spectrum-global-color-gray-900); } + .noPadding { + padding: 0; + min-width: 0; + } .is-selected:not(.emphasized) .spectrum-Icon { color: var(--spectrum-global-color-gray-900); } + .is-selected.disabled .spectrum-Icon { + color: var(--spectrum-global-color-gray-500); + } .tooltip { position: absolute; pointer-events: none; diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index 5114ad2d7a..d8edf0cbb1 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -11,6 +11,7 @@ import { decodeJSBinding, encodeJSBinding, + processObjectSync, processStringSync, } from "@budibase/string-templates" import { readableToRuntimeBinding } from "dataBinding" @@ -153,13 +154,6 @@ debouncedEval(expression, context, snippets) } - const getBindingValue = (binding, context, snippets) => { - const js = `return $("${binding.runtimeBinding}")` - const hbs = encodeJSBinding(js) - const res = processStringSync(hbs, { ...context, snippets }) - return JSON.stringify(res, null, 2) - } - const highlightJSON = json => { return formatHighlight(json, { keyColor: "#e06c75", @@ -172,11 +166,27 @@ } const enrichBindings = (bindings, context, snippets) => { - return bindings.map(binding => { + // Create a single big array to enrich in one go + const bindingStrings = bindings.map(binding => { + if (binding.runtimeBinding.startsWith('trim "')) { + // Account for nasty hardcoded HBS bindings for roles, for legacy + // compatibility + return `{{ ${binding.runtimeBinding} }}` + } else { + return `{{ literal ${binding.runtimeBinding} }}` + } + }) + const bindingEvauations = processObjectSync(bindingStrings, { + ...context, + snippets, + }) + + // Enrich bindings with evaluations and highlighted HTML + return bindings.map((binding, idx) => { if (!context) { return binding } - const value = getBindingValue(binding, context, snippets) + const value = JSON.stringify(bindingEvauations[idx], null, 2) return { ...binding, value, diff --git a/packages/builder/src/components/common/bindings/BindingSidePanel.svelte b/packages/builder/src/components/common/bindings/BindingSidePanel.svelte index 6ef2d35a6c..f364b39ba9 100644 --- a/packages/builder/src/components/common/bindings/BindingSidePanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingSidePanel.svelte @@ -75,13 +75,6 @@ if (!context || !binding.value || binding.value === "") { return } - - // Roles have always been broken for JS. We need to exclude them from - // showing a popover as it will show "Error while executing JS". - if (binding.category === "Role") { - return - } - stopHidingPopover() popoverAnchor = target hoverTarget = { diff --git a/packages/builder/src/pages/builder/portal/users/users/index.svelte b/packages/builder/src/pages/builder/portal/users/users/index.svelte index c2fbce5747..58da310104 100644 --- a/packages/builder/src/pages/builder/portal/users/users/index.svelte +++ b/packages/builder/src/pages/builder/portal/users/users/index.svelte @@ -60,6 +60,7 @@ userLimitReachedModal let searchEmail = undefined let selectedRows = [] + let selectedInvites = [] let bulkSaveResponse let customRenderers = [ { column: "email", component: EmailTableRenderer }, @@ -123,7 +124,7 @@ return {} } let pendingSchema = JSON.parse(JSON.stringify(tblSchema)) - pendingSchema.email.displayName = "Pending Invites" + pendingSchema.email.displayName = "Pending Users" return pendingSchema } @@ -132,6 +133,7 @@ const { admin, builder, userGroups, apps } = invite.info return { + _id: invite.code, email: invite.email, builder, admin, @@ -260,9 +262,26 @@ return } - await users.bulkDelete(ids) - notifications.success(`Successfully deleted ${selectedRows.length} rows`) + if (ids.length > 0) { + await users.bulkDelete(ids) + } + + if (selectedInvites.length > 0) { + await users.removeInvites( + selectedInvites.map(invite => ({ + code: invite._id, + })) + ) + pendingInvites = await users.getInvites() + } + + notifications.success( + `Successfully deleted ${ + selectedRows.length + selectedInvites.length + } users` + ) selectedRows = [] + selectedInvites = [] await fetch.refresh() } catch (error) { notifications.error("Error deleting users") @@ -328,15 +347,15 @@ {/if}
- - {#if selectedRows.length > 0} + {#if selectedRows.length > 0 || selectedInvites.length > 0} {/if} +
({ }) }, + /** + * Removes multiple user invites from Redis cache + */ + removeUserInvites: async inviteCodes => { + return await API.post({ + url: "/api/global/users/multi/invite/delete", + body: inviteCodes, + }) + }, + /** * Accepts an invite to join the platform and creates a user. * @param inviteCode the invite code sent in the email diff --git a/packages/frontend-core/src/components/grid/controls/HideColumnsButton.svelte b/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte similarity index 59% rename from packages/frontend-core/src/components/grid/controls/HideColumnsButton.svelte rename to packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte index 5b74e01958..0a85e41966 100644 --- a/packages/frontend-core/src/components/grid/controls/HideColumnsButton.svelte +++ b/packages/frontend-core/src/components/grid/controls/ColumnsSettingButton.svelte @@ -1,7 +1,8 @@
(open = !open)} @@ -54,25 +69,25 @@ {$stickyColumn.label}
- + + {/if} {#each $columns as column}
{column.label}
- toggleColumn(column, e.detail)} - disabled={column.primaryDisplay} + toggleColumn(column, e.detail)} + value={columnToPermissionOptions(column)} + {options} /> {/each} -
- toggleAll(true)}>Show all - toggleAll(false)}>Hide all -
@@ -83,15 +98,11 @@ flex-direction: column; gap: 12px; } - .buttons { - display: flex; - flex-direction: row; - gap: 8px; - } .columns { display: grid; align-items: center; grid-template-columns: 1fr auto; + gap: 8px; } .columns :global(.spectrum-Switch) { margin-right: 0; diff --git a/packages/frontend-core/src/components/grid/controls/ToggleActionButtonGroup.svelte b/packages/frontend-core/src/components/grid/controls/ToggleActionButtonGroup.svelte new file mode 100644 index 0000000000..e705b5016d --- /dev/null +++ b/packages/frontend-core/src/components/grid/controls/ToggleActionButtonGroup.svelte @@ -0,0 +1,43 @@ + + +
+ {#each options as option} + + dispatch("click", option.value)} + {disabled} + size="S" + icon={option.icon} + quiet + selected={option.value === value} + noPadding + /> + + {/each} +
+ + diff --git a/packages/frontend-core/src/components/grid/layout/ButtonColumn.svelte b/packages/frontend-core/src/components/grid/layout/ButtonColumn.svelte index 3448042894..20cfdb1ec5 100644 --- a/packages/frontend-core/src/components/grid/layout/ButtonColumn.svelte +++ b/packages/frontend-core/src/components/grid/layout/ButtonColumn.svelte @@ -16,9 +16,10 @@ scroll, isDragging, buttonColumnWidth, + showVScrollbar, } = getContext("grid") - let measureContainer + let container $: buttons = $props.buttons?.slice(0, 3) || [] $: columnsWidth = $visibleColumns.reduce( @@ -39,7 +40,7 @@ const width = entries?.[0]?.contentRect?.width ?? 0 buttonColumnWidth.set(width) }) - observer.observe(measureContainer) + observer.observe(container) }) @@ -50,7 +51,7 @@ class:hidden={$buttonColumnWidth === 0} >
($hoveredRowId = null)}> - + {#each $renderedRows as row} {@const rowSelected = !!$selectedRows[row._id]} {@const rowHovered = $hoveredRowId === row._id} @@ -59,7 +60,6 @@ class="row" on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)} on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)} - bind:this={measureContainer} > -
+
{#each buttons as button}