diff --git a/.github/workflows/deploy-cloud.yaml b/.github/workflows/deploy-cloud.yaml index 9f933746d6..389b10f7d3 100644 --- a/.github/workflows/deploy-cloud.yaml +++ b/.github/workflows/deploy-cloud.yaml @@ -12,31 +12,22 @@ jobs: runs-on: ubuntu-latest steps: - # - name: Fail if not a tag - # run: | - # if [[ $GITHUB_REF != refs/tags/* ]]; then - # echo "Workflow Dispatch can only be run on tags" - # exit 1 - # fi - - - uses: actions/checkout@v2 - # with: - # fetch-depth: 0 - - # - name: Fail if tag is not in master - # run: | - # if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then - # echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch" - # exit 1 - # fi - - - name: Pull values.yaml from budibase-infra + - name: Fail if not a tag run: | - curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \ - -H 'Accept: application/vnd.github.v3.raw' \ - -o values.production.yaml \ - -L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.yaml - wc -l values.production.yaml + if [[ $GITHUB_REF != refs/tags/* ]]; then + echo "Workflow Dispatch can only be run on tags" + exit 1 + fi + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Fail if tag is not in master + run: | + if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then + echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch" + exit 1 + fi - name: Get the latest budibase release version id: version @@ -48,29 +39,10 @@ jobs: fi echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v1 + - uses: passeidireto/trigger-external-workflow-action@main + env: + PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }} with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: eu-west-1 - - - name: Deploy to EKS - uses: craftech-io/eks-helm-deploy-action@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS__KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: eu-west-1 - cluster-name: budibase-eks-production - config-files: values.production.yaml - chart-path: charts/budibase - namespace: budibase - values: globals.appVersion=v${{ env.RELEASE_VERSION }},services.couchdb.url=${{ secrets.PRODUCTION_COUCHDB_URL }},services.couchdb.password=${{ secrets.PRODUCTION_COUCHDB_PASSWORD }} - name: budibase-prod - - - name: Discord Webhook Action - uses: tsickert/discord-webhook@v4.0.0 - with: - webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }} - content: "Production Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Cloud." - embed-title: ${{ env.RELEASE_VERSION }} + repository: budibase/budibase-deploys + event: budicloud-prod-deploy + github_pat: ${{ secrets.GH_ACCESS_TOKEN }} diff --git a/.github/workflows/deploy-preprod.yml b/.github/workflows/deploy-preprod.yml index 81395bc7e3..9b7bca4770 100644 --- a/.github/workflows/deploy-preprod.yml +++ b/.github/workflows/deploy-preprod.yml @@ -24,51 +24,18 @@ jobs: echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch" exit 1 fi - - name: Get the latest budibase release version id: version run: | release_version=$(cat lerna.json | jq -r '.version') echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: eu-west-1 - - name: Pull values.yaml from budibase-infra - run: | - curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \ - -H 'Accept: application/vnd.github.v3.raw' \ - -o values.preprod.yaml \ - -L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-preprod/values.yaml - wc -l values.preprod.yaml - - name: Deploy to Preprod Environment - uses: budibase/helm@v1.8.0 - with: - release: budibase-preprod - namespace: budibase - chart: charts/budibase - token: ${{ github.token }} - helm: helm3 - values: | - globals: - appVersion: v${{ env.RELEASE_VERSION }} - ingress: - enabled: true - nginx: true - value-files: >- - [ - "values.preprod.yaml" - ] + - uses: passeidireto/trigger-external-workflow-action@main env: - KUBECONFIG_FILE: '${{ secrets.PREPROD_KUBECONFIG }}' - - - name: Discord Webhook Action - uses: tsickert/discord-webhook@v4.0.0 + PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }} with: - webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }} - content: "Preprod Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Pre-prod." - embed-title: ${{ env.RELEASE_VERSION }} + repository: budibase/budibase-deploys + event: budicloud-preprod-deploy + github_pat: ${{ secrets.GH_ACCESS_TOKEN }} + diff --git a/lerna.json b/lerna.json index eded0d214d..f1d711e915 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.7.7-alpha.2", + "version": "2.7.16-alpha.2", "npmClient": "yarn", "packages": [ "packages/backend-core", diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts index 4660be81aa..d501bb2166 100644 --- a/packages/backend-core/src/db/lucene.ts +++ b/packages/backend-core/src/db/lucene.ts @@ -343,6 +343,9 @@ export class QueryBuilder { } const oneOf = (key: string, value: any) => { + if (!value) { + return `*:*` + } if (!Array.isArray(value)) { if (typeof value === "string") { value = value.split(",") diff --git a/packages/backend-core/src/db/tests/lucene.spec.ts b/packages/backend-core/src/db/tests/lucene.spec.ts index b4791a86e0..a82828d8f2 100644 --- a/packages/backend-core/src/db/tests/lucene.spec.ts +++ b/packages/backend-core/src/db/tests/lucene.spec.ts @@ -114,6 +114,25 @@ describe("lucene", () => { expect(resp.rows.length).toBe(2) }) + it("should return all rows when doing a one of search against falsey value", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addOneOf("property", null) + let resp = await builder.run() + expect(resp.rows.length).toBe(3) + + builder.addOneOf("property", undefined) + resp = await builder.run() + expect(resp.rows.length).toBe(3) + + builder.addOneOf("property", "") + resp = await builder.run() + expect(resp.rows.length).toBe(3) + + builder.addOneOf("property", []) + resp = await builder.run() + expect(resp.rows.length).toBe(0) + }) + it("should be able to perform a contains search", async () => { const builder = new QueryBuilder(dbName, INDEX_NAME) builder.addContains("property", ["word"]) diff --git a/packages/backend-core/src/security/encryption.ts b/packages/backend-core/src/security/encryption.ts index f9adb68955..7a8cfaf04a 100644 --- a/packages/backend-core/src/security/encryption.ts +++ b/packages/backend-core/src/security/encryption.ts @@ -1,12 +1,17 @@ import crypto from "crypto" +import fs from "fs" +import zlib from "zlib" import env from "../environment" +import { join } from "path" const ALGO = "aes-256-ctr" const SEPARATOR = "-" const ITERATIONS = 10000 -const RANDOM_BYTES = 16 const STRETCH_LENGTH = 32 +const SALT_LENGTH = 16 +const IV_LENGTH = 16 + export enum SecretOption { API = "api", ENCRYPTION = "encryption", @@ -31,15 +36,15 @@ export function getSecret(secretOption: SecretOption): string { return secret } -function stretchString(string: string, salt: Buffer) { - return crypto.pbkdf2Sync(string, salt, ITERATIONS, STRETCH_LENGTH, "sha512") +function stretchString(secret: string, salt: Buffer) { + return crypto.pbkdf2Sync(secret, salt, ITERATIONS, STRETCH_LENGTH, "sha512") } export function encrypt( input: string, secretOption: SecretOption = SecretOption.API ) { - const salt = crypto.randomBytes(RANDOM_BYTES) + const salt = crypto.randomBytes(SALT_LENGTH) const stretched = stretchString(getSecret(secretOption), salt) const cipher = crypto.createCipheriv(ALGO, stretched, salt) const base = cipher.update(input) @@ -60,3 +65,115 @@ export function decrypt( const final = decipher.final() return Buffer.concat([base, final]).toString() } + +export async function encryptFile( + { dir, filename }: { dir: string; filename: string }, + secret: string +) { + const outputFileName = `${filename}.enc` + + const filePath = join(dir, filename) + const inputFile = fs.createReadStream(filePath) + const outputFile = fs.createWriteStream(join(dir, outputFileName)) + + const salt = crypto.randomBytes(SALT_LENGTH) + const iv = crypto.randomBytes(IV_LENGTH) + const stretched = stretchString(secret, salt) + const cipher = crypto.createCipheriv(ALGO, stretched, iv) + + outputFile.write(salt) + outputFile.write(iv) + + inputFile.pipe(zlib.createGzip()).pipe(cipher).pipe(outputFile) + + return new Promise<{ filename: string; dir: string }>(r => { + outputFile.on("finish", () => { + r({ + filename: outputFileName, + dir, + }) + }) + }) +} + +async function getSaltAndIV(path: string) { + const fileStream = fs.createReadStream(path) + + const salt = await readBytes(fileStream, SALT_LENGTH) + const iv = await readBytes(fileStream, IV_LENGTH) + fileStream.close() + return { salt, iv } +} + +export async function decryptFile( + inputPath: string, + outputPath: string, + secret: string +) { + const { salt, iv } = await getSaltAndIV(inputPath) + const inputFile = fs.createReadStream(inputPath, { + start: SALT_LENGTH + IV_LENGTH, + }) + + const outputFile = fs.createWriteStream(outputPath) + + const stretched = stretchString(secret, salt) + const decipher = crypto.createDecipheriv(ALGO, stretched, iv) + + const unzip = zlib.createGunzip() + + inputFile.pipe(decipher).pipe(unzip).pipe(outputFile) + + return new Promise((res, rej) => { + outputFile.on("finish", () => { + outputFile.close() + res() + }) + + inputFile.on("error", e => { + outputFile.close() + rej(e) + }) + + decipher.on("error", e => { + outputFile.close() + rej(e) + }) + + unzip.on("error", e => { + outputFile.close() + rej(e) + }) + + outputFile.on("error", e => { + outputFile.close() + rej(e) + }) + }) +} + +function readBytes(stream: fs.ReadStream, length: number) { + return new Promise((resolve, reject) => { + let bytesRead = 0 + const data: Buffer[] = [] + + stream.on("readable", () => { + let chunk + + while ((chunk = stream.read(length - bytesRead)) !== null) { + data.push(chunk) + bytesRead += chunk.length + } + + resolve(Buffer.concat(data)) + }) + + stream.on("end", () => { + reject(new Error("Insufficient data in the stream.")) + }) + + stream.on("error", error => { + reject(error) + }) + }) +} diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts index bdf7a38726..e8a3c76c0a 100644 --- a/packages/backend-core/src/security/roles.ts +++ b/packages/backend-core/src/security/roles.ts @@ -140,9 +140,13 @@ export function lowerBuiltinRoleID(roleId1?: string, roleId2?: string): string { * Gets the role object, this is mainly useful for two purposes, to check if the level exists and * to check if the role inherits any others. * @param {string|null} roleId The level ID to lookup. + * @param {object|null} opts options for the function, like whether to halt errors, instead return public. * @returns {Promise} The role object, which may contain an "inherits" property. */ -export async function getRole(roleId?: string): Promise { +export async function getRole( + roleId?: string, + opts?: { defaultPublic?: boolean } +): Promise { if (!roleId) { return undefined } @@ -161,6 +165,9 @@ export async function getRole(roleId?: string): Promise { // finalise the ID role._id = getExternalRoleID(role._id) } catch (err) { + if (!isBuiltin(roleId) && opts?.defaultPublic) { + return cloneDeep(BUILTIN_ROLES.PUBLIC) + } // only throw an error if there is no role at all if (Object.keys(role).length === 0) { throw err diff --git a/packages/bbui/src/FancyForm/FancyCheckbox.svelte b/packages/bbui/src/FancyForm/FancyCheckbox.svelte index 191cc79485..0a2e5ac159 100644 --- a/packages/bbui/src/FancyForm/FancyCheckbox.svelte +++ b/packages/bbui/src/FancyForm/FancyCheckbox.svelte @@ -8,6 +8,8 @@ export let disabled = false export let error = null export let validate = null + export let indeterminate = false + export let compact = false const dispatch = createEventDispatcher() @@ -21,11 +23,19 @@ } - + - + -
+
{#if text} {text} {/if} @@ -47,6 +57,10 @@ line-clamp: 2; -webkit-box-orient: vertical; } + .text.compact { + font-size: 13px; + line-height: 15px; + } .text > :global(*) { font-size: inherit !important; } diff --git a/packages/bbui/src/FancyForm/FancyCheckboxGroup.svelte b/packages/bbui/src/FancyForm/FancyCheckboxGroup.svelte new file mode 100644 index 0000000000..aaea388c36 --- /dev/null +++ b/packages/bbui/src/FancyForm/FancyCheckboxGroup.svelte @@ -0,0 +1,68 @@ + + +{#if options && Array.isArray(options)} +
+ + {#if showSelectAll} + + {/if} + {#each options as option, i} + + {/each} + +
+{/if} + + diff --git a/packages/bbui/src/FancyForm/FancyField.svelte b/packages/bbui/src/FancyForm/FancyField.svelte index 0c99394599..455f4b38fb 100644 --- a/packages/bbui/src/FancyForm/FancyField.svelte +++ b/packages/bbui/src/FancyForm/FancyField.svelte @@ -11,6 +11,7 @@ export let value export let ref export let autoHeight + export let compact = false const formContext = getContext("fancy-form") const id = Math.random() @@ -42,6 +43,7 @@ class:disabled class:focused class:clickable + class:compact class:auto-height={autoHeight} >
@@ -61,7 +63,6 @@ diff --git a/packages/builder/src/components/backend/DatasourceNavigator/icons/index.js b/packages/builder/src/components/backend/DatasourceNavigator/icons/index.js index 18aa361570..2486942dea 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/icons/index.js +++ b/packages/builder/src/components/backend/DatasourceNavigator/icons/index.js @@ -44,6 +44,9 @@ export default ICONS export function getIcon(integrationType, schema) { const integrationList = get(integrations) + if (!integrationList) { + return + } if (integrationList[integrationType]?.iconUrl) { return { url: integrationList[integrationType].iconUrl } } else if (schema?.custom || !ICONS[integrationType]) { diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte index 21c7e07a25..1d84dbbe39 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte @@ -1,12 +1,19 @@ saveDatasource()} - confirmText={datasource.plus ? "Connect" : "Save and continue to query"} - cancelText="Back" - showSecondaryButton={datasource.plus} + {title} + onConfirm={() => nextStep()} + {confirmText} + cancelText={fetchTableStep ? "Cancel" : "Back"} + showSecondaryButton={datasourcePlus} size="L" disabled={!isValid} > - Connect your database to Budibase using the config below. + + {#if !fetchTableStep} + Connect your database to Budibase using the config below + {:else} + Choose what tables you want to sync with Budibase + {/if} - (isValid = e.detail)} - /> + {#if !fetchTableStep} + (isValid = e.detail)} + /> + {:else} +
+ +
+ {/if}
+ + diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte index 7b4808967d..14f81f915c 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte @@ -1,22 +1,27 @@ Add the URL of the sheet you want to connect. (isValid = e.detail)} /> {/if} + {#if step === GoogleDatasouceConfigStep.SET_SHEETS} + + Select which spreadsheets you want to connect. + + + + {#if setSheetsErrorTitle || setSheetsErrorMessage} + + {/if} + + {/if} diff --git a/packages/builder/src/components/commandPalette/CommandPalette.svelte b/packages/builder/src/components/commandPalette/CommandPalette.svelte index ae946dc10c..3a369446a3 100644 --- a/packages/builder/src/components/commandPalette/CommandPalette.svelte +++ b/packages/builder/src/components/commandPalette/CommandPalette.svelte @@ -69,7 +69,7 @@ name: "App", description: "", icon: "Play", - action: () => window.open(`/${$store.appId}`), + action: () => store.update(state => ({ ...state, showPreview: true })), }, { type: "Preview", diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index 3843eabf45..c95988e90c 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -19,7 +19,7 @@ readableToRuntimeBinding, runtimeToReadableBinding, } from "builderStore/dataBinding" - import { store } from "builderStore" + import { convertToJS } from "@budibase/string-templates" import { admin } from "stores/portal" import CodeEditor from "../CodeEditor/CodeEditor.svelte" @@ -339,25 +339,28 @@ {/if}
- - + {#if drawerActions?.hide} + + {/if} + {#if bindingDrawerActions?.save} + + {/if}
diff --git a/packages/builder/src/components/common/bindings/BindingPicker.svelte b/packages/builder/src/components/common/bindings/BindingPicker.svelte index 0f709fd262..50e7c66682 100644 --- a/packages/builder/src/components/common/bindings/BindingPicker.svelte +++ b/packages/builder/src/components/common/bindings/BindingPicker.svelte @@ -36,7 +36,7 @@ .map(([name, categoryBindings]) => ({ name, bindings: categoryBindings?.filter(binding => { - return binding.readableBinding.match(searchRgx) + return !search || binding.readableBinding.match(searchRgx) }), })) .filter(category => { @@ -46,7 +46,11 @@ ) }) $: filteredHelpers = helpers?.filter(helper => { - return helper.label.match(searchRgx) || helper.description.match(searchRgx) + return ( + !search || + helper.label.match(searchRgx) || + helper.description.match(searchRgx) + ) }) const getHelperExample = (helper, js) => { @@ -124,9 +128,6 @@ { - if (!search) { - return - } search = null }} class:searching={search} diff --git a/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte b/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte index bb01d557b8..55f7866453 100644 --- a/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte +++ b/packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte @@ -76,7 +76,7 @@ {/if}
- + Add the objects on the left to enrich your text. diff --git a/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte b/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte index f41b66bd77..7fbbf75a30 100644 --- a/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte +++ b/packages/builder/src/components/common/bindings/DrawerBindableInput.svelte @@ -5,8 +5,6 @@ runtimeToReadableBinding, } from "builderStore/dataBinding" - import { store } from "builderStore" - import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte" import { createEventDispatcher, setContext } from "svelte" import { isJSBinding } from "@budibase/string-templates" @@ -36,7 +34,6 @@ const saveBinding = () => { onChange(tempValue) - store.actions.settings.propertyFocus(null) onBlur() bindingDrawer.hide() } @@ -70,7 +67,6 @@
{ - store.actions.settings.propertyFocus(key) bindingDrawer.show() }} > diff --git a/packages/builder/src/components/deploy/AppActions.svelte b/packages/builder/src/components/deploy/AppActions.svelte index 6a381608bd..477a115d08 100644 --- a/packages/builder/src/components/deploy/AppActions.svelte +++ b/packages/builder/src/components/deploy/AppActions.svelte @@ -97,7 +97,10 @@ } const previewApp = () => { - window.open(`/${application}`) + store.update(state => ({ + ...state, + showPreview: true, + })) } const viewApp = () => { diff --git a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte index daa3f29592..57601d47ca 100644 --- a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte +++ b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte @@ -73,10 +73,6 @@ if (highlighted) { store.actions.settings.highlight(null) } - // To fix focus 'affect' when property is target of a drawer other actions in the builder. - if (propertyFocus) { - store.actions.settings.propertyFocus(null) - } }) diff --git a/packages/builder/src/components/integration/QueryEditor.svelte b/packages/builder/src/components/integration/QueryEditor.svelte index f543438b9c..51fdb4879e 100644 --- a/packages/builder/src/components/integration/QueryEditor.svelte +++ b/packages/builder/src/components/integration/QueryEditor.svelte @@ -186,7 +186,6 @@ } div :global(.CodeMirror) { - width: var(--code-mirror-width) !important; height: var(--code-mirror-height) !important; border-radius: var(--border-radius-s); font-family: var(--font-mono); diff --git a/packages/builder/src/components/start/ExportAppModal.svelte b/packages/builder/src/components/start/ExportAppModal.svelte index 948416b192..5cc3393a89 100644 --- a/packages/builder/src/components/start/ExportAppModal.svelte +++ b/packages/builder/src/components/start/ExportAppModal.svelte @@ -1,5 +1,11 @@ diff --git a/packages/builder/src/pages/builder/app/[application]/_components/PreviewOverlay.svelte b/packages/builder/src/pages/builder/app/[application]/_components/PreviewOverlay.svelte new file mode 100644 index 0000000000..d069d1b4c7 --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/_components/PreviewOverlay.svelte @@ -0,0 +1,91 @@ + + +
+
+
+
+ +
+