Merge pull request #11040 from Budibase/reformat-datasource-options-v2

Update datasource config to use tabs
This commit is contained in:
Andrew Kingston 2023-06-27 16:09:30 +01:00 committed by GitHub
commit b10bb67ee6
26 changed files with 809 additions and 686 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -99,11 +99,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,
}