Merge remote-tracking branch 'origin/develop' into feature/portal-pending-users-section
This commit is contained in:
commit
41006d363a
77 changed files with 1086 additions and 234 deletions
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "2.5.6-alpha.42",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"packages": ["packages/*"],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "2.5.6-alpha.42",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
@ -24,7 +24,7 @@
|
|||
"dependencies": {
|
||||
"@budibase/nano": "10.1.2",
|
||||
"@budibase/pouchdb-replication-stream": "1.2.10",
|
||||
"@budibase/types": "2.5.6-alpha.30",
|
||||
"@budibase/types": "2.5.6-alpha.42",
|
||||
"@shopify/jest-koa-mocks": "5.0.1",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
"aws-cloudfront-sign": "2.2.0",
|
||||
|
|
|
@ -3,7 +3,6 @@ import {
|
|||
Event,
|
||||
LicenseActivatedEvent,
|
||||
LicensePlanChangedEvent,
|
||||
LicenseTierChangedEvent,
|
||||
PlanType,
|
||||
Account,
|
||||
LicensePortalOpenedEvent,
|
||||
|
@ -11,22 +10,22 @@ import {
|
|||
LicenseCheckoutOpenedEvent,
|
||||
LicensePaymentFailedEvent,
|
||||
LicensePaymentRecoveredEvent,
|
||||
PriceDuration,
|
||||
} from "@budibase/types"
|
||||
|
||||
async function tierChanged(account: Account, from: number, to: number) {
|
||||
const properties: LicenseTierChangedEvent = {
|
||||
accountId: account.accountId,
|
||||
to,
|
||||
from,
|
||||
}
|
||||
await publishEvent(Event.LICENSE_TIER_CHANGED, properties)
|
||||
}
|
||||
|
||||
async function planChanged(account: Account, from: PlanType, to: PlanType) {
|
||||
async function planChanged(
|
||||
account: Account,
|
||||
from: PlanType,
|
||||
to: PlanType,
|
||||
quantity: number | undefined,
|
||||
duration: PriceDuration | undefined
|
||||
) {
|
||||
const properties: LicensePlanChangedEvent = {
|
||||
accountId: account.accountId,
|
||||
to,
|
||||
from,
|
||||
quantity,
|
||||
duration,
|
||||
}
|
||||
await publishEvent(Event.LICENSE_PLAN_CHANGED, properties)
|
||||
}
|
||||
|
@ -74,7 +73,6 @@ async function paymentRecovered(account: Account) {
|
|||
}
|
||||
|
||||
export default {
|
||||
tierChanged,
|
||||
planChanged,
|
||||
activated,
|
||||
checkoutOpened,
|
||||
|
|
|
@ -123,7 +123,6 @@ beforeAll(async () => {
|
|||
jest.spyOn(events.plugin, "imported")
|
||||
jest.spyOn(events.plugin, "deleted")
|
||||
|
||||
jest.spyOn(events.license, "tierChanged")
|
||||
jest.spyOn(events.license, "planChanged")
|
||||
jest.spyOn(events.license, "activated")
|
||||
jest.spyOn(events.license, "checkoutOpened")
|
||||
|
|
|
@ -7,16 +7,29 @@ import {
|
|||
PlanType,
|
||||
PriceDuration,
|
||||
PurchasedPlan,
|
||||
PurchasedPrice,
|
||||
Quotas,
|
||||
Subscription,
|
||||
} from "@budibase/types"
|
||||
|
||||
export function price(): PurchasedPrice {
|
||||
return {
|
||||
amount: 10000,
|
||||
amountMonthly: 10000,
|
||||
currency: "usd",
|
||||
duration: PriceDuration.MONTHLY,
|
||||
priceId: "price_123",
|
||||
dayPasses: undefined,
|
||||
isPerUser: true,
|
||||
}
|
||||
}
|
||||
|
||||
export const plan = (type: PlanType = PlanType.FREE): PurchasedPlan => {
|
||||
return {
|
||||
type,
|
||||
usesInvoicing: false,
|
||||
minUsers: 1,
|
||||
model: PlanModel.PER_USER,
|
||||
price: price(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "2.5.6-alpha.42",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,8 +38,8 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
||||
"@budibase/shared-core": "2.5.6-alpha.30",
|
||||
"@budibase/string-templates": "2.5.6-alpha.30",
|
||||
"@budibase/shared-core": "2.5.6-alpha.42",
|
||||
"@budibase/string-templates": "2.5.6-alpha.42",
|
||||
"@spectrum-css/accordion": "3.0.24",
|
||||
"@spectrum-css/actionbutton": "1.0.1",
|
||||
"@spectrum-css/actiongroup": "1.0.1",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "2.5.6-alpha.42",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -58,11 +58,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "2.5.6-alpha.30",
|
||||
"@budibase/client": "2.5.6-alpha.30",
|
||||
"@budibase/frontend-core": "2.5.6-alpha.30",
|
||||
"@budibase/shared-core": "2.5.6-alpha.30",
|
||||
"@budibase/string-templates": "2.5.6-alpha.30",
|
||||
"@budibase/bbui": "2.5.6-alpha.42",
|
||||
"@budibase/frontend-core": "2.5.6-alpha.42",
|
||||
"@budibase/shared-core": "2.5.6-alpha.42",
|
||||
"@budibase/string-templates": "2.5.6-alpha.42",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
|
|
|
@ -134,6 +134,7 @@ export const getFrontendStore = () => {
|
|||
previousTopNavPath: {},
|
||||
version: application.version,
|
||||
revertableVersion: application.revertableVersion,
|
||||
upgradableVersion: application.upgradableVersion,
|
||||
navigation: application.navigation || {},
|
||||
usedPlugins: application.usedPlugins || [],
|
||||
}))
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
} from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { API } from "api"
|
||||
import clientPackage from "@budibase/client/package.json"
|
||||
|
||||
export function show() {
|
||||
updateModal.show()
|
||||
|
@ -25,9 +24,9 @@
|
|||
|
||||
$: appId = $store.appId
|
||||
$: updateAvailable =
|
||||
clientPackage.version &&
|
||||
$store.upgradableVersion &&
|
||||
$store.version &&
|
||||
clientPackage.version !== $store.version
|
||||
$store.upgradableVersion !== $store.version
|
||||
$: revertAvailable = $store.revertableVersion != null
|
||||
|
||||
const refreshAppPackage = async () => {
|
||||
|
@ -46,7 +45,7 @@
|
|||
// Don't wait for the async refresh, since this causes modal flashing
|
||||
refreshAppPackage()
|
||||
notifications.success(
|
||||
`App updated successfully to version ${clientPackage.version}`
|
||||
`App updated successfully to version ${$store.upgradableVersion}`
|
||||
)
|
||||
} catch (err) {
|
||||
notifications.error(`Error updating app: ${err}`)
|
||||
|
@ -91,7 +90,7 @@
|
|||
{#if updateAvailable}
|
||||
<Body size="S">
|
||||
This app is currently using version <b>{$store.version}</b>, but version
|
||||
<b>{clientPackage.version}</b> is available. Updates can contain new features,
|
||||
<b>{$store.upgradableVersion}</b> is available. Updates can contain new features,
|
||||
performance improvements and bug fixes.
|
||||
</Body>
|
||||
{:else}
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
onMount(() => {
|
||||
unlimited = isUnlimited()
|
||||
percentage = getPercentage()
|
||||
if (warnWhenFull && percentage === 100) {
|
||||
if (warnWhenFull && percentage >= 100) {
|
||||
showWarning = true
|
||||
}
|
||||
})
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Panel title={$selectedLayout?.name} icon="Experience" borderLeft>
|
||||
<Panel title={$selectedLayout?.name} icon="Experience" borderLeft wide>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<Banner type="warning" showCloseButton={false}>
|
||||
Custom layouts are being deprecated. They will be removed in a future
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Panel borderLeft title="Navigation" icon="InfoOutline">
|
||||
<Panel borderLeft title="Navigation" icon="InfoOutline" wide>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
{#if $selectedScreen.layoutId}
|
||||
<Banner
|
||||
|
|
|
@ -149,6 +149,7 @@
|
|||
title={$selectedScreen.routing.route}
|
||||
icon={$selectedScreen.routing.route === "/" ? "Home" : "WebPage"}
|
||||
borderLeft
|
||||
wide
|
||||
>
|
||||
<Layout gap="S" paddingX="L" paddingY="XL">
|
||||
{#if $selectedScreen.layoutId}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import { Body, Layout } from "@budibase/bbui"
|
||||
</script>
|
||||
|
||||
<Panel borderLeft title="Theme" icon="InfoOutline">
|
||||
<Panel borderLeft title="Theme" icon="InfoOutline" wide>
|
||||
<Layout paddingX="L" paddingY="XL">
|
||||
<Body size="S">
|
||||
Your theme is set across all the screens within your app.
|
||||
|
|
|
@ -43,12 +43,18 @@
|
|||
}
|
||||
|
||||
$: quotaUsage = $licensing.quotaUsage
|
||||
|
||||
$: license = $auth.user?.license
|
||||
$: plan = license?.plan
|
||||
$: usesInvoicing = plan?.usesInvoicing
|
||||
|
||||
$: accountPortalAccess = $auth?.user?.accountPortalAccess
|
||||
$: quotaReset = quotaUsage?.quotaReset
|
||||
$: canManagePlan =
|
||||
($admin.cloud && accountPortalAccess) || (!$admin.cloud && $auth.isAdmin)
|
||||
|
||||
$: showButton = !usesInvoicing && accountPortalAccess
|
||||
|
||||
const setMonthlyUsage = () => {
|
||||
monthlyUsage = []
|
||||
if (quotaUsage.monthly) {
|
||||
|
@ -121,7 +127,7 @@
|
|||
const setTextRows = () => {
|
||||
textRows = []
|
||||
|
||||
if (cancelAt) {
|
||||
if (cancelAt && !usesInvoicing) {
|
||||
textRows.push({ message: "Subscription has been cancelled" })
|
||||
textRows.push({
|
||||
message: `${getDaysRemaining(cancelAt)} days remaining`,
|
||||
|
@ -213,7 +219,7 @@
|
|||
description="YOUR CURRENT PLAN"
|
||||
title={planTitle()}
|
||||
{primaryActionText}
|
||||
primaryAction={accountPortalAccess ? goToAccountPortal : undefined}
|
||||
primaryAction={showButton ? goToAccountPortal : undefined}
|
||||
{textRows}
|
||||
>
|
||||
<div class="content">
|
||||
|
@ -224,33 +230,23 @@
|
|||
<Usage {usage} warnWhenFull={WARN_USAGE.includes(usage.name)} />
|
||||
</div>
|
||||
{/each}
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">Monthly limits</Heading>
|
||||
<div class="detail">
|
||||
<TooltipWrapper tooltip={new Date(quotaReset)}>
|
||||
<Detail size="M">
|
||||
Resets in {daysRemainingInMonth} days
|
||||
</Detail>
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
</Layout>
|
||||
<Layout noPadding gap="M">
|
||||
{#each monthlyUsage as usage}
|
||||
<Usage {usage} warnWhenFull={WARN_USAGE.includes(usage.name)} />
|
||||
{/each}
|
||||
</Layout>
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
{#if monthlyUsage.length}
|
||||
<div class="column">
|
||||
<Layout noPadding gap="M">
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">Monthly limits</Heading>
|
||||
<div class="detail">
|
||||
<TooltipWrapper tooltip={new Date(quotaReset)}>
|
||||
<Detail size="M">
|
||||
Resets in {daysRemainingInMonth} days
|
||||
</Detail>
|
||||
</TooltipWrapper>
|
||||
</div>
|
||||
</Layout>
|
||||
<Layout noPadding gap="M">
|
||||
{#each monthlyUsage as usage}
|
||||
<Usage
|
||||
{usage}
|
||||
warnWhenFull={WARN_USAGE.includes(usage.name)}
|
||||
/>
|
||||
{/each}
|
||||
</Layout>
|
||||
</Layout>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</DashCard>
|
||||
</Layout>
|
||||
|
|
|
@ -176,7 +176,7 @@
|
|||
<Heading>Backups</Heading>
|
||||
{#if !$licensing.backupsEnabled}
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Pro plan</Tag>
|
||||
<Tag icon="LockClosed">Premium</Tag>
|
||||
</Tags>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import clientPackage from "@budibase/client/package.json"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import { users, auth, apps, groups, overview } from "stores/portal"
|
||||
import { fetchData } from "@budibase/frontend-core"
|
||||
|
@ -40,7 +39,7 @@
|
|||
},
|
||||
},
|
||||
})
|
||||
$: updateAvailable = clientPackage.version !== $store.version
|
||||
$: updateAvailable = $store.upgradableVersion !== $store.version
|
||||
$: isPublished = app?.status === AppStatus.DEPLOYED
|
||||
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
|
||||
$: appEditorText = appEditor?.firstName || appEditor?.email
|
||||
|
@ -172,8 +171,8 @@
|
|||
<Heading size="XS">{$store.version}</Heading>
|
||||
{#if updateAvailable}
|
||||
<div class="version-status">
|
||||
New version <strong>{clientPackage.version}</strong> is available
|
||||
-
|
||||
New version <strong>{$store.upgradableVersion}</strong> is
|
||||
available -
|
||||
<Link
|
||||
on:click={() => {
|
||||
$goto("./version")
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
<script>
|
||||
import { Layout, Heading, Body, Divider, Button } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import clientPackage from "@budibase/client/package.json"
|
||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||
|
||||
let versionModal
|
||||
|
||||
$: updateAvailable = clientPackage.version !== $store.version
|
||||
$: updateAvailable = $store.upgradableVersion !== $store.version
|
||||
</script>
|
||||
|
||||
<Layout noPadding>
|
||||
|
@ -18,7 +17,7 @@
|
|||
{#if updateAvailable}
|
||||
<Body>
|
||||
The app is currently using version <strong>{$store.version}</strong>
|
||||
but version <strong>{clientPackage.version}</strong> is available.
|
||||
but version <strong>{$store.upgradableVersion}</strong> is available.
|
||||
<br />
|
||||
Updates can contain new features, performance improvements and bug fixes.
|
||||
</Body>
|
||||
|
|
|
@ -378,7 +378,7 @@
|
|||
</div>
|
||||
{#if !$licensing.enforceableSSO}
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Enterprise plan</Tag>
|
||||
<Tag icon="LockClosed">Enterprise</Tag>
|
||||
</Tags>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -213,7 +213,7 @@
|
|||
{/if}
|
||||
{#if isCloud && !brandingEnabled}
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Pro</Tag>
|
||||
<Tag icon="LockClosed">Premium</Tag>
|
||||
</Tags>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -94,7 +94,7 @@
|
|||
<Heading size="M">Groups</Heading>
|
||||
{#if !$licensing.groupsEnabled}
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Pro plan</Tag>
|
||||
<Tag icon="LockClosed">Business</Tag>
|
||||
</Tags>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
$: invalidEmails = []
|
||||
|
||||
$: userCount = $licensing.userCount + userEmails.length
|
||||
$: willExceed = userCount > $licensing.userLimit
|
||||
$: willExceed = licensing.willExceedUserLimit(userCount)
|
||||
|
||||
$: importDisabled =
|
||||
!userEmails.length || !validEmails(userEmails) || !usersRole || willExceed
|
||||
|
|
|
@ -114,11 +114,13 @@ export function createUsersStore() {
|
|||
const getUserRole = ({ admin, builder }) =>
|
||||
admin?.global ? "admin" : builder?.global ? "developer" : "appUser"
|
||||
|
||||
const refreshUsage = fn => async args => {
|
||||
const response = await fn(args)
|
||||
await licensing.setQuotaUsage()
|
||||
return response
|
||||
}
|
||||
const refreshUsage =
|
||||
fn =>
|
||||
async (...args) => {
|
||||
const response = await fn(...args)
|
||||
await licensing.setQuotaUsage()
|
||||
return response
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
@ -133,7 +135,7 @@ export function createUsersStore() {
|
|||
updateInvite,
|
||||
getUserCountByApp,
|
||||
// any operation that adds or deletes users
|
||||
acceptInvite: refreshUsage(acceptInvite),
|
||||
acceptInvite,
|
||||
create: refreshUsage(create),
|
||||
save: refreshUsage(save),
|
||||
bulkDelete: refreshUsage(bulkDelete),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "2.5.6-alpha.42",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
|
@ -29,9 +29,9 @@
|
|||
"outputPath": "build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/backend-core": "2.5.6-alpha.30",
|
||||
"@budibase/string-templates": "2.5.6-alpha.30",
|
||||
"@budibase/types": "2.5.6-alpha.30",
|
||||
"@budibase/backend-core": "2.5.6-alpha.42",
|
||||
"@budibase/string-templates": "2.5.6-alpha.42",
|
||||
"@budibase/types": "2.5.6-alpha.42",
|
||||
"axios": "0.21.2",
|
||||
"chalk": "4.1.0",
|
||||
"cli-progress": "3.11.2",
|
||||
|
|
|
@ -5225,36 +5225,5 @@
|
|||
"type": "schema",
|
||||
"suffix": "repeater"
|
||||
}
|
||||
},
|
||||
"spreadsheet": {
|
||||
"name": "Spreadsheet",
|
||||
"icon": "ViewGrid",
|
||||
"settings": [
|
||||
{
|
||||
"key": "table",
|
||||
"type": "table",
|
||||
"label": "Table"
|
||||
},
|
||||
{
|
||||
"type": "filter",
|
||||
"label": "Filtering",
|
||||
"key": "filter"
|
||||
},
|
||||
{
|
||||
"type": "field/sortable",
|
||||
"label": "Sort Column",
|
||||
"key": "sortColumn"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Sort Order",
|
||||
"key": "sortOrder",
|
||||
"options": [
|
||||
"Ascending",
|
||||
"Descending"
|
||||
],
|
||||
"defaultValue": "Ascending"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "2.5.6-alpha.42",
|
||||
"license": "MPL-2.0",
|
||||
"module": "dist/budibase-client.js",
|
||||
"main": "dist/budibase-client.js",
|
||||
|
@ -19,11 +19,11 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "2.5.6-alpha.30",
|
||||
"@budibase/frontend-core": "2.5.6-alpha.30",
|
||||
"@budibase/shared-core": "2.5.6-alpha.30",
|
||||
"@budibase/string-templates": "2.5.6-alpha.30",
|
||||
"@budibase/types": "2.5.6-alpha.30",
|
||||
"@budibase/bbui": "2.5.6-alpha.42",
|
||||
"@budibase/frontend-core": "2.5.6-alpha.42",
|
||||
"@budibase/shared-core": "2.5.6-alpha.42",
|
||||
"@budibase/string-templates": "2.5.6-alpha.42",
|
||||
"@budibase/types": "2.5.6-alpha.42",
|
||||
"@spectrum-css/button": "^3.0.3",
|
||||
"@spectrum-css/card": "^3.0.3",
|
||||
"@spectrum-css/divider": "^1.0.3",
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"name": "@budibase/frontend-core",
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "2.5.6-alpha.42",
|
||||
"description": "Budibase frontend core libraries used in builder and client",
|
||||
"author": "Budibase",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "2.5.6-alpha.30",
|
||||
"@budibase/shared-core": "2.5.6-alpha.30",
|
||||
"@budibase/bbui": "2.5.6-alpha.42",
|
||||
"@budibase/shared-core": "2.5.6-alpha.42",
|
||||
"dayjs": "^1.11.7",
|
||||
"lodash": "^4.17.21",
|
||||
"socket.io-client": "^4.6.1",
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
export let updateValue = rows.actions.updateValue
|
||||
export let invertX = false
|
||||
export let invertY = false
|
||||
export let contentLines = 1
|
||||
|
||||
const emptyError = writable(null)
|
||||
|
||||
|
@ -84,5 +85,7 @@
|
|||
{readonly}
|
||||
{invertY}
|
||||
{invertX}
|
||||
{contentLines}
|
||||
/>
|
||||
<slot />
|
||||
</GridCell>
|
||||
|
|
|
@ -117,6 +117,9 @@
|
|||
.cell.error {
|
||||
--cell-color: var(--spectrum-global-color-red-500);
|
||||
}
|
||||
.cell.readonly {
|
||||
--cell-color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
.cell:not(.focused) {
|
||||
user-select: none;
|
||||
}
|
||||
|
|
|
@ -37,6 +37,8 @@
|
|||
$: sortedBy = column.name === $sort.column
|
||||
$: canMoveLeft = orderable && idx > 0
|
||||
$: canMoveRight = orderable && idx < $renderedColumns.length - 1
|
||||
$: ascendingLabel = column.schema?.type === "number" ? "low-high" : "A-Z"
|
||||
$: descendingLabel = column.schema?.type === "number" ? "high-low" : "Z-A"
|
||||
|
||||
const editColumn = () => {
|
||||
dispatch("edit-column", column.schema)
|
||||
|
@ -179,14 +181,14 @@
|
|||
on:click={sortAscending}
|
||||
disabled={column.name === $sort.column && $sort.order === "ascending"}
|
||||
>
|
||||
Sort A-Z
|
||||
Sort {ascendingLabel}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="SortOrderDown"
|
||||
on:click={sortDescending}
|
||||
disabled={column.name === $sort.column && $sort.order === "descending"}
|
||||
>
|
||||
Sort Z-A
|
||||
Sort {descendingLabel}
|
||||
</MenuItem>
|
||||
<MenuItem disabled={!canMoveLeft} icon="ChevronLeft" on:click={moveLeft}>
|
||||
Move left
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { onMount, tick } from "svelte"
|
||||
import { clickOutside } from "@budibase/bbui"
|
||||
|
||||
export let value
|
||||
export let focused = false
|
||||
|
@ -60,6 +61,7 @@
|
|||
on:change={handleChange}
|
||||
on:wheel|stopPropagation
|
||||
spellcheck="false"
|
||||
use:clickOutside={close}
|
||||
/>
|
||||
{:else}
|
||||
<div class="long-form-cell" on:click={editable ? open : null} class:editable>
|
||||
|
|
|
@ -2,6 +2,13 @@
|
|||
import TextCell from "./TextCell.svelte"
|
||||
|
||||
export let api
|
||||
export let onChange
|
||||
|
||||
const numberOnChange = value => {
|
||||
const float = parseFloat(value)
|
||||
const newValue = isNaN(float) ? null : float
|
||||
onChange(newValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<TextCell {...$$props} bind:api type="number" />
|
||||
<TextCell {...$$props} onChange={numberOnChange} bind:api type="number" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { Icon, clickOutside } from "@budibase/bbui"
|
||||
import { getColor } from "../lib/utils"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
|
@ -12,6 +12,7 @@
|
|||
export let api
|
||||
export let invertX = false
|
||||
export let invertY = false
|
||||
export let contentLines = 1
|
||||
|
||||
let isOpen = false
|
||||
let focusedOptionIdx = null
|
||||
|
@ -86,7 +87,11 @@
|
|||
class:open
|
||||
on:click|self={editable ? open : null}
|
||||
>
|
||||
<div class="values" on:click={editable ? open : null}>
|
||||
<div
|
||||
class="values"
|
||||
class:wrap={contentLines > 1}
|
||||
on:click={editable ? open : null}
|
||||
>
|
||||
{#each values as val}
|
||||
{@const color = getOptionColor(val)}
|
||||
{#if color}
|
||||
|
@ -113,6 +118,7 @@
|
|||
class:invertX
|
||||
class:invertY
|
||||
on:wheel={e => e.stopPropagation()}
|
||||
use:clickOutside={close}
|
||||
>
|
||||
{#each options as option, idx}
|
||||
{@const color = getOptionColor(option)}
|
||||
|
@ -160,6 +166,9 @@
|
|||
grid-row-gap: var(--cell-padding);
|
||||
overflow: hidden;
|
||||
padding: var(--cell-padding);
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.values.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.text {
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<script>
|
||||
import { getColor } from "../lib/utils"
|
||||
import { onMount, getContext } from "svelte"
|
||||
import { Icon, Input, ProgressCircle } from "@budibase/bbui"
|
||||
import { Icon, Input, ProgressCircle, clickOutside } from "@budibase/bbui"
|
||||
import { debounce } from "../../../utils/utils"
|
||||
|
||||
export let value
|
||||
|
@ -29,6 +29,7 @@
|
|||
export let onChange
|
||||
export let invertX = false
|
||||
export let invertY = false
|
||||
export let contentLines = 1
|
||||
|
||||
const { API, dispatch } = getContext("grid")
|
||||
const color = getColor(0)
|
||||
|
@ -243,7 +244,11 @@
|
|||
|
||||
<div class="wrapper" class:editable class:focused style="--color:{color};">
|
||||
<div class="container">
|
||||
<div class="values" on:wheel={e => (focused ? e.stopPropagation() : null)}>
|
||||
<div
|
||||
class="values"
|
||||
class:wrap={editable || contentLines > 1}
|
||||
on:wheel={e => (focused ? e.stopPropagation() : null)}
|
||||
>
|
||||
{#each value || [] as relationship, idx}
|
||||
{#if relationship.primaryDisplay}
|
||||
<div class="badge">
|
||||
|
@ -279,7 +284,13 @@
|
|||
</div>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="dropdown" class:invertX class:invertY on:wheel|stopPropagation>
|
||||
<div
|
||||
class="dropdown"
|
||||
class:invertX
|
||||
class:invertY
|
||||
on:wheel|stopPropagation
|
||||
use:clickOutside={close}
|
||||
>
|
||||
<div class="search">
|
||||
<Input
|
||||
autofocus
|
||||
|
@ -376,6 +387,9 @@
|
|||
grid-row-gap: var(--cell-padding);
|
||||
overflow: hidden;
|
||||
padding: var(--cell-padding);
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.values.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.count {
|
||||
|
|
|
@ -3,16 +3,13 @@
|
|||
import { ActionButton, Popover, Select } from "@budibase/bbui"
|
||||
|
||||
const { sort, columns, stickyColumn } = getContext("grid")
|
||||
const orderOptions = [
|
||||
{ label: "A-Z", value: "ascending" },
|
||||
{ label: "Z-A", value: "descending" },
|
||||
]
|
||||
|
||||
let open = false
|
||||
let anchor
|
||||
|
||||
$: columnOptions = getColumnOptions($stickyColumn, $columns)
|
||||
$: checkValidSortColumn($sort.column, $stickyColumn, $columns)
|
||||
$: orderOptions = getOrderOptions($sort.column, columnOptions)
|
||||
|
||||
const getColumnOptions = (stickyColumn, columns) => {
|
||||
let options = []
|
||||
|
@ -20,6 +17,7 @@
|
|||
options.push({
|
||||
label: stickyColumn.label || stickyColumn.name,
|
||||
value: stickyColumn.name,
|
||||
type: stickyColumn.schema?.type,
|
||||
})
|
||||
}
|
||||
return [
|
||||
|
@ -27,10 +25,25 @@
|
|||
...columns.map(col => ({
|
||||
label: col.label || col.name,
|
||||
value: col.name,
|
||||
type: col.schema?.type,
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
const getOrderOptions = (column, columnOptions) => {
|
||||
const type = columnOptions.find(col => col.value === column)?.type
|
||||
return [
|
||||
{
|
||||
label: type === "number" ? "Low-high" : "A-Z",
|
||||
value: "ascending",
|
||||
},
|
||||
{
|
||||
label: type === "number" ? "High-low" : "Z-A",
|
||||
value: "descending",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const updateSortColumn = e => {
|
||||
sort.update(state => ({
|
||||
...state,
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
selectedCellMap,
|
||||
focusedRow,
|
||||
columnHorizontalInversionIndex,
|
||||
contentLines,
|
||||
} = getContext("grid")
|
||||
|
||||
$: rowSelected = !!$selectedRows[row._id]
|
||||
|
@ -44,6 +45,7 @@
|
|||
focused={$focusedCellId === cellId}
|
||||
selectedUser={$selectedCellMap[cellId]}
|
||||
width={column.width}
|
||||
contentLines={$contentLines}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
<script>
|
||||
export let keybind
|
||||
export let padded = false
|
||||
export let overlay = false
|
||||
|
||||
$: parsedKeys = parseKeys(keybind)
|
||||
|
||||
const parseKeys = keybind => {
|
||||
return keybind?.split("+").map(key => {
|
||||
if (key.toLowerCase() === "ctrl") {
|
||||
return navigator.platform.startsWith("Mac") ? "⌘" : key
|
||||
} else if (key.toLowerCase() === "enter") {
|
||||
return "↵"
|
||||
}
|
||||
return key
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="keys" class:padded class:overlay>
|
||||
{#each parsedKeys as key}
|
||||
<div class="key">
|
||||
{key}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.keys {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 3px;
|
||||
}
|
||||
.keys.padded {
|
||||
padding: var(--cell-padding);
|
||||
}
|
||||
.key {
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.overlay .key {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #eee;
|
||||
}
|
||||
</style>
|
|
@ -7,6 +7,7 @@
|
|||
import { GutterWidth } from "../lib/constants"
|
||||
import { NewRowID } from "../lib/constants"
|
||||
import GutterCell from "../cells/GutterCell.svelte"
|
||||
import KeyboardShortcut from "./KeyboardShortcut.svelte"
|
||||
|
||||
const {
|
||||
hoveredRowId,
|
||||
|
@ -27,13 +28,14 @@
|
|||
columnHorizontalInversionIndex,
|
||||
} = getContext("grid")
|
||||
|
||||
let visible = false
|
||||
let isAdding = false
|
||||
let newRow = {}
|
||||
let offset = 0
|
||||
|
||||
$: firstColumn = $stickyColumn || $renderedColumns[0]
|
||||
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
||||
$: $tableId, (isAdding = false)
|
||||
$: $tableId, (visible = false)
|
||||
$: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
|
||||
|
||||
const shouldInvertY = (offset, inversionIndex, rows) => {
|
||||
|
@ -45,7 +47,8 @@
|
|||
|
||||
const addRow = async () => {
|
||||
// Blur the active cell and tick to let final value updates propagate
|
||||
$focusedCellAPI?.blur()
|
||||
isAdding = true
|
||||
$focusedCellId = null
|
||||
await tick()
|
||||
|
||||
// Create row
|
||||
|
@ -60,17 +63,19 @@
|
|||
$focusedCellId = `${savedRow._id}-${firstColumn.name}`
|
||||
}
|
||||
}
|
||||
isAdding = false
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
isAdding = false
|
||||
visible = false
|
||||
$focusedCellId = null
|
||||
$hoveredRowId = null
|
||||
document.removeEventListener("keydown", handleKeyPress)
|
||||
}
|
||||
|
||||
const startAdding = async () => {
|
||||
if (isAdding) {
|
||||
if (visible) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -95,7 +100,7 @@
|
|||
|
||||
// Update state and select initial cell
|
||||
newRow = {}
|
||||
isAdding = true
|
||||
visible = true
|
||||
$hoveredRowId = NewRowID
|
||||
if (firstColumn) {
|
||||
$focusedCellId = `${NewRowID}-${firstColumn.name}`
|
||||
|
@ -115,7 +120,7 @@
|
|||
}
|
||||
|
||||
const handleKeyPress = e => {
|
||||
if (!isAdding) {
|
||||
if (!visible) {
|
||||
return
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
|
@ -137,7 +142,7 @@
|
|||
</script>
|
||||
|
||||
<!-- Only show new row functionality if we have any columns -->
|
||||
{#if isAdding}
|
||||
{#if visible}
|
||||
<div
|
||||
class="container"
|
||||
class:floating={offset > 0}
|
||||
|
@ -148,6 +153,9 @@
|
|||
<div class="sticky-column" transition:fade={{ duration: 130 }}>
|
||||
<GutterCell on:expand={addViaModal} rowHovered>
|
||||
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
||||
{#if isAdding}
|
||||
<div in:fade={{ duration: 130 }} class="loading-overlay" />
|
||||
{/if}
|
||||
</GutterCell>
|
||||
{#if $stickyColumn}
|
||||
{@const cellId = `${NewRowID}-${$stickyColumn.name}`}
|
||||
|
@ -161,7 +169,14 @@
|
|||
{updateValue}
|
||||
rowIdx={0}
|
||||
{invertY}
|
||||
/>
|
||||
>
|
||||
{#if $stickyColumn?.schema?.autocolumn}
|
||||
<div class="readonly-overlay">Can't edit auto column</div>
|
||||
{/if}
|
||||
{#if isAdding}
|
||||
<div in:fade={{ duration: 130 }} class="loading-overlay" />
|
||||
{/if}
|
||||
</DataCell>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="normal-columns" transition:fade={{ duration: 130 }}>
|
||||
|
@ -181,15 +196,32 @@
|
|||
rowIdx={0}
|
||||
invertX={columnIdx >= $columnHorizontalInversionIndex}
|
||||
{invertY}
|
||||
/>
|
||||
>
|
||||
{#if column?.schema?.autocolumn}
|
||||
<div class="readonly-overlay">Can't edit auto column</div>
|
||||
{/if}
|
||||
{#if isAdding}
|
||||
<div in:fade={{ duration: 130 }} class="loading-overlay" />
|
||||
{/if}
|
||||
</DataCell>
|
||||
{/key}
|
||||
{/each}
|
||||
</div>
|
||||
</GridScrollWrapper>
|
||||
</div>
|
||||
<div class="buttons" transition:fade={{ duration: 130 }}>
|
||||
<Button size="M" cta on:click={addRow}>Save</Button>
|
||||
<Button size="M" secondary newStyles on:click={clear}>Cancel</Button>
|
||||
<Button size="M" cta on:click={addRow} disabled={isAdding}>
|
||||
<div class="button-with-keys">
|
||||
Save
|
||||
<KeyboardShortcut overlay keybind="Ctrl+Enter" />
|
||||
</div>
|
||||
</Button>
|
||||
<Button size="M" secondary newStyles on:click={clear}>
|
||||
<div class="button-with-keys">
|
||||
Cancel
|
||||
<KeyboardShortcut overlay keybind="Esc" />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -240,6 +272,14 @@
|
|||
top: calc(var(--row-height) + var(--offset) + 24px);
|
||||
left: var(--gutter-width);
|
||||
}
|
||||
.button-with-keys {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.button-with-keys :global(> div) {
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
/* Sticky column styles */
|
||||
.sticky-column {
|
||||
|
@ -262,4 +302,33 @@
|
|||
width: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Readonly cell overlay */
|
||||
.readonly-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: var(--row-height);
|
||||
width: 100%;
|
||||
padding: var(--cell-padding);
|
||||
font-style: italic;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Overlay while row is being added */
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: var(--row-height);
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
background: var(--spectrum-global-color-gray-400);
|
||||
opacity: 0.25;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import HeaderCell from "../cells/HeaderCell.svelte"
|
||||
import { GutterWidth, BlankRowID } from "../lib/constants"
|
||||
import GutterCell from "../cells/GutterCell.svelte"
|
||||
import KeyboardShortcut from "./KeyboardShortcut.svelte"
|
||||
|
||||
const {
|
||||
rows,
|
||||
|
@ -21,6 +22,7 @@
|
|||
focusedRow,
|
||||
scrollLeft,
|
||||
dispatch,
|
||||
contentLines,
|
||||
} = getContext("grid")
|
||||
|
||||
$: rowCount = $rows.length
|
||||
|
@ -85,6 +87,7 @@
|
|||
selectedUser={$selectedCellMap[cellId]}
|
||||
width={$stickyColumn.width}
|
||||
column={$stickyColumn}
|
||||
contentLines={$contentLines}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -103,7 +106,9 @@
|
|||
<GridCell
|
||||
width={$stickyColumn.width}
|
||||
highlighted={$hoveredRowId === BlankRowID}
|
||||
/>
|
||||
>
|
||||
<KeyboardShortcut padded keybind="Ctrl+Enter" />
|
||||
</GridCell>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -15,8 +15,22 @@
|
|||
selectedRows,
|
||||
} = getContext("grid")
|
||||
|
||||
const ignoredOriginSelectors = [
|
||||
".spectrum-Modal",
|
||||
"#builder-side-panel-container",
|
||||
]
|
||||
|
||||
// Global key listener which intercepts all key events
|
||||
const handleKeyDown = e => {
|
||||
// Avoid processing events sourced from certain origins
|
||||
if (e.target?.closest) {
|
||||
for (let selector of ignoredOriginSelectors) {
|
||||
if (e.target.closest(selector)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing selected avoid processing further key presses
|
||||
if (!$focusedCellId) {
|
||||
if (e.key === "Tab" || e.key?.startsWith("Arrow")) {
|
||||
|
@ -60,11 +74,6 @@
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid processing events sourced from modals
|
||||
if (e.target?.closest?.(".spectrum-Modal")) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
|
||||
// Handle the key ourselves
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/sdk",
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "2.5.6-alpha.42",
|
||||
"description": "Budibase Public API SDK",
|
||||
"author": "Budibase",
|
||||
"license": "MPL-2.0",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "2.5.6-alpha.42",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/index.ts",
|
||||
"repository": {
|
||||
|
@ -45,12 +45,12 @@
|
|||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "10.0.3",
|
||||
"@budibase/backend-core": "2.5.6-alpha.30",
|
||||
"@budibase/client": "2.5.6-alpha.30",
|
||||
"@budibase/pro": "2.5.6-alpha.30",
|
||||
"@budibase/shared-core": "2.5.6-alpha.30",
|
||||
"@budibase/string-templates": "2.5.6-alpha.30",
|
||||
"@budibase/types": "2.5.6-alpha.30",
|
||||
"@budibase/backend-core": "2.5.6-alpha.42",
|
||||
"@budibase/client": "2.5.6-alpha.42",
|
||||
"@budibase/pro": "2.5.6-alpha.42",
|
||||
"@budibase/shared-core": "2.5.6-alpha.42",
|
||||
"@budibase/string-templates": "2.5.6-alpha.42",
|
||||
"@budibase/types": "2.5.6-alpha.42",
|
||||
"@bull-board/api": "3.7.0",
|
||||
"@bull-board/koa": "3.9.4",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
|
|
|
@ -223,7 +223,7 @@ export async function fetchAppPackage(ctx: UserCtx) {
|
|||
)
|
||||
|
||||
ctx.body = {
|
||||
application,
|
||||
application: { ...application, upgradableVersion: envCore.VERSION },
|
||||
screens,
|
||||
layouts,
|
||||
clientLibPath,
|
||||
|
|
|
@ -12,7 +12,15 @@ import { getIntegration } from "../../integrations"
|
|||
import { getDatasourceAndQuery } from "./row/utils"
|
||||
import { invalidateDynamicVariables } from "../../threads/utils"
|
||||
import { db as dbCore, context, events } from "@budibase/backend-core"
|
||||
import { UserCtx, Datasource, Row } from "@budibase/types"
|
||||
import {
|
||||
UserCtx,
|
||||
Datasource,
|
||||
Row,
|
||||
CreateDatasourceResponse,
|
||||
UpdateDatasourceResponse,
|
||||
UpdateDatasourceRequest,
|
||||
CreateDatasourceRequest,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../../sdk"
|
||||
|
||||
export async function fetch(ctx: UserCtx) {
|
||||
|
@ -146,7 +154,7 @@ async function invalidateVariables(
|
|||
await invalidateDynamicVariables(toInvalidate)
|
||||
}
|
||||
|
||||
export async function update(ctx: UserCtx) {
|
||||
export async function update(ctx: UserCtx<any, UpdateDatasourceResponse>) {
|
||||
const db = context.getAppDB()
|
||||
const datasourceId = ctx.params.datasourceId
|
||||
let datasource = await sdk.datasources.get(datasourceId)
|
||||
|
@ -187,15 +195,17 @@ export async function update(ctx: UserCtx) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function save(ctx: UserCtx) {
|
||||
export async function save(
|
||||
ctx: UserCtx<CreateDatasourceRequest, CreateDatasourceResponse>
|
||||
) {
|
||||
const db = context.getAppDB()
|
||||
const plus = ctx.request.body.datasource.plus
|
||||
const fetchSchema = ctx.request.body.fetchSchema
|
||||
|
||||
const datasource = {
|
||||
_id: generateDatasourceID({ plus }),
|
||||
type: plus ? DocumentType.DATASOURCE_PLUS : DocumentType.DATASOURCE,
|
||||
...ctx.request.body.datasource,
|
||||
type: plus ? DocumentType.DATASOURCE_PLUS : DocumentType.DATASOURCE,
|
||||
}
|
||||
|
||||
let schemaError = null
|
||||
|
@ -218,7 +228,7 @@ export async function save(ctx: UserCtx) {
|
|||
}
|
||||
}
|
||||
|
||||
const response: any = {
|
||||
const response: CreateDatasourceResponse = {
|
||||
datasource: await sdk.datasources.removeSecretSingle(datasource),
|
||||
}
|
||||
if (schemaError) {
|
||||
|
|
|
@ -27,6 +27,7 @@ export const isProdAppID = dbCore.isProdAppID
|
|||
export const USER_METDATA_PREFIX = `${DocumentType.ROW}${SEPARATOR}${dbCore.InternalTable.USER_METADATA}${SEPARATOR}`
|
||||
export const LINK_USER_METADATA_PREFIX = `${DocumentType.LINK}${SEPARATOR}${dbCore.InternalTable.USER_METADATA}${SEPARATOR}`
|
||||
export const TABLE_ROW_PREFIX = `${DocumentType.ROW}${SEPARATOR}${DocumentType.TABLE}`
|
||||
export const AUTOMATION_LOG_PREFIX = `${DocumentType.AUTOMATION_LOG}${SEPARATOR}`
|
||||
export const ViewName = dbCore.ViewName
|
||||
export const InternalTables = dbCore.InternalTable
|
||||
export const UNICODE_MAX = dbCore.UNICODE_MAX
|
||||
|
|
|
@ -349,7 +349,7 @@ describe("row api - postgres", () => {
|
|||
},
|
||||
plus: true,
|
||||
source: "POSTGRES",
|
||||
type: "datasource",
|
||||
type: "datasource_plus",
|
||||
_id: expect.any(String),
|
||||
_rev: expect.any(String),
|
||||
createdAt: expect.any(String),
|
||||
|
|
|
@ -3,6 +3,7 @@ import { budibaseTempDir } from "../../../utilities/budibaseDir"
|
|||
import { streamFile, createTempFolder } from "../../../utilities/fileSystem"
|
||||
import { ObjectStoreBuckets } from "../../../constants"
|
||||
import {
|
||||
AUTOMATION_LOG_PREFIX,
|
||||
LINK_USER_METADATA_PREFIX,
|
||||
TABLE_ROW_PREFIX,
|
||||
USER_METDATA_PREFIX,
|
||||
|
@ -20,11 +21,15 @@ const uuid = require("uuid/v4")
|
|||
const tar = require("tar")
|
||||
const MemoryStream = require("memorystream")
|
||||
|
||||
type ExportOpts = {
|
||||
interface DBDumpOpts {
|
||||
filter?: any
|
||||
exportPath?: string
|
||||
}
|
||||
|
||||
interface ExportOpts extends DBDumpOpts {
|
||||
tar?: boolean
|
||||
excludeRows?: boolean
|
||||
excludeLogs?: boolean
|
||||
}
|
||||
|
||||
function tarFilesToTmp(tmpDir: string, files: string[]) {
|
||||
|
@ -49,7 +54,7 @@ function tarFilesToTmp(tmpDir: string, files: string[]) {
|
|||
* a filter function or the name of the export.
|
||||
* @return {*} either a readable stream or a string
|
||||
*/
|
||||
export async function exportDB(dbName: string, opts: ExportOpts = {}) {
|
||||
export async function exportDB(dbName: string, opts: DBDumpOpts = {}) {
|
||||
const exportOpts = {
|
||||
filter: opts?.filter,
|
||||
batch_size: 1000,
|
||||
|
@ -76,11 +81,14 @@ export async function exportDB(dbName: string, opts: ExportOpts = {}) {
|
|||
})
|
||||
}
|
||||
|
||||
function defineFilter(excludeRows?: boolean) {
|
||||
function defineFilter(excludeRows?: boolean, excludeLogs?: boolean) {
|
||||
const ids = [USER_METDATA_PREFIX, LINK_USER_METADATA_PREFIX]
|
||||
if (excludeRows) {
|
||||
ids.push(TABLE_ROW_PREFIX)
|
||||
}
|
||||
if (excludeLogs) {
|
||||
ids.push(AUTOMATION_LOG_PREFIX)
|
||||
}
|
||||
return (doc: any) =>
|
||||
!ids.map(key => doc._id.includes(key)).reduce((prev, curr) => prev || curr)
|
||||
}
|
||||
|
@ -130,8 +138,7 @@ export async function exportApp(appId: string, config?: ExportOpts) {
|
|||
// enforce an export of app DB to the tmp path
|
||||
const dbPath = join(tmpPath, DB_EXPORT_FILE)
|
||||
await exportDB(appId, {
|
||||
...config,
|
||||
filter: defineFilter(config?.excludeRows),
|
||||
filter: defineFilter(config?.excludeRows, config?.excludeLogs),
|
||||
exportPath: dbPath,
|
||||
})
|
||||
// if tar requested, return where the tarball is
|
||||
|
@ -155,6 +162,10 @@ export async function exportApp(appId: string, config?: ExportOpts) {
|
|||
* @returns {*} a readable stream of the backup which is written in real time
|
||||
*/
|
||||
export async function streamExportApp(appId: string, excludeRows: boolean) {
|
||||
const tmpPath = await exportApp(appId, { excludeRows, tar: true })
|
||||
const tmpPath = await exportApp(appId, {
|
||||
excludeRows,
|
||||
excludeLogs: true,
|
||||
tar: true,
|
||||
})
|
||||
return streamFile(tmpPath)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/shared-core",
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "2.5.6-alpha.42",
|
||||
"description": "Shared data utils",
|
||||
"main": "dist/cjs/src/index.js",
|
||||
"types": "dist/mjs/src/index.d.ts",
|
||||
|
@ -20,7 +20,7 @@
|
|||
"dev:builder": "yarn prebuild && concurrently \"tsc -p tsconfig.build.json --watch\" \"tsc -p tsconfig-cjs.build.json --watch\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/types": "2.5.6-alpha.30"
|
||||
"@budibase/types": "2.5.6-alpha.42"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^7.6.0",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/string-templates",
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "2.5.6-alpha.42",
|
||||
"description": "Handlebars wrapper for Budibase templating.",
|
||||
"main": "src/index.cjs",
|
||||
"module": "dist/bundle.mjs",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/types",
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "2.5.6-alpha.42",
|
||||
"description": "Budibase types",
|
||||
"main": "dist/cjs/index.js",
|
||||
"types": "dist/mjs/index.d.ts",
|
||||
|
|
19
packages/types/src/api/web/app/datasource.ts
Normal file
19
packages/types/src/api/web/app/datasource.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Datasource } from "../../../documents"
|
||||
|
||||
export interface CreateDatasourceResponse {
|
||||
datasource: Datasource
|
||||
error?: any
|
||||
}
|
||||
|
||||
export interface UpdateDatasourceResponse {
|
||||
datasource: Datasource
|
||||
}
|
||||
|
||||
export interface CreateDatasourceRequest {
|
||||
datasource: Datasource
|
||||
fetchSchema?: boolean
|
||||
}
|
||||
|
||||
export interface UpdateDatasourceRequest extends Datasource {
|
||||
datasource: Datasource
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
export * from "./backup"
|
||||
export * from "./datasource"
|
||||
|
|
|
@ -39,6 +39,7 @@ export interface Account extends CreateAccount {
|
|||
// licensing
|
||||
tier: string // deprecated
|
||||
planType?: PlanType
|
||||
/** @deprecated */
|
||||
planTier?: number
|
||||
license?: License
|
||||
installId?: string
|
||||
|
@ -47,6 +48,7 @@ export interface Account extends CreateAccount {
|
|||
stripeCustomerId?: string
|
||||
licenseKey?: string
|
||||
licenseKeyActivatedAt?: number
|
||||
licenseRequestedAt?: number
|
||||
licenseOverrides?: LicenseOverrides
|
||||
quotaUsage?: QuotaUsage
|
||||
}
|
||||
|
|
|
@ -42,3 +42,10 @@ export interface PaginationValues {
|
|||
page: string | number | null
|
||||
limit: number | null
|
||||
}
|
||||
|
||||
export interface PreviewQueryRequest extends Omit<Query, "parameters"> {
|
||||
parameters: {}
|
||||
flags?: {
|
||||
urlName?: boolean
|
||||
}
|
||||
}
|
||||
|
|
|
@ -138,7 +138,6 @@ export enum Event {
|
|||
|
||||
// LICENSE
|
||||
LICENSE_PLAN_CHANGED = "license:plan:changed",
|
||||
LICENSE_TIER_CHANGED = "license:tier:changed",
|
||||
LICENSE_ACTIVATED = "license:activated",
|
||||
LICENSE_PAYMENT_FAILED = "license:payment:failed",
|
||||
LICENSE_PAYMENT_RECOVERED = "license:payment:recovered",
|
||||
|
@ -328,7 +327,6 @@ export const AuditedEventFriendlyName: Record<Event, string | undefined> = {
|
|||
|
||||
// LICENSE - NOT AUDITED
|
||||
[Event.LICENSE_PLAN_CHANGED]: undefined,
|
||||
[Event.LICENSE_TIER_CHANGED]: undefined,
|
||||
[Event.LICENSE_ACTIVATED]: undefined,
|
||||
[Event.LICENSE_PAYMENT_FAILED]: undefined,
|
||||
[Event.LICENSE_PAYMENT_RECOVERED]: undefined,
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { PlanType } from "../licensing"
|
||||
|
||||
export interface LicenseTierChangedEvent {
|
||||
accountId: string
|
||||
from: number
|
||||
to: number
|
||||
}
|
||||
import { PlanType, PriceDuration } from "../licensing"
|
||||
|
||||
export interface LicensePlanChangedEvent {
|
||||
accountId: string
|
||||
from: PlanType
|
||||
to: PlanType
|
||||
// may not be on historical events
|
||||
// free plans won't have a duration
|
||||
duration: PriceDuration | undefined
|
||||
// may not be on historical events
|
||||
// free plans won't have a quantity
|
||||
quantity: number | undefined
|
||||
}
|
||||
|
||||
export interface LicenseActivatedEvent {
|
||||
|
|
|
@ -17,7 +17,6 @@ export enum PriceDuration {
|
|||
export interface AvailablePlan {
|
||||
type: PlanType
|
||||
maxUsers: number
|
||||
minUsers: number
|
||||
prices: AvailablePrice[]
|
||||
}
|
||||
|
||||
|
@ -38,7 +37,6 @@ export interface PurchasedPlan {
|
|||
type: PlanType
|
||||
model: PlanModel
|
||||
usesInvoicing: boolean
|
||||
minUsers: number
|
||||
price?: PurchasedPrice
|
||||
}
|
||||
|
||||
|
|
|
@ -55,12 +55,6 @@ export const isConstantQuota = (
|
|||
return quotaType === QuotaType.CONSTANT
|
||||
}
|
||||
|
||||
export interface Minimums {
|
||||
users: number
|
||||
}
|
||||
|
||||
export type PlanMinimums = { [key in PlanType]: Minimums }
|
||||
|
||||
export type PlanQuotas = { [key in PlanType]: Quotas | undefined }
|
||||
|
||||
export type MonthlyQuotas = {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/worker",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "2.5.6-alpha.30",
|
||||
"version": "2.5.6-alpha.42",
|
||||
"description": "Budibase background service",
|
||||
"main": "src/index.ts",
|
||||
"repository": {
|
||||
|
@ -37,10 +37,10 @@
|
|||
"author": "Budibase",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@budibase/backend-core": "2.5.6-alpha.30",
|
||||
"@budibase/pro": "2.5.6-alpha.30",
|
||||
"@budibase/string-templates": "2.5.6-alpha.30",
|
||||
"@budibase/types": "2.5.6-alpha.30",
|
||||
"@budibase/backend-core": "2.5.6-alpha.42",
|
||||
"@budibase/pro": "2.5.6-alpha.42",
|
||||
"@budibase/string-templates": "2.5.6-alpha.42",
|
||||
"@budibase/types": "2.5.6-alpha.42",
|
||||
"@koa/router": "8.0.8",
|
||||
"@sentry/node": "6.17.7",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
|
|
|
@ -424,7 +424,9 @@ export const inviteAccept = async (
|
|||
if (err.code === ErrorCode.USAGE_LIMIT_EXCEEDED) {
|
||||
// explicitly re-throw limit exceeded errors
|
||||
ctx.throw(400, err)
|
||||
return
|
||||
}
|
||||
console.warn("Error inviting user", err)
|
||||
ctx.throw(400, "Unable to create new user, invitation invalid.")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,17 @@ export const saveMetadata = async (
|
|||
if (existing) {
|
||||
metadata._rev = existing._rev
|
||||
}
|
||||
const res = await db.put(metadata)
|
||||
metadata._rev = res.rev
|
||||
try {
|
||||
const res = await db.put(metadata)
|
||||
metadata._rev = res.rev
|
||||
} catch (e: any) {
|
||||
// account can be updated frequently
|
||||
// ignore 409
|
||||
if (e.status !== 409) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
return metadata
|
||||
})
|
||||
}
|
||||
|
|
|
@ -14,6 +14,20 @@ const env = {
|
|||
ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY,
|
||||
BB_ADMIN_USER_EMAIL: process.env.BB_ADMIN_USER_EMAIL,
|
||||
BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD,
|
||||
POSTGRES_HOST: process.env.POSTGRES_HOST,
|
||||
POSTGRES_PORT: process.env.POSTGRES_PORT,
|
||||
POSTGRES_DB: process.env.POSTGRES_DB,
|
||||
POSTGRES_USER: process.env.POSTGRES_USER,
|
||||
POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD,
|
||||
MONGODB_CONNECTION_STRING: process.env.MONGODB_CONNECTION_STRING,
|
||||
MONGODB_DB: process.env.MONGODB_DB,
|
||||
REST_API_BASE_URL: process.env.REST_API_BASE_URL,
|
||||
REST_API_KEY: process.env.REST_API_KEY,
|
||||
MARIADB_HOST: process.env.MARIADB_HOST,
|
||||
MARIADB_PORT: process.env.MARIADB_PORT,
|
||||
MARIADB_DB: process.env.MARIADB_DB,
|
||||
MARIADB_USER: process.env.MARIADB_USER,
|
||||
MARIADB_PASSWORD: process.env.MARIADB_PASSWORD,
|
||||
}
|
||||
|
||||
export = env
|
||||
|
|
|
@ -7,6 +7,10 @@ import ScreenAPI from "./apis/ScreenAPI"
|
|||
import SelfAPI from "./apis/SelfAPI"
|
||||
import TableAPI from "./apis/TableAPI"
|
||||
import UserAPI from "./apis/UserAPI"
|
||||
import DatasourcesAPI from "./apis/DatasourcesAPI"
|
||||
import IntegrationsAPI from "./apis/IntegrationsAPI"
|
||||
import QueriesAPI from "./apis/QueriesAPI"
|
||||
import PermissionsAPI from "./apis/PermissionsAPI"
|
||||
import BudibaseInternalAPIClient from "./BudibaseInternalAPIClient"
|
||||
import { State } from "../../types"
|
||||
|
||||
|
@ -22,6 +26,10 @@ export default class BudibaseInternalAPI {
|
|||
self: SelfAPI
|
||||
tables: TableAPI
|
||||
users: UserAPI
|
||||
datasources: DatasourcesAPI
|
||||
integrations: IntegrationsAPI
|
||||
queries: QueriesAPI
|
||||
permissions: PermissionsAPI
|
||||
|
||||
constructor(state: State) {
|
||||
this.client = new BudibaseInternalAPIClient(state)
|
||||
|
@ -35,5 +43,9 @@ export default class BudibaseInternalAPI {
|
|||
this.self = new SelfAPI(this.client)
|
||||
this.tables = new TableAPI(this.client)
|
||||
this.users = new UserAPI(this.client)
|
||||
this.datasources = new DatasourcesAPI(this.client)
|
||||
this.integrations = new IntegrationsAPI(this.client)
|
||||
this.queries = new QueriesAPI(this.client)
|
||||
this.permissions = new PermissionsAPI(this.client)
|
||||
}
|
||||
}
|
||||
|
|
95
qa-core/src/internal-api/api/apis/DatasourcesAPI.ts
Normal file
95
qa-core/src/internal-api/api/apis/DatasourcesAPI.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
import { Response } from "node-fetch"
|
||||
import {
|
||||
Datasource,
|
||||
CreateDatasourceResponse,
|
||||
UpdateDatasourceResponse,
|
||||
} from "@budibase/types"
|
||||
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
|
||||
|
||||
export default class DatasourcesAPI {
|
||||
client: BudibaseInternalAPIClient
|
||||
|
||||
constructor(client: BudibaseInternalAPIClient) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async getIntegrations(): Promise<[Response, any]> {
|
||||
const [response, json] = await this.client.get(`/integrations`)
|
||||
expect(response).toHaveStatusCode(200)
|
||||
const integrationsCount = Object.keys(json).length
|
||||
expect(integrationsCount).toBe(16)
|
||||
return [response, json]
|
||||
}
|
||||
|
||||
async getAll(): Promise<[Response, Datasource[]]> {
|
||||
const [response, json] = await this.client.get(`/datasources`)
|
||||
expect(response).toHaveStatusCode(200)
|
||||
expect(json.length).toBeGreaterThan(0)
|
||||
return [response, json]
|
||||
}
|
||||
|
||||
async getTable(dataSourceId: string): Promise<[Response, Datasource]> {
|
||||
const [response, json] = await this.client.get(
|
||||
`/datasources/${dataSourceId}`
|
||||
)
|
||||
expect(response).toHaveStatusCode(200)
|
||||
expect(json._id).toEqual(dataSourceId)
|
||||
return [response, json]
|
||||
}
|
||||
|
||||
async add(body: any): Promise<[Response, CreateDatasourceResponse]> {
|
||||
const [response, json] = await this.client.post(`/datasources`, { body })
|
||||
expect(response).toHaveStatusCode(200)
|
||||
expect(json.datasource._id).toBeDefined()
|
||||
expect(json.datasource._rev).toBeDefined()
|
||||
|
||||
return [response, json]
|
||||
}
|
||||
|
||||
async update(body: any): Promise<[Response, UpdateDatasourceResponse]> {
|
||||
const [response, json] = await this.client.put(`/datasources/${body._id}`, {
|
||||
body,
|
||||
})
|
||||
expect(response).toHaveStatusCode(200)
|
||||
expect(json.datasource._id).toBeDefined()
|
||||
expect(json.datasource._rev).toBeDefined()
|
||||
|
||||
return [response, json]
|
||||
}
|
||||
|
||||
async previewQuery(body: any): Promise<[Response, any]> {
|
||||
const [response, json] = await this.client.post(`/queries/preview`, {
|
||||
body,
|
||||
})
|
||||
expect(response).toHaveStatusCode(200)
|
||||
return [response, json]
|
||||
}
|
||||
|
||||
async saveQuery(body: any): Promise<[Response, any]> {
|
||||
const [response, json] = await this.client.post(`/queries`, {
|
||||
body,
|
||||
})
|
||||
expect(response).toHaveStatusCode(200)
|
||||
return [response, json]
|
||||
}
|
||||
|
||||
async getQuery(queryId: string): Promise<[Response, any]> {
|
||||
const [response, json] = await this.client.get(`/queries/${queryId}`)
|
||||
expect(response).toHaveStatusCode(200)
|
||||
return [response, json]
|
||||
}
|
||||
|
||||
async getQueryPermissions(queryId: string): Promise<[Response, any]> {
|
||||
const [response, json] = await this.client.get(`/permissions/${queryId}`)
|
||||
expect(response).toHaveStatusCode(200)
|
||||
return [response, json]
|
||||
}
|
||||
|
||||
async delete(dataSourceId: string, revId: string): Promise<Response> {
|
||||
const [response, json] = await this.client.del(
|
||||
`/datasources/${dataSourceId}/${revId}`
|
||||
)
|
||||
expect(response).toHaveStatusCode(200)
|
||||
return response
|
||||
}
|
||||
}
|
18
qa-core/src/internal-api/api/apis/IntegrationsAPI.ts
Normal file
18
qa-core/src/internal-api/api/apis/IntegrationsAPI.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Response } from "node-fetch"
|
||||
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
|
||||
|
||||
export default class IntegrationsAPI {
|
||||
client: BudibaseInternalAPIClient
|
||||
|
||||
constructor(client: BudibaseInternalAPIClient) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async getAll(): Promise<[Response, any]> {
|
||||
const [response, json] = await this.client.get(`/integrations`)
|
||||
expect(response).toHaveStatusCode(200)
|
||||
const integrationsCount = Object.keys(json).length
|
||||
expect(integrationsCount).toBeGreaterThan(0)
|
||||
return [response, json]
|
||||
}
|
||||
}
|
16
qa-core/src/internal-api/api/apis/PermissionsAPI.ts
Normal file
16
qa-core/src/internal-api/api/apis/PermissionsAPI.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Response } from "node-fetch"
|
||||
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
|
||||
|
||||
export default class PermissionsAPI {
|
||||
client: BudibaseInternalAPIClient
|
||||
|
||||
constructor(client: BudibaseInternalAPIClient) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async getAll(id: string): Promise<[Response, any]> {
|
||||
const [response, json] = await this.client.get(`/permissions/${id}`)
|
||||
expect(response).toHaveStatusCode(200)
|
||||
return [response, json]
|
||||
}
|
||||
}
|
33
qa-core/src/internal-api/api/apis/QueriesAPI.ts
Normal file
33
qa-core/src/internal-api/api/apis/QueriesAPI.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { Response } from "node-fetch"
|
||||
import BudibaseInternalAPIClient from "../BudibaseInternalAPIClient"
|
||||
import { PreviewQueryRequest, Query } from "@budibase/types"
|
||||
|
||||
export default class DatasourcesAPI {
|
||||
client: BudibaseInternalAPIClient
|
||||
|
||||
constructor(client: BudibaseInternalAPIClient) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async preview(body: PreviewQueryRequest): Promise<[Response, any]> {
|
||||
const [response, json] = await this.client.post(`/queries/preview`, {
|
||||
body,
|
||||
})
|
||||
expect(response).toHaveStatusCode(200)
|
||||
return [response, json]
|
||||
}
|
||||
|
||||
async save(body: Query): Promise<[Response, any]> {
|
||||
const [response, json] = await this.client.post(`/queries`, {
|
||||
body,
|
||||
})
|
||||
expect(response).toHaveStatusCode(200)
|
||||
return [response, json]
|
||||
}
|
||||
|
||||
async get(queryId: string): Promise<[Response, any]> {
|
||||
const [response, json] = await this.client.get(`/queries/${queryId}`)
|
||||
expect(response).toHaveStatusCode(200)
|
||||
return [response, json]
|
||||
}
|
||||
}
|
71
qa-core/src/internal-api/fixtures/datasources.ts
Normal file
71
qa-core/src/internal-api/fixtures/datasources.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
// Add information about the data source to the fixtures file from 1password
|
||||
export const mongoDB = () => {
|
||||
return {
|
||||
datasource: {
|
||||
name: "MongoDB",
|
||||
source: "MONGODB",
|
||||
type: "datasource",
|
||||
config: {
|
||||
connectionString: process.env.MONGODB_CONNECTION_STRING,
|
||||
db: process.env.MONGODB_DB,
|
||||
},
|
||||
},
|
||||
|
||||
fetchSchema: false,
|
||||
}
|
||||
}
|
||||
|
||||
export const postgresSQL = () => {
|
||||
return {
|
||||
datasource: {
|
||||
name: "PostgresSQL",
|
||||
plus: true,
|
||||
source: "POSTGRES",
|
||||
type: "datasource",
|
||||
config: {
|
||||
database: process.env.POSTGRES_DB,
|
||||
host: process.env.POSTGRES_HOST,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
port: process.env.POSTGRES_PORT,
|
||||
schema: "public",
|
||||
user: process.env.POSTGRES_USER,
|
||||
},
|
||||
},
|
||||
fetchSchema: true,
|
||||
}
|
||||
}
|
||||
export const mariaDB = () => {
|
||||
return {
|
||||
datasource: {
|
||||
name: "MariaDB",
|
||||
plus: true,
|
||||
source: "MYSQL",
|
||||
type: "datasource",
|
||||
config: {
|
||||
database: process.env.MARIADB_DB,
|
||||
host: process.env.MARIADB_HOST,
|
||||
password: process.env.MARIADB_PASSWORD,
|
||||
port: process.env.MARIADB_PORT,
|
||||
schema: "public",
|
||||
user: process.env.MARIADB_USER,
|
||||
},
|
||||
},
|
||||
fetchSchema: true,
|
||||
}
|
||||
}
|
||||
|
||||
export const restAPI = () => {
|
||||
return {
|
||||
datasource: {
|
||||
name: "RestAPI",
|
||||
source: "REST",
|
||||
type: "datasource",
|
||||
config: {
|
||||
defaultHeaders: {},
|
||||
rejectUnauthorized: true,
|
||||
url: process.env.REST_API_BASE_URL,
|
||||
},
|
||||
},
|
||||
fetchSchema: false,
|
||||
}
|
||||
}
|
|
@ -4,3 +4,5 @@ export * as rows from "./rows"
|
|||
export * as screens from "./screens"
|
||||
export * as tables from "./tables"
|
||||
export * as users from "./users"
|
||||
export * as datasources from "./datasources"
|
||||
export * as queries from "./queries"
|
||||
|
|
123
qa-core/src/internal-api/fixtures/queries.ts
Normal file
123
qa-core/src/internal-api/fixtures/queries.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
import { PreviewQueryRequest } from "@budibase/types"
|
||||
|
||||
const query = (datasourceId: string, fields: any): any => {
|
||||
return {
|
||||
datasourceId: datasourceId,
|
||||
fields: fields,
|
||||
name: "Query 1",
|
||||
parameters: {},
|
||||
queryVerb: "read",
|
||||
schema: {},
|
||||
transformer: "return data",
|
||||
}
|
||||
}
|
||||
|
||||
export const mariaDB = (datasourceId: string): PreviewQueryRequest => {
|
||||
const fields = {
|
||||
sql: "SELECT * FROM employees LIMIT 10;",
|
||||
}
|
||||
return query(datasourceId, fields)
|
||||
}
|
||||
|
||||
export const mongoDB = (datasourceId: string): PreviewQueryRequest => {
|
||||
const fields = {
|
||||
extra: {
|
||||
collection: "movies",
|
||||
actionType: "find",
|
||||
},
|
||||
json: "",
|
||||
}
|
||||
return query(datasourceId, fields)
|
||||
}
|
||||
|
||||
export const postgres = (datasourceId: string): PreviewQueryRequest => {
|
||||
const fields = {
|
||||
sql: "SELECT * FROM customers;",
|
||||
}
|
||||
return query(datasourceId, fields)
|
||||
}
|
||||
|
||||
export const expectedSchemaFields = {
|
||||
mariaDB: {
|
||||
birth_date: "string",
|
||||
emp_no: "number",
|
||||
first_name: "string",
|
||||
gender: "string",
|
||||
hire_date: "string",
|
||||
last_name: "string",
|
||||
},
|
||||
mongoDB: {
|
||||
directors: "array",
|
||||
genres: "array",
|
||||
image: "string",
|
||||
plot: "string",
|
||||
rank: "number",
|
||||
rating: "number",
|
||||
release_date: "string",
|
||||
running_time_secs: "number",
|
||||
title: "string",
|
||||
year: "number",
|
||||
_id: "json",
|
||||
},
|
||||
postgres: {
|
||||
address: "string",
|
||||
city: "string",
|
||||
company_name: "string",
|
||||
contact_name: "string",
|
||||
contact_title: "string",
|
||||
country: "string",
|
||||
customer_id: "string",
|
||||
fax: "string",
|
||||
phone: "string",
|
||||
postal_code: "string",
|
||||
region: "string",
|
||||
},
|
||||
restAPI: {
|
||||
abilities: "array",
|
||||
base_experience: "number",
|
||||
forms: "array",
|
||||
game_indices: "array",
|
||||
height: "number",
|
||||
held_items: "array",
|
||||
id: "number",
|
||||
is_default: "string",
|
||||
location_area_encounters: "string",
|
||||
moves: "array",
|
||||
name: "string",
|
||||
order: "number",
|
||||
past_types: "array",
|
||||
species: "json",
|
||||
sprites: "json",
|
||||
stats: "array",
|
||||
types: "array",
|
||||
weight: "number",
|
||||
},
|
||||
}
|
||||
|
||||
const request = (datasourceId: string, fields: any, flags: any): any => {
|
||||
return {
|
||||
datasourceId: datasourceId,
|
||||
fields: fields,
|
||||
flags: flags,
|
||||
name: "Query 1",
|
||||
parameters: {},
|
||||
queryVerb: "read",
|
||||
schema: {},
|
||||
transformer: "return data",
|
||||
}
|
||||
}
|
||||
export const restAPI = (datasourceId: string): PreviewQueryRequest => {
|
||||
const fields = {
|
||||
authConfigId: null,
|
||||
bodyType: "none",
|
||||
disabledHeaders: {},
|
||||
headers: {},
|
||||
pagination: {},
|
||||
path: `${process.env.REST_API_BASE_URL}/pokemon/ditto`,
|
||||
queryString: "",
|
||||
}
|
||||
const flags = {
|
||||
urlName: true,
|
||||
}
|
||||
return request(datasourceId, fields, flags)
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import TestConfiguration from "../../config/TestConfiguration"
|
||||
import * as fixtures from "../../fixtures"
|
||||
|
||||
describe("Internal API - Data Sources", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.beforeAll()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await config.afterAll()
|
||||
})
|
||||
|
||||
it("Create an app with a data source", async () => {
|
||||
// Create app
|
||||
await config.createApp()
|
||||
|
||||
// Create Screen
|
||||
const roleArray = ["BASIC", "POWER", "ADMIN", "PUBLIC"]
|
||||
for (let role in roleArray) {
|
||||
const [response, screen] = await config.api.screens.create(
|
||||
fixtures.screens.generateScreen(roleArray[role])
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
69
qa-core/src/internal-api/tests/dataSources/mariaDB.spec.ts
Normal file
69
qa-core/src/internal-api/tests/dataSources/mariaDB.spec.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import TestConfiguration from "../../config/TestConfiguration"
|
||||
import * as fixtures from "../../fixtures"
|
||||
import { Query } from "@budibase/types"
|
||||
|
||||
describe("Internal API - Data Sources: MariaDB", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.beforeAll()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await config.afterAll()
|
||||
})
|
||||
|
||||
it("Create an app with a data source - MariaDB", async () => {
|
||||
// Create app
|
||||
await config.createApp()
|
||||
|
||||
// Get all integrations
|
||||
await config.api.integrations.getAll()
|
||||
|
||||
// Add data source
|
||||
const [dataSourceResponse, dataSourceJson] =
|
||||
await config.api.datasources.add(fixtures.datasources.mariaDB())
|
||||
|
||||
// Update data source
|
||||
const newDataSourceInfo = {
|
||||
...dataSourceJson.datasource,
|
||||
name: "MariaDB2",
|
||||
}
|
||||
const [updatedDataSourceResponse, updatedDataSourceJson] =
|
||||
await config.api.datasources.update(newDataSourceInfo)
|
||||
|
||||
// Query data source
|
||||
const [queryResponse, queryJson] = await config.api.queries.preview(
|
||||
fixtures.queries.mariaDB(updatedDataSourceJson.datasource._id!)
|
||||
)
|
||||
|
||||
expect(queryJson.rows.length).toEqual(10)
|
||||
expect(queryJson.schemaFields).toEqual(
|
||||
fixtures.queries.expectedSchemaFields.mariaDB
|
||||
)
|
||||
|
||||
// Save query
|
||||
const datasourcetoSave: Query = {
|
||||
...fixtures.queries.mariaDB(updatedDataSourceJson.datasource._id!),
|
||||
parameters: [],
|
||||
}
|
||||
|
||||
const [saveQueryResponse, saveQueryJson] = await config.api.queries.save(
|
||||
datasourcetoSave
|
||||
)
|
||||
// Get Query
|
||||
const [getQueryResponse, getQueryJson] = await config.api.queries.get(
|
||||
<string>saveQueryJson._id
|
||||
)
|
||||
|
||||
// Get Query permissions
|
||||
const [getQueryPermissionsResponse, getQueryPermissionsJson] =
|
||||
await config.api.permissions.getAll(saveQueryJson._id!)
|
||||
|
||||
// Delete data source
|
||||
const deleteResponse = await config.api.datasources.delete(
|
||||
updatedDataSourceJson.datasource._id!,
|
||||
updatedDataSourceJson.datasource._rev!
|
||||
)
|
||||
})
|
||||
})
|
69
qa-core/src/internal-api/tests/dataSources/mongoDB.spec.ts
Normal file
69
qa-core/src/internal-api/tests/dataSources/mongoDB.spec.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import TestConfiguration from "../../config/TestConfiguration"
|
||||
import * as fixtures from "../../fixtures"
|
||||
import { Query } from "@budibase/types"
|
||||
|
||||
describe("Internal API - Data Sources: MongoDB", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.beforeAll()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await config.afterAll()
|
||||
})
|
||||
|
||||
it("Create an app with a data source - MongoDB", async () => {
|
||||
// Create app
|
||||
await config.createApp()
|
||||
|
||||
// Get all integrations
|
||||
await config.api.integrations.getAll()
|
||||
|
||||
// Add data source
|
||||
const [dataSourceResponse, dataSourceJson] =
|
||||
await config.api.datasources.add(fixtures.datasources.mongoDB())
|
||||
|
||||
// Update data source
|
||||
const newDataSourceInfo = {
|
||||
...dataSourceJson.datasource,
|
||||
name: "MongoDB2",
|
||||
}
|
||||
const [updatedDataSourceResponse, updatedDataSourceJson] =
|
||||
await config.api.datasources.update(newDataSourceInfo)
|
||||
|
||||
// Query data source
|
||||
const [queryResponse, queryJson] = await config.api.queries.preview(
|
||||
fixtures.queries.mongoDB(updatedDataSourceJson.datasource._id!)
|
||||
)
|
||||
|
||||
expect(queryJson.rows.length).toBeGreaterThan(10)
|
||||
expect(queryJson.schemaFields).toEqual(
|
||||
fixtures.queries.expectedSchemaFields.mongoDB
|
||||
)
|
||||
|
||||
// Save query
|
||||
const datasourcetoSave: Query = {
|
||||
...fixtures.queries.mongoDB(updatedDataSourceJson.datasource._id!),
|
||||
parameters: [],
|
||||
}
|
||||
|
||||
const [saveQueryResponse, saveQueryJson] = await config.api.queries.save(
|
||||
datasourcetoSave
|
||||
)
|
||||
// Get Query
|
||||
const [getQueryResponse, getQueryJson] = await config.api.queries.get(
|
||||
<string>saveQueryJson._id
|
||||
)
|
||||
|
||||
// Get Query permissions
|
||||
const [getQueryPermissionsResponse, getQueryPermissionsJson] =
|
||||
await config.api.permissions.getAll(saveQueryJson._id!)
|
||||
|
||||
// Delete data source
|
||||
const deleteResponse = await config.api.datasources.delete(
|
||||
updatedDataSourceJson.datasource._id!,
|
||||
updatedDataSourceJson.datasource._rev!
|
||||
)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,69 @@
|
|||
import TestConfiguration from "../../config/TestConfiguration"
|
||||
import * as fixtures from "../../fixtures"
|
||||
import { Query } from "@budibase/types"
|
||||
|
||||
describe("Internal API - Data Sources: PostgresSQL", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.beforeAll()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await config.afterAll()
|
||||
})
|
||||
|
||||
it("Create an app with a data source - PostgresSQL", async () => {
|
||||
// Create app
|
||||
await config.createApp()
|
||||
|
||||
// Get all integrations
|
||||
await config.api.integrations.getAll()
|
||||
|
||||
// Add data source
|
||||
const [dataSourceResponse, dataSourceJson] =
|
||||
await config.api.datasources.add(fixtures.datasources.postgresSQL())
|
||||
|
||||
// Update data source
|
||||
const newDataSourceInfo = {
|
||||
...dataSourceJson.datasource,
|
||||
name: "PostgresSQL2",
|
||||
}
|
||||
const [updatedDataSourceResponse, updatedDataSourceJson] =
|
||||
await config.api.datasources.update(newDataSourceInfo)
|
||||
|
||||
// Query data source
|
||||
const [queryResponse, queryJson] = await config.api.queries.preview(
|
||||
fixtures.queries.postgres(updatedDataSourceJson.datasource._id!)
|
||||
)
|
||||
|
||||
expect(queryJson.rows.length).toEqual(91)
|
||||
expect(queryJson.schemaFields).toEqual(
|
||||
fixtures.queries.expectedSchemaFields.postgres
|
||||
)
|
||||
|
||||
// Save query
|
||||
const datasourcetoSave: Query = {
|
||||
...fixtures.queries.postgres(updatedDataSourceJson.datasource._id!),
|
||||
parameters: [],
|
||||
}
|
||||
|
||||
const [saveQueryResponse, saveQueryJson] = await config.api.queries.save(
|
||||
datasourcetoSave
|
||||
)
|
||||
// Get Query
|
||||
const [getQueryResponse, getQueryJson] = await config.api.queries.get(
|
||||
saveQueryJson._id!
|
||||
)
|
||||
|
||||
// Get Query permissions
|
||||
const [getQueryPermissionsResponse, getQueryPermissionsJson] =
|
||||
await config.api.permissions.getAll(saveQueryJson._id!)
|
||||
|
||||
// Delete data source
|
||||
const deleteResponse = await config.api.datasources.delete(
|
||||
updatedDataSourceJson.datasource._id!,
|
||||
updatedDataSourceJson.datasource._rev!
|
||||
)
|
||||
})
|
||||
})
|
69
qa-core/src/internal-api/tests/dataSources/restAPI.spec.ts
Normal file
69
qa-core/src/internal-api/tests/dataSources/restAPI.spec.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import TestConfiguration from "../../config/TestConfiguration"
|
||||
import * as fixtures from "../../fixtures"
|
||||
import { Query } from "@budibase/types"
|
||||
|
||||
describe("Internal API - Data Sources: REST API", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.beforeAll()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await config.afterAll()
|
||||
})
|
||||
|
||||
it("Create an app with a data source - REST API", async () => {
|
||||
// Create app
|
||||
await config.createApp()
|
||||
|
||||
// Get all integrations
|
||||
await config.api.integrations.getAll()
|
||||
|
||||
// Add data source
|
||||
const [dataSourceResponse, dataSourceJson] =
|
||||
await config.api.datasources.add(fixtures.datasources.restAPI())
|
||||
|
||||
// Update data source
|
||||
const newDataSourceInfo = {
|
||||
...dataSourceJson.datasource,
|
||||
name: "RestAPI - Updated",
|
||||
}
|
||||
const [updatedDataSourceResponse, updatedDataSourceJson] =
|
||||
await config.api.datasources.update(newDataSourceInfo)
|
||||
|
||||
// Query data source
|
||||
const [queryResponse, queryJson] = await config.api.queries.preview(
|
||||
fixtures.queries.restAPI(updatedDataSourceJson.datasource._id!)
|
||||
)
|
||||
|
||||
expect(queryJson.rows.length).toEqual(1)
|
||||
expect(queryJson.schemaFields).toEqual(
|
||||
fixtures.queries.expectedSchemaFields.restAPI
|
||||
)
|
||||
|
||||
// Save query
|
||||
const datasourcetoSave: Query = {
|
||||
...fixtures.queries.postgres(updatedDataSourceJson.datasource._id!),
|
||||
parameters: [],
|
||||
}
|
||||
|
||||
const [saveQueryResponse, saveQueryJson] = await config.api.queries.save(
|
||||
datasourcetoSave
|
||||
)
|
||||
// Get Query
|
||||
const [getQueryResponse, getQueryJson] = await config.api.queries.get(
|
||||
saveQueryJson._id!
|
||||
)
|
||||
|
||||
// Get Query permissions
|
||||
const [getQueryPermissionsResponse, getQueryPermissionsJson] =
|
||||
await config.api.permissions.getAll(saveQueryJson._id!)
|
||||
|
||||
// Delete data source
|
||||
const deleteResponse = await config.api.datasources.delete(
|
||||
updatedDataSourceJson.datasource._id!,
|
||||
updatedDataSourceJson.datasource._rev!
|
||||
)
|
||||
})
|
||||
})
|
|
@ -8,6 +8,7 @@ export * from "./responseMessage"
|
|||
export * from "./routing"
|
||||
export * from "./state"
|
||||
export * from "./unpublishAppResponse"
|
||||
export * from "./addedDatasource"
|
||||
|
||||
// re-export public api types
|
||||
export * from "@budibase/server/api/controllers/public/mapping/types"
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -1486,15 +1486,15 @@
|
|||
pouchdb-promise "^6.0.4"
|
||||
through2 "^2.0.0"
|
||||
|
||||
"@budibase/pro@2.5.6-alpha.29":
|
||||
version "2.5.6-alpha.29"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.5.6-alpha.29.tgz#71414f68a296535ef53ffb0453352ea137c4aeab"
|
||||
integrity sha512-tQuzMOo2WFxKvsUgYAfUEcLabRpmAD7hPlhBhCFzYasaXNbJiPhcwv4i52US0i0Wr2IXMb2X0d7fwa8tnbKzIA==
|
||||
"@budibase/pro@2.5.6-alpha.41":
|
||||
version "2.5.6-alpha.41"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.5.6-alpha.41.tgz#2632b398159211bf64bf6caff9753c877b6fc09d"
|
||||
integrity sha512-8Zfm+RtcXY77gdXXHti8QT+Vt+1ApuVqYL3KCkxBVbpzgQci/F3mipNXUX8bEy/y+QYAdgn6psDTWAqyr9VALg==
|
||||
dependencies:
|
||||
"@budibase/backend-core" "2.5.6-alpha.29"
|
||||
"@budibase/backend-core" "2.5.6-alpha.41"
|
||||
"@budibase/shared-core" "2.4.44-alpha.1"
|
||||
"@budibase/string-templates" "2.4.44-alpha.1"
|
||||
"@budibase/types" "2.5.6-alpha.29"
|
||||
"@budibase/types" "2.5.6-alpha.41"
|
||||
"@koa/router" "8.0.8"
|
||||
bull "4.10.1"
|
||||
joi "17.6.0"
|
||||
|
|
Loading…
Reference in a new issue