Merge branch 'develop' of github.com:Budibase/budibase into cheeks-lab-day-portal-redesign
This commit is contained in:
commit
9bb1cf3af8
|
@ -1,5 +1,5 @@
|
|||
name: Budibase Prerelease
|
||||
concurrency: release-prerelease
|
||||
#concurrency: release-prerelease
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -26,7 +26,7 @@ env:
|
|||
FEATURE_PREVIEW_URL: https://budirelease.live
|
||||
|
||||
jobs:
|
||||
release:
|
||||
release-images:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
@ -44,19 +44,12 @@ jobs:
|
|||
run: yarn install:pro develop
|
||||
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
- run: yarn bootstrap
|
||||
- run: yarn lint
|
||||
- run: yarn build
|
||||
- run: yarn build:sdk
|
||||
- run: yarn test
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: eu-west-1
|
||||
|
||||
- name: Publish budibase packages to NPM
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
@ -76,12 +69,6 @@ jobs:
|
|||
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||
|
||||
- name: Get the latest budibase release version
|
||||
id: version
|
||||
run: |
|
||||
release_version=$(cat lerna.json | jq -r '.version')
|
||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||
|
||||
- name: Tag and release Proxy service docker image
|
||||
run: |
|
||||
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||
|
@ -93,6 +80,26 @@ jobs:
|
|||
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
|
||||
RELEASE_TAG: k8s-release
|
||||
|
||||
deploy-to-release-env:
|
||||
needs: [release-images]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Get the current budibase release version
|
||||
id: version
|
||||
run: |
|
||||
release_version=$(cat lerna.json | jq -r '.version')
|
||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: eu-west-1
|
||||
|
||||
- name: Pull values.yaml from budibase-infra
|
||||
run: |
|
||||
curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \
|
||||
|
@ -149,3 +156,53 @@ jobs:
|
|||
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
|
||||
content: "Release Env Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Release Env."
|
||||
embed-title: ${{ env.RELEASE_VERSION }}
|
||||
|
||||
release-helm-chart:
|
||||
needs: [release-images]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Helm
|
||||
uses: azure/setup-helm@v1
|
||||
id: helm-install
|
||||
|
||||
# due to helm repo index issue: https://github.com/helm/helm/issues/7363
|
||||
# we need to create new package in a different dir, merge the index and move the package back
|
||||
- name: Build and release helm chart
|
||||
run: |
|
||||
git config user.name "Budibase Helm Bot"
|
||||
git config user.email "<>"
|
||||
git reset --hard
|
||||
git pull
|
||||
mkdir sync
|
||||
echo "Packaging chart to sync dir"
|
||||
helm package charts/budibase --version 0.0.0-develop --app-version develop --destination sync
|
||||
echo "Packaging successful"
|
||||
git checkout gh-pages
|
||||
echo "Indexing helm repo"
|
||||
helm repo index --merge docs/index.yaml sync
|
||||
mv -f sync/* docs
|
||||
rm -rf sync
|
||||
echo "Pushing new helm release"
|
||||
git add -A
|
||||
git commit -m "Helm Release: develop"
|
||||
git push
|
||||
|
||||
trigger-deploy-to-qa:
|
||||
needs: [release-helm-chart]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get the current budibase release version
|
||||
id: version
|
||||
run: |
|
||||
release_version=$(cat lerna.json | jq -r '.version')
|
||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||
|
||||
- uses: passeidireto/trigger-external-workflow-action@main
|
||||
env:
|
||||
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
|
||||
with:
|
||||
repository: budibase/budibase-deploys
|
||||
event: deploy-develop-to-qa
|
||||
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}
|
|
@ -73,7 +73,7 @@ jobs:
|
|||
git config user.email "<>"
|
||||
git reset --hard
|
||||
git pull
|
||||
helm package charts/budibase
|
||||
helm package charts/budibase --version "$RELEASE_VERSION" --app-version "$RELEASE_VERSION"
|
||||
git checkout gh-pages
|
||||
mv *.tgz docs
|
||||
helm repo index docs
|
||||
|
|
|
@ -4,6 +4,7 @@ builder/*
|
|||
packages/server/runtime_apps/
|
||||
.idea/
|
||||
bb-airgapped.tar.gz
|
||||
*.iml
|
||||
|
||||
# Logs
|
||||
logs
|
||||
|
|
|
@ -11,8 +11,10 @@ sources:
|
|||
- https://github.com/Budibase/budibase
|
||||
- https://budibase.com
|
||||
type: application
|
||||
version: 0.2.11
|
||||
appVersion: 1.0.214
|
||||
# populates on packaging
|
||||
version: 0.0.0
|
||||
# populates on packaging
|
||||
appVersion: 0.0.0
|
||||
dependencies:
|
||||
- name: couchdb
|
||||
version: 3.6.1
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.2.4-alpha.2",
|
||||
"version": "2.2.4-alpha.7",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
@ -15,4 +15,4 @@
|
|||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "2.2.4-alpha.2",
|
||||
"version": "2.2.4-alpha.7",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
@ -21,7 +21,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@budibase/nano": "10.1.1",
|
||||
"@budibase/types": "2.2.4-alpha.2",
|
||||
"@budibase/types": "2.2.4-alpha.7",
|
||||
"@shopify/jest-koa-mocks": "5.0.1",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
"aws-cloudfront-sign": "2.2.0",
|
||||
|
@ -79,4 +79,4 @@
|
|||
"typescript": "4.7.3"
|
||||
},
|
||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
||||
}
|
||||
}
|
|
@ -86,7 +86,7 @@ export const ObjectStore = (
|
|||
|
||||
// custom S3 is in use i.e. minio
|
||||
if (env.MINIO_URL) {
|
||||
if (opts.presigning && !env.MINIO_ENABLED) {
|
||||
if (opts.presigning && env.MINIO_ENABLED) {
|
||||
// IMPORTANT: Signed urls will inspect the host header of the request.
|
||||
// Normally a signed url will need to be generated with a specified host in mind.
|
||||
// To support dynamic hosts, e.g. some unknown self-hosted installation url,
|
||||
|
|
|
@ -13,6 +13,18 @@ const getClient = async (type: LockType): Promise<Redlock> => {
|
|||
}
|
||||
return noRetryRedlock
|
||||
}
|
||||
case LockType.DEFAULT: {
|
||||
if (!noRetryRedlock) {
|
||||
noRetryRedlock = await newRedlock(OPTIONS.DEFAULT)
|
||||
}
|
||||
return noRetryRedlock
|
||||
}
|
||||
case LockType.DELAY_500: {
|
||||
if (!noRetryRedlock) {
|
||||
noRetryRedlock = await newRedlock(OPTIONS.DELAY_500)
|
||||
}
|
||||
return noRetryRedlock
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Could not get redlock client: ${type}`)
|
||||
}
|
||||
|
@ -41,6 +53,9 @@ export const OPTIONS = {
|
|||
// see https://www.awsarchitectureblog.com/2015/03/backoff.html
|
||||
retryJitter: 100, // time in ms
|
||||
},
|
||||
DELAY_500: {
|
||||
retryDelay: 500,
|
||||
},
|
||||
}
|
||||
|
||||
export const newRedlock = async (opts: Options = {}) => {
|
||||
|
@ -55,19 +70,17 @@ export const doWithLock = async (opts: LockOptions, task: any) => {
|
|||
let lock
|
||||
try {
|
||||
// aquire lock
|
||||
let name: string
|
||||
if (opts.systemLock) {
|
||||
name = opts.name
|
||||
} else {
|
||||
name = `${tenancy.getTenantId()}_${opts.name}`
|
||||
}
|
||||
let name: string = `lock:${tenancy.getTenantId()}_${opts.name}`
|
||||
if (opts.nameSuffix) {
|
||||
name = name + `_${opts.nameSuffix}`
|
||||
}
|
||||
lock = await redlock.lock(name, opts.ttl)
|
||||
// perform locked task
|
||||
return task()
|
||||
// need to await to ensure completion before unlocking
|
||||
const result = await task()
|
||||
return result
|
||||
} catch (e: any) {
|
||||
console.log("lock error")
|
||||
// lock limit exceeded
|
||||
if (e.name === "LockError") {
|
||||
if (opts.type === LockType.TRY_ONCE) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "2.2.4-alpha.2",
|
||||
"version": "2.2.4-alpha.7",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,7 +38,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "1.2.1",
|
||||
"@budibase/string-templates": "2.2.4-alpha.2",
|
||||
"@budibase/string-templates": "2.2.4-alpha.7",
|
||||
"@spectrum-css/actionbutton": "1.0.1",
|
||||
"@spectrum-css/actiongroup": "1.0.1",
|
||||
"@spectrum-css/avatar": "3.0.2",
|
||||
|
@ -89,4 +89,4 @@
|
|||
"loader-utils": "1.4.1"
|
||||
},
|
||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
||||
}
|
||||
}
|
|
@ -10,10 +10,13 @@
|
|||
export let green = false
|
||||
export let active = false
|
||||
export let inactive = false
|
||||
export let hoverable = false
|
||||
</script>
|
||||
|
||||
<span
|
||||
on:click
|
||||
class="spectrum-Label"
|
||||
class:hoverable
|
||||
class:spectrum-Label--small={size === "S"}
|
||||
class:spectrum-Label--large={size === "L"}
|
||||
class:spectrum-Label--grey={grey}
|
||||
|
@ -27,3 +30,13 @@
|
|||
>
|
||||
<slot />
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.spectrum-Label--grey {
|
||||
background-color: var(--spectrum-global-color-gray-500);
|
||||
font-weight: 600;
|
||||
}
|
||||
.hoverable:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import "@spectrum-css/label/dist/index-vars.css"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Badge from "../Badge/Badge.svelte"
|
||||
|
||||
export let row
|
||||
export let value
|
||||
|
@ -24,17 +25,11 @@
|
|||
|
||||
{#each relationships as relationship}
|
||||
{#if relationship?.primaryDisplay}
|
||||
<span class="spectrum-Label spectrum-Label--grey" on:click={onClick}>
|
||||
<Badge hoverable grey on:click={onClick}>
|
||||
{relationship.primaryDisplay}
|
||||
</span>
|
||||
</Badge>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if leftover}
|
||||
<div>+{leftover} more</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
span:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "2.2.4-alpha.2",
|
||||
"version": "2.2.4-alpha.7",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -71,10 +71,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "2.2.4-alpha.2",
|
||||
"@budibase/client": "2.2.4-alpha.2",
|
||||
"@budibase/frontend-core": "2.2.4-alpha.2",
|
||||
"@budibase/string-templates": "2.2.4-alpha.2",
|
||||
"@budibase/bbui": "2.2.4-alpha.7",
|
||||
"@budibase/client": "2.2.4-alpha.7",
|
||||
"@budibase/frontend-core": "2.2.4-alpha.7",
|
||||
"@budibase/string-templates": "2.2.4-alpha.7",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
@ -123,4 +123,4 @@
|
|||
"vite": "^3.0.8"
|
||||
},
|
||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||
}
|
||||
}
|
|
@ -180,7 +180,7 @@
|
|||
onSelect(block)
|
||||
}}
|
||||
>
|
||||
<Icon name={showLooping ? "ChevronDown" : "ChevronUp"} />
|
||||
<Icon name={showLooping ? "ChevronUp" : "ChevronDown"} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -216,7 +216,6 @@
|
|||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
.table-title > div {
|
||||
margin-left: var(--spacing-xs);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { ActionButton, Modal, notifications } from "@budibase/bbui"
|
||||
import CreateEditRelationship from "../../Datasources/CreateEditRelationship.svelte"
|
||||
import { datasources, tables } from "../../../../stores/backend"
|
||||
import { datasources } from "../../../../stores/backend"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let table
|
||||
|
@ -21,8 +21,6 @@
|
|||
// Create datasource
|
||||
await datasources.save(datasource)
|
||||
notifications.success(`Relationship information saved.`)
|
||||
const tableList = await tables.fetch()
|
||||
await tables.select(tableList.find(tbl => tbl._id === table._id))
|
||||
dispatch("updatecolumns")
|
||||
} catch (err) {
|
||||
notifications.error(`Error saving relationship info: ${err}`)
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
// in the case of internal tables the sourceId will just be undefined
|
||||
$: tableOptions = $tables.list.filter(
|
||||
opt =>
|
||||
opt._id !== $tables.draft._id &&
|
||||
opt._id !== $tables.selected._id &&
|
||||
opt.type === table.type &&
|
||||
table.sourceId === opt.sourceId
|
||||
)
|
||||
|
@ -112,7 +112,7 @@
|
|||
|
||||
async function saveColumn() {
|
||||
if (field.type === AUTO_TYPE) {
|
||||
field = buildAutoColumn($tables.draft.name, field.name, field.subtype)
|
||||
field = buildAutoColumn($tables.selected.name, field.name, field.subtype)
|
||||
}
|
||||
if (field.type !== LINK_TYPE) {
|
||||
delete field.fieldName
|
||||
|
@ -310,7 +310,7 @@
|
|||
newError.name = `${PROHIBITED_COLUMN_NAMES.join(
|
||||
", "
|
||||
)} are not allowed as column names`
|
||||
} else if (inUse($tables.draft, fieldInfo.name, originalName)) {
|
||||
} else if (inUse($tables.selected, fieldInfo.name, originalName)) {
|
||||
newError.name = `Column name already in use.`
|
||||
}
|
||||
if (fieldInfo.fieldName && fieldInfo.tableId) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { tables, rows } from "stores/backend"
|
||||
import { tables } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import RowFieldControl from "../RowFieldControl.svelte"
|
||||
import { API } from "api"
|
||||
|
@ -25,7 +25,6 @@
|
|||
try {
|
||||
await API.saveRow({ ...row, tableId: table._id })
|
||||
notifications.success("Row saved successfully")
|
||||
rows.save()
|
||||
dispatch("updaterows")
|
||||
} catch (error) {
|
||||
if (error.handled) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { tables, rows } from "stores/backend"
|
||||
import { tables } from "stores/backend"
|
||||
import { roles } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import RowFieldControl from "../RowFieldControl.svelte"
|
||||
|
@ -57,7 +57,6 @@
|
|||
try {
|
||||
await API.saveRow({ ...row, tableId: table._id })
|
||||
notifications.success("User saved successfully")
|
||||
rows.save()
|
||||
dispatch("updaterows")
|
||||
} catch (error) {
|
||||
if (error.handled) {
|
||||
|
|
|
@ -9,13 +9,13 @@
|
|||
|
||||
$: views = $tables.list.flatMap(table => Object.keys(table.views || {}))
|
||||
|
||||
function saveView() {
|
||||
const saveView = async () => {
|
||||
if (views.includes(name)) {
|
||||
notifications.error(`View exists with name ${name}`)
|
||||
return
|
||||
}
|
||||
try {
|
||||
viewsStore.save({
|
||||
await viewsStore.save({
|
||||
name,
|
||||
tableId: $tables.selected._id,
|
||||
field,
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { get } from "svelte/store"
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import { goto, isActive, params } from "@roxi/routify"
|
||||
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
|
||||
import { database, datasources, queries, tables, views } from "stores/backend"
|
||||
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte"
|
||||
|
@ -14,40 +12,61 @@
|
|||
customQueryText,
|
||||
} from "helpers/data/utils"
|
||||
import IntegrationIcon from "./IntegrationIcon.svelte"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
let openDataSources = []
|
||||
$: enrichedDataSources = Array.isArray($datasources.list)
|
||||
? $datasources.list.map(datasource => {
|
||||
const selected = $datasources.selected === datasource._id
|
||||
const open = openDataSources.includes(datasource._id)
|
||||
const containsSelected = containsActiveEntity(datasource)
|
||||
const onlySource = $datasources.list.length === 1
|
||||
return {
|
||||
...datasource,
|
||||
selected,
|
||||
open: selected || open || containsSelected || onlySource,
|
||||
}
|
||||
})
|
||||
: []
|
||||
$: enrichedDataSources = enrichDatasources(
|
||||
$datasources,
|
||||
$params,
|
||||
$isActive,
|
||||
$tables,
|
||||
$queries,
|
||||
$views
|
||||
)
|
||||
$: openDataSource = enrichedDataSources.find(x => x.open)
|
||||
$: {
|
||||
// Ensure the open datasource is always included in the list of open
|
||||
// datasources
|
||||
// Ensure the open datasource is always actually open
|
||||
if (openDataSource) {
|
||||
openNode(openDataSource)
|
||||
}
|
||||
}
|
||||
|
||||
function selectDatasource(datasource) {
|
||||
openNode(datasource)
|
||||
datasources.select(datasource._id)
|
||||
$goto(`./datasource/${datasource._id}`)
|
||||
const enrichDatasources = (
|
||||
datasources,
|
||||
params,
|
||||
isActive,
|
||||
tables,
|
||||
queries,
|
||||
views
|
||||
) => {
|
||||
if (!datasources?.list?.length) {
|
||||
return []
|
||||
}
|
||||
return datasources.list.map(datasource => {
|
||||
const selected =
|
||||
isActive("./datasource") &&
|
||||
datasources.selectedDatasourceId === datasource._id
|
||||
const open = openDataSources.includes(datasource._id)
|
||||
const containsSelected = containsActiveEntity(
|
||||
datasource,
|
||||
params,
|
||||
isActive,
|
||||
tables,
|
||||
queries,
|
||||
views
|
||||
)
|
||||
const onlySource = datasources.list.length === 1
|
||||
return {
|
||||
...datasource,
|
||||
selected,
|
||||
containsSelected,
|
||||
open: selected || open || containsSelected || onlySource,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onClickQuery(query) {
|
||||
queries.select(query)
|
||||
$goto(`./datasource/${query.datasourceId}/${query._id}`)
|
||||
function selectDatasource(datasource) {
|
||||
openNode(datasource)
|
||||
$goto(`./datasource/${datasource._id}`)
|
||||
}
|
||||
|
||||
function closeNode(datasource) {
|
||||
|
@ -69,21 +88,39 @@
|
|||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await datasources.fetch()
|
||||
await queries.fetch()
|
||||
} catch (error) {
|
||||
notifications.error("Error fetching datasources and queries")
|
||||
}
|
||||
})
|
||||
|
||||
const containsActiveEntity = datasource => {
|
||||
// If we're view a query then the datasource ID is in the URL
|
||||
if ($params.selectedDatasource === datasource._id) {
|
||||
const containsActiveEntity = (
|
||||
datasource,
|
||||
params,
|
||||
isActive,
|
||||
tables,
|
||||
queries,
|
||||
views
|
||||
) => {
|
||||
// Check for being on a datasource page
|
||||
if (params.datasourceId === datasource._id) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for hardcoded datasource edge cases
|
||||
if (
|
||||
isActive("./datasource/bb_internal") &&
|
||||
datasource._id === "bb_internal"
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
isActive("./datasource/datasource_internal_bb_default") &&
|
||||
datasource._id === "datasource_internal_bb_default"
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for a matching query
|
||||
if (params.queryId) {
|
||||
const query = queries.list?.find(q => q._id === params.queryId)
|
||||
return datasource._id === query?.datasourceId
|
||||
}
|
||||
|
||||
// If there are no entities it can't contain anything
|
||||
if (!datasource.entities) {
|
||||
return false
|
||||
|
@ -96,13 +133,13 @@
|
|||
}
|
||||
|
||||
// Check for a matching table
|
||||
if ($params.selectedTable) {
|
||||
const selectedTable = get(tables).selected?._id
|
||||
if (params.tableId) {
|
||||
const selectedTable = tables.selected?._id
|
||||
return options.find(x => x._id === selectedTable) != null
|
||||
}
|
||||
|
||||
// Check for a matching view
|
||||
const selectedView = get(views).selected?.name
|
||||
const selectedView = views.selected?.name
|
||||
const table = options.find(table => {
|
||||
return table.views?.[selectedView] != null
|
||||
})
|
||||
|
@ -117,7 +154,7 @@
|
|||
border={idx > 0}
|
||||
text={datasource.name}
|
||||
opened={datasource.open}
|
||||
selected={datasource.selected}
|
||||
selected={$isActive("./datasource") && datasource.selected}
|
||||
withArrow={true}
|
||||
on:click={() => selectDatasource(datasource)}
|
||||
on:iconClick={() => toggleNode(datasource)}
|
||||
|
@ -143,11 +180,11 @@
|
|||
iconText={customQueryIconText(datasource, query)}
|
||||
iconColor={customQueryIconColor(datasource, query)}
|
||||
text={customQueryText(datasource, query)}
|
||||
opened={$queries.selected === query._id}
|
||||
selected={$queries.selected === query._id}
|
||||
on:click={() => onClickQuery(query)}
|
||||
selected={$isActive("./query/:queryId") &&
|
||||
$queries.selectedQueryId === query._id}
|
||||
on:click={() => $goto(`./query/${query._id}`)}
|
||||
>
|
||||
<EditQueryPopover {query} {onClickQuery} />
|
||||
<EditQueryPopover {query} />
|
||||
</NavItem>
|
||||
{/each}
|
||||
{/if}
|
||||
|
@ -156,6 +193,9 @@
|
|||
{/if}
|
||||
|
||||
<style>
|
||||
.hierarchy-items-container {
|
||||
margin: 0 calc(-1 * var(--spacing-xl));
|
||||
}
|
||||
.datasource-icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
|
|
@ -104,7 +104,6 @@
|
|||
}
|
||||
|
||||
function onClickTable(table) {
|
||||
tables.select(table)
|
||||
$goto(`../../table/${table._id}`)
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
import { onMount } from "svelte"
|
||||
import ICONS from "../icons"
|
||||
import { API } from "api"
|
||||
import { IntegrationTypes } from "constants/backend"
|
||||
import { IntegrationTypes, DatasourceTypes } from "constants/backend"
|
||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||
import GoogleDatasourceConfigModal from "components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte"
|
||||
|
@ -31,6 +31,7 @@
|
|||
$: customIntegrations = Object.entries(integrations).filter(
|
||||
entry => entry[1].custom
|
||||
)
|
||||
$: sortedIntegrations = sortIntegrations(integrations)
|
||||
|
||||
checkShowImport()
|
||||
|
||||
|
@ -99,6 +100,29 @@
|
|||
}
|
||||
integrations = newIntegrations
|
||||
}
|
||||
|
||||
function sortIntegrations(integrations) {
|
||||
let integrationsArray = Object.entries(integrations)
|
||||
function getTypeOrder(schema) {
|
||||
if (schema.type === DatasourceTypes.API) {
|
||||
return 1
|
||||
}
|
||||
if (schema.type === DatasourceTypes.RELATIONAL) {
|
||||
return 2
|
||||
}
|
||||
return schema.type?.charCodeAt(0)
|
||||
}
|
||||
|
||||
integrationsArray.sort((a, b) => {
|
||||
let typeOrderA = getTypeOrder(a[1])
|
||||
let typeOrderB = getTypeOrder(b[1])
|
||||
if (typeOrderA === typeOrderB) {
|
||||
return a[1].friendlyName?.localeCompare(b[1].friendlyName)
|
||||
}
|
||||
return typeOrderA < typeOrderB ? -1 : 1
|
||||
})
|
||||
return integrationsArray
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={internalTableModal}>
|
||||
|
@ -157,7 +181,7 @@
|
|||
<Layout noPadding gap="XS">
|
||||
<Body size="S">Connect to an external datasource</Body>
|
||||
<div class="item-list">
|
||||
{#each Object.entries(integrations).filter(([key, val]) => key !== IntegrationTypes.INTERNAL && !val.custom) as [integrationType, schema]}
|
||||
{#each sortedIntegrations.filter(([key, val]) => key !== IntegrationTypes.INTERNAL && !val.custom) as [integrationType, schema]}
|
||||
<DatasourceCard
|
||||
on:selected={evt => selectIntegration(evt.detail)}
|
||||
{schema}
|
||||
|
|
|
@ -64,7 +64,6 @@
|
|||
// reload
|
||||
await datasources.fetch()
|
||||
await queries.fetch()
|
||||
await datasources.select(datasourceId)
|
||||
|
||||
if (navigateDatasource) {
|
||||
$goto(`./datasource/${datasourceId}`)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { datasources, queries, tables } from "stores/backend"
|
||||
import { datasources } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { ActionMenu, MenuItem, Icon } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
|
@ -14,23 +14,10 @@
|
|||
|
||||
async function deleteDatasource() {
|
||||
try {
|
||||
let wasSelectedSource = $datasources.selected
|
||||
if (!wasSelectedSource && $queries.selected) {
|
||||
const queryId = $queries.selected
|
||||
wasSelectedSource = $datasources.list.find(ds =>
|
||||
queryId.includes(ds._id)
|
||||
)?._id
|
||||
}
|
||||
const wasSelectedTable = $tables.selected
|
||||
const isSelected = datasource.selected || datasource.containsSelected
|
||||
await datasources.delete(datasource)
|
||||
notifications.success("Datasource deleted")
|
||||
// Navigate to first index page if the source you are deleting is selected
|
||||
const entities = Object.values(datasource?.entities || {})
|
||||
if (
|
||||
wasSelectedSource === datasource._id ||
|
||||
(entities &&
|
||||
entities.find(entity => entity._id === wasSelectedTable?._id))
|
||||
) {
|
||||
if (isSelected) {
|
||||
$goto("./datasource")
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
@ -5,23 +5,17 @@
|
|||
import { datasources, queries } from "stores/backend"
|
||||
|
||||
export let query
|
||||
export let onClickQuery
|
||||
|
||||
let confirmDeleteDialog
|
||||
|
||||
async function deleteQuery() {
|
||||
try {
|
||||
const wasSelectedQuery = $queries.selected
|
||||
// need to calculate this before the query is deleted
|
||||
const navigateToDatasource = wasSelectedQuery === query._id
|
||||
|
||||
await queries.delete(query)
|
||||
await datasources.fetch()
|
||||
|
||||
if (navigateToDatasource) {
|
||||
await datasources.select(query.datasourceId)
|
||||
// Go back to the datasource if we are deleting the active query
|
||||
if ($queries.selectedQueryId === query._id) {
|
||||
$goto(`./datasource/${query.datasourceId}`)
|
||||
}
|
||||
await queries.delete(query)
|
||||
await datasources.fetch()
|
||||
notifications.success("Query deleted")
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting query")
|
||||
|
@ -31,7 +25,7 @@
|
|||
async function duplicateQuery() {
|
||||
try {
|
||||
const newQuery = await queries.duplicate(query)
|
||||
onClickQuery(newQuery)
|
||||
$goto(`./query/${newQuery._id}`)
|
||||
} catch (error) {
|
||||
notifications.error("Error duplicating query")
|
||||
}
|
||||
|
|
|
@ -1,39 +1,18 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { tables, views, database } from "stores/backend"
|
||||
import { TableNames } from "constants"
|
||||
import EditTablePopover from "./popovers/EditTablePopover.svelte"
|
||||
import EditViewPopover from "./popovers/EditViewPopover.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import { goto, isActive } from "@roxi/routify"
|
||||
|
||||
const alphabetical = (a, b) => a.name?.toLowerCase() > b.name?.toLowerCase()
|
||||
|
||||
export let sourceId
|
||||
|
||||
$: selectedView = $views.selected && $views.selected.name
|
||||
$: sortedTables = $tables.list
|
||||
.filter(table => table.sourceId === sourceId)
|
||||
.sort(alphabetical)
|
||||
|
||||
function selectTable(table) {
|
||||
tables.select(table)
|
||||
$goto(`./table/${table._id}`)
|
||||
}
|
||||
|
||||
function selectView(view) {
|
||||
views.select(view)
|
||||
$goto(`./view/${view.name}`)
|
||||
}
|
||||
|
||||
function onClickView(table, viewName) {
|
||||
if (selectedView === viewName) {
|
||||
return
|
||||
}
|
||||
selectView({
|
||||
name: viewName,
|
||||
...table.views[viewName],
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $database?._id}
|
||||
|
@ -44,8 +23,9 @@
|
|||
border={idx > 0}
|
||||
icon={table._id === TableNames.USERS ? "UserGroup" : "Table"}
|
||||
text={table.name}
|
||||
selected={$tables.selected?._id === table._id}
|
||||
on:click={() => selectTable(table)}
|
||||
selected={$isActive("./table/:tableId") &&
|
||||
$tables.selected?._id === table._id}
|
||||
on:click={() => $goto(`./table/${table._id}`)}
|
||||
>
|
||||
{#if table._id !== TableNames.USERS}
|
||||
<EditTablePopover {table} />
|
||||
|
@ -56,8 +36,9 @@
|
|||
indentLevel={2}
|
||||
icon="Remove"
|
||||
text={viewName}
|
||||
selected={selectedView === viewName}
|
||||
on:click={() => onClickView(table, viewName)}
|
||||
selected={$isActive("./view/:viewName") &&
|
||||
$views.selected?.name === viewName}
|
||||
on:click={() => $goto(`./view/${viewName}`)}
|
||||
>
|
||||
<EditViewPopover
|
||||
view={{ name: viewName, ...table.views[viewName] }}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import { store } from "builderStore"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { tables, datasources } from "stores/backend"
|
||||
|
@ -41,17 +41,16 @@
|
|||
}
|
||||
|
||||
async function deleteTable() {
|
||||
const wasSelectedTable = $tables.selected
|
||||
const isSelected = $params.tableId === table._id
|
||||
try {
|
||||
await tables.delete(table)
|
||||
await store.actions.screens.delete(templateScreens)
|
||||
await tables.fetch()
|
||||
if (table.type === "external") {
|
||||
await datasources.fetch()
|
||||
}
|
||||
notifications.success("Table deleted")
|
||||
if (wasSelectedTable && wasSelectedTable._id === table._id) {
|
||||
$goto("./table")
|
||||
if (isSelected) {
|
||||
$goto(`./datasource/${table.datasourceId}`)
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting table")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import { views } from "stores/backend"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
|
@ -33,11 +33,14 @@
|
|||
|
||||
async function deleteView() {
|
||||
try {
|
||||
const isSelected = $params.viewName === $views.selectedViewName
|
||||
const name = view.name
|
||||
const id = view.tableId
|
||||
await views.delete(name)
|
||||
notifications.success("View deleted")
|
||||
$goto(`./table/${id}`)
|
||||
if (isSelected) {
|
||||
$goto(`./table/${id}`)
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting view")
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
import { ProgressCircle } from "@budibase/bbui"
|
||||
import CopyInput from "components/common/inputs/CopyInput.svelte"
|
||||
|
||||
let feedbackModal
|
||||
let publishModal
|
||||
let asyncModal
|
||||
let publishCompleteModal
|
||||
|
@ -23,13 +22,13 @@
|
|||
|
||||
export let onOk
|
||||
|
||||
async function deployApp() {
|
||||
async function publishApp() {
|
||||
try {
|
||||
//In Progress
|
||||
asyncModal.show()
|
||||
publishModal.hide()
|
||||
|
||||
published = await API.deployAppChanges()
|
||||
published = await API.publishAppChanges($store.appId)
|
||||
|
||||
if (typeof onOk === "function") {
|
||||
await onOk()
|
||||
|
@ -56,20 +55,11 @@
|
|||
</script>
|
||||
|
||||
<Button cta on:click={publishModal.show}>Publish</Button>
|
||||
<Modal bind:this={feedbackModal}>
|
||||
<ModalContent
|
||||
title="Enjoying Budibase?"
|
||||
size="L"
|
||||
showConfirmButton={false}
|
||||
showCancelButton={false}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={publishModal}>
|
||||
<ModalContent
|
||||
title="Publish to Production"
|
||||
confirmText="Publish"
|
||||
onConfirm={deployApp}
|
||||
onConfirm={publishApp}
|
||||
dataCy={"deploy-app-modal"}
|
||||
>
|
||||
<span
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
<style>
|
||||
.panel {
|
||||
width: 260px;
|
||||
flex: 0 0 260px;
|
||||
background: var(--background);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -66,6 +67,7 @@
|
|||
}
|
||||
.panel.wide {
|
||||
width: 420px;
|
||||
flex: 0 0 420px;
|
||||
}
|
||||
.header {
|
||||
flex: 0 0 48px;
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<script>
|
||||
import { Body, Label, Input } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
export let parameters
|
||||
|
||||
onMount(() => {
|
||||
if (!parameters.confirm) {
|
||||
parameters.confirm = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Body size="S">Enter the message you wish to display to the user.</Body>
|
||||
<div class="params">
|
||||
<Label small>Title</Label>
|
||||
<Input placeholder="Prompt User" bind:value={parameters.customTitleText} />
|
||||
<Label small>Message</Label>
|
||||
<Input
|
||||
placeholder="Are you sure you want to continue?"
|
||||
bind:value={parameters.confirmText}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.root :global(p) {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.params {
|
||||
display: grid;
|
||||
column-gap: var(--spacing-l);
|
||||
row-gap: var(--spacing-s);
|
||||
grid-template-columns: 60px 1fr;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -16,5 +16,6 @@ export { default as ExportData } from "./ExportData.svelte"
|
|||
export { default as ContinueIf } from "./ContinueIf.svelte"
|
||||
export { default as UpdateFieldValue } from "./UpdateFieldValue.svelte"
|
||||
export { default as ShowNotification } from "./ShowNotification.svelte"
|
||||
export { default as PromptUser } from "./PromptUser.svelte"
|
||||
export { default as OpenSidePanel } from "./OpenSidePanel.svelte"
|
||||
export { default as CloseSidePanel } from "./CloseSidePanel.svelte"
|
||||
|
|
|
@ -117,6 +117,11 @@
|
|||
"component": "ShowNotification",
|
||||
"dependsOnFeature": "showNotificationAction"
|
||||
},
|
||||
{
|
||||
"name": "Prompt User",
|
||||
"type": "application",
|
||||
"component": "PromptUser"
|
||||
},
|
||||
{
|
||||
"name": "Open Side Panel",
|
||||
"type": "application",
|
||||
|
|
|
@ -29,11 +29,12 @@
|
|||
|
||||
export let query
|
||||
|
||||
const transformerDocs = "https://docs.budibase.com/docs/transformers"
|
||||
|
||||
let fields = query?.schema ? schemaToFields(query.schema) : []
|
||||
let parameters
|
||||
let data = []
|
||||
let saveId
|
||||
const transformerDocs = "https://docs.budibase.com/docs/transformers"
|
||||
|
||||
$: datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
|
||||
$: query.schema = fieldsToSchema(fields)
|
||||
|
@ -94,132 +95,144 @@
|
|||
try {
|
||||
const { _id } = await queries.save(query.datasourceId, query)
|
||||
saveId = _id
|
||||
notifications.success(`Query saved successfully.`)
|
||||
$goto(`../${_id}`)
|
||||
notifications.success(`Query saved successfully`)
|
||||
|
||||
// Go to the correct URL if we just created a new query
|
||||
if (!query._rev) {
|
||||
$goto(`../../${_id}`)
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error creating query")
|
||||
notifications.error("Error saving query")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Layout gap="S" noPadding>
|
||||
<Heading size="M">Query {integrationInfo?.friendlyName}</Heading>
|
||||
<Divider />
|
||||
<Heading size="S">Config</Heading>
|
||||
<div class="config">
|
||||
<div class="config-field">
|
||||
<Label>Query Name</Label>
|
||||
<Input bind:value={query.name} />
|
||||
</div>
|
||||
{#if queryConfig}
|
||||
<div class="config-field">
|
||||
<Label>Function</Label>
|
||||
<Select
|
||||
bind:value={query.queryVerb}
|
||||
on:change={resetDependentFields}
|
||||
options={Object.keys(queryConfig)}
|
||||
getOptionLabel={verb =>
|
||||
queryConfig[verb]?.displayName || capitalise(verb)}
|
||||
/>
|
||||
</div>
|
||||
<div class="config-field">
|
||||
<AccessLevelSelect {saveId} {query} label="Access Level" />
|
||||
</div>
|
||||
{#if integrationInfo?.extra && query.queryVerb}
|
||||
<ExtraQueryConfig
|
||||
{query}
|
||||
{populateExtraQuery}
|
||||
config={integrationInfo.extra}
|
||||
/>
|
||||
{/if}
|
||||
{#key query.parameters}
|
||||
<BindingBuilder
|
||||
queryBindings={query.parameters}
|
||||
bindable={false}
|
||||
on:change={e => {
|
||||
query.parameters = e.detail.map(binding => {
|
||||
return {
|
||||
name: binding.name,
|
||||
default: binding.value,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
{#if shouldShowQueryConfig}
|
||||
<div class="wrapper">
|
||||
<Layout gap="S" noPadding>
|
||||
<Heading size="M">Query {integrationInfo?.friendlyName}</Heading>
|
||||
<Divider />
|
||||
<Heading size="S">Config</Heading>
|
||||
<div class="config">
|
||||
<Heading size="S">Fields</Heading>
|
||||
<Body size="S">Fill in the fields specific to this query.</Body>
|
||||
<IntegrationQueryEditor
|
||||
{datasource}
|
||||
{query}
|
||||
height={200}
|
||||
schema={queryConfig[query.queryVerb]}
|
||||
bind:parameters
|
||||
/>
|
||||
<Divider />
|
||||
</div>
|
||||
<div class="config">
|
||||
<div class="help-heading">
|
||||
<Heading size="S">Transformer</Heading>
|
||||
<Icon
|
||||
on:click={() => window.open(transformerDocs)}
|
||||
hoverable
|
||||
name="Help"
|
||||
size="L"
|
||||
/>
|
||||
<div class="config-field">
|
||||
<Label>Query Name</Label>
|
||||
<Input bind:value={query.name} />
|
||||
</div>
|
||||
<Body size="S"
|
||||
>Add a JavaScript function to transform the query result.</Body
|
||||
>
|
||||
<CodeMirrorEditor
|
||||
height={200}
|
||||
label="Transformer"
|
||||
value={query.transformer}
|
||||
resize="vertical"
|
||||
on:change={e => (query.transformer = e.detail)}
|
||||
/>
|
||||
<Divider />
|
||||
</div>
|
||||
<div class="viewer-controls">
|
||||
<Heading size="S">Results</Heading>
|
||||
<ButtonGroup gap="M">
|
||||
<Button cta disabled={queryInvalid} on:click={saveQuery}>
|
||||
Save Query
|
||||
</Button>
|
||||
<Button secondary on:click={previewQuery}>Run Query</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<Body size="S">
|
||||
Below, you can preview the results from your query and change the schema.
|
||||
</Body>
|
||||
<section class="viewer">
|
||||
{#if data}
|
||||
<Tabs selected="JSON">
|
||||
<Tab title="JSON">
|
||||
<JSONPreview data={data[0]} minHeight="120" />
|
||||
</Tab>
|
||||
<Tab title="Schema">
|
||||
<KeyValueBuilder
|
||||
bind:object={fields}
|
||||
name="field"
|
||||
headings
|
||||
options={SchemaTypeOptions}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab title="Preview">
|
||||
<ExternalDataSourceTable {query} {data} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
{#if queryConfig}
|
||||
<div class="config-field">
|
||||
<Label>Function</Label>
|
||||
<Select
|
||||
bind:value={query.queryVerb}
|
||||
on:change={resetDependentFields}
|
||||
options={Object.keys(queryConfig)}
|
||||
getOptionLabel={verb =>
|
||||
queryConfig[verb]?.displayName || capitalise(verb)}
|
||||
/>
|
||||
</div>
|
||||
<div class="config-field">
|
||||
<AccessLevelSelect {saveId} {query} label="Access Level" />
|
||||
</div>
|
||||
{#if integrationInfo?.extra && query.queryVerb}
|
||||
<ExtraQueryConfig
|
||||
{query}
|
||||
{populateExtraQuery}
|
||||
config={integrationInfo.extra}
|
||||
/>
|
||||
{/if}
|
||||
{#key query.parameters}
|
||||
<BindingBuilder
|
||||
queryBindings={query.parameters}
|
||||
bindable={false}
|
||||
on:change={e => {
|
||||
query.parameters = e.detail.map(binding => {
|
||||
return {
|
||||
name: binding.name,
|
||||
default: binding.value,
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
{#if shouldShowQueryConfig}
|
||||
<Divider />
|
||||
<div class="config">
|
||||
<Heading size="S">Fields</Heading>
|
||||
<Body size="S">Fill in the fields specific to this query.</Body>
|
||||
<IntegrationQueryEditor
|
||||
{datasource}
|
||||
{query}
|
||||
height={200}
|
||||
schema={queryConfig[query.queryVerb]}
|
||||
bind:parameters
|
||||
/>
|
||||
<Divider />
|
||||
</div>
|
||||
<div class="config">
|
||||
<div class="help-heading">
|
||||
<Heading size="S">Transformer</Heading>
|
||||
<Icon
|
||||
on:click={() => window.open(transformerDocs)}
|
||||
hoverable
|
||||
name="Help"
|
||||
size="L"
|
||||
/>
|
||||
</div>
|
||||
<Body size="S"
|
||||
>Add a JavaScript function to transform the query result.</Body
|
||||
>
|
||||
<CodeMirrorEditor
|
||||
height={200}
|
||||
label="Transformer"
|
||||
value={query.transformer}
|
||||
resize="vertical"
|
||||
on:change={e => (query.transformer = e.detail)}
|
||||
/>
|
||||
<Divider />
|
||||
</div>
|
||||
<div class="viewer-controls">
|
||||
<Heading size="S">Results</Heading>
|
||||
<ButtonGroup gap="XS">
|
||||
<Button cta disabled={queryInvalid} on:click={saveQuery}>
|
||||
Save Query
|
||||
</Button>
|
||||
<Button secondary on:click={previewQuery}>Run Query</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<Body size="S">
|
||||
Below, you can preview the results from your query and change the
|
||||
schema.
|
||||
</Body>
|
||||
<section class="viewer">
|
||||
{#if data}
|
||||
<Tabs selected="JSON">
|
||||
<Tab title="JSON">
|
||||
<JSONPreview data={data[0]} minHeight="120" />
|
||||
</Tab>
|
||||
<Tab title="Schema">
|
||||
<KeyValueBuilder
|
||||
bind:object={fields}
|
||||
name="field"
|
||||
headings
|
||||
options={SchemaTypeOptions}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab title="Preview">
|
||||
<ExternalDataSourceTable {query} {data} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
width: 640px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.config {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-s);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { params } from "@roxi/routify"
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import { datasources, flags, integrations, queries } from "stores/backend"
|
||||
import {
|
||||
Banner,
|
||||
|
@ -23,7 +23,7 @@
|
|||
import CodeMirrorEditor, {
|
||||
EditorModes,
|
||||
} from "components/common/CodeMirrorEditor.svelte"
|
||||
import RestBodyInput from "../../_components/RestBodyInput.svelte"
|
||||
import RestBodyInput from "./RestBodyInput.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
import { onMount } from "svelte"
|
||||
import restUtils from "helpers/data/utils"
|
||||
|
@ -36,7 +36,7 @@
|
|||
} from "constants/backend"
|
||||
import JSONPreview from "components/integration/JSONPreview.svelte"
|
||||
import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte"
|
||||
import DynamicVariableModal from "../../_components/DynamicVariableModal.svelte"
|
||||
import DynamicVariableModal from "./DynamicVariableModal.svelte"
|
||||
import Placeholder from "assets/bb-spaceship.svg"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
|
@ -49,6 +49,8 @@
|
|||
toBindingsArray,
|
||||
} from "builderStore/dataBinding"
|
||||
|
||||
export let queryId
|
||||
|
||||
let query, datasource
|
||||
let breakQs = {},
|
||||
requestBindings = {}
|
||||
|
@ -102,8 +104,8 @@
|
|||
|
||||
function getSelectedQuery() {
|
||||
return cloneDeep(
|
||||
$queries.list.find(q => q._id === $queries.selected) || {
|
||||
datasourceId: $params.selectedDatasource,
|
||||
$queries.list.find(q => q._id === queryId) || {
|
||||
datasourceId: $params.datasourceId,
|
||||
parameters: [],
|
||||
fields: {
|
||||
// only init the objects, everything else is optional strings
|
||||
|
@ -159,6 +161,7 @@
|
|||
async function saveQuery() {
|
||||
const toSave = buildQuery()
|
||||
try {
|
||||
const isNew = !query._rev
|
||||
const { _id } = await queries.save(toSave.datasourceId, toSave)
|
||||
saveId = _id
|
||||
query = getSelectedQuery()
|
||||
|
@ -174,6 +177,9 @@
|
|||
staticVariables,
|
||||
restBindings
|
||||
)
|
||||
if (isNew) {
|
||||
$goto(`../../${_id}`)
|
||||
}
|
||||
} catch (err) {
|
||||
notifications.error(`Error saving query`)
|
||||
}
|
||||
|
@ -464,8 +470,9 @@
|
|||
on:click={saveQuery}
|
||||
tooltip={!hasSchema
|
||||
? "Saving a query before sending will mean no schema is generated"
|
||||
: null}>Save</Button
|
||||
>
|
||||
: null}
|
||||
>Save
|
||||
</Button>
|
||||
</div>
|
||||
<Tabs selected="Bindings" quiet noPadding noHorizPadding onTop>
|
||||
<Tab title="Bindings">
|
||||
|
@ -708,26 +715,33 @@
|
|||
margin: 0 auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 960px;
|
||||
}
|
||||
|
||||
.url-block {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.verb {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.url {
|
||||
flex: 4;
|
||||
}
|
||||
|
||||
.top {
|
||||
min-height: 50%;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: var(--spacing-xl);
|
||||
|
@ -735,40 +749,49 @@
|
|||
margin-right: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.green {
|
||||
color: #53a761;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #ea7d82;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.access {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.placeholder-internal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 200px;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: flex;
|
||||
margin-top: var(--spacing-xl);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.auth-select {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
|
@ -261,3 +261,12 @@ export const BannedSearchTypes = [
|
|||
"json",
|
||||
"jsonarray",
|
||||
]
|
||||
|
||||
export const DatasourceTypes = {
|
||||
RELATIONAL: "Relational",
|
||||
NON_RELATIONAL: "Non-relational",
|
||||
SPREADSHEET: "Spreadsheet",
|
||||
OBJECT_STORE: "Object store",
|
||||
GRAPH: "Graph",
|
||||
API: "API",
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ export const syncURLToState = options => {
|
|||
urlParam,
|
||||
stateKey,
|
||||
validate,
|
||||
update,
|
||||
baseUrl = "..",
|
||||
fallbackUrl,
|
||||
store,
|
||||
|
@ -38,7 +39,7 @@ export const syncURLToState = options => {
|
|||
let cachedPage = get(routify.page)
|
||||
let previousParamsHash = null
|
||||
let debug = false
|
||||
const log = (...params) => debug && console.log(...params)
|
||||
const log = (...params) => debug && console.log(`[${urlParam}]`, ...params)
|
||||
|
||||
// Navigate to a certain URL
|
||||
const gotoUrl = (url, params) => {
|
||||
|
@ -85,10 +86,16 @@ export const syncURLToState = options => {
|
|||
// Only update state if we have a new value
|
||||
if (urlValue !== stateValue) {
|
||||
log(`state.${stateKey} (${stateValue}) <= url.${urlParam} (${urlValue})`)
|
||||
store.update(state => {
|
||||
state[stateKey] = urlValue
|
||||
return state
|
||||
})
|
||||
if (update) {
|
||||
// Use custom update function if provided
|
||||
update(urlValue)
|
||||
} else {
|
||||
// Otherwise manually update the store
|
||||
store.update(state => ({
|
||||
...state,
|
||||
[stateKey]: urlValue,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,75 +1,46 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
import { Button, Tabs, Tab, Layout } from "@budibase/bbui"
|
||||
import { Button, Layout } from "@budibase/bbui"
|
||||
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
||||
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
|
||||
|
||||
let selected = "Sources"
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
|
||||
let modal
|
||||
|
||||
function selectFirstDatasource() {
|
||||
$redirect("./table")
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- routify:options index=1 -->
|
||||
<div class="root">
|
||||
<div class="nav">
|
||||
<Tabs {selected} on:select={selectFirstDatasource}>
|
||||
<Tab title="Sources">
|
||||
<Layout paddingX="L" paddingY="L" gap="S">
|
||||
<Button dataCy={`new-datasource`} cta wide on:click={modal.show}
|
||||
>Add source</Button
|
||||
>
|
||||
</Layout>
|
||||
<CreateDatasourceModal bind:modal />
|
||||
<DatasourceNavigator />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div class="data">
|
||||
<Panel title="Sources" borderRight>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<Button dataCy={`new-datasource`} cta on:click={modal.show}>
|
||||
Add source
|
||||
</Button>
|
||||
<CreateDatasourceModal bind:modal />
|
||||
<DatasourceNavigator />
|
||||
</Layout>
|
||||
</Panel>
|
||||
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
.data {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 260px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
padding: var(--spacing-l) 40px 40px 40px;
|
||||
padding: 28px 40px 40px 40px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
.content :global(> span) {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.nav {
|
||||
overflow-y: auto;
|
||||
background: var(--background);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
border-right: var(--border-light);
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
position: absolute;
|
||||
top: var(--spacing-l);
|
||||
right: var(--spacing-xl);
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<script>
|
||||
import { params } from "@roxi/routify"
|
||||
import { datasources } from "stores/backend"
|
||||
import { syncURLToState } from "helpers/urlStateSync"
|
||||
import * as routify from "@roxi/routify"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
const stopSyncing = syncURLToState({
|
||||
urlParam: "datasourceId",
|
||||
stateKey: "selectedDatasourceId",
|
||||
validate: id => $datasources.list?.some(ds => ds._id === id),
|
||||
update: datasources.select,
|
||||
fallbackUrl: "../",
|
||||
store: datasources,
|
||||
routify,
|
||||
})
|
||||
|
||||
onDestroy(stopSyncing)
|
||||
</script>
|
||||
|
||||
{#key $params.datasourceId}
|
||||
<slot />
|
||||
{/key}
|
|
@ -21,34 +21,31 @@
|
|||
import { cloneDeep } from "lodash/fp"
|
||||
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
||||
|
||||
let importQueriesModal
|
||||
|
||||
let changed,
|
||||
isValid = true
|
||||
let integration, baseDatasource, datasource
|
||||
let queryList
|
||||
const querySchema = {
|
||||
name: {},
|
||||
queryVerb: { displayName: "Method" },
|
||||
}
|
||||
|
||||
$: baseDatasource = $datasources.list.find(
|
||||
ds => ds._id === $datasources.selected
|
||||
)
|
||||
let importQueriesModal
|
||||
let changed = false
|
||||
let isValid = true
|
||||
let integration, baseDatasource, datasource
|
||||
let queryList
|
||||
|
||||
$: baseDatasource = $datasources.selected
|
||||
$: queryList = $queries.list.filter(
|
||||
query => query.datasourceId === datasource?._id
|
||||
)
|
||||
$: hasChanged(baseDatasource, datasource)
|
||||
$: updateDatasource(baseDatasource)
|
||||
|
||||
function hasChanged(base, ds) {
|
||||
const hasChanged = (base, ds) => {
|
||||
if (base && ds) {
|
||||
changed = !isEqual(base, ds)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDatasource() {
|
||||
const saveDatasource = async () => {
|
||||
try {
|
||||
// Create datasource
|
||||
await datasources.save(datasource)
|
||||
|
@ -63,12 +60,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
function onClickQuery(query) {
|
||||
queries.select(query)
|
||||
$goto(`./${query._id}`)
|
||||
}
|
||||
|
||||
function updateDatasource(base) {
|
||||
const updateDatasource = base => {
|
||||
if (base) {
|
||||
datasource = cloneDeep(base)
|
||||
integration = $integrations[datasource.source]
|
||||
|
@ -87,7 +79,7 @@
|
|||
|
||||
{#if datasource && integration}
|
||||
<section>
|
||||
<Layout>
|
||||
<Layout noPadding>
|
||||
<Layout gap="XS" noPadding>
|
||||
<header>
|
||||
<svelte:component
|
||||
|
@ -95,16 +87,16 @@
|
|||
height="26"
|
||||
width="26"
|
||||
/>
|
||||
<Heading size="M">{datasource.name}</Heading>
|
||||
<Heading size="M">{$datasources.selected?.name}</Heading>
|
||||
</header>
|
||||
<Body size="M">{integration.description}</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
<div class="config-header">
|
||||
<Heading size="S">Configuration</Heading>
|
||||
<Button disabled={!changed || !isValid} cta on:click={saveDatasource}
|
||||
>Save</Button
|
||||
>
|
||||
<Button disabled={!changed || !isValid} cta on:click={saveDatasource}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<IntegrationConfigForm
|
||||
on:change={hasChanged}
|
||||
|
@ -120,12 +112,16 @@
|
|||
<Heading size="S">Queries</Heading>
|
||||
<div class="query-buttons">
|
||||
{#if datasource?.source === IntegrationTypes.REST}
|
||||
<Button secondary on:click={() => importQueriesModal.show()}
|
||||
>Import</Button
|
||||
>
|
||||
<Button secondary on:click={() => importQueriesModal.show()}>
|
||||
Import
|
||||
</Button>
|
||||
{/if}
|
||||
<Button cta icon="Add" on:click={() => $goto("./new")}
|
||||
>Add query
|
||||
<Button
|
||||
cta
|
||||
icon="Add"
|
||||
on:click={() => $goto(`../../query/new/${datasource._id}`)}
|
||||
>
|
||||
Add query
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -137,7 +133,7 @@
|
|||
{#if queryList && queryList.length > 0}
|
||||
<div class="query-list">
|
||||
<Table
|
||||
on:click={({ detail }) => onClickQuery(detail)}
|
||||
on:click={({ detail }) => $goto(`../../query/${detail._id}`)}
|
||||
schema={querySchema}
|
||||
data={queryList}
|
||||
allowEditColumns={false}
|
|
@ -1,23 +0,0 @@
|
|||
<script>
|
||||
import { params } from "@roxi/routify"
|
||||
import { queries, datasources } from "stores/backend"
|
||||
import { IntegrationTypes } from "constants/backend"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
let datasourceId
|
||||
if ($params.query) {
|
||||
const query = $queries.list.find(q => q._id === $params.query)
|
||||
if (query) {
|
||||
queries.select(query)
|
||||
datasourceId = query.datasourceId
|
||||
}
|
||||
}
|
||||
const datasource = $datasources.list.find(
|
||||
ds => ds._id === $datasources.selected || ds._id === datasourceId
|
||||
)
|
||||
if (datasource?.source === IntegrationTypes.REST) {
|
||||
$redirect(`../rest/${$params.query}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -1,39 +0,0 @@
|
|||
<script>
|
||||
import { params, redirect } from "@roxi/routify"
|
||||
import { database, datasources, queries } from "stores/backend"
|
||||
import QueryInterface from "components/integration/QueryViewer.svelte"
|
||||
import { IntegrationTypes } from "constants/backend"
|
||||
|
||||
let selectedQuery, datasource
|
||||
$: selectedQuery = $queries.list.find(
|
||||
query => query._id === $queries.selected
|
||||
) || {
|
||||
datasourceId: $params.selectedDatasource,
|
||||
parameters: [],
|
||||
fields: {},
|
||||
queryVerb: "read",
|
||||
}
|
||||
$: datasource = $datasources.list.find(
|
||||
ds => ds._id === $params.selectedDatasource
|
||||
)
|
||||
$: {
|
||||
if (datasource?.source === IntegrationTypes.REST) {
|
||||
$redirect(`../rest/${$params.query}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<div class="inner">
|
||||
{#if $database._id && selectedQuery}
|
||||
<QueryInterface query={selectedQuery} />
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.inner {
|
||||
width: 640px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
|
@ -1,17 +0,0 @@
|
|||
<script>
|
||||
import { params } from "@roxi/routify"
|
||||
import { datasources } from "stores/backend"
|
||||
|
||||
if ($params.selectedDatasource && !$params.query) {
|
||||
const datasource = $datasources.list.find(
|
||||
m => m._id === $params.selectedDatasource
|
||||
)
|
||||
if (datasource) {
|
||||
datasources.select(datasource._id)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#key $params.selectedDatasource}
|
||||
<slot />
|
||||
{/key}
|
|
@ -1,13 +0,0 @@
|
|||
<script>
|
||||
import { params } from "@roxi/routify"
|
||||
import { queries } from "stores/backend"
|
||||
|
||||
if ($params.query) {
|
||||
const query = $queries.list.find(q => q._id === $params.query)
|
||||
if (query) {
|
||||
queries.select(query)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -1,7 +0,0 @@
|
|||
<script>
|
||||
import { datasources } from "stores/backend"
|
||||
|
||||
datasources.select("bb_internal")
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -4,13 +4,19 @@
|
|||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||
import { tables, datasources } from "stores/backend"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { onMount } from "svelte"
|
||||
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
|
||||
|
||||
let modal
|
||||
|
||||
$: internalTablesBySourceId = $tables.list.filter(
|
||||
table =>
|
||||
table.type !== "external" && $datasources.selected === table.sourceId
|
||||
table.type !== "external" && table.sourceId === BUDIBASE_INTERNAL_DB_ID
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
datasources.select(BUDIBASE_INTERNAL_DB_ID)
|
||||
})
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
|
@ -73,7 +79,7 @@
|
|||
background: var(--background);
|
||||
border: var(--border-dark);
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 0.75fr 20px;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
padding: var(--spacing-m);
|
||||
gap: var(--layout-xs);
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
<script>
|
||||
import { datasources } from "stores/backend"
|
||||
import { DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
|
||||
|
||||
datasources.select(DEFAULT_BB_DATASOURCE_ID)
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -4,12 +4,18 @@
|
|||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||
import { tables, datasources } from "stores/backend"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let modal
|
||||
$: internalTablesBySourceId = $tables.list.filter(
|
||||
table =>
|
||||
table.type !== "external" && $datasources.selected === table.sourceId
|
||||
table.type !== "external" && table.sourceId === DEFAULT_BB_DATASOURCE_ID
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
datasources.select(DEFAULT_BB_DATASOURCE_ID)
|
||||
})
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
|
@ -23,10 +29,11 @@
|
|||
<svelte:component this={ICONS.BUDIBASE} height="26" width="26" />
|
||||
<Heading size="M">Sample Data</Heading>
|
||||
</header>
|
||||
<Body size="M">A little something to get you up and running!</Body>
|
||||
<Body size="M"
|
||||
>If you have no need for this datasource, feel free to delete it.</Body
|
||||
>
|
||||
<Body size="M">
|
||||
A little something to get you up and running!
|
||||
<br />
|
||||
If you have no need for this datasource, feel free to delete it.
|
||||
</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
<Heading size="S">Tables</Heading>
|
||||
|
@ -73,7 +80,7 @@
|
|||
background: var(--background);
|
||||
border: var(--border-dark);
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 0.75fr 20px;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
padding: var(--spacing-m);
|
||||
gap: var(--layout-xs);
|
||||
|
|
|
@ -4,12 +4,16 @@
|
|||
import { onMount } from "svelte"
|
||||
|
||||
onMount(async () => {
|
||||
// navigate to first table in list, if not already selected
|
||||
$datasources.list.length > 0 && $redirect(`./${$datasources.list[0]._id}`)
|
||||
const { list, selected } = $datasources
|
||||
if (selected) {
|
||||
$redirect(`./${selected?._id}`)
|
||||
} else {
|
||||
$redirect(`./${list[0]._id}`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $datasources.list.length === 0}
|
||||
{#if !$datasources.list?.length}
|
||||
<i>Connect your first datasource to start building.</i>
|
||||
{:else}<i>Select a datasource to edit</i>{/if}
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
import { queries } from "stores/backend"
|
||||
import { syncURLToState } from "helpers/urlStateSync"
|
||||
import * as routify from "@roxi/routify"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
const stopSyncing = syncURLToState({
|
||||
urlParam: "queryId",
|
||||
stateKey: "selectedQueryId",
|
||||
validate: id => id === "new" || $queries.list?.some(q => q._id === id),
|
||||
update: queries.select,
|
||||
fallbackUrl: "../",
|
||||
store: queries,
|
||||
routify,
|
||||
})
|
||||
|
||||
onDestroy(stopSyncing)
|
||||
</script>
|
||||
|
||||
{#key $queries.selectedQueryId}
|
||||
<slot />
|
||||
{/key}
|
|
@ -0,0 +1,18 @@
|
|||
<script>
|
||||
import { database, queries, datasources } from "stores/backend"
|
||||
import QueryViewer from "components/integration/QueryViewer.svelte"
|
||||
import RestQueryViewer from "components/integration/RestQueryViewer.svelte"
|
||||
import { IntegrationTypes } from "constants/backend"
|
||||
|
||||
$: query = $queries.selected
|
||||
$: datasource = $datasources.list.find(ds => ds._id === query?.datasourceId)
|
||||
$: isRestQuery = datasource?.source === IntegrationTypes.REST
|
||||
</script>
|
||||
|
||||
{#if $database._id && query}
|
||||
{#if isRestQuery}
|
||||
<RestQueryViewer queryId={$queries.selectedQueryId} />
|
||||
{:else}
|
||||
<QueryViewer {query} />
|
||||
{/if}
|
||||
{/if}
|
|
@ -0,0 +1,16 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { queries } from "stores/backend"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
onMount(async () => {
|
||||
const { list, selected } = $queries
|
||||
if (selected) {
|
||||
$redirect(`./${selected?._id}`)
|
||||
} else if (list?.length) {
|
||||
$redirect(`./${list[0]._id}`)
|
||||
} else {
|
||||
$redirect("../")
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,38 @@
|
|||
<script>
|
||||
import { params, redirect } from "@roxi/routify"
|
||||
import { database, datasources } from "stores/backend"
|
||||
import QueryViewer from "components/integration/QueryViewer.svelte"
|
||||
import RestQueryViewer from "components/integration/RestQueryViewer.svelte"
|
||||
import { IntegrationTypes } from "constants/backend"
|
||||
|
||||
$: datasource = $datasources.list.find(ds => ds._id === $params.datasourceId)
|
||||
$: {
|
||||
if (!datasource) {
|
||||
$redirect("../../../")
|
||||
}
|
||||
}
|
||||
$: isRestQuery = datasource?.source === IntegrationTypes.REST
|
||||
$: query = buildNewQuery(isRestQuery)
|
||||
|
||||
const buildNewQuery = isRestQuery => {
|
||||
let query = {
|
||||
datasourceId: $params.datasourceId,
|
||||
parameters: [],
|
||||
fields: {},
|
||||
queryVerb: "read",
|
||||
}
|
||||
if (isRestQuery) {
|
||||
query.flags = {}
|
||||
query.fields = { disabledHeaders: {}, headers: {} }
|
||||
}
|
||||
return query
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $database._id && datasource && query}
|
||||
{#if isRestQuery}
|
||||
<RestQueryViewer />
|
||||
{:else}
|
||||
<QueryViewer {query} />
|
||||
{/if}
|
||||
{/if}
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
$redirect("../")
|
||||
</script>
|
|
@ -1,13 +0,0 @@
|
|||
<script>
|
||||
import { params } from "@roxi/routify"
|
||||
import { tables } from "stores/backend"
|
||||
|
||||
if ($params.selectedTable) {
|
||||
const table = $tables.list.find(m => m._id === $params.selectedTable)
|
||||
if (table) {
|
||||
tables.select(table)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -0,0 +1,20 @@
|
|||
<script>
|
||||
import { syncURLToState } from "helpers/urlStateSync"
|
||||
import { tables } from "stores/backend"
|
||||
import * as routify from "@roxi/routify"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
const stopSyncing = syncURLToState({
|
||||
urlParam: "tableId",
|
||||
stateKey: "selectedTableId",
|
||||
validate: id => $tables.list?.some(table => table._id === id),
|
||||
update: tables.select,
|
||||
fallbackUrl: "../",
|
||||
store: tables,
|
||||
routify,
|
||||
})
|
||||
|
||||
onDestroy(stopSyncing)
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -3,9 +3,11 @@
|
|||
import { tables, database } from "stores/backend"
|
||||
</script>
|
||||
|
||||
{#if $database?._id && $tables?.selected?.name}
|
||||
{#if $database?._id && $tables?.selected}
|
||||
<TableDataTable />
|
||||
{:else}<i>Create your first table to start building</i>{/if}
|
||||
{:else}
|
||||
<i>Create your first table to start building</i>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
i {
|
|
@ -4,7 +4,7 @@
|
|||
</script>
|
||||
|
||||
<RelationshipDataTable
|
||||
tableId={$params.selectedTable}
|
||||
rowId={$params.selectedRow}
|
||||
fieldName={decodeURI($params.selectedField)}
|
||||
tableId={$params.tableId}
|
||||
rowId={$params.rowId}
|
||||
fieldName={decodeURI($params.field)}
|
||||
/>
|
|
@ -1,19 +0,0 @@
|
|||
<script>
|
||||
import { tables } from "stores/backend"
|
||||
import { redirect, leftover } from "@roxi/routify"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
onMount(async () => {
|
||||
// navigate to first table in list, if not already selected
|
||||
// and this is the final url (i.e. no selectedTable)
|
||||
if (
|
||||
!$leftover &&
|
||||
$tables.list.length > 0 &&
|
||||
(!$tables.selected || !$tables.selected._id)
|
||||
) {
|
||||
$redirect(`./${$tables.list[0]._id}`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -1,14 +1,19 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
import { onMount } from "svelte"
|
||||
import { tables } from "stores/backend"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
onMount(async () => {
|
||||
$tables.list.length > 0 && $redirect(`./${$tables.list[0]._id}`)
|
||||
const { list, selected } = $tables
|
||||
if (selected) {
|
||||
$redirect(`./${selected?._id}`)
|
||||
} else if (list?.length) {
|
||||
$redirect(`./${list[0]._id}`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $tables.list.length === 0}
|
||||
{#if !$tables.list?.length}
|
||||
<i>Create your first table to start building</i>
|
||||
{:else}<i>Select a table to edit</i>{/if}
|
||||
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
<script>
|
||||
import { params } from "@roxi/routify"
|
||||
import { tables, views } from "stores/backend"
|
||||
|
||||
if ($params.selectedView) {
|
||||
let view
|
||||
const viewName = decodeURI($params.selectedView)
|
||||
for (let table of $tables.list) {
|
||||
if (table.views && table.views[viewName]) {
|
||||
view = table.views[viewName]
|
||||
}
|
||||
}
|
||||
if (view) {
|
||||
views.select({
|
||||
name: viewName,
|
||||
...view,
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -0,0 +1,20 @@
|
|||
<script>
|
||||
import { views } from "stores/backend"
|
||||
import { syncURLToState } from "helpers/urlStateSync"
|
||||
import * as routify from "@roxi/routify"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
const stopSyncing = syncURLToState({
|
||||
urlParam: "viewName",
|
||||
stateKey: "selectedViewName",
|
||||
validate: name => $views.list?.some(view => view.name === name),
|
||||
update: views.select,
|
||||
fallbackUrl: "../",
|
||||
store: views,
|
||||
routify,
|
||||
})
|
||||
|
||||
onDestroy(stopSyncing)
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -0,0 +1,16 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { views } from "stores/backend"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
onMount(async () => {
|
||||
const { list, selected } = $views
|
||||
if (selected) {
|
||||
$redirect(`./${selected?.name}`)
|
||||
} else if (list?.length) {
|
||||
$redirect(`./${list[0].name}`)
|
||||
} else {
|
||||
$redirect("../")
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -16,6 +16,7 @@
|
|||
import UpdateUserInfoModal from "components/settings/UpdateUserInfoModal.svelte"
|
||||
import ChangePasswordModal from "components/settings/ChangePasswordModal.svelte"
|
||||
import Logo from "assets/bb-emblem.svg"
|
||||
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
|
||||
// import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
||||
|
||||
let loaded = false
|
||||
|
@ -173,6 +174,9 @@
|
|||
{/each}
|
||||
</Tabs>
|
||||
<div class="toolbar">
|
||||
{#if $auth.user?.admin?.global}
|
||||
<ConfigChecklist />
|
||||
{/if}
|
||||
<div class="mobile-toggle">
|
||||
<Icon hoverable name="ShowMenu" on:click={showMobileMenu} />
|
||||
</div>
|
||||
|
|
|
@ -1,20 +1,36 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { queries, tables, views } from "./"
|
||||
import { writable, derived } from "svelte/store"
|
||||
import { queries, tables } from "./"
|
||||
import { API } from "api"
|
||||
|
||||
export const INITIAL_DATASOURCE_VALUES = {
|
||||
list: [],
|
||||
selected: null,
|
||||
schemaError: null,
|
||||
}
|
||||
|
||||
export function createDatasourcesStore() {
|
||||
const store = writable(INITIAL_DATASOURCE_VALUES)
|
||||
const { subscribe, update, set } = store
|
||||
const store = writable({
|
||||
list: [],
|
||||
selectedDatasourceId: null,
|
||||
schemaError: null,
|
||||
})
|
||||
const derivedStore = derived(store, $store => ({
|
||||
...$store,
|
||||
selected: $store.list?.find(ds => ds._id === $store.selectedDatasourceId),
|
||||
}))
|
||||
|
||||
async function updateDatasource(response) {
|
||||
const fetch = async () => {
|
||||
const datasources = await API.getDatasources()
|
||||
store.update(state => ({
|
||||
...state,
|
||||
list: datasources,
|
||||
}))
|
||||
}
|
||||
|
||||
const select = id => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
selectedDatasourceId: id,
|
||||
}))
|
||||
}
|
||||
|
||||
const updateDatasource = async response => {
|
||||
const { datasource, error } = response
|
||||
update(state => {
|
||||
store.update(state => {
|
||||
const currentIdx = state.list.findIndex(ds => ds._id === datasource._id)
|
||||
const sources = state.list
|
||||
if (currentIdx >= 0) {
|
||||
|
@ -24,82 +40,64 @@ export function createDatasourcesStore() {
|
|||
}
|
||||
return {
|
||||
list: sources,
|
||||
selected: datasource._id,
|
||||
selectedDatasourceId: datasource._id,
|
||||
schemaError: error,
|
||||
}
|
||||
})
|
||||
return datasource
|
||||
}
|
||||
|
||||
const updateSchema = async (datasource, tablesFilter) => {
|
||||
const response = await API.buildDatasourceSchema({
|
||||
datasourceId: datasource?._id,
|
||||
tablesFilter,
|
||||
})
|
||||
return await updateDatasource(response)
|
||||
}
|
||||
|
||||
const save = async (body, fetchSchema = false) => {
|
||||
let response
|
||||
if (body._id) {
|
||||
response = await API.updateDatasource(body)
|
||||
} else {
|
||||
response = await API.createDatasource({
|
||||
datasource: body,
|
||||
fetchSchema,
|
||||
})
|
||||
}
|
||||
return updateDatasource(response)
|
||||
}
|
||||
|
||||
const deleteDatasource = async datasource => {
|
||||
await API.deleteDatasource({
|
||||
datasourceId: datasource?._id,
|
||||
datasourceRev: datasource?._rev,
|
||||
})
|
||||
store.update(state => {
|
||||
const sources = state.list.filter(
|
||||
existing => existing._id !== datasource._id
|
||||
)
|
||||
return { list: sources, selected: null }
|
||||
})
|
||||
await queries.fetch()
|
||||
await tables.fetch()
|
||||
}
|
||||
|
||||
const removeSchemaError = () => {
|
||||
store.update(state => {
|
||||
return { ...state, schemaError: null }
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
update,
|
||||
init: async () => {
|
||||
const datasources = await API.getDatasources()
|
||||
set({
|
||||
list: datasources,
|
||||
selected: null,
|
||||
})
|
||||
},
|
||||
fetch: async () => {
|
||||
const datasources = await API.getDatasources()
|
||||
|
||||
// Clear selected if it no longer exists, otherwise keep it
|
||||
const selected = get(store).selected
|
||||
let nextSelected = null
|
||||
if (selected && datasources.find(source => source._id === selected)) {
|
||||
nextSelected = selected
|
||||
}
|
||||
|
||||
update(state => ({ ...state, list: datasources, selected: nextSelected }))
|
||||
},
|
||||
select: datasourceId => {
|
||||
update(state => ({ ...state, selected: datasourceId }))
|
||||
queries.unselect()
|
||||
tables.unselect()
|
||||
views.unselect()
|
||||
},
|
||||
unselect: () => {
|
||||
update(state => ({ ...state, selected: null }))
|
||||
},
|
||||
updateSchema: async (datasource, tablesFilter) => {
|
||||
const response = await API.buildDatasourceSchema({
|
||||
datasourceId: datasource?._id,
|
||||
tablesFilter,
|
||||
})
|
||||
return await updateDatasource(response)
|
||||
},
|
||||
save: async (body, fetchSchema = false) => {
|
||||
let response
|
||||
if (body._id) {
|
||||
response = await API.updateDatasource(body)
|
||||
} else {
|
||||
response = await API.createDatasource({
|
||||
datasource: body,
|
||||
fetchSchema,
|
||||
})
|
||||
}
|
||||
return updateDatasource(response)
|
||||
},
|
||||
delete: async datasource => {
|
||||
await API.deleteDatasource({
|
||||
datasourceId: datasource?._id,
|
||||
datasourceRev: datasource?._rev,
|
||||
})
|
||||
update(state => {
|
||||
const sources = state.list.filter(
|
||||
existing => existing._id !== datasource._id
|
||||
)
|
||||
return { list: sources, selected: null }
|
||||
})
|
||||
await queries.fetch()
|
||||
await tables.fetch()
|
||||
},
|
||||
removeSchemaError: () => {
|
||||
update(state => {
|
||||
return { ...state, schemaError: null }
|
||||
})
|
||||
},
|
||||
subscribe: derivedStore.subscribe,
|
||||
fetch,
|
||||
init: fetch,
|
||||
select,
|
||||
updateSchema,
|
||||
save,
|
||||
delete: deleteDatasource,
|
||||
removeSchemaError,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
export { database } from "./database"
|
||||
export { tables } from "./tables"
|
||||
export { views } from "./views"
|
||||
export { rows } from "./rows"
|
||||
export { permissions } from "./permissions"
|
||||
export { roles } from "./roles"
|
||||
export { datasources } from "./datasources"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { datasources, integrations, tables, views } from "./"
|
||||
import { writable, get, derived } from "svelte/store"
|
||||
import { datasources, integrations } from "./"
|
||||
import { API } from "api"
|
||||
import { duplicateName } from "helpers/duplicate"
|
||||
|
||||
|
@ -10,125 +10,127 @@ const sortQueries = queryList => {
|
|||
}
|
||||
|
||||
export function createQueriesStore() {
|
||||
const store = writable({ list: [], selected: null })
|
||||
const { subscribe, set, update } = store
|
||||
const store = writable({
|
||||
list: [],
|
||||
selectedQueryId: null,
|
||||
})
|
||||
const derivedStore = derived(store, $store => ({
|
||||
...$store,
|
||||
selected: $store.list?.find(q => q._id === $store.selectedQueryId),
|
||||
}))
|
||||
|
||||
const actions = {
|
||||
init: async () => {
|
||||
const queries = await API.getQueries()
|
||||
set({
|
||||
list: queries,
|
||||
selected: null,
|
||||
})
|
||||
},
|
||||
fetch: async () => {
|
||||
const queries = await API.getQueries()
|
||||
const fetch = async () => {
|
||||
const queries = await API.getQueries()
|
||||
sortQueries(queries)
|
||||
store.update(state => ({
|
||||
...state,
|
||||
list: queries,
|
||||
}))
|
||||
}
|
||||
|
||||
const save = async (datasourceId, query) => {
|
||||
const _integrations = get(integrations)
|
||||
const dataSource = get(datasources).list.filter(
|
||||
ds => ds._id === datasourceId
|
||||
)
|
||||
// Check if readable attribute is found
|
||||
if (dataSource.length !== 0) {
|
||||
const integration = _integrations[dataSource[0].source]
|
||||
const readable = integration.query[query.queryVerb].readable
|
||||
if (readable) {
|
||||
query.readable = readable
|
||||
}
|
||||
}
|
||||
query.datasourceId = datasourceId
|
||||
const savedQuery = await API.saveQuery(query)
|
||||
store.update(state => {
|
||||
const idx = state.list.findIndex(query => query._id === savedQuery._id)
|
||||
const queries = state.list
|
||||
if (idx >= 0) {
|
||||
queries.splice(idx, 1, savedQuery)
|
||||
} else {
|
||||
queries.push(savedQuery)
|
||||
}
|
||||
sortQueries(queries)
|
||||
update(state => ({
|
||||
...state,
|
||||
return {
|
||||
list: queries,
|
||||
}))
|
||||
},
|
||||
save: async (datasourceId, query) => {
|
||||
const _integrations = get(integrations)
|
||||
const dataSource = get(datasources).list.filter(
|
||||
ds => ds._id === datasourceId
|
||||
)
|
||||
// Check if readable attribute is found
|
||||
if (dataSource.length !== 0) {
|
||||
const integration = _integrations[dataSource[0].source]
|
||||
const readable = integration.query[query.queryVerb].readable
|
||||
if (readable) {
|
||||
query.readable = readable
|
||||
}
|
||||
selectedQueryId: savedQuery._id,
|
||||
}
|
||||
query.datasourceId = datasourceId
|
||||
const savedQuery = await API.saveQuery(query)
|
||||
update(state => {
|
||||
const idx = state.list.findIndex(query => query._id === savedQuery._id)
|
||||
const queries = state.list
|
||||
if (idx >= 0) {
|
||||
queries.splice(idx, 1, savedQuery)
|
||||
} else {
|
||||
queries.push(savedQuery)
|
||||
}
|
||||
sortQueries(queries)
|
||||
return {
|
||||
list: queries,
|
||||
selected: savedQuery._id,
|
||||
}
|
||||
})
|
||||
return savedQuery
|
||||
},
|
||||
import: async ({ data, datasourceId }) => {
|
||||
return await API.importQueries({
|
||||
datasourceId,
|
||||
data,
|
||||
})
|
||||
},
|
||||
select: query => {
|
||||
update(state => ({ ...state, selected: query._id }))
|
||||
views.unselect()
|
||||
tables.unselect()
|
||||
datasources.unselect()
|
||||
},
|
||||
unselect: () => {
|
||||
update(state => ({ ...state, selected: null }))
|
||||
},
|
||||
preview: async query => {
|
||||
const parameters = query.parameters.reduce(
|
||||
(acc, next) => ({
|
||||
...acc,
|
||||
[next.name]: next.default,
|
||||
}),
|
||||
{}
|
||||
)
|
||||
const result = await API.previewQuery({
|
||||
...query,
|
||||
parameters,
|
||||
})
|
||||
// Assume all the fields are strings and create a basic schema from the
|
||||
// unique fields returned by the server
|
||||
const schema = {}
|
||||
for (let [field, type] of Object.entries(result.schemaFields)) {
|
||||
schema[field] = type || "string"
|
||||
}
|
||||
return { ...result, schema, rows: result.rows || [] }
|
||||
},
|
||||
delete: async query => {
|
||||
await API.deleteQuery({
|
||||
queryId: query?._id,
|
||||
queryRev: query?._rev,
|
||||
})
|
||||
update(state => {
|
||||
state.list = state.list.filter(existing => existing._id !== query._id)
|
||||
if (state.selected === query._id) {
|
||||
state.selected = null
|
||||
}
|
||||
return state
|
||||
})
|
||||
},
|
||||
duplicate: async query => {
|
||||
let list = get(store).list
|
||||
const newQuery = { ...query }
|
||||
const datasourceId = query.datasourceId
|
||||
})
|
||||
return savedQuery
|
||||
}
|
||||
|
||||
delete newQuery._id
|
||||
delete newQuery._rev
|
||||
newQuery.name = duplicateName(
|
||||
query.name,
|
||||
list.map(q => q.name)
|
||||
)
|
||||
const importQueries = async ({ data, datasourceId }) => {
|
||||
return await API.importQueries({
|
||||
datasourceId,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
return actions.save(datasourceId, newQuery)
|
||||
},
|
||||
const select = id => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
selectedQueryId: id,
|
||||
}))
|
||||
}
|
||||
|
||||
const preview = async query => {
|
||||
const parameters = query.parameters.reduce(
|
||||
(acc, next) => ({
|
||||
...acc,
|
||||
[next.name]: next.default,
|
||||
}),
|
||||
{}
|
||||
)
|
||||
const result = await API.previewQuery({
|
||||
...query,
|
||||
parameters,
|
||||
})
|
||||
// Assume all the fields are strings and create a basic schema from the
|
||||
// unique fields returned by the server
|
||||
const schema = {}
|
||||
for (let [field, type] of Object.entries(result.schemaFields)) {
|
||||
schema[field] = type || "string"
|
||||
}
|
||||
return { ...result, schema, rows: result.rows || [] }
|
||||
}
|
||||
|
||||
const deleteQuery = async query => {
|
||||
await API.deleteQuery({
|
||||
queryId: query?._id,
|
||||
queryRev: query?._rev,
|
||||
})
|
||||
store.update(state => {
|
||||
state.list = state.list.filter(existing => existing._id !== query._id)
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
const duplicate = async query => {
|
||||
let list = get(store).list
|
||||
const newQuery = { ...query }
|
||||
const datasourceId = query.datasourceId
|
||||
|
||||
delete newQuery._id
|
||||
delete newQuery._rev
|
||||
newQuery.name = duplicateName(
|
||||
query.name,
|
||||
list.map(q => q.name)
|
||||
)
|
||||
|
||||
return await save(datasourceId, newQuery)
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
update,
|
||||
...actions,
|
||||
subscribe: derivedStore.subscribe,
|
||||
fetch,
|
||||
init: fetch,
|
||||
select,
|
||||
save,
|
||||
import: importQueries,
|
||||
delete: deleteQuery,
|
||||
preview,
|
||||
duplicate,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { tables } from "./"
|
||||
|
||||
export function createRowsStore() {
|
||||
const { subscribe } = writable([])
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
save: () => tables.select(get(tables).selected),
|
||||
delete: () => tables.select(get(tables).selected),
|
||||
}
|
||||
}
|
||||
|
||||
export const rows = createRowsStore()
|
|
@ -1,41 +1,35 @@
|
|||
import { get, writable } from "svelte/store"
|
||||
import { datasources, queries, views } from "./"
|
||||
import { get, writable, derived } from "svelte/store"
|
||||
import { datasources } from "./"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { API } from "api"
|
||||
import { SWITCHABLE_TYPES } from "constants/backend"
|
||||
|
||||
export function createTablesStore() {
|
||||
const store = writable({})
|
||||
const { subscribe, update, set } = store
|
||||
const store = writable({
|
||||
list: [],
|
||||
selectedTableId: null,
|
||||
})
|
||||
const derivedStore = derived(store, $store => ({
|
||||
...$store,
|
||||
selected: $store.list?.find(table => table._id === $store.selectedTableId),
|
||||
}))
|
||||
|
||||
async function fetch() {
|
||||
const fetch = async () => {
|
||||
const tables = await API.getTables()
|
||||
update(state => ({
|
||||
store.update(state => ({
|
||||
...state,
|
||||
list: tables,
|
||||
}))
|
||||
return tables
|
||||
}
|
||||
|
||||
async function select(table) {
|
||||
if (!table) {
|
||||
update(state => ({
|
||||
...state,
|
||||
selected: {},
|
||||
}))
|
||||
} else {
|
||||
update(state => ({
|
||||
...state,
|
||||
selected: table,
|
||||
draft: cloneDeep(table),
|
||||
}))
|
||||
views.unselect()
|
||||
queries.unselect()
|
||||
datasources.unselect()
|
||||
}
|
||||
const select = tableId => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
selectedTableId: tableId,
|
||||
}))
|
||||
}
|
||||
|
||||
async function save(table) {
|
||||
const save = async table => {
|
||||
const updatedTable = cloneDeep(table)
|
||||
const oldTable = get(store).list.filter(t => t._id === table._id)[0]
|
||||
|
||||
|
@ -72,96 +66,72 @@ export function createTablesStore() {
|
|||
if (table.type === "external") {
|
||||
await datasources.fetch()
|
||||
}
|
||||
await select(savedTable)
|
||||
await select(savedTable._id)
|
||||
return savedTable
|
||||
}
|
||||
|
||||
const deleteTable = async table => {
|
||||
await API.deleteTable({
|
||||
tableId: table?._id,
|
||||
tableRev: table?._rev,
|
||||
})
|
||||
await fetch()
|
||||
}
|
||||
|
||||
const saveField = async ({
|
||||
originalName,
|
||||
field,
|
||||
primaryDisplay = false,
|
||||
indexes,
|
||||
}) => {
|
||||
let draft = cloneDeep(get(derivedStore).selected)
|
||||
|
||||
// delete the original if renaming
|
||||
// need to handle if the column had no name, empty string
|
||||
if (originalName != null && originalName !== field.name) {
|
||||
delete draft.schema[originalName]
|
||||
draft._rename = {
|
||||
old: originalName,
|
||||
updated: field.name,
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally set display column
|
||||
if (primaryDisplay) {
|
||||
draft.primaryDisplay = field.name
|
||||
} else if (draft.primaryDisplay === originalName) {
|
||||
const fields = Object.keys(draft.schema)
|
||||
// pick another display column randomly if unselecting
|
||||
draft.primaryDisplay = fields.filter(
|
||||
name => name !== originalName || name !== field
|
||||
)[0]
|
||||
}
|
||||
if (indexes) {
|
||||
draft.indexes = indexes
|
||||
}
|
||||
draft.schema = {
|
||||
...draft.schema,
|
||||
[field.name]: cloneDeep(field),
|
||||
}
|
||||
|
||||
await save(draft)
|
||||
}
|
||||
|
||||
const deleteField = async field => {
|
||||
let draft = cloneDeep(get(derivedStore).selected)
|
||||
delete draft.schema[field.name]
|
||||
await save(draft)
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
update,
|
||||
subscribe: derivedStore.subscribe,
|
||||
fetch,
|
||||
init: fetch,
|
||||
select,
|
||||
unselect: () => {
|
||||
update(state => ({
|
||||
...state,
|
||||
selected: null,
|
||||
}))
|
||||
},
|
||||
save,
|
||||
init: async () => {
|
||||
const tables = await API.getTables()
|
||||
set({
|
||||
list: tables,
|
||||
selected: {},
|
||||
draft: {},
|
||||
})
|
||||
},
|
||||
delete: async table => {
|
||||
await API.deleteTable({
|
||||
tableId: table?._id,
|
||||
tableRev: table?._rev,
|
||||
})
|
||||
update(state => ({
|
||||
...state,
|
||||
list: state.list.filter(existing => existing._id !== table._id),
|
||||
selected: {},
|
||||
}))
|
||||
},
|
||||
saveField: async ({
|
||||
originalName,
|
||||
field,
|
||||
primaryDisplay = false,
|
||||
indexes,
|
||||
}) => {
|
||||
let promise
|
||||
update(state => {
|
||||
// delete the original if renaming
|
||||
// need to handle if the column had no name, empty string
|
||||
if (originalName != null && originalName !== field.name) {
|
||||
delete state.draft.schema[originalName]
|
||||
state.draft._rename = {
|
||||
old: originalName,
|
||||
updated: field.name,
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally set display column
|
||||
if (primaryDisplay) {
|
||||
state.draft.primaryDisplay = field.name
|
||||
} else if (state.draft.primaryDisplay === originalName) {
|
||||
const fields = Object.keys(state.draft.schema)
|
||||
// pick another display column randomly if unselecting
|
||||
state.draft.primaryDisplay = fields.filter(
|
||||
name => name !== originalName || name !== field
|
||||
)[0]
|
||||
}
|
||||
|
||||
if (indexes) {
|
||||
state.draft.indexes = indexes
|
||||
}
|
||||
|
||||
state.draft.schema = {
|
||||
...state.draft.schema,
|
||||
[field.name]: cloneDeep(field),
|
||||
}
|
||||
promise = save(state.draft)
|
||||
return state
|
||||
})
|
||||
if (promise) {
|
||||
await promise
|
||||
}
|
||||
},
|
||||
deleteField: async field => {
|
||||
let promise
|
||||
update(state => {
|
||||
delete state.draft.schema[field.name]
|
||||
promise = save(state.draft)
|
||||
return state
|
||||
})
|
||||
if (promise) {
|
||||
await promise
|
||||
}
|
||||
},
|
||||
delete: deleteTable,
|
||||
saveField,
|
||||
deleteField,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,52 +1,54 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { tables, datasources, queries } from "./"
|
||||
import { writable, get, derived } from "svelte/store"
|
||||
import { tables } from "./"
|
||||
import { API } from "api"
|
||||
|
||||
export function createViewsStore() {
|
||||
const { subscribe, update } = writable({
|
||||
list: [],
|
||||
selected: null,
|
||||
const store = writable({
|
||||
selectedViewName: null,
|
||||
})
|
||||
const derivedStore = derived([store, tables], ([$store, $tables]) => {
|
||||
let list = []
|
||||
$tables.list?.forEach(table => {
|
||||
list = list.concat(Object.values(table?.views || {}))
|
||||
})
|
||||
return {
|
||||
...$store,
|
||||
list,
|
||||
selected: list.find(view => view.name === $store.selectedViewName),
|
||||
}
|
||||
})
|
||||
|
||||
const select = name => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
selectedViewName: name,
|
||||
}))
|
||||
}
|
||||
|
||||
const deleteView = async view => {
|
||||
await API.deleteView(view)
|
||||
await tables.fetch()
|
||||
}
|
||||
|
||||
const save = async view => {
|
||||
const savedView = await API.saveView(view)
|
||||
const viewMeta = {
|
||||
name: view.name,
|
||||
...savedView,
|
||||
}
|
||||
|
||||
const viewTable = get(tables).list.find(table => table._id === view.tableId)
|
||||
|
||||
if (view.originalName) delete viewTable.views[view.originalName]
|
||||
viewTable.views[view.name] = viewMeta
|
||||
await tables.save(viewTable)
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
update,
|
||||
select: view => {
|
||||
update(state => ({
|
||||
...state,
|
||||
selected: view,
|
||||
}))
|
||||
tables.unselect()
|
||||
queries.unselect()
|
||||
datasources.unselect()
|
||||
},
|
||||
unselect: () => {
|
||||
update(state => ({
|
||||
...state,
|
||||
selected: null,
|
||||
}))
|
||||
},
|
||||
delete: async view => {
|
||||
await API.deleteView(view)
|
||||
await tables.fetch()
|
||||
},
|
||||
save: async view => {
|
||||
const savedView = await API.saveView(view)
|
||||
const viewMeta = {
|
||||
name: view.name,
|
||||
...savedView,
|
||||
}
|
||||
|
||||
const viewTable = get(tables).list.find(
|
||||
table => table._id === view.tableId
|
||||
)
|
||||
|
||||
if (view.originalName) delete viewTable.views[view.originalName]
|
||||
viewTable.views[view.name] = viewMeta
|
||||
await tables.save(viewTable)
|
||||
|
||||
update(state => ({ ...state, selected: viewMeta }))
|
||||
},
|
||||
subscribe: derivedStore.subscribe,
|
||||
select,
|
||||
delete: deleteView,
|
||||
save,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "2.2.4-alpha.2",
|
||||
"version": "2.2.4-alpha.7",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
@ -26,9 +26,9 @@
|
|||
"outputPath": "build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/backend-core": "2.2.4-alpha.2",
|
||||
"@budibase/string-templates": "2.2.4-alpha.2",
|
||||
"@budibase/types": "2.2.4-alpha.2",
|
||||
"@budibase/backend-core": "2.2.4-alpha.7",
|
||||
"@budibase/string-templates": "2.2.4-alpha.7",
|
||||
"@budibase/types": "2.2.4-alpha.7",
|
||||
"axios": "0.21.2",
|
||||
"chalk": "4.1.0",
|
||||
"cli-progress": "3.11.2",
|
||||
|
@ -54,4 +54,4 @@
|
|||
"eslint": "^7.20.0",
|
||||
"renamer": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "2.2.4-alpha.2",
|
||||
"version": "2.2.4-alpha.7",
|
||||
"license": "MPL-2.0",
|
||||
"module": "dist/budibase-client.js",
|
||||
"main": "dist/budibase-client.js",
|
||||
|
@ -19,9 +19,9 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "2.2.4-alpha.2",
|
||||
"@budibase/frontend-core": "2.2.4-alpha.2",
|
||||
"@budibase/string-templates": "2.2.4-alpha.2",
|
||||
"@budibase/bbui": "2.2.4-alpha.7",
|
||||
"@budibase/frontend-core": "2.2.4-alpha.7",
|
||||
"@budibase/string-templates": "2.2.4-alpha.7",
|
||||
"@spectrum-css/button": "^3.0.3",
|
||||
"@spectrum-css/card": "^3.0.3",
|
||||
"@spectrum-css/divider": "^1.0.3",
|
||||
|
@ -63,4 +63,4 @@
|
|||
"loader-utils": "1.4.1"
|
||||
},
|
||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
||||
}
|
||||
}
|
|
@ -327,6 +327,8 @@ const showNotificationHandler = action => {
|
|||
notificationStore.actions[type]?.(message, autoDismiss)
|
||||
}
|
||||
|
||||
const promptUserHandler = () => {}
|
||||
|
||||
const OpenSidePanelHandler = action => {
|
||||
const { id } = action.parameters
|
||||
if (id) {
|
||||
|
@ -357,6 +359,7 @@ const handlerMap = {
|
|||
["Export Data"]: exportDataHandler,
|
||||
["Continue if / Stop if"]: continueIfHandler,
|
||||
["Show Notification"]: showNotificationHandler,
|
||||
["Prompt User"]: promptUserHandler,
|
||||
["Open Side Panel"]: OpenSidePanelHandler,
|
||||
["Close Side Panel"]: CloseSidePanelHandler,
|
||||
}
|
||||
|
@ -366,6 +369,7 @@ const confirmTextMap = {
|
|||
["Save Row"]: "Are you sure you want to save this row?",
|
||||
["Execute Query"]: "Are you sure you want to execute this query?",
|
||||
["Trigger Automation"]: "Are you sure you want to trigger this automation?",
|
||||
["Prompt User"]: "Are you sure you want to contiune?",
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -417,8 +421,12 @@ export const enrichButtonActions = (actions, context) => {
|
|||
return new Promise(resolve => {
|
||||
const defaultText = confirmTextMap[action["##eventHandlerType"]]
|
||||
const confirmText = action.parameters?.confirmText || defaultText
|
||||
|
||||
const defaultTitleText = action["##eventHandlerType"]
|
||||
const customTitleText =
|
||||
action.parameters?.customTitleText || defaultTitleText
|
||||
confirmationStore.actions.showConfirmation(
|
||||
action["##eventHandlerType"],
|
||||
customTitleText,
|
||||
confirmText,
|
||||
async () => {
|
||||
// When confirmed, execute this action immediately,
|
||||
|
@ -429,7 +437,7 @@ export const enrichButtonActions = (actions, context) => {
|
|||
buttonContext.push(result)
|
||||
const newContext = { ...context, actions: buttonContext }
|
||||
|
||||
// Enrich and call the next button action
|
||||
// Enrich and call the next button action if there is more than one action remaining
|
||||
const next = enrichButtonActions(
|
||||
actions.slice(i + 1),
|
||||
newContext
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"name": "@budibase/frontend-core",
|
||||
"version": "2.2.4-alpha.2",
|
||||
"version": "2.2.4-alpha.7",
|
||||
"description": "Budibase frontend core libraries used in builder and client",
|
||||
"author": "Budibase",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "2.2.4-alpha.2",
|
||||
"@budibase/bbui": "2.2.4-alpha.7",
|
||||
"lodash": "^4.17.21",
|
||||
"svelte": "^3.46.2"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,11 +22,11 @@ export const buildAppEndpoints = API => ({
|
|||
},
|
||||
|
||||
/**
|
||||
* Deploys the current app.
|
||||
* Publishes the current app.
|
||||
*/
|
||||
deployAppChanges: async () => {
|
||||
publishAppChanges: async appId => {
|
||||
return await API.post({
|
||||
url: "/api/deploy",
|
||||
url: `/api/applications/${appId}/publish`,
|
||||
})
|
||||
},
|
||||
|
||||
|
@ -98,8 +98,8 @@ export const buildAppEndpoints = API => ({
|
|||
* @param appId the production ID of the app to unpublish
|
||||
*/
|
||||
unpublishApp: async appId => {
|
||||
return await API.delete({
|
||||
url: `/api/applications/${appId}?unpublish=1`,
|
||||
return await API.post({
|
||||
url: `/api/applications/${appId}/unpublish`,
|
||||
})
|
||||
},
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/sdk",
|
||||
"version": "2.2.4-alpha.2",
|
||||
"version": "2.2.4-alpha.7",
|
||||
"description": "Budibase Public API SDK",
|
||||
"author": "Budibase",
|
||||
"license": "MPL-2.0",
|
||||
|
@ -20,4 +20,4 @@
|
|||
"rollup-plugin-polyfill-node": "^0.8.0",
|
||||
"rollup-plugin-terser": "^7.0.2"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "2.2.4-alpha.2",
|
||||
"version": "2.2.4-alpha.7",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/index.ts",
|
||||
"repository": {
|
||||
|
@ -43,11 +43,11 @@
|
|||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "10.0.3",
|
||||
"@budibase/backend-core": "2.2.4-alpha.2",
|
||||
"@budibase/client": "2.2.4-alpha.2",
|
||||
"@budibase/pro": "2.2.4-alpha.2",
|
||||
"@budibase/string-templates": "2.2.4-alpha.2",
|
||||
"@budibase/types": "2.2.4-alpha.2",
|
||||
"@budibase/backend-core": "2.2.4-alpha.7",
|
||||
"@budibase/client": "2.2.4-alpha.7",
|
||||
"@budibase/pro": "2.2.4-alpha.7",
|
||||
"@budibase/string-templates": "2.2.4-alpha.7",
|
||||
"@budibase/types": "2.2.4-alpha.7",
|
||||
"@bull-board/api": "3.7.0",
|
||||
"@bull-board/koa": "3.9.4",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
|
@ -168,4 +168,4 @@
|
|||
"oracledb": "5.3.0"
|
||||
},
|
||||
"gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc"
|
||||
}
|
||||
}
|
|
@ -567,6 +567,40 @@
|
|||
"data"
|
||||
]
|
||||
},
|
||||
"deploymentOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_id": {
|
||||
"description": "The ID of the app.",
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"description": "Status of the deployment, whether it succeeded or failed",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"SUCCESS",
|
||||
"FAILURE"
|
||||
]
|
||||
},
|
||||
"appUrl": {
|
||||
"description": "The URL of the published app",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"_id",
|
||||
"status",
|
||||
"appUrl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"row": {
|
||||
"description": "The row to be created/updated, based on the table schema.",
|
||||
"type": "object",
|
||||
|
@ -1933,6 +1967,56 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/applications/{appId}/unpublish": {
|
||||
"post": {
|
||||
"operationId": "unpublish",
|
||||
"summary": "Unpublish an application",
|
||||
"tags": [
|
||||
"applications"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/appIdUrl"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "The app was published successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/applications/{appId}/publish": {
|
||||
"post": {
|
||||
"operationId": "publish",
|
||||
"summary": "Unpublish an application",
|
||||
"tags": [
|
||||
"applications"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/appIdUrl"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns the deployment object.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/deploymentOutput"
|
||||
},
|
||||
"examples": {
|
||||
"deployment": {
|
||||
"$ref": "#/components/examples/deploymentOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/applications/search": {
|
||||
"post": {
|
||||
"operationId": "search",
|
||||
|
|
|
@ -411,6 +411,30 @@ components:
|
|||
- version
|
||||
required:
|
||||
- data
|
||||
deploymentOutput:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
_id:
|
||||
description: The ID of the app.
|
||||
type: string
|
||||
status:
|
||||
description: Status of the deployment, whether it succeeded or failed
|
||||
type: string
|
||||
enum:
|
||||
- SUCCESS
|
||||
- FAILURE
|
||||
appUrl:
|
||||
description: The URL of the published app
|
||||
type: string
|
||||
required:
|
||||
- _id
|
||||
- status
|
||||
- appUrl
|
||||
required:
|
||||
- data
|
||||
row:
|
||||
description: The row to be created/updated, based on the table schema.
|
||||
type: object
|
||||
|
@ -1453,6 +1477,35 @@ paths:
|
|||
examples:
|
||||
application:
|
||||
$ref: "#/components/examples/application"
|
||||
"/applications/{appId}/unpublish":
|
||||
post:
|
||||
operationId: unpublish
|
||||
summary: Unpublish an application
|
||||
tags:
|
||||
- applications
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/appIdUrl"
|
||||
responses:
|
||||
"204":
|
||||
description: The app was published successfully.
|
||||
"/applications/{appId}/publish":
|
||||
post:
|
||||
operationId: publish
|
||||
summary: Unpublish an application
|
||||
tags:
|
||||
- applications
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/appIdUrl"
|
||||
responses:
|
||||
"200":
|
||||
description: Returns the deployment object.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/deploymentOutput"
|
||||
examples:
|
||||
deployment:
|
||||
$ref: "#/components/examples/deploymentOutput"
|
||||
/applications/search:
|
||||
post:
|
||||
operationId: search
|
||||
|
|
|
@ -80,6 +80,22 @@ const applicationOutputSchema = object(
|
|||
}
|
||||
)
|
||||
|
||||
const deploymentOutputSchema = object({
|
||||
_id: {
|
||||
description: "The ID of the app.",
|
||||
type: "string",
|
||||
},
|
||||
status: {
|
||||
description: "Status of the deployment, whether it succeeded or failed",
|
||||
type: "string",
|
||||
enum: ["SUCCESS", "FAILURE"],
|
||||
},
|
||||
appUrl: {
|
||||
description: "The URL of the published app",
|
||||
type: "string",
|
||||
},
|
||||
})
|
||||
|
||||
module.exports = new Resource()
|
||||
.setExamples({
|
||||
application: {
|
||||
|
@ -104,4 +120,7 @@ module.exports = new Resource()
|
|||
items: applicationOutputSchema,
|
||||
},
|
||||
}),
|
||||
deploymentOutput: object({
|
||||
data: deploymentOutputSchema,
|
||||
}),
|
||||
})
|
||||
|
|
|
@ -50,6 +50,7 @@ import {
|
|||
} from "@budibase/types"
|
||||
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
||||
import sdk from "../../sdk"
|
||||
import { getDB } from "@budibase/backend-core/src/db"
|
||||
|
||||
// utility function, need to do away with this
|
||||
async function getLayouts() {
|
||||
|
@ -464,41 +465,47 @@ export async function revertClient(ctx: BBContext) {
|
|||
ctx.body = app
|
||||
}
|
||||
|
||||
async function destroyApp(ctx: BBContext) {
|
||||
const unpublishApp = async (ctx: any) => {
|
||||
let appId = ctx.params.appId
|
||||
let isUnpublish = ctx.query && ctx.query.unpublish
|
||||
appId = dbCore.getProdAppID(appId)
|
||||
|
||||
if (isUnpublish) {
|
||||
appId = dbCore.getProdAppID(appId)
|
||||
const devAppId = dbCore.getDevAppID(appId)
|
||||
// sync before removing the published app
|
||||
await sdk.applications.syncApp(devAppId)
|
||||
}
|
||||
|
||||
const db = isUnpublish ? context.getProdAppDB() : context.getAppDB()
|
||||
const app = await db.get(DocumentType.APP_METADATA)
|
||||
const db = context.getProdAppDB()
|
||||
const result = await db.destroy()
|
||||
|
||||
if (isUnpublish) {
|
||||
await events.app.unpublished(app)
|
||||
} else {
|
||||
await quotas.removeApp()
|
||||
await events.app.deleted(app)
|
||||
await events.app.unpublished({ appId } as App)
|
||||
|
||||
// automations only in production
|
||||
await cleanupAutomations(appId)
|
||||
|
||||
await cache.app.invalidateAppMetadata(appId)
|
||||
return result
|
||||
}
|
||||
|
||||
async function destroyApp(ctx: BBContext) {
|
||||
let appId = ctx.params.appId
|
||||
appId = dbCore.getProdAppID(appId)
|
||||
const devAppId = dbCore.getDevAppID(appId)
|
||||
|
||||
// check if we need to unpublish first
|
||||
if (await dbCore.dbExists(appId)) {
|
||||
// app is deployed, run through unpublish flow
|
||||
await sdk.applications.syncApp(devAppId)
|
||||
await unpublishApp(ctx)
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (!env.isTest() && !isUnpublish) {
|
||||
const db = dbCore.getDB(devAppId)
|
||||
// standard app deletion flow
|
||||
const app = await db.get(DocumentType.APP_METADATA)
|
||||
const result = await db.destroy()
|
||||
await quotas.removeApp()
|
||||
await events.app.deleted(app)
|
||||
|
||||
if (!env.isTest()) {
|
||||
await deleteApp(appId)
|
||||
}
|
||||
// automations only in production
|
||||
if (isUnpublish) {
|
||||
await cleanupAutomations(appId)
|
||||
}
|
||||
// remove app role when the dev app is deleted (no trace of app anymore)
|
||||
else {
|
||||
await removeAppFromUserRoles(ctx, appId)
|
||||
}
|
||||
await cache.app.invalidateAppMetadata(appId)
|
||||
|
||||
await removeAppFromUserRoles(ctx, appId)
|
||||
await cache.app.invalidateAppMetadata(devAppId)
|
||||
return result
|
||||
}
|
||||
|
||||
|
@ -523,6 +530,21 @@ export async function destroy(ctx: BBContext) {
|
|||
ctx.body = result
|
||||
}
|
||||
|
||||
export const unpublish = async (ctx: BBContext) => {
|
||||
const prodAppId = dbCore.getProdAppID(ctx.params.appId)
|
||||
const dbExists = await dbCore.dbExists(prodAppId)
|
||||
|
||||
// check app has been published
|
||||
if (!dbExists) {
|
||||
return ctx.throw(400, "App has not been published.")
|
||||
}
|
||||
|
||||
await preDestroyApp(ctx)
|
||||
await unpublishApp(ctx)
|
||||
await postDestroyApp(ctx)
|
||||
ctx.status = 204
|
||||
}
|
||||
|
||||
export async function sync(ctx: BBContext) {
|
||||
const appId = ctx.params.appId
|
||||
try {
|
||||
|
|
|
@ -82,7 +82,7 @@ export async function importApps(ctx: Ctx) {
|
|||
"Import file is required and environment must be fresh to import apps."
|
||||
)
|
||||
}
|
||||
const file = ctx.request.files.importFile
|
||||
const file = ctx.request.files.importFile as any
|
||||
if (Array.isArray(file)) {
|
||||
ctx.throw(400, "Single file is required")
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ export default class Deployment {
|
|||
verification: any
|
||||
status?: string
|
||||
err?: any
|
||||
appUrl?: string
|
||||
|
||||
constructor(id = null) {
|
||||
this._id = id || newid()
|
||||
|
|
|
@ -94,7 +94,44 @@ async function initDeployedApp(prodAppId: any) {
|
|||
})
|
||||
}
|
||||
|
||||
async function deployApp(deployment: any, userId: string) {
|
||||
export async function fetchDeployments(ctx: any) {
|
||||
try {
|
||||
const db = context.getAppDB()
|
||||
const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS)
|
||||
const { updated, deployments } = await checkAllDeployments(deploymentDoc)
|
||||
if (updated) {
|
||||
await db.put(deployments)
|
||||
}
|
||||
ctx.body = Object.values(deployments.history).reverse()
|
||||
} catch (err) {
|
||||
ctx.body = []
|
||||
}
|
||||
}
|
||||
|
||||
export async function deploymentProgress(ctx: any) {
|
||||
try {
|
||||
const db = context.getAppDB()
|
||||
const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS)
|
||||
ctx.body = deploymentDoc[ctx.params.deploymentId]
|
||||
} catch (err) {
|
||||
ctx.throw(
|
||||
500,
|
||||
`Error fetching data for deployment ${ctx.params.deploymentId}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const publishApp = async function (ctx: any) {
|
||||
let deployment = new Deployment()
|
||||
console.log("Deployment object created")
|
||||
deployment.setStatus(DeploymentStatus.PENDING)
|
||||
console.log("Deployment object set to pending")
|
||||
deployment = await storeDeploymentHistory(deployment)
|
||||
console.log("Stored deployment history")
|
||||
|
||||
console.log("Deploying app...")
|
||||
|
||||
let app
|
||||
let replication
|
||||
try {
|
||||
const appId = context.getAppId()!
|
||||
|
@ -108,7 +145,7 @@ async function deployApp(deployment: any, userId: string) {
|
|||
productionAppId,
|
||||
AppBackupTrigger.PUBLISH,
|
||||
{
|
||||
createdBy: userId,
|
||||
createdBy: ctx.user._id,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -147,7 +184,7 @@ async function deployApp(deployment: any, userId: string) {
|
|||
console.log("Deployed app initialised, setting deployment to successful")
|
||||
deployment.setStatus(DeploymentStatus.SUCCESS)
|
||||
await storeDeploymentHistory(deployment)
|
||||
return appDoc
|
||||
app = appDoc
|
||||
} catch (err: any) {
|
||||
deployment.setStatus(DeploymentStatus.FAILURE, err.message)
|
||||
await storeDeploymentHistory(deployment)
|
||||
|
@ -160,62 +197,7 @@ async function deployApp(deployment: any, userId: string) {
|
|||
await replication.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchDeployments(ctx: any) {
|
||||
try {
|
||||
const db = context.getAppDB()
|
||||
const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS)
|
||||
const { updated, deployments } = await checkAllDeployments(deploymentDoc)
|
||||
if (updated) {
|
||||
await db.put(deployments)
|
||||
}
|
||||
ctx.body = Object.values(deployments.history).reverse()
|
||||
} catch (err) {
|
||||
ctx.body = []
|
||||
}
|
||||
}
|
||||
|
||||
export async function deploymentProgress(ctx: any) {
|
||||
try {
|
||||
const db = context.getAppDB()
|
||||
const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS)
|
||||
ctx.body = deploymentDoc[ctx.params.deploymentId]
|
||||
} catch (err) {
|
||||
ctx.throw(
|
||||
500,
|
||||
`Error fetching data for deployment ${ctx.params.deploymentId}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const isFirstDeploy = async () => {
|
||||
try {
|
||||
const db = context.getProdAppDB()
|
||||
await db.get(DocumentType.APP_METADATA)
|
||||
} catch (e: any) {
|
||||
if (e.status === 404) {
|
||||
return true
|
||||
}
|
||||
throw e
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const _deployApp = async function (ctx: any) {
|
||||
let deployment = new Deployment()
|
||||
console.log("Deployment object created")
|
||||
deployment.setStatus(DeploymentStatus.PENDING)
|
||||
console.log("Deployment object set to pending")
|
||||
deployment = await storeDeploymentHistory(deployment)
|
||||
console.log("Stored deployment history")
|
||||
|
||||
console.log("Deploying app...")
|
||||
|
||||
let app = await deployApp(deployment, ctx.user._id)
|
||||
|
||||
await events.app.published(app)
|
||||
ctx.body = deployment
|
||||
}
|
||||
|
||||
export { _deployApp as deployApp }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { db as dbCore, context } from "@budibase/backend-core"
|
||||
import { search as stringSearch, addRev } from "./utils"
|
||||
import * as controller from "../application"
|
||||
import * as deployController from "../deploy"
|
||||
import { Application } from "../../../definitions/common"
|
||||
|
||||
function fixAppID(app: Application, params: any) {
|
||||
|
@ -74,10 +75,26 @@ export async function destroy(ctx: any, next: any) {
|
|||
})
|
||||
}
|
||||
|
||||
export async function unpublish(ctx: any, next: any) {
|
||||
await context.doInAppContext(ctx.params.appId, async () => {
|
||||
await controller.unpublish(ctx)
|
||||
await next()
|
||||
})
|
||||
}
|
||||
|
||||
export async function publish(ctx: any, next: any) {
|
||||
await context.doInAppContext(ctx.params.appId, async () => {
|
||||
await deployController.publishApp(ctx)
|
||||
await next()
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
create,
|
||||
update,
|
||||
read,
|
||||
destroy,
|
||||
search,
|
||||
publish,
|
||||
unpublish,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Router from "@koa/router"
|
||||
import * as controller from "../controllers/application"
|
||||
import * as deploymentController from "../controllers/deploy"
|
||||
import authorized from "../../middleware/authorized"
|
||||
import { permissions } from "@budibase/backend-core"
|
||||
import { applicationValidator } from "./utils/validators"
|
||||
|
@ -37,6 +38,16 @@ router
|
|||
authorized(permissions.BUILDER),
|
||||
controller.revertClient
|
||||
)
|
||||
.post(
|
||||
"/api/applications/:appId/publish",
|
||||
authorized(permissions.BUILDER),
|
||||
deploymentController.publishApp
|
||||
)
|
||||
.post(
|
||||
"/api/applications/:appId/unpublish",
|
||||
authorized(permissions.BUILDER),
|
||||
controller.unpublish
|
||||
)
|
||||
.delete(
|
||||
"/api/applications/:appId",
|
||||
authorized(permissions.BUILDER),
|
||||
|
|
|
@ -16,6 +16,5 @@ router
|
|||
authorized(permissions.BUILDER),
|
||||
controller.deploymentProgress
|
||||
)
|
||||
.post("/api/deploy", authorized(permissions.BUILDER), controller.deployApp)
|
||||
|
||||
export = router
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import controller from "../../controllers/public/applications"
|
||||
import Endpoint from "./utils/Endpoint"
|
||||
const { nameValidator, applicationValidator } = require("../utils/validators")
|
||||
import { db } from "@budibase/backend-core"
|
||||
|
||||
const read = [],
|
||||
write = []
|
||||
|
@ -94,6 +95,49 @@ write.push(
|
|||
*/
|
||||
write.push(new Endpoint("delete", "/applications/:appId", controller.destroy))
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /applications/{appId}/unpublish:
|
||||
* post:
|
||||
* operationId: unpublish
|
||||
* summary: Unpublish an application
|
||||
* tags:
|
||||
* - applications
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/appIdUrl'
|
||||
* responses:
|
||||
* 204:
|
||||
* description: The app was published successfully.
|
||||
*/
|
||||
write.push(
|
||||
new Endpoint("post", "/applications/:appId/unpublish", controller.unpublish)
|
||||
)
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /applications/{appId}/publish:
|
||||
* post:
|
||||
* operationId: publish
|
||||
* summary: Unpublish an application
|
||||
* tags:
|
||||
* - applications
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/appIdUrl'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Returns the deployment object.
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/deploymentOutput'
|
||||
* examples:
|
||||
* deployment:
|
||||
* $ref: '#/components/examples/deploymentOutput'
|
||||
*/
|
||||
write.push(
|
||||
new Endpoint("post", "/applications/:appId/publish", controller.publish)
|
||||
)
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /applications/{appId}:
|
||||
|
|
|
@ -54,9 +54,13 @@ function processQueries(ctx: any) {
|
|||
}
|
||||
|
||||
export default async (ctx: any, next: any) => {
|
||||
if (!ctx.body) {
|
||||
return await next()
|
||||
}
|
||||
let urlParts = ctx.url.split("/")
|
||||
urlParts = urlParts.slice(4, urlParts.length)
|
||||
let body = {}
|
||||
|
||||
switch (urlParts[0]) {
|
||||
case Resources.APPLICATION:
|
||||
body = processApplications(ctx)
|
||||
|
|
|
@ -11,7 +11,6 @@ jest.mock("../../../utilities/redis", () => ({
|
|||
checkDebounce: jest.fn(),
|
||||
shutdown: jest.fn(),
|
||||
}))
|
||||
|
||||
import { clearAllApps, checkBuilderEndpoint } from "./utilities/TestFunctions"
|
||||
import * as setup from "./utilities"
|
||||
import { AppStatus } from "../../../db/utils"
|
||||
|
@ -160,33 +159,30 @@ describe("/applications", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("delete", () => {
|
||||
it("should delete app", async () => {
|
||||
await config.createApp("to-delete")
|
||||
describe("publish", () => {
|
||||
it("should publish app with dev app ID", async () => {
|
||||
const appId = config.getAppId()
|
||||
await request
|
||||
.delete(`/api/applications/${appId}`)
|
||||
.post(`/api/applications/${appId}/publish`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(events.app.deleted).toBeCalledTimes(1)
|
||||
expect(events.app.published).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should unpublish app", async () => {
|
||||
await config.createApp("to-unpublish")
|
||||
it("should publish app with prod app ID", async () => {
|
||||
const appId = config.getProdAppId()
|
||||
await request
|
||||
.delete(`/api/applications/${appId}?unpublish=true`)
|
||||
.post(`/api/applications/${appId}/publish`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(events.app.unpublished).toBeCalledTimes(1)
|
||||
expect(events.app.published).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("manage client library version", () => {
|
||||
it("should be able to update the app client library version", async () => {
|
||||
console.log(config.getAppId())
|
||||
await request
|
||||
.post(`/api/applications/${config.getAppId()}/client/update`)
|
||||
.set(config.defaultHeaders())
|
||||
|
@ -194,6 +190,7 @@ describe("/applications", () => {
|
|||
.expect(200)
|
||||
expect(events.app.versionUpdated).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should be able to revert the app client library version", async () => {
|
||||
// We need to first update the version so that we can then revert
|
||||
await request
|
||||
|
@ -267,4 +264,50 @@ describe("/applications", () => {
|
|||
env._set("DISABLE_AUTO_PROD_APP_SYNC", false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("unpublish", () => {
|
||||
it("should unpublish app with dev app ID", async () => {
|
||||
const appId = config.getAppId()
|
||||
await request
|
||||
.post(`/api/applications/${appId}/unpublish`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect(204)
|
||||
expect(events.app.unpublished).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should unpublish app with prod app ID", async () => {
|
||||
const appId = config.getProdAppId()
|
||||
await request
|
||||
.post(`/api/applications/${appId}/unpublish`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect(204)
|
||||
expect(events.app.unpublished).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete", () => {
|
||||
it("should delete published app and dev apps with dev app ID", async () => {
|
||||
await config.createApp("to-delete")
|
||||
const appId = config.getAppId()
|
||||
await request
|
||||
.delete(`/api/applications/${appId}`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(events.app.deleted).toBeCalledTimes(1)
|
||||
expect(events.app.unpublished).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should delete published app and dev app with prod app ID", async () => {
|
||||
await config.createApp("to-delete")
|
||||
const appId = config.getProdAppId()
|
||||
await request
|
||||
.delete(`/api/applications/${appId}`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(events.app.deleted).toBeCalledTimes(1)
|
||||
expect(events.app.unpublished).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -23,14 +23,13 @@ describe("/cloud", () => {
|
|||
// first we need to delete any existing apps on the system so it looks clean otherwise the
|
||||
// import will not run
|
||||
await request
|
||||
.delete(
|
||||
.post(
|
||||
`/api/applications/${dbCore.getProdAppID(
|
||||
config.getAppId()
|
||||
)}?unpublish=true`
|
||||
)}/unpublish`
|
||||
)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
.expect(204)
|
||||
await request
|
||||
.delete(`/api/applications/${config.getAppId()}`)
|
||||
.set(config.defaultHeaders())
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import * as setup from "./utilities"
|
||||
import { events } from "@budibase/backend-core"
|
||||
|
||||
describe("/deployments", () => {
|
||||
let request = setup.getRequest()
|
||||
let config = setup.getConfig()
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
|
||||
beforeEach(async () => {
|
||||
await config.init()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("deploy", () => {
|
||||
it("should deploy the application", async () => {
|
||||
await request
|
||||
.post(`/api/deploy`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect((events.app.published as jest.Mock).mock.calls.length).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -92,7 +92,7 @@ describe("/permission", () => {
|
|||
describe("check public user allowed", () => {
|
||||
it("should be able to read the row", async () => {
|
||||
// replicate changes before checking permissions
|
||||
await config.deploy()
|
||||
await config.publish()
|
||||
|
||||
const res = await request
|
||||
.get(`/api/${table._id}/rows`)
|
||||
|
|
|
@ -25,7 +25,7 @@ describe("/routing", () => {
|
|||
screen2.routing.roleId = BUILTIN_ROLE_IDS.POWER
|
||||
screen2.routing.route = route
|
||||
screen2 = await config.createScreen(screen2)
|
||||
await config.deploy()
|
||||
await config.publish()
|
||||
})
|
||||
|
||||
describe("fetch", () => {
|
||||
|
|
|
@ -113,7 +113,7 @@ describe("/webhooks", () => {
|
|||
describe("trigger", () => {
|
||||
it("should allow triggering from public", async () => {
|
||||
// replicate changes before checking webhook
|
||||
await config.deploy()
|
||||
await config.publish()
|
||||
|
||||
const res = await request
|
||||
.post(`/api/webhooks/trigger/${config.prodAppId}/${webhook._id}`)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue