Merge branch 'develop' into api-tests-generate-tenants
This commit is contained in:
commit
19632a5143
151 changed files with 3388 additions and 900 deletions
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.2.12-alpha.44",
|
"version": "2.2.12-alpha.50",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/backend-core",
|
"name": "@budibase/backend-core",
|
||||||
"version": "2.2.12-alpha.44",
|
"version": "2.2.12-alpha.50",
|
||||||
"description": "Budibase backend core libraries used in server and worker",
|
"description": "Budibase backend core libraries used in server and worker",
|
||||||
"main": "dist/src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/nano": "10.1.1",
|
"@budibase/nano": "10.1.1",
|
||||||
"@budibase/types": "2.2.12-alpha.44",
|
"@budibase/types": "2.2.12-alpha.50",
|
||||||
"@shopify/jest-koa-mocks": "5.0.1",
|
"@shopify/jest-koa-mocks": "5.0.1",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
"aws-cloudfront-sign": "2.2.0",
|
"aws-cloudfront-sign": "2.2.0",
|
||||||
|
|
|
@ -77,6 +77,7 @@ export const StaticDatabases = {
|
||||||
apiKeys: "apikeys",
|
apiKeys: "apikeys",
|
||||||
usageQuota: "usage_quota",
|
usageQuota: "usage_quota",
|
||||||
licenseInfo: "license_info",
|
licenseInfo: "license_info",
|
||||||
|
environmentVariables: "environmentvariables",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// contains information about tenancy and so on
|
// contains information about tenancy and so on
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
import { AsyncLocalStorage } from "async_hooks"
|
import { AsyncLocalStorage } from "async_hooks"
|
||||||
|
import { ContextMap } from "./mainContext"
|
||||||
|
|
||||||
export default class Context {
|
export default class Context {
|
||||||
static storage = new AsyncLocalStorage<Record<string, any>>()
|
static storage = new AsyncLocalStorage<ContextMap>()
|
||||||
|
|
||||||
static run(context: Record<string, any>, func: any) {
|
static run(context: ContextMap, func: any) {
|
||||||
return Context.storage.run(context, () => func())
|
return Context.storage.run(context, () => func())
|
||||||
}
|
}
|
||||||
|
|
||||||
static get(): Record<string, any> {
|
static get(): ContextMap {
|
||||||
return Context.storage.getStore() as Record<string, any>
|
return Context.storage.getStore() as ContextMap
|
||||||
}
|
|
||||||
|
|
||||||
static set(context: Record<string, any>) {
|
|
||||||
Context.storage.enterWith(context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ export type ContextMap = {
|
||||||
tenantId?: string
|
tenantId?: string
|
||||||
appId?: string
|
appId?: string
|
||||||
identity?: IdentityContext
|
identity?: IdentityContext
|
||||||
|
environmentVariables?: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
let TEST_APP_ID: string | null = null
|
let TEST_APP_ID: string | null = null
|
||||||
|
@ -75,7 +76,7 @@ export function getTenantIDFromAppID(appId: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateContext(updates: ContextMap) {
|
function updateContext(updates: ContextMap): ContextMap {
|
||||||
let context: ContextMap
|
let context: ContextMap
|
||||||
try {
|
try {
|
||||||
context = Context.get()
|
context = Context.get()
|
||||||
|
@ -120,15 +121,23 @@ export async function doInTenant(
|
||||||
return newContext(updates, task)
|
return newContext(updates, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function doInAppContext(appId: string, task: any): Promise<any> {
|
export async function doInAppContext(
|
||||||
if (!appId) {
|
appId: string | null,
|
||||||
|
task: any
|
||||||
|
): Promise<any> {
|
||||||
|
if (!appId && !env.isTest()) {
|
||||||
throw new Error("appId is required")
|
throw new Error("appId is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
const tenantId = getTenantIDFromAppID(appId)
|
let updates: ContextMap
|
||||||
const updates: ContextMap = { appId }
|
if (!appId) {
|
||||||
if (tenantId) {
|
updates = { appId: "" }
|
||||||
updates.tenantId = tenantId
|
} else {
|
||||||
|
const tenantId = getTenantIDFromAppID(appId)
|
||||||
|
updates = { appId }
|
||||||
|
if (tenantId) {
|
||||||
|
updates.tenantId = tenantId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return newContext(updates, task)
|
return newContext(updates, task)
|
||||||
}
|
}
|
||||||
|
@ -189,25 +198,25 @@ export const getProdAppId = () => {
|
||||||
return conversions.getProdAppID(appId)
|
return conversions.getProdAppID(appId)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateTenantId(tenantId?: string) {
|
export function doInEnvironmentContext(
|
||||||
let context: ContextMap = updateContext({
|
values: Record<string, string>,
|
||||||
tenantId,
|
task: any
|
||||||
})
|
) {
|
||||||
Context.set(context)
|
if (!values) {
|
||||||
|
throw new Error("Must supply environment variables.")
|
||||||
|
}
|
||||||
|
const updates = {
|
||||||
|
environmentVariables: values,
|
||||||
|
}
|
||||||
|
return newContext(updates, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateAppId(appId: string) {
|
export function getEnvironmentVariables() {
|
||||||
let context: ContextMap = updateContext({
|
const context = Context.get()
|
||||||
appId,
|
if (!context.environmentVariables) {
|
||||||
})
|
return null
|
||||||
try {
|
} else {
|
||||||
Context.set(context)
|
return context.environmentVariables
|
||||||
} catch (err) {
|
|
||||||
if (env.isTest()) {
|
|
||||||
TEST_APP_ID = appId
|
|
||||||
} else {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ const environment = {
|
||||||
},
|
},
|
||||||
JS_BCRYPT: process.env.JS_BCRYPT,
|
JS_BCRYPT: process.env.JS_BCRYPT,
|
||||||
JWT_SECRET: process.env.JWT_SECRET,
|
JWT_SECRET: process.env.JWT_SECRET,
|
||||||
|
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
|
||||||
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005",
|
||||||
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
COUCH_DB_USERNAME: process.env.COUCH_DB_USER,
|
||||||
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD,
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import {
|
||||||
|
Event,
|
||||||
|
EnvironmentVariableCreatedEvent,
|
||||||
|
EnvironmentVariableDeletedEvent,
|
||||||
|
EnvironmentVariableUpgradePanelOpenedEvent,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { publishEvent } from "../events"
|
||||||
|
|
||||||
|
async function created(name: string, environments: string[]) {
|
||||||
|
const properties: EnvironmentVariableCreatedEvent = {
|
||||||
|
name,
|
||||||
|
environments,
|
||||||
|
}
|
||||||
|
await publishEvent(Event.ENVIRONMENT_VARIABLE_CREATED, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleted(name: string) {
|
||||||
|
const properties: EnvironmentVariableDeletedEvent = {
|
||||||
|
name,
|
||||||
|
}
|
||||||
|
await publishEvent(Event.ENVIRONMENT_VARIABLE_DELETED, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upgradePanelOpened(userId: string) {
|
||||||
|
const properties: EnvironmentVariableUpgradePanelOpenedEvent = {
|
||||||
|
userId,
|
||||||
|
}
|
||||||
|
await publishEvent(
|
||||||
|
Event.ENVIRONMENT_VARIABLE_UPGRADE_PANEL_OPENED,
|
||||||
|
properties
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
created,
|
||||||
|
deleted,
|
||||||
|
upgradePanelOpened,
|
||||||
|
}
|
|
@ -20,3 +20,4 @@ export { default as backfill } from "./backfill"
|
||||||
export { default as group } from "./group"
|
export { default as group } from "./group"
|
||||||
export { default as plugin } from "./plugin"
|
export { default as plugin } from "./plugin"
|
||||||
export { default as backup } from "./backup"
|
export { default as backup } from "./backup"
|
||||||
|
export { default as environmentVariable } from "./environmentVariable"
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
UserPermissionAssignedEvent,
|
UserPermissionAssignedEvent,
|
||||||
UserPermissionRemovedEvent,
|
UserPermissionRemovedEvent,
|
||||||
UserUpdatedEvent,
|
UserUpdatedEvent,
|
||||||
|
UserOnboardingEvent,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
async function created(user: User, timestamp?: number) {
|
async function created(user: User, timestamp?: number) {
|
||||||
|
@ -36,6 +37,13 @@ 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,
|
||||||
|
}
|
||||||
|
await publishEvent(Event.USER_ONBOARDING_COMPLETE, properties)
|
||||||
|
}
|
||||||
|
|
||||||
// PERMISSIONS
|
// PERMISSIONS
|
||||||
|
|
||||||
async function permissionAdminAssigned(user: User, timestamp?: number) {
|
async function permissionAdminAssigned(user: User, timestamp?: number) {
|
||||||
|
@ -126,6 +134,7 @@ export default {
|
||||||
permissionAdminRemoved,
|
permissionAdminRemoved,
|
||||||
permissionBuilderAssigned,
|
permissionBuilderAssigned,
|
||||||
permissionBuilderRemoved,
|
permissionBuilderRemoved,
|
||||||
|
onboardingComplete,
|
||||||
invited,
|
invited,
|
||||||
inviteAccepted,
|
inviteAccepted,
|
||||||
passwordForceReset,
|
passwordForceReset,
|
||||||
|
|
|
@ -2,19 +2,45 @@ import crypto from "crypto"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
|
|
||||||
const ALGO = "aes-256-ctr"
|
const ALGO = "aes-256-ctr"
|
||||||
const SECRET = env.JWT_SECRET
|
|
||||||
const SEPARATOR = "-"
|
const SEPARATOR = "-"
|
||||||
const ITERATIONS = 10000
|
const ITERATIONS = 10000
|
||||||
const RANDOM_BYTES = 16
|
const RANDOM_BYTES = 16
|
||||||
const STRETCH_LENGTH = 32
|
const STRETCH_LENGTH = 32
|
||||||
|
|
||||||
|
export enum SecretOption {
|
||||||
|
JWT = "jwt",
|
||||||
|
ENCRYPTION = "encryption",
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSecret(secretOption: SecretOption): string {
|
||||||
|
let secret, secretName
|
||||||
|
switch (secretOption) {
|
||||||
|
case SecretOption.ENCRYPTION:
|
||||||
|
secret = env.ENCRYPTION_KEY
|
||||||
|
secretName = "ENCRYPTION_KEY"
|
||||||
|
break
|
||||||
|
case SecretOption.JWT:
|
||||||
|
default:
|
||||||
|
secret = env.JWT_SECRET
|
||||||
|
secretName = "JWT_SECRET"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (!secret) {
|
||||||
|
throw new Error(`Secret "${secretName}" has not been set in environment.`)
|
||||||
|
}
|
||||||
|
return secret
|
||||||
|
}
|
||||||
|
|
||||||
function stretchString(string: string, salt: Buffer) {
|
function stretchString(string: string, salt: Buffer) {
|
||||||
return crypto.pbkdf2Sync(string, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
|
return crypto.pbkdf2Sync(string, salt, ITERATIONS, STRETCH_LENGTH, "sha512")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encrypt(input: string) {
|
export function encrypt(
|
||||||
|
input: string,
|
||||||
|
secretOption: SecretOption = SecretOption.JWT
|
||||||
|
) {
|
||||||
const salt = crypto.randomBytes(RANDOM_BYTES)
|
const salt = crypto.randomBytes(RANDOM_BYTES)
|
||||||
const stretched = stretchString(SECRET!, salt)
|
const stretched = stretchString(getSecret(secretOption), salt)
|
||||||
const cipher = crypto.createCipheriv(ALGO, stretched, salt)
|
const cipher = crypto.createCipheriv(ALGO, stretched, salt)
|
||||||
const base = cipher.update(input)
|
const base = cipher.update(input)
|
||||||
const final = cipher.final()
|
const final = cipher.final()
|
||||||
|
@ -22,10 +48,13 @@ export function encrypt(input: string) {
|
||||||
return `${salt.toString("hex")}${SEPARATOR}${encrypted}`
|
return `${salt.toString("hex")}${SEPARATOR}${encrypted}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decrypt(input: string) {
|
export function decrypt(
|
||||||
|
input: string,
|
||||||
|
secretOption: SecretOption = SecretOption.JWT
|
||||||
|
) {
|
||||||
const [salt, encrypted] = input.split(SEPARATOR)
|
const [salt, encrypted] = input.split(SEPARATOR)
|
||||||
const saltBuffer = Buffer.from(salt, "hex")
|
const saltBuffer = Buffer.from(salt, "hex")
|
||||||
const stretched = stretchString(SECRET!, saltBuffer)
|
const stretched = stretchString(getSecret(secretOption), saltBuffer)
|
||||||
const decipher = crypto.createDecipheriv(ALGO, stretched, saltBuffer)
|
const decipher = crypto.createDecipheriv(ALGO, stretched, saltBuffer)
|
||||||
const base = decipher.update(Buffer.from(encrypted, "hex"))
|
const base = decipher.update(Buffer.from(encrypted, "hex"))
|
||||||
const final = decipher.final()
|
const final = decipher.final()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "2.2.12-alpha.44",
|
"version": "2.2.12-alpha.50",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
||||||
"@budibase/string-templates": "2.2.12-alpha.44",
|
"@budibase/string-templates": "2.2.12-alpha.50",
|
||||||
"@spectrum-css/accordion": "3.0.24",
|
"@spectrum-css/accordion": "3.0.24",
|
||||||
"@spectrum-css/actionbutton": "1.0.1",
|
"@spectrum-css/actionbutton": "1.0.1",
|
||||||
"@spectrum-css/actiongroup": "1.0.1",
|
"@spectrum-css/actiongroup": "1.0.1",
|
||||||
|
|
|
@ -3,6 +3,9 @@ export default function positionDropdown(
|
||||||
{ anchor, align, maxWidth, useAnchorWidth }
|
{ anchor, align, maxWidth, useAnchorWidth }
|
||||||
) {
|
) {
|
||||||
const update = () => {
|
const update = () => {
|
||||||
|
if (!anchor) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const anchorBounds = anchor.getBoundingClientRect()
|
const anchorBounds = anchor.getBoundingClientRect()
|
||||||
const elementBounds = element.getBoundingClientRect()
|
const elementBounds = element.getBoundingClientRect()
|
||||||
let styles = {
|
let styles = {
|
||||||
|
@ -13,6 +16,8 @@ export default function positionDropdown(
|
||||||
top: null,
|
top: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let popoverLeftPad = 20
|
||||||
|
|
||||||
// Determine vertical styles
|
// Determine vertical styles
|
||||||
if (window.innerHeight - anchorBounds.bottom < 100) {
|
if (window.innerHeight - anchorBounds.bottom < 100) {
|
||||||
styles.top = anchorBounds.top - elementBounds.height - 5
|
styles.top = anchorBounds.top - elementBounds.height - 5
|
||||||
|
@ -29,7 +34,13 @@ export default function positionDropdown(
|
||||||
styles.minWidth = anchorBounds.width
|
styles.minWidth = anchorBounds.width
|
||||||
}
|
}
|
||||||
if (align === "right") {
|
if (align === "right") {
|
||||||
styles.left = anchorBounds.left + anchorBounds.width - elementBounds.width
|
let left =
|
||||||
|
anchorBounds.left + anchorBounds.width / 2 - elementBounds.width
|
||||||
|
// Accommodate margin on popover: 1.25rem; ~20px
|
||||||
|
if (left + elementBounds.width + popoverLeftPad > window.innerWidth) {
|
||||||
|
left -= 20
|
||||||
|
}
|
||||||
|
styles.left = left
|
||||||
} else if (align === "right-side") {
|
} else if (align === "right-side") {
|
||||||
styles.left = anchorBounds.left + anchorBounds.width
|
styles.left = anchorBounds.left + anchorBounds.width
|
||||||
} else {
|
} else {
|
||||||
|
@ -54,8 +65,11 @@ export default function positionDropdown(
|
||||||
const resizeObserver = new ResizeObserver(entries => {
|
const resizeObserver = new ResizeObserver(entries => {
|
||||||
entries.forEach(update)
|
entries.forEach(update)
|
||||||
})
|
})
|
||||||
resizeObserver.observe(anchor)
|
if (anchor) {
|
||||||
|
resizeObserver.observe(anchor)
|
||||||
|
}
|
||||||
resizeObserver.observe(element)
|
resizeObserver.observe(element)
|
||||||
|
resizeObserver.observe(document.body)
|
||||||
|
|
||||||
document.addEventListener("scroll", update, true)
|
document.addEventListener("scroll", update, true)
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,13 @@
|
||||||
export let tooltip = undefined
|
export let tooltip = undefined
|
||||||
export let dataCy
|
export let dataCy
|
||||||
export let newStyles = true
|
export let newStyles = true
|
||||||
|
export let id
|
||||||
|
|
||||||
let showTooltip = false
|
let showTooltip = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
{id}
|
||||||
class:spectrum-Button--cta={cta}
|
class:spectrum-Button--cta={cta}
|
||||||
class:spectrum-Button--primary={primary}
|
class:spectrum-Button--primary={primary}
|
||||||
class:spectrum-Button--secondary={secondary}
|
class:spectrum-Button--secondary={secondary}
|
||||||
|
|
282
packages/bbui/src/Form/Core/EnvDropdown.svelte
Normal file
282
packages/bbui/src/Form/Core/EnvDropdown.svelte
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
<script>
|
||||||
|
import "@spectrum-css/textfield/dist/index-vars.css"
|
||||||
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
|
import clickOutside from "../../Actions/click_outside"
|
||||||
|
import Divider from "../../Divider/Divider.svelte"
|
||||||
|
|
||||||
|
export let value = null
|
||||||
|
export let placeholder = null
|
||||||
|
export let type = "text"
|
||||||
|
export let disabled = false
|
||||||
|
export let id = null
|
||||||
|
export let readonly = false
|
||||||
|
export let updateOnChange = true
|
||||||
|
export let dataCy
|
||||||
|
export let align
|
||||||
|
export let autofocus = false
|
||||||
|
export let variables
|
||||||
|
export let showModal
|
||||||
|
export let environmentVariablesEnabled
|
||||||
|
export let handleUpgradePanel
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let field
|
||||||
|
let focus = false
|
||||||
|
let iconFocused = false
|
||||||
|
let open = false
|
||||||
|
|
||||||
|
//eslint-disable-next-line
|
||||||
|
const STRIP_NAME_REGEX = /(?<=\.)(.*?)(?=\ })/g
|
||||||
|
|
||||||
|
// Strips the name out of the value which is {{ env.Variable }} resulting in an array like ["Variable"]
|
||||||
|
$: hbsValue = String(value)?.match(STRIP_NAME_REGEX) || []
|
||||||
|
|
||||||
|
const updateValue = newValue => {
|
||||||
|
if (readonly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (type === "number") {
|
||||||
|
const float = parseFloat(newValue)
|
||||||
|
newValue = isNaN(float) ? null : float
|
||||||
|
}
|
||||||
|
dispatch("change", newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFocus = () => {
|
||||||
|
if (readonly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
focus = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBlur = event => {
|
||||||
|
if (readonly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
focus = false
|
||||||
|
updateValue(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onInput = event => {
|
||||||
|
if (readonly || !updateOnChange) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateValue(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOutsideClick = event => {
|
||||||
|
if (open) {
|
||||||
|
event.stopPropagation()
|
||||||
|
open = false
|
||||||
|
focus = false
|
||||||
|
iconFocused = false
|
||||||
|
dispatch("closed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleVarSelect = variable => {
|
||||||
|
open = false
|
||||||
|
focus = false
|
||||||
|
iconFocused = false
|
||||||
|
updateValue(`{{ env.${variable} }}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
focus = autofocus
|
||||||
|
if (focus) field.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
function removeVariable() {
|
||||||
|
updateValue("")
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPopover() {
|
||||||
|
open = true
|
||||||
|
focus = true
|
||||||
|
iconFocused = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="spectrum-InputGroup">
|
||||||
|
<div
|
||||||
|
class:is-disabled={disabled || hbsValue.length}
|
||||||
|
class:is-focused={focus}
|
||||||
|
class="spectrum-Textfield"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class:close-color={hbsValue.length}
|
||||||
|
class:focused={iconFocused}
|
||||||
|
class="hoverable icon-position spectrum-Icon spectrum-Icon--sizeS spectrum-Textfield-validationIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
on:click={() => {
|
||||||
|
hbsValue.length ? removeVariable() : openPopover()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<use
|
||||||
|
xlink:href={`#spectrum-icon-18-${!hbsValue.length ? "Key" : "Close"}`}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<input
|
||||||
|
bind:this={field}
|
||||||
|
disabled={hbsValue.length || disabled}
|
||||||
|
{readonly}
|
||||||
|
{id}
|
||||||
|
data-cy={dataCy}
|
||||||
|
value={hbsValue.length ? `{{ ${hbsValue[0]} }}` : value}
|
||||||
|
placeholder={placeholder || ""}
|
||||||
|
on:click
|
||||||
|
on:blur
|
||||||
|
on:focus
|
||||||
|
on:input
|
||||||
|
on:keyup
|
||||||
|
on:blur={onBlur}
|
||||||
|
on:focus={onFocus}
|
||||||
|
on:input={onInput}
|
||||||
|
type={hbsValue.length ? "text" : type}
|
||||||
|
style={align ? `text-align: ${align};` : ""}
|
||||||
|
class="spectrum-Textfield-input"
|
||||||
|
inputmode={type === "number" ? "decimal" : "text"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if open}
|
||||||
|
<div
|
||||||
|
use:clickOutside={handleOutsideClick}
|
||||||
|
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||||
|
>
|
||||||
|
<ul class="spectrum-Menu" role="listbox">
|
||||||
|
{#if !environmentVariablesEnabled}
|
||||||
|
<div class="no-variables-text primary-text">
|
||||||
|
Upgrade your plan to get environment variables
|
||||||
|
</div>
|
||||||
|
{:else if variables.length}
|
||||||
|
<div style="max-height: 100px">
|
||||||
|
{#each variables as variable, idx}
|
||||||
|
<li
|
||||||
|
class="spectrum-Menu-item"
|
||||||
|
role="option"
|
||||||
|
aria-selected="true"
|
||||||
|
tabindex="0"
|
||||||
|
on:click={() => handleVarSelect(variable.name)}
|
||||||
|
>
|
||||||
|
<span class="spectrum-Menu-itemLabel">
|
||||||
|
<div class="primary-text">
|
||||||
|
{variable.name}
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="no-variables-text primary-text">
|
||||||
|
You don't have any environment variables yet
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
<Divider noMargin />
|
||||||
|
{#if environmentVariablesEnabled}
|
||||||
|
<div on:click={() => showModal()} class="add-variable">
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--sizeS "
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-Add" />
|
||||||
|
</svg>
|
||||||
|
<div class="primary-text">Add Variable</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div on:click={() => handleUpgradePanel()} class="add-variable">
|
||||||
|
<svg
|
||||||
|
class="spectrum-Icon spectrum-Icon--sizeS "
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-ArrowUp" />
|
||||||
|
</svg>
|
||||||
|
<div class="primary-text">Upgrade plan</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.spectrum-Textfield {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-position {
|
||||||
|
position: absolute;
|
||||||
|
top: 25%;
|
||||||
|
right: 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoverable:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--spectrum-global-color-blue-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectrum-InputGroup {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectrum-Popover {
|
||||||
|
max-height: 240px;
|
||||||
|
z-index: 999;
|
||||||
|
top: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spectrum-Popover.spectrum-Popover--bottom.spectrum-Picker-popover.is-open {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-variables-height {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-variables-text {
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-variable {
|
||||||
|
display: flex;
|
||||||
|
padding: var(--spacing-m) 0 var(--spacing-m) var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focused {
|
||||||
|
color: var(--spectrum-global-color-blue-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-variable:hover {
|
||||||
|
background: var(--grey-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-color {
|
||||||
|
color: var(--spectrum-global-color-gray-900) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-color:hover {
|
||||||
|
color: var(--spectrum-global-color-blue-400) !important;
|
||||||
|
}
|
||||||
|
</style>
|
52
packages/bbui/src/Form/EnvDropdown.svelte
Normal file
52
packages/bbui/src/Form/EnvDropdown.svelte
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<script>
|
||||||
|
import Field from "./Field.svelte"
|
||||||
|
import EnvDropdown from "./Core/EnvDropdown.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let value = null
|
||||||
|
export let label = null
|
||||||
|
export let labelPosition = "above"
|
||||||
|
export let placeholder = null
|
||||||
|
export let type = "text"
|
||||||
|
export let disabled = false
|
||||||
|
export let readonly = false
|
||||||
|
export let error = null
|
||||||
|
export let updateOnChange = true
|
||||||
|
export let quiet = false
|
||||||
|
export let dataCy
|
||||||
|
export let autofocus
|
||||||
|
export let variables
|
||||||
|
export let showModal
|
||||||
|
export let environmentVariablesEnabled
|
||||||
|
export let handleUpgradePanel
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
const onChange = e => {
|
||||||
|
value = e.detail
|
||||||
|
dispatch("change", e.detail)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Field {label} {labelPosition} {error}>
|
||||||
|
<EnvDropdown
|
||||||
|
{dataCy}
|
||||||
|
{updateOnChange}
|
||||||
|
{error}
|
||||||
|
{disabled}
|
||||||
|
{readonly}
|
||||||
|
{value}
|
||||||
|
{placeholder}
|
||||||
|
{type}
|
||||||
|
{quiet}
|
||||||
|
{autofocus}
|
||||||
|
{variables}
|
||||||
|
{showModal}
|
||||||
|
{environmentVariablesEnabled}
|
||||||
|
{handleUpgradePanel}
|
||||||
|
on:change={onChange}
|
||||||
|
on:click
|
||||||
|
on:input
|
||||||
|
on:blur
|
||||||
|
on:focus
|
||||||
|
on:keyup
|
||||||
|
/>
|
||||||
|
</Field>
|
|
@ -19,9 +19,7 @@
|
||||||
export let showTip = false
|
export let showTip = false
|
||||||
export let open = false
|
export let open = false
|
||||||
export let useAnchorWidth = false
|
export let useAnchorWidth = false
|
||||||
|
export let dismissible = true
|
||||||
let tipSvg =
|
|
||||||
'<svg xmlns="http://www.w3.org/svg/2000" width="23" height="12" class="spectrum-Popover-tip" > <path class="spectrum-Popover-tip-triangle" d="M 0.7071067811865476 0 L 11.414213562373096 10.707106781186548 L 22.121320343559645 0" /> </svg>'
|
|
||||||
|
|
||||||
$: tooltipClasses = showTip
|
$: tooltipClasses = showTip
|
||||||
? `spectrum-Popover--withTip spectrum-Popover--${direction}`
|
? `spectrum-Popover--withTip spectrum-Popover--${direction}`
|
||||||
|
@ -67,9 +65,15 @@
|
||||||
<Portal {target}>
|
<Portal {target}>
|
||||||
<div
|
<div
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
use:positionDropdown={{ anchor, align, maxWidth, useAnchorWidth }}
|
use:positionDropdown={{
|
||||||
|
anchor,
|
||||||
|
align,
|
||||||
|
maxWidth,
|
||||||
|
useAnchorWidth,
|
||||||
|
showTip: false,
|
||||||
|
}}
|
||||||
use:clickOutside={{
|
use:clickOutside={{
|
||||||
callback: handleOutsideClick,
|
callback: dismissible ? handleOutsideClick : () => {},
|
||||||
anchor,
|
anchor,
|
||||||
}}
|
}}
|
||||||
on:keydown={handleEscape}
|
on:keydown={handleEscape}
|
||||||
|
@ -78,10 +82,6 @@
|
||||||
data-cy={dataCy}
|
data-cy={dataCy}
|
||||||
transition:fly|local={{ y: -20, duration: 200 }}
|
transition:fly|local={{ y: -20, duration: 200 }}
|
||||||
>
|
>
|
||||||
{#if showTip}
|
|
||||||
{@html tipSvg}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
@ -91,6 +91,7 @@
|
||||||
.spectrum-Popover {
|
.spectrum-Popover {
|
||||||
min-width: var(--spectrum-global-dimension-size-2000);
|
min-width: var(--spectrum-global-dimension-size-2000);
|
||||||
border-color: var(--spectrum-global-color-gray-300);
|
border-color: var(--spectrum-global-color-gray-300);
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
.spectrum-Popover.is-open.spectrum-Popover--withTip {
|
.spectrum-Popover.is-open.spectrum-Popover--withTip {
|
||||||
margin-top: var(--spacing-xs);
|
margin-top: var(--spacing-xs);
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import Portal from "svelte-portal"
|
import Portal from "svelte-portal"
|
||||||
export let title
|
export let title
|
||||||
export let icon = ""
|
export let icon = ""
|
||||||
|
export let id
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
let selected = getContext("tab")
|
let selected = getContext("tab")
|
||||||
|
@ -31,10 +32,7 @@
|
||||||
$: {
|
$: {
|
||||||
if ($selected.title === title && tab_internal) {
|
if ($selected.title === title && tab_internal) {
|
||||||
if ($selected.info?.left !== tab_internal.getBoundingClientRect().left) {
|
if ($selected.info?.left !== tab_internal.getBoundingClientRect().left) {
|
||||||
$selected = {
|
setTabInfo()
|
||||||
...$selected,
|
|
||||||
info: tab_internal.getBoundingClientRect(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,6 +48,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
{id}
|
||||||
bind:this={tab_internal}
|
bind:this={tab_internal}
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
class:is-selected={$selected.title === title}
|
class:is-selected={$selected.title === title}
|
||||||
|
|
|
@ -27,6 +27,7 @@ export { default as RadioGroup } from "./Form/RadioGroup.svelte"
|
||||||
export { default as Checkbox } from "./Form/Checkbox.svelte"
|
export { default as Checkbox } from "./Form/Checkbox.svelte"
|
||||||
export { default as InputDropdown } from "./Form/InputDropdown.svelte"
|
export { default as InputDropdown } from "./Form/InputDropdown.svelte"
|
||||||
export { default as PickerDropdown } from "./Form/PickerDropdown.svelte"
|
export { default as PickerDropdown } from "./Form/PickerDropdown.svelte"
|
||||||
|
export { default as EnvDropdown } from "./Form/EnvDropdown.svelte"
|
||||||
export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte"
|
export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte"
|
||||||
export { default as Popover } from "./Popover/Popover.svelte"
|
export { default as Popover } from "./Popover/Popover.svelte"
|
||||||
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
|
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "2.2.12-alpha.44",
|
"version": "2.2.12-alpha.50",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -71,10 +71,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.2.12-alpha.44",
|
"@budibase/bbui": "2.2.12-alpha.50",
|
||||||
"@budibase/client": "2.2.12-alpha.44",
|
"@budibase/client": "2.2.12-alpha.50",
|
||||||
"@budibase/frontend-core": "2.2.12-alpha.44",
|
"@budibase/frontend-core": "2.2.12-alpha.50",
|
||||||
"@budibase/string-templates": "2.2.12-alpha.44",
|
"@budibase/string-templates": "2.2.12-alpha.50",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/accordion": "^3.0.24",
|
"@spectrum-css/accordion": "^3.0.24",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
import { TableNames } from "../constants"
|
import { TableNames } from "../constants"
|
||||||
import { JSONUtils } from "@budibase/frontend-core"
|
import { JSONUtils } from "@budibase/frontend-core"
|
||||||
import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json"
|
import ActionDefinitions from "components/design/settings/controls/ButtonActionEditor/manifest.json"
|
||||||
|
import { environment, licensing } from "stores/portal"
|
||||||
|
|
||||||
// Regex to match all instances of template strings
|
// Regex to match all instances of template strings
|
||||||
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
|
||||||
|
@ -53,8 +54,13 @@ export const getBindableProperties = (asset, componentId) => {
|
||||||
* Gets all rest bindable data fields
|
* Gets all rest bindable data fields
|
||||||
*/
|
*/
|
||||||
export const getRestBindings = () => {
|
export const getRestBindings = () => {
|
||||||
|
const environmentVariablesEnabled = get(licensing).environmentVariablesEnabled
|
||||||
const userBindings = getUserBindings()
|
const userBindings = getUserBindings()
|
||||||
return [...userBindings, ...getAuthBindings()]
|
return [
|
||||||
|
...userBindings,
|
||||||
|
...getAuthBindings(),
|
||||||
|
...(environmentVariablesEnabled ? getEnvironmentBindings() : []),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -89,6 +95,20 @@ export const getAuthBindings = () => {
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getEnvironmentBindings = () => {
|
||||||
|
let envVars = get(environment).variables
|
||||||
|
return envVars.map(variable => {
|
||||||
|
return {
|
||||||
|
type: "context",
|
||||||
|
runtimeBinding: `env.${makePropSafe(variable.name)}`,
|
||||||
|
readableBinding: `env.${variable.name}`,
|
||||||
|
category: "Environment",
|
||||||
|
icon: "Key",
|
||||||
|
display: { type: "string", name: variable.name },
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility - convert a key/value map to an array of custom 'context' bindings
|
* Utility - convert a key/value map to an array of custom 'context' bindings
|
||||||
* @param {object} valueMap Key/value pairings
|
* @param {object} valueMap Key/value pairings
|
||||||
|
|
|
@ -63,6 +63,10 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
selectedScreenId: null,
|
selectedScreenId: null,
|
||||||
selectedComponentId: null,
|
selectedComponentId: null,
|
||||||
selectedLayoutId: null,
|
selectedLayoutId: null,
|
||||||
|
|
||||||
|
// onboarding
|
||||||
|
onboarding: false,
|
||||||
|
tourNodes: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFrontendStore = () => {
|
export const getFrontendStore = () => {
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
|
import { environment, licensing } from "stores/portal"
|
||||||
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
||||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
|
@ -33,6 +34,7 @@
|
||||||
import { Utils } from "@budibase/frontend-core"
|
import { Utils } from "@budibase/frontend-core"
|
||||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let testData
|
export let testData
|
||||||
|
@ -166,6 +168,24 @@
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Environment bindings
|
||||||
|
if ($licensing.environmentVariablesEnabled) {
|
||||||
|
bindings = bindings.concat(
|
||||||
|
$environment.variables.map(variable => {
|
||||||
|
return {
|
||||||
|
label: `env.${variable.name}`,
|
||||||
|
path: `env.${variable.name}`,
|
||||||
|
icon: "Key",
|
||||||
|
category: "Environment",
|
||||||
|
display: {
|
||||||
|
type: "string",
|
||||||
|
name: variable.name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,6 +216,14 @@
|
||||||
onChange({ detail: tempFilters }, defKey)
|
onChange({ detail: tempFilters }, defKey)
|
||||||
drawer.hide()
|
drawer.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
await environment.loadVariables()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
|
|
|
@ -39,6 +39,23 @@
|
||||||
$: showError($fetch.error)
|
$: showError($fetch.error)
|
||||||
$: id, (filters = null)
|
$: id, (filters = null)
|
||||||
|
|
||||||
|
let appliedFilter
|
||||||
|
let rawFilter
|
||||||
|
let appliedSort
|
||||||
|
let selectedRows = []
|
||||||
|
|
||||||
|
$: enrichedSchema,
|
||||||
|
() => {
|
||||||
|
appliedFilter = null
|
||||||
|
rawFilter = null
|
||||||
|
appliedSort = null
|
||||||
|
selectedRows = []
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (Number.isInteger($fetch.pageNumber)) {
|
||||||
|
selectedRows = []
|
||||||
|
}
|
||||||
|
|
||||||
const showError = error => {
|
const showError = error => {
|
||||||
if (error) {
|
if (error) {
|
||||||
notifications.error(error?.message || "Unable to fetch data.")
|
notifications.error(error?.message || "Unable to fetch data.")
|
||||||
|
@ -95,11 +112,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch data whenever sorting option changes
|
// Fetch data whenever sorting option changes
|
||||||
const onSort = e => {
|
const onSort = async e => {
|
||||||
fetch.update({
|
const sort = {
|
||||||
sortColumn: e.detail.column,
|
sortColumn: e.detail.column,
|
||||||
sortOrder: e.detail.order,
|
sortOrder: e.detail.order,
|
||||||
})
|
}
|
||||||
|
await fetch.update(sort)
|
||||||
|
appliedSort = { ...sort }
|
||||||
|
appliedSort.sortOrder = appliedSort.sortOrder.toLowerCase()
|
||||||
|
selectedRows = []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch data whenever filters change
|
// Fetch data whenever filters change
|
||||||
|
@ -108,16 +129,19 @@
|
||||||
fetch.update({
|
fetch.update({
|
||||||
filter: filters,
|
filter: filters,
|
||||||
})
|
})
|
||||||
|
appliedFilter = e.detail
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch data whenever schema changes
|
// Fetch data whenever schema changes
|
||||||
const onUpdateColumns = () => {
|
const onUpdateColumns = () => {
|
||||||
|
selectedRows = []
|
||||||
fetch.refresh()
|
fetch.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch data whenever rows are modified. Unfortunately we have to lose
|
// Fetch data whenever rows are modified. Unfortunately we have to lose
|
||||||
// our pagination place, as our bookmarks will have shifted.
|
// our pagination place, as our bookmarks will have shifted.
|
||||||
const onUpdateRows = () => {
|
const onUpdateRows = () => {
|
||||||
|
selectedRows = []
|
||||||
fetch.refresh()
|
fetch.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,6 +166,9 @@
|
||||||
disableSorting
|
disableSorting
|
||||||
on:updatecolumns={onUpdateColumns}
|
on:updatecolumns={onUpdateColumns}
|
||||||
on:updaterows={onUpdateRows}
|
on:updaterows={onUpdateRows}
|
||||||
|
on:selectionUpdated={e => {
|
||||||
|
selectedRows = e.detail
|
||||||
|
}}
|
||||||
customPlaceholder
|
customPlaceholder
|
||||||
>
|
>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
|
@ -183,6 +210,9 @@
|
||||||
<ExportButton
|
<ExportButton
|
||||||
disabled={!hasRows || !hasCols}
|
disabled={!hasRows || !hasCols}
|
||||||
view={$tables.selected?._id}
|
view={$tables.selected?._id}
|
||||||
|
filters={appliedFilter}
|
||||||
|
sorting={appliedSort}
|
||||||
|
{selectedRows}
|
||||||
/>
|
/>
|
||||||
{#key id}
|
{#key id}
|
||||||
<TableFilterButton
|
<TableFilterButton
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
UNSORTABLE_TYPES,
|
UNSORTABLE_TYPES,
|
||||||
} from "constants"
|
} from "constants"
|
||||||
import RoleCell from "./cells/RoleCell.svelte"
|
import RoleCell from "./cells/RoleCell.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let schema = {}
|
export let schema = {}
|
||||||
export let data = []
|
export let data = []
|
||||||
|
@ -28,6 +29,8 @@
|
||||||
export let disableSorting = false
|
export let disableSorting = false
|
||||||
export let customPlaceholder = false
|
export let customPlaceholder = false
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let selectedRows = []
|
let selectedRows = []
|
||||||
let editableColumn
|
let editableColumn
|
||||||
let editableRow
|
let editableRow
|
||||||
|
@ -36,6 +39,7 @@
|
||||||
let customRenderers = []
|
let customRenderers = []
|
||||||
let confirmDelete
|
let confirmDelete
|
||||||
|
|
||||||
|
$: selectedRows, dispatch("selectionUpdated", selectedRows)
|
||||||
$: isUsersTable = tableId === TableNames.USERS
|
$: isUsersTable = tableId === TableNames.USERS
|
||||||
$: data && resetSelectedRows()
|
$: data && resetSelectedRows()
|
||||||
$: editRowComponent = isUsersTable ? CreateEditUser : CreateEditRow
|
$: editRowComponent = isUsersTable ? CreateEditUser : CreateEditRow
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
import ExportModal from "../modals/ExportModal.svelte"
|
import ExportModal from "../modals/ExportModal.svelte"
|
||||||
|
|
||||||
export let view
|
export let view
|
||||||
|
export let filters
|
||||||
|
export let sorting
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
|
export let selectedRows
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
</script>
|
</script>
|
||||||
|
@ -18,5 +21,5 @@
|
||||||
Export
|
Export
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<ExportModal {view} />
|
<ExportModal {view} {filters} {sorting} {selectedRows} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { Select, ModalContent, notifications } from "@budibase/bbui"
|
import {
|
||||||
|
Select,
|
||||||
|
ModalContent,
|
||||||
|
notifications,
|
||||||
|
Body,
|
||||||
|
Table,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import download from "downloadjs"
|
import download from "downloadjs"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
import { Constants, LuceneUtils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
const FORMATS = [
|
const FORMATS = [
|
||||||
{
|
{
|
||||||
|
@ -19,8 +26,71 @@
|
||||||
]
|
]
|
||||||
|
|
||||||
export let view
|
export let view
|
||||||
|
export let filters
|
||||||
|
export let sorting
|
||||||
|
export let selectedRows = []
|
||||||
|
|
||||||
let exportFormat = FORMATS[0].key
|
let exportFormat = FORMATS[0].key
|
||||||
|
let filterLookup
|
||||||
|
|
||||||
|
$: luceneFilter = LuceneUtils.buildLuceneQuery(filters)
|
||||||
|
$: exportOpDisplay = buildExportOpDisplay(sorting, filterDisplay, filters)
|
||||||
|
|
||||||
|
const buildFilterLookup = () => {
|
||||||
|
return Object.keys(Constants.OperatorOptions).reduce((acc, key) => {
|
||||||
|
const op = Constants.OperatorOptions[key]
|
||||||
|
acc[op.value] = op.label
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
filterLookup = buildFilterLookup()
|
||||||
|
|
||||||
|
const filterDisplay = () => {
|
||||||
|
if (!filters) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return filters.map(filter => {
|
||||||
|
let newFieldName = filter.field + ""
|
||||||
|
const parts = newFieldName.split(":")
|
||||||
|
parts.shift()
|
||||||
|
newFieldName = parts.join(":")
|
||||||
|
return {
|
||||||
|
Field: newFieldName,
|
||||||
|
Operation: filterLookup[filter.operator],
|
||||||
|
"Field Value": filter.value || "",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildExportOpDisplay = (sorting, filterDisplay) => {
|
||||||
|
let filterDisplayConfig = filterDisplay()
|
||||||
|
if (sorting) {
|
||||||
|
filterDisplayConfig = [
|
||||||
|
...filterDisplayConfig,
|
||||||
|
{
|
||||||
|
Field: sorting.sortColumn,
|
||||||
|
Operation: "Order By",
|
||||||
|
"Field Value": sorting.sortOrder,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return filterDisplayConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
const displaySchema = {
|
||||||
|
Field: {
|
||||||
|
type: "string",
|
||||||
|
fieldName: "Field",
|
||||||
|
},
|
||||||
|
Operation: {
|
||||||
|
type: "string",
|
||||||
|
fieldName: "Operation",
|
||||||
|
},
|
||||||
|
"Field Value": {
|
||||||
|
type: "string",
|
||||||
|
fieldName: "Value",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
async function exportView() {
|
async function exportView() {
|
||||||
try {
|
try {
|
||||||
|
@ -33,9 +103,74 @@
|
||||||
notifications.error(`Unable to export ${exportFormat.toUpperCase()} data`)
|
notifications.error(`Unable to export ${exportFormat.toUpperCase()} data`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function exportRows() {
|
||||||
|
if (selectedRows?.length) {
|
||||||
|
const data = await API.exportRows({
|
||||||
|
tableId: view,
|
||||||
|
rows: selectedRows.map(row => row._id),
|
||||||
|
format: exportFormat,
|
||||||
|
})
|
||||||
|
download(data, `export.${exportFormat}`)
|
||||||
|
} else if (filters || sorting) {
|
||||||
|
const data = await API.exportRows({
|
||||||
|
tableId: view,
|
||||||
|
format: exportFormat,
|
||||||
|
search: {
|
||||||
|
query: luceneFilter,
|
||||||
|
sort: sorting?.sortColumn,
|
||||||
|
sortOrder: sorting?.sortOrder,
|
||||||
|
paginate: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
download(data, `export.${exportFormat}`)
|
||||||
|
} else {
|
||||||
|
await exportView()
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent title="Export Data" confirmText="Export" onConfirm={exportView}>
|
<ModalContent
|
||||||
|
title="Export Data"
|
||||||
|
confirmText="Export"
|
||||||
|
onConfirm={exportRows}
|
||||||
|
size={filters?.length || sorting ? "M" : "S"}
|
||||||
|
>
|
||||||
|
{#if selectedRows?.length}
|
||||||
|
<Body size="S">
|
||||||
|
<strong>{selectedRows?.length}</strong>
|
||||||
|
{`row${selectedRows?.length > 1 ? "s" : ""} will be exported`}
|
||||||
|
</Body>
|
||||||
|
{:else if filters || (sorting?.sortOrder && sorting?.sortColumn)}
|
||||||
|
<Body size="S">
|
||||||
|
{#if !filters}
|
||||||
|
Exporting <strong>all</strong> rows
|
||||||
|
{:else}
|
||||||
|
Filters applied
|
||||||
|
{/if}
|
||||||
|
</Body>
|
||||||
|
|
||||||
|
<div class="table-wrap">
|
||||||
|
<Table
|
||||||
|
schema={displaySchema}
|
||||||
|
data={exportOpDisplay}
|
||||||
|
{filters}
|
||||||
|
loading={false}
|
||||||
|
rowCount={filters?.length + 1}
|
||||||
|
disableSorting={true}
|
||||||
|
allowSelectRows={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowEditColumns={false}
|
||||||
|
quiet={true}
|
||||||
|
compact={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Body size="S">
|
||||||
|
Exporting <strong>all</strong> rows
|
||||||
|
</Body>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
label="Format"
|
label="Format"
|
||||||
bind:value={exportFormat}
|
bind:value={exportFormat}
|
||||||
|
@ -45,3 +180,9 @@
|
||||||
getOptionValue={x => x.key}
|
getOptionValue={x => x.key}
|
||||||
/>
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.table-wrap :global(.wrapper) {
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -6,17 +6,26 @@
|
||||||
Toggle,
|
Toggle,
|
||||||
Button,
|
Button,
|
||||||
TextArea,
|
TextArea,
|
||||||
|
Modal,
|
||||||
|
EnvDropdown,
|
||||||
Accordion,
|
Accordion,
|
||||||
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
import { IntegrationTypes } from "constants/backend"
|
import { IntegrationTypes } from "constants/backend"
|
||||||
import { createValidationStore } from "helpers/validation/yup"
|
import { createValidationStore } from "helpers/validation/yup"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher, onMount } from "svelte"
|
||||||
|
import { environment, licensing, auth } from "stores/portal"
|
||||||
|
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
|
||||||
|
|
||||||
export let datasource
|
export let datasource
|
||||||
export let schema
|
export let schema
|
||||||
export let creating
|
export let creating
|
||||||
|
|
||||||
|
let createVariableModal
|
||||||
|
let selectedKey
|
||||||
|
|
||||||
const validation = createValidationStore()
|
const validation = createValidationStore()
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -70,6 +79,37 @@
|
||||||
.filter(el => filter(el))
|
.filter(el => filter(el))
|
||||||
.map(([key]) => key)
|
.map(([key]) => key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function save(data) {
|
||||||
|
try {
|
||||||
|
await environment.createVariable(data)
|
||||||
|
config[selectedKey] = `{{ env.${data.name} }}`
|
||||||
|
createVariableModal.hide()
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Failed to create variable: ${err.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showModal(configKey) {
|
||||||
|
selectedKey = configKey
|
||||||
|
createVariableModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpgradePanel() {
|
||||||
|
await environment.upgradePanelOpened()
|
||||||
|
$licensing.goToUpgradePage()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
await environment.loadVariables()
|
||||||
|
if ($auth.user) {
|
||||||
|
await licensing.init()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
|
@ -134,11 +174,15 @@
|
||||||
{:else}
|
{:else}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<Label>{getDisplayName(configKey)}</Label>
|
<Label>{getDisplayName(configKey)}</Label>
|
||||||
<Input
|
<EnvDropdown
|
||||||
|
showModal={() => showModal(configKey)}
|
||||||
|
variables={$environment.variables}
|
||||||
type={schema[configKey].type}
|
type={schema[configKey].type}
|
||||||
on:change
|
on:change
|
||||||
bind:value={config[configKey]}
|
bind:value={config[configKey]}
|
||||||
error={$validation.errors[configKey]}
|
error={$validation.errors[configKey]}
|
||||||
|
environmentVariablesEnabled={$licensing.environmentVariablesEnabled}
|
||||||
|
{handleUpgradePanel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -146,6 +190,10 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<Modal bind:this={createVariableModal}>
|
||||||
|
<CreateEditVariableModal {save} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.form-row {
|
.form-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
@ -12,10 +12,12 @@
|
||||||
import ViewDynamicVariables from "./variables/ViewDynamicVariables.svelte"
|
import ViewDynamicVariables from "./variables/ViewDynamicVariables.svelte"
|
||||||
import {
|
import {
|
||||||
getRestBindings,
|
getRestBindings,
|
||||||
|
getEnvironmentBindings,
|
||||||
readableToRuntimeBinding,
|
readableToRuntimeBinding,
|
||||||
runtimeToReadableMap,
|
runtimeToReadableMap,
|
||||||
} from "builderStore/dataBinding"
|
} from "builderStore/dataBinding"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import { licensing } from "stores/portal"
|
||||||
|
|
||||||
export let datasource
|
export let datasource
|
||||||
export let queries
|
export let queries
|
||||||
|
@ -93,6 +95,9 @@
|
||||||
headings
|
headings
|
||||||
bind:object={datasource.config.staticVariables}
|
bind:object={datasource.config.staticVariables}
|
||||||
on:change
|
on:change
|
||||||
|
bindings={$licensing.environmentVariablesEnabled
|
||||||
|
? getEnvironmentBindings()
|
||||||
|
: []}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
<div />
|
<div />
|
||||||
|
|
|
@ -1,9 +1,23 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { ModalContent, Layout, Select, Body, Input } from "@budibase/bbui"
|
import {
|
||||||
|
ModalContent,
|
||||||
|
Layout,
|
||||||
|
Select,
|
||||||
|
Body,
|
||||||
|
Input,
|
||||||
|
EnvDropdown,
|
||||||
|
Modal,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
import { AUTH_TYPE_LABELS, AUTH_TYPES } from "./authTypes"
|
import { AUTH_TYPE_LABELS, AUTH_TYPES } from "./authTypes"
|
||||||
import BindableCombobox from "components/common/bindings/BindableCombobox.svelte"
|
import BindableCombobox from "components/common/bindings/BindableCombobox.svelte"
|
||||||
import { getAuthBindings } from "builderStore/dataBinding"
|
import {
|
||||||
|
getAuthBindings,
|
||||||
|
getEnvironmentBindings,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
import { environment, licensing, auth } from "stores/portal"
|
||||||
|
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
|
||||||
|
|
||||||
export let configs
|
export let configs
|
||||||
export let currentConfig
|
export let currentConfig
|
||||||
|
@ -28,7 +42,19 @@
|
||||||
let hasErrors = false
|
let hasErrors = false
|
||||||
let hasChanged = false
|
let hasChanged = false
|
||||||
|
|
||||||
onMount(() => {
|
let createVariableModal
|
||||||
|
let formFieldkey
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
await environment.loadVariables()
|
||||||
|
if ($auth.user) {
|
||||||
|
await licensing.init()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
|
||||||
if (currentConfig) {
|
if (currentConfig) {
|
||||||
deconstructConfig()
|
deconstructConfig()
|
||||||
}
|
}
|
||||||
|
@ -146,6 +172,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const save = async data => {
|
||||||
|
try {
|
||||||
|
await environment.createVariable(data)
|
||||||
|
form.basic[formFieldkey] = `{{ env.${data.name} }}`
|
||||||
|
createVariableModal.hide()
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Failed to create variable: ${err.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onFieldChange = () => {
|
const onFieldChange = () => {
|
||||||
checkErrors()
|
checkErrors()
|
||||||
checkChanged()
|
checkChanged()
|
||||||
|
@ -154,6 +190,16 @@
|
||||||
const onConfirmInternal = () => {
|
const onConfirmInternal = () => {
|
||||||
onConfirm(constructConfig())
|
onConfirm(constructConfig())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleUpgradePanel() {
|
||||||
|
await environment.upgradePanelOpened()
|
||||||
|
$licensing.goToUpgradePage()
|
||||||
|
}
|
||||||
|
|
||||||
|
function showModal(key) {
|
||||||
|
formFieldkey = key
|
||||||
|
createVariableModal.show()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
@ -189,26 +235,40 @@
|
||||||
error={blurred.type ? errors.type : null}
|
error={blurred.type ? errors.type : null}
|
||||||
/>
|
/>
|
||||||
{#if form.type === AUTH_TYPES.BASIC}
|
{#if form.type === AUTH_TYPES.BASIC}
|
||||||
<Input
|
<EnvDropdown
|
||||||
label="Username"
|
label="Username"
|
||||||
bind:value={form.basic.username}
|
bind:value={form.basic.username}
|
||||||
on:change={onFieldChange}
|
on:change={onFieldChange}
|
||||||
on:blur={() => (blurred.basic.username = true)}
|
on:blur={() => (blurred.basic.username = true)}
|
||||||
error={blurred.basic.username ? errors.basic.username : null}
|
error={blurred.basic.username ? errors.basic.username : null}
|
||||||
|
showModal={() => showModal("configKey")}
|
||||||
|
variables={$environment.variables}
|
||||||
|
environmentVariablesEnabled={$licensing.environmentVariablesEnabled}
|
||||||
|
{handleUpgradePanel}
|
||||||
/>
|
/>
|
||||||
<Input
|
<EnvDropdown
|
||||||
label="Password"
|
label="Password"
|
||||||
|
type="password"
|
||||||
bind:value={form.basic.password}
|
bind:value={form.basic.password}
|
||||||
on:change={onFieldChange}
|
on:change={onFieldChange}
|
||||||
on:blur={() => (blurred.basic.password = true)}
|
on:blur={() => (blurred.basic.password = true)}
|
||||||
error={blurred.basic.password ? errors.basic.password : null}
|
error={blurred.basic.password ? errors.basic.password : null}
|
||||||
|
showModal={() => showModal("configKey")}
|
||||||
|
variables={$environment.variables}
|
||||||
|
environmentVariablesEnabled={$licensing.environmentVariablesEnabled}
|
||||||
|
{handleUpgradePanel}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if form.type === AUTH_TYPES.BEARER}
|
{#if form.type === AUTH_TYPES.BEARER}
|
||||||
<BindableCombobox
|
<BindableCombobox
|
||||||
label="Token"
|
label="Token"
|
||||||
value={form.bearer.token}
|
value={form.bearer.token}
|
||||||
bindings={getAuthBindings()}
|
bindings={[
|
||||||
|
...getAuthBindings(),
|
||||||
|
...($licensing.environmentVariablesEnabled
|
||||||
|
? getEnvironmentBindings()
|
||||||
|
: []),
|
||||||
|
]}
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
form.bearer.token = e.detail
|
form.bearer.token = e.detail
|
||||||
onFieldChange()
|
onFieldChange()
|
||||||
|
@ -226,3 +286,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
|
<Modal bind:this={createVariableModal}>
|
||||||
|
<CreateEditVariableModal {save} />
|
||||||
|
</Modal>
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { ProgressCircle } from "@budibase/bbui"
|
import { ProgressCircle } from "@budibase/bbui"
|
||||||
import CopyInput from "components/common/inputs/CopyInput.svelte"
|
import CopyInput from "components/common/inputs/CopyInput.svelte"
|
||||||
|
import TourWrap from "../portal/onboarding/TourWrap.svelte"
|
||||||
|
import { TOUR_STEP_KEYS } from "../portal/onboarding/tours.js"
|
||||||
|
|
||||||
let publishModal
|
let publishModal
|
||||||
let asyncModal
|
let asyncModal
|
||||||
|
@ -54,7 +56,11 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button cta on:click={publishModal.show}>Publish</Button>
|
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}>
|
||||||
|
<Button cta on:click={publishModal.show} id={"builder-app-publish-button"}>
|
||||||
|
Publish
|
||||||
|
</Button>
|
||||||
|
</TourWrap>
|
||||||
<Modal bind:this={publishModal}>
|
<Modal bind:this={publishModal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Publish to production"
|
title="Publish to production"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { datasources, flags, integrations, queries } from "stores/backend"
|
import { datasources, flags, integrations, queries } from "stores/backend"
|
||||||
|
import { environment } from "stores/portal"
|
||||||
import {
|
import {
|
||||||
Banner,
|
Banner,
|
||||||
Body,
|
Body,
|
||||||
|
@ -362,6 +363,13 @@
|
||||||
notifications.error("Error getting datasources")
|
notifications.error("Error getting datasources")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// load the environment variables
|
||||||
|
await environment.loadVariables()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error(`Error getting environment variables - ${error}`)
|
||||||
|
}
|
||||||
|
|
||||||
datasource = $datasources.list.find(ds => ds._id === query?.datasourceId)
|
datasource = $datasources.list.find(ds => ds._id === query?.datasourceId)
|
||||||
const datasourceUrl = datasource?.config.url
|
const datasourceUrl = datasource?.config.url
|
||||||
const qs = query?.fields.queryString
|
const qs = query?.fields.queryString
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
ModalContent,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Checkbox,
|
||||||
|
Heading,
|
||||||
|
notifications,
|
||||||
|
Context,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { environment } from "stores/portal"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
const modalContext = getContext(Context.Modal)
|
||||||
|
|
||||||
|
export let save
|
||||||
|
export let row
|
||||||
|
|
||||||
|
let deleteDialog
|
||||||
|
let name = row?.name || ""
|
||||||
|
let productionValue
|
||||||
|
let developmentValue
|
||||||
|
let useProductionValue = true
|
||||||
|
|
||||||
|
const deleteVariable = async name => {
|
||||||
|
try {
|
||||||
|
await environment.deleteVariable(name)
|
||||||
|
modalContext.hide()
|
||||||
|
notifications.success("Environment variable deleted")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveVariable = async () => {
|
||||||
|
try {
|
||||||
|
await save({
|
||||||
|
name,
|
||||||
|
production: productionValue,
|
||||||
|
development: developmentValue,
|
||||||
|
})
|
||||||
|
notifications.success("Environment variable saved")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Error saving environment variable - ${err.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
onConfirm={() => saveVariable()}
|
||||||
|
title={!row ? "Add new environment variable" : "Edit environment variable"}
|
||||||
|
>
|
||||||
|
<Input disabled={row} label="Name" bind:value={name} />
|
||||||
|
<div>
|
||||||
|
<Heading size="XS">Production</Heading>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
label="Value"
|
||||||
|
on:change={e => {
|
||||||
|
productionValue = e.detail
|
||||||
|
if (useProductionValue) {
|
||||||
|
developmentValue = e.detail
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={productionValue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Heading size="XS">Development</Heading>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
on:change={e => {
|
||||||
|
developmentValue = e.detail
|
||||||
|
}}
|
||||||
|
disabled={useProductionValue}
|
||||||
|
label="Value"
|
||||||
|
value={useProductionValue ? productionValue : developmentValue}
|
||||||
|
/>
|
||||||
|
<Checkbox bind:value={useProductionValue} text="Use production value" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer" slot="footer">
|
||||||
|
{#if row}
|
||||||
|
<Button on:click={deleteDialog.show} warning>Delete</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={deleteDialog}
|
||||||
|
onOk={() => {
|
||||||
|
deleteVariable(row.name)
|
||||||
|
}}
|
||||||
|
okText="Delete Environment Variable"
|
||||||
|
title="Confirm Deletion"
|
||||||
|
>
|
||||||
|
Are you sure you wish to delete the environment variable
|
||||||
|
<i>{row.name}?</i>
|
||||||
|
This action cannot be undone.
|
||||||
|
</ConfirmDialog>
|
|
@ -0,0 +1,173 @@
|
||||||
|
<script>
|
||||||
|
import { Popover, Layout, Heading, Body, Button } from "@budibase/bbui"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { TOURS } from "./tours.js"
|
||||||
|
import { goto, layout, isActive } from "@roxi/routify"
|
||||||
|
|
||||||
|
let popoverAnchor
|
||||||
|
let popover
|
||||||
|
let tourSteps = null
|
||||||
|
let tourStep
|
||||||
|
let tourStepIdx
|
||||||
|
let lastStep
|
||||||
|
|
||||||
|
$: tourNodes = { ...$store.tourNodes }
|
||||||
|
$: tourKey = $store.tourKey
|
||||||
|
$: tourStepKey = $store.tourStepKey
|
||||||
|
|
||||||
|
const initTour = targetKey => {
|
||||||
|
if (!targetKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tourSteps = [...TOURS[targetKey]]
|
||||||
|
tourStepIdx = 0
|
||||||
|
tourStep = { ...tourSteps[tourStepIdx] }
|
||||||
|
}
|
||||||
|
|
||||||
|
$: initTour(tourKey)
|
||||||
|
|
||||||
|
const updateTourStep = targetStepKey => {
|
||||||
|
if (!tourSteps?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
|
||||||
|
lastStep = tourStepIdx + 1 == tourSteps.length
|
||||||
|
tourStep = { ...tourSteps[tourStepIdx] }
|
||||||
|
tourStep.onLoad()
|
||||||
|
}
|
||||||
|
|
||||||
|
$: updateTourStep(tourStepKey)
|
||||||
|
|
||||||
|
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) {
|
||||||
|
store.update(state => {
|
||||||
|
if (!state.previousTopNavPath) state.previousTopNavPath = {}
|
||||||
|
state.previousTopNavPath[activeNav.path] = window.location.pathname
|
||||||
|
$goto(state.previousTopNavPath[step.route] || step.route)
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = async () => {
|
||||||
|
if (!lastStep === true) {
|
||||||
|
let target = tourSteps[tourStepIdx + 1]
|
||||||
|
if (target) {
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
tourStepKey: target.id,
|
||||||
|
}))
|
||||||
|
navigateStep(target)
|
||||||
|
} else {
|
||||||
|
console.log("Could not retrieve step")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (typeof tourStep.onComplete === "function") {
|
||||||
|
tourStep.onComplete()
|
||||||
|
}
|
||||||
|
popover.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCurrentStepIdx = (steps, tourStepKey) => {
|
||||||
|
if (!steps?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (steps?.length && !tourStepKey) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return steps.findIndex(step => step.id === tourStepKey)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#key tourStepKey}
|
||||||
|
<Popover
|
||||||
|
align={tourStep?.align}
|
||||||
|
bind:this={popover}
|
||||||
|
anchor={popoverAnchor}
|
||||||
|
dataCy="tour-popover-menu"
|
||||||
|
maxWidth={300}
|
||||||
|
dismissible={false}
|
||||||
|
>
|
||||||
|
<Layout gap="M">
|
||||||
|
<div class="tour-header">
|
||||||
|
<Heading size="XS">{tourStep?.title || "-"}</Heading>
|
||||||
|
<div>{`${tourStepIdx + 1}/${tourSteps?.length}`}</div>
|
||||||
|
</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 tourStepIdx > 0}
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
on:click={previousStep}
|
||||||
|
disabled={tourStepIdx == 0}
|
||||||
|
>
|
||||||
|
<div>Back</div>
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<Button cta on:click={nextStep}>
|
||||||
|
<div>{lastStep ? "Finish" : "Next"}</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</Popover>
|
||||||
|
{/key}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tour-navigation {
|
||||||
|
grid-gap: var(--spectrum-alias-grid-baseline);
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
:global([data-cy="tour-popover-menu"]) {
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.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>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script>
|
||||||
|
import { tourHandler } from "./tourHandler"
|
||||||
|
import { TOURS } from "./tours"
|
||||||
|
import { onMount, onDestroy } from "svelte"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
|
export let tourStepKey
|
||||||
|
|
||||||
|
let currentTour
|
||||||
|
let ready = false
|
||||||
|
let handler
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!$store.tourKey) return
|
||||||
|
|
||||||
|
currentTour = TOURS[$store.tourKey].find(step => step.id === tourStepKey)
|
||||||
|
|
||||||
|
const elem = document.querySelector(currentTour.query)
|
||||||
|
handler = tourHandler(elem, tourStepKey)
|
||||||
|
ready = true
|
||||||
|
})
|
||||||
|
onDestroy(() => {
|
||||||
|
if (handler) {
|
||||||
|
handler.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
|
@ -0,0 +1,10 @@
|
||||||
|
<div>
|
||||||
|
In this section you can mange 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>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<div>
|
||||||
|
Once you’re 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>
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as OnboardingData } from "./OnboardingData.svelte"
|
||||||
|
export { default as OnboardingDesign } from "./OnboardingDesign.svelte"
|
||||||
|
export { default as OnboardingPublish } from "./OnboardingPublish.svelte"
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { store } from "builderStore/index"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
|
const registerNode = async (node, tourStepKey) => {
|
||||||
|
if (!node) {
|
||||||
|
console.log("Tour Handler - an anchor node is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!get(store).tourKey) {
|
||||||
|
console.log("Tour Handler - No active tour ", tourStepKey, node)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
store.update(state => {
|
||||||
|
const update = {
|
||||||
|
...state,
|
||||||
|
tourNodes: {
|
||||||
|
...state.tourNodes,
|
||||||
|
[tourStepKey]: node,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return update
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tourHandler(node, tourStepKey) {
|
||||||
|
if (node && tourStepKey) {
|
||||||
|
registerNode(node, tourStepKey)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
destroy: () => {
|
||||||
|
const updatedTourNodes = get(store).tourNodes
|
||||||
|
if (updatedTourNodes && updatedTourNodes[tourStepKey]) {
|
||||||
|
delete updatedTourNodes[tourStepKey]
|
||||||
|
store.update(state => {
|
||||||
|
const update = {
|
||||||
|
...state,
|
||||||
|
tourNodes: {
|
||||||
|
...updatedTourNodes,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return update
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
95
packages/builder/src/components/portal/onboarding/tours.js
Normal file
95
packages/builder/src/components/portal/onboarding/tours.js
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { users, auth } from "stores/portal"
|
||||||
|
import analytics from "analytics"
|
||||||
|
import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps"
|
||||||
|
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_AUTOMATE_SECTION: "builder-automate-section",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TOUR_KEYS = {
|
||||||
|
TOUR_BUILDER_ONBOARDING: "builder-onboarding",
|
||||||
|
}
|
||||||
|
|
||||||
|
const tourEvent = eventKey => {
|
||||||
|
analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, {
|
||||||
|
eventSource: EventSource.PORTAL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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: ".topcenternav .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: ".topcenternav .spectrum-Tabs-item#builder-design-tab",
|
||||||
|
onLoad: () => {
|
||||||
|
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
|
||||||
|
},
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TOUR_STEP_KEYS.BUILDER_AUTOMATE_SECTION,
|
||||||
|
title: "Automations",
|
||||||
|
route: "/builder/app/:application/automate",
|
||||||
|
query: ".topcenternav .spectrum-Tabs-item#builder-automate-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_AUTOMATE_SECTION)
|
||||||
|
},
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: TOUR_STEP_KEYS.BUILDER_APP_PUBLISH,
|
||||||
|
title: "Publish",
|
||||||
|
layout: OnboardingPublish,
|
||||||
|
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 users.save({
|
||||||
|
...get(auth).user,
|
||||||
|
onboardedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update the cached user
|
||||||
|
await auth.getSelf()
|
||||||
|
|
||||||
|
store.update(state => ({
|
||||||
|
...state,
|
||||||
|
tourNodes: undefined,
|
||||||
|
tourKey: undefined,
|
||||||
|
tourKeyStep: undefined,
|
||||||
|
onboarding: false,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TOURS = getTours()
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Toggle, Body } from "@budibase/bbui"
|
import { ModalContent, Toggle, Body, InlineAlert } from "@budibase/bbui"
|
||||||
|
import { licensing } from "stores/portal"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let published
|
export let published
|
||||||
|
@ -16,6 +17,11 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent {title} {confirmText} onConfirm={exportApp}>
|
<ModalContent {title} {confirmText} onConfirm={exportApp}>
|
||||||
|
{#if licensing.environmentVariablesEnabled}
|
||||||
|
<InlineAlert
|
||||||
|
header="Do not share your budibase application exports publicly as they may contain sensitive information such as database credentials or secret keys."
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<Body
|
<Body
|
||||||
>Apps can be exported with or without data that is within internal tables -
|
>Apps can be exported with or without data that is within internal tables -
|
||||||
select this below.</Body
|
select this below.</Body
|
||||||
|
|
|
@ -4,37 +4,45 @@
|
||||||
Heading,
|
Heading,
|
||||||
notifications,
|
notifications,
|
||||||
Layout,
|
Layout,
|
||||||
Input,
|
|
||||||
Body,
|
Body,
|
||||||
ActionButton,
|
|
||||||
Modal,
|
Modal,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { admin, auth } from "stores/portal"
|
import { admin, auth } from "stores/portal"
|
||||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
|
||||||
import ImportAppsModal from "./_components/ImportAppsModal.svelte"
|
import ImportAppsModal from "./_components/ImportAppsModal.svelte"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import { FancyForm, FancyInput, ActionButton } from "@budibase/bbui"
|
||||||
|
import { TestimonialPage } from "@budibase/frontend-core/src/components"
|
||||||
|
import { passwordsMatch, handleError } from "../auth/_components/utils"
|
||||||
|
|
||||||
let adminUser = {}
|
|
||||||
let error
|
|
||||||
let modal
|
let modal
|
||||||
|
let form
|
||||||
|
let errors = {}
|
||||||
|
let formData = {}
|
||||||
|
let submitted = false
|
||||||
|
|
||||||
$: tenantId = $auth.tenantId
|
$: tenantId = $auth.tenantId
|
||||||
$: multiTenancyEnabled = $admin.multiTenancy
|
|
||||||
$: cloud = $admin.cloud
|
$: cloud = $admin.cloud
|
||||||
$: imported = $admin.importComplete
|
$: imported = $admin.importComplete
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
form.validate()
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitted = true
|
||||||
try {
|
try {
|
||||||
adminUser.tenantId = tenantId
|
let adminUser = { ...formData, tenantId }
|
||||||
|
delete adminUser.confirmationPassword
|
||||||
// Save the admin user
|
// Save the admin user
|
||||||
await API.createAdminUser(adminUser)
|
await API.createAdminUser(adminUser)
|
||||||
notifications.success("Admin user created")
|
notifications.success("Admin user created")
|
||||||
await admin.init()
|
await admin.init()
|
||||||
$goto("../portal")
|
$goto("../portal")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
submitted = false
|
||||||
notifications.error("Failed to create admin user")
|
notifications.error("Failed to create admin user")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,35 +61,103 @@
|
||||||
<Modal bind:this={modal} padding={false} width="600px">
|
<Modal bind:this={modal} padding={false} width="600px">
|
||||||
<ImportAppsModal />
|
<ImportAppsModal />
|
||||||
</Modal>
|
</Modal>
|
||||||
<section>
|
|
||||||
<div class="container">
|
<TestimonialPage>
|
||||||
<Layout>
|
<Layout gap="M" noPadding>
|
||||||
|
<Layout justifyItems="center" noPadding>
|
||||||
<img alt="logo" src={Logo} />
|
<img alt="logo" src={Logo} />
|
||||||
<Layout gap="XS" justifyItems="center" noPadding>
|
<Heading size="M">Create an admin user</Heading>
|
||||||
<Heading size="M">Create an admin user</Heading>
|
<Body>The admin user has access to everything in Budibase.</Body>
|
||||||
<Body size="M" textAlign="center">
|
</Layout>
|
||||||
The admin user has access to everything in Budibase.
|
<Layout gap="S" noPadding>
|
||||||
</Body>
|
<FancyForm bind:this={form}>
|
||||||
</Layout>
|
<FancyInput
|
||||||
<Layout gap="XS" noPadding>
|
label="Email"
|
||||||
<Input label="Email" bind:value={adminUser.email} />
|
value={formData.email}
|
||||||
<PasswordRepeatInput bind:password={adminUser.password} bind:error />
|
on:change={e => {
|
||||||
</Layout>
|
formData = {
|
||||||
<Layout gap="XS" noPadding>
|
...formData,
|
||||||
<Button cta disabled={error} on:click={save}>
|
email: e.detail,
|
||||||
Create super admin user
|
}
|
||||||
</Button>
|
}}
|
||||||
{#if multiTenancyEnabled}
|
validate={() => {
|
||||||
<ActionButton
|
let fieldError = {
|
||||||
quiet
|
email: !formData.email ? "Please enter a valid email" : undefined,
|
||||||
on:click={() => {
|
}
|
||||||
admin.unload()
|
errors = handleError({ ...errors, ...fieldError })
|
||||||
$goto("../auth/org")
|
}}
|
||||||
}}
|
disabled={submitted}
|
||||||
>
|
error={errors.email}
|
||||||
Change organisation
|
/>
|
||||||
</ActionButton>
|
<FancyInput
|
||||||
{:else if !cloud && !imported}
|
label="Password"
|
||||||
|
value={formData.password}
|
||||||
|
type="password"
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
password: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
let fieldError = {}
|
||||||
|
|
||||||
|
fieldError["password"] = !formData.password
|
||||||
|
? "Please enter a password"
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
fieldError["confirmationPassword"] =
|
||||||
|
!passwordsMatch(
|
||||||
|
formData.password,
|
||||||
|
formData.confirmationPassword
|
||||||
|
) && formData.confirmationPassword
|
||||||
|
? "Passwords must match"
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
errors = handleError({ ...errors, ...fieldError })
|
||||||
|
}}
|
||||||
|
error={errors.password}
|
||||||
|
disabled={submitted}
|
||||||
|
/>
|
||||||
|
<FancyInput
|
||||||
|
label="Repeat Password"
|
||||||
|
value={formData.confirmationPassword}
|
||||||
|
type="password"
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
confirmationPassword: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
let fieldError = {
|
||||||
|
confirmationPassword:
|
||||||
|
!passwordsMatch(
|
||||||
|
formData.password,
|
||||||
|
formData.confirmationPassword
|
||||||
|
) && formData.password
|
||||||
|
? "Passwords must match"
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
errors = handleError({ ...errors, ...fieldError })
|
||||||
|
}}
|
||||||
|
error={errors.confirmationPassword}
|
||||||
|
disabled={submitted}
|
||||||
|
/>
|
||||||
|
</FancyForm>
|
||||||
|
</Layout>
|
||||||
|
<Layout gap="XS" noPadding justifyItems="center">
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
disabled={Object.keys(errors).length > 0 || submitted}
|
||||||
|
on:click={save}
|
||||||
|
>
|
||||||
|
Create super admin user
|
||||||
|
</Button>
|
||||||
|
</Layout>
|
||||||
|
<Layout gap="XS" noPadding justifyItems="center">
|
||||||
|
<div class="user-actions">
|
||||||
|
{#if !cloud && !imported}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
quiet
|
quiet
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -91,28 +167,13 @@
|
||||||
Import from cloud
|
Import from cloud
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</Layout>
|
||||||
</section>
|
</TestimonialPage>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 260px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
img {
|
img {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore } from "builderStore"
|
||||||
import { roles, flags } from "stores/backend"
|
import { roles, flags } from "stores/backend"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
import {
|
import {
|
||||||
ActionMenu,
|
ActionMenu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
@ -10,6 +11,7 @@
|
||||||
Heading,
|
Heading,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
|
||||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||||
import DeployNavigation from "components/deploy/DeployNavigation.svelte"
|
import DeployNavigation from "components/deploy/DeployNavigation.svelte"
|
||||||
|
@ -17,6 +19,9 @@
|
||||||
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
import { isActive, 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 { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
|
||||||
|
@ -62,6 +67,23 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initTour = async () => {
|
||||||
|
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,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!hasSynced && application) {
|
if (!hasSynced && application) {
|
||||||
try {
|
try {
|
||||||
|
@ -69,6 +91,7 @@
|
||||||
// check if user has beta access
|
// check if user has beta access
|
||||||
// const betaResponse = await API.checkBetaAccess($auth?.user?.email)
|
// const betaResponse = await API.checkBetaAccess($auth?.user?.email)
|
||||||
// betaAccess = betaResponse.access
|
// betaAccess = betaResponse.access
|
||||||
|
initTour()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Failed to sync with production database")
|
notifications.error("Failed to sync with production database")
|
||||||
}
|
}
|
||||||
|
@ -88,6 +111,7 @@
|
||||||
<!-- This should probably be some kind of loading state? -->
|
<!-- This should probably be some kind of loading state? -->
|
||||||
<div class="loading" />
|
<div class="loading" />
|
||||||
{:then _}
|
{:then _}
|
||||||
|
<TourPopover />
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<div class="top-nav">
|
<div class="top-nav">
|
||||||
<div class="topleftnav">
|
<div class="topleftnav">
|
||||||
|
@ -140,12 +164,15 @@
|
||||||
<div class="topcenternav">
|
<div class="topcenternav">
|
||||||
<Tabs {selected} size="M">
|
<Tabs {selected} size="M">
|
||||||
{#each $layout.children as { path, title }}
|
{#each $layout.children as { path, title }}
|
||||||
<Tab
|
<TourWrap tourStepKey={`builder-${title}-section`}>
|
||||||
quiet
|
<Tab
|
||||||
selected={$isActive(path)}
|
quiet
|
||||||
on:click={topItemNavigate(path)}
|
selected={$isActive(path)}
|
||||||
title={capitalise(title)}
|
on:click={topItemNavigate(path)}
|
||||||
/>
|
title={capitalise(title)}
|
||||||
|
id={`builder-${title}-tab`}
|
||||||
|
/>
|
||||||
|
</TourWrap>
|
||||||
{/each}
|
{/each}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { ActionButton } from "@budibase/bbui"
|
import { FancyButton } from "@budibase/bbui"
|
||||||
import GoogleLogo from "assets/google-logo.png"
|
import GoogleLogo from "assets/google-logo.png"
|
||||||
import { auth, organisation } from "stores/portal"
|
import { auth, organisation } from "stores/portal"
|
||||||
|
|
||||||
|
@ -10,31 +10,11 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if show}
|
{#if show}
|
||||||
<ActionButton
|
<FancyButton
|
||||||
|
icon={GoogleLogo}
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
window.open(`/api/global/auth/${tenantId}/google`, "_blank")}
|
window.open(`/api/global/auth/${tenantId}/google`, "_blank")}
|
||||||
>
|
>
|
||||||
<div class="inner">
|
Log in with Google
|
||||||
<img src={GoogleLogo} alt="google icon" />
|
</FancyButton>
|
||||||
<p>Sign in with Google</p>
|
|
||||||
</div>
|
|
||||||
</ActionButton>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
|
||||||
.inner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding-top: var(--spacing-xs);
|
|
||||||
padding-bottom: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
.inner img {
|
|
||||||
width: 18px;
|
|
||||||
margin: 3px 10px 3px 3px;
|
|
||||||
}
|
|
||||||
.inner p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { ActionButton, notifications } from "@budibase/bbui"
|
import { notifications, FancyButton } from "@budibase/bbui"
|
||||||
import OidcLogo from "assets/oidc-logo.png"
|
import OidcLogo from "assets/oidc-logo.png"
|
||||||
import Auth0Logo from "assets/auth0-logo.png"
|
import Auth0Logo from "assets/auth0-logo.png"
|
||||||
import MicrosoftLogo from "assets/microsoft-logo.png"
|
import MicrosoftLogo from "assets/microsoft-logo.png"
|
||||||
|
@ -33,34 +33,14 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if show}
|
{#if show}
|
||||||
<ActionButton
|
<FancyButton
|
||||||
|
icon={src}
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
window.open(
|
window.open(
|
||||||
`/api/global/auth/${$auth.tenantId}/oidc/configs/${$oidc.uuid}`,
|
`/api/global/auth/${$auth.tenantId}/oidc/configs/${$oidc.uuid}`,
|
||||||
"_blank"
|
"_blank"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div class="inner">
|
{`Log in with ${$oidc.name || "OIDC"}`}
|
||||||
<img {src} alt="oidc icon" />
|
</FancyButton>
|
||||||
<p>{`Sign in with ${$oidc.name || "OIDC"}`}</p>
|
|
||||||
</div>
|
|
||||||
</ActionButton>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
|
||||||
.inner {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding-top: var(--spacing-xs);
|
|
||||||
padding-bottom: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
.inner img {
|
|
||||||
width: 18px;
|
|
||||||
margin: 3px 10px 3px 3px;
|
|
||||||
}
|
|
||||||
.inner p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
17
packages/builder/src/pages/builder/auth/_components/utils.js
Normal file
17
packages/builder/src/pages/builder/auth/_components/utils.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
export const handleError = err => {
|
||||||
|
let update = { ...err }
|
||||||
|
return Object.keys(update).reduce((acc, key) => {
|
||||||
|
if (update[key]) {
|
||||||
|
acc[key] = update[key]
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const passwordsMatch = (password, confirmation) => {
|
||||||
|
let confirm = confirmation?.trim()
|
||||||
|
let pwd = password?.trim()
|
||||||
|
return (
|
||||||
|
typeof confirm === "string" && typeof pwd === "string" && confirm == pwd
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,25 +1,35 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
notifications,
|
notifications,
|
||||||
Input,
|
|
||||||
Button,
|
Button,
|
||||||
Layout,
|
Layout,
|
||||||
Body,
|
Body,
|
||||||
Heading,
|
Heading,
|
||||||
ActionButton,
|
Icon,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { organisation, auth } from "stores/portal"
|
import { organisation, auth } from "stores/portal"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
import { TestimonialPage } from "@budibase/frontend-core/src/components"
|
||||||
|
import { FancyForm, FancyInput } from "@budibase/bbui"
|
||||||
|
|
||||||
let email = ""
|
let email = ""
|
||||||
|
let form
|
||||||
|
let error
|
||||||
|
let submitted = false
|
||||||
|
|
||||||
async function forgot() {
|
async function forgot() {
|
||||||
|
form.validate()
|
||||||
|
if (error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitted = true
|
||||||
try {
|
try {
|
||||||
await auth.forgotPassword(email)
|
await auth.forgotPassword(email)
|
||||||
notifications.success("Email sent - please check your inbox")
|
notifications.success("Email sent - please check your inbox")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
submitted = false
|
||||||
notifications.error("Unable to send reset password link")
|
notifications.error("Unable to send reset password link")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,45 +43,64 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="login">
|
<TestimonialPage>
|
||||||
<div class="main">
|
<Layout gap="S" noPadding>
|
||||||
<Layout>
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
<Layout noPadding justifyItems="center">
|
<span class="heading-wrap">
|
||||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
<Heading size="M">
|
||||||
</Layout>
|
<div class="heading-content">
|
||||||
<Layout gap="XS" noPadding>
|
<span class="back-chev" on:click={() => $goto("../")}>
|
||||||
<Heading textAlign="center">Forgotten your password?</Heading>
|
<Icon name="ChevronLeft" size="XL" />
|
||||||
<Body size="S" textAlign="center">
|
</span>
|
||||||
No problem! Just enter your account's email address and we'll send you
|
Forgotten your password?
|
||||||
a link to reset it.
|
</div>
|
||||||
</Body>
|
</Heading>
|
||||||
<Input label="Email" bind:value={email} />
|
</span>
|
||||||
</Layout>
|
<Layout gap="XS" noPadding>
|
||||||
<Layout gap="XS" nopadding>
|
<Body size="M">
|
||||||
<Button cta on:click={forgot} disabled={!email}>
|
No problem! Just enter your account's email address and we'll send you a
|
||||||
Reset your password
|
link to reset it.
|
||||||
</Button>
|
</Body>
|
||||||
<ActionButton quiet on:click={() => $goto("../")}>Back</ActionButton>
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
|
||||||
</div>
|
<Layout gap="S" noPadding>
|
||||||
|
<FancyForm bind:this={form}>
|
||||||
|
<FancyInput
|
||||||
|
label="Email"
|
||||||
|
value={email}
|
||||||
|
on:change={e => {
|
||||||
|
email = e.detail
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
if (!email) {
|
||||||
|
return "Please enter your email"
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}}
|
||||||
|
{error}
|
||||||
|
disabled={submitted}
|
||||||
|
/>
|
||||||
|
</FancyForm>
|
||||||
|
</Layout>
|
||||||
|
<div>
|
||||||
|
<Button disabled={!email || error || submitted} cta on:click={forgot}>
|
||||||
|
Reset password
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</TestimonialPage>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.login {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
}
|
}
|
||||||
|
.back-chev {
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: -5px;
|
||||||
|
}
|
||||||
|
.heading-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
Button,
|
Button,
|
||||||
Divider,
|
Divider,
|
||||||
Heading,
|
Heading,
|
||||||
Input,
|
|
||||||
Layout,
|
Layout,
|
||||||
notifications,
|
notifications,
|
||||||
Link,
|
Link,
|
||||||
|
@ -14,22 +13,30 @@
|
||||||
import { auth, organisation, oidc, admin } from "stores/portal"
|
import { auth, organisation, oidc, admin } from "stores/portal"
|
||||||
import GoogleButton from "./_components/GoogleButton.svelte"
|
import GoogleButton from "./_components/GoogleButton.svelte"
|
||||||
import OIDCButton from "./_components/OIDCButton.svelte"
|
import OIDCButton from "./_components/OIDCButton.svelte"
|
||||||
|
import { handleError } from "./_components/utils"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
|
import { TestimonialPage } from "@budibase/frontend-core/src/components"
|
||||||
|
import { FancyForm, FancyInput } from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
let username = ""
|
|
||||||
let password = ""
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
let form
|
||||||
|
let errors = {}
|
||||||
|
let formData = {}
|
||||||
|
|
||||||
$: company = $organisation.company || "Budibase"
|
$: company = $organisation.company || "Budibase"
|
||||||
$: multiTenancyEnabled = $admin.multiTenancy
|
|
||||||
$: cloud = $admin.cloud
|
$: cloud = $admin.cloud
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
|
form.validate()
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
console.log("errors")
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await auth.login({
|
await auth.login({
|
||||||
username: username.trim(),
|
username: formData?.username.trim(),
|
||||||
password,
|
password: formData?.password,
|
||||||
})
|
})
|
||||||
if ($auth?.user?.forceResetPassword) {
|
if ($auth?.user?.forceResetPassword) {
|
||||||
$goto("./reset")
|
$goto("./reset")
|
||||||
|
@ -57,75 +64,96 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeydown} />
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
<div class="login">
|
|
||||||
<div class="main">
|
<TestimonialPage>
|
||||||
<Layout>
|
<Layout gap="S" noPadding>
|
||||||
<Layout noPadding justifyItems="center">
|
<Layout justifyItems="center" noPadding>
|
||||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
|
||||||
<Heading textAlign="center">Sign in to {company}</Heading>
|
|
||||||
</Layout>
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<GoogleButton />
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
|
|
||||||
{/if}
|
|
||||||
<Divider noGrid />
|
|
||||||
<Layout gap="XS" noPadding>
|
|
||||||
<Body size="S" textAlign="center">Sign in with email</Body>
|
|
||||||
<Input label="Email" bind:value={username} />
|
|
||||||
<Input
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
on:change
|
|
||||||
bind:value={password}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
<Layout gap="XS" noPadding>
|
|
||||||
<Button cta disabled={!username && !password} on:click={login}
|
|
||||||
>Sign in to {company}</Button
|
|
||||||
>
|
|
||||||
<ActionButton quiet on:click={() => $goto("./forgot")}>
|
|
||||||
Forgot password?
|
|
||||||
</ActionButton>
|
|
||||||
{#if multiTenancyEnabled && !cloud}
|
|
||||||
<ActionButton
|
|
||||||
quiet
|
|
||||||
on:click={() => {
|
|
||||||
admin.unload()
|
|
||||||
$goto("./org")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Change organisation
|
|
||||||
</ActionButton>
|
|
||||||
{/if}
|
|
||||||
</Layout>
|
|
||||||
{#if cloud}
|
|
||||||
<Body size="xs" textAlign="center">
|
|
||||||
By using Budibase Cloud
|
|
||||||
<br />
|
|
||||||
you are agreeing to our
|
|
||||||
<Link href="https://budibase.com/eula" target="_blank"
|
|
||||||
>License Agreement</Link
|
|
||||||
>
|
|
||||||
</Body>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
<Heading size="M">Log in to Budibase</Heading>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
<Layout gap="S" noPadding>
|
||||||
</div>
|
{#if loaded && ($organisation.google || $organisation.oidc)}
|
||||||
|
<FancyForm>
|
||||||
|
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
|
||||||
|
<GoogleButton />
|
||||||
|
</FancyForm>
|
||||||
|
<Divider />
|
||||||
|
{/if}
|
||||||
|
<FancyForm bind:this={form}>
|
||||||
|
<FancyInput
|
||||||
|
label="Your work email"
|
||||||
|
value={formData.username}
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
username: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
let fieldError = {
|
||||||
|
username: !formData.username
|
||||||
|
? "Please enter a valid email"
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
errors = handleError({ ...errors, ...fieldError })
|
||||||
|
}}
|
||||||
|
error={errors.username}
|
||||||
|
/>
|
||||||
|
<FancyInput
|
||||||
|
label="Password"
|
||||||
|
value={formData.password}
|
||||||
|
type="password"
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
password: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
let fieldError = {
|
||||||
|
password: !formData.password
|
||||||
|
? "Please enter your password"
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
errors = handleError({ ...errors, ...fieldError })
|
||||||
|
}}
|
||||||
|
error={errors.password}
|
||||||
|
/>
|
||||||
|
</FancyForm>
|
||||||
|
</Layout>
|
||||||
|
<Layout gap="XS" noPadding justifyItems="center">
|
||||||
|
<Button cta disabled={Object.keys(errors).length > 0} on:click={login}>
|
||||||
|
Log in to {company}
|
||||||
|
</Button>
|
||||||
|
</Layout>
|
||||||
|
<Layout gap="XS" noPadding justifyItems="center">
|
||||||
|
<div class="user-actions">
|
||||||
|
<ActionButton quiet on:click={() => $goto("./forgot")}>
|
||||||
|
Forgot password
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
{#if cloud}
|
||||||
|
<Body size="xs" textAlign="center">
|
||||||
|
By using Budibase Cloud
|
||||||
|
<br />
|
||||||
|
you are agreeing to our
|
||||||
|
<Link href="https://budibase.com/eula" target="_blank" secondary={true}>
|
||||||
|
License Agreement
|
||||||
|
</Link>
|
||||||
|
</Body>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</TestimonialPage>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.login {
|
.user-actions {
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,43 @@
|
||||||
<script>
|
<script>
|
||||||
import { Body, Button, Heading, Layout, notifications } from "@budibase/bbui"
|
import { Body, Button, Heading, Layout, notifications } from "@budibase/bbui"
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
|
||||||
import { auth, organisation } from "stores/portal"
|
import { auth, organisation } from "stores/portal"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
|
import { TestimonialPage } from "@budibase/frontend-core/src/components"
|
||||||
|
import { FancyForm, FancyInput } from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import { handleError, passwordsMatch } from "./_components/utils"
|
||||||
|
|
||||||
const resetCode = $params["?code"]
|
const resetCode = $params["?code"]
|
||||||
let password, error
|
let form
|
||||||
|
let formData = {}
|
||||||
|
let errors = {}
|
||||||
|
let loaded = false
|
||||||
|
|
||||||
|
$: submitted = false
|
||||||
$: forceResetPassword = $auth?.user?.forceResetPassword
|
$: forceResetPassword = $auth?.user?.forceResetPassword
|
||||||
|
|
||||||
async function reset() {
|
async function reset() {
|
||||||
|
form.validate()
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitted = true
|
||||||
try {
|
try {
|
||||||
if (forceResetPassword) {
|
if (forceResetPassword) {
|
||||||
await auth.updateSelf({
|
await auth.updateSelf({
|
||||||
password,
|
password: formData.password,
|
||||||
forceResetPassword: false,
|
forceResetPassword: false,
|
||||||
})
|
})
|
||||||
$goto("../portal/")
|
$goto("../portal/")
|
||||||
} else {
|
} else {
|
||||||
await auth.resetPassword(password, resetCode)
|
await auth.resetPassword(formData.password, resetCode)
|
||||||
notifications.success("Password reset successfully")
|
notifications.success("Password reset successfully")
|
||||||
// send them to login if reset successful
|
// send them to login if reset successful
|
||||||
$goto("./login")
|
$goto("./login")
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
submitted = false
|
||||||
notifications.error("Unable to reset password")
|
notifications.error("Unable to reset password")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,47 +49,92 @@
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting org config")
|
notifications.error("Error getting org config")
|
||||||
}
|
}
|
||||||
|
loaded = true
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="login">
|
<TestimonialPage>
|
||||||
<div class="main">
|
<Layout gap="S" noPadding>
|
||||||
<Layout>
|
{#if loaded}
|
||||||
<Layout noPadding justifyItems="center">
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
<img src={$organisation.logoUrl || Logo} alt="Organisation logo" />
|
{/if}
|
||||||
</Layout>
|
<Layout gap="XS" noPadding>
|
||||||
<Layout gap="XS" noPadding>
|
<Heading size="M">Reset your password</Heading>
|
||||||
<Heading textAlign="center">Reset your password</Heading>
|
<Body size="M">Please enter the new password you'd like to use.</Body>
|
||||||
<Body size="S" textAlign="center">
|
|
||||||
Please enter the new password you'd like to use.
|
|
||||||
</Body>
|
|
||||||
<PasswordRepeatInput bind:password bind:error />
|
|
||||||
</Layout>
|
|
||||||
<Button
|
|
||||||
cta
|
|
||||||
on:click={reset}
|
|
||||||
disabled={error || (forceResetPassword ? false : !resetCode)}
|
|
||||||
>
|
|
||||||
Reset your password
|
|
||||||
</Button>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
|
||||||
</div>
|
<Layout gap="S" noPadding>
|
||||||
|
<FancyForm bind:this={form}>
|
||||||
|
<FancyInput
|
||||||
|
label="Password"
|
||||||
|
value={formData.password}
|
||||||
|
type="password"
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
password: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
let fieldError = {}
|
||||||
|
|
||||||
|
fieldError["password"] = !formData.password
|
||||||
|
? "Please enter a password"
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
fieldError["confirmationPassword"] =
|
||||||
|
!passwordsMatch(
|
||||||
|
formData.password,
|
||||||
|
formData.confirmationPassword
|
||||||
|
) && formData.confirmationPassword
|
||||||
|
? "Passwords must match"
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
errors = handleError({ ...errors, ...fieldError })
|
||||||
|
}}
|
||||||
|
error={errors.password}
|
||||||
|
disabled={submitted}
|
||||||
|
/>
|
||||||
|
<FancyInput
|
||||||
|
label="Repeat Password"
|
||||||
|
value={formData.confirmationPassword}
|
||||||
|
type="password"
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
confirmationPassword: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
const isValid =
|
||||||
|
!passwordsMatch(
|
||||||
|
formData.password,
|
||||||
|
formData.confirmationPassword
|
||||||
|
) && formData.password
|
||||||
|
|
||||||
|
let fieldError = {
|
||||||
|
confirmationPassword: isValid ? "Passwords must match" : null,
|
||||||
|
}
|
||||||
|
|
||||||
|
errors = handleError({ ...errors, ...fieldError })
|
||||||
|
}}
|
||||||
|
error={errors.confirmationPassword}
|
||||||
|
disabled={submitted}
|
||||||
|
/>
|
||||||
|
</FancyForm>
|
||||||
|
</Layout>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
disabled={Object.keys(errors).length > 0 ||
|
||||||
|
(forceResetPassword ? false : !resetCode)}
|
||||||
|
cta
|
||||||
|
on:click={reset}>Reset your password</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</TestimonialPage>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.login {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
width: 260px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,70 +1,192 @@
|
||||||
<script>
|
<script>
|
||||||
import { Layout, Heading, Body, Button, notifications } from "@budibase/bbui"
|
import { Layout, Heading, Body, Button, notifications } from "@budibase/bbui"
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { users, organisation } from "stores/portal"
|
import { users, organisation, auth } from "stores/portal"
|
||||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
|
import { TestimonialPage } from "@budibase/frontend-core/src/components"
|
||||||
|
import { FancyForm, FancyInput } from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import { handleError, passwordsMatch } from "../auth/_components/utils"
|
||||||
|
|
||||||
const inviteCode = $params["?code"]
|
const inviteCode = $params["?code"]
|
||||||
let password, error
|
let form
|
||||||
|
let formData = {}
|
||||||
|
let onboarding = false
|
||||||
|
let errors = {}
|
||||||
|
|
||||||
$: company = $organisation.company || "Budibase"
|
$: company = $organisation.company || "Budibase"
|
||||||
|
|
||||||
async function acceptInvite() {
|
async function acceptInvite() {
|
||||||
|
form.validate()
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onboarding = true
|
||||||
try {
|
try {
|
||||||
await users.acceptInvite(inviteCode, password)
|
const { password, firstName, lastName } = formData
|
||||||
|
await users.acceptInvite(inviteCode, password, firstName, lastName)
|
||||||
notifications.success("Invitation accepted successfully")
|
notifications.success("Invitation accepted successfully")
|
||||||
$goto("../auth/login")
|
await login()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(error.message)
|
notifications.error(error.message)
|
||||||
|
onboarding = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getInvite() {
|
||||||
|
try {
|
||||||
|
const invite = await users.fetchInvite(inviteCode)
|
||||||
|
if (invite?.email) {
|
||||||
|
formData.email = invite?.email
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
try {
|
||||||
|
await auth.login({
|
||||||
|
username: formData.email.trim(),
|
||||||
|
password: formData.password.trim(),
|
||||||
|
})
|
||||||
|
notifications.success("Logged in successfully")
|
||||||
|
$goto("../portal")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(err.message ? err.message : "Invalid credentials") //not likely, considering.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await organisation.init()
|
await organisation.init()
|
||||||
|
await getInvite()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error getting org config")
|
notifications.error("Error getting invite config")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<TestimonialPage>
|
||||||
<div class="container">
|
<Layout gap="S" noPadding>
|
||||||
<Layout>
|
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
<Layout gap="XS" noPadding>
|
||||||
<Layout gap="XS" justifyItems="center" noPadding>
|
<Heading size="M">Join {company}</Heading>
|
||||||
<Heading size="M">Invitation to {company}</Heading>
|
<Body size="M">Create your account to access your budibase apps!</Body>
|
||||||
<Body textAlign="center" size="M">
|
|
||||||
Please enter a password to get started.
|
|
||||||
</Body>
|
|
||||||
</Layout>
|
|
||||||
<PasswordRepeatInput bind:error bind:password />
|
|
||||||
<Button disabled={error} cta on:click={acceptInvite}>
|
|
||||||
Accept invite
|
|
||||||
</Button>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
|
||||||
</section>
|
<Layout gap="S" noPadding>
|
||||||
|
<FancyForm bind:this={form}>
|
||||||
|
<FancyInput
|
||||||
|
label="Email"
|
||||||
|
value={formData.email}
|
||||||
|
disabled={true}
|
||||||
|
error={errors.email}
|
||||||
|
/>
|
||||||
|
<FancyInput
|
||||||
|
label="First name"
|
||||||
|
value={formData.firstName}
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
firstName: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
let fieldError = {
|
||||||
|
firstName: !formData.firstName
|
||||||
|
? "Please enter your first name"
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
errors = handleError({ ...errors, ...fieldError })
|
||||||
|
}}
|
||||||
|
error={errors.firstName}
|
||||||
|
disabled={onboarding}
|
||||||
|
/>
|
||||||
|
<FancyInput
|
||||||
|
label="Last name (optional)"
|
||||||
|
value={formData.lastName}
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
lastName: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={onboarding}
|
||||||
|
/>
|
||||||
|
<FancyInput
|
||||||
|
label="Password"
|
||||||
|
value={formData.password}
|
||||||
|
type="password"
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
password: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
let fieldError = {}
|
||||||
|
|
||||||
|
fieldError["password"] = !formData.password
|
||||||
|
? "Please enter a password"
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
fieldError["confirmationPassword"] =
|
||||||
|
!passwordsMatch(
|
||||||
|
formData.password,
|
||||||
|
formData.confirmationPassword
|
||||||
|
) && formData.confirmationPassword
|
||||||
|
? "Passwords must match"
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
errors = handleError({ ...errors, ...fieldError })
|
||||||
|
}}
|
||||||
|
error={errors.password}
|
||||||
|
disabled={onboarding}
|
||||||
|
/>
|
||||||
|
<FancyInput
|
||||||
|
label="Repeat password"
|
||||||
|
value={formData.confirmationPassword}
|
||||||
|
type="password"
|
||||||
|
on:change={e => {
|
||||||
|
formData = {
|
||||||
|
...formData,
|
||||||
|
confirmationPassword: e.detail,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
validate={() => {
|
||||||
|
let fieldError = {
|
||||||
|
confirmationPassword:
|
||||||
|
!passwordsMatch(
|
||||||
|
formData.password,
|
||||||
|
formData.confirmationPassword
|
||||||
|
) && formData.password
|
||||||
|
? "Passwords must match"
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
errors = handleError({ ...errors, ...fieldError })
|
||||||
|
}}
|
||||||
|
error={errors.confirmationPassword}
|
||||||
|
disabled={onboarding}
|
||||||
|
/>
|
||||||
|
</FancyForm>
|
||||||
|
</Layout>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
disabled={Object.keys(errors).length > 0 || onboarding}
|
||||||
|
cta
|
||||||
|
on:click={acceptInvite}
|
||||||
|
>
|
||||||
|
Create account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</TestimonialPage>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 300px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
img {
|
img {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script>
|
||||||
|
import { ActionButton, Modal } from "@budibase/bbui"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import { environment } from "stores/portal"
|
||||||
|
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
|
||||||
|
|
||||||
|
export let row
|
||||||
|
|
||||||
|
let editVariableModal
|
||||||
|
let deleteDialog
|
||||||
|
|
||||||
|
const save = async data => {
|
||||||
|
await environment.updateVariable(data)
|
||||||
|
editVariableModal.hide()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionButton size="S" on:click={editVariableModal.show}>Edit</ActionButton>
|
||||||
|
|
||||||
|
<Modal bind:this={editVariableModal}>
|
||||||
|
<CreateEditVariableModal {row} {save} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={deleteDialog}
|
||||||
|
onOk={async () => {
|
||||||
|
await environment.deleteVariable(row.name)
|
||||||
|
}}
|
||||||
|
okText="Delete Environment Variable"
|
||||||
|
title="Confirm Deletion"
|
||||||
|
>
|
||||||
|
Are you sure you wish to delete the environment variable
|
||||||
|
<i>{row.name}?</i>
|
||||||
|
This action cannot be undone.
|
||||||
|
</ConfirmDialog>
|
|
@ -0,0 +1,145 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Modal,
|
||||||
|
Table,
|
||||||
|
Tags,
|
||||||
|
Tag,
|
||||||
|
InlineAlert,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { environment, licensing, auth, admin } from "stores/portal"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
|
||||||
|
import EditVariableColumn from "./_components/EditVariableColumn.svelte"
|
||||||
|
|
||||||
|
let modal
|
||||||
|
|
||||||
|
const customRenderers = [{ column: "edit", component: EditVariableColumn }]
|
||||||
|
|
||||||
|
$: noEncryptionKey = $environment.status?.encryptionKeyAvailable === false
|
||||||
|
$: schema = buildSchema(noEncryptionKey)
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await environment.checkStatus()
|
||||||
|
await environment.loadVariables()
|
||||||
|
})
|
||||||
|
|
||||||
|
const buildSchema = noEncryptionKey => {
|
||||||
|
const schema = {
|
||||||
|
name: {
|
||||||
|
width: "2fr",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (!noEncryptionKey) {
|
||||||
|
schema.edit = {
|
||||||
|
width: "auto",
|
||||||
|
borderLeft: true,
|
||||||
|
displayName: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = async data => {
|
||||||
|
try {
|
||||||
|
await environment.createVariable(data)
|
||||||
|
modal.hide()
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Error saving variable: ${err.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Layout noPadding>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<div class="title">
|
||||||
|
<Heading size="M">Environment Variables</Heading>
|
||||||
|
{#if !$licensing.environmentVariablesEnabled}
|
||||||
|
<Tags>
|
||||||
|
<Tag icon="LockClosed">Business plan</Tag>
|
||||||
|
</Tags>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Body
|
||||||
|
>Add and manage environment variables for development and production</Body
|
||||||
|
>
|
||||||
|
</Layout>
|
||||||
|
<Divider size="S" />
|
||||||
|
|
||||||
|
{#if $licensing.environmentVariablesEnabled}
|
||||||
|
{#if noEncryptionKey}
|
||||||
|
<InlineAlert
|
||||||
|
message="Your Budibase installation does not have a key for encryption, please update your app service's environment variables to contain an 'ENCRYPTION_KEY' value."
|
||||||
|
header="No encryption key found"
|
||||||
|
type="error"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<Button on:click={modal.show} cta disabled={noEncryptionKey}
|
||||||
|
>Add Variable</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Layout noPadding>
|
||||||
|
<Table
|
||||||
|
{schema}
|
||||||
|
data={$environment.variables}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
{customRenderers}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
{:else}
|
||||||
|
<div class="buttons">
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
disabled={!$auth.accountPortalAccess && $admin.cloud}
|
||||||
|
on:click={async () => {
|
||||||
|
await environment.upgradePanelOpened()
|
||||||
|
$licensing.goToUpgradePage()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Upgrade
|
||||||
|
</Button>
|
||||||
|
<!--Show the view plans button-->
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
on:click={() => {
|
||||||
|
window.open("https://budibase.com/pricing/", "_blank")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View Plans
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<CreateEditVariableModal {save} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -28,7 +28,7 @@ export function createDatasourcesStore() {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateDatasource = async response => {
|
const updateDatasource = response => {
|
||||||
const { datasource, error } = response
|
const { datasource, error } = response
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
const currentIdx = state.list.findIndex(ds => ds._id === datasource._id)
|
const currentIdx = state.list.findIndex(ds => ds._id === datasource._id)
|
||||||
|
@ -52,7 +52,7 @@ export function createDatasourcesStore() {
|
||||||
datasourceId: datasource?._id,
|
datasourceId: datasource?._id,
|
||||||
tablesFilter,
|
tablesFilter,
|
||||||
})
|
})
|
||||||
return await updateDatasource(response)
|
return updateDatasource(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = async (body, fetchSchema = false) => {
|
const save = async (body, fetchSchema = false) => {
|
||||||
|
|
68
packages/builder/src/stores/portal/environment.js
Normal file
68
packages/builder/src/stores/portal/environment.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { API } from "api"
|
||||||
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
|
||||||
|
export function createEnvironmentStore() {
|
||||||
|
const { subscribe, update } = writable({
|
||||||
|
variables: [],
|
||||||
|
status: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
async function checkStatus() {
|
||||||
|
const status = await API.checkEnvironmentVariableStatus()
|
||||||
|
update(store => {
|
||||||
|
store.status = status
|
||||||
|
return store
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadVariables() {
|
||||||
|
const envVars = await API.fetchEnvironmentVariables()
|
||||||
|
const mappedVars = envVars.variables.map(name => ({ name }))
|
||||||
|
update(store => {
|
||||||
|
store.variables = mappedVars
|
||||||
|
return store
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createVariable(data) {
|
||||||
|
await API.createEnvironmentVariable(data)
|
||||||
|
let mappedVar = { name: data.name }
|
||||||
|
update(store => {
|
||||||
|
store.variables = [mappedVar, ...store.variables]
|
||||||
|
return store
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteVariable(varName) {
|
||||||
|
await API.deleteEnvironmentVariable(varName)
|
||||||
|
update(store => {
|
||||||
|
store.variables = store.variables.filter(
|
||||||
|
envVar => envVar.name !== varName
|
||||||
|
)
|
||||||
|
return store
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateVariable(data) {
|
||||||
|
await API.updateEnvironmentVariable(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upgradePanelOpened() {
|
||||||
|
await API.publishEvent(
|
||||||
|
Constants.EventPublishType.ENV_VAR_UPGRADE_PANEL_OPENED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
checkStatus,
|
||||||
|
loadVariables,
|
||||||
|
createVariable,
|
||||||
|
deleteVariable,
|
||||||
|
updateVariable,
|
||||||
|
upgradePanelOpened,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const environment = createEnvironmentStore()
|
|
@ -11,4 +11,5 @@ export { groups } from "./groups"
|
||||||
export { plugins } from "./plugins"
|
export { plugins } from "./plugins"
|
||||||
export { backups } from "./backups"
|
export { backups } from "./backups"
|
||||||
export { overview } from "./overview"
|
export { overview } from "./overview"
|
||||||
|
export { environment } from "./environment"
|
||||||
export { menu } from "./menu"
|
export { menu } from "./menu"
|
||||||
|
|
|
@ -60,6 +60,9 @@ export const createLicensingStore = () => {
|
||||||
const backupsEnabled = license.features.includes(
|
const backupsEnabled = license.features.includes(
|
||||||
Constants.Features.BACKUPS
|
Constants.Features.BACKUPS
|
||||||
)
|
)
|
||||||
|
const environmentVariablesEnabled = license.features.includes(
|
||||||
|
Constants.Features.ENVIRONMENT_VARIABLES
|
||||||
|
)
|
||||||
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
return {
|
return {
|
||||||
|
@ -68,6 +71,7 @@ export const createLicensingStore = () => {
|
||||||
isFreePlan,
|
isFreePlan,
|
||||||
groupsEnabled,
|
groupsEnabled,
|
||||||
backupsEnabled,
|
backupsEnabled,
|
||||||
|
environmentVariablesEnabled,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -50,6 +50,10 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
||||||
title: "Organisation",
|
title: "Organisation",
|
||||||
href: "/builder/portal/settings/organisation",
|
href: "/builder/portal/settings/organisation",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Environment",
|
||||||
|
href: "/builder/portal/settings/environment",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
if (!$admin.cloud) {
|
if (!$admin.cloud) {
|
||||||
settingsSubPages.push({
|
settingsSubPages.push({
|
||||||
|
|
|
@ -29,13 +29,19 @@ export function createUsersStore() {
|
||||||
async function invite(payload) {
|
async function invite(payload) {
|
||||||
return API.inviteUsers(payload)
|
return API.inviteUsers(payload)
|
||||||
}
|
}
|
||||||
async function acceptInvite(inviteCode, password) {
|
async function acceptInvite(inviteCode, password, firstName, lastName) {
|
||||||
return API.acceptInvite({
|
return API.acceptInvite({
|
||||||
inviteCode,
|
inviteCode,
|
||||||
password,
|
password,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchInvite(inviteCode) {
|
||||||
|
return API.getUserInvite(inviteCode)
|
||||||
|
}
|
||||||
|
|
||||||
async function create(data) {
|
async function create(data) {
|
||||||
let mappedUsers = data.users.map(user => {
|
let mappedUsers = data.users.map(user => {
|
||||||
const body = {
|
const body = {
|
||||||
|
@ -101,6 +107,7 @@ export function createUsersStore() {
|
||||||
fetch,
|
fetch,
|
||||||
invite,
|
invite,
|
||||||
acceptInvite,
|
acceptInvite,
|
||||||
|
fetchInvite,
|
||||||
create,
|
create,
|
||||||
save,
|
save,
|
||||||
bulkDelete,
|
bulkDelete,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "2.2.12-alpha.44",
|
"version": "2.2.12-alpha.50",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -26,9 +26,9 @@
|
||||||
"outputPath": "build"
|
"outputPath": "build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/backend-core": "2.2.12-alpha.44",
|
"@budibase/backend-core": "2.2.12-alpha.50",
|
||||||
"@budibase/string-templates": "2.2.12-alpha.44",
|
"@budibase/string-templates": "2.2.12-alpha.50",
|
||||||
"@budibase/types": "2.2.12-alpha.44",
|
"@budibase/types": "2.2.12-alpha.50",
|
||||||
"axios": "0.21.2",
|
"axios": "0.21.2",
|
||||||
"chalk": "4.1.0",
|
"chalk": "4.1.0",
|
||||||
"cli-progress": "3.11.2",
|
"cli-progress": "3.11.2",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "2.2.12-alpha.44",
|
"version": "2.2.12-alpha.50",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.2.12-alpha.44",
|
"@budibase/bbui": "2.2.12-alpha.50",
|
||||||
"@budibase/frontend-core": "2.2.12-alpha.44",
|
"@budibase/frontend-core": "2.2.12-alpha.50",
|
||||||
"@budibase/string-templates": "2.2.12-alpha.44",
|
"@budibase/string-templates": "2.2.12-alpha.50",
|
||||||
"@spectrum-css/button": "^3.0.3",
|
"@spectrum-css/button": "^3.0.3",
|
||||||
"@spectrum-css/card": "^3.0.3",
|
"@spectrum-css/card": "^3.0.3",
|
||||||
"@spectrum-css/divider": "^1.0.3",
|
"@spectrum-css/divider": "^1.0.3",
|
||||||
|
@ -48,6 +48,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-alias": "^3.1.5",
|
"@rollup/plugin-alias": "^3.1.5",
|
||||||
"@rollup/plugin-commonjs": "^18.0.0",
|
"@rollup/plugin-commonjs": "^18.0.0",
|
||||||
|
"@rollup/plugin-image": "^3.0.2",
|
||||||
"@rollup/plugin-node-resolve": "^11.2.1",
|
"@rollup/plugin-node-resolve": "^11.2.1",
|
||||||
"postcss": "^8.2.10",
|
"postcss": "^8.2.10",
|
||||||
"rollup": "^2.44.0",
|
"rollup": "^2.44.0",
|
||||||
|
|
|
@ -5,6 +5,7 @@ import svelte from "rollup-plugin-svelte"
|
||||||
import { terser } from "rollup-plugin-terser"
|
import { terser } from "rollup-plugin-terser"
|
||||||
import postcss from "rollup-plugin-postcss"
|
import postcss from "rollup-plugin-postcss"
|
||||||
import svg from "rollup-plugin-svg"
|
import svg from "rollup-plugin-svg"
|
||||||
|
import image from "@rollup/plugin-image"
|
||||||
import json from "rollup-plugin-json"
|
import json from "rollup-plugin-json"
|
||||||
import nodePolyfills from "rollup-plugin-polyfill-node"
|
import nodePolyfills from "rollup-plugin-polyfill-node"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
@ -87,6 +88,7 @@ export default {
|
||||||
dedupe: ["svelte", "svelte/internal"],
|
dedupe: ["svelte", "svelte/internal"],
|
||||||
}),
|
}),
|
||||||
svg(),
|
svg(),
|
||||||
|
image(),
|
||||||
json(),
|
json(),
|
||||||
production && terser(),
|
production && terser(),
|
||||||
!production && visualizer(),
|
!production && visualizer(),
|
||||||
|
|
|
@ -83,6 +83,14 @@
|
||||||
magic-string "^0.25.7"
|
magic-string "^0.25.7"
|
||||||
resolve "^1.17.0"
|
resolve "^1.17.0"
|
||||||
|
|
||||||
|
"@rollup/plugin-image@^3.0.2":
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/plugin-image/-/plugin-image-3.0.2.tgz#8a66389510517495c5d10d392140cdefa43b27c2"
|
||||||
|
integrity sha512-eGVrD6lummWH5ENo9LWX3JY62uBb9okUNQ2htXkugrG6WjACrMUVhWvss+0wW3fwJWmFYpoEny3yL4spEdh15g==
|
||||||
|
dependencies:
|
||||||
|
"@rollup/pluginutils" "^5.0.1"
|
||||||
|
mini-svg-data-uri "^1.4.4"
|
||||||
|
|
||||||
"@rollup/plugin-inject@^4.0.0":
|
"@rollup/plugin-inject@^4.0.0":
|
||||||
version "4.0.4"
|
version "4.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-inject/-/plugin-inject-4.0.4.tgz#fbeee66e9a700782c4f65c8b0edbafe58678fbc2"
|
resolved "https://registry.yarnpkg.com/@rollup/plugin-inject/-/plugin-inject-4.0.4.tgz#fbeee66e9a700782c4f65c8b0edbafe58678fbc2"
|
||||||
|
@ -113,6 +121,15 @@
|
||||||
estree-walker "^1.0.1"
|
estree-walker "^1.0.1"
|
||||||
picomatch "^2.2.2"
|
picomatch "^2.2.2"
|
||||||
|
|
||||||
|
"@rollup/pluginutils@^5.0.1":
|
||||||
|
version "5.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.2.tgz#012b8f53c71e4f6f9cb317e311df1404f56e7a33"
|
||||||
|
integrity sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==
|
||||||
|
dependencies:
|
||||||
|
"@types/estree" "^1.0.0"
|
||||||
|
estree-walker "^2.0.2"
|
||||||
|
picomatch "^2.3.1"
|
||||||
|
|
||||||
"@socket.io/component-emitter@~3.1.0":
|
"@socket.io/component-emitter@~3.1.0":
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
|
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
|
||||||
|
@ -182,6 +199,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
|
||||||
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
|
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
|
||||||
|
|
||||||
|
"@types/estree@^1.0.0":
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
|
||||||
|
integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "16.11.7"
|
version "16.11.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.7.tgz#36820945061326978c42a01e56b61cd223dfdc42"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.7.tgz#36820945061326978c42a01e56b61cd223dfdc42"
|
||||||
|
@ -599,7 +621,7 @@ estree-walker@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
|
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
|
||||||
integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
|
integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
|
||||||
|
|
||||||
estree-walker@^2.0.1:
|
estree-walker@^2.0.1, estree-walker@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
|
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
|
||||||
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
|
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
|
||||||
|
@ -845,6 +867,11 @@ merge-stream@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
|
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
|
||||||
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
|
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
|
||||||
|
|
||||||
|
mini-svg-data-uri@^1.4.4:
|
||||||
|
version "1.4.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939"
|
||||||
|
integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==
|
||||||
|
|
||||||
minimatch@^3.0.2, minimatch@^3.0.4:
|
minimatch@^3.0.2, minimatch@^3.0.4:
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||||
|
@ -955,6 +982,11 @@ picomatch@^2.2.2:
|
||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
|
||||||
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
|
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
|
||||||
|
|
||||||
|
picomatch@^2.3.1:
|
||||||
|
version "2.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||||
|
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||||
|
|
||||||
pify@^5.0.0:
|
pify@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f"
|
resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f"
|
||||||
|
|
BIN
packages/frontend-core/assets/bg.png
Normal file
BIN
packages/frontend-core/assets/bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 MiB |
BIN
packages/frontend-core/assets/bulgaria.png
Normal file
BIN
packages/frontend-core/assets/bulgaria.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 105 KiB |
BIN
packages/frontend-core/assets/covanta.png
Normal file
BIN
packages/frontend-core/assets/covanta.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
BIN
packages/frontend-core/assets/schnellecke.png
Normal file
BIN
packages/frontend-core/assets/schnellecke.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/frontend-core",
|
"name": "@budibase/frontend-core",
|
||||||
"version": "2.2.12-alpha.44",
|
"version": "2.2.12-alpha.50",
|
||||||
"description": "Budibase frontend core libraries used in builder and client",
|
"description": "Budibase frontend core libraries used in builder and client",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.2.12-alpha.44",
|
"@budibase/bbui": "2.2.12-alpha.50",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"svelte": "^3.46.2"
|
"svelte": "^3.46.2"
|
||||||
}
|
}
|
||||||
|
|
36
packages/frontend-core/src/api/environmentVariables.js
Normal file
36
packages/frontend-core/src/api/environmentVariables.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
export const buildEnvironmentVariableEndpoints = API => ({
|
||||||
|
checkEnvironmentVariableStatus: async () => {
|
||||||
|
return await API.get({
|
||||||
|
url: `/api/env/variables/status`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a list of environment variables
|
||||||
|
*/
|
||||||
|
fetchEnvironmentVariables: async () => {
|
||||||
|
return await API.get({
|
||||||
|
url: `/api/env/variables`,
|
||||||
|
json: false,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
createEnvironmentVariable: async data => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/env/variables`,
|
||||||
|
body: data,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteEnvironmentVariable: async varName => {
|
||||||
|
return await API.delete({
|
||||||
|
url: `/api/env/variables/${varName}`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
updateEnvironmentVariable: async data => {
|
||||||
|
return await API.patch({
|
||||||
|
url: `/api/env/variables/${data.name}`,
|
||||||
|
body: data,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
13
packages/frontend-core/src/api/events.js
Normal file
13
packages/frontend-core/src/api/events.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export const buildEventEndpoints = API => ({
|
||||||
|
/**
|
||||||
|
* Publish a specific event to the backend.
|
||||||
|
*/
|
||||||
|
publishEvent: async eventType => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/global/event/publish`,
|
||||||
|
body: {
|
||||||
|
type: eventType,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
|
@ -26,7 +26,8 @@ import { buildLicensingEndpoints } from "./licensing"
|
||||||
import { buildGroupsEndpoints } from "./groups"
|
import { buildGroupsEndpoints } from "./groups"
|
||||||
import { buildPluginEndpoints } from "./plugins"
|
import { buildPluginEndpoints } from "./plugins"
|
||||||
import { buildBackupsEndpoints } from "./backups"
|
import { buildBackupsEndpoints } from "./backups"
|
||||||
|
import { buildEnvironmentVariableEndpoints } from "./environmentVariables"
|
||||||
|
import { buildEventEndpoints } from "./events"
|
||||||
const defaultAPIClientConfig = {
|
const defaultAPIClientConfig = {
|
||||||
/**
|
/**
|
||||||
* Certain definitions can't change at runtime for client apps, such as the
|
* Certain definitions can't change at runtime for client apps, such as the
|
||||||
|
@ -247,5 +248,7 @@ export const createAPIClient = config => {
|
||||||
...buildGroupsEndpoints(API),
|
...buildGroupsEndpoints(API),
|
||||||
...buildPluginEndpoints(API),
|
...buildPluginEndpoints(API),
|
||||||
...buildBackupsEndpoints(API),
|
...buildBackupsEndpoints(API),
|
||||||
|
...buildEnvironmentVariableEndpoints(API),
|
||||||
|
...buildEventEndpoints(API),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,12 +67,13 @@ export const buildRowEndpoints = API => ({
|
||||||
* @param format the format to export (csv or json)
|
* @param format the format to export (csv or json)
|
||||||
* @param columns which columns to export (all if undefined)
|
* @param columns which columns to export (all if undefined)
|
||||||
*/
|
*/
|
||||||
exportRows: async ({ tableId, rows, format, columns }) => {
|
exportRows: async ({ tableId, rows, format, columns, search }) => {
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: `/api/${tableId}/rows/exportRows?format=${format}`,
|
url: `/api/${tableId}/rows/exportRows?format=${format}`,
|
||||||
body: {
|
body: {
|
||||||
rows,
|
rows,
|
||||||
columns,
|
columns,
|
||||||
|
...search,
|
||||||
},
|
},
|
||||||
parseResponse: async response => {
|
parseResponse: async response => {
|
||||||
return await response.text()
|
return await response.text()
|
||||||
|
|
|
@ -146,6 +146,16 @@ export const buildUserEndpoints = API => ({
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the invitation associated with a provided code.
|
||||||
|
* @param code The unique code for the target invite
|
||||||
|
*/
|
||||||
|
getUserInvite: async code => {
|
||||||
|
return await API.get({
|
||||||
|
url: `/api/global/users/invite/${code}`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invites multiple users to the current tenant.
|
* Invites multiple users to the current tenant.
|
||||||
* @param users An array of users to invite
|
* @param users An array of users to invite
|
||||||
|
@ -168,13 +178,17 @@ export const buildUserEndpoints = API => ({
|
||||||
* Accepts an invite to join the platform and creates a user.
|
* Accepts an invite to join the platform and creates a user.
|
||||||
* @param inviteCode the invite code sent in the email
|
* @param inviteCode the invite code sent in the email
|
||||||
* @param password the password for the newly created user
|
* @param password the password for the newly created user
|
||||||
|
* @param firstName the first name of the new user
|
||||||
|
* @param lastName the last name of the new user
|
||||||
*/
|
*/
|
||||||
acceptInvite: async ({ inviteCode, password }) => {
|
acceptInvite: async ({ inviteCode, password, firstName, lastName }) => {
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: "/api/global/users/invite/accept",
|
url: "/api/global/users/invite/accept",
|
||||||
body: {
|
body: {
|
||||||
inviteCode,
|
inviteCode,
|
||||||
password,
|
password,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
|
<script>
|
||||||
|
import BG from "../../assets/bg.png"
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="split-page">
|
<div class="split-page">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right spectrum spectrum--darkest">
|
||||||
|
<img src={BG} alt="background" />
|
||||||
<slot name="right" />
|
<slot name="right" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,15 +30,19 @@
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.right {
|
.right {
|
||||||
background: linear-gradient(
|
position: relative;
|
||||||
to bottom right,
|
}
|
||||||
var(--spectrum-global-color-gray-300) 0%,
|
.right img {
|
||||||
var(--background) 100%
|
position: absolute;
|
||||||
);
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
min-height: 480px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 740px) {
|
@media (max-width: 740px) {
|
||||||
|
|
|
@ -1,6 +1,34 @@
|
||||||
<script>
|
<script>
|
||||||
import SplitPage from "./SplitPage.svelte"
|
import SplitPage from "./SplitPage.svelte"
|
||||||
import { Layout } from "@budibase/bbui"
|
import { Layout } from "@budibase/bbui"
|
||||||
|
import Bulgaria from "../../assets/bulgaria.png"
|
||||||
|
import Covanta from "../../assets/covanta.png"
|
||||||
|
import Schnellecke from "../../assets/schnellecke.png"
|
||||||
|
|
||||||
|
const testimonials = [
|
||||||
|
{
|
||||||
|
text: "Budibase was the only solution that checked all the boxes for Covanta. Covanta expects to realize $3.2MM in savings due to the elimination of redundant data entry.",
|
||||||
|
name: "Charles Link",
|
||||||
|
role: "Senior Director, Data and Analytics",
|
||||||
|
image: Covanta,
|
||||||
|
imageSize: 105,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Budibase was mission-critical for us and went a long way in preventing what could have become a humanitarian crisis here in Bulgaria.",
|
||||||
|
name: "Bozhidar Bozhanov",
|
||||||
|
role: "Government of Bulgaria",
|
||||||
|
image: Bulgaria,
|
||||||
|
imageSize: 49,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Centralization of authentication, quick turnaround time for requests, integration with different database systems has given it the edge and it’s now used daily for internal development for those apps that you know you need but don’t feel value in losing days of development to reinvent the wheel.",
|
||||||
|
name: "Davide Lenzarini",
|
||||||
|
role: "IT manager",
|
||||||
|
image: Schnellecke,
|
||||||
|
imageSize: 141,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const testimonial = testimonials[Math.floor(Math.random() * 3)]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SplitPage>
|
<SplitPage>
|
||||||
|
@ -8,19 +36,17 @@
|
||||||
<div class="wrapper" slot="right">
|
<div class="wrapper" slot="right">
|
||||||
<div class="testimonial">
|
<div class="testimonial">
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
|
<img
|
||||||
|
width={testimonial.imageSize}
|
||||||
|
alt="a-happy-budibase-user"
|
||||||
|
src={testimonial.image}
|
||||||
|
/>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
"Here is an example of how Budibase changed my life for the better and
|
"{testimonial.text}"
|
||||||
now all I do is eat, sleep, build apps, repeat."
|
|
||||||
</div>
|
</div>
|
||||||
<div class="user">
|
<div class="author">
|
||||||
<img
|
<div class="name">{testimonial.name}</div>
|
||||||
alt="a-happy-budibase-user"
|
<div class="company">{testimonial.role}</div>
|
||||||
src="https://icon-library.com/images/male-user-icon/male-user-icon-24.jpg"
|
|
||||||
/>
|
|
||||||
<div class="author">
|
|
||||||
<div class="name">No-code Enthusiast</div>
|
|
||||||
<div class="company">Bedroom TLD</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,23 +61,13 @@
|
||||||
place-items: center;
|
place-items: center;
|
||||||
}
|
}
|
||||||
.testimonial {
|
.testimonial {
|
||||||
width: 280px;
|
width: 380px;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
}
|
}
|
||||||
.text {
|
.text {
|
||||||
font-size: var(--font-size-l);
|
font-size: var(--font-size-l);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
img {
|
|
||||||
width: 40px;
|
|
||||||
}
|
|
||||||
.user {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
}
|
|
||||||
.name {
|
.name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--spectrum-global-color-gray-900);
|
color: var(--spectrum-global-color-gray-900);
|
||||||
|
|
|
@ -114,6 +114,7 @@ export const ApiVersion = "1"
|
||||||
export const Features = {
|
export const Features = {
|
||||||
USER_GROUPS: "userGroups",
|
USER_GROUPS: "userGroups",
|
||||||
BACKUPS: "appBackups",
|
BACKUPS: "appBackups",
|
||||||
|
ENVIRONMENT_VARIABLES: "environmentVariables",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Role IDs
|
// Role IDs
|
||||||
|
@ -174,3 +175,7 @@ export const Themes = [
|
||||||
base: "darkest",
|
base: "darkest",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const EventPublishType = {
|
||||||
|
ENV_VAR_UPGRADE_PANEL_OPENED: "environment_variable_upgrade_panel_opened",
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/sdk",
|
"name": "@budibase/sdk",
|
||||||
"version": "2.2.12-alpha.44",
|
"version": "2.2.12-alpha.50",
|
||||||
"description": "Budibase Public API SDK",
|
"description": "Budibase Public API SDK",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
// @ts-ignore
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
module FetchMock {
|
module FetchMock {
|
||||||
|
// @ts-ignore
|
||||||
const fetch = jest.requireActual("node-fetch")
|
const fetch = jest.requireActual("node-fetch")
|
||||||
let failCount = 0
|
let failCount = 0
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "2.2.12-alpha.44",
|
"version": "2.2.12-alpha.50",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -43,11 +43,11 @@
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "10.0.3",
|
"@apidevtools/swagger-parser": "10.0.3",
|
||||||
"@budibase/backend-core": "2.2.12-alpha.44",
|
"@budibase/backend-core": "2.2.12-alpha.50",
|
||||||
"@budibase/client": "2.2.12-alpha.44",
|
"@budibase/client": "2.2.12-alpha.50",
|
||||||
"@budibase/pro": "2.2.12-alpha.44",
|
"@budibase/pro": "2.2.12-alpha.50",
|
||||||
"@budibase/string-templates": "2.2.12-alpha.44",
|
"@budibase/string-templates": "2.2.12-alpha.50",
|
||||||
"@budibase/types": "2.2.12-alpha.44",
|
"@budibase/types": "2.2.12-alpha.50",
|
||||||
"@bull-board/api": "3.7.0",
|
"@bull-board/api": "3.7.0",
|
||||||
"@bull-board/koa": "3.9.4",
|
"@bull-board/koa": "3.9.4",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
|
|
@ -29,6 +29,7 @@ async function init() {
|
||||||
ACCOUNT_PORTAL_URL: "http://localhost:10001",
|
ACCOUNT_PORTAL_URL: "http://localhost:10001",
|
||||||
ACCOUNT_PORTAL_API_KEY: "budibase",
|
ACCOUNT_PORTAL_API_KEY: "budibase",
|
||||||
JWT_SECRET: "testsecret",
|
JWT_SECRET: "testsecret",
|
||||||
|
ENCRYPTION_KEY: "testsecret",
|
||||||
REDIS_PASSWORD: "budibase",
|
REDIS_PASSWORD: "budibase",
|
||||||
MINIO_ACCESS_KEY: "budibase",
|
MINIO_ACCESS_KEY: "budibase",
|
||||||
MINIO_SECRET_KEY: "budibase",
|
MINIO_SECRET_KEY: "budibase",
|
||||||
|
|
|
@ -112,12 +112,11 @@ function checkAppName(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createInstance(template: any, includeSampleData: boolean) {
|
async function createInstance(
|
||||||
const tenantId = tenancy.isMultiTenant() ? tenancy.getTenantId() : null
|
appId: string,
|
||||||
const baseAppId = generateAppID(tenantId)
|
template: any,
|
||||||
const appId = generateDevAppID(baseAppId)
|
includeSampleData: boolean
|
||||||
await context.updateAppId(appId)
|
) {
|
||||||
|
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
await db.put({
|
await db.put({
|
||||||
_id: "_design/database",
|
_id: "_design/database",
|
||||||
|
@ -250,82 +249,90 @@ async function performAppCreate(ctx: BBContext) {
|
||||||
instanceConfig.file = ctx.request.files.templateFile
|
instanceConfig.file = ctx.request.files.templateFile
|
||||||
}
|
}
|
||||||
const includeSampleData = isQsTrue(ctx.request.body.sampleData)
|
const includeSampleData = isQsTrue(ctx.request.body.sampleData)
|
||||||
const instance = await createInstance(instanceConfig, includeSampleData)
|
const tenantId = tenancy.isMultiTenant() ? tenancy.getTenantId() : null
|
||||||
const appId = instance._id
|
const appId = generateDevAppID(generateAppID(tenantId))
|
||||||
const db = context.getAppDB()
|
|
||||||
|
|
||||||
let newApplication: App = {
|
return await context.doInAppContext(appId, async () => {
|
||||||
_id: DocumentType.APP_METADATA,
|
const instance = await createInstance(
|
||||||
_rev: undefined,
|
appId,
|
||||||
appId,
|
instanceConfig,
|
||||||
type: "app",
|
includeSampleData
|
||||||
version: packageJson.version,
|
)
|
||||||
componentLibraries: ["@budibase/standard-components"],
|
const db = context.getAppDB()
|
||||||
name: name,
|
|
||||||
url: url,
|
|
||||||
template: templateKey,
|
|
||||||
instance,
|
|
||||||
tenantId: tenancy.getTenantId(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
status: AppStatus.DEV,
|
|
||||||
navigation: {
|
|
||||||
navigation: "Top",
|
|
||||||
title: name,
|
|
||||||
navWidth: "Large",
|
|
||||||
navBackground: "var(--spectrum-global-color-gray-100)",
|
|
||||||
links: [
|
|
||||||
{
|
|
||||||
url: "/home",
|
|
||||||
text: "Home",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
theme: "spectrum--light",
|
|
||||||
customTheme: {
|
|
||||||
buttonBorderRadius: "16px",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we used a template or imported an app there will be an existing doc.
|
let newApplication: App = {
|
||||||
// Fetch and migrate some metadata from the existing app.
|
_id: DocumentType.APP_METADATA,
|
||||||
try {
|
_rev: undefined,
|
||||||
const existing: App = await db.get(DocumentType.APP_METADATA)
|
appId,
|
||||||
const keys: (keyof App)[] = [
|
type: "app",
|
||||||
"_rev",
|
version: packageJson.version,
|
||||||
"navigation",
|
componentLibraries: ["@budibase/standard-components"],
|
||||||
"theme",
|
name: name,
|
||||||
"customTheme",
|
url: url,
|
||||||
"icon",
|
template: templateKey,
|
||||||
]
|
instance,
|
||||||
keys.forEach(key => {
|
tenantId: tenancy.getTenantId(),
|
||||||
if (existing[key]) {
|
updatedAt: new Date().toISOString(),
|
||||||
// @ts-ignore
|
createdAt: new Date().toISOString(),
|
||||||
newApplication[key] = existing[key]
|
status: AppStatus.DEV,
|
||||||
}
|
navigation: {
|
||||||
})
|
navigation: "Top",
|
||||||
|
title: name,
|
||||||
// Migrate navigation settings and screens if required
|
navWidth: "Large",
|
||||||
if (existing) {
|
navBackground: "var(--spectrum-global-color-gray-100)",
|
||||||
const navigation = await migrateAppNavigation()
|
links: [
|
||||||
if (navigation) {
|
{
|
||||||
newApplication.navigation = navigation
|
url: "/home",
|
||||||
}
|
text: "Home",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
theme: "spectrum--light",
|
||||||
|
customTheme: {
|
||||||
|
buttonBorderRadius: "16px",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
// Nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await db.put(newApplication, { force: true })
|
// If we used a template or imported an app there will be an existing doc.
|
||||||
newApplication._rev = response.rev
|
// Fetch and migrate some metadata from the existing app.
|
||||||
|
try {
|
||||||
|
const existing: App = await db.get(DocumentType.APP_METADATA)
|
||||||
|
const keys: (keyof App)[] = [
|
||||||
|
"_rev",
|
||||||
|
"navigation",
|
||||||
|
"theme",
|
||||||
|
"customTheme",
|
||||||
|
"icon",
|
||||||
|
]
|
||||||
|
keys.forEach(key => {
|
||||||
|
if (existing[key]) {
|
||||||
|
// @ts-ignore
|
||||||
|
newApplication[key] = existing[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/* istanbul ignore next */
|
// Migrate navigation settings and screens if required
|
||||||
if (!env.isTest()) {
|
if (existing) {
|
||||||
await createApp(appId)
|
const navigation = await migrateAppNavigation()
|
||||||
}
|
if (navigation) {
|
||||||
|
newApplication.navigation = navigation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
await cache.app.invalidateAppMetadata(appId, newApplication)
|
const response = await db.put(newApplication, { force: true })
|
||||||
return newApplication
|
newApplication._rev = response.rev
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (!env.isTest()) {
|
||||||
|
await createApp(appId)
|
||||||
|
}
|
||||||
|
|
||||||
|
await cache.app.invalidateAppMetadata(appId, newApplication)
|
||||||
|
return newApplication
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function creationEvents(request: any, app: App) {
|
async function creationEvents(request: any, app: App) {
|
||||||
|
|
|
@ -86,7 +86,7 @@ export async function importApps(ctx: Ctx) {
|
||||||
if (Array.isArray(file)) {
|
if (Array.isArray(file)) {
|
||||||
ctx.throw(400, "Single file is required")
|
ctx.throw(400, "Single file is required")
|
||||||
}
|
}
|
||||||
if (file.type !== "application/gzip") {
|
if (file.type !== "application/gzip" && file.type !== "application/x-gzip") {
|
||||||
ctx.throw(400, "Import file must be a gzipped tarball.")
|
ctx.throw(400, "Import file must be a gzipped tarball.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,11 @@ import { getIntegration } from "../../integrations"
|
||||||
import { getDatasourceAndQuery } from "./row/utils"
|
import { getDatasourceAndQuery } from "./row/utils"
|
||||||
import { invalidateDynamicVariables } from "../../threads/utils"
|
import { invalidateDynamicVariables } from "../../threads/utils"
|
||||||
import { db as dbCore, context, events } from "@budibase/backend-core"
|
import { db as dbCore, context, events } from "@budibase/backend-core"
|
||||||
import { BBContext, Datasource, Row } from "@budibase/types"
|
import { UserCtx, Datasource, Row } from "@budibase/types"
|
||||||
|
import sdk from "../../sdk"
|
||||||
|
import { mergeConfigs } from "../../sdk/app/datasources/datasources"
|
||||||
|
|
||||||
export async function fetch(ctx: BBContext) {
|
export async function fetch(ctx: UserCtx) {
|
||||||
// Get internal tables
|
// Get internal tables
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const internalTables = await db.allDocs(
|
const internalTables = await db.allDocs(
|
||||||
|
@ -43,25 +45,23 @@ export async function fetch(ctx: BBContext) {
|
||||||
)
|
)
|
||||||
).rows.map(row => row.doc)
|
).rows.map(row => row.doc)
|
||||||
|
|
||||||
const allDatasources = [bbInternalDb, ...datasources]
|
const allDatasources: Datasource[] = await sdk.datasources.removeSecrets([
|
||||||
|
bbInternalDb,
|
||||||
|
...datasources,
|
||||||
|
])
|
||||||
|
|
||||||
for (let datasource of allDatasources) {
|
for (let datasource of allDatasources) {
|
||||||
if (datasource.config && datasource.config.auth) {
|
|
||||||
// strip secrets from response so they don't show in the network request
|
|
||||||
delete datasource.config.auth
|
|
||||||
}
|
|
||||||
|
|
||||||
if (datasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE) {
|
if (datasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE) {
|
||||||
datasource.entities = internal[datasource._id]
|
datasource.entities = internal[datasource._id!]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = [bbInternalDb, ...datasources]
|
ctx.body = [bbInternalDb, ...datasources]
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildSchemaFromDb(ctx: BBContext) {
|
export async function buildSchemaFromDb(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const datasource = await db.get(ctx.params.datasourceId)
|
const datasource = await sdk.datasources.get(ctx.params.datasourceId)
|
||||||
const tablesFilter = ctx.request.body.tablesFilter
|
const tablesFilter = ctx.request.body.tablesFilter
|
||||||
|
|
||||||
let { tables, error } = await buildSchemaHelper(datasource)
|
let { tables, error } = await buildSchemaHelper(datasource)
|
||||||
|
@ -146,11 +146,11 @@ async function invalidateVariables(
|
||||||
await invalidateDynamicVariables(toInvalidate)
|
await invalidateDynamicVariables(toInvalidate)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function update(ctx: BBContext) {
|
export async function update(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const datasourceId = ctx.params.datasourceId
|
const datasourceId = ctx.params.datasourceId
|
||||||
let datasource = await db.get(datasourceId)
|
let datasource = await sdk.datasources.get(datasourceId)
|
||||||
const auth = datasource.config.auth
|
const auth = datasource.config?.auth
|
||||||
await invalidateVariables(datasource, ctx.request.body)
|
await invalidateVariables(datasource, ctx.request.body)
|
||||||
|
|
||||||
const isBudibaseSource = datasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE
|
const isBudibaseSource = datasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE
|
||||||
|
@ -159,10 +159,13 @@ export async function update(ctx: BBContext) {
|
||||||
? { name: ctx.request.body?.name }
|
? { name: ctx.request.body?.name }
|
||||||
: ctx.request.body
|
: ctx.request.body
|
||||||
|
|
||||||
datasource = { ...datasource, ...dataSourceBody }
|
datasource = {
|
||||||
|
...datasource,
|
||||||
|
...sdk.datasources.mergeConfigs(dataSourceBody, datasource),
|
||||||
|
}
|
||||||
if (auth && !ctx.request.body.auth) {
|
if (auth && !ctx.request.body.auth) {
|
||||||
// don't strip auth config from DB
|
// don't strip auth config from DB
|
||||||
datasource.config.auth = auth
|
datasource.config!.auth = auth
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await db.put(datasource)
|
const response = await db.put(datasource)
|
||||||
|
@ -179,10 +182,12 @@ export async function update(ctx: BBContext) {
|
||||||
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.message = "Datasource saved successfully."
|
ctx.message = "Datasource saved successfully."
|
||||||
ctx.body = { datasource }
|
ctx.body = {
|
||||||
|
datasource: await sdk.datasources.removeSecretSingle(datasource),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function save(ctx: BBContext) {
|
export async function save(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const plus = ctx.request.body.datasource.plus
|
const plus = ctx.request.body.datasource.plus
|
||||||
const fetchSchema = ctx.request.body.fetchSchema
|
const fetchSchema = ctx.request.body.fetchSchema
|
||||||
|
@ -213,7 +218,9 @@ export async function save(ctx: BBContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: any = { datasource }
|
const response: any = {
|
||||||
|
datasource: await sdk.datasources.removeSecretSingle(datasource),
|
||||||
|
}
|
||||||
if (schemaError) {
|
if (schemaError) {
|
||||||
response.error = schemaError
|
response.error = schemaError
|
||||||
}
|
}
|
||||||
|
@ -251,11 +258,11 @@ async function destroyInternalTablesBySourceId(datasourceId: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function destroy(ctx: BBContext) {
|
export async function destroy(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const datasourceId = ctx.params.datasourceId
|
const datasourceId = ctx.params.datasourceId
|
||||||
|
|
||||||
const datasource = await db.get(datasourceId)
|
const datasource = await sdk.datasources.get(datasourceId)
|
||||||
// Delete all queries for the datasource
|
// Delete all queries for the datasource
|
||||||
|
|
||||||
if (datasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE) {
|
if (datasource.type === dbCore.BUDIBASE_DATASOURCE_TYPE) {
|
||||||
|
@ -279,13 +286,14 @@ export async function destroy(ctx: BBContext) {
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function find(ctx: BBContext) {
|
export async function find(ctx: UserCtx) {
|
||||||
const database = context.getAppDB()
|
const database = context.getAppDB()
|
||||||
ctx.body = await database.get(ctx.params.datasourceId)
|
const datasource = await database.get(ctx.params.datasourceId)
|
||||||
|
ctx.body = await sdk.datasources.removeSecretSingle(datasource)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dynamic query functionality
|
// dynamic query functionality
|
||||||
export async function query(ctx: BBContext) {
|
export async function query(ctx: UserCtx) {
|
||||||
const queryJson = ctx.request.body
|
const queryJson = ctx.request.body
|
||||||
try {
|
try {
|
||||||
ctx.body = await getDatasourceAndQuery(queryJson)
|
ctx.body = await getDatasourceAndQuery(queryJson)
|
||||||
|
@ -313,7 +321,7 @@ function updateError(error: any, newError: any, tables: string[]) {
|
||||||
|
|
||||||
async function buildSchemaHelper(datasource: Datasource) {
|
async function buildSchemaHelper(datasource: Datasource) {
|
||||||
const Connector = await getIntegration(datasource.source)
|
const Connector = await getIntegration(datasource.source)
|
||||||
|
datasource = await sdk.datasources.enrich(datasource)
|
||||||
// Connect to the DB and build the schema
|
// Connect to the DB and build the schema
|
||||||
const connector = new Connector(datasource.config)
|
const connector = new Connector(datasource.config)
|
||||||
await connector.buildSchema(datasource._id, datasource.entities)
|
await connector.buildSchema(datasource._id, datasource.entities)
|
||||||
|
|
|
@ -7,6 +7,8 @@ import { invalidateDynamicVariables } from "../../../threads/utils"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import { events, context, utils, constants } from "@budibase/backend-core"
|
import { events, context, utils, constants } from "@budibase/backend-core"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
import { QueryEvent } from "../../../threads/definitions"
|
||||||
|
|
||||||
const Runner = new Thread(ThreadType.QUERY, {
|
const Runner = new Thread(ThreadType.QUERY, {
|
||||||
timeoutMs: env.QUERY_THREAD_TIMEOUT || 10000,
|
timeoutMs: env.QUERY_THREAD_TIMEOUT || 10000,
|
||||||
|
@ -81,7 +83,7 @@ export async function save(ctx: any) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const query = ctx.request.body
|
const query = ctx.request.body
|
||||||
|
|
||||||
const datasource = await db.get(query.datasourceId)
|
const datasource = await sdk.datasources.get(query.datasourceId)
|
||||||
|
|
||||||
let eventFn
|
let eventFn
|
||||||
if (!query._id) {
|
if (!query._id) {
|
||||||
|
@ -126,9 +128,9 @@ function getAuthConfig(ctx: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function preview(ctx: any) {
|
export async function preview(ctx: any) {
|
||||||
const db = context.getAppDB()
|
const { datasource, envVars } = await sdk.datasources.getWithEnvVars(
|
||||||
|
ctx.request.body.datasourceId
|
||||||
const datasource = await db.get(ctx.request.body.datasourceId)
|
)
|
||||||
const query = ctx.request.body
|
const query = ctx.request.body
|
||||||
// preview may not have a queryId as it hasn't been saved, but if it does
|
// preview may not have a queryId as it hasn't been saved, but if it does
|
||||||
// this stops dynamic variables from calling the same query
|
// this stops dynamic variables from calling the same query
|
||||||
|
@ -137,20 +139,22 @@ export async function preview(ctx: any) {
|
||||||
const authConfigCtx: any = getAuthConfig(ctx)
|
const authConfigCtx: any = getAuthConfig(ctx)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const runFn = () =>
|
const inputs: QueryEvent = {
|
||||||
Runner.run({
|
appId: ctx.appId,
|
||||||
appId: ctx.appId,
|
datasource,
|
||||||
datasource,
|
queryVerb,
|
||||||
queryVerb,
|
fields,
|
||||||
fields,
|
parameters,
|
||||||
parameters,
|
transformer,
|
||||||
transformer,
|
queryId,
|
||||||
queryId,
|
// have to pass down to the thread runner - can't put into context now
|
||||||
ctx: {
|
environmentVariables: envVars,
|
||||||
user: ctx.user,
|
ctx: {
|
||||||
auth: { ...authConfigCtx },
|
user: ctx.user,
|
||||||
},
|
auth: { ...authConfigCtx },
|
||||||
})
|
},
|
||||||
|
}
|
||||||
|
const runFn = () => Runner.run(inputs)
|
||||||
|
|
||||||
const { rows, keys, info, extra } = await quotas.addQuery(runFn, {
|
const { rows, keys, info, extra } = await quotas.addQuery(runFn, {
|
||||||
datasourceId: datasource._id,
|
datasourceId: datasource._id,
|
||||||
|
@ -201,7 +205,9 @@ async function execute(
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
|
|
||||||
const query = await db.get(ctx.params.queryId)
|
const query = await db.get(ctx.params.queryId)
|
||||||
const datasource = await db.get(query.datasourceId)
|
const { datasource, envVars } = await sdk.datasources.getWithEnvVars(
|
||||||
|
query.datasourceId
|
||||||
|
)
|
||||||
|
|
||||||
let authConfigCtx: any = {}
|
let authConfigCtx: any = {}
|
||||||
if (!opts.isAutomation) {
|
if (!opts.isAutomation) {
|
||||||
|
@ -219,21 +225,23 @@ async function execute(
|
||||||
|
|
||||||
// call the relevant CRUD method on the integration class
|
// call the relevant CRUD method on the integration class
|
||||||
try {
|
try {
|
||||||
const runFn = () =>
|
const inputs: QueryEvent = {
|
||||||
Runner.run({
|
appId: ctx.appId,
|
||||||
appId: ctx.appId,
|
datasource,
|
||||||
datasource,
|
queryVerb: query.queryVerb,
|
||||||
queryVerb: query.queryVerb,
|
fields: query.fields,
|
||||||
fields: query.fields,
|
pagination: ctx.request.body.pagination,
|
||||||
pagination: ctx.request.body.pagination,
|
parameters: enrichedParameters,
|
||||||
parameters: enrichedParameters,
|
transformer: query.transformer,
|
||||||
transformer: query.transformer,
|
queryId: ctx.params.queryId,
|
||||||
queryId: ctx.params.queryId,
|
// have to pass down to the thread runner - can't put into context now
|
||||||
ctx: {
|
environmentVariables: envVars,
|
||||||
user: ctx.user,
|
ctx: {
|
||||||
auth: { ...authConfigCtx },
|
user: ctx.user,
|
||||||
},
|
auth: { ...authConfigCtx },
|
||||||
})
|
},
|
||||||
|
}
|
||||||
|
const runFn = () => Runner.run(inputs)
|
||||||
|
|
||||||
const { rows, pagination, extra } = await quotas.addQuery(runFn, {
|
const { rows, pagination, extra } = await quotas.addQuery(runFn, {
|
||||||
datasourceId: datasource._id,
|
datasourceId: datasource._id,
|
||||||
|
@ -266,18 +274,18 @@ export async function executeV2(
|
||||||
const removeDynamicVariables = async (queryId: any) => {
|
const removeDynamicVariables = async (queryId: any) => {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const query = await db.get(queryId)
|
const query = await db.get(queryId)
|
||||||
const datasource = await db.get(query.datasourceId)
|
const datasource = await sdk.datasources.get(query.datasourceId)
|
||||||
const dynamicVariables = datasource.config.dynamicVariables
|
const dynamicVariables = datasource.config?.dynamicVariables as any[]
|
||||||
|
|
||||||
if (dynamicVariables) {
|
if (dynamicVariables) {
|
||||||
// delete dynamic variables from the datasource
|
// delete dynamic variables from the datasource
|
||||||
datasource.config.dynamicVariables = dynamicVariables.filter(
|
datasource.config!.dynamicVariables = dynamicVariables!.filter(
|
||||||
(dv: any) => dv.queryId !== queryId
|
(dv: any) => dv.queryId !== queryId
|
||||||
)
|
)
|
||||||
await db.put(datasource)
|
await db.put(datasource)
|
||||||
|
|
||||||
// invalidate the deleted variables
|
// invalidate the deleted variables
|
||||||
const variablesToDelete = dynamicVariables.filter(
|
const variablesToDelete = dynamicVariables!.filter(
|
||||||
(dv: any) => dv.queryId === queryId
|
(dv: any) => dv.queryId === queryId
|
||||||
)
|
)
|
||||||
await invalidateDynamicVariables(variablesToDelete)
|
await invalidateDynamicVariables(variablesToDelete)
|
||||||
|
@ -289,7 +297,7 @@ export async function destroy(ctx: any) {
|
||||||
const queryId = ctx.params.queryId
|
const queryId = ctx.params.queryId
|
||||||
await removeDynamicVariables(queryId)
|
await removeDynamicVariables(queryId)
|
||||||
const query = await db.get(queryId)
|
const query = await db.get(queryId)
|
||||||
const datasource = await db.get(query.datasourceId)
|
const datasource = await sdk.datasources.get(query.datasourceId)
|
||||||
await db.remove(ctx.params.queryId, ctx.params.revId)
|
await db.remove(ctx.params.queryId, ctx.params.revId)
|
||||||
ctx.message = `Query deleted.`
|
ctx.message = `Query deleted.`
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { cloneDeep } from "lodash/fp"
|
||||||
import { processFormulas, processDates } from "../../../utilities/rowProcessor"
|
import { processFormulas, processDates } from "../../../utilities/rowProcessor"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context } from "@budibase/backend-core"
|
||||||
import { removeKeyNumbering } from "./utils"
|
import { removeKeyNumbering } from "./utils"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
export interface ManyRelationship {
|
export interface ManyRelationship {
|
||||||
tableId?: string
|
tableId?: string
|
||||||
|
@ -664,8 +665,7 @@ export class ExternalRequest {
|
||||||
throw "Unable to run without a table name"
|
throw "Unable to run without a table name"
|
||||||
}
|
}
|
||||||
if (!this.datasource) {
|
if (!this.datasource) {
|
||||||
const db = context.getAppDB()
|
this.datasource = await sdk.datasources.get(datasourceId!)
|
||||||
this.datasource = await db.get(datasourceId)
|
|
||||||
if (!this.datasource || !this.datasource.entities) {
|
if (!this.datasource || !this.datasource.entities) {
|
||||||
throw "No tables found, fetch tables before query."
|
throw "No tables found, fetch tables before query."
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,9 @@ import {
|
||||||
Table,
|
Table,
|
||||||
Datasource,
|
Datasource,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
|
const { cleanExportRows } = require("./utils")
|
||||||
|
|
||||||
export async function handleRequest(
|
export async function handleRequest(
|
||||||
operation: Operation,
|
operation: Operation,
|
||||||
|
@ -99,7 +102,7 @@ export async function destroy(ctx: BBContext) {
|
||||||
export async function bulkDestroy(ctx: BBContext) {
|
export async function bulkDestroy(ctx: BBContext) {
|
||||||
const { rows } = ctx.request.body
|
const { rows } = ctx.request.body
|
||||||
const tableId = ctx.params.tableId
|
const tableId = ctx.params.tableId
|
||||||
let promises = []
|
let promises: Promise<Row[] | { row: Row; table: Table }>[] = []
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
promises.push(
|
promises.push(
|
||||||
handleRequest(Operation.DELETE, tableId, {
|
handleRequest(Operation.DELETE, tableId, {
|
||||||
|
@ -179,27 +182,30 @@ export async function validate(ctx: BBContext) {
|
||||||
|
|
||||||
export async function exportRows(ctx: BBContext) {
|
export async function exportRows(ctx: BBContext) {
|
||||||
const { datasourceId } = breakExternalTableId(ctx.params.tableId)
|
const { datasourceId } = breakExternalTableId(ctx.params.tableId)
|
||||||
const db = context.getAppDB()
|
|
||||||
const format = ctx.query.format
|
const format = ctx.query.format
|
||||||
const { columns } = ctx.request.body
|
const { columns } = ctx.request.body
|
||||||
const datasource = await db.get(datasourceId)
|
const datasource = await sdk.datasources.get(datasourceId!)
|
||||||
if (!datasource || !datasource.entities) {
|
if (!datasource || !datasource.entities) {
|
||||||
ctx.throw(400, "Datasource has not been configured for plus API.")
|
ctx.throw(400, "Datasource has not been configured for plus API.")
|
||||||
}
|
}
|
||||||
ctx.request.body = {
|
|
||||||
query: {
|
if (ctx.request.body.rows) {
|
||||||
oneOf: {
|
ctx.request.body = {
|
||||||
_id: ctx.request.body.rows.map(
|
query: {
|
||||||
(row: string) => JSON.parse(decodeURI(row))[0]
|
oneOf: {
|
||||||
),
|
_id: ctx.request.body.rows.map(
|
||||||
|
(row: string) => JSON.parse(decodeURI(row))[0]
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = await search(ctx)
|
let result = await search(ctx)
|
||||||
let rows: Row[] = []
|
let rows: Row[] = []
|
||||||
|
|
||||||
// Filter data to only specified columns if required
|
// Filter data to only specified columns if required
|
||||||
|
|
||||||
if (columns && columns.length) {
|
if (columns && columns.length) {
|
||||||
for (let i = 0; i < result.rows.length; i++) {
|
for (let i = 0; i < result.rows.length; i++) {
|
||||||
rows[i] = {}
|
rows[i] = {}
|
||||||
|
@ -211,22 +217,26 @@ export async function exportRows(ctx: BBContext) {
|
||||||
rows = result.rows
|
rows = result.rows
|
||||||
}
|
}
|
||||||
|
|
||||||
let headers = Object.keys(rows[0])
|
// @ts-ignore
|
||||||
|
let schema = datasource.entities[tableName].schema
|
||||||
|
let exportRows = cleanExportRows(rows, schema, format, columns)
|
||||||
|
|
||||||
|
let headers = Object.keys(schema)
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const exporter = exporters[format]
|
const exporter = exporters[format]
|
||||||
const filename = `export.${format}`
|
const filename = `export.${format}`
|
||||||
|
|
||||||
// send down the file
|
// send down the file
|
||||||
ctx.attachment(filename)
|
ctx.attachment(filename)
|
||||||
return apiFileReturn(exporter(headers, rows))
|
return apiFileReturn(exporter(headers, exportRows))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchEnrichedRow(ctx: BBContext) {
|
export async function fetchEnrichedRow(ctx: BBContext) {
|
||||||
const id = ctx.params.rowId
|
const id = ctx.params.rowId
|
||||||
const tableId = ctx.params.tableId
|
const tableId = ctx.params.tableId
|
||||||
const { datasourceId, tableName } = breakExternalTableId(tableId)
|
const { datasourceId, tableName } = breakExternalTableId(tableId)
|
||||||
const db = context.getAppDB()
|
const datasource: Datasource = await sdk.datasources.get(datasourceId!)
|
||||||
const datasource: Datasource = await db.get(datasourceId)
|
|
||||||
if (!tableName) {
|
if (!tableName) {
|
||||||
ctx.throw(400, "Unable to find table.")
|
ctx.throw(400, "Unable to find table.")
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ import {
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { context, db as dbCore } from "@budibase/backend-core"
|
import { context, db as dbCore } from "@budibase/backend-core"
|
||||||
import { finaliseRow, updateRelatedFormula } from "./staticFormula"
|
import { finaliseRow, updateRelatedFormula } from "./staticFormula"
|
||||||
import { csv, json, jsonWithSchema, Format, isFormat } from "../view/exporters"
|
import { csv, json, jsonWithSchema, Format } from "../view/exporters"
|
||||||
import { apiFileReturn } from "../../../utilities/fileSystem"
|
import { apiFileReturn } from "../../../utilities/fileSystem"
|
||||||
import {
|
import {
|
||||||
Ctx,
|
Ctx,
|
||||||
|
@ -38,6 +38,8 @@ import {
|
||||||
Table,
|
Table,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
const { cleanExportRows } = require("./utils")
|
||||||
|
|
||||||
const CALCULATION_TYPES = {
|
const CALCULATION_TYPES = {
|
||||||
SUM: "sum",
|
SUM: "sum",
|
||||||
COUNT: "count",
|
COUNT: "count",
|
||||||
|
@ -357,6 +359,14 @@ export async function search(ctx: Ctx) {
|
||||||
params.version = ctx.version
|
params.version = ctx.version
|
||||||
params.tableId = tableId
|
params.tableId = tableId
|
||||||
|
|
||||||
|
let table
|
||||||
|
if (params.sort && !params.sortType) {
|
||||||
|
table = await db.get(tableId)
|
||||||
|
const schema = table.schema
|
||||||
|
const sortField = schema[params.sort]
|
||||||
|
params.sortType = sortField.type == "number" ? "number" : "string"
|
||||||
|
}
|
||||||
|
|
||||||
let response
|
let response
|
||||||
if (paginate) {
|
if (paginate) {
|
||||||
response = await paginatedSearch(query, params)
|
response = await paginatedSearch(query, params)
|
||||||
|
@ -370,7 +380,7 @@ export async function search(ctx: Ctx) {
|
||||||
if (tableId === InternalTables.USER_METADATA) {
|
if (tableId === InternalTables.USER_METADATA) {
|
||||||
response.rows = await getGlobalUsersFromMetadata(response.rows)
|
response.rows = await getGlobalUsersFromMetadata(response.rows)
|
||||||
}
|
}
|
||||||
const table = await db.get(tableId)
|
table = table || (await db.get(tableId))
|
||||||
response.rows = await outputProcessing(table, response.rows)
|
response.rows = await outputProcessing(table, response.rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -389,16 +399,25 @@ export async function exportRows(ctx: Ctx) {
|
||||||
const table = await db.get(ctx.params.tableId)
|
const table = await db.get(ctx.params.tableId)
|
||||||
const rowIds = ctx.request.body.rows
|
const rowIds = ctx.request.body.rows
|
||||||
let format = ctx.query.format
|
let format = ctx.query.format
|
||||||
const { columns } = ctx.request.body
|
const { columns, query } = ctx.request.body
|
||||||
let response = (
|
|
||||||
await db.allDocs({
|
let result
|
||||||
include_docs: true,
|
if (rowIds) {
|
||||||
keys: rowIds,
|
let response = (
|
||||||
})
|
await db.allDocs({
|
||||||
).rows.map(row => row.doc)
|
include_docs: true,
|
||||||
|
keys: rowIds,
|
||||||
|
})
|
||||||
|
).rows.map(row => row.doc)
|
||||||
|
|
||||||
|
result = await outputProcessing(table, response)
|
||||||
|
} else if (query) {
|
||||||
|
let searchResponse = await exports.search(ctx)
|
||||||
|
result = searchResponse.rows
|
||||||
|
}
|
||||||
|
|
||||||
let result = (await outputProcessing(table, response)) as Row[]
|
|
||||||
let rows: Row[] = []
|
let rows: Row[] = []
|
||||||
|
let schema = table.schema
|
||||||
|
|
||||||
// Filter data to only specified columns if required
|
// Filter data to only specified columns if required
|
||||||
if (columns && columns.length) {
|
if (columns && columns.length) {
|
||||||
|
@ -412,12 +431,16 @@ export async function exportRows(ctx: Ctx) {
|
||||||
rows = result
|
rows = result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let exportRows = cleanExportRows(rows, schema, format, columns)
|
||||||
if (format === Format.CSV) {
|
if (format === Format.CSV) {
|
||||||
ctx.attachment("export.csv")
|
ctx.attachment("export.csv")
|
||||||
return apiFileReturn(csv(Object.keys(rows[0]), rows))
|
return apiFileReturn(csv(Object.keys(rows[0]), exportRows))
|
||||||
} else if (format === Format.JSON) {
|
} else if (format === Format.JSON) {
|
||||||
ctx.attachment("export.json")
|
ctx.attachment("export.json")
|
||||||
return apiFileReturn(json(rows))
|
return apiFileReturn(json(exportRows))
|
||||||
|
} else if (format === Format.JSON_WITH_SCHEMA) {
|
||||||
|
ctx.attachment("export.json")
|
||||||
|
return apiFileReturn(jsonWithSchema(schema, exportRows))
|
||||||
} else {
|
} else {
|
||||||
throw "Format not recognised"
|
throw "Format not recognised"
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,9 @@ import { BBContext, Row, Table } from "@budibase/types"
|
||||||
export { removeKeyNumbering } from "../../../integrations/base/utils"
|
export { removeKeyNumbering } from "../../../integrations/base/utils"
|
||||||
const validateJs = require("validate.js")
|
const validateJs = require("validate.js")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
|
import { Format } from "../view/exporters"
|
||||||
import { Ctx } from "@budibase/types"
|
import { Ctx } from "@budibase/types"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
validateJs.extend(validateJs.validators.datetime, {
|
validateJs.extend(validateJs.validators.datetime, {
|
||||||
parse: function (value: string) {
|
parse: function (value: string) {
|
||||||
|
@ -21,8 +23,7 @@ validateJs.extend(validateJs.validators.datetime, {
|
||||||
|
|
||||||
export async function getDatasourceAndQuery(json: any) {
|
export async function getDatasourceAndQuery(json: any) {
|
||||||
const datasourceId = json.endpoint.datasourceId
|
const datasourceId = json.endpoint.datasourceId
|
||||||
const db = context.getAppDB()
|
const datasource = await sdk.datasources.get(datasourceId)
|
||||||
const datasource = await db.get(datasourceId)
|
|
||||||
return makeExternalQuery(datasource, json)
|
return makeExternalQuery(datasource, json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,3 +118,40 @@ export async function validate({
|
||||||
}
|
}
|
||||||
return { valid: Object.keys(errors).length === 0, errors }
|
return { valid: Object.keys(errors).length === 0, errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function cleanExportRows(
|
||||||
|
rows: any[],
|
||||||
|
schema: any,
|
||||||
|
format: string,
|
||||||
|
columns: string[]
|
||||||
|
) {
|
||||||
|
let cleanRows = [...rows]
|
||||||
|
|
||||||
|
const relationships = Object.entries(schema)
|
||||||
|
.filter((entry: any[]) => entry[1].type === FieldTypes.LINK)
|
||||||
|
.map(entry => entry[0])
|
||||||
|
|
||||||
|
relationships.forEach(column => {
|
||||||
|
cleanRows.forEach(row => {
|
||||||
|
delete row[column]
|
||||||
|
})
|
||||||
|
delete schema[column]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Intended to avoid 'undefined' in export
|
||||||
|
if (format === Format.CSV) {
|
||||||
|
const schemaKeys = Object.keys(schema)
|
||||||
|
for (let key of schemaKeys) {
|
||||||
|
if (columns?.length && columns.indexOf(key) > 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (let row of cleanRows) {
|
||||||
|
if (row[key] == null) {
|
||||||
|
row[key] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanRows
|
||||||
|
}
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
require("svelte/register")
|
require("svelte/register")
|
||||||
|
|
||||||
const send = require("koa-send")
|
import { resolve, join } from "../../../utilities/centralPath"
|
||||||
const { resolve, join } = require("../../../utilities/centralPath")
|
|
||||||
const uuid = require("uuid")
|
const uuid = require("uuid")
|
||||||
import { ObjectStoreBuckets } from "../../../constants"
|
import { ObjectStoreBuckets } from "../../../constants"
|
||||||
const { processString } = require("@budibase/string-templates")
|
import { processString } from "@budibase/string-templates"
|
||||||
const {
|
import {
|
||||||
loadHandlebarsFile,
|
loadHandlebarsFile,
|
||||||
NODE_MODULES_PATH,
|
NODE_MODULES_PATH,
|
||||||
TOP_LEVEL_PATH,
|
TOP_LEVEL_PATH,
|
||||||
} = require("../../../utilities/fileSystem")
|
} from "../../../utilities/fileSystem"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
const { DocumentType } = require("../../../db/utils")
|
import { DocumentType } from "../../../db/utils"
|
||||||
const { context, objectStore, utils } = require("@budibase/backend-core")
|
import { context, objectStore, utils } from "@budibase/backend-core"
|
||||||
const AWS = require("aws-sdk")
|
import AWS from "aws-sdk"
|
||||||
const fs = require("fs")
|
import fs from "fs"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
const send = require("koa-send")
|
||||||
|
|
||||||
async function prepareUpload({ s3Key, bucket, metadata, file }: any) {
|
async function prepareUpload({ s3Key, bucket, metadata, file }: any) {
|
||||||
const response = await objectStore.upload({
|
const response = await objectStore.upload({
|
||||||
|
@ -110,7 +111,7 @@ export const serveApp = async function (ctx: any) {
|
||||||
title: appInfo.name,
|
title: appInfo.name,
|
||||||
production: env.isProd(),
|
production: env.isProd(),
|
||||||
appId,
|
appId,
|
||||||
clientLibPath: objectStore.clientLibraryUrl(appId, appInfo.version),
|
clientLibPath: objectStore.clientLibraryUrl(appId!, appInfo.version),
|
||||||
usedPlugins: plugins,
|
usedPlugins: plugins,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -135,7 +136,7 @@ export const serveBuilderPreview = async function (ctx: any) {
|
||||||
let appId = context.getAppId()
|
let appId = context.getAppId()
|
||||||
const previewHbs = loadHandlebarsFile(`${__dirname}/templates/preview.hbs`)
|
const previewHbs = loadHandlebarsFile(`${__dirname}/templates/preview.hbs`)
|
||||||
ctx.body = await processString(previewHbs, {
|
ctx.body = await processString(previewHbs, {
|
||||||
clientLibPath: objectStore.clientLibraryUrl(appId, appInfo.version),
|
clientLibPath: objectStore.clientLibraryUrl(appId!, appInfo.version),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// just return the app info for jest to assert on
|
// just return the app info for jest to assert on
|
||||||
|
@ -150,13 +151,11 @@ export const serveClientLibrary = async function (ctx: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSignedUploadURL = async function (ctx: any) {
|
export const getSignedUploadURL = async function (ctx: any) {
|
||||||
const database = context.getAppDB()
|
|
||||||
|
|
||||||
// Ensure datasource is valid
|
// Ensure datasource is valid
|
||||||
let datasource
|
let datasource
|
||||||
try {
|
try {
|
||||||
const { datasourceId } = ctx.params
|
const { datasourceId } = ctx.params
|
||||||
datasource = await database.get(datasourceId)
|
datasource = await sdk.datasources.get(datasourceId, { enriched: true })
|
||||||
if (!datasource) {
|
if (!datasource) {
|
||||||
ctx.throw(400, "The specified datasource could not be found")
|
ctx.throw(400, "The specified datasource could not be found")
|
||||||
}
|
}
|
||||||
|
@ -172,8 +171,8 @@ export const getSignedUploadURL = async function (ctx: any) {
|
||||||
// Determine type of datasource and generate signed URL
|
// Determine type of datasource and generate signed URL
|
||||||
let signedUrl
|
let signedUrl
|
||||||
let publicUrl
|
let publicUrl
|
||||||
const awsRegion = datasource?.config?.region || "eu-west-1"
|
const awsRegion = (datasource?.config?.region || "eu-west-1") as string
|
||||||
if (datasource.source === "S3") {
|
if (datasource?.source === "S3") {
|
||||||
const { bucket, key } = ctx.request.body || {}
|
const { bucket, key } = ctx.request.body || {}
|
||||||
if (!bucket || !key) {
|
if (!bucket || !key) {
|
||||||
ctx.throw(400, "bucket and key values are required")
|
ctx.throw(400, "bucket and key values are required")
|
||||||
|
@ -182,8 +181,8 @@ export const getSignedUploadURL = async function (ctx: any) {
|
||||||
try {
|
try {
|
||||||
const s3 = new AWS.S3({
|
const s3 = new AWS.S3({
|
||||||
region: awsRegion,
|
region: awsRegion,
|
||||||
accessKeyId: datasource?.config?.accessKeyId,
|
accessKeyId: datasource?.config?.accessKeyId as string,
|
||||||
secretAccessKey: datasource?.config?.secretAccessKey,
|
secretAccessKey: datasource?.config?.secretAccessKey as string,
|
||||||
apiVersion: "2006-03-01",
|
apiVersion: "2006-03-01",
|
||||||
signatureVersion: "v4",
|
signatureVersion: "v4",
|
||||||
})
|
})
|
||||||
|
|
|
@ -219,7 +219,7 @@ export async function save(ctx: BBContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const datasource = await db.get(datasourceId)
|
const datasource = await sdk.datasources.get(datasourceId)
|
||||||
if (!datasource.entities) {
|
if (!datasource.entities) {
|
||||||
datasource.entities = {}
|
datasource.entities = {}
|
||||||
}
|
}
|
||||||
|
@ -322,15 +322,17 @@ export async function destroy(ctx: BBContext) {
|
||||||
const datasourceId = getDatasourceId(tableToDelete)
|
const datasourceId = getDatasourceId(tableToDelete)
|
||||||
|
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const datasource = await db.get(datasourceId)
|
const datasource = await sdk.datasources.get(datasourceId!)
|
||||||
const tables = datasource.entities
|
const tables = datasource.entities
|
||||||
|
|
||||||
const operation = Operation.DELETE_TABLE
|
const operation = Operation.DELETE_TABLE
|
||||||
await makeTableRequest(datasource, operation, tableToDelete, tables)
|
if (tables) {
|
||||||
|
await makeTableRequest(datasource, operation, tableToDelete, tables)
|
||||||
|
cleanupRelationships(tableToDelete, tables)
|
||||||
|
delete tables[tableToDelete.name]
|
||||||
|
datasource.entities = tables
|
||||||
|
}
|
||||||
|
|
||||||
cleanupRelationships(tableToDelete, tables)
|
|
||||||
|
|
||||||
delete datasource.entities[tableToDelete.name]
|
|
||||||
await db.put(datasource)
|
await db.put(datasource)
|
||||||
|
|
||||||
return tableToDelete
|
return tableToDelete
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { apiFileReturn } from "../../../utilities/fileSystem"
|
||||||
import { csv, json, jsonWithSchema, Format, isFormat } from "./exporters"
|
import { csv, json, jsonWithSchema, Format, isFormat } from "./exporters"
|
||||||
import { deleteView, getView, getViews, saveView } from "./utils"
|
import { deleteView, getView, getViews, saveView } from "./utils"
|
||||||
import { fetchView } from "../row"
|
import { fetchView } from "../row"
|
||||||
import { FieldTypes } from "../../../constants"
|
|
||||||
import { context, events } from "@budibase/backend-core"
|
import { context, events } from "@budibase/backend-core"
|
||||||
import { DocumentType } from "../../../db/utils"
|
import { DocumentType } from "../../../db/utils"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
|
@ -15,6 +14,7 @@ import {
|
||||||
TableSchema,
|
TableSchema,
|
||||||
View,
|
View,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import { cleanExportRows } from "../row/utils"
|
||||||
|
|
||||||
const { cloneDeep, isEqual } = require("lodash")
|
const { cloneDeep, isEqual } = require("lodash")
|
||||||
|
|
||||||
|
@ -162,39 +162,17 @@ export async function exportView(ctx: BBContext) {
|
||||||
schema = table.schema
|
schema = table.schema
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove any relationships
|
let exportRows = cleanExportRows(rows, schema, format, [])
|
||||||
const relationships = Object.entries(schema)
|
|
||||||
.filter(entry => entry[1].type === FieldTypes.LINK)
|
|
||||||
.map(entry => entry[0])
|
|
||||||
// iterate relationship columns and remove from and row and schema
|
|
||||||
relationships.forEach(column => {
|
|
||||||
rows.forEach(row => {
|
|
||||||
delete row[column]
|
|
||||||
})
|
|
||||||
delete schema[column]
|
|
||||||
})
|
|
||||||
|
|
||||||
// make sure no "undefined" entries appear in the CSV
|
|
||||||
if (format === Format.CSV) {
|
|
||||||
const schemaKeys = Object.keys(schema)
|
|
||||||
for (let key of schemaKeys) {
|
|
||||||
for (let row of rows) {
|
|
||||||
if (row[key] == null) {
|
|
||||||
row[key] = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (format === Format.CSV) {
|
if (format === Format.CSV) {
|
||||||
ctx.attachment(`${viewName}.csv`)
|
ctx.attachment(`${viewName}.csv`)
|
||||||
ctx.body = apiFileReturn(csv(Object.keys(schema), rows))
|
ctx.body = apiFileReturn(csv(Object.keys(schema), exportRows))
|
||||||
} else if (format === Format.JSON) {
|
} else if (format === Format.JSON) {
|
||||||
ctx.attachment(`${viewName}.json`)
|
ctx.attachment(`${viewName}.json`)
|
||||||
ctx.body = apiFileReturn(json(rows))
|
ctx.body = apiFileReturn(json(exportRows))
|
||||||
} else if (format === Format.JSON_WITH_SCHEMA) {
|
} else if (format === Format.JSON_WITH_SCHEMA) {
|
||||||
ctx.attachment(`${viewName}.json`)
|
ctx.attachment(`${viewName}.json`)
|
||||||
ctx.body = apiFileReturn(jsonWithSchema(schema, rows))
|
ctx.body = apiFileReturn(jsonWithSchema(schema, exportRows))
|
||||||
} else {
|
} else {
|
||||||
throw "Format not recognised"
|
throw "Format not recognised"
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,60 +39,62 @@ export async function destroy(ctx: BBContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildSchema(ctx: BBContext) {
|
export async function buildSchema(ctx: BBContext) {
|
||||||
await context.updateAppId(ctx.params.instance)
|
await context.doInAppContext(ctx.params.instance, async () => {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const webhook = (await db.get(ctx.params.id)) as Webhook
|
const webhook = (await db.get(ctx.params.id)) as Webhook
|
||||||
webhook.bodySchema = toJsonSchema(ctx.request.body)
|
webhook.bodySchema = toJsonSchema(ctx.request.body)
|
||||||
// update the automation outputs
|
// update the automation outputs
|
||||||
if (webhook.action.type === WebhookActionType.AUTOMATION) {
|
if (webhook.action.type === WebhookActionType.AUTOMATION) {
|
||||||
let automation = (await db.get(webhook.action.target)) as Automation
|
let automation = (await db.get(webhook.action.target)) as Automation
|
||||||
const autoOutputs = automation.definition.trigger.schema.outputs
|
const autoOutputs = automation.definition.trigger.schema.outputs
|
||||||
let properties = webhook.bodySchema.properties
|
let properties = webhook.bodySchema.properties
|
||||||
// reset webhook outputs
|
// reset webhook outputs
|
||||||
autoOutputs.properties = {
|
autoOutputs.properties = {
|
||||||
body: autoOutputs.properties.body,
|
body: autoOutputs.properties.body,
|
||||||
}
|
|
||||||
for (let prop of Object.keys(properties)) {
|
|
||||||
autoOutputs.properties[prop] = {
|
|
||||||
type: properties[prop].type,
|
|
||||||
description: AUTOMATION_DESCRIPTION,
|
|
||||||
}
|
}
|
||||||
|
for (let prop of Object.keys(properties)) {
|
||||||
|
autoOutputs.properties[prop] = {
|
||||||
|
type: properties[prop].type,
|
||||||
|
description: AUTOMATION_DESCRIPTION,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.put(automation)
|
||||||
}
|
}
|
||||||
await db.put(automation)
|
ctx.body = await db.put(webhook)
|
||||||
}
|
})
|
||||||
ctx.body = await db.put(webhook)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function trigger(ctx: BBContext) {
|
export async function trigger(ctx: BBContext) {
|
||||||
const prodAppId = dbCore.getProdAppID(ctx.params.instance)
|
const prodAppId = dbCore.getProdAppID(ctx.params.instance)
|
||||||
await context.updateAppId(prodAppId)
|
await context.doInAppContext(prodAppId, async () => {
|
||||||
try {
|
try {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const webhook = (await db.get(ctx.params.id)) as Webhook
|
const webhook = (await db.get(ctx.params.id)) as Webhook
|
||||||
// validate against the schema
|
// validate against the schema
|
||||||
if (webhook.bodySchema) {
|
if (webhook.bodySchema) {
|
||||||
validate(ctx.request.body, webhook.bodySchema)
|
validate(ctx.request.body, webhook.bodySchema)
|
||||||
}
|
}
|
||||||
const target = await db.get(webhook.action.target)
|
const target = await db.get(webhook.action.target)
|
||||||
if (webhook.action.type === WebhookActionType.AUTOMATION) {
|
if (webhook.action.type === WebhookActionType.AUTOMATION) {
|
||||||
// trigger with both the pure request and then expand it
|
// trigger with both the pure request and then expand it
|
||||||
// incase the user has produced a schema to bind to
|
// incase the user has produced a schema to bind to
|
||||||
await triggers.externalTrigger(target, {
|
await triggers.externalTrigger(target, {
|
||||||
body: ctx.request.body,
|
body: ctx.request.body,
|
||||||
...ctx.request.body,
|
...ctx.request.body,
|
||||||
appId: prodAppId,
|
appId: prodAppId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ctx.status = 200
|
|
||||||
ctx.body = {
|
|
||||||
message: "Webhook trigger fired successfully",
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err.status === 404) {
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
message: "Application not deployed yet.",
|
message: "Webhook trigger fired successfully",
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.status === 404) {
|
||||||
|
ctx.status = 200
|
||||||
|
ctx.body = {
|
||||||
|
message: "Application not deployed yet.",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ export { default as publicRoutes } from "./public"
|
||||||
|
|
||||||
const appBackupRoutes = pro.appBackups
|
const appBackupRoutes = pro.appBackups
|
||||||
const scheduleRoutes = pro.schedules
|
const scheduleRoutes = pro.schedules
|
||||||
|
const environmentVariableRoutes = pro.environmentVariables
|
||||||
|
|
||||||
export const mainRoutes: Router[] = [
|
export const mainRoutes: Router[] = [
|
||||||
appBackupRoutes,
|
appBackupRoutes,
|
||||||
|
@ -63,6 +64,7 @@ export const mainRoutes: Router[] = [
|
||||||
migrationRoutes,
|
migrationRoutes,
|
||||||
pluginRoutes,
|
pluginRoutes,
|
||||||
scheduleRoutes,
|
scheduleRoutes,
|
||||||
|
environmentVariableRoutes,
|
||||||
// these need to be handled last as they still use /api/:tableId
|
// these need to be handled last as they still use /api/:tableId
|
||||||
// this could be breaking as koa may recognise other routes as this
|
// this could be breaking as koa may recognise other routes as this
|
||||||
tableRoutes,
|
tableRoutes,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as rowController from "../controllers/row"
|
||||||
import authorized from "../../middleware/authorized"
|
import authorized from "../../middleware/authorized"
|
||||||
import { paramResource, paramSubResource } from "../../middleware/resourceId"
|
import { paramResource, paramSubResource } from "../../middleware/resourceId"
|
||||||
import { permissions } from "@budibase/backend-core"
|
import { permissions } from "@budibase/backend-core"
|
||||||
const { internalSearchValidator } = require("./utils/validators")
|
import { internalSearchValidator } from "./utils/validators"
|
||||||
const { PermissionType, PermissionLevel } = permissions
|
const { PermissionType, PermissionLevel } = permissions
|
||||||
|
|
||||||
const router: Router = new Router()
|
const router: Router = new Router()
|
||||||
|
|
|
@ -2,7 +2,8 @@ jest.mock("pg")
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
||||||
import { checkCacheForDynamicVariable } from "../../../threads/utils"
|
import { checkCacheForDynamicVariable } from "../../../threads/utils"
|
||||||
import { events } from "@budibase/backend-core"
|
import { context, events } from "@budibase/backend-core"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
let { basicDatasource } = setup.structures
|
let { basicDatasource } = setup.structures
|
||||||
const pg = require("pg")
|
const pg = require("pg")
|
||||||
|
@ -184,4 +185,37 @@ describe("/datasources", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("check secret replacement", () => {
|
||||||
|
async function makeDatasource() {
|
||||||
|
datasource = basicDatasource()
|
||||||
|
datasource.datasource.config.password = "testing"
|
||||||
|
const res = await request
|
||||||
|
.post(`/api/datasources`)
|
||||||
|
.send(datasource)
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
return res.body.datasource
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should save a datasource with password", async () => {
|
||||||
|
const datasource = await makeDatasource()
|
||||||
|
expect(datasource.config.password).toBe("--secret-value--")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not the password on update with the --secret-value--", async () => {
|
||||||
|
const datasource = await makeDatasource()
|
||||||
|
await request
|
||||||
|
.put(`/api/datasources/${datasource._id}`)
|
||||||
|
.send(datasource)
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
await context.doInAppContext(config.getAppId(), async () => {
|
||||||
|
const dbDatasource: any = await sdk.datasources.get(datasource._id)
|
||||||
|
expect(dbDatasource.config.password).toBe("testing")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -24,6 +24,7 @@ export interface TriggerOutput {
|
||||||
|
|
||||||
export interface AutomationContext extends AutomationResults {
|
export interface AutomationContext extends AutomationResults {
|
||||||
steps: any[]
|
steps: any[]
|
||||||
|
env?: Record<string, string>
|
||||||
trigger: any
|
trigger: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,44 +7,3 @@
|
||||||
export interface QueryOptions {
|
export interface QueryOptions {
|
||||||
disableReturning?: boolean
|
disableReturning?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AuthType {
|
|
||||||
BASIC = "basic",
|
|
||||||
BEARER = "bearer",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthConfig {
|
|
||||||
_id: string
|
|
||||||
name: string
|
|
||||||
type: AuthType
|
|
||||||
config: BasicAuthConfig | BearerAuthConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BasicAuthConfig {
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BearerAuthConfig {
|
|
||||||
token: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RestConfig {
|
|
||||||
url: string
|
|
||||||
rejectUnauthorized: boolean
|
|
||||||
defaultHeaders: {
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
legacyHttpParser: boolean
|
|
||||||
authConfigs: AuthConfig[]
|
|
||||||
staticVariables: {
|
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
dynamicVariables: [
|
|
||||||
{
|
|
||||||
name: string
|
|
||||||
queryId: string
|
|
||||||
value: string
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { bootstrap } from "global-agent"
|
import { bootstrap } from "global-agent"
|
||||||
const fixPath = require("fix-path")
|
const fixPath = require("fix-path")
|
||||||
const { checkDevelopmentEnvironment } = require("./utilities/fileSystem")
|
import { checkDevelopmentEnvironment } from "./utilities/fileSystem"
|
||||||
|
|
||||||
function runServer() {
|
function runServer() {
|
||||||
// this will shutdown the system if development environment not ready
|
// this will shutdown the system if development environment not ready
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { QueryJson, Datasource } from "@budibase/types"
|
import { QueryJson, Datasource } from "@budibase/types"
|
||||||
const { getIntegration } = require("../index")
|
import { getIntegration } from "../index"
|
||||||
|
import sdk from "../../sdk"
|
||||||
|
|
||||||
export async function makeExternalQuery(
|
export async function makeExternalQuery(
|
||||||
datasource: Datasource,
|
datasource: Datasource,
|
||||||
json: QueryJson
|
json: QueryJson
|
||||||
) {
|
) {
|
||||||
|
datasource = await sdk.datasources.enrich(datasource)
|
||||||
const Integration = await getIntegration(datasource.source)
|
const Integration = await getIntegration(datasource.source)
|
||||||
// query is the opinionated function
|
// query is the opinionated function
|
||||||
if (Integration.prototype.query) {
|
if (Integration.prototype.query) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Operation, QueryJson, RenameColumn, Table } from "@budibase/types"
|
||||||
import { breakExternalTableId } from "../utils"
|
import { breakExternalTableId } from "../utils"
|
||||||
import SchemaBuilder = Knex.SchemaBuilder
|
import SchemaBuilder = Knex.SchemaBuilder
|
||||||
import CreateTableBuilder = Knex.CreateTableBuilder
|
import CreateTableBuilder = Knex.CreateTableBuilder
|
||||||
const { FieldTypes, RelationshipTypes } = require("../../constants")
|
import { FieldTypes, RelationshipTypes } from "../../constants"
|
||||||
|
|
||||||
function generateSchema(
|
function generateSchema(
|
||||||
schema: CreateTableBuilder,
|
schema: CreateTableBuilder,
|
||||||
|
|
|
@ -5,8 +5,8 @@ import {
|
||||||
IntegrationBase,
|
IntegrationBase,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
const AWS = require("aws-sdk")
|
import AWS from "aws-sdk"
|
||||||
const { AWS_REGION } = require("../db/dynamoClient")
|
import { AWS_REGION } from "../db/dynamoClient"
|
||||||
|
|
||||||
interface DynamoDBConfig {
|
interface DynamoDBConfig {
|
||||||
region: string
|
region: string
|
||||||
|
@ -182,7 +182,7 @@ class DynamoDBIntegration implements IntegrationBase {
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
async describe(query: { table: string }) {
|
async describe(query: { table: string }): Promise<any> {
|
||||||
const params = {
|
const params = {
|
||||||
TableName: query.table,
|
TableName: query.table,
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue