Updating REST creation, removing the modal step, updating the config page to move things around as per designs.

This commit is contained in:
mike12345567 2021-11-30 16:21:16 +00:00
parent 40aa9656e1
commit 3e5f9b9505
13 changed files with 413 additions and 257 deletions

View File

@ -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)
}

View File

@ -9,16 +9,40 @@
} from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import { capitalise } from "helpers"
import { IntegrationTypes } from "constants"
export let integration
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
</script>
<form>
<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"}
<div class="form-row ssl">
<Label>{capitalise(configKey)}</Label>
@ -29,13 +53,13 @@
<KeyValueBuilder
bind:this={addButton}
defaults={schema[configKey].default}
bind:object={integration[configKey]}
bind:object={config[configKey]}
noAddButton={true}
/>
{:else if schema[configKey].type === "boolean"}
<div class="form-row">
<Label>{capitalise(configKey)}</Label>
<Toggle text="" bind:value={integration[configKey]} />
<Toggle text="" bind:value={config[configKey]} />
</div>
{:else if schema[configKey].type === "longForm"}
<div class="form-row">
@ -43,7 +67,7 @@
<TextArea
type={schema[configKey].type}
on:change
bind:value={integration[configKey]}
bind:value={config[configKey]}
/>
</div>
{:else}
@ -52,7 +76,7 @@
<Input
type={schema[configKey].type}
on:change
bind:value={integration[configKey]}
bind:value={config[configKey]}
/>
</div>
{/if}

View File

@ -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>

View File

@ -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>

View File

@ -3,9 +3,11 @@
import { onMount } from "svelte"
import ICONS from "../icons"
import api from "builderStore/api"
import { IntegrationNames } from "constants"
import { IntegrationNames, IntegrationTypes } from "constants"
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
import { createRestDatasource } from "builderStore/datasource"
import { goto } from "@roxi/routify"
export let modal
let integrations = []
@ -13,8 +15,6 @@
let internalTableModal
let externalDatasourceModal
const INTERNAL = "BUDIBASE"
onMount(() => {
fetchIntegrations()
})
@ -35,10 +35,14 @@
}
}
function chooseNextModal() {
if (integration.type === INTERNAL) {
async function chooseNextModal() {
if (integration.type === IntegrationTypes.INTERNAL) {
externalDatasourceModal.hide()
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 {
externalDatasourceModal.show()
}
@ -48,7 +52,7 @@
const response = await api.get("/api/integrations")
const json = await response.json()
integrations = {
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
[IntegrationTypes.INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
...json,
}
return json
@ -80,8 +84,8 @@
to your app using Budibase's built-in database.
</Body>
<div
class:selected={integration.type === INTERNAL}
on:click={() => selectIntegration(INTERNAL)}
class:selected={integration.type === IntegrationTypes.INTERNAL}
on:click={() => selectIntegration(IntegrationTypes.INTERNAL)}
class="item hoverable"
>
<div class="item-body">
@ -96,7 +100,7 @@
<Detail size="S">Connect to data source</Detail>
</div>
<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
class:selected={integration.type === integrationType}
on:click={() => selectIntegration(integrationType)}

View File

@ -1,11 +1,10 @@
<script>
import { goto } from "@roxi/routify"
import { ModalContent, notifications, Body, Layout } from "@budibase/bbui"
import analytics, { Events } from "analytics"
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
import { datasources, tables } from "stores/backend"
import { IntegrationNames } from "constants"
import cloneDeep from "lodash/cloneDeepWith"
import { saveDatasource as save } from "builderStore/datasource"
export let integration
export let modal
@ -13,45 +12,13 @@
// kill the reference so the input isn't saved
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() {
const datasource = prepareData()
try {
// 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)
const resp = await save(config)
$goto(`./datasource/${resp._id}`)
notifications.success(`Datasource updated successfully.`)
analytics.captureEvent(Events.DATASOURCE.CREATED, {
name: resp.name,
source: resp.source,
})
return true
} catch (err) {
notifications.error(`Error saving datasource: ${err}`)
return false
}
}
</script>
@ -72,7 +39,11 @@
</Body>
</Layout>
<IntegrationConfigForm schema={config.schema} integration={config.config} />
<IntegrationConfigForm
schema={config.schema}
integration={config}
creating={true}
/>
</ModalContent>
<style>

View File

@ -17,7 +17,7 @@
await datasources.delete(datasource)
notifications.success("Datasource deleted")
// navigate to first index page if the source you are deleting is selected
const entities = Object.values(datasource.entities)
const entities = Object.values(datasource?.entities || {})
if (
wasSelectedSource === datasource._id ||
(entities &&

View File

@ -15,19 +15,36 @@ export const AppStatus = {
DEPLOYED: "published",
}
export const IntegrationNames = {
POSTGRES: "PostgreSQL",
MONGODB: "MongoDB",
COUCHDB: "CouchDB",
export const IntegrationTypes = {
POSTGRES: "POSTGRES",
MONGODB: "MONGODB",
COUCHDB: "COUCHDB",
S3: "S3",
MYSQL: "MySQL",
MYSQL: "MYSQL",
REST: "REST",
DYNAMODB: "DynamoDB",
ELASTICSEARCH: "ElasticSearch",
SQL_SERVER: "SQL Server",
AIRTABLE: "Airtable",
ARANGODB: "ArangoDB",
ORACLE: "Oracle",
DYNAMODB: "DYNAMODB",
ELASTICSEARCH: "ELASTICSEARCH",
SQL_SERVER: "SQL_SERVER",
AIRTABLE: "AIRTABLE",
ARANGODB: "ARANGODB",
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

View File

@ -6,74 +6,18 @@
Body,
Divider,
Layout,
Modal,
InlineAlert,
ActionButton,
notifications,
} from "@budibase/bbui"
import { datasources, integrations, queries, tables } from "stores/backend"
import { notifications } from "@budibase/bbui"
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
import CreateExternalTableModal from "./modals/CreateExternalTableModal.svelte"
import RestExtraConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/RestExtraConfigForm.svelte"
import PlusConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/PlusConfigForm.svelte"
import ICONS from "components/backend/DatasourceNavigator/icons"
import { IntegrationTypes } from "constants"
import { capitalise } from "helpers"
let relationshipModal
let createExternalTableModal
let selectedFromRelationship, selectedToRelationship
$: datasource = $datasources.list.find(ds => ds._id === $datasources.selected)
$: 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() {
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) {
queries.select(query)
$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>
<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}
<section>
<Layout>
@ -156,92 +60,15 @@
<Button secondary on:click={saveDatasource}>Save</Button>
</div>
<Body size="S">
Connect your database to Budibase using the config below.
Connect your data source to Budibase using the config below.
</Body>
<IntegrationConfigForm
schema={integration.datasource}
integration={datasource.config}
integration={datasource}
/>
</div>
{#if datasource.plus}
<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>
<PlusConfigForm bind:datasource save={saveDatasource} />
{/if}
<Divider />
<div class="query-header">
@ -257,6 +84,9 @@
</div>
{/each}
</div>
{#if datasource?.source === IntegrationTypes.REST}
<RestExtraConfigForm bind:datasource />
{/if}
</Layout>
</section>
{/if}
@ -329,17 +159,4 @@
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>

View File

@ -23,8 +23,9 @@ module RestModule {
datasource: {
url: {
type: DatasourceFieldTypes.STRING,
default: "localhost",
required: true,
default: "",
required: false,
deprecated: true,
},
defaultHeaders: {
type: DatasourceFieldTypes.OBJECT,