Merge branch 'develop' of github.com:Budibase/budibase into feature/BUDI-7052
This commit is contained in:
commit
b84b8dd988
|
@ -159,7 +159,7 @@ jobs:
|
|||
run: |
|
||||
cd qa-core
|
||||
yarn setup
|
||||
yarn test:ci
|
||||
yarn serve:test:self:ci
|
||||
env:
|
||||
BB_ADMIN_USER_EMAIL: admin
|
||||
BB_ADMIN_USER_PASSWORD: admin
|
||||
|
|
|
@ -6,7 +6,7 @@ concurrency:
|
|||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*-alpha.*
|
||||
- "*-alpha.*"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
|
|
|
@ -6,9 +6,9 @@ concurrency:
|
|||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
# Exclude all pre-releases
|
||||
- "!v*[0-9]+.[0-9]+.[0-9]+-*"
|
||||
- "!*[0-9]+.[0-9]+.[0-9]+-*"
|
||||
|
||||
env:
|
||||
# Posthog token used by ui at build time
|
||||
|
@ -98,7 +98,7 @@ jobs:
|
|||
git fetch
|
||||
mkdir sync
|
||||
echo "Packaging chart to sync dir"
|
||||
helm package charts/budibase --version 0.0.0-master --app-version v"$RELEASE_VERSION" --destination sync
|
||||
helm package charts/budibase --version 0.0.0-master --app-version "$RELEASE_VERSION" --destination sync
|
||||
echo "Packaging successful"
|
||||
git checkout gh-pages
|
||||
echo "Indexing helm repo"
|
||||
|
|
|
@ -43,7 +43,7 @@ jobs:
|
|||
run: |
|
||||
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||
|
||||
release_tag=v${{ env.RELEASE_VERSION }}
|
||||
release_tag=${{ env.RELEASE_VERSION }}
|
||||
|
||||
# Pull apps and worker images
|
||||
docker pull budibase/apps:$release_tag
|
||||
|
@ -108,8 +108,8 @@ jobs:
|
|||
- name: Perform Github Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: v${{ env.RELEASE_VERSION }}
|
||||
tag_name: v${{ env.RELEASE_VERSION }}
|
||||
name: ${{ env.RELEASE_VERSION }}
|
||||
tag_name: ${{ env.RELEASE_VERSION }}
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
packages/cli/build/cli-win.exe
|
||||
|
|
|
@ -71,7 +71,7 @@ jobs:
|
|||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }}
|
||||
tags: budibase/budibase,budibase/budibase:${{ env.RELEASE_VERSION }}
|
||||
file: ./hosting/single/Dockerfile
|
||||
- name: Tag and release Budibase Azure App Service docker image
|
||||
uses: docker/build-push-action@v2
|
||||
|
@ -80,5 +80,5 @@ jobs:
|
|||
push: true
|
||||
platforms: linux/amd64
|
||||
build-args: TARGETBUILD=aas
|
||||
tags: budibase/budibase-aas,budibase/budibase-aas:v${{ env.RELEASE_VERSION }}
|
||||
tags: budibase/budibase-aas,budibase/budibase-aas:${{ env.RELEASE_VERSION }}
|
||||
file: ./hosting/single/Dockerfile
|
||||
|
|
|
@ -209,7 +209,7 @@ services:
|
|||
# Override values in couchDB subchart
|
||||
couchdb:
|
||||
## clusterSize is the initial size of the CouchDB cluster.
|
||||
clusterSize: 3
|
||||
clusterSize: 1
|
||||
allowAdminParty: false
|
||||
|
||||
# Secret Management
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.8.12-alpha.3",
|
||||
"version": "2.8.18-alpha.0",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -20,6 +20,8 @@ export enum Header {
|
|||
TYPE = "x-budibase-type",
|
||||
PREVIEW_ROLE = "x-budibase-role",
|
||||
TENANT_ID = "x-budibase-tenant-id",
|
||||
VERIFICATION_CODE = "x-budibase-verification-code",
|
||||
RETURN_VERIFICATION_CODE = "x-budibase-return-verification-code",
|
||||
TOKEN = "x-budibase-token",
|
||||
CSRF_TOKEN = "x-csrf-token",
|
||||
CORRELATION_ID = "x-budibase-correlation-id",
|
||||
|
|
|
@ -2,6 +2,3 @@ export * as correlation from "./correlation/correlation"
|
|||
export { logger } from "./pino/logger"
|
||||
export * from "./alerts"
|
||||
export * as system from "./system"
|
||||
|
||||
// turn off or on context logging i.e. tenantId, appId etc
|
||||
export let LOG_CONTEXT = true
|
||||
|
|
|
@ -2,11 +2,9 @@ import pino, { LoggerOptions } from "pino"
|
|||
import pinoPretty from "pino-pretty"
|
||||
|
||||
import { IdentityType } from "@budibase/types"
|
||||
|
||||
import env from "../../environment"
|
||||
import * as context from "../../context"
|
||||
import * as correlation from "../correlation"
|
||||
import { LOG_CONTEXT } from "../index"
|
||||
|
||||
import { localFileDestination } from "../system"
|
||||
|
||||
|
@ -93,15 +91,13 @@ if (!env.DISABLE_PINO_LOGGER) {
|
|||
|
||||
let contextObject = {}
|
||||
|
||||
if (LOG_CONTEXT) {
|
||||
contextObject = {
|
||||
tenantId: getTenantId(),
|
||||
appId: getAppId(),
|
||||
automationId: getAutomationId(),
|
||||
identityId: identity?._id,
|
||||
identityType: identity?.type,
|
||||
correlationId: correlation.getId(),
|
||||
}
|
||||
contextObject = {
|
||||
tenantId: getTenantId(),
|
||||
appId: getAppId(),
|
||||
automationId: getAutomationId(),
|
||||
identityId: identity?._id,
|
||||
identityType: identity?.type,
|
||||
correlationId: correlation.getId(),
|
||||
}
|
||||
|
||||
const mergingObject: any = {
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
setContext("drawer-actions", {
|
||||
hide,
|
||||
show,
|
||||
headless,
|
||||
})
|
||||
|
||||
const easeInOutQuad = x => {
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
Icon,
|
||||
Checkbox,
|
||||
DatePicker,
|
||||
Detail,
|
||||
} from "@budibase/bbui"
|
||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
|
@ -32,7 +33,7 @@
|
|||
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||
import {
|
||||
bindingsToCompletions,
|
||||
jsAutocomplete,
|
||||
hbAutocomplete,
|
||||
EditorModes,
|
||||
} from "components/common/CodeEditor"
|
||||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
||||
|
@ -55,6 +56,7 @@
|
|||
let drawer
|
||||
let fillWidth = true
|
||||
let inputData
|
||||
let codeBindingOpen = false
|
||||
|
||||
$: filters = lookForFilters(schemaProperties) || []
|
||||
$: tempFilters = filters
|
||||
|
@ -70,6 +72,13 @@
|
|||
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||
$: isTrigger = block?.type === "TRIGGER"
|
||||
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
||||
$: codeMode =
|
||||
stepId === "EXECUTE_BASH" ? EditorModes.Handlebars : EditorModes.JS
|
||||
|
||||
$: stepCompletions =
|
||||
codeMode === EditorModes.Handlebars
|
||||
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
||||
: []
|
||||
|
||||
/**
|
||||
* TODO - Remove after November 2023
|
||||
|
@ -489,6 +498,18 @@
|
|||
/>
|
||||
{:else if value.customType === "code"}
|
||||
<CodeEditorModal>
|
||||
{#if codeMode == EditorModes.JS}
|
||||
<ActionButton
|
||||
on:click={() => (codeBindingOpen = !codeBindingOpen)}
|
||||
quiet
|
||||
icon={codeBindingOpen ? "ChevronDown" : "ChevronRight"}
|
||||
>
|
||||
<Detail size="S">Bindings</Detail>
|
||||
</ActionButton>
|
||||
{#if codeBindingOpen}
|
||||
<pre>{JSON.stringify(bindings, null, 2)}</pre>
|
||||
{/if}
|
||||
{/if}
|
||||
<CodeEditor
|
||||
value={inputData[key]}
|
||||
on:change={e => {
|
||||
|
@ -496,19 +517,22 @@
|
|||
onChange({ detail: e.detail }, key)
|
||||
inputData[key] = e.detail
|
||||
}}
|
||||
completions={[
|
||||
jsAutocomplete([
|
||||
...bindingsToCompletions(bindings, EditorModes.JS),
|
||||
]),
|
||||
]}
|
||||
mode={EditorModes.JS}
|
||||
completions={stepCompletions}
|
||||
mode={codeMode}
|
||||
autocompleteEnabled={codeMode != EditorModes.JS}
|
||||
height={500}
|
||||
/>
|
||||
<div class="messaging">
|
||||
<Icon name="FlashOn" />
|
||||
<div class="messaging-wrap">
|
||||
<div>Add available bindings by typing <strong>$</strong></div>
|
||||
</div>
|
||||
{#if codeMode == EditorModes.Handlebars}
|
||||
<Icon name="FlashOn" />
|
||||
<div class="messaging-wrap">
|
||||
<div>
|
||||
Add available bindings by typing <strong>
|
||||
}}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</CodeEditorModal>
|
||||
{:else if value.customType === "loopOption"}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { Label } from "@budibase/bbui"
|
||||
import { onMount, createEventDispatcher } from "svelte"
|
||||
import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates"
|
||||
|
||||
import {
|
||||
autocompletion,
|
||||
|
@ -51,6 +52,7 @@
|
|||
export let mode = EditorModes.Handlebars
|
||||
export let value = ""
|
||||
export let placeholder = null
|
||||
export let autocompleteEnabled = true
|
||||
|
||||
// Export a function to expose caret position
|
||||
export const getCaretPosition = () => {
|
||||
|
@ -80,7 +82,7 @@
|
|||
|
||||
// For handlebars only.
|
||||
const bindStyle = new MatchDecorator({
|
||||
regexp: /{{[."#\-\w\s\][]*}}/g,
|
||||
regexp: FIND_ANY_HBS_REGEX,
|
||||
decoration: () => {
|
||||
return Decoration.mark({
|
||||
tag: "span",
|
||||
|
@ -150,12 +152,6 @@
|
|||
syntaxHighlighting(oneDarkHighlightStyle, { fallback: true }),
|
||||
highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
autocompletion({
|
||||
override: [...completions],
|
||||
closeOnBlur: true,
|
||||
icons: false,
|
||||
optionClass: () => "autocomplete-option",
|
||||
}),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.updateListener.of(v => {
|
||||
const docStr = v.state.doc?.toString()
|
||||
|
@ -178,11 +174,16 @@
|
|||
|
||||
const buildExtensions = base => {
|
||||
const complete = [...base]
|
||||
if (mode.name == "javascript") {
|
||||
complete.push(javascript())
|
||||
complete.push(highlightWhitespace())
|
||||
complete.push(lineNumbers())
|
||||
complete.push(foldGutter())
|
||||
|
||||
if (autocompleteEnabled) {
|
||||
complete.push(
|
||||
autocompletion({
|
||||
override: [...completions],
|
||||
closeOnBlur: true,
|
||||
icons: false,
|
||||
optionClass: () => "autocomplete-option",
|
||||
})
|
||||
)
|
||||
complete.push(
|
||||
EditorView.inputHandler.of((view, from, to, insert) => {
|
||||
if (insert === "$") {
|
||||
|
@ -212,6 +213,13 @@
|
|||
)
|
||||
}
|
||||
|
||||
if (mode.name == "javascript") {
|
||||
complete.push(javascript())
|
||||
complete.push(highlightWhitespace())
|
||||
complete.push(lineNumbers())
|
||||
complete.push(foldGutter())
|
||||
}
|
||||
|
||||
if (placeholder) {
|
||||
complete.push(placeholderFn(placeholder))
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import { isBuilderInputFocused } from "helpers"
|
||||
|
||||
export let store
|
||||
|
||||
|
@ -8,9 +9,16 @@
|
|||
if (!(e.ctrlKey || e.metaKey)) {
|
||||
return
|
||||
}
|
||||
if (e.shiftKey && e.key === "Z") {
|
||||
|
||||
let keyLowerCase = e.key.toLowerCase()
|
||||
|
||||
// Ignore events when typing
|
||||
if (isBuilderInputFocused(e)) {
|
||||
return
|
||||
}
|
||||
if (e.shiftKey && keyLowerCase === "z") {
|
||||
store.redo()
|
||||
} else if (e.key === "z") {
|
||||
} else if (keyLowerCase === "z") {
|
||||
store.undo()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -341,7 +341,7 @@
|
|||
</Tab>
|
||||
{/if}
|
||||
<div class="drawer-actions">
|
||||
{#if drawerActions?.hide}
|
||||
{#if typeof drawerActions.hide === "function" && drawerActions.headless}
|
||||
<Button
|
||||
secondary
|
||||
quiet
|
||||
|
@ -352,7 +352,7 @@
|
|||
Cancel
|
||||
</Button>
|
||||
{/if}
|
||||
{#if bindingDrawerActions?.save}
|
||||
{#if typeof bindingDrawerActions?.save === "function" && drawerActions.headless}
|
||||
<Button
|
||||
cta
|
||||
disabled={!valid}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Popover, Layout, Heading, Body, Button } from "@budibase/bbui"
|
||||
import { Popover, Layout, Heading, Body, Button, Link } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { TOURS } from "./tours.js"
|
||||
import { goto, layout, isActive } from "@roxi/routify"
|
||||
|
@ -10,17 +10,20 @@
|
|||
let tourStep
|
||||
let tourStepIdx
|
||||
let lastStep
|
||||
let skipping = false
|
||||
|
||||
$: tourNodes = { ...$store.tourNodes }
|
||||
$: tourKey = $store.tourKey
|
||||
$: tourStepKey = $store.tourStepKey
|
||||
$: tour = TOURS[tourKey]
|
||||
$: tourOnSkip = tour?.onSkip
|
||||
|
||||
const updateTourStep = (targetStepKey, tourKey) => {
|
||||
if (!tourKey) {
|
||||
return
|
||||
}
|
||||
if (!tourSteps?.length) {
|
||||
tourSteps = [...TOURS[tourKey]]
|
||||
tourSteps = [...tour.steps]
|
||||
}
|
||||
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
|
||||
lastStep = tourStepIdx + 1 == tourSteps.length
|
||||
|
@ -71,23 +74,8 @@
|
|||
tourStep.onComplete()
|
||||
}
|
||||
popover.hide()
|
||||
if (tourStep.endRoute) {
|
||||
$goto(tourStep.endRoute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const previousStep = async () => {
|
||||
if (tourStepIdx > 0) {
|
||||
let target = tourSteps[tourStepIdx - 1]
|
||||
if (target) {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
tourStepKey: target.id,
|
||||
}))
|
||||
navigateStep(target)
|
||||
} else {
|
||||
console.log("Could not retrieve step")
|
||||
if (tour.endRoute) {
|
||||
$goto(tour.endRoute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -132,16 +120,23 @@
|
|||
</Body>
|
||||
<div class="tour-footer">
|
||||
<div class="tour-navigation">
|
||||
{#if tourStepIdx > 0}
|
||||
<Button
|
||||
{#if typeof tourOnSkip === "function"}
|
||||
<Link
|
||||
secondary
|
||||
on:click={previousStep}
|
||||
disabled={tourStepIdx == 0}
|
||||
quiet
|
||||
on:click={() => {
|
||||
skipping = true
|
||||
tourOnSkip()
|
||||
if (tour.endRoute) {
|
||||
$goto(tour.endRoute)
|
||||
}
|
||||
}}
|
||||
disabled={skipping}
|
||||
>
|
||||
<div>Back</div>
|
||||
</Button>
|
||||
Skip
|
||||
</Link>
|
||||
{/if}
|
||||
<Button cta on:click={nextStep}>
|
||||
<Button cta on:click={nextStep} disabled={skipping}>
|
||||
<div>{lastStep ? "Finish" : "Next"}</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -157,9 +152,10 @@
|
|||
padding: var(--spacing-xl);
|
||||
}
|
||||
.tour-navigation {
|
||||
grid-gap: var(--spectrum-alias-grid-baseline);
|
||||
grid-gap: var(--spacing-xl);
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
.tour-body :global(.feature-list) {
|
||||
margin-bottom: 0px;
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
const registerTourNode = (tourKey, stepKey) => {
|
||||
if (ready && !registered && tourKey) {
|
||||
currentTourStep = TOURS[tourKey].find(step => step.id === stepKey)
|
||||
currentTourStep = TOURS[tourKey].steps.find(step => step.id === stepKey)
|
||||
if (!currentTourStep) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { auth } from "stores/portal"
|
|||
import analytics from "analytics"
|
||||
import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps"
|
||||
import { API } from "api"
|
||||
|
||||
const ONBOARDING_EVENT_PREFIX = "onboarding"
|
||||
|
||||
export const TOUR_STEP_KEYS = {
|
||||
|
@ -20,6 +21,37 @@ export const TOUR_KEYS = {
|
|||
FEATURE_ONBOARDING: "feature-onboarding",
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
store.update(state => ({
|
||||
...state,
|
||||
tourNodes: undefined,
|
||||
tourKey: undefined,
|
||||
tourKeyStep: undefined,
|
||||
onboarding: false,
|
||||
}))
|
||||
} catch (e) {
|
||||
console.log("Onboarding failed", e)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const tourEvent = eventKey => {
|
||||
analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, {
|
||||
eventSource: EventSource.PORTAL,
|
||||
|
@ -28,111 +60,81 @@ const tourEvent = eventKey => {
|
|||
|
||||
const getTours = () => {
|
||||
return {
|
||||
[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: [
|
||||
{
|
||||
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)
|
||||
[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",
|
||||
},
|
||||
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 })
|
||||
},
|
||||
{
|
||||
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)
|
||||
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,
|
||||
},
|
||||
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",
|
||||
endRoute: "/builder/app/:application/data",
|
||||
query: ".toprightnav #builder-app-publish-button",
|
||||
onLoad: () => {
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
|
||||
},
|
||||
onComplete: async () => {
|
||||
// Mark the users onboarding as complete
|
||||
// Clear all tour related state
|
||||
if (get(auth).user) {
|
||||
await API.updateSelf({
|
||||
onboardedAt: new Date().toISOString(),
|
||||
})
|
||||
|
||||
// Update the cached user
|
||||
await auth.getSelf()
|
||||
|
||||
store.update(state => ({
|
||||
...state,
|
||||
tourNodes: undefined,
|
||||
tourKey: undefined,
|
||||
tourKeyStep: undefined,
|
||||
onboarding: false,
|
||||
}))
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
[TOUR_KEYS.FEATURE_ONBOARDING]: [
|
||||
{
|
||||
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: async () => {
|
||||
// Push the onboarding forward
|
||||
if (get(auth).user) {
|
||||
await API.updateSelf({
|
||||
onboardedAt: new Date().toISOString(),
|
||||
})
|
||||
|
||||
// Update the cached user
|
||||
await auth.getSelf()
|
||||
|
||||
store.update(state => ({
|
||||
...state,
|
||||
tourNodes: undefined,
|
||||
tourKey: undefined,
|
||||
tourKeyStep: undefined,
|
||||
onboarding: false,
|
||||
}))
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,3 +29,15 @@ export const lowercase = s => s.substring(0, 1).toLowerCase() + s.substring(1)
|
|||
export const get_name = s => (!s ? "" : last(s.split("/")))
|
||||
|
||||
export const get_capitalised_name = name => pipe(name, [get_name, capitalise])
|
||||
|
||||
export const isBuilderInputFocused = e => {
|
||||
const activeTag = document.activeElement?.tagName.toLowerCase()
|
||||
const inCodeEditor = document.activeElement?.classList?.contains("cm-content")
|
||||
if (
|
||||
(inCodeEditor || ["input", "textarea"].indexOf(activeTag) !== -1) &&
|
||||
e.key !== "Escape"
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -7,4 +7,5 @@ export {
|
|||
get_name,
|
||||
get_capitalised_name,
|
||||
lowercase,
|
||||
isBuilderInputFocused,
|
||||
} from "./helpers"
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
import TourPopover from "components/portal/onboarding/TourPopover.svelte"
|
||||
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
||||
import { UserAvatars } from "@budibase/frontend-core"
|
||||
import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
|
||||
import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
|
||||
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
||||
|
||||
export let application
|
||||
|
@ -87,17 +87,10 @@
|
|||
// Check if onboarding is enabled.
|
||||
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
|
||||
if (!$auth.user?.onboardedAt) {
|
||||
// Determine the correct step
|
||||
const activeNav = $layout.children.find(c => $isActive(c.path))
|
||||
const onboardingTour = TOURS[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]
|
||||
const targetStep = activeNav
|
||||
? onboardingTour.find(step => step.route === activeNav?.path)
|
||||
: null
|
||||
await store.update(state => ({
|
||||
...state,
|
||||
onboarding: true,
|
||||
tourKey: TOUR_KEYS.TOUR_BUILDER_ONBOARDING,
|
||||
tourStepKey: targetStep?.id,
|
||||
}))
|
||||
} else {
|
||||
// Feature tour date
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import { goto, isActive } from "@roxi/routify"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { isBuilderInputFocused } from "helpers"
|
||||
|
||||
let confirmDeleteDialog
|
||||
let confirmEjectDialog
|
||||
|
@ -100,13 +101,7 @@
|
|||
return
|
||||
}
|
||||
// Ignore events when typing
|
||||
const activeTag = document.activeElement?.tagName.toLowerCase()
|
||||
const inCodeEditor =
|
||||
document.activeElement?.classList?.contains("cm-content")
|
||||
if (
|
||||
(inCodeEditor || ["input", "textarea"].indexOf(activeTag) !== -1) &&
|
||||
e.key !== "Escape"
|
||||
) {
|
||||
if (isBuilderInputFocused(e)) {
|
||||
return
|
||||
}
|
||||
// Key events are always for the selected component
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
<script>
|
||||
import {
|
||||
Layout,
|
||||
Heading,
|
||||
Body,
|
||||
Helpers,
|
||||
Divider,
|
||||
notifications,
|
||||
Icon,
|
||||
TextArea,
|
||||
} from "@budibase/bbui"
|
||||
import { auth, admin } from "stores/portal"
|
||||
import { redirect } from "@roxi/routify"
|
||||
import { API } from "api"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let diagnosticInfo = ""
|
||||
|
||||
// Make sure page can't be visited directly in cloud
|
||||
$: {
|
||||
if ($admin.cloud) {
|
||||
$redirect("../../portal")
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSystemDebugInfo() {
|
||||
const diagnostics = await API.fetchSystemDebugInfo()
|
||||
diagnosticInfo = {
|
||||
browser: {
|
||||
language: navigator.language || navigator.userLanguage,
|
||||
userAgent: navigator.userAgent,
|
||||
platform: navigator.platform,
|
||||
vendor: navigator.vendor,
|
||||
},
|
||||
server: diagnostics,
|
||||
}
|
||||
}
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
await Helpers.copyToClipboard(JSON.stringify(diagnosticInfo, undefined, 2))
|
||||
notifications.success("Copied")
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await fetchSystemDebugInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $auth.isAdmin && diagnosticInfo}
|
||||
<Layout noPadding>
|
||||
<Layout gap="XS">
|
||||
<Heading size="M">Diagnostics</Heading>
|
||||
Please include this diagnostic information in support requests and github issues
|
||||
by clicking the button on the top right to copy to clipboard.
|
||||
<Divider />
|
||||
<Body size="M">
|
||||
<section>
|
||||
<div on:click={copyToClipboard} class="copy-icon">
|
||||
<Icon name="Copy" size="M" />
|
||||
</div>
|
||||
<TextArea
|
||||
height="45vh"
|
||||
disabled
|
||||
value={JSON.stringify(diagnosticInfo, undefined, 2)}
|
||||
/>
|
||||
</section>
|
||||
</Body>
|
||||
</Layout>
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
section {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -64,6 +64,10 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
|||
title: "Version",
|
||||
href: "/builder/portal/settings/version",
|
||||
})
|
||||
settingsSubPages.push({
|
||||
title: "Diagnostics",
|
||||
href: "/builder/portal/settings/diagnostics",
|
||||
})
|
||||
}
|
||||
menu.push({
|
||||
title: "Settings",
|
||||
|
|
|
@ -8,27 +8,31 @@
|
|||
|
||||
let structureLookupMap = {}
|
||||
|
||||
const registerBlockComponent = (id, order, parentId, instance) => {
|
||||
const registerBlockComponent = (id, parentId, order, instance) => {
|
||||
// Ensure child map exists
|
||||
if (!structureLookupMap[parentId]) {
|
||||
structureLookupMap[parentId] = {}
|
||||
}
|
||||
// Add this instance in this order, overwriting any existing instance in
|
||||
// this order in case of repeaters
|
||||
structureLookupMap[parentId][order] = instance
|
||||
structureLookupMap[parentId][id] = { order, instance }
|
||||
}
|
||||
|
||||
const unregisterBlockComponent = (order, parentId) => {
|
||||
const unregisterBlockComponent = (id, parentId) => {
|
||||
// Ensure child map exists
|
||||
if (!structureLookupMap[parentId]) {
|
||||
return
|
||||
}
|
||||
delete structureLookupMap[parentId][order]
|
||||
delete structureLookupMap[parentId][id]
|
||||
}
|
||||
|
||||
const eject = () => {
|
||||
// Start the new structure with the root component
|
||||
let definition = structureLookupMap[$component.id][0]
|
||||
const rootMap = structureLookupMap[$component.id] || {}
|
||||
let definition = Object.values(rootMap)[0]?.instance
|
||||
if (!definition) {
|
||||
return
|
||||
}
|
||||
|
||||
// Copy styles from block to root component
|
||||
definition._styles = {
|
||||
|
@ -49,10 +53,7 @@
|
|||
const attachChildren = (rootComponent, map) => {
|
||||
// Transform map into children array
|
||||
let id = rootComponent._id
|
||||
const children = Object.entries(map[id] || {}).map(([order, instance]) => ({
|
||||
order,
|
||||
instance,
|
||||
}))
|
||||
const children = Object.values(map[id] || {})
|
||||
if (!children.length) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
// Create a fake component instance so that we can use the core Component
|
||||
// to render this part of the block, taking advantage of binding enrichment
|
||||
$: id = `${block.id}-${context ?? rand}`
|
||||
$: parentId = $component?.id
|
||||
$: inBuilder = $builderStore.inBuilder
|
||||
$: instance = {
|
||||
_component: `@budibase/standard-components/${type}`,
|
||||
_id: id,
|
||||
|
@ -38,14 +40,14 @@
|
|||
// Register this block component if we're inside the builder so it can be
|
||||
// ejected later
|
||||
$: {
|
||||
if ($builderStore.inBuilder) {
|
||||
block.registerComponent(id, order ?? 0, $component?.id, instance)
|
||||
if (inBuilder) {
|
||||
block.registerComponent(id, parentId, order ?? 0, instance)
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if ($builderStore.inBuilder) {
|
||||
block.unregisterComponent(order ?? 0, $component?.id)
|
||||
if (inBuilder) {
|
||||
block.unregisterComponent(id, parentId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -126,7 +126,7 @@
|
|||
order={1}
|
||||
>
|
||||
{#if enrichedSearchColumns?.length}
|
||||
{#each enrichedSearchColumns as column, idx}
|
||||
{#each enrichedSearchColumns as column, idx (column.name)}
|
||||
<BlockComponent
|
||||
type={column.componentType}
|
||||
props={{
|
||||
|
|
|
@ -170,7 +170,7 @@
|
|||
order={1}
|
||||
>
|
||||
{#if enrichedSearchColumns?.length}
|
||||
{#each enrichedSearchColumns as column, idx}
|
||||
{#each enrichedSearchColumns as column, idx (column.name)}
|
||||
<BlockComponent
|
||||
type={column.componentType}
|
||||
props={{
|
||||
|
|
|
@ -478,7 +478,7 @@ export const enrichButtonActions = (actions, context) => {
|
|||
actions.slice(i + 1),
|
||||
newContext
|
||||
)
|
||||
resolve(await next())
|
||||
resolve(typeof next === "function" ? await next() : true)
|
||||
} else {
|
||||
resolve(false)
|
||||
}
|
||||
|
|
|
@ -123,6 +123,15 @@ export const buildAppEndpoints = API => ({
|
|||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets budibase platform debug information.
|
||||
*/
|
||||
fetchSystemDebugInfo: async () => {
|
||||
return await API.get({
|
||||
url: `/api/debug/diagnostics`,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Syncs an app with the production database.
|
||||
* @param appId the ID of the app to sync
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 9c564edb37cb619cb5971e10c4317fa6e7c5bb00
|
||||
Subproject commit 4d9840700e7684581c39965b7cb6a2b2428c477c
|
|
@ -0,0 +1,48 @@
|
|||
import os from "os"
|
||||
import process from "process"
|
||||
import { env } from "@budibase/backend-core"
|
||||
import { GetDiagnosticsResponse, UserCtx } from "@budibase/types"
|
||||
|
||||
export async function systemDebugInfo(
|
||||
ctx: UserCtx<void, GetDiagnosticsResponse>
|
||||
) {
|
||||
const { days, hours, minutes } = secondsToHMS(os.uptime())
|
||||
const totalMemory = convertBytes(os.totalmem())
|
||||
|
||||
ctx.body = {
|
||||
budibaseVersion: env.VERSION,
|
||||
hosting: env.DEPLOYMENT_ENVIRONMENT,
|
||||
nodeVersion: process.version,
|
||||
platform: process.platform,
|
||||
cpuArch: process.arch,
|
||||
cpuCores: os.cpus().length,
|
||||
cpuInfo: os.cpus()[0].model,
|
||||
totalMemory: `${totalMemory.gb}GB`,
|
||||
uptime: `${days} day(s), ${hours} hour(s), ${minutes} minute(s)`,
|
||||
}
|
||||
}
|
||||
|
||||
function secondsToHMS(seconds: number) {
|
||||
const MINUTE_IN_SECONDS = 60
|
||||
const HOUR_IN_SECONDS = 3600
|
||||
const DAY_IN_SECONDS = HOUR_IN_SECONDS * 24
|
||||
|
||||
const minutes = Math.floor((seconds / MINUTE_IN_SECONDS) % 60)
|
||||
const hours = Math.floor((seconds / HOUR_IN_SECONDS) % 24)
|
||||
const days = Math.floor(seconds / DAY_IN_SECONDS)
|
||||
|
||||
return {
|
||||
days,
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
}
|
||||
}
|
||||
|
||||
function convertBytes(bytes: number) {
|
||||
const kb = bytes / 1024
|
||||
const mb = kb / 1024
|
||||
const gb = mb / 1024
|
||||
|
||||
return { gb, mb, kb }
|
||||
}
|
|
@ -157,7 +157,7 @@ export async function preview(ctx: any) {
|
|||
}
|
||||
const runFn = () => Runner.run(inputs)
|
||||
|
||||
const { rows, keys, info, extra } = await quotas.addQuery(runFn, {
|
||||
const { rows, keys, info, extra } = await quotas.addQuery<any>(runFn, {
|
||||
datasourceId: datasource._id,
|
||||
})
|
||||
const schemaFields: any = {}
|
||||
|
@ -246,9 +246,12 @@ async function execute(
|
|||
}
|
||||
const runFn = () => Runner.run(inputs)
|
||||
|
||||
const { rows, pagination, extra, info } = await quotas.addQuery(runFn, {
|
||||
datasourceId: datasource._id,
|
||||
})
|
||||
const { rows, pagination, extra, info } = await quotas.addQuery<any>(
|
||||
runFn,
|
||||
{
|
||||
datasourceId: datasource._id,
|
||||
}
|
||||
)
|
||||
// remove the raw from execution incase transformer being used to hide data
|
||||
if (extra?.raw) {
|
||||
delete extra.raw
|
||||
|
|
|
@ -25,7 +25,7 @@ export async function patch(ctx: any): Promise<any> {
|
|||
return save(ctx)
|
||||
}
|
||||
try {
|
||||
const { row, table } = await quotas.addQuery(
|
||||
const { row, table } = await quotas.addQuery<any>(
|
||||
() => pickApi(tableId).patch(ctx),
|
||||
{
|
||||
datasourceId: tableId,
|
||||
|
@ -104,7 +104,7 @@ export async function destroy(ctx: any) {
|
|||
const tableId = utils.getTableId(ctx)
|
||||
let response, row
|
||||
if (inputs.rows) {
|
||||
let { rows } = await quotas.addQuery(
|
||||
let { rows } = await quotas.addQuery<any>(
|
||||
() => pickApi(tableId).bulkDestroy(ctx),
|
||||
{
|
||||
datasourceId: tableId,
|
||||
|
@ -117,7 +117,7 @@ export async function destroy(ctx: any) {
|
|||
gridSocket?.emitRowDeletion(ctx, row._id)
|
||||
}
|
||||
} else {
|
||||
let resp = await quotas.addQuery(() => pickApi(tableId).destroy(ctx), {
|
||||
let resp = await quotas.addQuery<any>(() => pickApi(tableId).destroy(ctx), {
|
||||
datasourceId: tableId,
|
||||
})
|
||||
await quotas.removeRow()
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import Router from "@koa/router"
|
||||
import * as controller from "../controllers/debug"
|
||||
import authorized from "../../middleware/authorized"
|
||||
import { permissions } from "@budibase/backend-core"
|
||||
|
||||
const router: Router = new Router()
|
||||
|
||||
router.get(
|
||||
"/api/debug/diagnostics",
|
||||
authorized(permissions.BUILDER),
|
||||
controller.systemDebugInfo
|
||||
)
|
||||
|
||||
export default router
|
|
@ -25,6 +25,7 @@ import devRoutes from "./dev"
|
|||
import migrationRoutes from "./migrations"
|
||||
import pluginRoutes from "./plugin"
|
||||
import opsRoutes from "./ops"
|
||||
import debugRoutes from "./debug"
|
||||
import Router from "@koa/router"
|
||||
import { api as pro } from "@budibase/pro"
|
||||
|
||||
|
@ -63,6 +64,7 @@ export const mainRoutes: Router[] = [
|
|||
migrationRoutes,
|
||||
pluginRoutes,
|
||||
opsRoutes,
|
||||
debugRoutes,
|
||||
scheduleRoutes,
|
||||
environmentVariableRoutes,
|
||||
// these need to be handled last as they still use /api/:tableId
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
|
||||
const setup = require("./utilities")
|
||||
import os from "os"
|
||||
|
||||
jest.mock("process", () => ({
|
||||
arch: "arm64",
|
||||
version: "v14.20.1",
|
||||
platform: "darwin",
|
||||
}))
|
||||
|
||||
describe("/component", () => {
|
||||
let request = setup.getRequest()
|
||||
let config = setup.getConfig()
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
os.cpus = () => [
|
||||
{
|
||||
model: "test",
|
||||
speed: 12323,
|
||||
times: {
|
||||
user: 0,
|
||||
nice: 0,
|
||||
sys: 0,
|
||||
idle: 0,
|
||||
irq: 0,
|
||||
},
|
||||
},
|
||||
]
|
||||
os.uptime = () => 123123123123
|
||||
os.totalmem = () => 10000000000
|
||||
})
|
||||
|
||||
describe("/api/debug", () => {
|
||||
it("should return debug information to the frontend", async () => {
|
||||
const res = await request
|
||||
.get(`/api/debug/diagnostics`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body).toEqual({
|
||||
budibaseVersion: "0.0.0",
|
||||
cpuArch: "arm64",
|
||||
cpuCores: 1,
|
||||
cpuInfo: "test",
|
||||
hosting: "docker-compose",
|
||||
nodeVersion: "v14.20.1",
|
||||
platform: "darwin",
|
||||
totalMemory: "9.313225746154785GB",
|
||||
uptime: "1425036 day(s), 3 hour(s), 32 minute(s)",
|
||||
})
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await checkBuilderEndpoint({
|
||||
config,
|
||||
method: "GET",
|
||||
url: `/api/debug/diagnostics`,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -18,6 +18,7 @@ module.exports.doesContainString = templates.doesContainString
|
|||
module.exports.disableEscaping = templates.disableEscaping
|
||||
module.exports.findHBSBlocks = templates.findHBSBlocks
|
||||
module.exports.convertToJS = templates.convertToJS
|
||||
module.exports.FIND_ANY_HBS_REGEX = templates.FIND_ANY_HBS_REGEX
|
||||
|
||||
if (!process.env.NO_JS) {
|
||||
const { VM } = require("vm2")
|
||||
|
@ -28,7 +29,7 @@ if (!process.env.NO_JS) {
|
|||
setJSRunner((js, context) => {
|
||||
const vm = new VM({
|
||||
sandbox: context,
|
||||
timeout: 1000
|
||||
timeout: 1000,
|
||||
})
|
||||
return vm.run(js)
|
||||
})
|
||||
|
|
|
@ -389,3 +389,5 @@ module.exports.convertToJS = hbs => {
|
|||
js += "`;"
|
||||
return `${varBlock}${js}`
|
||||
}
|
||||
|
||||
module.exports.FIND_ANY_HBS_REGEX = FIND_ANY_HBS_REGEX
|
||||
|
|
|
@ -20,6 +20,7 @@ export const doesContainString = templates.doesContainString
|
|||
export const disableEscaping = templates.disableEscaping
|
||||
export const findHBSBlocks = templates.findHBSBlocks
|
||||
export const convertToJS = templates.convertToJS
|
||||
export const FIND_ANY_HBS_REGEX = templates.FIND_ANY_HBS_REGEX
|
||||
|
||||
if (process && !process.env.NO_JS) {
|
||||
/**
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { Account } from "../../documents"
|
||||
import { Hosting } from "../../sdk"
|
||||
|
||||
export interface CreateAccountRequest {
|
||||
|
@ -11,3 +12,11 @@ export interface CreateAccountRequest {
|
|||
name?: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface SearchAccountsRequest {
|
||||
// one or the other - not both
|
||||
email?: string
|
||||
tenantId?: string
|
||||
}
|
||||
|
||||
export type SearchAccountsResponse = Account[]
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
export interface GetDiagnosticsResponse {
|
||||
budibaseVersion: string
|
||||
hosting: string
|
||||
nodeVersion: string
|
||||
platform: string
|
||||
cpuArch: string
|
||||
cpuCores: number
|
||||
cpuInfo: string
|
||||
totalMemory: string
|
||||
uptime: string
|
||||
}
|
|
@ -2,6 +2,7 @@ export * from "./analytics"
|
|||
export * from "./auth"
|
||||
export * from "./user"
|
||||
export * from "./errors"
|
||||
export * from "./debug"
|
||||
export * from "./schedule"
|
||||
export * from "./system"
|
||||
export * from "./app"
|
||||
|
|
|
@ -5,6 +5,9 @@ const config: Config.InitialOptions = {
|
|||
setupFiles: ["./src/jest/jestSetup.ts"],
|
||||
setupFilesAfterEnv: ["./src/jest/jest.extends.ts"],
|
||||
testEnvironment: "node",
|
||||
transform: {
|
||||
"^.+\\.ts?$": "@swc/jest",
|
||||
},
|
||||
globalSetup: "./src/jest/globalSetup.ts",
|
||||
globalTeardown: "./src/jest/globalTeardown.ts",
|
||||
moduleNameMapper: {
|
||||
|
|
|
@ -15,8 +15,10 @@
|
|||
"test:watch": "yarn run test --watch",
|
||||
"test:debug": "DEBUG=1 yarn run test",
|
||||
"test:notify": "node scripts/testResultsWebhook",
|
||||
"test:smoke": "yarn run test --testPathIgnorePatterns=/.+\\.integration\\.spec\\.ts",
|
||||
"test:ci": "start-server-and-test dev:built http://localhost:4001/health test:smoke",
|
||||
"test:cloud:prod": "yarn run test --testPathIgnorePatterns=\\.integration\\.",
|
||||
"test:cloud:qa": "yarn run test",
|
||||
"test:self:ci": "yarn run test --testPathIgnorePatterns=\\.integration\\. \\.cloud\\.",
|
||||
"serve:test:self:ci": "start-server-and-test dev:built http://localhost:4001/health test:self:ci",
|
||||
"serve": "start-server-and-test dev:built http://localhost:4001/health",
|
||||
"dev:built": "cd ../ && yarn dev:built"
|
||||
},
|
||||
|
@ -30,6 +32,8 @@
|
|||
"jest": "29.0.0",
|
||||
"prettier": "2.7.1",
|
||||
"start-server-and-test": "1.14.0",
|
||||
"@swc/core": "^1.3.25",
|
||||
"@swc/jest": "^0.2.24",
|
||||
"timekeeper": "2.2.0",
|
||||
"ts-jest": "29.0.0",
|
||||
"ts-node": "10.8.1",
|
||||
|
|
|
@ -12,6 +12,8 @@ function init() {
|
|||
BB_ADMIN_USER_EMAIL: "admin",
|
||||
BB_ADMIN_USER_PASSWORD: "admin",
|
||||
LOG_LEVEL: "info",
|
||||
JEST_TIMEOUT: "60000",
|
||||
DISABLE_PINO_LOGGER: "1",
|
||||
}
|
||||
let envFile = ""
|
||||
Object.keys(envFileJson).forEach(key => {
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import AccountInternalAPIClient from "./AccountInternalAPIClient"
|
||||
import { AccountAPI, LicenseAPI } from "./apis"
|
||||
import { AccountAPI, LicenseAPI, AuthAPI } from "./apis"
|
||||
import { State } from "../../types"
|
||||
|
||||
export default class AccountInternalAPI {
|
||||
client: AccountInternalAPIClient
|
||||
|
||||
auth: AuthAPI
|
||||
accounts: AccountAPI
|
||||
licenses: LicenseAPI
|
||||
|
||||
constructor(state: State) {
|
||||
this.client = new AccountInternalAPIClient(state)
|
||||
this.auth = new AuthAPI(this.client)
|
||||
this.accounts = new AccountAPI(this.client)
|
||||
this.licenses = new LicenseAPI(this.client)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { Response } from "node-fetch"
|
||||
import env from "../../environment"
|
||||
import fetch, { HeadersInit } from "node-fetch"
|
||||
import { State } from "../../types"
|
||||
import { Header } from "@budibase/backend-core"
|
||||
|
||||
type APIMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
|
||||
|
||||
|
@ -28,7 +30,7 @@ export default class AccountInternalAPIClient {
|
|||
|
||||
apiCall =
|
||||
(method: APIMethod) =>
|
||||
async (url = "", options: ApiOptions = {}) => {
|
||||
async (url = "", options: ApiOptions = {}): Promise<[Response, any]> => {
|
||||
const requestOptions = {
|
||||
method,
|
||||
body: JSON.stringify(options.body),
|
||||
|
@ -46,7 +48,7 @@ export default class AccountInternalAPIClient {
|
|||
if (options.internal) {
|
||||
requestOptions.headers = {
|
||||
...requestOptions.headers,
|
||||
...{ "x-budibase-api-key": env.ACCOUNT_PORTAL_API_KEY },
|
||||
...{ [Header.API_KEY]: env.ACCOUNT_PORTAL_API_KEY },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,75 +1,117 @@
|
|||
import { Response } from "node-fetch"
|
||||
import { Account, CreateAccountRequest } from "@budibase/types"
|
||||
import {
|
||||
Account,
|
||||
CreateAccountRequest,
|
||||
SearchAccountsRequest,
|
||||
SearchAccountsResponse,
|
||||
} from "@budibase/types"
|
||||
import AccountInternalAPIClient from "../AccountInternalAPIClient"
|
||||
import { APIRequestOpts } from "../../../types"
|
||||
import { Header } from "@budibase/backend-core"
|
||||
import BaseAPI from "./BaseAPI"
|
||||
|
||||
export default class AccountAPI {
|
||||
export default class AccountAPI extends BaseAPI {
|
||||
client: AccountInternalAPIClient
|
||||
|
||||
constructor(client: AccountInternalAPIClient) {
|
||||
super()
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async validateEmail(
|
||||
email: string,
|
||||
opts: APIRequestOpts = { doExpect: true }
|
||||
): Promise<Response> {
|
||||
const [response, json] = await this.client.post(
|
||||
`/api/accounts/validate/email`,
|
||||
{
|
||||
async validateEmail(email: string, opts: APIRequestOpts = { status: 200 }) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.post(`/api/accounts/validate/email`, {
|
||||
body: { email },
|
||||
}
|
||||
)
|
||||
if (opts.doExpect) {
|
||||
expect(response).toHaveStatusCode(200)
|
||||
}
|
||||
return response
|
||||
})
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async validateTenantId(
|
||||
tenantId: string,
|
||||
opts: APIRequestOpts = { doExpect: true }
|
||||
): Promise<Response> {
|
||||
const [response, json] = await this.client.post(
|
||||
`/api/accounts/validate/tenantId`,
|
||||
{
|
||||
opts: APIRequestOpts = { status: 200 }
|
||||
) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.post(`/api/accounts/validate/tenantId`, {
|
||||
body: { tenantId },
|
||||
}
|
||||
)
|
||||
if (opts.doExpect) {
|
||||
expect(response).toHaveStatusCode(200)
|
||||
}
|
||||
return response
|
||||
})
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async create(
|
||||
body: CreateAccountRequest,
|
||||
opts: APIRequestOpts = { doExpect: true }
|
||||
opts: APIRequestOpts & { autoVerify: boolean } = {
|
||||
status: 201,
|
||||
autoVerify: false,
|
||||
}
|
||||
): Promise<[Response, Account]> {
|
||||
const headers = {
|
||||
"no-verify": "1",
|
||||
}
|
||||
const [response, json] = await this.client.post(`/api/accounts`, {
|
||||
body,
|
||||
headers,
|
||||
})
|
||||
if (opts.doExpect) {
|
||||
expect(response).toHaveStatusCode(201)
|
||||
}
|
||||
return [response, json]
|
||||
return this.doRequest(() => {
|
||||
const headers = {
|
||||
"no-verify": opts.autoVerify ? "1" : "0",
|
||||
}
|
||||
return this.client.post(`/api/accounts`, {
|
||||
body,
|
||||
headers,
|
||||
})
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async delete(accountID: string) {
|
||||
const [response, json] = await this.client.del(
|
||||
`/api/accounts/${accountID}`,
|
||||
{
|
||||
async delete(accountID: string, opts: APIRequestOpts = { status: 204 }) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.del(`/api/accounts/${accountID}`, {
|
||||
internal: true,
|
||||
})
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async deleteCurrentAccount(opts: APIRequestOpts = { status: 204 }) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.del(`/api/accounts`)
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async verifyAccount(
|
||||
verificationCode: string,
|
||||
opts: APIRequestOpts = { status: 200 }
|
||||
) {
|
||||
return this.doRequest(() => {
|
||||
return this.client.post(`/api/accounts/verify`, {
|
||||
body: { verificationCode },
|
||||
})
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async sendVerificationEmail(
|
||||
email: string,
|
||||
opts: APIRequestOpts = { status: 200 }
|
||||
): Promise<[Response, string]> {
|
||||
return this.doRequest(async () => {
|
||||
const [response] = await this.client.post(`/api/accounts/verify/send`, {
|
||||
body: { email },
|
||||
headers: {
|
||||
[Header.RETURN_VERIFICATION_CODE]: "1",
|
||||
},
|
||||
})
|
||||
const code = response.headers.get(Header.VERIFICATION_CODE)
|
||||
return [response, code]
|
||||
}, opts)
|
||||
}
|
||||
|
||||
async search(
|
||||
searchType: string,
|
||||
search: "email" | "tenantId",
|
||||
opts: APIRequestOpts = { status: 200 }
|
||||
): Promise<[Response, SearchAccountsResponse]> {
|
||||
return this.doRequest(() => {
|
||||
let body: SearchAccountsRequest = {}
|
||||
if (search === "email") {
|
||||
body.email = searchType
|
||||
} else if (search === "tenantId") {
|
||||
body.tenantId = searchType
|
||||
}
|
||||
)
|
||||
// can't use expect here due to use in global teardown
|
||||
if (response.status !== 204) {
|
||||
throw new Error(`Could not delete accountId=${accountID}`)
|
||||
}
|
||||
return response
|
||||
return this.client.post(`/api/accounts/search`, {
|
||||
body,
|
||||
internal: true,
|
||||
})
|
||||
}, opts)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { Response } from "node-fetch"
|
||||
import AccountInternalAPIClient from "../AccountInternalAPIClient"
|
||||
import { APIRequestOpts } from "../../../types"
|
||||
import BaseAPI from "./BaseAPI"
|
||||
|
||||
export default class AuthAPI extends BaseAPI {
|
||||
client: AccountInternalAPIClient
|
||||
|
||||
constructor(client: AccountInternalAPIClient) {
|
||||
super()
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async login(
|
||||
email: string,
|
||||
password: string,
|
||||
opts: APIRequestOpts = { doExpect: true, status: 200 }
|
||||
): Promise<[Response, string]> {
|
||||
return this.doRequest(async () => {
|
||||
const [res] = await this.client.post(`/api/auth/login`, {
|
||||
body: {
|
||||
email: email,
|
||||
password: password,
|
||||
},
|
||||
})
|
||||
const cookie = res.headers.get("set-cookie")
|
||||
return [res, cookie]
|
||||
}, opts)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { Response } from "node-fetch"
|
||||
import { APIRequestOpts } from "../../../types"
|
||||
|
||||
export default class BaseAPI {
|
||||
async doRequest(
|
||||
request: () => Promise<[Response, any]>,
|
||||
opts: APIRequestOpts
|
||||
): Promise<[Response, any]> {
|
||||
const [response, body] = await request()
|
||||
|
||||
// do expect on by default
|
||||
if (opts.doExpect === undefined) {
|
||||
opts.doExpect = true
|
||||
}
|
||||
if (opts.doExpect && opts.status) {
|
||||
expect(response).toHaveStatusCode(opts.status)
|
||||
}
|
||||
return [response, body]
|
||||
}
|
||||
}
|
|
@ -1,31 +1,27 @@
|
|||
import AccountInternalAPIClient from "../AccountInternalAPIClient"
|
||||
import { Account, UpdateLicenseRequest } from "@budibase/types"
|
||||
import { Response } from "node-fetch"
|
||||
import BaseAPI from "./BaseAPI"
|
||||
import { APIRequestOpts } from "../../../types"
|
||||
|
||||
export default class LicenseAPI {
|
||||
export default class LicenseAPI extends BaseAPI {
|
||||
client: AccountInternalAPIClient
|
||||
|
||||
constructor(client: AccountInternalAPIClient) {
|
||||
super()
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async updateLicense(
|
||||
accountId: string,
|
||||
body: UpdateLicenseRequest
|
||||
body: UpdateLicenseRequest,
|
||||
opts: APIRequestOpts = { status: 200 }
|
||||
): Promise<[Response, Account]> {
|
||||
const [response, json] = await this.client.put(
|
||||
`/api/accounts/${accountId}/license`,
|
||||
{
|
||||
return this.doRequest(() => {
|
||||
return this.client.put(`/api/accounts/${accountId}/license`, {
|
||||
body,
|
||||
internal: true,
|
||||
}
|
||||
)
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(
|
||||
`Could not update license for accountId=${accountId}: ${response.status}`
|
||||
)
|
||||
}
|
||||
return [response, json]
|
||||
})
|
||||
}, opts)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export { default as AuthAPI } from "./AuthAPI"
|
||||
export { default as AccountAPI } from "./AccountAPI"
|
||||
export { default as LicenseAPI } from "./LicenseAPI"
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { AccountInternalAPI } from "../api"
|
||||
import { BudibaseTestConfiguration } from "../../shared"
|
||||
|
||||
export default class TestConfiguration<T> extends BudibaseTestConfiguration {
|
||||
// apis
|
||||
api: AccountInternalAPI
|
||||
|
||||
context: T
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.api = new AccountInternalAPI(this.state)
|
||||
this.context = <T>{}
|
||||
}
|
||||
|
||||
async beforeAll() {
|
||||
await super.beforeAll()
|
||||
await this.setApiKey()
|
||||
}
|
||||
|
||||
async afterAll() {
|
||||
await super.afterAll()
|
||||
}
|
||||
|
||||
async setApiKey() {
|
||||
const apiKeyResponse = await this.internalApi.self.getApiKey()
|
||||
this.state.apiKey = apiKeyResponse.apiKey
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { generator } from "../../shared"
|
||||
import { Hosting, CreateAccountRequest } from "@budibase/types"
|
||||
|
||||
// TODO: Refactor me to central location
|
||||
export const generateAccount = (): CreateAccountRequest => {
|
||||
const uuid = generator.guid()
|
||||
|
||||
const email = `${uuid}@budibase.com`
|
||||
const tenant = `tenant${uuid.replace(/-/g, "")}`
|
||||
|
||||
return {
|
||||
email,
|
||||
hosting: Hosting.CLOUD,
|
||||
name: email,
|
||||
password: uuid,
|
||||
profession: "software_engineer",
|
||||
size: "10+",
|
||||
tenantId: tenant,
|
||||
tenantName: tenant,
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * as accounts from "./accounts"
|
|
@ -0,0 +1,29 @@
|
|||
import TestConfiguration from "../../config/TestConfiguration"
|
||||
import * as fixtures from "../../fixtures"
|
||||
import { generator } from "../../../shared"
|
||||
|
||||
describe("Account Internal Operations", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.beforeAll()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await config.afterAll()
|
||||
})
|
||||
|
||||
it("performs account deletion by ID", async () => {
|
||||
// Deleting by unknown id doesn't work
|
||||
const accountId = generator.string()
|
||||
await config.api.accounts.delete(accountId, { status: 404 })
|
||||
|
||||
// Create new account
|
||||
const [_, account] = await config.api.accounts.create({
|
||||
...fixtures.accounts.generateAccount(),
|
||||
})
|
||||
|
||||
// New account can be deleted
|
||||
await config.api.accounts.delete(account.accountId)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,92 @@
|
|||
import TestConfiguration from "../../config/TestConfiguration"
|
||||
import * as fixtures from "../../fixtures"
|
||||
import { generator } from "../../../shared"
|
||||
|
||||
describe("Accounts", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.beforeAll()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await config.afterAll()
|
||||
})
|
||||
|
||||
it("performs signup and deletion flow", async () => {
|
||||
await config.doInNewState(async () => {
|
||||
// Create account
|
||||
const createAccountRequest = fixtures.accounts.generateAccount()
|
||||
const email = createAccountRequest.email
|
||||
const tenantId = createAccountRequest.tenantId
|
||||
|
||||
// Validation - email and tenant ID allowed
|
||||
await config.api.accounts.validateEmail(email)
|
||||
await config.api.accounts.validateTenantId(tenantId)
|
||||
|
||||
// Create unverified account
|
||||
await config.api.accounts.create(createAccountRequest)
|
||||
|
||||
// Validation - email and tenant ID no longer valid
|
||||
await config.api.accounts.validateEmail(email, { status: 400 })
|
||||
await config.api.accounts.validateTenantId(tenantId, { status: 400 })
|
||||
|
||||
// Attempt to log in using unverified account
|
||||
await config.loginAsAccount(createAccountRequest, { status: 400 })
|
||||
|
||||
// Re-send verification email to get access to code
|
||||
const [_, code] = await config.accountsApi.accounts.sendVerificationEmail(
|
||||
email
|
||||
)
|
||||
|
||||
// Send the verification request
|
||||
await config.accountsApi.accounts.verifyAccount(code!)
|
||||
|
||||
// Can now log in to the account
|
||||
await config.loginAsAccount(createAccountRequest)
|
||||
|
||||
// Delete account
|
||||
await config.api.accounts.deleteCurrentAccount()
|
||||
|
||||
// Can't log in
|
||||
await config.loginAsAccount(createAccountRequest, { status: 403 })
|
||||
})
|
||||
})
|
||||
|
||||
describe("Searching accounts", () => {
|
||||
it("search by tenant ID", async () => {
|
||||
const tenantId = generator.string()
|
||||
|
||||
// Empty result
|
||||
const [_, emptyBody] = await config.api.accounts.search(
|
||||
tenantId,
|
||||
"tenantId"
|
||||
)
|
||||
expect(emptyBody.length).toBe(0)
|
||||
|
||||
// Hit result
|
||||
const [hitRes, hitBody] = await config.api.accounts.search(
|
||||
config.state.tenantId!,
|
||||
"tenantId"
|
||||
)
|
||||
expect(hitBody.length).toBe(1)
|
||||
expect(hitBody[0].tenantId).toBe(config.state.tenantId)
|
||||
})
|
||||
|
||||
it("searches by email", async () => {
|
||||
const email = generator.email()
|
||||
|
||||
// Empty result
|
||||
const [_, emptyBody] = await config.api.accounts.search(email, "email")
|
||||
expect(emptyBody.length).toBe(0)
|
||||
|
||||
// Hit result
|
||||
const [hitRes, hitBody] = await config.api.accounts.search(
|
||||
config.state.email!,
|
||||
"email"
|
||||
)
|
||||
expect(hitBody.length).toBe(1)
|
||||
expect(hitBody[0].email).toBe(config.state.email)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,5 +1,4 @@
|
|||
process.env.DISABLE_PINO_LOGGER = "1"
|
||||
import { DEFAULT_TENANT_ID, logging } from "@budibase/backend-core"
|
||||
import { DEFAULT_TENANT_ID } from "@budibase/backend-core"
|
||||
import { AccountInternalAPI } from "../account-api"
|
||||
import * as fixtures from "../internal-api/fixtures"
|
||||
import { BudibaseInternalAPI } from "../internal-api"
|
||||
|
@ -7,10 +6,6 @@ import { Account, CreateAccountRequest, Feature } from "@budibase/types"
|
|||
import env from "../environment"
|
||||
import { APIRequestOpts } from "../types"
|
||||
|
||||
// turn off or on context logging i.e. tenantId, appId etc
|
||||
// it's not applicable for the qa run
|
||||
logging.LOG_CONTEXT = false
|
||||
|
||||
const accountsApi = new AccountInternalAPI({})
|
||||
const internalApi = new BudibaseInternalAPI({})
|
||||
|
||||
|
@ -23,7 +18,10 @@ async function createAccount(): Promise<[CreateAccountRequest, Account]> {
|
|||
const account = fixtures.accounts.generateAccount()
|
||||
await accountsApi.accounts.validateEmail(account.email, API_OPTS)
|
||||
await accountsApi.accounts.validateTenantId(account.tenantId, API_OPTS)
|
||||
const [res, newAccount] = await accountsApi.accounts.create(account, API_OPTS)
|
||||
const [res, newAccount] = await accountsApi.accounts.create(account, {
|
||||
...API_OPTS,
|
||||
autoVerify: true,
|
||||
})
|
||||
await updateLicense(newAccount.accountId)
|
||||
return [account, newAccount]
|
||||
}
|
||||
|
@ -31,25 +29,34 @@ async function createAccount(): Promise<[CreateAccountRequest, Account]> {
|
|||
const UNLIMITED = { value: -1 }
|
||||
|
||||
async function updateLicense(accountId: string) {
|
||||
await accountsApi.licenses.updateLicense(accountId, {
|
||||
overrides: {
|
||||
// add all features
|
||||
features: Object.values(Feature),
|
||||
quotas: {
|
||||
usage: {
|
||||
monthly: {
|
||||
automations: UNLIMITED,
|
||||
},
|
||||
static: {
|
||||
rows: UNLIMITED,
|
||||
users: UNLIMITED,
|
||||
userGroups: UNLIMITED,
|
||||
plugins: UNLIMITED,
|
||||
const [response] = await accountsApi.licenses.updateLicense(
|
||||
accountId,
|
||||
{
|
||||
overrides: {
|
||||
// add all features
|
||||
features: Object.values(Feature),
|
||||
quotas: {
|
||||
usage: {
|
||||
monthly: {
|
||||
automations: UNLIMITED,
|
||||
},
|
||||
static: {
|
||||
rows: UNLIMITED,
|
||||
users: UNLIMITED,
|
||||
userGroups: UNLIMITED,
|
||||
plugins: UNLIMITED,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
{ doExpect: false }
|
||||
)
|
||||
if (response.status !== 200) {
|
||||
throw new Error(
|
||||
`Could not update license for accountId=${accountId}: ${response.status}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function loginAsAdmin() {
|
||||
|
@ -68,8 +75,7 @@ async function loginAsAdmin() {
|
|||
}
|
||||
|
||||
async function loginAsAccount(account: CreateAccountRequest) {
|
||||
const [res, cookie] = await internalApi.auth.login(
|
||||
account.tenantId,
|
||||
const [res, cookie] = await accountsApi.auth.login(
|
||||
account.email,
|
||||
account.password,
|
||||
API_OPTS
|
||||
|
@ -90,6 +96,8 @@ async function setup() {
|
|||
// @ts-ignore
|
||||
global.qa.tenantId = account.tenantId
|
||||
// @ts-ignore
|
||||
global.qa.email = account.email
|
||||
// @ts-ignore
|
||||
global.qa.accountId = newAccount.accountId
|
||||
await loginAsAccount(account)
|
||||
} else {
|
||||
|
|
|
@ -10,8 +10,13 @@ const API_OPTS: APIRequestOpts = { doExpect: false }
|
|||
async function deleteAccount() {
|
||||
// @ts-ignore
|
||||
const accountID = global.qa.accountId
|
||||
// can't run 'expect' blocks in teardown
|
||||
await accountsApi.accounts.delete(accountID)
|
||||
|
||||
const [response] = await accountsApi.accounts.delete(accountID, {
|
||||
doExpect: false,
|
||||
})
|
||||
if (response.status !== 204) {
|
||||
throw new Error(`status: ${response.status} not equal to expected: 201`)
|
||||
}
|
||||
}
|
||||
|
||||
async function teardown() {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { logging } from "@budibase/backend-core"
|
||||
logging.LOG_CONTEXT = false
|
||||
|
||||
jest.retryTimes(2)
|
||||
jest.setTimeout(60000)
|
||||
const envTimeout = process.env.JEST_TIMEOUT
|
||||
const timeout = envTimeout && parseInt(envTimeout)
|
||||
jest.setTimeout(timeout || 60000)
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { BudibaseInternalAPI } from "../internal-api"
|
||||
import { AccountInternalAPI } from "../account-api"
|
||||
import { CreateAppRequest, State } from "../types"
|
||||
import { APIRequestOpts, CreateAppRequest, State } from "../types"
|
||||
import * as fixtures from "../internal-api/fixtures"
|
||||
import { CreateAccountRequest } from "@budibase/types"
|
||||
|
||||
export default class BudibaseTestConfiguration {
|
||||
// apis
|
||||
|
@ -23,6 +24,8 @@ export default class BudibaseTestConfiguration {
|
|||
// @ts-ignore
|
||||
this.state.tenantId = global.qa.tenantId
|
||||
// @ts-ignore
|
||||
this.state.email = global.qa.email
|
||||
// @ts-ignore
|
||||
this.state.cookie = global.qa.authCookie
|
||||
}
|
||||
|
||||
|
@ -40,10 +43,49 @@ export default class BudibaseTestConfiguration {
|
|||
|
||||
// AUTH
|
||||
|
||||
async doInNewState(task: () => Promise<any>) {
|
||||
return this.doWithState(task, {})
|
||||
}
|
||||
|
||||
async doWithState(task: () => Promise<any>, state: State) {
|
||||
const original = { ...this.state }
|
||||
|
||||
// override the state
|
||||
this.state.apiKey = state.apiKey
|
||||
this.state.appId = state.appId
|
||||
this.state.cookie = state.cookie
|
||||
this.state.tableId = state.tableId
|
||||
this.state.tenantId = state.tenantId
|
||||
this.state.email = state.email
|
||||
|
||||
await task()
|
||||
|
||||
// restore the state
|
||||
this.state.apiKey = original.apiKey
|
||||
this.state.appId = original.appId
|
||||
this.state.cookie = original.cookie
|
||||
this.state.tableId = original.tableId
|
||||
this.state.tenantId = original.tenantId
|
||||
this.state.email = original.email
|
||||
}
|
||||
|
||||
async loginAsAccount(
|
||||
account: CreateAccountRequest,
|
||||
opts: APIRequestOpts = {}
|
||||
) {
|
||||
const [_, cookie] = await this.accountsApi.auth.login(
|
||||
account.email,
|
||||
account.password,
|
||||
opts
|
||||
)
|
||||
this.state.cookie = cookie
|
||||
}
|
||||
|
||||
async login(email: string, password: string, tenantId?: string) {
|
||||
if (!tenantId && this.state.tenantId) {
|
||||
tenantId = this.state.tenantId
|
||||
} else {
|
||||
}
|
||||
if (!tenantId) {
|
||||
throw new Error("Could not determine tenant id")
|
||||
}
|
||||
const [res, cookie] = await this.internalApi.auth.login(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export interface APIRequestOpts {
|
||||
// in some cases we need to bypass the expect assertion in an api call
|
||||
// e.g. during global setup where jest is not available
|
||||
doExpect: boolean
|
||||
doExpect?: boolean
|
||||
status?: number
|
||||
}
|
||||
|
|
|
@ -4,4 +4,5 @@ export interface State {
|
|||
cookie?: string
|
||||
tableId?: string
|
||||
tenantId?: string
|
||||
email?: string
|
||||
}
|
||||
|
|
|
@ -455,6 +455,13 @@
|
|||
slash "^3.0.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
"@jest/create-cache-key-function@^27.4.2":
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@jest/create-cache-key-function/-/create-cache-key-function-27.5.1.tgz#7448fae15602ea95c828f5eceed35c202a820b31"
|
||||
integrity sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ==
|
||||
dependencies:
|
||||
"@jest/types" "^27.5.1"
|
||||
|
||||
"@jest/environment@^29.5.0":
|
||||
version "29.5.0"
|
||||
resolved "https://registry.npmjs.org/@jest/environment/-/environment-29.5.0.tgz"
|
||||
|
@ -589,6 +596,17 @@
|
|||
slash "^3.0.0"
|
||||
write-file-atomic "^4.0.2"
|
||||
|
||||
"@jest/types@^27.5.1":
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80"
|
||||
integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==
|
||||
dependencies:
|
||||
"@types/istanbul-lib-coverage" "^2.0.0"
|
||||
"@types/istanbul-reports" "^3.0.0"
|
||||
"@types/node" "*"
|
||||
"@types/yargs" "^16.0.0"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@jest/types@^29.0.0", "@jest/types@^29.5.0":
|
||||
version "29.5.0"
|
||||
resolved "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz"
|
||||
|
@ -738,6 +756,80 @@
|
|||
dependencies:
|
||||
"@sinonjs/commons" "^2.0.0"
|
||||
|
||||
"@swc/core-darwin-arm64@1.3.70":
|
||||
version "1.3.70"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.70.tgz#056ac6899e22cb7f7be21388d4d938ca5123a72b"
|
||||
integrity sha512-31+mcl0dgdRHvZRjhLOK9V6B+qJ7nxDZYINr9pBlqGWxknz37Vld5KK19Kpr79r0dXUZvaaelLjCnJk9dA2PcQ==
|
||||
|
||||
"@swc/core-darwin-x64@1.3.70":
|
||||
version "1.3.70"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.3.70.tgz#3945814de6fadbee5b46cb2a3422353acb420c5c"
|
||||
integrity sha512-GMFJ65E18zQC80t0os+TZvI+8lbRuitncWVge/RXmXbVLPRcdykP4EJ87cqzcG5Ah0z18/E0T+ixD6jHRisrYQ==
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf@1.3.70":
|
||||
version "1.3.70"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.70.tgz#7960e54ede1af75a7ef99ee53febf37fea6269a8"
|
||||
integrity sha512-wjhCwS8LCiAq2VedF1b4Bryyw68xZnfMED4pLRazAl8BaUlDFANfRBORNunxlfHQj4V3x39IaiLgCZRHMdzXBg==
|
||||
|
||||
"@swc/core-linux-arm64-gnu@1.3.70":
|
||||
version "1.3.70"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.70.tgz#df9654e5040bbeb1619739756a7f50100e38ace8"
|
||||
integrity sha512-9D/Rx67cAOnMiexvCqARxvhj7coRajTp5HlJHuf+rfwMqI2hLhpO9/pBMQxBUAWxODO/ksQ/OF+GJRjmtWw/2A==
|
||||
|
||||
"@swc/core-linux-arm64-musl@1.3.70":
|
||||
version "1.3.70"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.70.tgz#2c2aab5a136c7eb409ddc9cdc4f947a68fd74493"
|
||||
integrity sha512-gkjxBio7XD+1GlQVVyPP/qeFkLu83VhRHXaUrkNYpr5UZG9zZurBERT9nkS6Y+ouYh+Q9xmw57aIyd2KvD2zqQ==
|
||||
|
||||
"@swc/core-linux-x64-gnu@1.3.70":
|
||||
version "1.3.70"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.70.tgz#774351532b154ed36a5c6d14b647e7a8ab510028"
|
||||
integrity sha512-/nCly+V4xfMVwfEUoLLAukxUSot/RcSzsf6GdsGTjFcrp5sZIntAjokYRytm3VT1c2TK321AfBorsi9R5w8Y7Q==
|
||||
|
||||
"@swc/core-linux-x64-musl@1.3.70":
|
||||
version "1.3.70"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.70.tgz#c0b1b4ad5f4ef187eaa093589a4933ecb6836546"
|
||||
integrity sha512-HoOsPJbt361KGKaivAK0qIiYARkhzlxeAfvF5NlnKxkIMOZpQ46Lwj3tR0VWohKbrhS+cYKFlVuDi5XnDkx0XA==
|
||||
|
||||
"@swc/core-win32-arm64-msvc@1.3.70":
|
||||
version "1.3.70"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.70.tgz#8640267ce3959db0e7e682103677a5e0500b5ea7"
|
||||
integrity sha512-hm4IBK/IaRil+aj1cWU6f0GyAdHpw/Jr5nyFYLM2c/tt7w2t5hgb8NjzM2iM84lOClrig1fG6edj2vCF1dFzNQ==
|
||||
|
||||
"@swc/core-win32-ia32-msvc@1.3.70":
|
||||
version "1.3.70"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.70.tgz#f95d5656622f5a963bc0125da9fda84cf40faa8d"
|
||||
integrity sha512-5cgKUKIT/9Fp5fCA+zIjYCQ4dSvjFYOeWGZR3QiTXGkC4bGa1Ji9SEPyeIAX0iruUnKjYaZB9RvHK2tNn7RLrQ==
|
||||
|
||||
"@swc/core-win32-x64-msvc@1.3.70":
|
||||
version "1.3.70"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.70.tgz#5b3acddb96fdf60df089b837061915cb4be94eaa"
|
||||
integrity sha512-LE8lW46+TQBzVkn2mHBlk8DIElPIZ2dO5P8AbJiARNBAnlqQWu67l9gWM89UiZ2l33J2cI37pHzON3tKnT8f9g==
|
||||
|
||||
"@swc/core@^1.3.25":
|
||||
version "1.3.70"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.3.70.tgz#f5ddc6fe6add7a99f5b94d2214ad0d8527d11479"
|
||||
integrity sha512-LWVWlEDLlOD25PvA2NEz41UzdwXnlDyBiZbe69s3zM0DfCPwZXLUm79uSqH9ItsOjTrXSL5/1+XUL6C/BZwChA==
|
||||
optionalDependencies:
|
||||
"@swc/core-darwin-arm64" "1.3.70"
|
||||
"@swc/core-darwin-x64" "1.3.70"
|
||||
"@swc/core-linux-arm-gnueabihf" "1.3.70"
|
||||
"@swc/core-linux-arm64-gnu" "1.3.70"
|
||||
"@swc/core-linux-arm64-musl" "1.3.70"
|
||||
"@swc/core-linux-x64-gnu" "1.3.70"
|
||||
"@swc/core-linux-x64-musl" "1.3.70"
|
||||
"@swc/core-win32-arm64-msvc" "1.3.70"
|
||||
"@swc/core-win32-ia32-msvc" "1.3.70"
|
||||
"@swc/core-win32-x64-msvc" "1.3.70"
|
||||
|
||||
"@swc/jest@^0.2.24":
|
||||
version "0.2.26"
|
||||
resolved "https://registry.yarnpkg.com/@swc/jest/-/jest-0.2.26.tgz#6ef2d6d31869e3aaddc132603bc21f2e4c57cc5d"
|
||||
integrity sha512-7lAi7q7ShTO3E5Gt1Xqf3pIhRbERxR1DUxvtVa9WKzIB+HGQ7wZP5sYx86zqnaEoKKGhmOoZ7gyW0IRu8Br5+A==
|
||||
dependencies:
|
||||
"@jest/create-cache-key-function" "^27.4.2"
|
||||
jsonc-parser "^3.2.0"
|
||||
|
||||
"@techpass/passport-openidconnect@0.3.2":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.npmjs.org/@techpass/passport-openidconnect/-/passport-openidconnect-0.3.2.tgz"
|
||||
|
@ -885,6 +977,13 @@
|
|||
resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz"
|
||||
integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==
|
||||
|
||||
"@types/yargs@^16.0.0":
|
||||
version "16.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.5.tgz#12cc86393985735a283e387936398c2f9e5f88e3"
|
||||
integrity sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@types/yargs@^17.0.8":
|
||||
version "17.0.22"
|
||||
resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.22.tgz"
|
||||
|
@ -2866,6 +2965,11 @@ json5@^2.2.1, json5@^2.2.2:
|
|||
resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz"
|
||||
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
|
||||
|
||||
jsonc-parser@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76"
|
||||
integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==
|
||||
|
||||
jsonwebtoken@9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz"
|
||||
|
|
|
@ -13,6 +13,6 @@ node ./bumpVersion.js $1
|
|||
NEW_VERSION=$(node -p "require('../lerna.json').version")
|
||||
git add ../lerna.json
|
||||
git commit -m "Bump version to $NEW_VERSION"
|
||||
git tag v$NEW_VERSION
|
||||
git tag $NEW_VERSION
|
||||
git push
|
||||
git push --tags
|
Loading…
Reference in New Issue