diff --git a/charts/budibase/templates/app-service-deployment.yaml b/charts/budibase/templates/app-service-deployment.yaml
index 278bd1767f..171bf5e6c1 100644
--- a/charts/budibase/templates/app-service-deployment.yaml
+++ b/charts/budibase/templates/app-service-deployment.yaml
@@ -30,6 +30,7 @@ spec:
{{- toYaml .Values.services.apps.templateLabels | indent 8 -}}
{{ end }}
spec:
+ terminationGracePeriodSeconds: 60
containers:
- env:
- name: BUDIBASE_ENVIRONMENT
diff --git a/charts/budibase/templates/worker-service-deployment.yaml b/charts/budibase/templates/worker-service-deployment.yaml
index 94fdd0b94e..a68d466487 100644
--- a/charts/budibase/templates/worker-service-deployment.yaml
+++ b/charts/budibase/templates/worker-service-deployment.yaml
@@ -30,6 +30,7 @@ spec:
{{- toYaml .Values.services.worker.templateLabels | indent 8 -}}
{{ end }}
spec:
+ terminationGracePeriodSeconds: 60
containers:
- env:
- name: BUDIBASE_ENVIRONMENT
diff --git a/lerna.json b/lerna.json
index b957585f7b..a572de1d7f 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
- "version": "3.10.0",
+ "version": "3.10.2",
"npmClient": "yarn",
"concurrency": 20,
"command": {
diff --git a/packages/backend-core/src/events/publishers/user.ts b/packages/backend-core/src/events/publishers/user.ts
index 43e5355bd5..14d336e852 100644
--- a/packages/backend-core/src/events/publishers/user.ts
+++ b/packages/backend-core/src/events/publishers/user.ts
@@ -14,7 +14,6 @@ import {
UserPermissionAssignedEvent,
UserPermissionRemovedEvent,
UserUpdatedEvent,
- UserOnboardingEvent,
} from "@budibase/types"
import { isScim } from "../../context"
@@ -51,16 +50,6 @@ async function deleted(user: User) {
await publishEvent(Event.USER_DELETED, properties)
}
-export async function onboardingComplete(user: User) {
- const properties: UserOnboardingEvent = {
- userId: user._id as string,
- audited: {
- email: user.email,
- },
- }
- await publishEvent(Event.USER_ONBOARDING_COMPLETE, properties)
-}
-
// PERMISSIONS
async function permissionAdminAssigned(user: User, timestamp?: number) {
@@ -191,7 +180,6 @@ export default {
permissionAdminRemoved,
permissionBuilderAssigned,
permissionBuilderRemoved,
- onboardingComplete,
invited,
inviteAccepted,
passwordForceReset,
diff --git a/packages/backend-core/src/users/events.ts b/packages/backend-core/src/users/events.ts
index f170c9ffe9..c6c3e94b33 100644
--- a/packages/backend-core/src/users/events.ts
+++ b/packages/backend-core/src/users/events.ts
@@ -76,10 +76,6 @@ export const handleSaveEvents = async (
await events.user.permissionAdminRemoved(user)
}
- if (isOnboardingComplete(user, existingUser)) {
- await events.user.onboardingComplete(user)
- }
-
if (
!existingUser.forceResetPassword &&
user.forceResetPassword &&
@@ -122,10 +118,6 @@ const isRemovingAdmin = (user: any, existingUser: any) => {
return isRemovingPermission(user, existingUser, hasAdminPermissions)
}
-const isOnboardingComplete = (user: any, existingUser: any) => {
- return !existingUser?.onboardedAt && typeof user.onboardedAt === "string"
-}
-
/**
* Check if a permission is being added to a new or existing user.
*/
diff --git a/packages/builder/src/components/common/EditableIcon.svelte b/packages/builder/src/components/common/EditableIcon.svelte
index 150a23ecb7..1c3975c6f9 100644
--- a/packages/builder/src/components/common/EditableIcon.svelte
+++ b/packages/builder/src/components/common/EditableIcon.svelte
@@ -1,15 +1,13 @@
-
@@ -28,7 +26,7 @@
-
+
diff --git a/packages/builder/src/components/portal/onboarding/TourWrap.svelte b/packages/builder/src/components/portal/onboarding/TourWrap.svelte
deleted file mode 100644
index db0841a05b..0000000000
--- a/packages/builder/src/components/portal/onboarding/TourWrap.svelte
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
diff --git a/packages/builder/src/components/portal/onboarding/steps/NewFormSteps.svelte b/packages/builder/src/components/portal/onboarding/steps/NewFormSteps.svelte
deleted file mode 100644
index 7c3679f9e8..0000000000
--- a/packages/builder/src/components/portal/onboarding/steps/NewFormSteps.svelte
+++ /dev/null
@@ -1,9 +0,0 @@
-
- When faced with a sizable form, consider implementing a multi-step approach to
- enhance user experience.
-
-
- Breaking the form into multiple steps can significantly improve usability by
- making the process more digestible for your users.
-
-
diff --git a/packages/builder/src/components/portal/onboarding/steps/NewViewUpdateFormRowId.svelte b/packages/builder/src/components/portal/onboarding/steps/NewViewUpdateFormRowId.svelte
deleted file mode 100644
index db34e9cb5d..0000000000
--- a/packages/builder/src/components/portal/onboarding/steps/NewViewUpdateFormRowId.svelte
+++ /dev/null
@@ -1,17 +0,0 @@
-
- You can use bindings to set the Row ID on your form.
-
- This will allow you to pull the correct information into your form and allow
- you to update!
-
-
- How to pass a row ID using bindings
-
-
-
-
diff --git a/packages/builder/src/components/portal/onboarding/steps/OnboardingData.svelte b/packages/builder/src/components/portal/onboarding/steps/OnboardingData.svelte
deleted file mode 100644
index c2bd0ad5e9..0000000000
--- a/packages/builder/src/components/portal/onboarding/steps/OnboardingData.svelte
+++ /dev/null
@@ -1,10 +0,0 @@
-
- In this section you can manage the data for your app:
-
- - Connect data sources
- - Edit data
- - Manage read & write access
- - Create views
- - Add bindings
-
-
diff --git a/packages/builder/src/components/portal/onboarding/steps/OnboardingDesign.svelte b/packages/builder/src/components/portal/onboarding/steps/OnboardingDesign.svelte
deleted file mode 100644
index 84d84777f5..0000000000
--- a/packages/builder/src/components/portal/onboarding/steps/OnboardingDesign.svelte
+++ /dev/null
@@ -1,10 +0,0 @@
-
- After setting up your data, Design is where you build the screens for your
- app:
-
- - Add screens
- - Add components
- - Choose your theme
- - Edit navigation
-
-
diff --git a/packages/builder/src/components/portal/onboarding/steps/OnboardingPublish.svelte b/packages/builder/src/components/portal/onboarding/steps/OnboardingPublish.svelte
deleted file mode 100644
index 8913d77482..0000000000
--- a/packages/builder/src/components/portal/onboarding/steps/OnboardingPublish.svelte
+++ /dev/null
@@ -1,7 +0,0 @@
-
- Once you’re happy with your app you can publish it to production!
-
- After publishing, any changes you make will not take affect until you next
- publish.
-
-
diff --git a/packages/builder/src/components/portal/onboarding/steps/index.js b/packages/builder/src/components/portal/onboarding/steps/index.js
deleted file mode 100644
index e15d191652..0000000000
--- a/packages/builder/src/components/portal/onboarding/steps/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export { default as OnboardingData } from "./OnboardingData.svelte"
-export { default as OnboardingDesign } from "./OnboardingDesign.svelte"
-export { default as OnboardingPublish } from "./OnboardingPublish.svelte"
-export { default as NewViewUpdateFormRowId } from "./NewViewUpdateFormRowId.svelte"
-export { default as NewFormSteps } from "./NewFormSteps.svelte"
diff --git a/packages/builder/src/components/portal/onboarding/tourHandler.js b/packages/builder/src/components/portal/onboarding/tourHandler.js
deleted file mode 100644
index c89475438d..0000000000
--- a/packages/builder/src/components/portal/onboarding/tourHandler.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { builderStore } from "@/stores/builder"
-import { get } from "svelte/store"
-
-const registerNode = async (node, tourStepKey) => {
- if (!node) {
- console.warn("Tour Handler - an anchor node is required")
- }
-
- if (!get(builderStore).tourKey) {
- console.warn("Tour Handler - No active tour ", tourStepKey, node)
- return
- }
-
- builderStore.registerTourNode(tourStepKey, node)
-}
-
-export function tourHandler(node, tourStepKey) {
- if (node && tourStepKey) {
- registerNode(node, tourStepKey)
- }
- return {
- destroy: () => {
- builderStore.destroyTourNode(tourStepKey)
- },
- }
-}
diff --git a/packages/builder/src/components/portal/onboarding/tours.js b/packages/builder/src/components/portal/onboarding/tours.js
deleted file mode 100644
index ad62bdde4d..0000000000
--- a/packages/builder/src/components/portal/onboarding/tours.js
+++ /dev/null
@@ -1,238 +0,0 @@
-import { get } from "svelte/store"
-import { builderStore } from "@/stores/builder"
-import { auth } from "@/stores/portal"
-import analytics from "@/analytics"
-import {
- OnboardingData,
- OnboardingDesign,
- OnboardingPublish,
- NewViewUpdateFormRowId,
- NewFormSteps,
-} from "./steps"
-import { API } from "@/api"
-
-const ONBOARDING_EVENT_PREFIX = "onboarding"
-
-export const TOUR_STEP_KEYS = {
- BUILDER_APP_PUBLISH: "builder-app-publish",
- BUILDER_DATA_SECTION: "builder-data-section",
- BUILDER_DESIGN_SECTION: "builder-design-section",
- BUILDER_USER_MANAGEMENT: "builder-user-management",
- BUILDER_AUTOMATION_SECTION: "builder-automation-section",
- FEATURE_USER_MANAGEMENT: "feature-user-management",
- BUILDER_FORM_CREATE_STEPS: "builder-form-create-steps",
- BUILDER_FORM_VIEW_UPDATE_STEPS: "builder-form-view-update-steps",
- BUILDER_FORM_ROW_ID: "builder-form-row-id",
-}
-
-export const TOUR_KEYS = {
- TOUR_BUILDER_ONBOARDING: "builder-onboarding",
- FEATURE_ONBOARDING: "feature-onboarding",
- BUILDER_FORM_CREATE: "builder-form-create",
- BUILDER_FORM_VIEW_UPDATE: "builder-form-view-update",
-}
-
-export const getCurrentStepIdx = (steps, tourStepKey) => {
- if (!steps?.length) {
- return
- }
- if (steps?.length && !tourStepKey) {
- return 0
- }
- return steps.findIndex(step => step.id === tourStepKey)
-}
-
-const endUserOnboarding = async ({ skipped = false } = {}) => {
- // Mark the users onboarding as complete
- // Clear all tour related state
- if (get(auth).user) {
- try {
- await API.updateSelf({
- onboardedAt: new Date().toISOString(),
- })
-
- if (skipped) {
- tourEvent("skipped")
- }
-
- // Update the cached user
- await auth.getSelf()
-
- builderStore.endBuilderOnboarding()
- builderStore.setTour()
- } catch (e) {
- console.error("Onboarding failed", e)
- return false
- }
- return true
- }
-}
-
-const endTour = async ({ key, skipped = false } = {}) => {
- const { tours = {} } = get(auth).user
- tours[key] = new Date().toISOString()
-
- await API.updateSelf({
- tours,
- })
-
- if (skipped) {
- tourEvent(key, skipped)
- }
-
- // Update the cached user
- await auth.getSelf()
-
- // Reset tour state
- builderStore.setTour()
-}
-
-const tourEvent = (eventKey, skipped) => {
- analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, {
- eventSource: EventSource.PORTAL,
- skipped,
- })
-}
-
-const getTours = () => {
- return {
- [TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: {
- steps: [
- {
- id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION,
- title: "Data",
- route: "/builder/app/:application/data",
- layout: OnboardingData,
- query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
- onLoad: async () => {
- tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
- },
- align: "left",
- },
- {
- id: TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION,
- title: "Design",
- route: "/builder/app/:application/design",
- layout: OnboardingDesign,
- query: ".topleftnav .spectrum-Tabs-item#builder-design-tab",
- onLoad: () => {
- tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
- },
- align: "left",
- },
- {
- id: TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION,
- title: "Automations",
- route: "/builder/app/:application/automation",
- query: ".topleftnav .spectrum-Tabs-item#builder-automation-tab",
- body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
- onLoad: () => {
- tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION)
- },
- align: "left",
- },
- {
- id: TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT,
- title: "Users",
- query: ".toprightnav #builder-app-users-button",
- body: "Add users to your app and control what level of access they have.",
- onLoad: () => {
- tourEvent(TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT)
- },
- },
- {
- id: TOUR_STEP_KEYS.BUILDER_APP_PUBLISH,
- title: "Publish",
- layout: OnboardingPublish,
- route: "/builder/app/:application/design",
- query: ".toprightnav #builder-app-publish-button",
- onLoad: () => {
- tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
- },
- onComplete: endUserOnboarding,
- },
- ],
- onSkip: async () => {
- await endUserOnboarding({ skipped: true })
- },
- endRoute: "/builder/app/:application/data",
- },
- [TOUR_KEYS.FEATURE_ONBOARDING]: {
- steps: [
- {
- id: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
- title: "Users",
- query: ".toprightnav #builder-app-users-button",
- body: "Add users to your app and control what level of access they have.",
- onLoad: () => {
- tourEvent(TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT)
- },
- onComplete: endUserOnboarding,
- },
- ],
- },
- [TOUR_KEYS.BUILDER_FORM_CREATE]: {
- steps: [
- {
- id: TOUR_STEP_KEYS.BUILDER_FORM_CREATE_STEPS,
- title: "Add multiple steps",
- layout: NewFormSteps,
- query: "#steps-prop-control-wrap",
- onComplete: () => {
- builderStore.highlightSetting()
- endTour({ key: TOUR_KEYS.BUILDER_FORM_CREATE })
- },
- onLoad: () => {
- tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_CREATE_STEPS)
- builderStore.highlightSetting("steps", "info")
- },
- align: "left-outside",
- },
- ],
- },
- [TOUR_KEYS.BUILDER_FORM_VIEW_UPDATE]: {
- steps: [
- {
- id: TOUR_STEP_KEYS.BUILDER_FORM_ROW_ID,
- title: "Add row ID to update a row",
- layout: NewViewUpdateFormRowId,
- query: "#rowId-prop-control-wrap",
- onLoad: () => {
- tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_ROW_ID)
- builderStore.highlightSetting("rowId", "info")
- },
- align: "left-outside",
- },
- {
- id: TOUR_STEP_KEYS.BUILDER_FORM_VIEW_UPDATE_STEPS,
- title: "Add multiple steps",
- layout: NewFormSteps,
- query: "#steps-prop-control-wrap",
- onComplete: () => {
- builderStore.highlightSetting()
- endTour({ key: TOUR_KEYS.BUILDER_FORM_VIEW_UPDATE })
- },
- onLoad: () => {
- tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_VIEW_UPDATE_STEPS)
- builderStore.highlightSetting("steps", "info")
- },
- align: "left-outside",
- scrollIntoView: true,
- },
- ],
- onSkip: async () => {
- builderStore.highlightSetting()
- endTour({ key: TOUR_KEYS.BUILDER_FORM_VIEW_UPDATE, skipped: true })
- },
- },
- }
-}
-
-export const TOURS = getTours()
-export const TOURSBYSTEP = Object.keys(TOURS).reduce((acc, tour) => {
- TOURS[tour].steps.forEach(element => {
- acc[element.id] = element
- acc[element.id]["tour"] = tour
- })
- return acc
-}, {})
diff --git a/packages/builder/src/components/start/ChooseIconModal.svelte b/packages/builder/src/components/start/ChooseIconModal.svelte
index 3128b70807..ece0bf47a5 100644
--- a/packages/builder/src/components/start/ChooseIconModal.svelte
+++ b/packages/builder/src/components/start/ChooseIconModal.svelte
@@ -1,18 +1,9 @@
-
diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte
index cda02d4835..b932e277a0 100644
--- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte
@@ -8,7 +8,7 @@
userStore,
deploymentStore,
} from "@/stores/builder"
- import { auth, appsStore } from "@/stores/portal"
+ import { appsStore } from "@/stores/portal"
import {
Icon,
Tabs,
@@ -22,11 +22,8 @@
import { isActive, url, goto, layout, redirect } from "@roxi/routify"
import { capitalise } from "@/helpers"
import { onMount, onDestroy } from "svelte"
- import TourWrap from "@/components/portal/onboarding/TourWrap.svelte"
- import TourPopover from "@/components/portal/onboarding/TourPopover.svelte"
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
import { UserAvatars } from "@budibase/frontend-core"
- import { TOUR_KEYS } from "@/components/portal/onboarding/tours.js"
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
import EnterpriseBasicTrialModal from "@/components/portal/onboarding/EnterpriseBasicTrialModal.svelte"
import UpdateAppTopNav from "@/components/common/UpdateAppTopNav.svelte"
@@ -37,7 +34,6 @@
let hasSynced = false
let loaded = false
- $: loaded && initTour()
$: selected = capitalise(
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
)
@@ -75,20 +71,6 @@
$goto($builderStore.previousTopNavPath[path] || path)
}
- const initTour = async () => {
- // Check if onboarding is enabled.
- if (!$auth.user?.onboardedAt) {
- builderStore.startBuilderOnboarding()
- } else {
- // Feature tour date
- const release_date = new Date("2023-03-01T00:00:00.000Z")
- const onboarded = new Date($auth.user?.onboardedAt)
- if (onboarded < release_date) {
- builderStore.setTour(TOUR_KEYS.FEATURE_ONBOARDING)
- }
- }
- }
-
onMount(async () => {
if (!hasSynced && application) {
try {
@@ -107,8 +89,6 @@
})
-
-
{#if $builderStore.builderSidePanel}
{/if}
@@ -122,17 +102,15 @@
{#each $layout.children as { path, title }}
-
- topItemNavigate(path)}
- title={capitalise(title)}
- id={`builder-${title}-tab`}
- />
-
+ topItemNavigate(path)}
+ title={capitalise(title)}
+ id={`builder-${title}-tab`}
+ />
{/each}
diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte
index 020f86357b..0c2f25bd34 100644
--- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte
+++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsPanel.svelte
@@ -17,14 +17,6 @@
import { ActionButton, notifications } from "@budibase/bbui"
import { capitalise } from "@/helpers"
import { builderStore } from "@/stores/builder"
- import TourWrap from "@/components/portal/onboarding/TourWrap.svelte"
- import { TOUR_STEP_KEYS } from "@/components/portal/onboarding/tours.js"
-
- const {
- BUILDER_FORM_CREATE_STEPS,
- BUILDER_FORM_VIEW_UPDATE_STEPS,
- BUILDER_FORM_ROW_ID,
- } = TOUR_STEP_KEYS
const onUpdateName = async value => {
try {
@@ -111,21 +103,13 @@
{#if section === "settings"}
-
-
-
+
{/if}
{#if section === "styles"}
| null
- tourKey: string | null
- tourStepKey: string | null
hoveredComponentId: string | null
websocket?: Socket
}
@@ -27,10 +22,6 @@ export const INITIAL_BUILDER_STATE: BuilderState = {
highlightedSetting: null,
propertyFocus: null,
builderSidePanel: false,
- onboarding: false,
- tourNodes: null,
- tourKey: null,
- tourStepKey: null,
hoveredComponentId: null,
}
@@ -49,9 +40,6 @@ export class BuilderStore extends BudiStore {
this.showBuilderSidePanel = this.showBuilderSidePanel.bind(this)
this.setPreviousTopNavPath = this.setPreviousTopNavPath.bind(this)
this.selectResource = this.selectResource.bind(this)
- this.registerTourNode = this.registerTourNode.bind(this)
- this.destroyTourNode = this.destroyTourNode.bind(this)
- this.startBuilderOnboarding = this.startBuilderOnboarding.bind(this)
}
init(app: App) {
@@ -118,55 +106,6 @@ export class BuilderStore extends BudiStore {
resourceId: id,
})
}
-
- registerTourNode(tourStepKey: string, node: HTMLElement) {
- this.update(state => {
- const update = {
- ...state,
- tourNodes: {
- ...state.tourNodes,
- [tourStepKey]: node,
- },
- }
- return update
- })
- }
-
- destroyTourNode(tourStepKey: string) {
- const store = get(this.store)
- if (store.tourNodes?.[tourStepKey]) {
- const nodes = { ...store.tourNodes }
- delete nodes[tourStepKey]
- this.update(state => ({
- ...state,
- tourNodes: nodes,
- }))
- }
- }
-
- startBuilderOnboarding() {
- this.update(state => ({
- ...state,
- onboarding: true,
- tourKey: TOUR_KEYS.TOUR_BUILDER_ONBOARDING,
- }))
- }
-
- endBuilderOnboarding() {
- this.update(state => ({
- ...state,
- onboarding: false,
- }))
- }
-
- setTour(tourKey?: string | null) {
- this.update(state => ({
- ...state,
- tourStepKey: null,
- tourNodes: null,
- tourKey: tourKey || null,
- }))
- }
}
export const builderStore = new BuilderStore()
diff --git a/packages/builder/src/stores/builder/tests/builder.test.js b/packages/builder/src/stores/builder/tests/builder.test.js
index a1ff37df7d..06e95383ea 100644
--- a/packages/builder/src/stores/builder/tests/builder.test.js
+++ b/packages/builder/src/stores/builder/tests/builder.test.js
@@ -209,26 +209,4 @@ describe("Builder store", () => {
[dataRoute]: updatedURL,
})
})
-
- it("Register a builder tour node", ctx => {
- const fakeNode = { name: "node" }
- ctx.test.builderStore.registerTourNode("sampleKey", fakeNode)
-
- const registeredNodes = ctx.test.store.tourNodes
-
- expect(registeredNodes).not.toBeNull()
- expect(Object.keys(registeredNodes).length).toBe(1)
- expect(registeredNodes["sampleKey"]).toStrictEqual(fakeNode)
- })
-
- it("Clear a destroyed tour node", ctx => {
- const fakeNode = { name: "node" }
- ctx.test.builderStore.registerTourNode("sampleKey", fakeNode)
-
- expect(ctx.test.store.tourNodes).not.toBeNull()
- expect(Object.keys(ctx.test.store.tourNodes).length).toBe(1)
-
- ctx.test.builderStore.destroyTourNode("sampleKey")
- expect(ctx.test.store.tourNodes).toStrictEqual({})
- })
})
diff --git a/packages/server/package.json b/packages/server/package.json
index 2915e45069..253db3d5c2 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -135,7 +135,8 @@
"validate.js": "0.13.1",
"worker-farm": "1.7.0",
"xml2js": "0.6.2",
- "zod-validation-error": "^3.4.0"
+ "zod-validation-error": "^3.4.0",
+ "http-graceful-shutdown": "^3.1.12"
},
"devDependencies": {
"@babel/core": "^7.22.5",
diff --git a/packages/server/src/koa.ts b/packages/server/src/koa.ts
index acae433cc3..c9da38d459 100644
--- a/packages/server/src/koa.ts
+++ b/packages/server/src/koa.ts
@@ -7,8 +7,8 @@ import * as automations from "./automations"
import { Thread } from "./threads"
import * as redis from "./utilities/redis"
import { events, logging, middleware, timers } from "@budibase/backend-core"
-import destroyable from "server-destroy"
import { userAgent } from "koa-useragent"
+import gracefulShutdown from "http-graceful-shutdown"
export default function createKoaApp() {
const app = new Koa()
@@ -40,55 +40,49 @@ export default function createKoaApp() {
app.use(userAgent)
const server = http.createServer(app.callback())
- destroyable(server)
- let shuttingDown = false,
- errCode = 0
-
- server.on("close", async () => {
- // already in process
- if (shuttingDown) {
- return
- }
- shuttingDown = true
- console.log("Server Closed")
+ const shutdown = async () => {
+ console.log("Server shutting down gracefully...")
timers.cleanup()
await automations.shutdown()
await redis.shutdown()
events.shutdown()
await Thread.shutdown()
api.shutdown()
- if (!env.isTest()) {
- process.exit(errCode)
+ }
+
+ gracefulShutdown(server, {
+ signals: "SIGINT SIGTERM",
+ timeout: 30000, // in ms
+ onShutdown: shutdown,
+ forceExit: !env.isTest,
+ finally: () => {
+ console.log("Server shutdown complete")
+ },
+ })
+
+ process.on("uncaughtException", async err => {
+ // @ts-ignore
+ // don't worry about this error, comes from zlib isn't important
+ if (err?.["code"] === "ERR_INVALID_CHAR") {
+ logging.logAlert("Uncaught exception.", err)
+ return
+ }
+ await shutdown()
+ if (!env.isTest) {
+ process.exit(1)
+ }
+ })
+
+ process.on("unhandledRejection", async reason => {
+ logging.logAlert("Unhandled Promise Rejection", reason as Error)
+ await shutdown()
+ if (!env.isTest) {
+ process.exit(1)
}
})
const listener = server.listen(env.PORT || 0)
- const shutdown = () => {
- server.close()
- // @ts-ignore
- server.destroy()
- }
-
- process.on("uncaughtException", err => {
- // @ts-ignore
- // don't worry about this error, comes from zlib isn't important
- if (err && err["code"] === "ERR_INVALID_CHAR") {
- return
- }
- errCode = -1
- logging.logAlert("Uncaught exception.", err)
- shutdown()
- })
-
- process.on("SIGTERM", () => {
- shutdown()
- })
-
- process.on("SIGINT", () => {
- shutdown()
- })
-
return { app, server: listener }
}
diff --git a/packages/server/src/sdk/app/tables/getters.ts b/packages/server/src/sdk/app/tables/getters.ts
index f32150c63d..d472aaef22 100644
--- a/packages/server/src/sdk/app/tables/getters.ts
+++ b/packages/server/src/sdk/app/tables/getters.ts
@@ -17,177 +17,234 @@ import datasources from "../datasources"
import sdk from "../../../sdk"
import { ensureQueryUISet } from "../views/utils"
import { isV2 } from "../views"
+import { tracer } from "dd-trace"
export async function processTable(table: Table): Promise {
- if (!table) {
- return table
- }
+ return await tracer.trace("processTable", async span => {
+ if (!table) {
+ return table
+ }
- table = { ...table }
- if (table.views) {
- for (const [key, view] of Object.entries(table.views)) {
- if (!isV2(view)) {
- continue
+ span.addTags({ tableId: table._id })
+
+ table = { ...table }
+ if (table.views) {
+ span.addTags({ numViews: Object.keys(table.views).length })
+ for (const [key, view] of Object.entries(table.views)) {
+ if (!isV2(view)) {
+ continue
+ }
+ table.views[key] = ensureQueryUISet(view)
}
- table.views[key] = ensureQueryUISet(view)
}
- }
- if (table._id && isExternalTableID(table._id)) {
- // Old created external tables via Budibase might have a missing field name breaking some UI such as filters
- if (table.schema["id"] && !table.schema["id"].name) {
- table.schema["id"].name = "id"
+ if (table._id && isExternalTableID(table._id)) {
+ span.addTags({ isExternal: true })
+ // Old created external tables via Budibase might have a missing field name breaking some UI such as filters
+ if (table.schema["id"] && !table.schema["id"].name) {
+ table.schema["id"].name = "id"
+ }
+ return {
+ ...table,
+ type: "table",
+ sourceType: TableSourceType.EXTERNAL,
+ }
+ } else {
+ span.addTags({ isExternal: false })
+ const processed: Table = {
+ ...table,
+ type: "table",
+ primary: ["_id"], // internal tables must always use _id as primary key
+ sourceId: table.sourceId || INTERNAL_TABLE_SOURCE_ID,
+ sourceType: TableSourceType.INTERNAL,
+ sql: true,
+ }
+ return processed
}
- return {
- ...table,
- type: "table",
- sourceType: TableSourceType.EXTERNAL,
- }
- } else {
- const processed: Table = {
- ...table,
- type: "table",
- primary: ["_id"], // internal tables must always use _id as primary key
- sourceId: table.sourceId || INTERNAL_TABLE_SOURCE_ID,
- sourceType: TableSourceType.INTERNAL,
- sql: true,
- }
- return processed
- }
+ })
}
export async function processTables(tables: Table[]): Promise {
- return await Promise.all(tables.map(table => processTable(table)))
+ return await tracer.trace("processTables", async span => {
+ span.addTags({ numTables: tables.length })
+ return await Promise.all(tables.map(table => processTable(table)))
+ })
}
async function processEntities(tables: Record) {
- for (let key of Object.keys(tables)) {
- tables[key] = await processTable(tables[key])
- }
- return tables
+ return await tracer.trace("processEntities", async span => {
+ span.addTags({ numTables: Object.keys(tables).length })
+ for (let key of Object.keys(tables)) {
+ tables[key] = await processTable(tables[key])
+ }
+ return tables
+ })
}
export async function getAllInternalTables(db?: Database): Promise {
- if (!db) {
- db = context.getAppDB()
- }
- const internalTables = await db.allDocs(
- getTableParams(null, {
- include_docs: true,
- })
- )
- return await processTables(internalTables.rows.map(row => row.doc!))
+ return await tracer.trace("getAllInternalTables", async span => {
+ if (!db) {
+ db = context.getAppDB()
+ }
+ span.addTags({ db: db.name })
+ const internalTables = await db.allDocs(
+ getTableParams(null, {
+ include_docs: true,
+ })
+ )
+ span.addTags({ numTables: internalTables.rows.length })
+ return await processTables(internalTables.rows.map(row => row.doc!))
+ })
}
async function getAllExternalTables(): Promise {
- // this is all datasources, we'll need to filter out internal
- const datasources = await sdk.datasources.fetch({ enriched: true })
- const allEntities = datasources
- .filter(datasource => datasource._id !== INTERNAL_TABLE_SOURCE_ID)
- .map(datasource => datasource.entities)
- let final: Table[] = []
- for (let entities of allEntities) {
- if (entities) {
- final = final.concat(Object.values(entities))
+ return await tracer.trace("getAllExternalTables", async span => {
+ // this is all datasources, we'll need to filter out internal
+ const datasources = await sdk.datasources.fetch({ enriched: true })
+ span.addTags({ numDatasources: datasources.length })
+
+ const allEntities = datasources
+ .filter(datasource => datasource._id !== INTERNAL_TABLE_SOURCE_ID)
+ .map(datasource => datasource.entities)
+ span.addTags({ numEntities: allEntities.length })
+
+ let final: Table[] = []
+ for (let entities of allEntities) {
+ if (entities) {
+ final = final.concat(Object.values(entities))
+ }
}
- }
- return await processTables(final)
+ span.addTags({ numTables: final.length })
+ return await processTables(final)
+ })
}
export async function getExternalTable(
datasourceId: string,
tableName: string
): Promise {
- const entities = await getExternalTablesInDatasource(datasourceId)
- if (!entities[tableName]) {
- throw new Error(`Unable to find table named "${tableName}"`)
- }
- const table = await processTable(entities[tableName])
- if (!table.sourceId) {
- table.sourceId = datasourceId
- }
- return table
+ return await tracer.trace("getExternalTable", async span => {
+ span.addTags({ datasourceId, tableName })
+ const entities = await getExternalTablesInDatasource(datasourceId)
+ if (!entities[tableName]) {
+ throw new Error(`Unable to find table named "${tableName}"`)
+ }
+ const table = await processTable(entities[tableName])
+ if (!table.sourceId) {
+ table.sourceId = datasourceId
+ }
+ return table
+ })
}
export async function getTable(tableId: string): Promise {
- const db = context.getAppDB()
- let output: Table
- if (tableId && isExternalTableID(tableId)) {
- let { datasourceId, tableName } = breakExternalTableId(tableId)
- const datasource = await datasources.get(datasourceId)
- const table = await getExternalTable(datasourceId, tableName)
- output = { ...table, sql: isSQL(datasource) }
- } else {
- output = await db.get(tableId)
- }
- return await processTable(output)
+ return await tracer.trace("getTable", async span => {
+ const db = context.getAppDB()
+ span.addTags({ tableId, db: db.name })
+ let output: Table
+ if (tableId && isExternalTableID(tableId)) {
+ let { datasourceId, tableName } = breakExternalTableId(tableId)
+ span.addTags({ isExternal: true, datasourceId, tableName })
+ const datasource = await datasources.get(datasourceId)
+ const table = await getExternalTable(datasourceId, tableName)
+ output = { ...table, sql: isSQL(datasource) }
+ span.addTags({ isSQL: isSQL(datasource) })
+ } else {
+ output = await db.get(tableId)
+ }
+ return await processTable(output)
+ })
}
export async function doesTableExist(tableId: string): Promise {
- try {
- const table = await getTable(tableId)
- return !!table
- } catch (err) {
- return false
- }
+ return await tracer.trace("doesTableExist", async span => {
+ span.addTags({ tableId })
+ try {
+ const table = await getTable(tableId)
+ span.addTags({ tableExists: !!table })
+ return !!table
+ } catch (err) {
+ span.addTags({ tableExists: false })
+ return false
+ }
+ })
}
export async function getAllTables() {
- const [internal, external] = await Promise.all([
- getAllInternalTables(),
- getAllExternalTables(),
- ])
- return await processTables([...internal, ...external])
+ return await tracer.trace("getAllTables", async span => {
+ const [internal, external] = await Promise.all([
+ getAllInternalTables(),
+ getAllExternalTables(),
+ ])
+ span.addTags({
+ numInternalTables: internal.length,
+ numExternalTables: external.length,
+ })
+ return await processTables([...internal, ...external])
+ })
}
export async function getExternalTablesInDatasource(
datasourceId: string
): Promise> {
- const datasource = await datasources.get(datasourceId, { enriched: true })
- if (!datasource || !datasource.entities) {
- throw new Error("Datasource is not configured fully.")
- }
- return await processEntities(datasource.entities)
+ return await tracer.trace("getExternalTablesInDatasource", async span => {
+ const datasource = await datasources.get(datasourceId, { enriched: true })
+ if (!datasource || !datasource.entities) {
+ throw new Error("Datasource is not configured fully.")
+ }
+ span.addTags({
+ datasourceId,
+ numEntities: Object.keys(datasource.entities).length,
+ })
+ return await processEntities(datasource.entities)
+ })
}
export async function getTables(tableIds: string[]): Promise {
- const externalTableIds = tableIds.filter(tableId =>
- isExternalTableID(tableId)
- ),
- internalTableIds = tableIds.filter(tableId => !isExternalTableID(tableId))
- let tables: Table[] = []
- if (externalTableIds.length) {
- const externalTables = await getAllExternalTables()
- tables = tables.concat(
- externalTables.filter(
- table => externalTableIds.indexOf(table._id!) !== -1
+ return tracer.trace("getTables", async span => {
+ span.addTags({ numTableIds: tableIds.length })
+ const externalTableIds = tableIds.filter(tableId =>
+ isExternalTableID(tableId)
+ ),
+ internalTableIds = tableIds.filter(tableId => !isExternalTableID(tableId))
+ let tables: Table[] = []
+ if (externalTableIds.length) {
+ const externalTables = await getAllExternalTables()
+ tables = tables.concat(
+ externalTables.filter(
+ table => externalTableIds.indexOf(table._id!) !== -1
+ )
)
- )
- }
- if (internalTableIds.length) {
- const db = context.getAppDB()
- const internalTables = await db.getMultiple(internalTableIds, {
- allowMissing: true,
- })
- tables = tables.concat(internalTables)
- }
- return await processTables(tables)
+ }
+ if (internalTableIds.length) {
+ const db = context.getAppDB()
+ const internalTables = await db.getMultiple(internalTableIds, {
+ allowMissing: true,
+ })
+ tables = tables.concat(internalTables)
+ }
+ span.addTags({ numTables: tables.length })
+ return await processTables(tables)
+ })
}
export async function enrichViewSchemas(
table: Table
): Promise {
- const views = []
- for (const view of Object.values(table.views ?? [])) {
- if (sdk.views.isV2(view)) {
- views.push(await sdk.views.enrichSchema(view, table.schema))
- } else views.push(view)
- }
+ return await tracer.trace("enrichViewSchemas", async span => {
+ span.addTags({ tableId: table._id })
+ const views = []
+ for (const view of Object.values(table.views ?? [])) {
+ if (sdk.views.isV2(view)) {
+ views.push(await sdk.views.enrichSchema(view, table.schema))
+ } else views.push(view)
+ }
- return {
- ...table,
- views: views.reduce((p, v) => {
- p[v.name!] = v
- return p
- }, {} as TableViewsResponse),
- }
+ return {
+ ...table,
+ views: views.reduce((p, v) => {
+ p[v.name!] = v
+ return p
+ }, {} as TableViewsResponse),
+ }
+ })
}
diff --git a/packages/types/src/api/web/auth.ts b/packages/types/src/api/web/auth.ts
index 5ef6169086..ba61439786 100644
--- a/packages/types/src/api/web/auth.ts
+++ b/packages/types/src/api/web/auth.ts
@@ -38,10 +38,8 @@ export interface UpdateSelfRequest {
lastName?: string
password?: string
forceResetPassword?: boolean
- onboardedAt?: string
freeTrialConfirmedAt?: string
appFavourites?: string[]
- tours?: Record
appSort?: string
}
diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts
index 14fb395211..dde6c78e5c 100644
--- a/packages/types/src/documents/global/user.ts
+++ b/packages/types/src/documents/global/user.ts
@@ -64,15 +64,15 @@ export interface User extends Document {
status?: UserStatus
createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now()
userGroups?: string[]
- onboardedAt?: string
freeTrialConfirmedAt?: string
- tours?: Record
scimInfo?: { isSync: true } & Record
appFavourites?: string[]
ssoId?: string
appSort?: string
budibaseAccess?: boolean
accountPortalAccess?: boolean
+ onboardedAt?: string // deprecated and no longer saved
+ tours?: Record // deprecated and no longer saved
}
export interface UserBindings extends Document {
diff --git a/packages/types/src/sdk/events/user.ts b/packages/types/src/sdk/events/user.ts
index eb3ebe97a5..ab72b9888b 100644
--- a/packages/types/src/sdk/events/user.ts
+++ b/packages/types/src/sdk/events/user.ts
@@ -24,14 +24,6 @@ export interface UserDeletedEvent extends BaseEvent {
}
}
-export interface UserOnboardingEvent extends BaseEvent {
- userId: string
- step?: string
- audited: {
- email: string
- }
-}
-
export interface UserPermissionAssignedEvent extends BaseEvent {
userId: string
audited: {
diff --git a/packages/worker/package.json b/packages/worker/package.json
index 135e0528ac..4ed98fd532 100644
--- a/packages/worker/package.json
+++ b/packages/worker/package.json
@@ -75,7 +75,8 @@
"pouchdb": "7.3.0",
"pouchdb-all-dbs": "1.1.1",
"server-destroy": "1.0.1",
- "uuid": "^8.3.2"
+ "uuid": "^8.3.2",
+ "http-graceful-shutdown": "^3.1.12"
},
"devDependencies": {
"@jest/types": "^29.6.3",
diff --git a/packages/worker/src/api/routes/global/tests/self.spec.ts b/packages/worker/src/api/routes/global/tests/self.spec.ts
index bf34f14aa0..be3f816870 100644
--- a/packages/worker/src/api/routes/global/tests/self.spec.ts
+++ b/packages/worker/src/api/routes/global/tests/self.spec.ts
@@ -47,13 +47,12 @@ describe("/api/global/self", () => {
})
})
- it("should update onboarding", async () => {
+ it("should update free trial confirmation date", async () => {
const user = await config.createUser()
await config.createSession(user)
const res = await config.api.self
.updateSelf(user, {
- onboardedAt: "2023-03-07T14:10:54.869Z",
freeTrialConfirmedAt: "2024-03-17T14:10:54.869Z",
})
.expect(200)
@@ -61,7 +60,6 @@ describe("/api/global/self", () => {
const dbUser = (await config.getUser(user.email))!
user._rev = dbUser._rev
- expect(dbUser.onboardedAt).toBe("2023-03-07T14:10:54.869Z")
expect(dbUser.freeTrialConfirmedAt).toBe("2024-03-17T14:10:54.869Z")
expect(res.body._id).toBe(user._id)
})
diff --git a/packages/worker/src/api/routes/validation/users.ts b/packages/worker/src/api/routes/validation/users.ts
index 46c66285fd..c509f7ba0b 100644
--- a/packages/worker/src/api/routes/validation/users.ts
+++ b/packages/worker/src/api/routes/validation/users.ts
@@ -25,10 +25,8 @@ export const buildSelfSaveValidation = () => {
forceResetPassword: Joi.boolean().optional(),
firstName: OPTIONAL_STRING,
lastName: OPTIONAL_STRING,
- onboardedAt: Joi.string().optional(),
freeTrialConfirmedAt: Joi.string().optional(),
appFavourites: Joi.array().optional(),
- tours: Joi.object().optional(),
appSort: Joi.string().optional(),
}
return auth.joiValidator.body(Joi.object(schema).required().unknown(false))
diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts
index bfb022f213..767de5c7b2 100644
--- a/packages/worker/src/index.ts
+++ b/packages/worker/src/index.ts
@@ -26,12 +26,12 @@ db.init()
import koaBody from "koa-body"
import http from "http"
import api from "./api"
+import gracefulShutdown from "http-graceful-shutdown"
const koaSession = require("koa-session")
import { userAgent } from "koa-useragent"
-import destroyable from "server-destroy"
import { initPro } from "./initPro"
import { handleScimBody } from "./middleware/handleScimBody"
@@ -86,29 +86,40 @@ app.use(auth.passport.session())
app.use(api.routes())
const server = http.createServer(app.callback())
-destroyable(server)
-let shuttingDown = false,
- errCode = 0
-server.on("close", async () => {
- if (shuttingDown) {
- return
- }
- shuttingDown = true
- console.log("Server Closed")
+const shutdown = async () => {
+ console.log("Worker service shutting down gracefully...")
timers.cleanup()
events.shutdown()
await redis.clients.shutdown()
await queue.shutdown()
- if (!env.isTest()) {
- process.exit(errCode)
+}
+
+gracefulShutdown(server, {
+ signals: "SIGINT SIGTERM",
+ timeout: 30000,
+ onShutdown: shutdown,
+ forceExit: !env.isTest,
+ finally: () => {
+ console.log("Worker service shutdown complete")
+ },
+})
+
+process.on("uncaughtException", async err => {
+ logging.logAlert("Uncaught exception.", err)
+ await shutdown()
+ if (!env.isTest) {
+ process.exit(1)
}
})
-const shutdown = () => {
- server.close()
- server.destroy()
-}
+process.on("unhandledRejection", async reason => {
+ logging.logAlert("Unhandled Promise Rejection", reason as Error)
+ await shutdown()
+ if (!env.isTest) {
+ process.exit(1)
+ }
+})
export default server.listen(parseInt(env.PORT || "4002"), async () => {
let startupLog = `Worker running on ${JSON.stringify(server.address())}`
@@ -125,17 +136,3 @@ export default server.listen(parseInt(env.PORT || "4002"), async () => {
// can't integrate directly into backend-core due to cyclic issues
await events.processors.init(proSdk.auditLogs.write)
})
-
-process.on("uncaughtException", err => {
- errCode = -1
- logging.logAlert("Uncaught exception.", err)
- shutdown()
-})
-
-process.on("SIGTERM", () => {
- shutdown()
-})
-
-process.on("SIGINT", () => {
- shutdown()
-})
diff --git a/yarn.lock b/yarn.lock
index e9fe907277..a26d093ae7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13161,6 +13161,13 @@ http-errors@~1.6.2:
setprototypeof "1.1.0"
statuses ">= 1.4.0 < 2"
+http-graceful-shutdown@^3.1.12:
+ version "3.1.14"
+ resolved "https://registry.yarnpkg.com/http-graceful-shutdown/-/http-graceful-shutdown-3.1.14.tgz#a4d48ac5d985da18b4d35c050acd3ef10f02113d"
+ integrity sha512-aTbGAZDUtRt7gRmU+li7rt5WbJeemULZHLNrycJ1dRBU80Giut6NvzG8h5u1TW1zGHXkPGpEtoEKhPKogIRKdA==
+ dependencies:
+ debug "^4.3.4"
+
http-proxy-agent@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a"