Merge branch 'master' into ts/type-create-screen-modal

This commit is contained in:
Adria Navarro 2025-05-08 11:04:12 +02:00 committed by GitHub
commit ff1ac05d3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 309 additions and 1005 deletions

View File

@ -30,6 +30,7 @@ spec:
{{- toYaml .Values.services.apps.templateLabels | indent 8 -}} {{- toYaml .Values.services.apps.templateLabels | indent 8 -}}
{{ end }} {{ end }}
spec: spec:
terminationGracePeriodSeconds: 60
containers: containers:
- env: - env:
- name: BUDIBASE_ENVIRONMENT - name: BUDIBASE_ENVIRONMENT

View File

@ -30,6 +30,7 @@ spec:
{{- toYaml .Values.services.worker.templateLabels | indent 8 -}} {{- toYaml .Values.services.worker.templateLabels | indent 8 -}}
{{ end }} {{ end }}
spec: spec:
terminationGracePeriodSeconds: 60
containers: containers:
- env: - env:
- name: BUDIBASE_ENVIRONMENT - name: BUDIBASE_ENVIRONMENT

View File

@ -1,6 +1,6 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "3.10.0", "version": "3.10.2",
"npmClient": "yarn", "npmClient": "yarn",
"concurrency": 20, "concurrency": 20,
"command": { "command": {

View File

@ -14,7 +14,6 @@ import {
UserPermissionAssignedEvent, UserPermissionAssignedEvent,
UserPermissionRemovedEvent, UserPermissionRemovedEvent,
UserUpdatedEvent, UserUpdatedEvent,
UserOnboardingEvent,
} from "@budibase/types" } from "@budibase/types"
import { isScim } from "../../context" import { isScim } from "../../context"
@ -51,16 +50,6 @@ async function deleted(user: User) {
await publishEvent(Event.USER_DELETED, properties) 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 // PERMISSIONS
async function permissionAdminAssigned(user: User, timestamp?: number) { async function permissionAdminAssigned(user: User, timestamp?: number) {
@ -191,7 +180,6 @@ export default {
permissionAdminRemoved, permissionAdminRemoved,
permissionBuilderAssigned, permissionBuilderAssigned,
permissionBuilderRemoved, permissionBuilderRemoved,
onboardingComplete,
invited, invited,
inviteAccepted, inviteAccepted,
passwordForceReset, passwordForceReset,

View File

@ -76,10 +76,6 @@ export const handleSaveEvents = async (
await events.user.permissionAdminRemoved(user) await events.user.permissionAdminRemoved(user)
} }
if (isOnboardingComplete(user, existingUser)) {
await events.user.onboardingComplete(user)
}
if ( if (
!existingUser.forceResetPassword && !existingUser.forceResetPassword &&
user.forceResetPassword && user.forceResetPassword &&
@ -122,10 +118,6 @@ const isRemovingAdmin = (user: any, existingUser: any) => {
return isRemovingPermission(user, existingUser, hasAdminPermissions) 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. * Check if a permission is being added to a new or existing user.
*/ */

View File

@ -1,15 +1,13 @@
<script> <script lang="ts">
import { Icon, Modal } from "@budibase/bbui"
import ChooseIconModal from "@/components/start/ChooseIconModal.svelte" import ChooseIconModal from "@/components/start/ChooseIconModal.svelte"
import { Icon, Modal } from "@budibase/bbui"
export let name export let name: string
export let size = "M" export let size: "M" = "M"
export let app export let color: string
export let color export let disabled: boolean = false
export let autoSave = false
export let disabled = false
let modal let modal: Modal
</script> </script>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
@ -28,7 +26,7 @@
</div> </div>
<Modal bind:this={modal}> <Modal bind:this={modal}>
<ChooseIconModal {name} {color} {app} {autoSave} on:change /> <ChooseIconModal {name} {color} on:change />
</Modal> </Modal>
<style> <style>

View File

@ -27,8 +27,6 @@
sortedScreens, sortedScreens,
appPublished, appPublished,
} from "@/stores/builder" } from "@/stores/builder"
import TourWrap from "@/components/portal/onboarding/TourWrap.svelte"
import { TOUR_STEP_KEYS } from "@/components/portal/onboarding/tours.js"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
export let application export let application
@ -153,12 +151,6 @@
</div> </div>
</div> </div>
{/if} {/if}
<TourWrap
stepKeys={[
TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT,
TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
]}
>
<div class="app-action-button users"> <div class="app-action-button users">
<div class="app-action" id="builder-app-users-button"> <div class="app-action" id="builder-app-users-button">
<ActionButton <ActionButton
@ -172,7 +164,6 @@
</ActionButton> </ActionButton>
</div> </div>
</div> </div>
</TourWrap>
<div class="app-action-button preview"> <div class="app-action-button preview">
<div class="app-action"> <div class="app-action">
@ -201,7 +192,6 @@
<div bind:this={appActionPopoverAnchor}> <div bind:this={appActionPopoverAnchor}>
<div class="app-action"> <div class="app-action">
<Icon name={$appPublished ? "GlobeCheck" : "GlobeStrike"} /> <Icon name={$appPublished ? "GlobeCheck" : "GlobeStrike"} />
<TourWrap stepKeys={[TOUR_STEP_KEYS.BUILDER_APP_PUBLISH]}>
<span class="publish-open" id="builder-app-publish-button"> <span class="publish-open" id="builder-app-publish-button">
Publish Publish
<Icon <Icon
@ -209,7 +199,6 @@
size="M" size="M"
/> />
</span> </span>
</TourWrap>
</div> </div>
</div> </div>
<Popover <Popover

View File

@ -1,167 +0,0 @@
<script>
import { Popover, Layout, Heading, Body, Button, Link } from "@budibase/bbui"
import { TOURS, getCurrentStepIdx } from "./tours.js"
import { goto, layout, isActive } from "@roxi/routify"
import { builderStore } from "@/stores/builder"
let popoverAnchor
let popover
let tourSteps = null
let tourStep
let tourStepIdx
let lastStep
let skipping = false
$: tourNodes = { ...$builderStore.tourNodes }
$: tourKey = $builderStore.tourKey
$: tourStepKey = $builderStore.tourStepKey
$: tour = TOURS[tourKey]
$: tourOnSkip = tour?.onSkip
const updateTourStep = (targetStepKey, tourKey) => {
if (!tourKey) {
tourSteps = null
tourStepIdx = null
lastStep = null
tourStep = null
popoverAnchor = null
popover = null
skipping = false
return
}
if (!tourSteps?.length) {
tourSteps = [...tour.steps]
}
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
lastStep = tourStepIdx + 1 == tourSteps.length
tourStep = { ...tourSteps[tourStepIdx] }
tourStep.onLoad()
}
$: updateTourStep(tourStepKey, tourKey)
const showPopover = (tourStep, tourNodes, popover) => {
if (!tourStep) {
return
}
popoverAnchor = tourNodes[tourStep.id]
popover?.show()
}
$: showPopover(tourStep, tourNodes, popover)
const navigateStep = step => {
if (step.route) {
const activeNav = $layout.children.find(c => $isActive(c.path))
if (activeNav) {
builderStore.setPreviousTopNavPath(
activeNav.path,
window.location.pathname
)
$goto($builderStore.previousTopNavPath[step.route] || step.route)
}
}
}
const nextStep = async () => {
if (!lastStep === true) {
let target = tourSteps[tourStepIdx + 1]
if (target) {
builderStore.update(state => ({
...state,
tourStepKey: target.id,
}))
navigateStep(target)
} else {
console.warn("Could not retrieve step")
}
} else {
if (typeof tourStep.onComplete === "function") {
tourStep.onComplete()
}
popover.hide()
if (tour.endRoute) {
$goto(tour.endRoute)
}
}
}
</script>
{#if tourKey}
{#key tourStepKey}
<Popover
align={tourStep?.align}
bind:this={popover}
anchor={popoverAnchor}
maxWidth={300}
dismissible={false}
offset={12}
handlePositionUpdate={tourStep?.positionHandler}
customZIndex={3}
>
<div class="tour-content">
<Layout noPadding gap="M">
<div class="tour-header">
<Heading size="XS">{tourStep?.title || "-"}</Heading>
{#if tourSteps?.length > 1}
<div>{`${tourStepIdx + 1}/${tourSteps?.length}`}</div>
{/if}
</div>
<Body size="S">
<span class="tour-body">
{#if tourStep.layout}
<svelte:component this={tourStep.layout} />
{:else}
{tourStep?.body || ""}
{/if}
</span>
</Body>
<div class="tour-footer">
<div class="tour-navigation">
{#if typeof tourOnSkip === "function" && !lastStep}
<Link
secondary
quiet
on:click={() => {
skipping = true
tourOnSkip()
if (tour.endRoute) {
$goto(tour.endRoute)
}
}}
disabled={skipping}
>
Skip
</Link>
{/if}
<Button cta on:click={nextStep} disabled={skipping}>
<div>{lastStep ? "Finish" : "Next"}</div>
</Button>
</div>
</div>
</Layout>
</div>
</Popover>
{/key}
{/if}
<style>
.tour-content {
padding: var(--spacing-xl);
}
.tour-navigation {
grid-gap: var(--spacing-xl);
display: flex;
justify-content: end;
align-items: center;
}
.tour-body :global(.feature-list) {
margin-bottom: 0px;
padding-left: var(--spacing-xl);
}
.tour-header {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>

View File

@ -1,64 +0,0 @@
<script>
import { tourHandler } from "./tourHandler"
import { TOURSBYSTEP, TOURS, getCurrentStepIdx } from "./tours"
import { onMount, onDestroy } from "svelte"
import { builderStore } from "@/stores/builder"
export let stepKeys = []
let ready = false
let registered = {}
const registerTourNode = (tourKey, stepKey) => {
const step = TOURSBYSTEP[stepKey]
if (ready && step && !registered[stepKey] && step?.tour === tourKey) {
const elem = document.querySelector(step.query)
registered[stepKey] = tourHandler(elem, stepKey)
}
}
const scrollToStep = () => {
let tourStepIdx = getCurrentStepIdx(
TOURS[tourKeyWatch]?.steps,
tourStepKeyWatch
)
let currentStep = TOURS[tourKeyWatch]?.steps?.[tourStepIdx]
if (currentStep?.scrollIntoView) {
let currentNode = $builderStore.tourNodes?.[currentStep.id]
if (currentNode) {
currentNode.scrollIntoView({ behavior: "smooth", block: "center" })
}
}
}
$: tourKeyWatch = $builderStore.tourKey
$: tourStepKeyWatch = $builderStore.tourStepKey
$: if (tourKeyWatch || stepKeys || ready) {
stepKeys.forEach(tourStepKey => {
registerTourNode(tourKeyWatch, tourStepKey)
})
}
$: scrollToStep(tourKeyWatch, tourStepKeyWatch)
onMount(() => {
ready = true
})
onDestroy(() => {
Object.entries(registered).forEach(entry => {
const handler = entry[1]
const stepKey = entry[0]
// Run step destroy, de-register nodes in the builderStore and local cache
handler.destroy()
delete registered[stepKey]
// Check if the step is part of an active tour. End the tour if that is the case
const step = TOURSBYSTEP[stepKey]
if (step.tour === tourKeyWatch) {
builderStore.setTour()
}
})
})
</script>
<slot />

View File

@ -1,9 +0,0 @@
<div>
When faced with a sizable form, consider implementing a multi-step approach to
enhance user experience.
<p>
Breaking the form into multiple steps can significantly improve usability by
making the process more digestible for your users.
</p>
</div>

View File

@ -1,17 +0,0 @@
<div>
You can use bindings to set the Row ID on your form.
<p>
This will allow you to pull the correct information into your form and allow
you to update!
</p>
<a href="https://docs.budibase.com/docs/form-block" target="_blank">
How to pass a row ID using bindings
</a>
</div>
<style>
a {
color: inherit;
text-decoration: underline;
}
</style>

View File

@ -1,10 +0,0 @@
<div>
In this section you can manage the data for your app:
<ul class="feature-list">
<li>Connect data sources</li>
<li>Edit data</li>
<li>Manage read & write access</li>
<li>Create views</li>
<li>Add bindings</li>
</ul>
</div>

View File

@ -1,10 +0,0 @@
<div>
After setting up your data, Design is where you build the screens for your
app:
<ul class="feature-list">
<li>Add screens</li>
<li>Add components</li>
<li>Choose your theme</li>
<li>Edit navigation</li>
</ul>
</div>

View File

@ -1,7 +0,0 @@
<div>
Once youre happy with your app you can publish it to production!
<p>
After publishing, any changes you make will not take affect until you next
publish.
</p>
</div>

View File

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

View File

@ -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)
},
}
}

View File

@ -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
}, {})

View File

@ -1,18 +1,9 @@
<script> <script lang="ts">
import { import { ColorPicker, Icon, Label, ModalContent } from "@budibase/bbui"
ModalContent,
Icon,
ColorPicker,
Label,
notifications,
} from "@budibase/bbui"
import { appsStore } from "@/stores/portal"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
export let app export let name: string
export let name export let color: string
export let color
export let autoSave = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -44,18 +35,9 @@
] ]
const save = async () => { const save = async () => {
if (!autoSave) {
dispatch("change", { color, name }) dispatch("change", { color, name })
return return
} }
try {
await appsStore.save(app.instance._id, {
icon: { name, color },
})
} catch (error) {
notifications.error("Error updating app")
}
}
</script> </script>
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->

View File

@ -8,7 +8,7 @@
userStore, userStore,
deploymentStore, deploymentStore,
} from "@/stores/builder" } from "@/stores/builder"
import { auth, appsStore } from "@/stores/portal" import { appsStore } from "@/stores/portal"
import { import {
Icon, Icon,
Tabs, Tabs,
@ -22,11 +22,8 @@
import { isActive, url, goto, layout, redirect } from "@roxi/routify" import { isActive, url, goto, layout, redirect } from "@roxi/routify"
import { capitalise } from "@/helpers" import { capitalise } from "@/helpers"
import { onMount, onDestroy } from "svelte" 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 BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
import { UserAvatars } from "@budibase/frontend-core" import { UserAvatars } from "@budibase/frontend-core"
import { TOUR_KEYS } from "@/components/portal/onboarding/tours.js"
import PreviewOverlay from "./_components/PreviewOverlay.svelte" import PreviewOverlay from "./_components/PreviewOverlay.svelte"
import EnterpriseBasicTrialModal from "@/components/portal/onboarding/EnterpriseBasicTrialModal.svelte" import EnterpriseBasicTrialModal from "@/components/portal/onboarding/EnterpriseBasicTrialModal.svelte"
import UpdateAppTopNav from "@/components/common/UpdateAppTopNav.svelte" import UpdateAppTopNav from "@/components/common/UpdateAppTopNav.svelte"
@ -37,7 +34,6 @@
let hasSynced = false let hasSynced = false
let loaded = false let loaded = false
$: loaded && initTour()
$: selected = capitalise( $: selected = capitalise(
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data" $layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
) )
@ -75,20 +71,6 @@
$goto($builderStore.previousTopNavPath[path] || path) $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 () => { onMount(async () => {
if (!hasSynced && application) { if (!hasSynced && application) {
try { try {
@ -107,8 +89,6 @@
}) })
</script> </script>
<TourPopover />
{#if $builderStore.builderSidePanel} {#if $builderStore.builderSidePanel}
<BuilderSidePanel /> <BuilderSidePanel />
{/if} {/if}
@ -122,7 +102,6 @@
</a> </a>
<Tabs {selected} size="M"> <Tabs {selected} size="M">
{#each $layout.children as { path, title }} {#each $layout.children as { path, title }}
<TourWrap stepKeys={[`builder-${title}-section`]}>
<Tab <Tab
link link
href={$url(path)} href={$url(path)}
@ -132,7 +111,6 @@
title={capitalise(title)} title={capitalise(title)}
id={`builder-${title}-tab`} id={`builder-${title}-tab`}
/> />
</TourWrap>
{/each} {/each}
</Tabs> </Tabs>
</div> </div>

View File

@ -17,14 +17,6 @@
import { ActionButton, notifications } from "@budibase/bbui" import { ActionButton, notifications } from "@budibase/bbui"
import { capitalise } from "@/helpers" import { capitalise } from "@/helpers"
import { builderStore } from "@/stores/builder" 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 => { const onUpdateName = async value => {
try { try {
@ -111,13 +103,6 @@
</div> </div>
</span> </span>
{#if section === "settings"} {#if section === "settings"}
<TourWrap
stepKeys={[
BUILDER_FORM_CREATE_STEPS,
BUILDER_FORM_VIEW_UPDATE_STEPS,
BUILDER_FORM_ROW_ID,
]}
>
<ComponentSettingsSection <ComponentSettingsSection
{componentInstance} {componentInstance}
{componentDefinition} {componentDefinition}
@ -125,7 +110,6 @@
{componentBindings} {componentBindings}
{isScreen} {isScreen}
/> />
</TourWrap>
{/if} {/if}
{#if section === "styles"} {#if section === "styles"}
<DesignSection <DesignSection

View File

@ -9,13 +9,10 @@
screenStore, screenStore,
navigationStore, navigationStore,
permissions as permissionsStore, permissions as permissionsStore,
builderStore,
datasources, datasources,
appStore, appStore,
} from "@/stores/builder" } from "@/stores/builder"
import { auth } from "@/stores/portal"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { TOUR_KEYS } from "@/components/portal/onboarding/tours.js"
import * as screenTemplating from "@/templates/screenTemplating" import * as screenTemplating from "@/templates/screenTemplating"
import { Roles } from "@/constants/backend" import { Roles } from "@/constants/backend"
import { AutoScreenTypes } from "@/constants" import { AutoScreenTypes } from "@/constants"
@ -159,19 +156,6 @@
) )
).flat() ).flat()
const newScreens = await createScreens(screenTemplates) const newScreens = await createScreens(screenTemplates)
if (type === "update" || type === "create") {
const associatedTour =
type === "update"
? TOUR_KEYS.BUILDER_FORM_VIEW_UPDATE
: TOUR_KEYS.BUILDER_FORM_CREATE
const tourRequired = !$auth?.user?.tours?.[associatedTour]
if (tourRequired) {
builderStore.setTour(associatedTour)
}
}
loadNewScreen(newScreens[0]) loadNewScreen(newScreens[0])
} }

View File

@ -143,7 +143,7 @@
return Constants.Roles.CREATOR return Constants.Roles.CREATOR
} }
if (user?.roles[prodAppId]) { if (user?.roles?.[prodAppId]) {
return user.roles[prodAppId] return user.roles[prodAppId]
} }

View File

@ -3,7 +3,6 @@ import { createBuilderWebsocket } from "./websocket.js"
import { Socket } from "socket.io-client" import { Socket } from "socket.io-client"
import { BuilderSocketEvent } from "@budibase/shared-core" import { BuilderSocketEvent } from "@budibase/shared-core"
import { BudiStore } from "../BudiStore.js" import { BudiStore } from "../BudiStore.js"
import { TOUR_KEYS } from "@/components/portal/onboarding/tours.js"
import { App } from "@budibase/types" import { App } from "@budibase/types"
interface BuilderState { interface BuilderState {
@ -14,10 +13,6 @@ interface BuilderState {
} | null } | null
propertyFocus: string | null propertyFocus: string | null
builderSidePanel: boolean builderSidePanel: boolean
onboarding: boolean
tourNodes: Record<string, HTMLElement> | null
tourKey: string | null
tourStepKey: string | null
hoveredComponentId: string | null hoveredComponentId: string | null
websocket?: Socket websocket?: Socket
} }
@ -27,10 +22,6 @@ export const INITIAL_BUILDER_STATE: BuilderState = {
highlightedSetting: null, highlightedSetting: null,
propertyFocus: null, propertyFocus: null,
builderSidePanel: false, builderSidePanel: false,
onboarding: false,
tourNodes: null,
tourKey: null,
tourStepKey: null,
hoveredComponentId: null, hoveredComponentId: null,
} }
@ -49,9 +40,6 @@ export class BuilderStore extends BudiStore<BuilderState> {
this.showBuilderSidePanel = this.showBuilderSidePanel.bind(this) this.showBuilderSidePanel = this.showBuilderSidePanel.bind(this)
this.setPreviousTopNavPath = this.setPreviousTopNavPath.bind(this) this.setPreviousTopNavPath = this.setPreviousTopNavPath.bind(this)
this.selectResource = this.selectResource.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) { init(app: App) {
@ -118,55 +106,6 @@ export class BuilderStore extends BudiStore<BuilderState> {
resourceId: id, 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() export const builderStore = new BuilderStore()

View File

@ -209,26 +209,4 @@ describe("Builder store", () => {
[dataRoute]: updatedURL, [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({})
})
}) })

View File

@ -135,7 +135,8 @@
"validate.js": "0.13.1", "validate.js": "0.13.1",
"worker-farm": "1.7.0", "worker-farm": "1.7.0",
"xml2js": "0.6.2", "xml2js": "0.6.2",
"zod-validation-error": "^3.4.0" "zod-validation-error": "^3.4.0",
"http-graceful-shutdown": "^3.1.12"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.22.5", "@babel/core": "^7.22.5",

View File

@ -7,8 +7,8 @@ import * as automations from "./automations"
import { Thread } from "./threads" import { Thread } from "./threads"
import * as redis from "./utilities/redis" import * as redis from "./utilities/redis"
import { events, logging, middleware, timers } from "@budibase/backend-core" import { events, logging, middleware, timers } from "@budibase/backend-core"
import destroyable from "server-destroy"
import { userAgent } from "koa-useragent" import { userAgent } from "koa-useragent"
import gracefulShutdown from "http-graceful-shutdown"
export default function createKoaApp() { export default function createKoaApp() {
const app = new Koa() const app = new Koa()
@ -40,55 +40,49 @@ export default function createKoaApp() {
app.use(userAgent) app.use(userAgent)
const server = http.createServer(app.callback()) const server = http.createServer(app.callback())
destroyable(server)
let shuttingDown = false, const shutdown = async () => {
errCode = 0 console.log("Server shutting down gracefully...")
server.on("close", async () => {
// already in process
if (shuttingDown) {
return
}
shuttingDown = true
console.log("Server Closed")
timers.cleanup() timers.cleanup()
await automations.shutdown() await automations.shutdown()
await redis.shutdown() await redis.shutdown()
events.shutdown() events.shutdown()
await Thread.shutdown() await Thread.shutdown()
api.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 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 } return { app, server: listener }
} }

View File

@ -17,14 +17,19 @@ import datasources from "../datasources"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { ensureQueryUISet } from "../views/utils" import { ensureQueryUISet } from "../views/utils"
import { isV2 } from "../views" import { isV2 } from "../views"
import { tracer } from "dd-trace"
export async function processTable(table: Table): Promise<Table> { export async function processTable(table: Table): Promise<Table> {
return await tracer.trace("processTable", async span => {
if (!table) { if (!table) {
return table return table
} }
span.addTags({ tableId: table._id })
table = { ...table } table = { ...table }
if (table.views) { if (table.views) {
span.addTags({ numViews: Object.keys(table.views).length })
for (const [key, view] of Object.entries(table.views)) { for (const [key, view] of Object.entries(table.views)) {
if (!isV2(view)) { if (!isV2(view)) {
continue continue
@ -33,6 +38,7 @@ export async function processTable(table: Table): Promise<Table> {
} }
} }
if (table._id && isExternalTableID(table._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 // 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) { if (table.schema["id"] && !table.schema["id"].name) {
table.schema["id"].name = "id" table.schema["id"].name = "id"
@ -43,6 +49,7 @@ export async function processTable(table: Table): Promise<Table> {
sourceType: TableSourceType.EXTERNAL, sourceType: TableSourceType.EXTERNAL,
} }
} else { } else {
span.addTags({ isExternal: false })
const processed: Table = { const processed: Table = {
...table, ...table,
type: "table", type: "table",
@ -53,50 +60,70 @@ export async function processTable(table: Table): Promise<Table> {
} }
return processed return processed
} }
})
} }
export async function processTables(tables: Table[]): Promise<Table[]> { export async function processTables(tables: Table[]): Promise<Table[]> {
return await tracer.trace("processTables", async span => {
span.addTags({ numTables: tables.length })
return await Promise.all(tables.map(table => processTable(table))) return await Promise.all(tables.map(table => processTable(table)))
})
} }
async function processEntities(tables: Record<string, Table>) { async function processEntities(tables: Record<string, Table>) {
return await tracer.trace("processEntities", async span => {
span.addTags({ numTables: Object.keys(tables).length })
for (let key of Object.keys(tables)) { for (let key of Object.keys(tables)) {
tables[key] = await processTable(tables[key]) tables[key] = await processTable(tables[key])
} }
return tables return tables
})
} }
export async function getAllInternalTables(db?: Database): Promise<Table[]> { export async function getAllInternalTables(db?: Database): Promise<Table[]> {
return await tracer.trace("getAllInternalTables", async span => {
if (!db) { if (!db) {
db = context.getAppDB() db = context.getAppDB()
} }
span.addTags({ db: db.name })
const internalTables = await db.allDocs<Table>( const internalTables = await db.allDocs<Table>(
getTableParams(null, { getTableParams(null, {
include_docs: true, include_docs: true,
}) })
) )
span.addTags({ numTables: internalTables.rows.length })
return await processTables(internalTables.rows.map(row => row.doc!)) return await processTables(internalTables.rows.map(row => row.doc!))
})
} }
async function getAllExternalTables(): Promise<Table[]> { async function getAllExternalTables(): Promise<Table[]> {
return await tracer.trace("getAllExternalTables", async span => {
// this is all datasources, we'll need to filter out internal // this is all datasources, we'll need to filter out internal
const datasources = await sdk.datasources.fetch({ enriched: true }) const datasources = await sdk.datasources.fetch({ enriched: true })
span.addTags({ numDatasources: datasources.length })
const allEntities = datasources const allEntities = datasources
.filter(datasource => datasource._id !== INTERNAL_TABLE_SOURCE_ID) .filter(datasource => datasource._id !== INTERNAL_TABLE_SOURCE_ID)
.map(datasource => datasource.entities) .map(datasource => datasource.entities)
span.addTags({ numEntities: allEntities.length })
let final: Table[] = [] let final: Table[] = []
for (let entities of allEntities) { for (let entities of allEntities) {
if (entities) { if (entities) {
final = final.concat(Object.values(entities)) final = final.concat(Object.values(entities))
} }
} }
span.addTags({ numTables: final.length })
return await processTables(final) return await processTables(final)
})
} }
export async function getExternalTable( export async function getExternalTable(
datasourceId: string, datasourceId: string,
tableName: string tableName: string
): Promise<Table> { ): Promise<Table> {
return await tracer.trace("getExternalTable", async span => {
span.addTags({ datasourceId, tableName })
const entities = await getExternalTablesInDatasource(datasourceId) const entities = await getExternalTablesInDatasource(datasourceId)
if (!entities[tableName]) { if (!entities[tableName]) {
throw new Error(`Unable to find table named "${tableName}"`) throw new Error(`Unable to find table named "${tableName}"`)
@ -106,50 +133,75 @@ export async function getExternalTable(
table.sourceId = datasourceId table.sourceId = datasourceId
} }
return table return table
})
} }
export async function getTable(tableId: string): Promise<Table> { export async function getTable(tableId: string): Promise<Table> {
return await tracer.trace("getTable", async span => {
const db = context.getAppDB() const db = context.getAppDB()
span.addTags({ tableId, db: db.name })
let output: Table let output: Table
if (tableId && isExternalTableID(tableId)) { if (tableId && isExternalTableID(tableId)) {
let { datasourceId, tableName } = breakExternalTableId(tableId) let { datasourceId, tableName } = breakExternalTableId(tableId)
span.addTags({ isExternal: true, datasourceId, tableName })
const datasource = await datasources.get(datasourceId) const datasource = await datasources.get(datasourceId)
const table = await getExternalTable(datasourceId, tableName) const table = await getExternalTable(datasourceId, tableName)
output = { ...table, sql: isSQL(datasource) } output = { ...table, sql: isSQL(datasource) }
span.addTags({ isSQL: isSQL(datasource) })
} else { } else {
output = await db.get<Table>(tableId) output = await db.get<Table>(tableId)
} }
return await processTable(output) return await processTable(output)
})
} }
export async function doesTableExist(tableId: string): Promise<boolean> { export async function doesTableExist(tableId: string): Promise<boolean> {
return await tracer.trace("doesTableExist", async span => {
span.addTags({ tableId })
try { try {
const table = await getTable(tableId) const table = await getTable(tableId)
span.addTags({ tableExists: !!table })
return !!table return !!table
} catch (err) { } catch (err) {
span.addTags({ tableExists: false })
return false return false
} }
})
} }
export async function getAllTables() { export async function getAllTables() {
return await tracer.trace("getAllTables", async span => {
const [internal, external] = await Promise.all([ const [internal, external] = await Promise.all([
getAllInternalTables(), getAllInternalTables(),
getAllExternalTables(), getAllExternalTables(),
]) ])
span.addTags({
numInternalTables: internal.length,
numExternalTables: external.length,
})
return await processTables([...internal, ...external]) return await processTables([...internal, ...external])
})
} }
export async function getExternalTablesInDatasource( export async function getExternalTablesInDatasource(
datasourceId: string datasourceId: string
): Promise<Record<string, Table>> { ): Promise<Record<string, Table>> {
return await tracer.trace("getExternalTablesInDatasource", async span => {
const datasource = await datasources.get(datasourceId, { enriched: true }) const datasource = await datasources.get(datasourceId, { enriched: true })
if (!datasource || !datasource.entities) { if (!datasource || !datasource.entities) {
throw new Error("Datasource is not configured fully.") throw new Error("Datasource is not configured fully.")
} }
span.addTags({
datasourceId,
numEntities: Object.keys(datasource.entities).length,
})
return await processEntities(datasource.entities) return await processEntities(datasource.entities)
})
} }
export async function getTables(tableIds: string[]): Promise<Table[]> { export async function getTables(tableIds: string[]): Promise<Table[]> {
return tracer.trace("getTables", async span => {
span.addTags({ numTableIds: tableIds.length })
const externalTableIds = tableIds.filter(tableId => const externalTableIds = tableIds.filter(tableId =>
isExternalTableID(tableId) isExternalTableID(tableId)
), ),
@ -170,12 +222,16 @@ export async function getTables(tableIds: string[]): Promise<Table[]> {
}) })
tables = tables.concat(internalTables) tables = tables.concat(internalTables)
} }
span.addTags({ numTables: tables.length })
return await processTables(tables) return await processTables(tables)
})
} }
export async function enrichViewSchemas( export async function enrichViewSchemas(
table: Table table: Table
): Promise<FindTableResponse> { ): Promise<FindTableResponse> {
return await tracer.trace("enrichViewSchemas", async span => {
span.addTags({ tableId: table._id })
const views = [] const views = []
for (const view of Object.values(table.views ?? [])) { for (const view of Object.values(table.views ?? [])) {
if (sdk.views.isV2(view)) { if (sdk.views.isV2(view)) {
@ -190,4 +246,5 @@ export async function enrichViewSchemas(
return p return p
}, {} as TableViewsResponse), }, {} as TableViewsResponse),
} }
})
} }

View File

@ -38,10 +38,8 @@ export interface UpdateSelfRequest {
lastName?: string lastName?: string
password?: string password?: string
forceResetPassword?: boolean forceResetPassword?: boolean
onboardedAt?: string
freeTrialConfirmedAt?: string freeTrialConfirmedAt?: string
appFavourites?: string[] appFavourites?: string[]
tours?: Record<string, Date>
appSort?: string appSort?: string
} }

View File

@ -64,15 +64,15 @@ export interface User extends Document {
status?: UserStatus status?: UserStatus
createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now() createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now()
userGroups?: string[] userGroups?: string[]
onboardedAt?: string
freeTrialConfirmedAt?: string freeTrialConfirmedAt?: string
tours?: Record<string, Date>
scimInfo?: { isSync: true } & Record<string, any> scimInfo?: { isSync: true } & Record<string, any>
appFavourites?: string[] appFavourites?: string[]
ssoId?: string ssoId?: string
appSort?: string appSort?: string
budibaseAccess?: boolean budibaseAccess?: boolean
accountPortalAccess?: boolean accountPortalAccess?: boolean
onboardedAt?: string // deprecated and no longer saved
tours?: Record<string, Date> // deprecated and no longer saved
} }
export interface UserBindings extends Document { export interface UserBindings extends Document {

View File

@ -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 { export interface UserPermissionAssignedEvent extends BaseEvent {
userId: string userId: string
audited: { audited: {

View File

@ -75,7 +75,8 @@
"pouchdb": "7.3.0", "pouchdb": "7.3.0",
"pouchdb-all-dbs": "1.1.1", "pouchdb-all-dbs": "1.1.1",
"server-destroy": "1.0.1", "server-destroy": "1.0.1",
"uuid": "^8.3.2" "uuid": "^8.3.2",
"http-graceful-shutdown": "^3.1.12"
}, },
"devDependencies": { "devDependencies": {
"@jest/types": "^29.6.3", "@jest/types": "^29.6.3",

View File

@ -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() const user = await config.createUser()
await config.createSession(user) await config.createSession(user)
const res = await config.api.self const res = await config.api.self
.updateSelf(user, { .updateSelf(user, {
onboardedAt: "2023-03-07T14:10:54.869Z",
freeTrialConfirmedAt: "2024-03-17T14:10:54.869Z", freeTrialConfirmedAt: "2024-03-17T14:10:54.869Z",
}) })
.expect(200) .expect(200)
@ -61,7 +60,6 @@ describe("/api/global/self", () => {
const dbUser = (await config.getUser(user.email))! const dbUser = (await config.getUser(user.email))!
user._rev = dbUser._rev 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(dbUser.freeTrialConfirmedAt).toBe("2024-03-17T14:10:54.869Z")
expect(res.body._id).toBe(user._id) expect(res.body._id).toBe(user._id)
}) })

View File

@ -25,10 +25,8 @@ export const buildSelfSaveValidation = () => {
forceResetPassword: Joi.boolean().optional(), forceResetPassword: Joi.boolean().optional(),
firstName: OPTIONAL_STRING, firstName: OPTIONAL_STRING,
lastName: OPTIONAL_STRING, lastName: OPTIONAL_STRING,
onboardedAt: Joi.string().optional(),
freeTrialConfirmedAt: Joi.string().optional(), freeTrialConfirmedAt: Joi.string().optional(),
appFavourites: Joi.array().optional(), appFavourites: Joi.array().optional(),
tours: Joi.object().optional(),
appSort: Joi.string().optional(), appSort: Joi.string().optional(),
} }
return auth.joiValidator.body(Joi.object(schema).required().unknown(false)) return auth.joiValidator.body(Joi.object(schema).required().unknown(false))

View File

@ -26,12 +26,12 @@ db.init()
import koaBody from "koa-body" import koaBody from "koa-body"
import http from "http" import http from "http"
import api from "./api" import api from "./api"
import gracefulShutdown from "http-graceful-shutdown"
const koaSession = require("koa-session") const koaSession = require("koa-session")
import { userAgent } from "koa-useragent" import { userAgent } from "koa-useragent"
import destroyable from "server-destroy"
import { initPro } from "./initPro" import { initPro } from "./initPro"
import { handleScimBody } from "./middleware/handleScimBody" import { handleScimBody } from "./middleware/handleScimBody"
@ -86,29 +86,40 @@ app.use(auth.passport.session())
app.use(api.routes()) app.use(api.routes())
const server = http.createServer(app.callback()) const server = http.createServer(app.callback())
destroyable(server)
let shuttingDown = false, const shutdown = async () => {
errCode = 0 console.log("Worker service shutting down gracefully...")
server.on("close", async () => {
if (shuttingDown) {
return
}
shuttingDown = true
console.log("Server Closed")
timers.cleanup() timers.cleanup()
events.shutdown() events.shutdown()
await redis.clients.shutdown() await redis.clients.shutdown()
await queue.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 = () => { process.on("unhandledRejection", async reason => {
server.close() logging.logAlert("Unhandled Promise Rejection", reason as Error)
server.destroy() await shutdown()
} if (!env.isTest) {
process.exit(1)
}
})
export default server.listen(parseInt(env.PORT || "4002"), async () => { export default server.listen(parseInt(env.PORT || "4002"), async () => {
let startupLog = `Worker running on ${JSON.stringify(server.address())}` 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 // can't integrate directly into backend-core due to cyclic issues
await events.processors.init(proSdk.auditLogs.write) 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()
})

View File

@ -13161,6 +13161,13 @@ http-errors@~1.6.2:
setprototypeof "1.1.0" setprototypeof "1.1.0"
statuses ">= 1.4.0 < 2" 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: http-proxy-agent@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a"