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: - -
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: - -
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"