diff --git a/.eslintrc.json b/.eslintrc.json index d94c749042..75584b8163 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,7 +5,7 @@ "jest": true, "node": true }, - "parser": "babel-eslint", + "parser": "@babel/eslint-parser", "parserOptions": { "ecmaVersion": 2019, "sourceType": "module", @@ -18,17 +18,23 @@ "*.spec.js", "bundle.js" ], - "plugins": ["svelte3"], "extends": ["eslint:recommended"], "overrides": [ { - "files": ["*.svelte"], - "processor": "svelte3/svelte3" + "files": ["**/*.svelte"], + "extends": "plugin:svelte/recommended", + "parser": "svelte-eslint-parser", + "parserOptions": { + "parser": "@babel/eslint-parser", + "ecmaVersion": 2019, + "sourceType": "module", + "allowImportExportEverywhere": true + } + }, { "files": ["**/*.ts"], "parser": "@typescript-eslint/parser", - "plugins": [], "extends": ["eslint:recommended"], "rules": { "no-unused-vars": "off", @@ -41,7 +47,8 @@ } ], "rules": { - "no-self-assign": "off" + "no-self-assign": "off", + "no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_", "destructuredArrayIgnorePattern": "^_" }] }, "globals": { "GeolocationPositionError": true diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 2a2c10cb7d..0000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,19 +0,0 @@ -# Configuration for probot-stale - https://github.com/probot/stale -# Number of days of inactivity before an Issue or Pull Request becomes stale -daysUntilStale: 60 -# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. -# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. -daysUntilClose: false -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - security - - roadmap -# Label to use when marking an issue as stale -staleLabel: stale -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index f2e7f2eda9..9509a22e99 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -12,9 +12,6 @@ on: - master - develop pull_request: - branches: - - master - - develop workflow_dispatch: env: @@ -162,7 +159,7 @@ jobs: run: | cd qa-core yarn setup - yarn test:ci + yarn serve:test:self:ci env: BB_ADMIN_USER_EMAIL: admin BB_ADMIN_USER_PASSWORD: admin @@ -185,7 +182,7 @@ jobs: pro_commit=$(git rev-parse HEAD) branch="${{ github.base_ref || github.ref_name }}" - echo "Running on branch `$branch` (base_ref=${{ github.base_ref }}, ref_name=${{ github.head_ref }})" + echo "Running on branch '$branch' (base_ref=${{ github.base_ref }}, ref_name=${{ github.head_ref }})" if [[ $branch == "master" ]]; then base_commit=$(git rev-parse origin/master) diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml index 3c33dfcd86..7f8b8f1d55 100644 --- a/.github/workflows/release-master.yml +++ b/.github/workflows/release-master.yml @@ -34,7 +34,6 @@ jobs: exit 1 fi - - uses: actions/setup-node@v1 with: node-version: 14.x @@ -58,9 +57,12 @@ jobs: echo //registry.npmjs.org/:_authToken=${NPM_TOKEN} >> .npmrc yarn release - - name: "Get Previous tag" - id: previoustag - uses: "WyriHaximus/github-action-get-previous-tag@v1" + - name: "Get Current tag" + id: currenttag + run: | + version=v$(./scripts/getCurrentVersion.sh) + echo 'Using tag $version' + echo "::set-output name=tag::$resversionult" - name: Build/release Docker images run: | @@ -69,7 +71,7 @@ jobs: env: DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} - BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }} + BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.tag }} release-helm-chart: needs: [release-images] diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml new file mode 100644 index 0000000000..8cda3a9342 --- /dev/null +++ b/.github/workflows/stale_bot.yml @@ -0,0 +1,29 @@ +name: Close stale issues and PRs # https://github.com/actions/stale +on: + workflow_dispatch: + schedule: + - cron: '30 1 * * *' # 1:30 every morning + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v8 + with: + # stale rules + days-before-stale: 60 + days-before-pr-stale: 7 + stale-issue-label: stale + stale-issue-message: "This issue has been automatically marked as stale because it has not had any activity for 60 days." + + # close rules + # days after being marked as stale to close + days-before-close: 30 + close-issue-label: closed-stale + close-issue-message: This issue has been automatically closed it has not had any activity in 90 days." + days-before-pr-close: 7 + + # exemptions + exempt-pr-labels: pinned,security,roadmap + + diff --git a/.tool-versions b/.tool-versions index 094292d096..da92e03885 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -nodejs 14.20.1 +nodejs 14.21.3 python 3.10.0 \ No newline at end of file diff --git a/babel.config.json b/babel.config.json new file mode 100644 index 0000000000..a6c42326bb --- /dev/null +++ b/babel.config.json @@ -0,0 +1,3 @@ +{ + "presets": [["@babel/preset-env", { "targets": { "node": "current" } }]] +} diff --git a/charts/budibase/templates/proxy-service-deployment.yaml b/charts/budibase/templates/proxy-service-deployment.yaml index e4825ea5d0..c087627100 100644 --- a/charts/budibase/templates/proxy-service-deployment.yaml +++ b/charts/budibase/templates/proxy-service-deployment.yaml @@ -40,6 +40,24 @@ spec: - image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }} imagePullPolicy: Always name: proxy-service + livenessProbe: + httpGet: + path: /health + port: {{ .Values.services.proxy.port }} + initialDelaySeconds: 0 + periodSeconds: 5 + successThreshold: 1 + failureThreshold: 2 + timeoutSeconds: 3 + readinessProbe: + httpGet: + path: /health + port: {{ .Values.services.proxy.port }} + initialDelaySeconds: 0 + periodSeconds: 5 + successThreshold: 1 + failureThreshold: 2 + timeoutSeconds: 3 ports: - containerPort: {{ .Values.services.proxy.port }} env: diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index ac35929be1..2fb4c36fa8 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -231,18 +231,33 @@ An overview of the CI pipelines can be found [here](../.github/workflows/README. ### Pro -@budibase/pro is the closed source package that supports licensed features in budibase. By default the package will be pulled from NPM and will not normally need to be touched in local development. If you require to update code inside the pro package it can be cloned to the same root level as budibase, e.g. +@budibase/pro is the closed source package that supports licensed features in budibase. By default the package will be pulled from NPM and will not normally need to be touched in local development. If you need to make an update to pro and have access to the repo, then you can update your submodule within the mono-repo by running `git submodule update --init` - from here you can use normal submodule flow to develop a change within pro. + +Once you have updated to use the pro submodule, it will be linked into all of your local dependencies by NX as with all other monorepo packages. If you have been using the NPM version of `@budibase/pro` then you may need to run a `git reset --hard` to fix all of the pro versions back to `0.0.0` to be monorepo aware. + +From here - to develop a change in pro, you can follow the below flow: ``` -. -|_ budibase -|_ budibase-pro +# enter the pro submodule +cd packages/pro +# get the base branch you are working from (same as monorepo) +git fetch +git checkout +# create a branch, named the same as the branch in your monorepo +git checkout -b +... make changes +# commit the changes you've made, with a message for pro +git commit +# within the monorepo, add the pro reference to your branch, commit it with a message like "Update pro ref" +cd ../.. +git add packages/pro +git commit ``` +From here, you will have created a branch in the pro repository and commited the reference to your branch on the monorepo. When you eventually PR this work back into the mainline branch, you will need to first merge your pro PR to the pro mainline, then go into your PR in the monorepo and update the reference again to the new mainline. + Note that only budibase maintainers will be able to access the pro repo. -By default, NX will make sure that dependencies are replaced with local source aware version. This is achieved using the `yarn link` command. To see specifically how dependencies are linked see [scripts/link-dependencies.sh](../scripts/link-dependencies.sh). The same link script is used to link dependencies to account-portal in local dev. - ### Troubleshooting Sometimes, things go wrong. This can be due to incompatible updates on the budibase platform. To clear down your development environment and start again follow **Step 6. Cleanup**, then proceed from **Step 3. Install and Build** in the setup guide above to create a fresh Budibase installation. diff --git a/hosting/.env b/hosting/.env index c2b6d55eef..8a0756c0e3 100644 --- a/hosting/.env +++ b/hosting/.env @@ -28,3 +28,4 @@ BB_ADMIN_USER_PASSWORD= # A path that is watched for plugin bundles. Any bundles found are imported automatically/ PLUGINS_DIR= +ROLLING_LOG_MAX_SIZE= \ No newline at end of file diff --git a/nx.json b/nx.json index fc0712eed4..c2f44ef70d 100644 --- a/nx.json +++ b/nx.json @@ -1,9 +1,10 @@ { "tasksRunnerOptions": { "default": { - "runner": "nx/tasks-runners/default", + "runner": "nx-cloud", "options": { - "cacheableOperations": ["build", "test"] + "cacheableOperations": ["build", "test"], + "accessToken": "MmM4OGYxNzItMDBlYy00ZmE3LTk4MTYtNmJhYWMyZjBjZTUyfHJlYWQ=" } } }, diff --git a/package.json b/package.json index 95edf63a03..a392aeee78 100644 --- a/package.json +++ b/package.json @@ -3,27 +3,32 @@ "private": true, "devDependencies": { "@esbuild-plugins/tsconfig-paths": "^0.1.2", - "@nx/js": "16.2.1", + "@nx/js": "16.4.3", "@rollup/plugin-json": "^4.0.2", "@typescript-eslint/parser": "5.45.0", - "babel-eslint": "^10.0.3", "esbuild": "^0.18.17", "esbuild-node-externals": "^1.8.0", - "eslint": "^7.28.0", + "eslint": "^8.44.0", "eslint-plugin-cypress": "^2.11.3", - "eslint-plugin-svelte3": "^3.2.0", "husky": "^8.0.3", "js-yaml": "^4.1.0", "kill-port": "^1.6.1", - "lerna": "7.0.2", + "lerna": "7.1.1", "madge": "^6.0.0", "minimist": "^1.2.8", + "nx": "16.4.3", + "nx-cloud": "16.0.5", "prettier": "2.8.8", "prettier-plugin-svelte": "^2.3.0", "rimraf": "^3.0.2", "rollup-plugin-replace": "^2.2.0", "svelte": "^3.38.2", - "typescript": "4.7.3" + "typescript": "4.7.3", + "@babel/core": "^7.22.5", + "@babel/eslint-parser": "^7.22.5", + "@babel/preset-env": "^7.22.5", + "eslint-plugin-svelte": "^2.32.2", + "svelte-eslint-parser": "^0.32.0" }, "scripts": { "preinstall": "node scripts/syncProPackage.js", @@ -41,7 +46,7 @@ "restore": "yarn run clean && yarn run bootstrap && yarn run build", "nuke": "yarn run nuke:packages && yarn run nuke:docker", "nuke:packages": "yarn run restore", - "nuke:docker": "lerna run --stream --parallel dev:stack:nuke", + "nuke:docker": "lerna run --stream dev:stack:nuke", "clean": "lerna clean", "kill-builder": "kill-port 3000", "kill-server": "kill-port 4001 4002", @@ -49,13 +54,13 @@ "dev": "yarn run kill-all && lerna run --stream --parallel dev:builder", "dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker", "dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server", - "dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built", + "dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built", "dev:docker": "yarn build:docker:pre && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0", "test": "lerna run --stream test --stream", - "lint:eslint": "eslint packages && eslint qa-core", + "lint:eslint": "eslint packages qa-core --max-warnings=0", "lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"", "lint": "yarn run lint:eslint && yarn run lint:prettier", - "lint:fix:eslint": "eslint --fix packages qa-core", + "lint:fix:eslint": "eslint --fix --max-warnings=0 packages qa-core", "lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"", "lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint", "build:specs": "lerna run --stream specs", @@ -103,5 +108,8 @@ "@budibase/string-templates": "0.0.0", "@budibase/types": "0.0.0" }, + "engines": { + "node": ">=14.0.0 <15.0.0" + }, "dependencies": {} } diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 4a1ed5c373..7f3c064c92 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -51,6 +51,7 @@ "pouchdb": "7.3.0", "pouchdb-find": "7.2.2", "redlock": "4.2.0", + "rotating-file-stream": "3.1.0", "sanitize-s3-objectkey": "0.0.1", "semver": "7.3.7", "tar-fs": "2.1.1", diff --git a/packages/backend-core/src/auth/auth.ts b/packages/backend-core/src/auth/auth.ts index fb2fd2cf51..0100a2d0e2 100644 --- a/packages/backend-core/src/auth/auth.ts +++ b/packages/backend-core/src/auth/auth.ts @@ -159,7 +159,7 @@ export async function updateUserOAuth(userId: string, oAuthConfig: any) { try { const db = getGlobalDB() - const dbUser = await db.get(userId) + const dbUser = await db.get(userId) //Do not overwrite the refresh token if a valid one is not provided. if (typeof details.refreshToken !== "string") { diff --git a/packages/backend-core/src/cache/appMetadata.ts b/packages/backend-core/src/cache/appMetadata.ts index 5b66c356d3..0c320ec776 100644 --- a/packages/backend-core/src/cache/appMetadata.ts +++ b/packages/backend-core/src/cache/appMetadata.ts @@ -2,9 +2,14 @@ import { getAppClient } from "../redis/init" import { doWithDB, DocumentType } from "../db" import { Database, App } from "@budibase/types" -const AppState = { - INVALID: "invalid", +export enum AppState { + INVALID = "invalid", } + +export interface DeletedApp { + state: AppState +} + const EXPIRY_SECONDS = 3600 /** @@ -31,7 +36,7 @@ function isInvalid(metadata?: { state: string }) { * @param {string} appId the id of the app to get metadata from. * @returns {object} the app metadata. */ -export async function getAppMetadata(appId: string) { +export async function getAppMetadata(appId: string): Promise { const client = await getAppClient() // try cache let metadata = await client.get(appId) @@ -61,11 +66,8 @@ export async function getAppMetadata(appId: string) { } await client.store(appId, metadata, expiry) } - // we've stored in the cache an object to tell us that it is currently invalid - if (isInvalid(metadata)) { - throw { status: 404, message: "No app metadata found" } - } - return metadata as App + + return metadata } /** diff --git a/packages/backend-core/src/cache/user.ts b/packages/backend-core/src/cache/user.ts index b514c3af9b..8281bfca62 100644 --- a/packages/backend-core/src/cache/user.ts +++ b/packages/backend-core/src/cache/user.ts @@ -12,7 +12,7 @@ const EXPIRY_SECONDS = 3600 */ async function populateFromDB(userId: string, tenantId: string) { const db = tenancy.getTenantDB(tenantId) - const user = await db.get(userId) + const user = await db.get(userId) user.budibaseAccess = true if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { const account = await accounts.getAccount(user.email) diff --git a/packages/backend-core/src/constants/misc.ts b/packages/backend-core/src/constants/misc.ts index ba2533cf4a..0c68798164 100644 --- a/packages/backend-core/src/constants/misc.ts +++ b/packages/backend-core/src/constants/misc.ts @@ -20,6 +20,8 @@ export enum Header { TYPE = "x-budibase-type", PREVIEW_ROLE = "x-budibase-role", TENANT_ID = "x-budibase-tenant-id", + VERIFICATION_CODE = "x-budibase-verification-code", + RETURN_VERIFICATION_CODE = "x-budibase-return-verification-code", TOKEN = "x-budibase-token", CSRF_TOKEN = "x-csrf-token", CORRELATION_ID = "x-budibase-correlation-id", diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/db/constants.ts new file mode 100644 index 0000000000..aea485e3e3 --- /dev/null +++ b/packages/backend-core/src/db/constants.ts @@ -0,0 +1,10 @@ +export const CONSTANT_INTERNAL_ROW_COLS = [ + "_id", + "_rev", + "type", + "createdAt", + "updatedAt", + "tableId", +] as const + +export const CONSTANT_EXTERNAL_ROW_COLS = ["_id", "_rev", "tableId"] as const diff --git a/packages/backend-core/src/db/couch/index.ts b/packages/backend-core/src/db/couch/index.ts index c731d20d6c..932efed3f7 100644 --- a/packages/backend-core/src/db/couch/index.ts +++ b/packages/backend-core/src/db/couch/index.ts @@ -2,3 +2,4 @@ export * from "./connections" export * from "./DatabaseImpl" export * from "./utils" export { init, getPouch, getPouchDB, closePouchDB } from "./pouchDB" +export * from "../constants" diff --git a/packages/backend-core/src/db/searchIndexes/searchIndexes.ts b/packages/backend-core/src/db/searchIndexes/searchIndexes.ts index f03259b47f..b953e3516e 100644 --- a/packages/backend-core/src/db/searchIndexes/searchIndexes.ts +++ b/packages/backend-core/src/db/searchIndexes/searchIndexes.ts @@ -5,7 +5,7 @@ export async function createUserIndex() { const db = getGlobalDB() let designDoc try { - designDoc = await db.get("_design/database") + designDoc = await db.get("_design/database") } catch (err: any) { if (err.status === 404) { designDoc = { _id: "_design/database" } diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index 6034296996..4ebf8392b5 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -2,7 +2,7 @@ import env from "../environment" import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants" import { getTenantId, getGlobalDBName } from "../context" import { doWithDB, directCouchAllDbs } from "./db" -import { getAppMetadata } from "../cache/appMetadata" +import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata" import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions" import { App, Database } from "@budibase/types" import { getStartEndKeyURL } from "../docIds" @@ -101,7 +101,9 @@ export async function getAllApps({ const response = await Promise.allSettled(appPromises) const apps = response .filter( - (result: any) => result.status === "fulfilled" && result.value != null + (result: any) => + result.status === "fulfilled" && + result.value?.state !== AppState.INVALID ) .map(({ value }: any) => value) if (!all) { @@ -126,7 +128,11 @@ export async function getAppsByIDs(appIds: string[]) { ) // have to list the apps which exist, some may have been deleted return settled - .filter(promise => promise.status === "fulfilled") + .filter( + promise => + promise.status === "fulfilled" && + (promise.value as DeletedApp).state !== AppState.INVALID + ) .map(promise => (promise as PromiseFulfilledResult).value) } diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index c0785ef419..b8d2eb2a54 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -47,7 +47,10 @@ function httpLogging() { return process.env.HTTP_LOGGING } -function findVersion() { +function getPackageJsonFields(): { + VERSION: string + SERVICE_NAME: string +} { function findFileInAncestors( fileName: string, currentDir: string @@ -69,10 +72,14 @@ function findVersion() { try { const packageJsonFile = findFileInAncestors("package.json", process.cwd()) const content = readFileSync(packageJsonFile!, "utf-8") - return JSON.parse(content).version + const parsedContent = JSON.parse(content) + return { + VERSION: parsedContent.version, + SERVICE_NAME: parsedContent.name, + } } catch { // throwing an error here is confusing/causes backend-core to be hard to import - return undefined + return { VERSION: "", SERVICE_NAME: "" } } } @@ -154,7 +161,7 @@ const environment = { ENABLE_SSO_MAINTENANCE_MODE: selfHosted ? process.env.ENABLE_SSO_MAINTENANCE_MODE : false, - VERSION: findVersion(), + ...getPackageJsonFields(), DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER, OFFLINE_MODE: process.env.OFFLINE_MODE, _set(key: any, value: any) { @@ -162,6 +169,7 @@ const environment = { // @ts-ignore environment[key] = value }, + ROLLING_LOG_MAX_SIZE: process.env.ROLLING_LOG_MAX_SIZE || "10M", } // clean up any environment variable edge cases diff --git a/packages/backend-core/src/logging/index.ts b/packages/backend-core/src/logging/index.ts index b87062c478..0824fa681b 100644 --- a/packages/backend-core/src/logging/index.ts +++ b/packages/backend-core/src/logging/index.ts @@ -1,6 +1,4 @@ export * as correlation from "./correlation/correlation" export { logger } from "./pino/logger" export * from "./alerts" - -// turn off or on context logging i.e. tenantId, appId etc -export let LOG_CONTEXT = true +export * as system from "./system" diff --git a/packages/backend-core/src/logging/pino/logger.ts b/packages/backend-core/src/logging/pino/logger.ts index c96bc83e04..7c444a3a59 100644 --- a/packages/backend-core/src/logging/pino/logger.ts +++ b/packages/backend-core/src/logging/pino/logger.ts @@ -1,37 +1,60 @@ -import env from "../../environment" import pino, { LoggerOptions } from "pino" +import pinoPretty from "pino-pretty" + +import { IdentityType } from "@budibase/types" +import env from "../../environment" import * as context from "../../context" import * as correlation from "../correlation" -import { IdentityType } from "@budibase/types" -import { LOG_CONTEXT } from "../index" + +import { localFileDestination } from "../system" // LOGGER let pinoInstance: pino.Logger | undefined if (!env.DISABLE_PINO_LOGGER) { + const level = env.LOG_LEVEL const pinoOptions: LoggerOptions = { - level: env.LOG_LEVEL, + level, formatters: { - level: label => { - return { level: label.toUpperCase() } + level: level => { + return { level: level.toUpperCase() } }, bindings: () => { - return {} + if (env.SELF_HOSTED) { + // "service" is being injected in datadog using the pod names, + // so we should leave it blank to allow the default behaviour if it's not running self-hosted + return { + service: env.SERVICE_NAME, + } + } else { + return {} + } }, }, timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`, } - if (env.isDev()) { - pinoOptions.transport = { - target: "pino-pretty", - options: { - singleLine: true, - }, - } + const destinations: pino.StreamEntry[] = [] + + destinations.push( + env.isDev() + ? { + stream: pinoPretty({ singleLine: true }), + level: level as pino.Level, + } + : { stream: process.stdout, level: level as pino.Level } + ) + + if (env.SELF_HOSTED) { + destinations.push({ + stream: localFileDestination(), + level: level as pino.Level, + }) } - pinoInstance = pino(pinoOptions) + pinoInstance = destinations.length + ? pino(pinoOptions, pino.multistream(destinations)) + : pino(pinoOptions) // CONSOLE OVERRIDES @@ -83,15 +106,13 @@ if (!env.DISABLE_PINO_LOGGER) { let contextObject = {} - if (LOG_CONTEXT) { - contextObject = { - tenantId: getTenantId(), - appId: getAppId(), - automationId: getAutomationId(), - identityId: identity?._id, - identityType: identity?.type, - correlationId: correlation.getId(), - } + contextObject = { + tenantId: getTenantId(), + appId: getAppId(), + automationId: getAutomationId(), + identityId: identity?._id, + identityType: identity?.type, + correlationId: correlation.getId(), } const mergingObject: any = { diff --git a/packages/backend-core/src/logging/system.ts b/packages/backend-core/src/logging/system.ts new file mode 100644 index 0000000000..d918c6efd6 --- /dev/null +++ b/packages/backend-core/src/logging/system.ts @@ -0,0 +1,81 @@ +import fs from "fs" +import path from "path" +import * as rfs from "rotating-file-stream" + +import env from "../environment" +import { budibaseTempDir } from "../objectStore" + +const logsFileName = `budibase.log` +const budibaseLogsHistoryFileName = "budibase-logs-history.txt" + +const logsPath = path.join(budibaseTempDir(), "systemlogs") + +function getFullPath(fileName: string) { + return path.join(logsPath, fileName) +} + +export function getSingleFileMaxSizeInfo(totalMaxSize: string) { + const regex = /(\d+)([A-Za-z])/ + const match = totalMaxSize?.match(regex) + if (!match) { + console.warn(`totalMaxSize does not have a valid value`, { + totalMaxSize, + }) + return undefined + } + + const size = +match[1] + const unit = match[2] + if (size === 1) { + switch (unit) { + case "B": + return { size: `${size}B`, totalHistoryFiles: 1 } + case "K": + return { size: `${(size * 1000) / 2}B`, totalHistoryFiles: 1 } + case "M": + return { size: `${(size * 1000) / 2}K`, totalHistoryFiles: 1 } + case "G": + return { size: `${(size * 1000) / 2}M`, totalHistoryFiles: 1 } + default: + return undefined + } + } + + if (size % 2 === 0) { + return { size: `${size / 2}${unit}`, totalHistoryFiles: 1 } + } + + return { size: `1${unit}`, totalHistoryFiles: size - 1 } +} + +export function localFileDestination() { + const fileInfo = getSingleFileMaxSizeInfo(env.ROLLING_LOG_MAX_SIZE) + const outFile = rfs.createStream(logsFileName, { + // As we have a rolling size, we want to half the max size + size: fileInfo?.size, + path: logsPath, + maxFiles: fileInfo?.totalHistoryFiles || 1, + immutable: true, + history: budibaseLogsHistoryFileName, + initialRotation: false, + }) + + return outFile +} + +export function getLogReadStream() { + const streams = [] + const historyFile = getFullPath(budibaseLogsHistoryFileName) + if (fs.existsSync(historyFile)) { + const fileContent = fs.readFileSync(historyFile, "utf-8") + const historyFiles = fileContent.split("\n") + for (const historyFile of historyFiles.filter(x => x)) { + streams.push(fs.readFileSync(historyFile)) + } + } + + streams.push(fs.readFileSync(getFullPath(logsFileName))) + + const combinedContent = Buffer.concat(streams) + return combinedContent +} diff --git a/packages/backend-core/src/logging/tests/system.spec.ts b/packages/backend-core/src/logging/tests/system.spec.ts new file mode 100644 index 0000000000..b84d8e8456 --- /dev/null +++ b/packages/backend-core/src/logging/tests/system.spec.ts @@ -0,0 +1,61 @@ +import { getSingleFileMaxSizeInfo } from "../system" + +describe("system", () => { + describe("getSingleFileMaxSizeInfo", () => { + it.each([ + ["100B", "50B"], + ["200K", "100K"], + ["20M", "10M"], + ["4G", "2G"], + ])( + "Halving even number (%s) returns halved size and 1 history file (%s)", + (totalValue, expectedMaxSize) => { + const result = getSingleFileMaxSizeInfo(totalValue) + expect(result).toEqual({ + size: expectedMaxSize, + totalHistoryFiles: 1, + }) + } + ) + + it.each([ + ["5B", "1B", 4], + ["17K", "1K", 16], + ["21M", "1M", 20], + ["3G", "1G", 2], + ])( + "Halving an odd number (%s) returns as many files as size (-1) (%s)", + (totalValue, expectedMaxSize, totalHistoryFiles) => { + const result = getSingleFileMaxSizeInfo(totalValue) + expect(result).toEqual({ + size: expectedMaxSize, + totalHistoryFiles, + }) + } + ) + + it.each([ + ["1B", "1B"], + ["1K", "500B"], + ["1M", "500K"], + ["1G", "500M"], + ])( + "Halving '%s' returns halved unit (%s)", + (totalValue, expectedMaxSize) => { + const result = getSingleFileMaxSizeInfo(totalValue) + expect(result).toEqual({ + size: expectedMaxSize, + totalHistoryFiles: 1, + }) + } + ) + + it.each([[undefined], [""], ["50"], ["wrongvalue"]])( + "Halving wrongly formatted value ('%s') returns undefined", + totalValue => { + const result = getSingleFileMaxSizeInfo(totalValue!) + expect(result).toBeUndefined() + } + ) + }) +}) diff --git a/packages/backend-core/src/users.ts b/packages/backend-core/src/users.ts index 166136df3c..b49058f546 100644 --- a/packages/backend-core/src/users.ts +++ b/packages/backend-core/src/users.ts @@ -67,9 +67,9 @@ export const bulkUpdateGlobalUsers = async (users: User[]) => { export async function getById(id: string, opts?: GetOpts): Promise { const db = context.getGlobalDB() - let user = await db.get(id) + let user = await db.get(id) if (opts?.cleanup) { - user = removeUserPassword(user) + user = removeUserPassword(user) as User } return user } diff --git a/packages/backend-core/tests/core/utilities/jestUtils.ts b/packages/backend-core/tests/core/utilities/jestUtils.ts index d84eac548c..4a3da8db8c 100644 --- a/packages/backend-core/tests/core/utilities/jestUtils.ts +++ b/packages/backend-core/tests/core/utilities/jestUtils.ts @@ -1,3 +1,5 @@ +import { db } from "../../../src" + export function expectFunctionWasCalledTimesWith( jestFunction: any, times: number, @@ -7,3 +9,22 @@ export function expectFunctionWasCalledTimesWith( jestFunction.mock.calls.filter((call: any) => call[0] === argument).length ).toBe(times) } + +export const expectAnyInternalColsAttributes: { + [K in (typeof db.CONSTANT_INTERNAL_ROW_COLS)[number]]: any +} = { + tableId: expect.anything(), + type: expect.anything(), + _id: expect.anything(), + _rev: expect.anything(), + createdAt: expect.anything(), + updatedAt: expect.anything(), +} + +export const expectAnyExternalColsAttributes: { + [K in (typeof db.CONSTANT_EXTERNAL_ROW_COLS)[number]]: any +} = { + tableId: expect.anything(), + _id: expect.anything(), + _rev: expect.anything(), +} 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/bbui/src/Button/Button.svelte b/packages/bbui/src/Button/Button.svelte index efd5f33bd2..9e49d84d44 100644 --- a/packages/bbui/src/Button/Button.svelte +++ b/packages/bbui/src/Button/Button.svelte @@ -1,6 +1,7 @@ - + {/if} + {#if $$slots} + + {/if} + + diff --git a/packages/bbui/src/FancyForm/FancyButtonRadio.svelte b/packages/bbui/src/FancyForm/FancyButtonRadio.svelte index 510fd8efb8..3048b09555 100644 --- a/packages/bbui/src/FancyForm/FancyButtonRadio.svelte +++ b/packages/bbui/src/FancyForm/FancyButtonRadio.svelte @@ -15,8 +15,6 @@ const dispatch = createEventDispatcher() - $: placeholder = !value - const extractProperty = (value, property) => { if (value && typeof value === "object") { return value[property] diff --git a/packages/bbui/src/Form/Core/CheckboxGroup.svelte b/packages/bbui/src/Form/Core/CheckboxGroup.svelte index 640d5d99cd..2b8a1e438a 100644 --- a/packages/bbui/src/Form/Core/CheckboxGroup.svelte +++ b/packages/bbui/src/Form/Core/CheckboxGroup.svelte @@ -12,23 +12,24 @@ export let getOptionValue = option => option const dispatch = createEventDispatcher() + const onChange = e => { - let tempValue = value - let isChecked = e.target.checked - if (!tempValue.includes(e.target.value) && isChecked) { - tempValue.push(e.target.value) + const optionValue = e.target.value + if (e.target.checked && !value.includes(optionValue)) { + dispatch("change", [...value, optionValue]) + } else { + dispatch( + "change", + value.filter(x => x !== optionValue) + ) } - value = tempValue - dispatch( - "change", - tempValue.filter(val => val !== e.target.value || isChecked) - ) }
{#if options && Array.isArray(options)} {#each options as option} + {@const optionValue = getOptionValue(option)}
{:else if variables.length}
- {#each variables as variable, idx} + {#each variables as variable}
  • import "@spectrum-css/link/dist/index-vars.css" import { createEventDispatcher } from "svelte" + import Tooltip from "../Tooltip/Tooltip.svelte" export let href = "#" export let size = "M" @@ -10,18 +11,61 @@ export let overBackground = false export let target export let download + export let disabled = false + export let tooltip = null const dispatch = createEventDispatcher() + + const onClick = e => { + if (!disabled) { + dispatch("click") + e.stopPropagation() + } + } dispatch("click") && e.stopPropagation()} + on:click={onClick} {href} {target} {download} + class:disabled class:spectrum-Link--primary={primary} class:spectrum-Link--secondary={secondary} class:spectrum-Link--overBackground={overBackground} class:spectrum-Link--quiet={quiet} - class="spectrum-Link spectrum-Link--size{size}"> + + {#if tooltip} +
    + +
    + {/if} + + + diff --git a/packages/bbui/src/Modal/ModalContent.svelte b/packages/bbui/src/Modal/ModalContent.svelte index 226414ee11..3ca584504c 100644 --- a/packages/bbui/src/Modal/ModalContent.svelte +++ b/packages/bbui/src/Modal/ModalContent.svelte @@ -1,3 +1,7 @@ + + + + + +
    (hovered = true)} + on:mouseleave={() => (hovered = false)} +> + +
    + +{#if visible && text && left != null && top != null} + + + {text} + + + +{/if} + + diff --git a/packages/bbui/src/Tooltip/TempTooltip.svelte b/packages/bbui/src/Tooltip/TempTooltip.svelte new file mode 100644 index 0000000000..0d590b1ec6 --- /dev/null +++ b/packages/bbui/src/Tooltip/TempTooltip.svelte @@ -0,0 +1,39 @@ + + + + + diff --git a/packages/bbui/src/index.js b/packages/bbui/src/index.js index d26b938dd5..97762d2b3a 100644 --- a/packages/bbui/src/index.js +++ b/packages/bbui/src/index.js @@ -36,13 +36,19 @@ export { default as Layout } from "./Layout/Layout.svelte" export { default as Page } from "./Layout/Page.svelte" export { default as Link } from "./Link/Link.svelte" export { default as Tooltip } from "./Tooltip/Tooltip.svelte" +export { default as TempTooltip } from "./Tooltip/TempTooltip.svelte" +export { + default as AbsTooltip, + TooltipPosition, + TooltipType, +} from "./Tooltip/AbsTooltip.svelte" export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte" export { default as Menu } from "./Menu/Menu.svelte" export { default as MenuSection } from "./Menu/Section.svelte" export { default as MenuSeparator } from "./Menu/Separator.svelte" export { default as MenuItem } from "./Menu/Item.svelte" export { default as Modal } from "./Modal/Modal.svelte" -export { default as ModalContent } from "./Modal/ModalContent.svelte" +export { default as ModalContent, keepOpen } from "./Modal/ModalContent.svelte" export { default as NotificationDisplay } from "./Notification/NotificationDisplay.svelte" export { default as Notification } from "./Notification/Notification.svelte" export { default as SideNavigation } from "./SideNavigation/Navigation.svelte" diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index e9c8643bce..bbe116721a 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -491,6 +491,7 @@ const getSelectedRowsBindings = asset => { readableBinding: `${table._instanceName}.Selected rows`, category: "Selected rows", icon: "ViewRow", + display: { name: table._instanceName }, })) ) @@ -506,6 +507,7 @@ const getSelectedRowsBindings = asset => { )}.${makePropSafe("selectedRows")}`, readableBinding: `${block._instanceName}.Selected rows`, category: "Selected rows", + display: { name: block._instanceName }, })) ) } diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index 9dca6a64e6..2ca8057b48 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -3,6 +3,7 @@ import { getAutomationStore } from "./store/automation" import { getTemporalStore } from "./store/temporal" import { getThemeStore } from "./store/theme" import { getUserStore } from "./store/users" +import { getDeploymentStore } from "./store/deployments" import { derived } from "svelte/store" import { findComponent, findComponentPath } from "./componentUtils" import { RoleUtils } from "@budibase/frontend-core" @@ -14,6 +15,7 @@ export const automationStore = getAutomationStore() export const themeStore = getThemeStore() export const temporalStore = getTemporalStore() export const userStore = getUserStore() +export const deploymentStore = getDeploymentStore() // Setup history for screens export const screenHistoryStore = createHistoryStore({ @@ -118,3 +120,24 @@ export const selectedAutomation = derived(automationStore, $automationStore => { x => x._id === $automationStore.selectedAutomationId ) }) + +// Derive map of resource IDs to other users. +// We only ever care about a single user in each resource, so if multiple users +// share the same datasource we can just overwrite them. +export const userSelectedResourceMap = derived(userStore, $userStore => { + let map = {} + $userStore.forEach(user => { + const resource = user.builderMetadata?.selectedResourceId + if (resource) { + if (!map[resource]) { + map[resource] = [] + } + map[resource].push(user) + } + }) + return map +}) + +export const isOnlyUser = derived(userStore, $userStore => { + return $userStore.length < 2 +}) diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index 9e5516c512..4ebf0515d6 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -248,4 +248,36 @@ const automationActions = store => ({ } await store.actions.save(newAutomation) }, + replace: async (automationId, automation) => { + if (!automation) { + store.update(state => { + // Remove the automation + state.automations = state.automations.filter( + x => x._id !== automationId + ) + // Select a new automation if required + if (automationId === state.selectedAutomationId) { + store.actions.select(state.automations[0]?._id) + } + return state + }) + } else { + const index = get(store).automations.findIndex( + x => x._id === automation._id + ) + if (index === -1) { + // Automation addition + store.update(state => ({ + ...state, + automations: [...state.automations, automation], + })) + } else { + // Automation update + store.update(state => { + state.automations[index] = automation + return state + }) + } + } + }, }) diff --git a/packages/builder/src/builderStore/store/deployments.js b/packages/builder/src/builderStore/store/deployments.js new file mode 100644 index 0000000000..9f7a68cb46 --- /dev/null +++ b/packages/builder/src/builderStore/store/deployments.js @@ -0,0 +1,22 @@ +import { writable } from "svelte/store" +import { API } from "api" +import { notifications } from "@budibase/bbui" + +export const getDeploymentStore = () => { + let store = writable([]) + + const load = async () => { + try { + store.set(await API.getAppDeployments()) + } catch (err) { + notifications.error("Error fetching deployments") + } + } + + return { + subscribe: store.subscribe, + actions: { + load, + }, + } +} diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 5de58f02e7..f312a58e97 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -38,6 +38,7 @@ import { import { makePropSafe as safe } from "@budibase/string-templates" import { getComponentFieldOptions } from "helpers/formFields" import { createBuilderWebsocket } from "builderStore/websocket" +import { BuilderSocketEvent } from "@budibase/shared-core" const INITIAL_FRONTEND_STATE = { initialised: false, @@ -353,6 +354,33 @@ export const getFrontendStore = () => { } return await sequentialScreenPatch(patchFn, screenId) }, + replace: async (screenId, screen) => { + if (!screenId) { + return + } + if (!screen) { + // Screen deletion + store.update(state => ({ + ...state, + screens: state.screens.filter(x => x._id !== screenId), + })) + } else { + const index = get(store).screens.findIndex(x => x._id === screen._id) + if (index === -1) { + // Screen addition + store.update(state => ({ + ...state, + screens: [...state.screens, screen], + })) + } else { + // Screen update + store.update(state => { + state.screens[index] = screen + return state + }) + } + } + }, delete: async screens => { const screensToDelete = Array.isArray(screens) ? screens : [screens] @@ -1305,7 +1333,7 @@ export const getFrontendStore = () => { links: { save: async (url, title) => { const navigation = get(store).navigation - let links = [...navigation?.links] + let links = [...(navigation?.links ?? [])] // Skip if we have an identical link if (links.find(link => link.url === url && link.text === title)) { @@ -1365,6 +1393,21 @@ export const getFrontendStore = () => { }) }, }, + websocket: { + selectResource: id => { + websocket.emit(BuilderSocketEvent.SelectResource, { + resourceId: id, + }) + }, + }, + metadata: { + replace: metadata => { + store.update(state => ({ + ...state, + ...metadata, + })) + }, + }, } return store diff --git a/packages/builder/src/builderStore/websocket.js b/packages/builder/src/builderStore/websocket.js index af6d58ee7f..6121831c38 100644 --- a/packages/builder/src/builderStore/websocket.js +++ b/packages/builder/src/builderStore/websocket.js @@ -1,10 +1,17 @@ import { createWebsocket } from "@budibase/frontend-core" -import { userStore, store } from "builderStore" +import { + userStore, + store, + deploymentStore, + automationStore, +} from "builderStore" import { datasources, tables } from "stores/backend" import { get } from "svelte/store" import { auth } from "stores/portal" import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core" +import { apps } from "stores/portal" import { notifications } from "@budibase/bbui" +import { helpers } from "@budibase/shared-core" export const createBuilderWebsocket = appId => { const socket = createWebsocket("/socket/builder") @@ -31,7 +38,6 @@ export const createBuilderWebsocket = appId => { }) socket.onOther(BuilderSocketEvent.LockTransfer, ({ userId }) => { if (userId === get(auth)?.user?._id) { - notifications.success("You can now edit screens and automations") store.update(state => ({ ...state, hasLock: true, @@ -39,15 +45,37 @@ export const createBuilderWebsocket = appId => { } }) - // Table events + // Data section events socket.onOther(BuilderSocketEvent.TableChange, ({ id, table }) => { tables.replaceTable(id, table) }) - - // Datasource events socket.onOther(BuilderSocketEvent.DatasourceChange, ({ id, datasource }) => { datasources.replaceDatasource(id, datasource) }) + // Design section events + socket.onOther(BuilderSocketEvent.ScreenChange, ({ id, screen }) => { + store.actions.screens.replace(id, screen) + }) + socket.onOther(BuilderSocketEvent.AppMetadataChange, ({ metadata }) => { + store.actions.metadata.replace(metadata) + }) + socket.onOther( + BuilderSocketEvent.AppPublishChange, + async ({ user, published }) => { + await apps.load() + if (published) { + await deploymentStore.actions.load() + } + const verb = published ? "published" : "unpublished" + notifications.success(`${helpers.getUserLabel(user)} ${verb} this app`) + } + ) + + // Automations + socket.onOther(BuilderSocketEvent.AutomationChange, ({ id, automation }) => { + automationStore.actions.replace(id, automation) + }) + return socket } diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte index 96dc8f4686..15dd864168 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte @@ -168,7 +168,7 @@ Plugins
    - {#each Object.entries(plugins) as [idx, action]} + {#each Object.entries(plugins) as [_, action]}
    + {@html html}
    diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationList.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationList.svelte index 80d65a5cb6..cce0f4eeab 100644 --- a/packages/builder/src/components/automation/AutomationPanel/AutomationList.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/AutomationList.svelte @@ -1,6 +1,10 @@
    - {#each $automationStore.automations.sort(aut => aut.name) as automation, idx} + {#each $automationStore.automations.sort(aut => aut.name) as automation} 0} icon="ShareAndroid" text={automation.name} selected={automation._id === selectedAutomationId} on:click={() => selectAutomation(automation._id)} + selectedBy={$userSelectedResourceMap[automation._id]} > @@ -40,6 +44,5 @@ flex-direction: column; justify-content: flex-start; align-items: stretch; - margin: 0 calc(-1 * var(--spacing-xl)); } diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte index 85e6a5faa3..fc52b7323a 100644 --- a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte @@ -11,8 +11,8 @@ - + diff --git a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte index 5fb27eaaf3..647a8081cf 100644 --- a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte +++ b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte @@ -71,7 +71,7 @@
    - {#each triggers as [idx, trigger]} + {#each triggers as [_, trigger]}
    { Object.values(schema || {}).forEach(col => { @@ -112,6 +109,7 @@ {disableSorting} {customPlaceholder} allowEditRows={allowEditing} + allowEditColumns={allowEditing} showAutoColumns={!hideAutocolumns} {allowClickRows} on:clickrelationship={e => selectRelationship(e.detail)} diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index f319f09b16..4761ccee02 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -18,7 +18,7 @@ import { TableNames, UNEDITABLE_USER_FIELDS } from "constants" import { FIELDS, - RelationshipTypes, + RelationshipType, ALLOWABLE_STRING_OPTIONS, ALLOWABLE_NUMBER_OPTIONS, ALLOWABLE_STRING_TYPES, @@ -58,7 +58,6 @@ let table = $tables.selected let confirmDeleteDialog - let deletion let savingColumn let deleteColName let jsonSchemaModal @@ -185,7 +184,7 @@ dispatch("updatecolumns") if ( saveColumn.type === LINK_TYPE && - saveColumn.relationshipType === RelationshipTypes.MANY_TO_MANY + saveColumn.relationshipType === RelationshipType.MANY_TO_MANY ) { // Fetching the new tables tables.fetch() @@ -216,7 +215,6 @@ notifications.success(`Column ${editableColumn.name} deleted`) confirmDeleteDialog.hide() hide() - deletion = false dispatch("updatecolumns") } } catch (error) { @@ -240,7 +238,7 @@ // Default relationships many to many if (editableColumn.type === LINK_TYPE) { - editableColumn.relationshipType = RelationshipTypes.MANY_TO_MANY + editableColumn.relationshipType = RelationshipType.MANY_TO_MANY } if (editableColumn.type === FORMULA_TYPE) { editableColumn.formulaType = "dynamic" @@ -267,13 +265,11 @@ function confirmDelete() { confirmDeleteDialog.show() - deletion = true } function hideDeleteDialog() { confirmDeleteDialog.hide() deleteColName = "" - deletion = false } function getRelationshipOptions(field) { @@ -290,17 +286,17 @@ { name: `Many ${thisName} rows → many ${linkName} rows`, alt: `Many ${table.name} rows → many ${linkTable.name} rows`, - value: RelationshipTypes.MANY_TO_MANY, + value: RelationshipType.MANY_TO_MANY, }, { name: `One ${linkName} row → many ${thisName} rows`, alt: `One ${linkTable.name} rows → many ${table.name} rows`, - value: RelationshipTypes.ONE_TO_MANY, + value: RelationshipType.ONE_TO_MANY, }, { name: `One ${thisName} row → many ${linkName} rows`, alt: `One ${table.name} rows → many ${linkTable.name} rows`, - value: RelationshipTypes.MANY_TO_ONE, + value: RelationshipType.MANY_TO_ONE, }, ] } diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditRow.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditRow.svelte index ebff97e0fc..3643602e22 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditRow.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditRow.svelte @@ -1,10 +1,9 @@ diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte index 4c069402a7..c18ba313e0 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte @@ -5,7 +5,7 @@ import { notifications } from "@budibase/bbui" import RowFieldControl from "../RowFieldControl.svelte" import { API } from "api" - import { ModalContent, Select, Link } from "@budibase/bbui" + import { keepOpen, ModalContent, Select, Link } from "@budibase/bbui" import ErrorsBox from "components/common/ErrorsBox.svelte" import { goto } from "@roxi/routify" @@ -51,7 +51,7 @@ errors = [...errors, { message: "Role is required" }] } if (errors.length) { - return false + return keepOpen } try { @@ -79,8 +79,8 @@ } else { notifications.error("Error saving user") } - // Prevent closing the modal on errors - return false + + return keepOpen } } @@ -95,9 +95,9 @@ {#if !creating}
    A user's email, role, first and last names cannot be changed from within - the app builder. Please go to the user portal to do this. + the app builder. Please go to the + user portal + to do this.
    {/if} - import { ModalContent, Select, Input, Button } from "@budibase/bbui" + import { keepOpen, ModalContent, Select, Input, Button } from "@budibase/bbui" import { onMount } from "svelte" import { API } from "api" import { notifications } from "@budibase/bbui" @@ -76,7 +76,7 @@ errors.push({ message: "Please choose permissions" }) } if (errors.length) { - return false + return keepOpen } // Save/create the role @@ -85,7 +85,7 @@ notifications.success("Role saved successfully") } catch (error) { notifications.error(`Error saving role - ${error.message}`) - return false + return keepOpen } } diff --git a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte index e2dd1b4cc3..f7b6f61a10 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte @@ -13,6 +13,7 @@ } from "helpers/data/utils" import IntegrationIcon from "./IntegrationIcon.svelte" import { TableNames } from "constants" + import { userSelectedResourceMap } from "builderStore" let openDataSources = [] @@ -166,8 +167,9 @@ selected={$isActive("./table/:tableId") && $tables.selected?._id === TableNames.USERS} on:click={() => selectTable(TableNames.USERS)} + selectedBy={$userSelectedResourceMap[TableNames.USERS]} /> - {#each enrichedDataSources as datasource, idx} + {#each enrichedDataSources as datasource} selectDatasource(datasource)} on:iconClick={() => toggleNode(datasource)} + selectedBy={$userSelectedResourceMap[datasource._id]} >
    $goto(`./query/${query._id}`)} + selectedBy={$userSelectedResourceMap[query._id]} > @@ -212,7 +216,7 @@ diff --git a/packages/builder/src/components/backend/TableNavigator/popovers/EditViewPopover.svelte b/packages/builder/src/components/backend/TableNavigator/popovers/EditViewPopover.svelte index b637f46f5e..99f19935a1 100644 --- a/packages/builder/src/components/backend/TableNavigator/popovers/EditViewPopover.svelte +++ b/packages/builder/src/components/backend/TableNavigator/popovers/EditViewPopover.svelte @@ -35,9 +35,8 @@ try { const isSelected = decodeURIComponent($params.viewName) === $views.selectedViewName - const name = view.name const id = view.tableId - await views.delete(name) + await views.delete(view) notifications.success("View deleted") if (isSelected) { $goto(`./table/${id}`) diff --git a/packages/builder/src/components/commandPalette/CommandPalette.svelte b/packages/builder/src/components/commandPalette/CommandPalette.svelte index 5421e2f123..3c95dbbd0b 100644 --- a/packages/builder/src/components/commandPalette/CommandPalette.svelte +++ b/packages/builder/src/components/commandPalette/CommandPalette.svelte @@ -93,42 +93,42 @@ `https://github.com/Budibase/budibase/issues/new?assignees=&labels=bug&template=bug_report.md&title=` ), }, - ...$datasources?.list.map(datasource => ({ + ...($datasources?.list?.map(datasource => ({ type: "Datasource", name: `${datasource.name}`, icon: "Data", action: () => $goto(`./data/datasource/${datasource._id}`), - })), - ...$tables?.list.map(table => ({ + })) ?? []), + ...($tables?.list?.map(table => ({ type: "Table", name: table.name, icon: "Table", action: () => $goto(`./data/table/${table._id}`), - })), - ...$views?.list.map(view => ({ + })) ?? []), + ...($views?.list?.map(view => ({ type: "View", name: view.name, icon: "Remove", action: () => $goto(`./data/view/${view.name}`), - })), - ...$queries?.list.map(query => ({ + })) ?? []), + ...($queries?.list?.map(query => ({ type: "Query", name: query.name, icon: "SQLQuery", action: () => $goto(`./data/query/${query._id}`), - })), + })) ?? []), ...$sortedScreens.map(screen => ({ type: "Screen", name: screen.routing.route, icon: "WebPage", action: () => $goto(`./design/${screen._id}/components`), })), - ...$automationStore?.automations.map(automation => ({ + ...($automationStore?.automations?.map(automation => ({ type: "Automation", name: automation.name, icon: "ShareAndroid", action: () => $goto(`./automation/${automation._id}`), - })), + })) ?? []), ...Constants.Themes.map(theme => ({ type: "Change Builder Theme", name: theme.name, @@ -208,8 +208,8 @@ async function deployApp() { try { - await API.deployAppChanges() - notifications.success("Application published successfully") + await API.publishAppChanges($store.appId) + notifications.success("App published successfully") } catch (error) { notifications.error("Error publishing app") } @@ -237,11 +237,11 @@
    - {#each categories as [name, results], catIdx} + {#each categories as [name, results]}
    {name}
    - {#each results as command, cmdIdx} + {#each results as command}
    runAction(command)} diff --git a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte index 406885aeb4..7ab7c5dddf 100644 --- a/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte +++ b/packages/builder/src/components/common/CodeEditor/CodeEditor.svelte @@ -8,6 +8,8 @@ closeBrackets, completionKeymap, closeBracketsKeymap, + acceptCompletion, + completionStatus, } from "@codemirror/autocomplete" import { EditorView, @@ -35,7 +37,8 @@ defaultKeymap, historyKeymap, history, - indentWithTab, + indentMore, + indentLess, } from "@codemirror/commands" import { Compartment } from "@codemirror/state" import { javascript } from "@codemirror/lang-javascript" @@ -109,6 +112,22 @@ let isDark = !currentTheme.includes("light") let themeConfig = new Compartment() + const indentWithTabCustom = { + key: "Tab", + run: view => { + if (completionStatus(view.state) == "active") { + acceptCompletion(view) + return true + } + indentMore(view) + return true + }, + shift: view => { + indentLess(view) + return true + }, + } + const buildKeymap = () => { const baseMap = [ ...closeBracketsKeymap, @@ -116,7 +135,7 @@ ...historyKeymap, ...foldKeymap, ...completionKeymap, - indentWithTab, + indentWithTabCustom, ] return baseMap } diff --git a/packages/builder/src/components/common/CreationPage.svelte b/packages/builder/src/components/common/CreationPage.svelte new file mode 100644 index 0000000000..9631e9d961 --- /dev/null +++ b/packages/builder/src/components/common/CreationPage.svelte @@ -0,0 +1,39 @@ + + +
    +
    + {#if showClose} + + {/if} +
    +
    + {heading} +
    + +
    + + diff --git a/packages/builder/src/components/common/CustomSVG.svelte b/packages/builder/src/components/common/CustomSVG.svelte index ce6e964c38..3cd73a8d7a 100644 --- a/packages/builder/src/components/common/CustomSVG.svelte +++ b/packages/builder/src/components/common/CustomSVG.svelte @@ -20,4 +20,5 @@ } + {@html substituteSize(svgHtml)} diff --git a/packages/builder/src/components/common/HelpMenu.svelte b/packages/builder/src/components/common/HelpMenu.svelte index 06ff277d18..25d33d087f 100644 --- a/packages/builder/src/components/common/HelpMenu.svelte +++ b/packages/builder/src/components/common/HelpMenu.svelte @@ -4,7 +4,8 @@ import { licensing } from "stores/portal" import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags" - $: isPremiumUser = $licensing.license && !$licensing.isFreePlan + $: isBusinessAndAbove = + $licensing.isBusinessPlan || $licensing.isEnterprisePlan let show let hide @@ -55,22 +56,22 @@
    {#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING)} -
    +
    Email support
    - {#if !isPremiumUser} + {#if !isBusinessAndAbove}
    - Premium + Business
    {/if}
    diff --git a/packages/builder/src/components/common/NavItem.svelte b/packages/builder/src/components/common/NavItem.svelte index 73a8d3d647..6590463408 100644 --- a/packages/builder/src/components/common/NavItem.svelte +++ b/packages/builder/src/components/common/NavItem.svelte @@ -1,6 +1,8 @@
    {/if} -
    {text}
    +
    + {text} + {#if selectedBy} + + {/if} +
    + {#if withActions}
    {/if} + {#if $$slots.right}
    @@ -119,13 +140,16 @@ } .nav-item.highlighted { background-color: var(--spectrum-global-color-gray-200); + --avatars-background: var(--spectrum-global-color-gray-200); } .nav-item.selected { background-color: var(--spectrum-global-color-gray-300); + --avatars-background: var(--spectrum-global-color-gray-300); color: var(--ink); } .nav-item:hover { background-color: var(--spectrum-global-color-gray-300); + --avatars-background: var(--spectrum-global-color-gray-300); } .nav-item:hover .actions { visibility: visible; @@ -197,6 +221,9 @@ color: var(--spectrum-global-color-gray-900); order: 2; width: 0; + display: flex; + align-items: center; + gap: 8px; } .scrollable .text { flex: 0 0 auto; diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index 61afa6e24d..cdc96e87c5 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -208,7 +208,9 @@
    Current Handlebars syntax is invalid, please check the guide - here + here for more details.
    {:else} diff --git a/packages/builder/src/components/common/bindings/BindingPicker.svelte b/packages/builder/src/components/common/bindings/BindingPicker.svelte index 50e7c66682..f752e37315 100644 --- a/packages/builder/src/components/common/bindings/BindingPicker.svelte +++ b/packages/builder/src/components/common/bindings/BindingPicker.svelte @@ -88,6 +88,7 @@ {/if} {#if hoverTarget.description}
    + {@html hoverTarget.description}
    {/if} @@ -124,7 +125,6 @@ /> - { @@ -162,7 +162,6 @@
      {#each category.bindings as binding} -
    • { diff --git a/packages/builder/src/components/deploy/AppActions.svelte b/packages/builder/src/components/deploy/AppActions.svelte index a9aa3ff506..4b366f2412 100644 --- a/packages/builder/src/components/deploy/AppActions.svelte +++ b/packages/builder/src/components/deploy/AppActions.svelte @@ -10,19 +10,17 @@ Link, Modal, StatusLight, + AbsTooltip, } from "@budibase/bbui" import RevertModal from "components/deploy/RevertModal.svelte" import VersionModal from "components/deploy/VersionModal.svelte" import UpdateAppModal from "components/start/UpdateAppModal.svelte" - import { processStringSync } from "@budibase/string-templates" import ConfirmDialog from "components/common/ConfirmDialog.svelte" import analytics, { Events, EventSource } from "analytics" - import { checkIncomingDeploymentStatus } from "components/deploy/utils" import { API } from "api" - import { onMount } from "svelte" import { apps } from "stores/portal" - import { store } from "builderStore" + import { deploymentStore, store, isOnlyUser } from "builderStore" import TourWrap from "components/portal/onboarding/TourWrap.svelte" import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js" import { goto } from "@roxi/routify" @@ -34,37 +32,31 @@ let updateAppModal let revertModal let versionModal - let appActionPopover let appActionPopoverOpen = false let appActionPopoverAnchor - let publishing = false $: filteredApps = $apps.filter(app => app.devId === application) $: selectedApp = filteredApps?.length ? filteredApps[0] : null - - $: deployments = [] - $: latestDeployments = deployments + $: latestDeployments = $deploymentStore .filter(deployment => deployment.status === "SUCCESS") .sort((a, b) => a.updatedAt > b.updatedAt) - $: isPublished = selectedApp?.status === "published" && latestDeployments?.length > 0 - $: updateAvailable = $store.upgradableVersion && $store.version && $store.upgradableVersion !== $store.version - $: canPublish = !publishing && loaded + $: lastDeployed = getLastDeployedString($deploymentStore) const initialiseApp = async () => { const applicationPkg = await API.fetchAppPackage($store.devId) await store.actions.initialise(applicationPkg) } - const updateDeploymentString = () => { + const getLastDeployedString = deployments => { return deployments?.length ? processStringSync("Published {{ duration time 'millisecond' }} ago", { time: @@ -73,27 +65,6 @@ : "" } - const reviewPendingDeployments = (deployments, newDeployments) => { - if (deployments.length > 0) { - const pending = checkIncomingDeploymentStatus(deployments, newDeployments) - if (pending.length) { - notifications.warning( - "Deployment has been queued and will be processed shortly" - ) - } - } - } - - async function fetchDeployments() { - try { - const newDeployments = await API.getAppDeployments() - reviewPendingDeployments(deployments, newDeployments) - return newDeployments - } catch (err) { - notifications.error("Error fetching deployment overview") - } - } - const previewApp = () => { store.update(state => ({ ...state, @@ -116,14 +87,11 @@ async function publishApp() { try { publishing = true - await API.publishAppChanges($store.appId) - - notifications.send("App published", { + notifications.send("App published successfully", { type: "success", icon: "GlobeCheck", }) - await completePublish() } catch (error) { console.error(error) @@ -163,210 +131,201 @@ const completePublish = async () => { try { await apps.load() - deployments = await fetchDeployments() + await deploymentStore.actions.load() } catch (err) { notifications.error("Error refreshing app") } } - - onMount(async () => { - if (!$apps.length) { - await apps.load() - } - deployments = await fetchDeployments() - }) -{#if $store.hasLock} -
      -
      - - {#if updateAvailable} -
      -
      - - - Update - -
      -
      - {/if} - -
      -
      - { - store.update(state => { - state.builderSidePanel = true - return state - }) - }} - > - Users - -
      -
      -
      - -
      +
      +
      + {#if updateAvailable && $isOnlyUser} +
      - - Preview + + + Update
      - - -
      { - if (!appActionPopoverOpen) { - appActionPopover.show() - } else { - appActionPopover.hide() - } - }} - > -
      -
      - - - - Publish - - - -
      + {/if} + +
      +
      + { + store.update(state => { + state.builderSidePanel = true + return state + }) + }} + > + Users +
      - { - appActionPopoverOpen = false - }} - on:open={() => { - appActionPopoverOpen = true - }} - > -
      - - - - { - if (isPublished) { - viewApp() - } else { - appActionPopover.hide() - updateAppModal.show() - } - }} - > - {$store.url} - {#if isPublished} - - {:else} - - {/if} - - +
      + - - - {#if isPublished} - - {updateDeploymentString(deployments)} - - - Unpublish - - - Revert - - {:else} - Not published - {/if} - - -
      - {#if $store.hasLock} - {#if isPublished} - { - $goto("./settings/embed") - appActionPopover.hide() - }} - > - Embed - - {/if} - - {/if} -
      - -
      - +
      +
      + + Preview +
      -
      - - - Are you sure you want to unpublish the app {selectedApp?.name}? - - - - { + if (!appActionPopoverOpen) { + appActionPopover.show() + } else { + appActionPopover.hide() + } }} - onUpdateComplete={async () => { - await initialiseApp() - }} - /> - + > +
      +
      + + + + Publish + + + +
      +
      + { + appActionPopoverOpen = false + }} + on:open={() => { + appActionPopoverOpen = true + }} + > +
      + + + { + if (isPublished) { + viewApp() + } else { + appActionPopover.hide() + updateAppModal.show() + } + }} + > + {$store.url} + {#if isPublished} + + {:else} + + {/if} + + - - -{:else} -
      -
      - - Preview - + + + {#if isPublished} + + {lastDeployed} + + + Unpublish + + + + + Revert + + + + {:else} + Not published + {/if} + + +
      + {#if isPublished} + { + $goto("./settings/embed") + appActionPopover.hide() + }} + > + Embed + + {/if} + +
      + +
      +
      -{/if} +
      + + + + Are you sure you want to unpublish the app {selectedApp?.name}? + + + + { + await initialiseApp() + }} + /> + + + + diff --git a/packages/builder/src/components/deploy/DeploymentHistory.svelte b/packages/builder/src/components/deploy/DeploymentHistory.svelte deleted file mode 100644 index e025abf1c7..0000000000 --- a/packages/builder/src/components/deploy/DeploymentHistory.svelte +++ /dev/null @@ -1,236 +0,0 @@ - - -{#if deployments.length > 0} -
      -
      - Deployment History -
      - {#if deployments.some(deployment => deployment.status === DeploymentStatus.SUCCESS)} - View Your Deployed App → - - {/if} -
      -
      -
      - {#each deployments as deployment} -
      -
      - - {formatDate(deployment.updatedAt, "fullDate")} - - - {formatDate(deployment.updatedAt, "timeOnly")} - -
      -
      - {#if deployment.status.toLowerCase() === "pending"} - - {/if} -
      showErrorReasonModal(deployment.err)} - class={`deployment-status ${deployment.status}`} - > - - {deployment.status} - {#if deployment.status === DeploymentStatus.FAILURE} - - {/if} - -
      -
      -
      - {/each} -
      -
      -{/if} - - - - - - {errorReason} - - - - diff --git a/packages/builder/src/components/deploy/utils.js b/packages/builder/src/components/deploy/utils.js deleted file mode 100644 index cb254f0dbf..0000000000 --- a/packages/builder/src/components/deploy/utils.js +++ /dev/null @@ -1,25 +0,0 @@ -export const DeploymentStatus = { - SUCCESS: "SUCCESS", - PENDING: "PENDING", - FAILURE: "FAILURE", -} - -// Required to check any updated deployment statuses between polls -export function checkIncomingDeploymentStatus(current, incoming) { - return incoming.reduce((acc, incomingDeployment) => { - if (incomingDeployment.status === DeploymentStatus.FAILURE) { - const currentDeployment = current.find( - deployment => deployment._id === incomingDeployment._id - ) - - //We have just been notified of an ongoing deployments failure - if ( - !currentDeployment || - currentDeployment.status === DeploymentStatus.PENDING - ) { - acc.push(incomingDeployment) - } - } - return acc - }, []) -} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenDetailsModal.svelte b/packages/builder/src/components/design/ScreenDetailsModal.svelte similarity index 100% rename from packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenDetailsModal.svelte rename to packages/builder/src/components/design/ScreenDetailsModal.svelte 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/ButtonActionEditor/ButtonActionDrawer.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte index aa8e1af950..ef6410abca 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte @@ -16,6 +16,7 @@ makeStateBinding, } from "builderStore/dataBinding" import { currentAsset, store } from "builderStore" + import { cloneDeep } from "lodash/fp" const flipDurationMs = 150 const EVENT_TYPE_KEY = "##eventHandlerType" @@ -29,6 +30,26 @@ let actionQuery let selectedAction = actions?.length ? actions[0] : null + const setUpdateActions = actions => { + return actions + ? cloneDeep(actions) + .filter(action => { + return ( + action[EVENT_TYPE_KEY] === "Update State" && + action.parameters?.type === "set" && + action.parameters.key + ) + }) + .reduce((acc, action) => { + acc[action.id] = action + return acc + }, {}) + : [] + } + + // Snapshot original action state + let updateStateActions = setUpdateActions(actions) + $: { // Ensure parameters object is never null if (selectedAction && !selectedAction.parameters) { @@ -125,8 +146,9 @@ actions = e.detail.items } - const getAllBindings = (bindings, eventContextBindings, actions) => { + const getAllBindings = (actionBindings, eventContextBindings, actions) => { let allBindings = [] + let cloneActionBindings = cloneDeep(actionBindings) if (!actions) { return [] } @@ -144,11 +166,19 @@ .forEach(action => { // Check we have a binding for this action, and generate one if not const stateBinding = makeStateBinding(action.parameters.key) - const hasKey = bindings.some(binding => { + const hasKey = actionBindings.some(binding => { return binding.runtimeBinding === stateBinding.runtimeBinding }) if (!hasKey) { - bindings.push(stateBinding) + let existing = updateStateActions[action.id] + if (existing) { + const existingBinding = makeStateBinding(existing.parameters.key) + cloneActionBindings = cloneActionBindings.filter( + binding => + binding.runtimeBinding !== existingBinding.runtimeBinding + ) + } + allBindings.push(stateBinding) } }) // Get which indexes are asynchronous automations as we want to filter them out from the bindings @@ -164,17 +194,23 @@ .filter(index => index !== undefined) // Based on the above, filter out the asynchronous automations from the bindings - if (asynchronousAutomationIndexes) { - allBindings = eventContextBindings - .filter((binding, index) => { + let contextBindings = asynchronousAutomationIndexes + ? eventContextBindings.filter((binding, index) => { return !asynchronousAutomationIndexes.includes(index) }) - .concat(bindings) - } else { - allBindings = eventContextBindings.concat(bindings) - } + : eventContextBindings + + allBindings = contextBindings + .concat(cloneActionBindings) + .concat(allBindings) + return allBindings } + + const toDisplay = eventKey => { + const type = actionTypes.find(action => action.name == eventKey) + return type?.displayName || type?.name + } @@ -200,7 +236,9 @@
        {#each category as actionType}
      • - {actionType.name} + + {actionType.displayName || actionType.name} +
      • {/each}
      @@ -231,7 +269,7 @@ >
      - {index + 1}. {action[EVENT_TYPE_KEY]} + {index + 1}. {toDisplay(action[EVENT_TYPE_KEY])}
      -
      {actionText}
      -Define actions +
      + {actionText} +
      @@ -89,9 +90,7 @@ diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte index 109eb9a956..e4a5f171ff 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte @@ -1,5 +1,5 @@
      - - Please specify one or more rows to delete. +
      + + + {/if} +
      diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte index 72d38cded0..971a5ede44 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/TriggerAutomation.svelte @@ -42,7 +42,6 @@ } }) - $: hasAutomations = automations && automations.length > 0 $: selectedAutomation = automations?.find( a => a._id === parameters?.automationId ) @@ -145,12 +144,6 @@ padding-bottom: 20px; } - .params { - display: flex; - flex-wrap: nowrap; - gap: 25px; - } - .synchronous-info { display: flex; gap: var(--spacing-s); diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json index 2ec7235c59..6ed545f541 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/manifest.json @@ -24,6 +24,7 @@ }, { "name": "Delete Row", + "displayName": "Delete Rows", "type": "data", "component": "DeleteRow" }, diff --git a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte index 5f1abdbeca..cebb429ac4 100644 --- a/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/ColumnEditor/ColumnEditor.svelte @@ -20,6 +20,7 @@ let drawer let boundValue + $: text = getText(value) $: datasource = getDatasourceForProvider($currentAsset, componentInstance) $: schema = getSchema($currentAsset, datasource) $: options = allowCellEditing @@ -31,6 +32,17 @@ allowLinks: true, }) + const getText = value => { + if (!value?.length) { + return "All columns" + } + let text = `${value.length} column` + if (value.length !== 1) { + text += "s" + } + return text + } + const getSchema = (asset, datasource) => { const schema = getSchemaForDatasource(asset, datasource).schema @@ -76,7 +88,7 @@
      - Configure columns + {text}
      diff --git a/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte b/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte index f9127460e2..f9dccf586c 100644 --- a/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte +++ b/packages/builder/src/components/design/settings/controls/FieldConfiguration/FieldConfiguration.svelte @@ -8,32 +8,39 @@ getSchemaForDatasource, } from "builderStore/dataBinding" import { currentAsset } from "builderStore" - import { getFields } from "helpers/searchFields" export let componentInstance export let value = [] - const convertOldColumnFormat = oldColumns => { - if (typeof oldColumns?.[0] === "string") { - value = oldColumns.map(field => ({ name: field, displayName: field })) - } - } - - $: convertOldColumnFormat(value) - const dispatch = createEventDispatcher() let drawer let boundValue + $: text = getText(value) + $: convertOldColumnFormat(value) $: datasource = getDatasourceForProvider($currentAsset, componentInstance) $: schema = getSchema($currentAsset, datasource) $: options = Object.keys(schema || {}) $: sanitisedValue = getValidColumns(value, options) $: updateBoundValue(sanitisedValue) - $: enrichedSchemaFields = getFields(Object.values(schema || {}), { - allowLinks: true, - }) + + const getText = value => { + if (!value?.length) { + return "All fields" + } + let text = `${value.length} field` + if (value.length !== 1) { + text += "s" + } + return text + } + + const convertOldColumnFormat = oldColumns => { + if (typeof oldColumns?.[0] === "string") { + value = oldColumns.map(field => ({ name: field, displayName: field })) + } + } const getSchema = (asset, datasource) => { const schema = getSchemaForDatasource(asset, datasource).schema @@ -79,7 +86,10 @@ } -Configure fields +
      + {text} +
      + Configure the fields in your form. @@ -87,3 +97,9 @@ + + diff --git a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte index e8bbb8a354..4b1ab0d68a 100644 --- a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte +++ b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte @@ -192,7 +192,7 @@
      - {#each rawFilters as filter, idx} + {#each rawFilters as filter} - import { store } from "builderStore" - import { redirect } from "@roxi/routify" - - // Prevent access for other users than the lock holder - $: { - if (!$store.hasLock) { - $redirect("../data") - } - } - - diff --git a/packages/builder/src/pages/builder/app/[application]/design/blank.png b/packages/builder/src/pages/builder/app/[application]/design/blank.png new file mode 100644 index 0000000000..4867cc8dc3 Binary files /dev/null and b/packages/builder/src/pages/builder/app/[application]/design/blank.png differ diff --git a/packages/builder/src/pages/builder/app/[application]/design/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/index.svelte index 6490000b35..e9c72a6e1f 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/index.svelte @@ -1,67 +1,12 @@ - -{#if loaded} -
      - - logo -
      - LET’S BRING THIS APP TO LIFE -
      - -
      -
      -{/if} - - diff --git a/packages/builder/src/pages/builder/app/[application]/design/new.svelte b/packages/builder/src/pages/builder/app/[application]/design/new.svelte new file mode 100644 index 0000000000..ab9c8eb91d --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/design/new.svelte @@ -0,0 +1,104 @@ + + +
      + 0} + onClose={() => $goto(`./${$store.screens[0]._id}`)} + heading={hasScreens ? "Create new screen" : "Create your first screen"} + > +
      + Start from scratch or create screens from your data +
      + +
      +
      createScreenModal.show("blank")}> +
      + +
      +
      + Blank screen + Add an empty blank screen +
      +
      + +
      createScreenModal.show("table")}> +
      + +
      +
      + Table + View, edit and delete rows on a table +
      +
      +
      +
      +
      + + + + diff --git a/packages/builder/src/pages/builder/app/[application]/design/table.png b/packages/builder/src/pages/builder/app/[application]/design/table.png new file mode 100644 index 0000000000..0bfda10bbe Binary files /dev/null and b/packages/builder/src/pages/builder/app/[application]/design/table.png differ diff --git a/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte index 225e3977c3..7063a271be 100644 --- a/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte @@ -1,8 +1,9 @@ @@ -44,12 +45,20 @@ active={$isActive("./version")} />
      - { - deleteModal.show() - }} - /> + + { + deleteModal.show() + }} + /> +
      @@ -61,7 +70,7 @@ diff --git a/packages/builder/src/pages/builder/portal/apps/create.svelte b/packages/builder/src/pages/builder/portal/apps/create.svelte index c1b7e5d115..1f2c579071 100644 --- a/packages/builder/src/pages/builder/portal/apps/create.svelte +++ b/packages/builder/src/pages/builder/portal/apps/create.svelte @@ -11,7 +11,6 @@ let template let creationModal = false let appLimitModal - let creatingApp = false const initiateAppCreation = () => { if ($licensing?.usageMetrics?.apps >= 100) { @@ -19,13 +18,11 @@ } else { template = null creationModal.show() - creatingApp = true } } const stopAppCreation = () => { template = null - creatingApp = false } const initiateAppImport = () => { @@ -34,7 +31,6 @@ } else { template = { fromFile: true } creationModal.show() - creatingApp = true } } diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte index ccbeb6d57a..dc5ee25688 100644 --- a/packages/builder/src/pages/builder/portal/apps/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/index.svelte @@ -30,7 +30,6 @@ let creationModal let appLimitModal let accountLockedModal - let creatingApp = false let searchTerm = "" let creatingFromTemplate = false let automationErrors @@ -123,14 +122,12 @@ } else { template = null creationModal.show() - creatingApp = true } } const initiateAppImport = () => { template = { fromFile: true } creationModal.show() - creatingApp = true } const autoCreateApp = async () => { @@ -173,7 +170,6 @@ const stopAppCreation = () => { template = null - creatingApp = false } function createAppFromTemplateUrl(templateKey) { diff --git a/packages/builder/src/pages/builder/portal/apps/onboarding/index.svelte b/packages/builder/src/pages/builder/portal/apps/onboarding/index.svelte index e67d3422c0..efc9236699 100644 --- a/packages/builder/src/pages/builder/portal/apps/onboarding/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/onboarding/index.svelte @@ -7,8 +7,6 @@ import { API } from "api" import { store, automationStore } from "builderStore" import { auth, admin } from "stores/portal" - import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen" - import { Roles } from "constants/backend" let name = "My first app" let url = "my-first-app" @@ -38,11 +36,6 @@ // Create user await auth.setInitInfo({}) - let defaultScreenTemplate = createFromScratchScreen.create() - defaultScreenTemplate.routing.route = "/home" - defaultScreenTemplate.routing.roldId = Roles.BASIC - await store.actions.screens.save(defaultScreenTemplate) - appId = createdApp.instance._id return createdApp } diff --git a/packages/builder/src/pages/builder/portal/settings/diagnostics.svelte b/packages/builder/src/pages/builder/portal/settings/diagnostics.svelte new file mode 100644 index 0000000000..bba0b4ea29 --- /dev/null +++ b/packages/builder/src/pages/builder/portal/settings/diagnostics.svelte @@ -0,0 +1,84 @@ + + +{#if $auth.isAdmin && diagnosticInfo} + + + Diagnostics + Please include this diagnostic information in support requests and github issues + by clicking the button on the top right to copy to clipboard. + + +
      +
      + +
      +