Merge branch 'develop' of github.com:Budibase/budibase into cheeks-fixes
This commit is contained in:
commit
8c9d4a9126
|
@ -10,4 +10,4 @@ packages/builder/.routify
|
|||
packages/builder/cypress/support/queryLevelTransformerFunction.js
|
||||
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
||||
packages/builder/cypress/reports
|
||||
packages/sdk/sdk
|
||||
packages/sdk/sdk
|
||||
|
|
|
@ -16,8 +16,7 @@
|
|||
"dist",
|
||||
"public",
|
||||
"*.spec.js",
|
||||
"bundle.js",
|
||||
"packages/pro"
|
||||
"bundle.js"
|
||||
],
|
||||
"plugins": ["svelte3"],
|
||||
"extends": ["eslint:recommended"],
|
||||
|
@ -30,9 +29,7 @@
|
|||
"files": ["**/*.ts"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [],
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"extends": ["eslint:recommended"],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"no-inner-declarations": "off",
|
||||
|
|
|
@ -32,10 +32,11 @@ jobs:
|
|||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
- run: yarn
|
||||
- run: cd scripts && yarn
|
||||
- name: Tag prerelease
|
||||
run: |
|
||||
cd scripts
|
||||
# setup the username and email.
|
||||
git config --global user.name "Budibase Staging Release Bot"
|
||||
git config --global user.email "<>"
|
||||
./scripts/versionCommit.sh prerelease
|
||||
./versionCommit.sh prerelease
|
||||
|
|
|
@ -42,12 +42,13 @@ jobs:
|
|||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
- run: yarn
|
||||
- run: cd scripts && yarn
|
||||
- name: Tag release
|
||||
run: |
|
||||
cd scripts
|
||||
# setup the username and email.
|
||||
git config --global user.name "Budibase Staging Release Bot"
|
||||
git config --global user.email "<>"
|
||||
BUMP_TYPE_INPUT=${{ github.event.inputs.versioning }}
|
||||
BUMP_TYPE=${BUMP_TYPE_INPUT:-"patch"}
|
||||
./scripts/versionCommit.sh $BUMP_TYPE
|
||||
./versionCommit.sh $BUMP_TYPE
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
{
|
||||
"version": "2.7.34-alpha.9",
|
||||
"version": "2.7.35",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"packages": ["packages/*"],
|
||||
"useNx": true,
|
||||
"command": {
|
||||
"publish": {
|
||||
|
@ -19,4 +17,4 @@
|
|||
"loadEnvFiles": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
"prettier-plugin-svelte": "^2.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup-plugin-replace": "^2.2.0",
|
||||
"semver": "^7.5.0",
|
||||
"svelte": "^3.38.2",
|
||||
"typescript": "4.7.3"
|
||||
},
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
GoogleInnerConfig,
|
||||
OIDCConfig,
|
||||
OIDCInnerConfig,
|
||||
OIDCLogosConfig,
|
||||
SCIMConfig,
|
||||
SCIMInnerConfig,
|
||||
SettingsConfig,
|
||||
|
@ -191,6 +192,10 @@ export function getDefaultGoogleConfig(): GoogleInnerConfig | undefined {
|
|||
|
||||
// OIDC
|
||||
|
||||
export async function getOIDCLogosDoc(): Promise<OIDCLogosConfig | undefined> {
|
||||
return getConfig<OIDCLogosConfig>(ConfigType.OIDC_LOGOS)
|
||||
}
|
||||
|
||||
async function getOIDCConfigDoc(): Promise<OIDCConfig | undefined> {
|
||||
return getConfig<OIDCConfig>(ConfigType.OIDC)
|
||||
}
|
||||
|
|
|
@ -57,6 +57,9 @@ class Replication {
|
|||
appReplicateOpts() {
|
||||
return {
|
||||
filter: (doc: any) => {
|
||||
if (doc._id && doc._id.startsWith(DocumentType.AUTOMATION_LOG)) {
|
||||
return false
|
||||
}
|
||||
return doc._id !== DocumentType.APP_METADATA
|
||||
},
|
||||
}
|
||||
|
|
|
@ -99,9 +99,15 @@
|
|||
bind:this={button}
|
||||
>
|
||||
{#if fieldIcon}
|
||||
<span class="option-extra icon">
|
||||
<Icon size="S" name={fieldIcon} />
|
||||
</span>
|
||||
{#if !useOptionIconImage}
|
||||
<span class="option-extra icon">
|
||||
<Icon size="S" name={fieldIcon} />
|
||||
</span>
|
||||
{:else}
|
||||
<span class="option-extra icon field-icon">
|
||||
<img src={fieldIcon} alt="icon" width="15" height="15" />
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if fieldColour}
|
||||
<span class="option-extra">
|
||||
|
@ -311,4 +317,8 @@
|
|||
max-width: 170px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.option-extra.icon.field-icon {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
<script>
|
||||
import { get } from "svelte/store"
|
||||
import { ActionButton, Modal, notifications } from "@budibase/bbui"
|
||||
import CreateEditRelationship from "../../Datasources/CreateEditRelationship.svelte"
|
||||
import { datasources, integrations } from "../../../../stores/backend"
|
||||
import { ActionButton, notifications } from "@budibase/bbui"
|
||||
import CreateEditRelationshipModal from "../../Datasources/CreateEditRelationshipModal.svelte"
|
||||
import { datasources } from "../../../../stores/backend"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { integrationForDatasource } from "stores/selectors"
|
||||
|
||||
export let table
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: datasource = findDatasource(table?._id)
|
||||
$: plusTables = datasource?.plus
|
||||
? Object.values(datasource?.entities || {})
|
||||
: []
|
||||
$: tables = datasource?.plus ? Object.values(datasource?.entities || {}) : []
|
||||
|
||||
let modal
|
||||
|
||||
|
@ -26,34 +22,32 @@
|
|||
})
|
||||
}
|
||||
|
||||
async function saveRelationship() {
|
||||
try {
|
||||
// Create datasource
|
||||
await datasources.update({
|
||||
datasource,
|
||||
integration: integrationForDatasource(get(integrations), datasource),
|
||||
})
|
||||
notifications.success(`Relationship information saved.`)
|
||||
dispatch("updatecolumns")
|
||||
} catch (err) {
|
||||
notifications.error(`Error saving relationship info: ${err}`)
|
||||
}
|
||||
const afterSave = ({ action }) => {
|
||||
notifications.success(`Relationship ${action} successfully`)
|
||||
dispatch("updatecolumns")
|
||||
}
|
||||
|
||||
const onError = err => {
|
||||
notifications.error(`Error saving relationship info: ${err}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if datasource}
|
||||
<div>
|
||||
<ActionButton icon="DataCorrelated" primary quiet on:click={modal.show}>
|
||||
<ActionButton
|
||||
icon="DataCorrelated"
|
||||
primary
|
||||
quiet
|
||||
on:click={() => modal.show({ fromTable: table })}
|
||||
>
|
||||
Define relationship
|
||||
</ActionButton>
|
||||
</div>
|
||||
<Modal bind:this={modal}>
|
||||
<CreateEditRelationship
|
||||
{datasource}
|
||||
save={saveRelationship}
|
||||
close={modal.hide}
|
||||
{plusTables}
|
||||
selectedFromTable={table}
|
||||
/>
|
||||
</Modal>
|
||||
<CreateEditRelationshipModal
|
||||
bind:this={modal}
|
||||
{datasource}
|
||||
{tables}
|
||||
{afterSave}
|
||||
{onError}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -1,249 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Heading,
|
||||
Body,
|
||||
Divider,
|
||||
InlineAlert,
|
||||
Button,
|
||||
notifications,
|
||||
Modal,
|
||||
Table,
|
||||
FancyCheckboxGroup,
|
||||
} from "@budibase/bbui"
|
||||
import { datasources, integrations, tables } from "stores/backend"
|
||||
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
|
||||
import CreateExternalTableModal from "./CreateExternalTableModal.svelte"
|
||||
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
|
||||
export let datasource
|
||||
export let save
|
||||
|
||||
let tableSchema = {
|
||||
name: {},
|
||||
primary: { displayName: "Primary Key" },
|
||||
}
|
||||
let relationshipSchema = {
|
||||
tables: {},
|
||||
columns: {},
|
||||
}
|
||||
let relationshipModal
|
||||
let createExternalTableModal
|
||||
let selectedFromRelationship, selectedToRelationship
|
||||
let confirmDialog
|
||||
let specificTables = null
|
||||
let tableList
|
||||
|
||||
$: integration = datasource && $integrations[datasource.source]
|
||||
$: plusTables = datasource?.plus
|
||||
? Object.values(datasource?.entities || {})
|
||||
: []
|
||||
$: relationships = getRelationships(plusTables)
|
||||
$: schemaError = $datasources.schemaError
|
||||
$: relationshipInfo = relationshipTableData(relationships)
|
||||
|
||||
function getRelationships(tables) {
|
||||
if (!tables || !Array.isArray(tables)) {
|
||||
return {}
|
||||
}
|
||||
let pairs = {}
|
||||
for (let table of tables) {
|
||||
for (let column of Object.values(table.schema)) {
|
||||
if (column.type !== "link") {
|
||||
continue
|
||||
}
|
||||
// these relationships have an id to pair them to each other
|
||||
// one has a main for the from side
|
||||
const key = column.main ? "from" : "to"
|
||||
pairs[column._id] = {
|
||||
...pairs[column._id],
|
||||
[key]: column,
|
||||
}
|
||||
}
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
function buildRelationshipDisplayString(fromCol, toCol) {
|
||||
function getTableName(tableId) {
|
||||
if (!tableId || typeof tableId !== "string") {
|
||||
return null
|
||||
}
|
||||
return plusTables.find(table => table._id === tableId)?.name || "Unknown"
|
||||
}
|
||||
if (!toCol || !fromCol) {
|
||||
return "Cannot build name"
|
||||
}
|
||||
const fromTableName = getTableName(toCol.tableId)
|
||||
const toTableName = getTableName(fromCol.tableId)
|
||||
const throughTableName = getTableName(fromCol.through)
|
||||
|
||||
let displayString
|
||||
if (throughTableName) {
|
||||
displayString = `${fromTableName} ↔ ${toTableName}`
|
||||
} else {
|
||||
displayString = `${fromTableName} → ${toTableName}`
|
||||
}
|
||||
return displayString
|
||||
}
|
||||
|
||||
async function updateDatasourceSchema() {
|
||||
try {
|
||||
await datasources.updateSchema(datasource, specificTables)
|
||||
notifications.success(`Datasource ${name} tables updated successfully.`)
|
||||
await tables.fetch()
|
||||
} catch (error) {
|
||||
notifications.error(
|
||||
`Error updating datasource schema ${
|
||||
error?.message ? `: ${error.message}` : ""
|
||||
}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function onClickTable(table) {
|
||||
$goto(`../../table/${table._id}`)
|
||||
}
|
||||
|
||||
function openRelationshipModal(fromRelationship, toRelationship) {
|
||||
selectedFromRelationship = fromRelationship || {}
|
||||
selectedToRelationship = toRelationship || {}
|
||||
relationshipModal.show()
|
||||
}
|
||||
|
||||
function createNewTable() {
|
||||
createExternalTableModal.show()
|
||||
}
|
||||
|
||||
function relationshipTableData(relations) {
|
||||
return Object.values(relations).map(relationship => ({
|
||||
tables: buildRelationshipDisplayString(
|
||||
relationship.from,
|
||||
relationship.to
|
||||
),
|
||||
columns: `${relationship.from?.name} to ${relationship.to?.name}`,
|
||||
from: relationship.from,
|
||||
to: relationship.to,
|
||||
}))
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={relationshipModal}>
|
||||
<CreateEditRelationship
|
||||
{datasource}
|
||||
{save}
|
||||
close={relationshipModal.hide}
|
||||
{plusTables}
|
||||
fromRelationship={selectedFromRelationship}
|
||||
toRelationship={selectedToRelationship}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={createExternalTableModal}>
|
||||
<CreateExternalTableModal {datasource} />
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDialog}
|
||||
okText="Fetch tables"
|
||||
onOk={updateDatasourceSchema}
|
||||
onCancel={() => confirmDialog.hide()}
|
||||
warning={false}
|
||||
title="Confirm table fetch"
|
||||
>
|
||||
<Body>
|
||||
If you have fetched tables from this database before, this action may
|
||||
overwrite any changes you made after your initial fetch.
|
||||
</Body>
|
||||
<br />
|
||||
<div class="table-checkboxes">
|
||||
<FancyCheckboxGroup options={tableList} bind:selected={specificTables} />
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
|
||||
<Divider />
|
||||
<div class="query-header">
|
||||
<Heading size="S">Tables</Heading>
|
||||
<div class="table-buttons">
|
||||
<Button
|
||||
secondary
|
||||
on:click={async () => {
|
||||
tableList = await datasources.getTableNames(datasource)
|
||||
confirmDialog.show()
|
||||
}}
|
||||
>
|
||||
Fetch tables
|
||||
</Button>
|
||||
<Button cta icon="Add" on:click={createNewTable}>New table</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Body>
|
||||
This datasource can determine tables automatically. Budibase can fetch your
|
||||
tables directly from the database and you can use them without having to write
|
||||
any queries at all.
|
||||
</Body>
|
||||
{#if schemaError}
|
||||
<InlineAlert
|
||||
type="error"
|
||||
header="Error fetching tables"
|
||||
message={schemaError}
|
||||
onConfirm={datasources.removeSchemaError}
|
||||
/>
|
||||
{/if}
|
||||
{#if plusTables && Object.values(plusTables).length > 0}
|
||||
<Table
|
||||
on:click={({ detail }) => onClickTable(detail)}
|
||||
schema={tableSchema}
|
||||
data={Object.values(plusTables)}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={false}
|
||||
customRenderers={[{ column: "primary", component: ArrayRenderer }]}
|
||||
/>
|
||||
{:else}
|
||||
<Body size="S"><i>No tables found.</i></Body>
|
||||
{/if}
|
||||
{#if integration.relationships !== false}
|
||||
<Divider />
|
||||
<div class="query-header">
|
||||
<Heading size="S">Relationships</Heading>
|
||||
<Button primary on:click={() => openRelationshipModal()}>
|
||||
Define relationship
|
||||
</Button>
|
||||
</div>
|
||||
<Body>
|
||||
Tell budibase how your tables are related to get even more smart features.
|
||||
</Body>
|
||||
{#if relationshipInfo && relationshipInfo.length > 0}
|
||||
<Table
|
||||
on:click={({ detail }) => openRelationshipModal(detail.from, detail.to)}
|
||||
schema={relationshipSchema}
|
||||
data={relationshipInfo}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={false}
|
||||
/>
|
||||
{:else}
|
||||
<Body size="S"><i>No relationships configured.</i></Body>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.query-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0 0 var(--spacing-s) 0;
|
||||
}
|
||||
|
||||
.table-buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.table-checkboxes {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -1,86 +0,0 @@
|
|||
<script>
|
||||
import { Body, notifications } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import { API } from "api"
|
||||
import ICONS from "../icons"
|
||||
|
||||
export let integration = {}
|
||||
let integrations = []
|
||||
const INTERNAL = "BUDIBASE"
|
||||
|
||||
async function fetchIntegrations() {
|
||||
let otherIntegrations
|
||||
try {
|
||||
otherIntegrations = await API.getIntegrations()
|
||||
} catch (error) {
|
||||
otherIntegrations = {}
|
||||
notifications.error("Error getting integrations")
|
||||
}
|
||||
integrations = {
|
||||
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
||||
...otherIntegrations,
|
||||
}
|
||||
}
|
||||
|
||||
function selectIntegration(integrationType) {
|
||||
const selected = integrations[integrationType]
|
||||
|
||||
// build the schema
|
||||
const schema = {}
|
||||
for (let key of Object.keys(selected.datasource)) {
|
||||
schema[key] = selected.datasource[key].default
|
||||
}
|
||||
|
||||
integration = {
|
||||
type: integrationType,
|
||||
plus: selected.plus,
|
||||
...schema,
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchIntegrations()
|
||||
})
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<div class="integration-list">
|
||||
{#each Object.entries(integrations) as [integrationType, schema]}
|
||||
<div
|
||||
class="integration hoverable"
|
||||
class:selected={integration.type === integrationType}
|
||||
on:click={() => selectIntegration(integrationType)}
|
||||
>
|
||||
<svelte:component
|
||||
this={ICONS[integrationType]}
|
||||
height="50"
|
||||
width="50"
|
||||
/>
|
||||
<Body size="XS">{schema.name || integrationType}</Body>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.integration-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
grid-gap: var(--spectrum-alias-grid-baseline);
|
||||
}
|
||||
|
||||
.integration {
|
||||
display: grid;
|
||||
background: var(--background-alt);
|
||||
place-items: center;
|
||||
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
||||
padding: var(--spectrum-alias-item-padding-s);
|
||||
transition: 0.3s all;
|
||||
border-radius: var(--spectrum-alias-item-rounded-border-radius-s);
|
||||
}
|
||||
|
||||
.integration:hover,
|
||||
.selected {
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
}
|
||||
</style>
|
|
@ -1,137 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Divider,
|
||||
Heading,
|
||||
ActionButton,
|
||||
Badge,
|
||||
Body,
|
||||
Layout,
|
||||
} from "@budibase/bbui"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
import RestAuthenticationBuilder from "./auth/RestAuthenticationBuilder.svelte"
|
||||
import ViewDynamicVariables from "./variables/ViewDynamicVariables.svelte"
|
||||
import {
|
||||
getRestBindings,
|
||||
getEnvironmentBindings,
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableMap,
|
||||
} from "builderStore/dataBinding"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { licensing } from "stores/portal"
|
||||
|
||||
export let datasource
|
||||
export let queries
|
||||
|
||||
let addHeader
|
||||
|
||||
let parsedHeaders = runtimeToReadableMap(
|
||||
getRestBindings(),
|
||||
cloneDeep(datasource?.config?.defaultHeaders)
|
||||
)
|
||||
|
||||
const onDefaultHeaderUpdate = headers => {
|
||||
const flatHeaders = cloneDeep(headers).reduce((acc, entry) => {
|
||||
acc[entry.name] = readableToRuntimeBinding(getRestBindings(), entry.value)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
datasource.config.defaultHeaders = flatHeaders
|
||||
}
|
||||
</script>
|
||||
|
||||
<Divider />
|
||||
<div class="section-header">
|
||||
<div class="badge">
|
||||
<Heading size="S">Headers</Heading>
|
||||
<Badge quiet grey>Optional</Badge>
|
||||
</div>
|
||||
<div class="headerRight">
|
||||
<slot name="headerRight" />
|
||||
</div>
|
||||
</div>
|
||||
<Body size="S">
|
||||
Headers enable you to provide additional information about the request, such
|
||||
as format.
|
||||
</Body>
|
||||
<KeyValueBuilder
|
||||
bind:this={addHeader}
|
||||
bind:object={parsedHeaders}
|
||||
on:change={evt => onDefaultHeaderUpdate(evt.detail)}
|
||||
noAddButton
|
||||
bindings={getRestBindings()}
|
||||
/>
|
||||
<div>
|
||||
<ActionButton icon="Add" on:click={() => addHeader.addEntry()}>
|
||||
Add header
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
<div class="section-header">
|
||||
<div class="badge">
|
||||
<Heading size="S">Authentication</Heading>
|
||||
<Badge quiet grey>Optional</Badge>
|
||||
</div>
|
||||
<div class="headerRight">
|
||||
<slot name="headerRight" />
|
||||
</div>
|
||||
</div>
|
||||
<Body size="S">
|
||||
Create an authentication config that can be shared with queries.
|
||||
</Body>
|
||||
<RestAuthenticationBuilder bind:configs={datasource.config.authConfigs} />
|
||||
|
||||
<Divider />
|
||||
<div class="section-header">
|
||||
<div class="badge">
|
||||
<Heading size="S">Variables</Heading>
|
||||
<Badge quiet grey>Optional</Badge>
|
||||
</div>
|
||||
<div class="headerRight">
|
||||
<slot name="headerRight" />
|
||||
</div>
|
||||
</div>
|
||||
<Body size="S"
|
||||
>Variables enable you to store and re-use values in queries, with the choice
|
||||
of a static value such as a token using static variables, or a value from a
|
||||
query response using dynamic variables.</Body
|
||||
>
|
||||
<Heading size="XS">Static</Heading>
|
||||
<Layout noPadding gap="XS">
|
||||
<KeyValueBuilder
|
||||
name="Variable"
|
||||
keyPlaceholder="Name"
|
||||
headings
|
||||
bind:object={datasource.config.staticVariables}
|
||||
on:change
|
||||
bindings={$licensing.environmentVariablesEnabled
|
||||
? getEnvironmentBindings()
|
||||
: []}
|
||||
/>
|
||||
</Layout>
|
||||
<div />
|
||||
<Heading size="XS">Dynamic</Heading>
|
||||
<Body size="S">
|
||||
Dynamic variables are evaluated when a dependant query is executed. The value
|
||||
is cached for a period of time and will be refreshed if a query fails.
|
||||
</Body>
|
||||
<ViewDynamicVariables {queries} {datasource} />
|
||||
|
||||
<style>
|
||||
.section-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.headerRight {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,66 @@
|
|||
<script>
|
||||
import { Modal } from "@budibase/bbui"
|
||||
import { get } from "svelte/store"
|
||||
import CreateEditRelationship from "./CreateEditRelationship.svelte"
|
||||
import { integrations, datasources } from "stores/backend"
|
||||
import { integrationForDatasource } from "stores/selectors"
|
||||
|
||||
export let datasource
|
||||
export let tables
|
||||
export let beforeSave = async () => {}
|
||||
export let afterSave = async () => {}
|
||||
export let onError = async () => {}
|
||||
|
||||
let relationshipModal
|
||||
let fromRelationship = {}
|
||||
let toRelationship = {}
|
||||
let fromTable = null
|
||||
|
||||
export function show({
|
||||
fromRelationship: selectedFromRelationship = {},
|
||||
toRelationship: selectedToRelationship = {},
|
||||
fromTable: selectedFromTable = null,
|
||||
}) {
|
||||
fromRelationship = selectedFromRelationship
|
||||
toRelationship = selectedToRelationship
|
||||
fromTable = selectedFromTable
|
||||
|
||||
relationshipModal.show()
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
relationshipModal.hide()
|
||||
}
|
||||
|
||||
// action is one of 'created', 'updated' or 'deleted'
|
||||
async function saveRelationship(action) {
|
||||
try {
|
||||
await beforeSave({ action, datasource })
|
||||
|
||||
const integration = integrationForDatasource(
|
||||
get(integrations),
|
||||
datasource
|
||||
)
|
||||
await datasources.update({ datasource, integration })
|
||||
|
||||
await afterSave({ datasource, action })
|
||||
} catch (err) {
|
||||
await onError({ err, datasource, action })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={relationshipModal}>
|
||||
<CreateEditRelationship
|
||||
save={saveRelationship}
|
||||
close={relationshipModal.hide}
|
||||
selectedFromTable={fromTable}
|
||||
{datasource}
|
||||
plusTables={tables}
|
||||
{fromRelationship}
|
||||
{toRelationship}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -40,6 +40,8 @@
|
|||
let userOnboardResponse = null
|
||||
let userLimitReachedModal
|
||||
|
||||
let inviteFailureResponse = ""
|
||||
|
||||
$: queryIsEmail = emailValidator(query) === true
|
||||
$: prodAppId = apps.getProdAppID($store.appId)
|
||||
$: promptInvite = showInvite(
|
||||
|
@ -308,19 +310,6 @@
|
|||
let userInviteResponse
|
||||
try {
|
||||
userInviteResponse = await users.onboard(payload)
|
||||
|
||||
const newUser = userInviteResponse?.successful.find(
|
||||
user => user.email === newUserEmail
|
||||
)
|
||||
if (newUser) {
|
||||
notifications.success(
|
||||
userInviteResponse.created
|
||||
? "User created successfully"
|
||||
: "User invite successful"
|
||||
)
|
||||
} else {
|
||||
throw new Error("User invite failed")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
notifications.error("Error inviting user")
|
||||
|
@ -331,12 +320,31 @@
|
|||
|
||||
const onInviteUser = async () => {
|
||||
userOnboardResponse = await inviteUser()
|
||||
const originalQuery = query + ""
|
||||
query = null
|
||||
|
||||
const userInviteSuccess = userOnboardResponse?.successful
|
||||
if (userInviteSuccess && userInviteSuccess[0].email === query) {
|
||||
query = null
|
||||
query = userInviteSuccess[0].email
|
||||
const newUser = userOnboardResponse?.successful.find(
|
||||
user => user.email === originalQuery
|
||||
)
|
||||
if (newUser) {
|
||||
query = originalQuery
|
||||
notifications.success(
|
||||
userOnboardResponse.created
|
||||
? "User created successfully"
|
||||
: "User invite successful"
|
||||
)
|
||||
} else {
|
||||
const failedUser = userOnboardResponse?.unsuccessful.find(
|
||||
user => user.email === originalQuery
|
||||
)
|
||||
inviteFailureResponse =
|
||||
failedUser?.reason === "Unavailable"
|
||||
? "Email already in use. Please use a different email."
|
||||
: failedUser?.reason
|
||||
|
||||
notifications.error(inviteFailureResponse)
|
||||
}
|
||||
userOnboardResponse = null
|
||||
}
|
||||
|
||||
const onUpdateUserInvite = async (invite, role) => {
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
// A REST integration is created immediately, we don't need to display a config modal.
|
||||
loading = true
|
||||
datasources
|
||||
.create({ integration, fields: configFromIntegration(integration) })
|
||||
.create({ integration, config: configFromIntegration(integration) })
|
||||
.then(datasource => {
|
||||
store.setIntegration(integration)
|
||||
store.setDatasource(datasource)
|
||||
|
|
|
@ -3,9 +3,11 @@
|
|||
import AuthTypeRenderer from "./AuthTypeRenderer.svelte"
|
||||
import RestAuthenticationModal from "./RestAuthenticationModal.svelte"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let configs = []
|
||||
export let authConfigs = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let currentConfig = null
|
||||
let modal
|
||||
|
||||
|
@ -20,8 +22,10 @@
|
|||
}
|
||||
|
||||
const onConfirm = config => {
|
||||
let newAuthConfigs
|
||||
|
||||
if (currentConfig) {
|
||||
configs = configs.map(c => {
|
||||
newAuthConfigs = authConfigs.map(c => {
|
||||
// replace the current config with the new one
|
||||
if (c._id === currentConfig._id) {
|
||||
return config
|
||||
|
@ -30,27 +34,36 @@
|
|||
})
|
||||
} else {
|
||||
config._id = Helpers.uuid()
|
||||
configs = [...configs, config]
|
||||
newAuthConfigs = [...authConfigs, config]
|
||||
}
|
||||
|
||||
dispatch("change", newAuthConfigs)
|
||||
}
|
||||
|
||||
const onRemove = () => {
|
||||
configs = configs.filter(c => {
|
||||
const newAuthConfigs = authConfigs.filter(c => {
|
||||
return c._id !== currentConfig._id
|
||||
})
|
||||
|
||||
dispatch("change", newAuthConfigs)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<RestAuthenticationModal {configs} {currentConfig} {onConfirm} {onRemove} />
|
||||
<RestAuthenticationModal
|
||||
configs={authConfigs}
|
||||
{currentConfig}
|
||||
{onConfirm}
|
||||
{onRemove}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Layout gap="S" noPadding>
|
||||
{#if configs && configs.length > 0}
|
||||
{#if authConfigs && authConfigs.length > 0}
|
||||
<Table
|
||||
on:click={({ detail }) => openConfigModal(detail)}
|
||||
{schema}
|
||||
data={configs}
|
||||
data={authConfigs}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={false}
|
|
@ -0,0 +1,27 @@
|
|||
<script>
|
||||
import RestAuthenticationBuilder from "./RestAuthenticationBuilder.svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import SaveDatasourceButton from "../SaveDatasourceButton.svelte"
|
||||
import Panel from "../Panel.svelte"
|
||||
import Tooltip from "../Tooltip.svelte"
|
||||
|
||||
export let datasource
|
||||
$: updatedDatasource = cloneDeep(datasource)
|
||||
|
||||
const updateAuthConfigs = newAuthConfigs => {
|
||||
updatedDatasource.config.authConfigs = newAuthConfigs
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel>
|
||||
<SaveDatasourceButton slot="controls" {datasource} {updatedDatasource} />
|
||||
<Tooltip
|
||||
slot="tooltip"
|
||||
title="REST Authentication"
|
||||
href="https://docs.budibase.com/docs/rest-authentication"
|
||||
/>
|
||||
<RestAuthenticationBuilder
|
||||
on:change={({ detail }) => updateAuthConfigs(detail)}
|
||||
authConfigs={updatedDatasource.config.authConfigs}
|
||||
/>
|
||||
</Panel>
|
|
@ -0,0 +1,53 @@
|
|||
<script>
|
||||
import { ActionButton } from "@budibase/bbui"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
import {
|
||||
getRestBindings,
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableMap,
|
||||
} from "builderStore/dataBinding"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import SaveDatasourceButton from "./SaveDatasourceButton.svelte"
|
||||
import Panel from "./Panel.svelte"
|
||||
import Tooltip from "./Tooltip.svelte"
|
||||
|
||||
export let datasource
|
||||
let restBindings = getRestBindings()
|
||||
let addHeader
|
||||
|
||||
$: updatedDatasource = cloneDeep(datasource)
|
||||
$: parsedHeaders = runtimeToReadableMap(
|
||||
restBindings,
|
||||
updatedDatasource?.config?.defaultHeaders
|
||||
)
|
||||
|
||||
const onDefaultHeaderUpdate = headers => {
|
||||
const flatHeaders = cloneDeep(headers).reduce((acc, entry) => {
|
||||
acc[entry.name] = readableToRuntimeBinding(restBindings, entry.value)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
updatedDatasource.config.defaultHeaders = flatHeaders
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel>
|
||||
<SaveDatasourceButton slot="controls" {datasource} {updatedDatasource} />
|
||||
<Tooltip
|
||||
slot="tooltip"
|
||||
title="REST Headers"
|
||||
href="https://docs.budibase.com/docs/rest-queries#headers"
|
||||
/>
|
||||
<KeyValueBuilder
|
||||
bind:this={addHeader}
|
||||
object={parsedHeaders}
|
||||
on:change={evt => onDefaultHeaderUpdate(evt.detail)}
|
||||
noAddButton
|
||||
bindings={restBindings}
|
||||
/>
|
||||
<div>
|
||||
<ActionButton icon="Add" on:click={() => addHeader.addEntry()}>
|
||||
Add header
|
||||
</ActionButton>
|
||||
</div>
|
||||
</Panel>
|
|
@ -0,0 +1,27 @@
|
|||
<script>
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<div class="header">
|
||||
<div class="controls">
|
||||
<slot name="controls" />
|
||||
</div>
|
||||
<div class="tooltip">
|
||||
<slot name="tooltip" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
import { Button, Modal } from "@budibase/bbui"
|
||||
import ImportQueriesModal from "./RestImportQueriesModal.svelte"
|
||||
|
||||
let importQueriesModal
|
||||
</script>
|
||||
|
||||
<Modal bind:this={importQueriesModal}>
|
||||
<ImportQueriesModal createDatasource={false} datasourceId={"todo"} />
|
||||
</Modal>
|
||||
|
||||
<div class="button">
|
||||
<Button secondary on:click={() => importQueriesModal.show()}>Import</Button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.button {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,132 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import {
|
||||
ModalContent,
|
||||
notifications,
|
||||
Body,
|
||||
Layout,
|
||||
Tabs,
|
||||
Tab,
|
||||
Heading,
|
||||
TextArea,
|
||||
Dropzone,
|
||||
} from "@budibase/bbui"
|
||||
import { datasources, queries } from "stores/backend"
|
||||
import { writable } from "svelte/store"
|
||||
|
||||
export let navigateDatasource = false
|
||||
export let datasourceId
|
||||
export let createDatasource = false
|
||||
export let onCancel
|
||||
|
||||
const data = writable({
|
||||
url: "",
|
||||
raw: "",
|
||||
file: undefined,
|
||||
})
|
||||
|
||||
let lastTouched = "url"
|
||||
|
||||
const getData = async () => {
|
||||
let dataString
|
||||
|
||||
// parse the file into memory and send as string
|
||||
if (lastTouched === "file") {
|
||||
dataString = await $data.file.text()
|
||||
} else if (lastTouched === "url") {
|
||||
const response = await fetch($data.url)
|
||||
dataString = await response.text()
|
||||
} else if (lastTouched === "raw") {
|
||||
dataString = $data.raw
|
||||
}
|
||||
|
||||
return dataString
|
||||
}
|
||||
|
||||
async function importQueries() {
|
||||
try {
|
||||
const dataString = await getData()
|
||||
|
||||
if (!datasourceId && !createDatasource) {
|
||||
throw new Error("No datasource id")
|
||||
}
|
||||
|
||||
const body = {
|
||||
data: dataString,
|
||||
datasourceId,
|
||||
}
|
||||
|
||||
const importResult = await queries.import(body)
|
||||
if (!datasourceId) {
|
||||
datasourceId = importResult.datasourceId
|
||||
}
|
||||
|
||||
// reload
|
||||
await datasources.fetch()
|
||||
await queries.fetch()
|
||||
|
||||
if (navigateDatasource) {
|
||||
$goto(`./datasource/${datasourceId}`)
|
||||
}
|
||||
|
||||
notifications.success("Imported successfully")
|
||||
return true
|
||||
} catch (error) {
|
||||
notifications.error("Error importing queries")
|
||||
return false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
onConfirm={() => importQueries()}
|
||||
{onCancel}
|
||||
confirmText={"Import"}
|
||||
cancelText="Back"
|
||||
size="L"
|
||||
>
|
||||
<Layout noPadding>
|
||||
<Heading size="S">Import</Heading>
|
||||
<Body size="XS"
|
||||
>Import your rest collection using one of the options below</Body
|
||||
>
|
||||
<Tabs selected="File">
|
||||
<!-- Commenting until nginx csp issue resolved -->
|
||||
<!-- <Tab title="Link">
|
||||
<Input
|
||||
bind:value={$data.url}
|
||||
on:change={() => (lastTouched = "url")}
|
||||
label="Enter a URL"
|
||||
placeholder="e.g. https://petstore.swagger.io/v2/swagger.json"
|
||||
/>
|
||||
</Tab> -->
|
||||
<Tab title="File">
|
||||
<Dropzone
|
||||
gallery={false}
|
||||
value={$data.file ? [$data.file] : []}
|
||||
on:change={e => {
|
||||
$data.file = e.detail?.[0]
|
||||
lastTouched = "file"
|
||||
}}
|
||||
fileTags={[
|
||||
"OpenAPI 3.0",
|
||||
"OpenAPI 2.0",
|
||||
"Swagger 2.0",
|
||||
"cURL",
|
||||
"YAML",
|
||||
"JSON",
|
||||
]}
|
||||
maximum={1}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab title="Raw Text">
|
||||
<TextArea
|
||||
bind:value={$data.raw}
|
||||
on:change={() => (lastTouched = "raw")}
|
||||
label={"Paste raw text"}
|
||||
placeholder={'e.g. curl --location --request GET "https://example.com"'}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Layout>
|
||||
</ModalContent>
|
|
@ -0,0 +1,43 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { Button, Table } from "@budibase/bbui"
|
||||
import { queries } from "stores/backend"
|
||||
import CapitaliseRenderer from "components/common/renderers/CapitaliseRenderer.svelte"
|
||||
import RestImportButton from "./RestImportButton.svelte"
|
||||
import Panel from "../Panel.svelte"
|
||||
import Tooltip from "../Tooltip.svelte"
|
||||
|
||||
export let datasource
|
||||
|
||||
$: queryList = $queries.list.filter(
|
||||
query => query.datasourceId === datasource._id
|
||||
)
|
||||
</script>
|
||||
|
||||
<Panel>
|
||||
<div slot="controls">
|
||||
<Button cta on:click={() => $goto(`../../query/new/${datasource._id}`)}>
|
||||
Create new query
|
||||
</Button>
|
||||
{#if datasource.source === "REST"}
|
||||
<RestImportButton datasourceId={datasource._id} />
|
||||
{/if}
|
||||
</div>
|
||||
<Tooltip
|
||||
slot="tooltip"
|
||||
title="Custom queries"
|
||||
href="https://docs.budibase.com/docs/data-sources#custom-queries "
|
||||
/>
|
||||
<Table
|
||||
on:click={({ detail }) => $goto(`../../query/${detail._id}`)}
|
||||
schema={{
|
||||
name: {},
|
||||
queryVerb: { displayName: "Method" },
|
||||
}}
|
||||
data={queryList}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={false}
|
||||
customRenderers={[{ column: "queryVerb", component: CapitaliseRenderer }]}
|
||||
/>
|
||||
</Panel>
|
|
@ -0,0 +1,108 @@
|
|||
<script>
|
||||
import { Button, Table, notifications } from "@budibase/bbui"
|
||||
import CreateEditRelationshipModal from "components/backend/Datasources/CreateEditRelationshipModal.svelte"
|
||||
import {
|
||||
tables as tablesStore,
|
||||
integrations as integrationsStore,
|
||||
datasources as datasourcesStore,
|
||||
} from "stores/backend"
|
||||
import { DatasourceFeature } from "@budibase/types"
|
||||
import { API } from "api"
|
||||
import Panel from "./Panel.svelte"
|
||||
import Tooltip from "./Tooltip.svelte"
|
||||
|
||||
export let datasource
|
||||
|
||||
let modal
|
||||
|
||||
$: tables = Object.values(datasource.entities)
|
||||
$: relationships = getRelationships(tables)
|
||||
|
||||
function getRelationships(tables) {
|
||||
const relatedColumns = {}
|
||||
|
||||
tables.forEach(({ name: tableName, schema }) => {
|
||||
Object.values(schema).forEach(column => {
|
||||
if (column.type !== "link") return
|
||||
|
||||
relatedColumns[column._id] ??= {}
|
||||
relatedColumns[column._id].through =
|
||||
relatedColumns[column._id].through || column.through
|
||||
|
||||
relatedColumns[column._id][column.main ? "from" : "to"] = {
|
||||
...column,
|
||||
tableName,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return Object.values(relatedColumns).map(({ from, to, through }) => {
|
||||
return {
|
||||
tables: `${from.tableName} ${through ? "↔" : "→"} ${to.tableName}`,
|
||||
columns: `${from.name} to ${to.name}`,
|
||||
from,
|
||||
to,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleRowClick = ({ detail }) => {
|
||||
modal.show({ fromRelationship: detail.from, toRelationship: detail.to })
|
||||
}
|
||||
|
||||
const beforeSave = async ({ datasource }) => {
|
||||
const integration = $integrationsStore[datasource.source]
|
||||
if (!integration.features[DatasourceFeature.CONNECTION_CHECKING]) return
|
||||
|
||||
const { connected } = await API.validateDatasource(datasource)
|
||||
|
||||
if (!connected) {
|
||||
throw "Invalid connection"
|
||||
}
|
||||
}
|
||||
|
||||
const afterSave = async ({ action }) => {
|
||||
await tablesStore.fetch()
|
||||
await datasourcesStore.fetch()
|
||||
notifications.success(`Relationship ${action} successfully`)
|
||||
}
|
||||
|
||||
const onError = async ({ action, err }) => {
|
||||
let notificationVerb = "creating"
|
||||
|
||||
if (action === "updated") {
|
||||
notificationVerb = "updating"
|
||||
} else if (action === "deleted") {
|
||||
notificationVerb = "deleting"
|
||||
}
|
||||
notifications.error(`Error ${notificationVerb} datasource: ${err}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<CreateEditRelationshipModal
|
||||
bind:this={modal}
|
||||
{datasource}
|
||||
{beforeSave}
|
||||
{afterSave}
|
||||
{onError}
|
||||
{tables}
|
||||
/>
|
||||
|
||||
<Panel>
|
||||
<Button slot="controls" cta on:click={modal.show}>
|
||||
Define relationships
|
||||
</Button>
|
||||
<Tooltip
|
||||
slot="tooltip"
|
||||
title="Relationships"
|
||||
href="https://docs.budibase.com/docs/relationships"
|
||||
/>
|
||||
<Table
|
||||
on:click={handleRowClick}
|
||||
schema={{ tables: {}, columns: {} }}
|
||||
data={relationships}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={false}
|
||||
/>
|
||||
</Panel>
|
|
@ -0,0 +1,29 @@
|
|||
<script>
|
||||
import { get } from "svelte/store"
|
||||
import { isEqual } from "lodash"
|
||||
import { integrationForDatasource } from "stores/selectors"
|
||||
import { integrations, datasources } from "stores/backend"
|
||||
import { notifications, Button } from "@budibase/bbui"
|
||||
|
||||
export let datasource
|
||||
export let updatedDatasource
|
||||
|
||||
$: hasChanged = !isEqual(datasource, updatedDatasource)
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
const integration = integrationForDatasource(
|
||||
get(integrations),
|
||||
updatedDatasource
|
||||
)
|
||||
await datasources.update({ datasource: updatedDatasource, integration })
|
||||
notifications.success(
|
||||
`Datasource ${updatedDatasource.name} updated successfully`
|
||||
)
|
||||
} catch (error) {
|
||||
notifications.error(`Error saving datasource: ${error.message}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button disabled={!hasChanged} cta on:click={save}>Save</Button>
|
|
@ -0,0 +1,71 @@
|
|||
<script>
|
||||
import { Body, Button, notifications, Modal, Toggle } from "@budibase/bbui"
|
||||
import { datasources, tables } from "stores/backend"
|
||||
import CreateExternalTableModal from "./CreateExternalTableModal.svelte"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import ValuesList from "components/common/ValuesList.svelte"
|
||||
|
||||
export let datasource
|
||||
|
||||
let createExternalTableModal
|
||||
let confirmDialog
|
||||
let specificTables = null
|
||||
let requireSpecificTables = false
|
||||
|
||||
async function updateDatasourceSchema() {
|
||||
try {
|
||||
await datasources.updateSchema(datasource, specificTables)
|
||||
notifications.success(`Datasource ${name} tables updated successfully`)
|
||||
await tables.fetch()
|
||||
} catch (error) {
|
||||
notifications.error(
|
||||
`Error updating datasource schema ${
|
||||
error?.message ? `: ${error.message}` : ""
|
||||
}`
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={createExternalTableModal}>
|
||||
<CreateExternalTableModal {datasource} />
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDialog}
|
||||
okText="Fetch tables"
|
||||
onOk={updateDatasourceSchema}
|
||||
onCancel={() => confirmDialog.hide()}
|
||||
warning={false}
|
||||
title="Confirm table fetch"
|
||||
>
|
||||
<Toggle
|
||||
bind:value={requireSpecificTables}
|
||||
on:change={e => {
|
||||
requireSpecificTables = e.detail
|
||||
specificTables = null
|
||||
}}
|
||||
thin
|
||||
text="Fetch listed tables only (one per line)"
|
||||
/>
|
||||
{#if requireSpecificTables}
|
||||
<ValuesList label="" bind:values={specificTables} />
|
||||
{/if}
|
||||
<br />
|
||||
<Body>
|
||||
If you have fetched tables from this database before, this action may
|
||||
overwrite any changes you made after your initial fetch.
|
||||
</Body>
|
||||
</ConfirmDialog>
|
||||
|
||||
<div class="buttons">
|
||||
<Button cta on:click={createExternalTableModal.show}>Create new table</Button>
|
||||
<Button secondary on:click={confirmDialog.show}>Fetch tables</Button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,33 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { Table } from "@budibase/bbui"
|
||||
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
||||
import Controls from "./Controls.svelte"
|
||||
import Panel from "../Panel.svelte"
|
||||
import Tooltip from "../Tooltip.svelte"
|
||||
|
||||
export let datasource
|
||||
|
||||
let tableSchema = {
|
||||
name: {},
|
||||
primary: { displayName: "Primary Key" },
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel>
|
||||
<Controls slot="controls" {datasource} />
|
||||
<Tooltip
|
||||
slot="tooltip"
|
||||
title="Using data in your app"
|
||||
href="https://docs.budibase.com/docs/data"
|
||||
/>
|
||||
<Table
|
||||
on:click={({ detail: table }) => $goto(`../../table/${table._id}`)}
|
||||
schema={tableSchema}
|
||||
data={Object.values(datasource?.entities || {})}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={false}
|
||||
customRenderers={[{ column: "primary", component: ArrayRenderer }]}
|
||||
/>
|
||||
</Panel>
|
|
@ -0,0 +1,10 @@
|
|||
<script>
|
||||
import { ActionButton } from "@budibase/bbui"
|
||||
|
||||
export let title = ""
|
||||
export let href = null
|
||||
</script>
|
||||
|
||||
<ActionButton quiet icon="Help" on:click={() => window.open(href, "_blank")}>
|
||||
{title}
|
||||
</ActionButton>
|
|
@ -1,14 +1,13 @@
|
|||
<script>
|
||||
import { Body, Table, BoldRenderer, CodeRenderer } from "@budibase/bbui"
|
||||
import { queries as queriesStore } from "stores/backend"
|
||||
import { queries } from "stores/backend"
|
||||
import { goto } from "@roxi/routify"
|
||||
|
||||
export let datasource
|
||||
export let queries
|
||||
|
||||
let dynamicVariables = []
|
||||
|
||||
$: enrichDynamicVariables(datasource, queries)
|
||||
$: enrichDynamicVariables(datasource, $queries.list)
|
||||
|
||||
const dynamicVariableSchema = {
|
||||
name: "",
|
||||
|
@ -18,7 +17,7 @@
|
|||
|
||||
const onClick = dynamicVariable => {
|
||||
const queryId = dynamicVariable.queryId
|
||||
queriesStore.select({ _id: queryId })
|
||||
queries.select({ _id: queryId })
|
||||
$goto(`./${queryId}`)
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<script>
|
||||
import { Heading, Layout } from "@budibase/bbui"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
import ViewDynamicVariables from "./ViewDynamicVariables.svelte"
|
||||
import { getEnvironmentBindings } from "builderStore/dataBinding"
|
||||
import { licensing } from "stores/portal"
|
||||
import { queries } from "stores/backend"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import SaveDatasourceButton from "../SaveDatasourceButton.svelte"
|
||||
import Panel from "../Panel.svelte"
|
||||
import Tooltip from "../Tooltip.svelte"
|
||||
|
||||
export let datasource
|
||||
|
||||
$: updatedDatasource = cloneDeep(datasource)
|
||||
|
||||
$: queriesForDatasource = $queries.list.filter(
|
||||
query => query.datasourceId === datasource?._id
|
||||
)
|
||||
|
||||
const handleChange = newUnparsedStaticVariables => {
|
||||
const newStaticVariables = {}
|
||||
|
||||
newUnparsedStaticVariables.forEach(({ name, value }) => {
|
||||
newStaticVariables[name] = value
|
||||
})
|
||||
|
||||
updatedDatasource.config.staticVariables = newStaticVariables
|
||||
}
|
||||
</script>
|
||||
|
||||
<Panel>
|
||||
<SaveDatasourceButton slot="controls" {datasource} {updatedDatasource} />
|
||||
<Tooltip
|
||||
slot="tooltip"
|
||||
title="REST variables"
|
||||
href="https://docs.budibase.com/docs/rest-variables"
|
||||
/>
|
||||
|
||||
<Layout>
|
||||
<Layout noPadding gap="XS">
|
||||
<Heading size="S">Static</Heading>
|
||||
<KeyValueBuilder
|
||||
name="Variable"
|
||||
keyPlaceholder="Name"
|
||||
headings
|
||||
object={updatedDatasource.config.staticVariables}
|
||||
on:change={({ detail }) => handleChange(detail)}
|
||||
bindings={$licensing.environmentVariablesEnabled
|
||||
? getEnvironmentBindings()
|
||||
: []}
|
||||
/>
|
||||
</Layout>
|
||||
<Layout noPadding gap="XS">
|
||||
<Heading size="S">Dynamic</Heading>
|
||||
<ViewDynamicVariables queries={queriesForDatasource} {datasource} />
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Panel>
|
|
@ -1,163 +1,89 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import {
|
||||
Button,
|
||||
Heading,
|
||||
Body,
|
||||
Divider,
|
||||
Layout,
|
||||
notifications,
|
||||
Table,
|
||||
Modal,
|
||||
} from "@budibase/bbui"
|
||||
import { datasources, integrations, queries, tables } from "stores/backend"
|
||||
import EditDatasourceConfig from "./_components/EditDatasourceConfig.svelte"
|
||||
import RestExtraConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/rest/RestExtraConfigForm.svelte"
|
||||
import PlusConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/PlusConfigForm.svelte"
|
||||
import { Tabs, Tab, Heading, Body, Layout } from "@budibase/bbui"
|
||||
import { datasources, integrations } from "stores/backend"
|
||||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||
import CapitaliseRenderer from "components/common/renderers/CapitaliseRenderer.svelte"
|
||||
import { IntegrationTypes } from "constants/backend"
|
||||
import { isEqual } from "lodash"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
import EditDatasourceConfig from "./_components/EditDatasourceConfig.svelte"
|
||||
import TablesPanel from "./_components/panels/Tables/index.svelte"
|
||||
import RelationshipsPanel from "./_components/panels/Relationships.svelte"
|
||||
import QueriesPanel from "./_components/panels/Queries/index.svelte"
|
||||
import RestHeadersPanel from "./_components/panels/Headers.svelte"
|
||||
import RestAuthenticationPanel from "./_components/panels/Authentication/index.svelte"
|
||||
import RestVariablesPanel from "./_components/panels/Variables/index.svelte"
|
||||
|
||||
const querySchema = {
|
||||
name: {},
|
||||
queryVerb: { displayName: "Method" },
|
||||
}
|
||||
let selectedPanel = null
|
||||
let panelOptions = []
|
||||
|
||||
let importQueriesModal
|
||||
let changed = false
|
||||
let isValid = true
|
||||
let integration, baseDatasource, datasource
|
||||
let queryList
|
||||
let loading = false
|
||||
// datasources.selected can return null temporarily on datasource deletion
|
||||
$: datasource = $datasources.selected || {}
|
||||
|
||||
$: baseDatasource = $datasources.selected
|
||||
$: queryList = $queries.list.filter(
|
||||
query => query.datasourceId === datasource?._id
|
||||
)
|
||||
$: hasChanged(baseDatasource, datasource)
|
||||
$: updateDatasource(baseDatasource)
|
||||
$: getOptions(datasource)
|
||||
|
||||
const hasChanged = (base, ds) => {
|
||||
if (base && ds) {
|
||||
changed = !isEqual(base, ds)
|
||||
}
|
||||
}
|
||||
|
||||
const saveDatasource = async () => {
|
||||
try {
|
||||
// Create datasource
|
||||
await datasources.update({ datasource, integration })
|
||||
if (datasource?.plus) {
|
||||
await tables.fetch()
|
||||
}
|
||||
await datasources.fetch()
|
||||
notifications.success(`Datasource ${name} updated successfully.`)
|
||||
baseDatasource = cloneDeep(datasource)
|
||||
} catch (err) {
|
||||
notifications.error(`Error saving datasource: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
const updateDatasource = base => {
|
||||
if (base) {
|
||||
datasource = cloneDeep(base)
|
||||
integration = $integrations[datasource.source]
|
||||
const getOptions = datasource => {
|
||||
if (datasource.plus) {
|
||||
// Google Sheets' integration definition specifies `relationships: false` as it doesn't support relationships like other plus datasources
|
||||
panelOptions =
|
||||
$integrations[datasource.source].relationships === false
|
||||
? ["Tables", "Queries"]
|
||||
: ["Tables", "Relationships", "Queries"]
|
||||
// TODO what is this for?
|
||||
selectedPanel = panelOptions.includes(selectedPanel)
|
||||
? selectedPanel
|
||||
: "Tables"
|
||||
} else if (datasource.source === "REST") {
|
||||
panelOptions = ["Queries", "Headers", "Authentication", "Variables"]
|
||||
selectedPanel = panelOptions.includes(selectedPanel)
|
||||
? selectedPanel
|
||||
: "Queries"
|
||||
} else {
|
||||
panelOptions = ["Queries"]
|
||||
selectedPanel = "Queries"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={importQueriesModal}>
|
||||
{#if datasource.source === "REST"}
|
||||
<ImportRestQueriesModal
|
||||
createDatasource={false}
|
||||
datasourceId={datasource._id}
|
||||
/>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
{#if datasource && integration}
|
||||
<section>
|
||||
<Layout noPadding>
|
||||
<Layout gap="XS" noPadding>
|
||||
<header>
|
||||
<svelte:component
|
||||
this={ICONS[datasource.source]}
|
||||
height="26"
|
||||
width="26"
|
||||
/>
|
||||
<Heading size="M">{$datasources.selected?.name}</Heading>
|
||||
</header>
|
||||
</Layout>
|
||||
<EditDatasourceConfig {datasource} />
|
||||
{#if datasource.plus}
|
||||
<PlusConfigForm bind:datasource save={saveDatasource} />
|
||||
{/if}
|
||||
<Divider />
|
||||
<div class="query-header">
|
||||
<Heading size="S">Queries</Heading>
|
||||
<div class="query-buttons">
|
||||
{#if datasource?.source === IntegrationTypes.REST}
|
||||
<Button secondary on:click={() => importQueriesModal.show()}>
|
||||
Import
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
cta
|
||||
icon="Add"
|
||||
on:click={() => $goto(`../../query/new/${datasource._id}`)}
|
||||
>
|
||||
Add query
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Body size="S">
|
||||
To build an app using a datasource, you must first query the data. A
|
||||
query is a request for data or information from a datasource, for
|
||||
example a database table.
|
||||
</Body>
|
||||
{#if queryList && queryList.length > 0}
|
||||
<div class="query-list">
|
||||
<Table
|
||||
on:click={({ detail }) => $goto(`../../query/${detail._id}`)}
|
||||
schema={querySchema}
|
||||
data={queryList}
|
||||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={false}
|
||||
customRenderers={[
|
||||
{ column: "queryVerb", component: CapitaliseRenderer },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if datasource?.source === IntegrationTypes.REST}
|
||||
<RestExtraConfigForm
|
||||
queries={queryList}
|
||||
bind:datasource
|
||||
on:change={hasChanged}
|
||||
>
|
||||
<Button
|
||||
slot="headerRight"
|
||||
disabled={!changed || !isValid || loading}
|
||||
cta
|
||||
on:click={saveDatasource}
|
||||
>
|
||||
<div class="save-button-content">
|
||||
{#if loading}
|
||||
<Spinner size="10">Save</Spinner>
|
||||
{/if}
|
||||
Save
|
||||
</div>
|
||||
</Button>
|
||||
</RestExtraConfigForm>
|
||||
{/if}
|
||||
<section>
|
||||
<Layout noPadding>
|
||||
<Layout gap="XS" noPadding>
|
||||
<header>
|
||||
<svelte:component
|
||||
this={ICONS[datasource.source]}
|
||||
height="26"
|
||||
width="26"
|
||||
/>
|
||||
<Heading size="M">{$datasources.selected?.name}</Heading>
|
||||
</header>
|
||||
</Layout>
|
||||
</section>
|
||||
{/if}
|
||||
<EditDatasourceConfig {datasource} />
|
||||
<div class="tabs">
|
||||
<Tabs size="L" noPadding noHorizPadding selected={selectedPanel}>
|
||||
{#each panelOptions as panelOption}
|
||||
<Tab
|
||||
title={panelOption}
|
||||
on:click={() => (selectedPanel = panelOption)}
|
||||
/>
|
||||
{/each}
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{#if selectedPanel === null}
|
||||
<Body>loading...</Body>
|
||||
{:else if selectedPanel === "Tables"}
|
||||
<TablesPanel {datasource} />
|
||||
{:else if selectedPanel === "Relationships"}
|
||||
<RelationshipsPanel {datasource} />
|
||||
{:else if selectedPanel === "Queries"}
|
||||
<QueriesPanel {datasource} />
|
||||
{:else if selectedPanel === "Headers"}
|
||||
<RestHeadersPanel {datasource} />
|
||||
{:else if selectedPanel === "Authentication"}
|
||||
<RestAuthenticationPanel {datasource} />
|
||||
{:else if selectedPanel === "Variables"}
|
||||
<RestVariablesPanel {datasource} />
|
||||
{:else}
|
||||
<Body>Something went wrong</Body>
|
||||
{/if}
|
||||
</Layout>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
section {
|
||||
|
@ -166,32 +92,13 @@
|
|||
}
|
||||
|
||||
header {
|
||||
margin-top: 35px;
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.query-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.query-buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
.query-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.save-button-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
.tabs {
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -29,14 +29,13 @@
|
|||
}
|
||||
})
|
||||
|
||||
$: src = !$oidc.logo
|
||||
? OidcLogo
|
||||
: preDefinedIcons[$oidc.logo] || `/global/logos_oidc/${$oidc.logo}`
|
||||
$: oidcLogoImageURL = preDefinedIcons[$oidc.logo] ?? $oidc.logo
|
||||
$: logoSrc = oidcLogoImageURL ?? OidcLogo
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<FancyButton
|
||||
icon={src}
|
||||
icon={logoSrc}
|
||||
on:click={() => {
|
||||
const url = `/api/global/auth/${$auth.tenantId}/oidc/configs/${$oidc.uuid}`
|
||||
if (samePage) {
|
||||
|
|
|
@ -125,11 +125,14 @@ export function createDatasourcesStore() {
|
|||
}
|
||||
|
||||
const create = async ({ integration, config }) => {
|
||||
const count = sourceCount(integration.name)
|
||||
const nameModifier = count === 0 ? "" : ` ${count + 1}`
|
||||
|
||||
const datasource = {
|
||||
type: "datasource",
|
||||
source: integration.name,
|
||||
config,
|
||||
name: `${integration.friendlyName}-${sourceCount(integration.name) + 1}`,
|
||||
name: `${integration.friendlyName}${nameModifier}`,
|
||||
plus: integration.plus && integration.name !== IntegrationTypes.REST,
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { getContext } from "svelte"
|
||||
import InnerForm from "./InnerForm.svelte"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { writable } from "svelte/store"
|
||||
|
||||
export let dataSource
|
||||
export let theme
|
||||
|
@ -23,6 +24,7 @@
|
|||
let loaded = false
|
||||
let schema
|
||||
let table
|
||||
let currentStep = writable(1)
|
||||
|
||||
$: fetchSchema(dataSource)
|
||||
$: schemaKey = generateSchemaKey(schema)
|
||||
|
@ -92,6 +94,7 @@
|
|||
{initialValues}
|
||||
{disableValidation}
|
||||
{editAutoColumns}
|
||||
{currentStep}
|
||||
>
|
||||
<slot />
|
||||
</InnerForm>
|
||||
|
|
|
@ -13,16 +13,19 @@
|
|||
export let disableValidation = false
|
||||
export let editAutoColumns = false
|
||||
|
||||
// We export this store so that when we remount the inner form we can still
|
||||
// persist what step we're on
|
||||
export let currentStep
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable, Provider, ActionTypes } = getContext("sdk")
|
||||
|
||||
let fields = []
|
||||
const currentStep = writable(1)
|
||||
const formState = writable({
|
||||
values: {},
|
||||
errors: {},
|
||||
valid: true,
|
||||
currentStep: 1,
|
||||
currentStep: get(currentStep),
|
||||
})
|
||||
|
||||
// Reactive derived stores to derive form state from field array
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit d99e1baee70edf726bcc1a83684bf9bd641d04de
|
||||
Subproject commit 544c7e067de69832469cde673e59501480d6d98a
|
|
@ -14,7 +14,7 @@ jest.mock("../../../utilities/redis", () => ({
|
|||
import { clearAllApps, checkBuilderEndpoint } from "./utilities/TestFunctions"
|
||||
import * as setup from "./utilities"
|
||||
import { AppStatus } from "../../../db/utils"
|
||||
import { events, utils } from "@budibase/backend-core"
|
||||
import { events, utils, context } from "@budibase/backend-core"
|
||||
import env from "../../../environment"
|
||||
|
||||
jest.setTimeout(15000)
|
||||
|
@ -324,4 +324,35 @@ describe("/applications", () => {
|
|||
expect(events.app.unpublished).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /api/applications/:appId/sync", () => {
|
||||
it("should not sync automation logs", async () => {
|
||||
// setup the apps
|
||||
await config.createApp("testing-auto-logs")
|
||||
const automation = await config.createAutomation()
|
||||
await config.publish()
|
||||
await context.doInAppContext(config.getProdAppId(), () => {
|
||||
return config.createAutomationLog(automation)
|
||||
})
|
||||
|
||||
// do the sync
|
||||
const appId = config.getAppId()
|
||||
await request
|
||||
.post(`/api/applications/${appId}/sync`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
// does exist in prod
|
||||
const prodLogs = await config.getAutomationLogs()
|
||||
expect(prodLogs.data.length).toBe(1)
|
||||
|
||||
// delete prod app so we revert to dev log search
|
||||
await config.unpublish()
|
||||
|
||||
// doesn't exist in dev
|
||||
const devLogs = await config.getAutomationLogs()
|
||||
expect(devLogs.data.length).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -382,18 +382,26 @@ class MongoIntegration implements IntegrationBase {
|
|||
return this.client.connect()
|
||||
}
|
||||
|
||||
createObjectIds(json: any) {
|
||||
matchId(value?: string) {
|
||||
return value?.match(/(?<=objectid\(['"]).*(?=['"]\))/gi)?.[0]
|
||||
}
|
||||
|
||||
hasObjectId(value?: any): boolean {
|
||||
return (
|
||||
typeof value === "string" && value.toLowerCase().startsWith("objectid")
|
||||
)
|
||||
}
|
||||
|
||||
createObjectIds(json: any): any {
|
||||
const self = this
|
||||
|
||||
function interpolateObjectIds(json: any) {
|
||||
for (let field of Object.keys(json || {})) {
|
||||
if (json[field] instanceof Object) {
|
||||
json[field] = self.createObjectIds(json[field])
|
||||
}
|
||||
if (
|
||||
typeof json[field] === "string" &&
|
||||
json[field].toLowerCase().startsWith("objectid")
|
||||
) {
|
||||
const id = json[field].match(/(?<=objectid\(['"]).*(?=['"]\))/gi)?.[0]
|
||||
if (self.hasObjectId(json[field])) {
|
||||
const id = self.matchId(json[field])
|
||||
if (id) {
|
||||
json[field] = ObjectId.createFromHexString(id)
|
||||
}
|
||||
|
@ -404,7 +412,14 @@ class MongoIntegration implements IntegrationBase {
|
|||
|
||||
if (Array.isArray(json)) {
|
||||
for (let i = 0; i < json.length; i++) {
|
||||
json[i] = interpolateObjectIds(json[i])
|
||||
if (self.hasObjectId(json[i])) {
|
||||
const id = self.matchId(json[i])
|
||||
if (id) {
|
||||
json[i] = ObjectId.createFromHexString(id)
|
||||
}
|
||||
} else {
|
||||
json[i] = interpolateObjectIds(json[i])
|
||||
}
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
basicScreen,
|
||||
basicLayout,
|
||||
basicWebhook,
|
||||
basicAutomationResults,
|
||||
} from "./structures"
|
||||
import {
|
||||
constants,
|
||||
|
@ -48,6 +49,7 @@ import {
|
|||
Table,
|
||||
SearchFilters,
|
||||
UserRoles,
|
||||
Automation,
|
||||
} from "@budibase/types"
|
||||
import { BUILTIN_ROLE_IDS } from "@budibase/backend-core/src/security/roles"
|
||||
|
||||
|
@ -720,6 +722,26 @@ class TestConfiguration {
|
|||
return { datasource, query: basedOnQuery }
|
||||
}
|
||||
|
||||
// AUTOMATION LOG
|
||||
|
||||
async createAutomationLog(automation: Automation) {
|
||||
return await context.doInAppContext(this.getProdAppId(), async () => {
|
||||
return await pro.sdk.automations.logs.storeLog(
|
||||
automation,
|
||||
basicAutomationResults(automation._id!)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async getAutomationLogs() {
|
||||
return context.doInAppContext(this.appId, async () => {
|
||||
const now = new Date()
|
||||
return await pro.sdk.automations.logs.logSearch({
|
||||
startDate: new Date(now.getTime() - 100000).toISOString(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// QUERY
|
||||
|
||||
async previewQuery(
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
import {
|
||||
Automation,
|
||||
AutomationActionStepId,
|
||||
AutomationResults,
|
||||
AutomationStatus,
|
||||
AutomationStep,
|
||||
AutomationStepType,
|
||||
AutomationTrigger,
|
||||
|
@ -241,6 +243,23 @@ export function collectAutomation(tableId?: string): Automation {
|
|||
return automation as Automation
|
||||
}
|
||||
|
||||
export function basicAutomationResults(
|
||||
automationId: string
|
||||
): AutomationResults {
|
||||
return {
|
||||
automationId,
|
||||
status: AutomationStatus.SUCCESS,
|
||||
trigger: "trigger",
|
||||
steps: [
|
||||
{
|
||||
stepId: AutomationActionStepId.SERVER_LOG,
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function basicRow(tableId: string) {
|
||||
return {
|
||||
name: "Test Contact",
|
||||
|
|
|
@ -79,6 +79,12 @@ export interface OIDCConfigs {
|
|||
configs: OIDCInnerConfig[]
|
||||
}
|
||||
|
||||
export interface OIDCLogosInnerConfig {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export interface OIDCLogosConfig extends Config<OIDCLogosInnerConfig> {}
|
||||
|
||||
export interface OIDCInnerConfig {
|
||||
configUrl: string
|
||||
clientID: string
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
SSOConfig,
|
||||
SSOConfigType,
|
||||
UserCtx,
|
||||
OIDCLogosConfig,
|
||||
} from "@budibase/types"
|
||||
import * as pro from "@budibase/pro"
|
||||
|
||||
|
@ -280,13 +281,39 @@ export async function save(ctx: UserCtx<Config>) {
|
|||
}
|
||||
}
|
||||
|
||||
function enrichOIDCLogos(oidcLogos: OIDCLogosConfig) {
|
||||
if (!oidcLogos) {
|
||||
return
|
||||
}
|
||||
oidcLogos.config = Object.keys(oidcLogos.config || {}).reduce(
|
||||
(acc: any, key: string) => {
|
||||
if (!key.endsWith("Etag")) {
|
||||
const etag = oidcLogos.config[`${key}Etag`]
|
||||
const objectStoreUrl = objectStore.getGlobalFileUrl(
|
||||
oidcLogos.type,
|
||||
key,
|
||||
etag
|
||||
)
|
||||
acc[key] = objectStoreUrl
|
||||
} else {
|
||||
acc[key] = oidcLogos.config[key]
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
export async function find(ctx: UserCtx) {
|
||||
try {
|
||||
// Find the config with the most granular scope based on context
|
||||
const type = ctx.params.type
|
||||
const scopedConfig = await configs.getConfig(type)
|
||||
let scopedConfig = await configs.getConfig(type)
|
||||
|
||||
if (scopedConfig) {
|
||||
if (type === ConfigType.OIDC_LOGOS) {
|
||||
enrichOIDCLogos(scopedConfig)
|
||||
}
|
||||
ctx.body = scopedConfig
|
||||
} else {
|
||||
// don't throw an error, there simply is nothing to return
|
||||
|
@ -300,16 +327,21 @@ export async function find(ctx: UserCtx) {
|
|||
export async function publicOidc(ctx: Ctx<void, GetPublicOIDCConfigResponse>) {
|
||||
try {
|
||||
// Find the config with the most granular scope based on context
|
||||
const config = await configs.getOIDCConfig()
|
||||
const oidcConfig = await configs.getOIDCConfig()
|
||||
const oidcCustomLogos = await configs.getOIDCLogosDoc()
|
||||
|
||||
if (!config) {
|
||||
if (oidcCustomLogos) {
|
||||
enrichOIDCLogos(oidcCustomLogos)
|
||||
}
|
||||
|
||||
if (!oidcConfig) {
|
||||
ctx.body = []
|
||||
} else {
|
||||
ctx.body = [
|
||||
{
|
||||
logo: config.logo,
|
||||
name: config.name,
|
||||
uuid: config.uuid,
|
||||
logo: oidcCustomLogos?.config[oidcConfig.logo] ?? oidcConfig.logo,
|
||||
name: oidcConfig.name,
|
||||
uuid: oidcConfig.uuid,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
node_modules/
|
|
@ -1,7 +1,7 @@
|
|||
const fs = require("fs")
|
||||
const semver = require("semver")
|
||||
|
||||
const filePath = "lerna.json"
|
||||
const filePath = "../lerna.json"
|
||||
const versionBump = process.argv[2] || "patch"
|
||||
|
||||
// Read and parse lerna.json file
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "scripts",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"semver": "^7.5.3"
|
||||
}
|
||||
}
|
|
@ -7,10 +7,10 @@ then
|
|||
fi
|
||||
|
||||
# Bump the version in lerna.json
|
||||
node scripts/bumpVersion.js $1
|
||||
node ./bumpVersion.js $1
|
||||
|
||||
|
||||
NEW_VERSION=$(node -p "require('./lerna.json').version")
|
||||
NEW_VERSION=$(node -p "require('../lerna.json').version")
|
||||
git add lerna.json
|
||||
git commit -m "Bump version to $NEW_VERSION"
|
||||
git tag v$NEW_VERSION
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
semver@^7.5.3:
|
||||
version "7.5.3"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e"
|
||||
integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
|
@ -22426,13 +22426,6 @@ semver@^7.2.1, semver@^7.3.5:
|
|||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
semver@^7.5.0:
|
||||
version "7.5.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.0.tgz#ed8c5dc8efb6c629c88b23d41dc9bf40c1d96cd0"
|
||||
integrity sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
semver@~2.3.1:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-2.3.2.tgz#b9848f25d6cf36333073ec9ef8856d42f1233e52"
|
||||
|
|
Loading…
Reference in New Issue