Updating REST creation, removing the modal step, updating the config page to move things around as per designs.
This commit is contained in:
parent
40aa9656e1
commit
3e5f9b9505
|
@ -0,0 +1,44 @@
|
||||||
|
import { datasources, tables } from "../stores/backend"
|
||||||
|
import { IntegrationNames } from "../constants"
|
||||||
|
import analytics, { Events } from "../analytics"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import cloneDeep from "lodash/cloneDeepWith"
|
||||||
|
|
||||||
|
function prepareData(config) {
|
||||||
|
let datasource = {}
|
||||||
|
let existingTypeCount = get(datasources).list.filter(
|
||||||
|
ds => ds.source === config.type
|
||||||
|
).length
|
||||||
|
|
||||||
|
let baseName = IntegrationNames[config.type]
|
||||||
|
let name =
|
||||||
|
existingTypeCount === 0 ? baseName : `${baseName}-${existingTypeCount + 1}`
|
||||||
|
|
||||||
|
datasource.type = "datasource"
|
||||||
|
datasource.source = config.type
|
||||||
|
datasource.config = config.config
|
||||||
|
datasource.name = name
|
||||||
|
datasource.plus = config.plus
|
||||||
|
|
||||||
|
return datasource
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveDatasource(config) {
|
||||||
|
const datasource = prepareData(config)
|
||||||
|
// Create datasource
|
||||||
|
const resp = await datasources.save(datasource, datasource.plus)
|
||||||
|
|
||||||
|
// update the tables incase data source plus
|
||||||
|
await tables.fetch()
|
||||||
|
await datasources.select(resp._id)
|
||||||
|
analytics.captureEvent(Events.DATASOURCE.CREATED, {
|
||||||
|
name: resp.name,
|
||||||
|
source: resp.source,
|
||||||
|
})
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRestDatasource(integration) {
|
||||||
|
const config = cloneDeep(integration)
|
||||||
|
return saveDatasource(config)
|
||||||
|
}
|
|
@ -9,16 +9,40 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
|
import { IntegrationTypes } from "constants"
|
||||||
|
|
||||||
export let integration
|
export let integration
|
||||||
export let schema
|
export let schema
|
||||||
|
export let creating
|
||||||
|
|
||||||
|
function filter([key, value]) {
|
||||||
|
if (!value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !(
|
||||||
|
(integration.source === IntegrationTypes.REST &&
|
||||||
|
key === "defaultHeaders") ||
|
||||||
|
value.deprecated
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$: config = integration.config
|
||||||
|
$: configKeys = Object.entries(schema || {})
|
||||||
|
.filter(el => filter(el))
|
||||||
|
.map(([key]) => key)
|
||||||
|
|
||||||
let addButton
|
let addButton
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<Layout gap="S">
|
<Layout gap="S">
|
||||||
{#each Object.keys(schema) as configKey}
|
{#if !creating}
|
||||||
|
<div class="form-row">
|
||||||
|
<Label>{capitalise("Name")}</Label>
|
||||||
|
<Input on:change bind:value={integration.name} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#each configKeys as configKey}
|
||||||
{#if schema[configKey].type === "object"}
|
{#if schema[configKey].type === "object"}
|
||||||
<div class="form-row ssl">
|
<div class="form-row ssl">
|
||||||
<Label>{capitalise(configKey)}</Label>
|
<Label>{capitalise(configKey)}</Label>
|
||||||
|
@ -29,13 +53,13 @@
|
||||||
<KeyValueBuilder
|
<KeyValueBuilder
|
||||||
bind:this={addButton}
|
bind:this={addButton}
|
||||||
defaults={schema[configKey].default}
|
defaults={schema[configKey].default}
|
||||||
bind:object={integration[configKey]}
|
bind:object={config[configKey]}
|
||||||
noAddButton={true}
|
noAddButton={true}
|
||||||
/>
|
/>
|
||||||
{:else if schema[configKey].type === "boolean"}
|
{:else if schema[configKey].type === "boolean"}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<Label>{capitalise(configKey)}</Label>
|
<Label>{capitalise(configKey)}</Label>
|
||||||
<Toggle text="" bind:value={integration[configKey]} />
|
<Toggle text="" bind:value={config[configKey]} />
|
||||||
</div>
|
</div>
|
||||||
{:else if schema[configKey].type === "longForm"}
|
{:else if schema[configKey].type === "longForm"}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
|
@ -43,7 +67,7 @@
|
||||||
<TextArea
|
<TextArea
|
||||||
type={schema[configKey].type}
|
type={schema[configKey].type}
|
||||||
on:change
|
on:change
|
||||||
bind:value={integration[configKey]}
|
bind:value={config[configKey]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
@ -52,7 +76,7 @@
|
||||||
<Input
|
<Input
|
||||||
type={schema[configKey].type}
|
type={schema[configKey].type}
|
||||||
on:change
|
on:change
|
||||||
bind:value={integration[configKey]}
|
bind:value={config[configKey]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -0,0 +1,249 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Divider,
|
||||||
|
InlineAlert,
|
||||||
|
ActionButton,
|
||||||
|
notifications,
|
||||||
|
Modal,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { datasources, integrations, tables } from "stores/backend"
|
||||||
|
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
|
||||||
|
import CreateExternalTableModal from "./CreateExternalTableModal.svelte"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
|
export let datasource
|
||||||
|
export let save
|
||||||
|
|
||||||
|
let relationshipModal
|
||||||
|
let createExternalTableModal
|
||||||
|
let selectedFromRelationship, selectedToRelationship
|
||||||
|
|
||||||
|
$: integration = datasource && $integrations[datasource.source]
|
||||||
|
$: plusTables = datasource?.plus
|
||||||
|
? Object.values(datasource?.entities || {})
|
||||||
|
: []
|
||||||
|
$: relationships = getRelationships(plusTables)
|
||||||
|
$: schemaError = $datasources.schemaError
|
||||||
|
|
||||||
|
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} through ${throughTableName} → ${toTableName}`
|
||||||
|
} else {
|
||||||
|
displayString = `${fromTableName} → ${toTableName}`
|
||||||
|
}
|
||||||
|
return displayString
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDatasourceSchema() {
|
||||||
|
try {
|
||||||
|
await datasources.updateSchema(datasource)
|
||||||
|
notifications.success(`Datasource ${name} tables updated successfully.`)
|
||||||
|
await tables.fetch()
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Error updating datasource schema: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickTable(table) {
|
||||||
|
tables.select(table)
|
||||||
|
$goto(`../../table/${table._id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRelationshipModal(fromRelationship, toRelationship) {
|
||||||
|
selectedFromRelationship = fromRelationship || {}
|
||||||
|
selectedToRelationship = toRelationship || {}
|
||||||
|
relationshipModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewTable() {
|
||||||
|
createExternalTableModal.show()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={relationshipModal}>
|
||||||
|
<CreateEditRelationship
|
||||||
|
{datasource}
|
||||||
|
{save}
|
||||||
|
close={relationshipModal.hide}
|
||||||
|
{plusTables}
|
||||||
|
fromRelationship={selectedFromRelationship}
|
||||||
|
toRelationship={selectedToRelationship}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal bind:this={createExternalTableModal}>
|
||||||
|
<CreateExternalTableModal {datasource} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<div class="query-header">
|
||||||
|
<Heading size="S">Tables</Heading>
|
||||||
|
<div class="table-buttons">
|
||||||
|
<div>
|
||||||
|
<ActionButton
|
||||||
|
size="S"
|
||||||
|
quiet
|
||||||
|
icon="DataRefresh"
|
||||||
|
on:click={updateDatasourceSchema}
|
||||||
|
>
|
||||||
|
Fetch tables from database
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</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}
|
||||||
|
<div class="query-list">
|
||||||
|
{#each plusTables as table}
|
||||||
|
<div class="query-list-item" on:click={() => onClickTable(table)}>
|
||||||
|
<p class="query-name">{table.name}</p>
|
||||||
|
<p>Primary Key: {table.primary}</p>
|
||||||
|
<p>→</p>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div class="add-table">
|
||||||
|
<Button cta on:click={createNewTable}>Create new table</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if plusTables?.length !== 0}
|
||||||
|
<Divider />
|
||||||
|
<div class="query-header">
|
||||||
|
<Heading size="S">Relationships</Heading>
|
||||||
|
<ActionButton
|
||||||
|
icon="DataCorrelated"
|
||||||
|
primary
|
||||||
|
size="S"
|
||||||
|
quiet
|
||||||
|
on:click={openRelationshipModal}
|
||||||
|
>
|
||||||
|
Define existing relationship
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
<Body>
|
||||||
|
Tell budibase how your tables are related to get even more smart features.
|
||||||
|
</Body>
|
||||||
|
{/if}
|
||||||
|
<div class="query-list">
|
||||||
|
{#each Object.values(relationships) as relationship}
|
||||||
|
<div
|
||||||
|
class="query-list-item"
|
||||||
|
on:click={() => openRelationshipModal(relationship.from, relationship.to)}
|
||||||
|
>
|
||||||
|
<p class="query-name">
|
||||||
|
{buildRelationshipDisplayString(relationship.from, relationship.to)}
|
||||||
|
</p>
|
||||||
|
<p>{relationship.from?.name} to {relationship.to?.name}</p>
|
||||||
|
<p>→</p>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.query-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 0 var(--spacing-s) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-list-item {
|
||||||
|
border-radius: var(--border-radius-m);
|
||||||
|
background: var(--background);
|
||||||
|
border: var(--border-dark);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 0.75fr 20px;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: var(--spacing-m);
|
||||||
|
padding-right: var(--spacing-m);
|
||||||
|
gap: var(--layout-xs);
|
||||||
|
transition: 200ms background ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-list-item:hover {
|
||||||
|
background: var(--grey-1);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--grey-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-name {
|
||||||
|
color: var(--ink);
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-buttons div {
|
||||||
|
grid-column-end: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-table {
|
||||||
|
margin-top: var(--spacing-m);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script>
|
||||||
|
import { Divider, Heading, Button } from "@budibase/bbui"
|
||||||
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
|
|
||||||
|
export let datasource
|
||||||
|
|
||||||
|
let addHeader
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<div class="query-header">
|
||||||
|
<Heading size="S">Headers</Heading>
|
||||||
|
<Button secondary on:click={() => addHeader.addEntry()}>Add Header</Button>
|
||||||
|
</div>
|
||||||
|
<KeyValueBuilder
|
||||||
|
bind:this={addHeader}
|
||||||
|
bind:object={datasource.config.defaultHeaders}
|
||||||
|
noAddButton={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.query-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 0 var(--spacing-s) 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -3,9 +3,11 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import ICONS from "../icons"
|
import ICONS from "../icons"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import { IntegrationNames } from "constants"
|
import { IntegrationNames, IntegrationTypes } from "constants"
|
||||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||||
|
import { createRestDatasource } from "builderStore/datasource"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
export let modal
|
export let modal
|
||||||
let integrations = []
|
let integrations = []
|
||||||
|
@ -13,8 +15,6 @@
|
||||||
let internalTableModal
|
let internalTableModal
|
||||||
let externalDatasourceModal
|
let externalDatasourceModal
|
||||||
|
|
||||||
const INTERNAL = "BUDIBASE"
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
fetchIntegrations()
|
fetchIntegrations()
|
||||||
})
|
})
|
||||||
|
@ -35,10 +35,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseNextModal() {
|
async function chooseNextModal() {
|
||||||
if (integration.type === INTERNAL) {
|
if (integration.type === IntegrationTypes.INTERNAL) {
|
||||||
externalDatasourceModal.hide()
|
externalDatasourceModal.hide()
|
||||||
internalTableModal.show()
|
internalTableModal.show()
|
||||||
|
} else if (integration.type === IntegrationTypes.REST) {
|
||||||
|
// skip modal for rest, create straight away
|
||||||
|
const resp = await createRestDatasource(integration)
|
||||||
|
$goto(`./datasource/${resp._id}`)
|
||||||
} else {
|
} else {
|
||||||
externalDatasourceModal.show()
|
externalDatasourceModal.show()
|
||||||
}
|
}
|
||||||
|
@ -48,7 +52,7 @@
|
||||||
const response = await api.get("/api/integrations")
|
const response = await api.get("/api/integrations")
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
integrations = {
|
integrations = {
|
||||||
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
[IntegrationTypes.INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
||||||
...json,
|
...json,
|
||||||
}
|
}
|
||||||
return json
|
return json
|
||||||
|
@ -80,8 +84,8 @@
|
||||||
to your app using Budibase's built-in database.
|
to your app using Budibase's built-in database.
|
||||||
</Body>
|
</Body>
|
||||||
<div
|
<div
|
||||||
class:selected={integration.type === INTERNAL}
|
class:selected={integration.type === IntegrationTypes.INTERNAL}
|
||||||
on:click={() => selectIntegration(INTERNAL)}
|
on:click={() => selectIntegration(IntegrationTypes.INTERNAL)}
|
||||||
class="item hoverable"
|
class="item hoverable"
|
||||||
>
|
>
|
||||||
<div class="item-body">
|
<div class="item-body">
|
||||||
|
@ -96,7 +100,7 @@
|
||||||
<Detail size="S">Connect to data source</Detail>
|
<Detail size="S">Connect to data source</Detail>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-list">
|
<div class="item-list">
|
||||||
{#each Object.entries(integrations).filter(([key]) => key !== INTERNAL) as [integrationType, schema]}
|
{#each Object.entries(integrations).filter(([key]) => key !== IntegrationTypes.INTERNAL) as [integrationType, schema]}
|
||||||
<div
|
<div
|
||||||
class:selected={integration.type === integrationType}
|
class:selected={integration.type === integrationType}
|
||||||
on:click={() => selectIntegration(integrationType)}
|
on:click={() => selectIntegration(integrationType)}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { ModalContent, notifications, Body, Layout } from "@budibase/bbui"
|
import { ModalContent, notifications, Body, Layout } from "@budibase/bbui"
|
||||||
import analytics, { Events } from "analytics"
|
|
||||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||||
import { datasources, tables } from "stores/backend"
|
|
||||||
import { IntegrationNames } from "constants"
|
import { IntegrationNames } from "constants"
|
||||||
import cloneDeep from "lodash/cloneDeepWith"
|
import cloneDeep from "lodash/cloneDeepWith"
|
||||||
|
import { saveDatasource as save } from "builderStore/datasource"
|
||||||
|
|
||||||
export let integration
|
export let integration
|
||||||
export let modal
|
export let modal
|
||||||
|
@ -13,45 +12,13 @@
|
||||||
// kill the reference so the input isn't saved
|
// kill the reference so the input isn't saved
|
||||||
let config = cloneDeep(integration)
|
let config = cloneDeep(integration)
|
||||||
|
|
||||||
function prepareData() {
|
|
||||||
let datasource = {}
|
|
||||||
let existingTypeCount = $datasources.list.filter(
|
|
||||||
ds => ds.source == config.type
|
|
||||||
).length
|
|
||||||
|
|
||||||
let baseName = IntegrationNames[config.type]
|
|
||||||
let name =
|
|
||||||
existingTypeCount === 0
|
|
||||||
? baseName
|
|
||||||
: `${baseName}-${existingTypeCount + 1}`
|
|
||||||
|
|
||||||
datasource.type = "datasource"
|
|
||||||
datasource.source = config.type
|
|
||||||
datasource.config = config.config
|
|
||||||
datasource.name = name
|
|
||||||
datasource.plus = config.plus
|
|
||||||
|
|
||||||
return datasource
|
|
||||||
}
|
|
||||||
async function saveDatasource() {
|
async function saveDatasource() {
|
||||||
const datasource = prepareData()
|
|
||||||
try {
|
try {
|
||||||
// Create datasource
|
const resp = await save(config)
|
||||||
const resp = await datasources.save(datasource, datasource.plus)
|
|
||||||
|
|
||||||
// update the tables incase data source plus
|
|
||||||
await tables.fetch()
|
|
||||||
await datasources.select(resp._id)
|
|
||||||
$goto(`./datasource/${resp._id}`)
|
$goto(`./datasource/${resp._id}`)
|
||||||
notifications.success(`Datasource updated successfully.`)
|
notifications.success(`Datasource updated successfully.`)
|
||||||
analytics.captureEvent(Events.DATASOURCE.CREATED, {
|
|
||||||
name: resp.name,
|
|
||||||
source: resp.source,
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(`Error saving datasource: ${err}`)
|
notifications.error(`Error saving datasource: ${err}`)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -72,7 +39,11 @@
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<IntegrationConfigForm schema={config.schema} integration={config.config} />
|
<IntegrationConfigForm
|
||||||
|
schema={config.schema}
|
||||||
|
integration={config}
|
||||||
|
creating={true}
|
||||||
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
await datasources.delete(datasource)
|
await datasources.delete(datasource)
|
||||||
notifications.success("Datasource deleted")
|
notifications.success("Datasource deleted")
|
||||||
// navigate to first index page if the source you are deleting is selected
|
// navigate to first index page if the source you are deleting is selected
|
||||||
const entities = Object.values(datasource.entities)
|
const entities = Object.values(datasource?.entities || {})
|
||||||
if (
|
if (
|
||||||
wasSelectedSource === datasource._id ||
|
wasSelectedSource === datasource._id ||
|
||||||
(entities &&
|
(entities &&
|
||||||
|
|
|
@ -15,19 +15,36 @@ export const AppStatus = {
|
||||||
DEPLOYED: "published",
|
DEPLOYED: "published",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IntegrationNames = {
|
export const IntegrationTypes = {
|
||||||
POSTGRES: "PostgreSQL",
|
POSTGRES: "POSTGRES",
|
||||||
MONGODB: "MongoDB",
|
MONGODB: "MONGODB",
|
||||||
COUCHDB: "CouchDB",
|
COUCHDB: "COUCHDB",
|
||||||
S3: "S3",
|
S3: "S3",
|
||||||
MYSQL: "MySQL",
|
MYSQL: "MYSQL",
|
||||||
REST: "REST",
|
REST: "REST",
|
||||||
DYNAMODB: "DynamoDB",
|
DYNAMODB: "DYNAMODB",
|
||||||
ELASTICSEARCH: "ElasticSearch",
|
ELASTICSEARCH: "ELASTICSEARCH",
|
||||||
SQL_SERVER: "SQL Server",
|
SQL_SERVER: "SQL_SERVER",
|
||||||
AIRTABLE: "Airtable",
|
AIRTABLE: "AIRTABLE",
|
||||||
ARANGODB: "ArangoDB",
|
ARANGODB: "ARANGODB",
|
||||||
ORACLE: "Oracle",
|
ORACLE: "ORACLE",
|
||||||
|
INTERNAL: "INTERNAL",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IntegrationNames = {
|
||||||
|
[IntegrationTypes.POSTGRES]: "PostgreSQL",
|
||||||
|
[IntegrationTypes.MONGODB]: "MongoDB",
|
||||||
|
[IntegrationTypes.COUCHDB]: "CouchDB",
|
||||||
|
[IntegrationTypes.S3]: "S3",
|
||||||
|
[IntegrationTypes.MYSQL]: "MySQL",
|
||||||
|
[IntegrationTypes.REST]: "REST",
|
||||||
|
[IntegrationTypes.DYNAMODB]: "DynamoDB",
|
||||||
|
[IntegrationTypes.ELASTICSEARCH]: "ElasticSearch",
|
||||||
|
[IntegrationTypes.SQL_SERVER]: "SQL Server",
|
||||||
|
[IntegrationTypes.AIRTABLE]: "Airtable",
|
||||||
|
[IntegrationTypes.ARANGODB]: "ArangoDB",
|
||||||
|
[IntegrationTypes.ORACLE]: "Oracle",
|
||||||
|
[IntegrationTypes.INTERNAL]: "Internal",
|
||||||
}
|
}
|
||||||
|
|
||||||
// fields on the user table that cannot be edited
|
// fields on the user table that cannot be edited
|
||||||
|
|
|
@ -6,74 +6,18 @@
|
||||||
Body,
|
Body,
|
||||||
Divider,
|
Divider,
|
||||||
Layout,
|
Layout,
|
||||||
Modal,
|
notifications,
|
||||||
InlineAlert,
|
|
||||||
ActionButton,
|
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { datasources, integrations, queries, tables } from "stores/backend"
|
import { datasources, integrations, queries, tables } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||||
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
|
import RestExtraConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/RestExtraConfigForm.svelte"
|
||||||
import CreateExternalTableModal from "./modals/CreateExternalTableModal.svelte"
|
import PlusConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/PlusConfigForm.svelte"
|
||||||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||||
|
import { IntegrationTypes } from "constants"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
|
|
||||||
let relationshipModal
|
|
||||||
let createExternalTableModal
|
|
||||||
let selectedFromRelationship, selectedToRelationship
|
|
||||||
|
|
||||||
$: datasource = $datasources.list.find(ds => ds._id === $datasources.selected)
|
$: datasource = $datasources.list.find(ds => ds._id === $datasources.selected)
|
||||||
$: integration = datasource && $integrations[datasource.source]
|
$: integration = datasource && $integrations[datasource.source]
|
||||||
$: plusTables = datasource?.plus
|
|
||||||
? Object.values(datasource.entities || {})
|
|
||||||
: []
|
|
||||||
$: relationships = getRelationships(plusTables)
|
|
||||||
$: schemaError = $datasources.schemaError
|
|
||||||
|
|
||||||
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} through ${throughTableName} → ${toTableName}`
|
|
||||||
} else {
|
|
||||||
displayString = `${fromTableName} → ${toTableName}`
|
|
||||||
}
|
|
||||||
return displayString
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveDatasource() {
|
async function saveDatasource() {
|
||||||
try {
|
try {
|
||||||
|
@ -89,52 +33,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDatasourceSchema() {
|
|
||||||
try {
|
|
||||||
await datasources.updateSchema(datasource)
|
|
||||||
notifications.success(`Datasource ${name} tables updated successfully.`)
|
|
||||||
await tables.fetch()
|
|
||||||
} catch (err) {
|
|
||||||
notifications.error(`Error updating datasource schema: ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClickQuery(query) {
|
function onClickQuery(query) {
|
||||||
queries.select(query)
|
queries.select(query)
|
||||||
$goto(`./${query._id}`)
|
$goto(`./${query._id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClickTable(table) {
|
|
||||||
tables.select(table)
|
|
||||||
$goto(`../../table/${table._id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function openRelationshipModal(fromRelationship, toRelationship) {
|
|
||||||
selectedFromRelationship = fromRelationship || {}
|
|
||||||
selectedToRelationship = toRelationship || {}
|
|
||||||
relationshipModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
function createNewTable() {
|
|
||||||
createExternalTableModal.show()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={relationshipModal}>
|
|
||||||
<CreateEditRelationship
|
|
||||||
{datasource}
|
|
||||||
save={saveDatasource}
|
|
||||||
close={relationshipModal.hide}
|
|
||||||
{plusTables}
|
|
||||||
fromRelationship={selectedFromRelationship}
|
|
||||||
toRelationship={selectedToRelationship}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal bind:this={createExternalTableModal}>
|
|
||||||
<CreateExternalTableModal {datasource} />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{#if datasource && integration}
|
{#if datasource && integration}
|
||||||
<section>
|
<section>
|
||||||
<Layout>
|
<Layout>
|
||||||
|
@ -156,92 +60,15 @@
|
||||||
<Button secondary on:click={saveDatasource}>Save</Button>
|
<Button secondary on:click={saveDatasource}>Save</Button>
|
||||||
</div>
|
</div>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Connect your database to Budibase using the config below.
|
Connect your data source to Budibase using the config below.
|
||||||
</Body>
|
</Body>
|
||||||
<IntegrationConfigForm
|
<IntegrationConfigForm
|
||||||
schema={integration.datasource}
|
schema={integration.datasource}
|
||||||
integration={datasource.config}
|
integration={datasource}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if datasource.plus}
|
{#if datasource.plus}
|
||||||
<Divider />
|
<PlusConfigForm bind:datasource save={saveDatasource} />
|
||||||
<div class="query-header">
|
|
||||||
<Heading size="S">Tables</Heading>
|
|
||||||
<div class="table-buttons">
|
|
||||||
<div>
|
|
||||||
<ActionButton
|
|
||||||
size="S"
|
|
||||||
quiet
|
|
||||||
icon="DataRefresh"
|
|
||||||
on:click={updateDatasourceSchema}
|
|
||||||
>
|
|
||||||
Fetch tables from database
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</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}
|
|
||||||
<div class="query-list">
|
|
||||||
{#each plusTables as table}
|
|
||||||
<div class="query-list-item" on:click={() => onClickTable(table)}>
|
|
||||||
<p class="query-name">{table.name}</p>
|
|
||||||
<p>Primary Key: {table.primary}</p>
|
|
||||||
<p>→</p>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
<div class="add-table">
|
|
||||||
<Button cta on:click={createNewTable}>Create new table</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if plusTables?.length !== 0}
|
|
||||||
<Divider />
|
|
||||||
<div class="query-header">
|
|
||||||
<Heading size="S">Relationships</Heading>
|
|
||||||
<ActionButton
|
|
||||||
icon="DataCorrelated"
|
|
||||||
primary
|
|
||||||
size="S"
|
|
||||||
quiet
|
|
||||||
on:click={openRelationshipModal}
|
|
||||||
>
|
|
||||||
Define existing relationship
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
<Body>
|
|
||||||
Tell budibase how your tables are related to get even more smart
|
|
||||||
features.
|
|
||||||
</Body>
|
|
||||||
{/if}
|
|
||||||
<div class="query-list">
|
|
||||||
{#each Object.values(relationships) as relationship}
|
|
||||||
<div
|
|
||||||
class="query-list-item"
|
|
||||||
on:click={() =>
|
|
||||||
openRelationshipModal(relationship.from, relationship.to)}
|
|
||||||
>
|
|
||||||
<p class="query-name">
|
|
||||||
{buildRelationshipDisplayString(
|
|
||||||
relationship.from,
|
|
||||||
relationship.to
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p>{relationship.from?.name} to {relationship.to?.name}</p>
|
|
||||||
<p>→</p>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
<Divider />
|
<Divider />
|
||||||
<div class="query-header">
|
<div class="query-header">
|
||||||
|
@ -257,6 +84,9 @@
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{#if datasource?.source === IntegrationTypes.REST}
|
||||||
|
<RestExtraConfigForm bind:datasource />
|
||||||
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -329,17 +159,4 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-buttons {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-buttons div {
|
|
||||||
grid-column-end: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-table {
|
|
||||||
margin-top: var(--spacing-m);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -23,8 +23,9 @@ module RestModule {
|
||||||
datasource: {
|
datasource: {
|
||||||
url: {
|
url: {
|
||||||
type: DatasourceFieldTypes.STRING,
|
type: DatasourceFieldTypes.STRING,
|
||||||
default: "localhost",
|
default: "",
|
||||||
required: true,
|
required: false,
|
||||||
|
deprecated: true,
|
||||||
},
|
},
|
||||||
defaultHeaders: {
|
defaultHeaders: {
|
||||||
type: DatasourceFieldTypes.OBJECT,
|
type: DatasourceFieldTypes.OBJECT,
|
||||||
|
|
Loading…
Reference in New Issue