Merge pull request #11040 from Budibase/reformat-datasource-options-v2
Update datasource config to use tabs
This commit is contained in:
commit
b10bb67ee6
|
@ -1,18 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { get } from "svelte/store"
|
import { ActionButton, notifications } from "@budibase/bbui"
|
||||||
import { ActionButton, Modal, notifications } from "@budibase/bbui"
|
import CreateEditRelationshipModal from "../../Datasources/CreateEditRelationshipModal.svelte"
|
||||||
import CreateEditRelationship from "../../Datasources/CreateEditRelationship.svelte"
|
import { datasources } from "../../../../stores/backend"
|
||||||
import { datasources, integrations } from "../../../../stores/backend"
|
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { integrationForDatasource } from "stores/selectors"
|
|
||||||
|
|
||||||
export let table
|
export let table
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: datasource = findDatasource(table?._id)
|
$: datasource = findDatasource(table?._id)
|
||||||
$: plusTables = datasource?.plus
|
$: tables = datasource?.plus ? Object.values(datasource?.entities || {}) : []
|
||||||
? Object.values(datasource?.entities || {})
|
|
||||||
: []
|
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
|
@ -26,34 +22,32 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveRelationship() {
|
const afterSave = ({ action }) => {
|
||||||
try {
|
notifications.success(`Relationship ${action} successfully`)
|
||||||
// Create datasource
|
dispatch("updatecolumns")
|
||||||
await datasources.update({
|
}
|
||||||
datasource,
|
|
||||||
integration: integrationForDatasource(get(integrations), datasource),
|
const onError = err => {
|
||||||
})
|
notifications.error(`Error saving relationship info: ${err}`)
|
||||||
notifications.success(`Relationship information saved.`)
|
|
||||||
dispatch("updatecolumns")
|
|
||||||
} catch (err) {
|
|
||||||
notifications.error(`Error saving relationship info: ${err}`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if datasource}
|
{#if datasource}
|
||||||
<div>
|
<div>
|
||||||
<ActionButton icon="DataCorrelated" primary quiet on:click={modal.show}>
|
<ActionButton
|
||||||
|
icon="DataCorrelated"
|
||||||
|
primary
|
||||||
|
quiet
|
||||||
|
on:click={() => modal.show({ fromTable: table })}
|
||||||
|
>
|
||||||
Define relationship
|
Define relationship
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
<Modal bind:this={modal}>
|
<CreateEditRelationshipModal
|
||||||
<CreateEditRelationship
|
bind:this={modal}
|
||||||
{datasource}
|
{datasource}
|
||||||
save={saveRelationship}
|
{tables}
|
||||||
close={modal.hide}
|
{afterSave}
|
||||||
{plusTables}
|
{onError}
|
||||||
selectedFromTable={table}
|
/>
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,249 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
Heading,
|
|
||||||
Body,
|
|
||||||
Divider,
|
|
||||||
InlineAlert,
|
|
||||||
Button,
|
|
||||||
notifications,
|
|
||||||
Modal,
|
|
||||||
Table,
|
|
||||||
FancyCheckboxGroup,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { datasources, integrations, tables } from "stores/backend"
|
|
||||||
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
|
|
||||||
import CreateExternalTableModal from "./CreateExternalTableModal.svelte"
|
|
||||||
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
|
|
||||||
export let datasource
|
|
||||||
export let save
|
|
||||||
|
|
||||||
let tableSchema = {
|
|
||||||
name: {},
|
|
||||||
primary: { displayName: "Primary Key" },
|
|
||||||
}
|
|
||||||
let relationshipSchema = {
|
|
||||||
tables: {},
|
|
||||||
columns: {},
|
|
||||||
}
|
|
||||||
let relationshipModal
|
|
||||||
let createExternalTableModal
|
|
||||||
let selectedFromRelationship, selectedToRelationship
|
|
||||||
let confirmDialog
|
|
||||||
let specificTables = null
|
|
||||||
let tableList
|
|
||||||
|
|
||||||
$: integration = datasource && $integrations[datasource.source]
|
|
||||||
$: plusTables = datasource?.plus
|
|
||||||
? Object.values(datasource?.entities || {})
|
|
||||||
: []
|
|
||||||
$: relationships = getRelationships(plusTables)
|
|
||||||
$: schemaError = $datasources.schemaError
|
|
||||||
$: relationshipInfo = relationshipTableData(relationships)
|
|
||||||
|
|
||||||
function getRelationships(tables) {
|
|
||||||
if (!tables || !Array.isArray(tables)) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
let pairs = {}
|
|
||||||
for (let table of tables) {
|
|
||||||
for (let column of Object.values(table.schema)) {
|
|
||||||
if (column.type !== "link") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// these relationships have an id to pair them to each other
|
|
||||||
// one has a main for the from side
|
|
||||||
const key = column.main ? "from" : "to"
|
|
||||||
pairs[column._id] = {
|
|
||||||
...pairs[column._id],
|
|
||||||
[key]: column,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pairs
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildRelationshipDisplayString(fromCol, toCol) {
|
|
||||||
function getTableName(tableId) {
|
|
||||||
if (!tableId || typeof tableId !== "string") {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return plusTables.find(table => table._id === tableId)?.name || "Unknown"
|
|
||||||
}
|
|
||||||
if (!toCol || !fromCol) {
|
|
||||||
return "Cannot build name"
|
|
||||||
}
|
|
||||||
const fromTableName = getTableName(toCol.tableId)
|
|
||||||
const toTableName = getTableName(fromCol.tableId)
|
|
||||||
const throughTableName = getTableName(fromCol.through)
|
|
||||||
|
|
||||||
let displayString
|
|
||||||
if (throughTableName) {
|
|
||||||
displayString = `${fromTableName} ↔ ${toTableName}`
|
|
||||||
} else {
|
|
||||||
displayString = `${fromTableName} → ${toTableName}`
|
|
||||||
}
|
|
||||||
return displayString
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateDatasourceSchema() {
|
|
||||||
try {
|
|
||||||
await datasources.updateSchema(datasource, specificTables)
|
|
||||||
notifications.success(`Datasource ${name} tables updated successfully.`)
|
|
||||||
await tables.fetch()
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error(
|
|
||||||
`Error updating datasource schema ${
|
|
||||||
error?.message ? `: ${error.message}` : ""
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClickTable(table) {
|
|
||||||
$goto(`../../table/${table._id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function openRelationshipModal(fromRelationship, toRelationship) {
|
|
||||||
selectedFromRelationship = fromRelationship || {}
|
|
||||||
selectedToRelationship = toRelationship || {}
|
|
||||||
relationshipModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
function createNewTable() {
|
|
||||||
createExternalTableModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
function relationshipTableData(relations) {
|
|
||||||
return Object.values(relations).map(relationship => ({
|
|
||||||
tables: buildRelationshipDisplayString(
|
|
||||||
relationship.from,
|
|
||||||
relationship.to
|
|
||||||
),
|
|
||||||
columns: `${relationship.from?.name} to ${relationship.to?.name}`,
|
|
||||||
from: relationship.from,
|
|
||||||
to: relationship.to,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal bind:this={relationshipModal}>
|
|
||||||
<CreateEditRelationship
|
|
||||||
{datasource}
|
|
||||||
{save}
|
|
||||||
close={relationshipModal.hide}
|
|
||||||
{plusTables}
|
|
||||||
fromRelationship={selectedFromRelationship}
|
|
||||||
toRelationship={selectedToRelationship}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal bind:this={createExternalTableModal}>
|
|
||||||
<CreateExternalTableModal {datasource} />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
bind:this={confirmDialog}
|
|
||||||
okText="Fetch tables"
|
|
||||||
onOk={updateDatasourceSchema}
|
|
||||||
onCancel={() => confirmDialog.hide()}
|
|
||||||
warning={false}
|
|
||||||
title="Confirm table fetch"
|
|
||||||
>
|
|
||||||
<Body>
|
|
||||||
If you have fetched tables from this database before, this action may
|
|
||||||
overwrite any changes you made after your initial fetch.
|
|
||||||
</Body>
|
|
||||||
<br />
|
|
||||||
<div class="table-checkboxes">
|
|
||||||
<FancyCheckboxGroup options={tableList} bind:selected={specificTables} />
|
|
||||||
</div>
|
|
||||||
</ConfirmDialog>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
<div class="query-header">
|
|
||||||
<Heading size="S">Tables</Heading>
|
|
||||||
<div class="table-buttons">
|
|
||||||
<Button
|
|
||||||
secondary
|
|
||||||
on:click={async () => {
|
|
||||||
tableList = await datasources.getTableNames(datasource)
|
|
||||||
confirmDialog.show()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Fetch tables
|
|
||||||
</Button>
|
|
||||||
<Button cta icon="Add" on:click={createNewTable}>New table</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Body>
|
|
||||||
This datasource can determine tables automatically. Budibase can fetch your
|
|
||||||
tables directly from the database and you can use them without having to write
|
|
||||||
any queries at all.
|
|
||||||
</Body>
|
|
||||||
{#if schemaError}
|
|
||||||
<InlineAlert
|
|
||||||
type="error"
|
|
||||||
header="Error fetching tables"
|
|
||||||
message={schemaError}
|
|
||||||
onConfirm={datasources.removeSchemaError}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#if plusTables && Object.values(plusTables).length > 0}
|
|
||||||
<Table
|
|
||||||
on:click={({ detail }) => onClickTable(detail)}
|
|
||||||
schema={tableSchema}
|
|
||||||
data={Object.values(plusTables)}
|
|
||||||
allowEditColumns={false}
|
|
||||||
allowEditRows={false}
|
|
||||||
allowSelectRows={false}
|
|
||||||
customRenderers={[{ column: "primary", component: ArrayRenderer }]}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<Body size="S"><i>No tables found.</i></Body>
|
|
||||||
{/if}
|
|
||||||
{#if integration.relationships !== false}
|
|
||||||
<Divider />
|
|
||||||
<div class="query-header">
|
|
||||||
<Heading size="S">Relationships</Heading>
|
|
||||||
<Button primary on:click={() => openRelationshipModal()}>
|
|
||||||
Define relationship
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Body>
|
|
||||||
Tell budibase how your tables are related to get even more smart features.
|
|
||||||
</Body>
|
|
||||||
{#if relationshipInfo && relationshipInfo.length > 0}
|
|
||||||
<Table
|
|
||||||
on:click={({ detail }) => openRelationshipModal(detail.from, detail.to)}
|
|
||||||
schema={relationshipSchema}
|
|
||||||
data={relationshipInfo}
|
|
||||||
allowEditColumns={false}
|
|
||||||
allowEditRows={false}
|
|
||||||
allowSelectRows={false}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<Body size="S"><i>No relationships configured.</i></Body>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.query-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0 0 var(--spacing-s) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-checkboxes {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,86 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Body, notifications } from "@budibase/bbui"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { API } from "api"
|
|
||||||
import ICONS from "../icons"
|
|
||||||
|
|
||||||
export let integration = {}
|
|
||||||
let integrations = []
|
|
||||||
const INTERNAL = "BUDIBASE"
|
|
||||||
|
|
||||||
async function fetchIntegrations() {
|
|
||||||
let otherIntegrations
|
|
||||||
try {
|
|
||||||
otherIntegrations = await API.getIntegrations()
|
|
||||||
} catch (error) {
|
|
||||||
otherIntegrations = {}
|
|
||||||
notifications.error("Error getting integrations")
|
|
||||||
}
|
|
||||||
integrations = {
|
|
||||||
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
|
||||||
...otherIntegrations,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectIntegration(integrationType) {
|
|
||||||
const selected = integrations[integrationType]
|
|
||||||
|
|
||||||
// build the schema
|
|
||||||
const schema = {}
|
|
||||||
for (let key of Object.keys(selected.datasource)) {
|
|
||||||
schema[key] = selected.datasource[key].default
|
|
||||||
}
|
|
||||||
|
|
||||||
integration = {
|
|
||||||
type: integrationType,
|
|
||||||
plus: selected.plus,
|
|
||||||
...schema,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
fetchIntegrations()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<div class="integration-list">
|
|
||||||
{#each Object.entries(integrations) as [integrationType, schema]}
|
|
||||||
<div
|
|
||||||
class="integration hoverable"
|
|
||||||
class:selected={integration.type === integrationType}
|
|
||||||
on:click={() => selectIntegration(integrationType)}
|
|
||||||
>
|
|
||||||
<svelte:component
|
|
||||||
this={ICONS[integrationType]}
|
|
||||||
height="50"
|
|
||||||
width="50"
|
|
||||||
/>
|
|
||||||
<Body size="XS">{schema.name || integrationType}</Body>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.integration-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
||||||
grid-gap: var(--spectrum-alias-grid-baseline);
|
|
||||||
}
|
|
||||||
|
|
||||||
.integration {
|
|
||||||
display: grid;
|
|
||||||
background: var(--background-alt);
|
|
||||||
place-items: center;
|
|
||||||
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
|
||||||
padding: var(--spectrum-alias-item-padding-s);
|
|
||||||
transition: 0.3s all;
|
|
||||||
border-radius: var(--spectrum-alias-item-rounded-border-radius-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.integration:hover,
|
|
||||||
.selected {
|
|
||||||
background: var(--spectrum-alias-background-color-tertiary);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,137 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
Divider,
|
|
||||||
Heading,
|
|
||||||
ActionButton,
|
|
||||||
Badge,
|
|
||||||
Body,
|
|
||||||
Layout,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
|
||||||
import RestAuthenticationBuilder from "./auth/RestAuthenticationBuilder.svelte"
|
|
||||||
import ViewDynamicVariables from "./variables/ViewDynamicVariables.svelte"
|
|
||||||
import {
|
|
||||||
getRestBindings,
|
|
||||||
getEnvironmentBindings,
|
|
||||||
readableToRuntimeBinding,
|
|
||||||
runtimeToReadableMap,
|
|
||||||
} from "builderStore/dataBinding"
|
|
||||||
import { cloneDeep } from "lodash/fp"
|
|
||||||
import { licensing } from "stores/portal"
|
|
||||||
|
|
||||||
export let datasource
|
|
||||||
export let queries
|
|
||||||
|
|
||||||
let addHeader
|
|
||||||
|
|
||||||
let parsedHeaders = runtimeToReadableMap(
|
|
||||||
getRestBindings(),
|
|
||||||
cloneDeep(datasource?.config?.defaultHeaders)
|
|
||||||
)
|
|
||||||
|
|
||||||
const onDefaultHeaderUpdate = headers => {
|
|
||||||
const flatHeaders = cloneDeep(headers).reduce((acc, entry) => {
|
|
||||||
acc[entry.name] = readableToRuntimeBinding(getRestBindings(), entry.value)
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
datasource.config.defaultHeaders = flatHeaders
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="badge">
|
|
||||||
<Heading size="S">Headers</Heading>
|
|
||||||
<Badge quiet grey>Optional</Badge>
|
|
||||||
</div>
|
|
||||||
<div class="headerRight">
|
|
||||||
<slot name="headerRight" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Body size="S">
|
|
||||||
Headers enable you to provide additional information about the request, such
|
|
||||||
as format.
|
|
||||||
</Body>
|
|
||||||
<KeyValueBuilder
|
|
||||||
bind:this={addHeader}
|
|
||||||
bind:object={parsedHeaders}
|
|
||||||
on:change={evt => onDefaultHeaderUpdate(evt.detail)}
|
|
||||||
noAddButton
|
|
||||||
bindings={getRestBindings()}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<ActionButton icon="Add" on:click={() => addHeader.addEntry()}>
|
|
||||||
Add header
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="badge">
|
|
||||||
<Heading size="S">Authentication</Heading>
|
|
||||||
<Badge quiet grey>Optional</Badge>
|
|
||||||
</div>
|
|
||||||
<div class="headerRight">
|
|
||||||
<slot name="headerRight" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Body size="S">
|
|
||||||
Create an authentication config that can be shared with queries.
|
|
||||||
</Body>
|
|
||||||
<RestAuthenticationBuilder bind:configs={datasource.config.authConfigs} />
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="badge">
|
|
||||||
<Heading size="S">Variables</Heading>
|
|
||||||
<Badge quiet grey>Optional</Badge>
|
|
||||||
</div>
|
|
||||||
<div class="headerRight">
|
|
||||||
<slot name="headerRight" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Body size="S"
|
|
||||||
>Variables enable you to store and re-use values in queries, with the choice
|
|
||||||
of a static value such as a token using static variables, or a value from a
|
|
||||||
query response using dynamic variables.</Body
|
|
||||||
>
|
|
||||||
<Heading size="XS">Static</Heading>
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
<KeyValueBuilder
|
|
||||||
name="Variable"
|
|
||||||
keyPlaceholder="Name"
|
|
||||||
headings
|
|
||||||
bind:object={datasource.config.staticVariables}
|
|
||||||
on:change
|
|
||||||
bindings={$licensing.environmentVariablesEnabled
|
|
||||||
? getEnvironmentBindings()
|
|
||||||
: []}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
<div />
|
|
||||||
<Heading size="XS">Dynamic</Heading>
|
|
||||||
<Body size="S">
|
|
||||||
Dynamic variables are evaluated when a dependant query is executed. The value
|
|
||||||
is cached for a period of time and will be refreshed if a query fails.
|
|
||||||
</Body>
|
|
||||||
<ViewDynamicVariables {queries} {datasource} />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.section-header {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerRight {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
<script>
|
||||||
|
import { Modal } from "@budibase/bbui"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import CreateEditRelationship from "./CreateEditRelationship.svelte"
|
||||||
|
import { integrations, datasources } from "stores/backend"
|
||||||
|
import { integrationForDatasource } from "stores/selectors"
|
||||||
|
|
||||||
|
export let datasource
|
||||||
|
export let tables
|
||||||
|
export let beforeSave = async () => {}
|
||||||
|
export let afterSave = async () => {}
|
||||||
|
export let onError = async () => {}
|
||||||
|
|
||||||
|
let relationshipModal
|
||||||
|
let fromRelationship = {}
|
||||||
|
let toRelationship = {}
|
||||||
|
let fromTable = null
|
||||||
|
|
||||||
|
export function show({
|
||||||
|
fromRelationship: selectedFromRelationship = {},
|
||||||
|
toRelationship: selectedToRelationship = {},
|
||||||
|
fromTable: selectedFromTable = null,
|
||||||
|
}) {
|
||||||
|
fromRelationship = selectedFromRelationship
|
||||||
|
toRelationship = selectedToRelationship
|
||||||
|
fromTable = selectedFromTable
|
||||||
|
|
||||||
|
relationshipModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hide() {
|
||||||
|
relationshipModal.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
// action is one of 'created', 'updated' or 'deleted'
|
||||||
|
async function saveRelationship(action) {
|
||||||
|
try {
|
||||||
|
await beforeSave({ action, datasource })
|
||||||
|
|
||||||
|
const integration = integrationForDatasource(
|
||||||
|
get(integrations),
|
||||||
|
datasource
|
||||||
|
)
|
||||||
|
await datasources.update({ datasource, integration })
|
||||||
|
|
||||||
|
await afterSave({ datasource, action })
|
||||||
|
} catch (err) {
|
||||||
|
await onError({ err, datasource, action })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={relationshipModal}>
|
||||||
|
<CreateEditRelationship
|
||||||
|
save={saveRelationship}
|
||||||
|
close={relationshipModal.hide}
|
||||||
|
selectedFromTable={fromTable}
|
||||||
|
{datasource}
|
||||||
|
plusTables={tables}
|
||||||
|
{fromRelationship}
|
||||||
|
{toRelationship}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
|
@ -33,7 +33,7 @@
|
||||||
// A REST integration is created immediately, we don't need to display a config modal.
|
// A REST integration is created immediately, we don't need to display a config modal.
|
||||||
loading = true
|
loading = true
|
||||||
datasources
|
datasources
|
||||||
.create({ integration, fields: configFromIntegration(integration) })
|
.create({ integration, config: configFromIntegration(integration) })
|
||||||
.then(datasource => {
|
.then(datasource => {
|
||||||
store.setIntegration(integration)
|
store.setIntegration(integration)
|
||||||
store.setDatasource(datasource)
|
store.setDatasource(datasource)
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
import AuthTypeRenderer from "./AuthTypeRenderer.svelte"
|
import AuthTypeRenderer from "./AuthTypeRenderer.svelte"
|
||||||
import RestAuthenticationModal from "./RestAuthenticationModal.svelte"
|
import RestAuthenticationModal from "./RestAuthenticationModal.svelte"
|
||||||
import { Helpers } from "@budibase/bbui"
|
import { Helpers } from "@budibase/bbui"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let configs = []
|
export let authConfigs = []
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
let currentConfig = null
|
let currentConfig = null
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
|
@ -20,8 +22,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const onConfirm = config => {
|
const onConfirm = config => {
|
||||||
|
let newAuthConfigs
|
||||||
|
|
||||||
if (currentConfig) {
|
if (currentConfig) {
|
||||||
configs = configs.map(c => {
|
newAuthConfigs = authConfigs.map(c => {
|
||||||
// replace the current config with the new one
|
// replace the current config with the new one
|
||||||
if (c._id === currentConfig._id) {
|
if (c._id === currentConfig._id) {
|
||||||
return config
|
return config
|
||||||
|
@ -30,27 +34,36 @@
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
config._id = Helpers.uuid()
|
config._id = Helpers.uuid()
|
||||||
configs = [...configs, config]
|
newAuthConfigs = [...authConfigs, config]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatch("change", newAuthConfigs)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onRemove = () => {
|
const onRemove = () => {
|
||||||
configs = configs.filter(c => {
|
const newAuthConfigs = authConfigs.filter(c => {
|
||||||
return c._id !== currentConfig._id
|
return c._id !== currentConfig._id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
dispatch("change", newAuthConfigs)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<RestAuthenticationModal {configs} {currentConfig} {onConfirm} {onRemove} />
|
<RestAuthenticationModal
|
||||||
|
configs={authConfigs}
|
||||||
|
{currentConfig}
|
||||||
|
{onConfirm}
|
||||||
|
{onRemove}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Layout gap="S" noPadding>
|
<Layout gap="S" noPadding>
|
||||||
{#if configs && configs.length > 0}
|
{#if authConfigs && authConfigs.length > 0}
|
||||||
<Table
|
<Table
|
||||||
on:click={({ detail }) => openConfigModal(detail)}
|
on:click={({ detail }) => openConfigModal(detail)}
|
||||||
{schema}
|
{schema}
|
||||||
data={configs}
|
data={authConfigs}
|
||||||
allowEditColumns={false}
|
allowEditColumns={false}
|
||||||
allowEditRows={false}
|
allowEditRows={false}
|
||||||
allowSelectRows={false}
|
allowSelectRows={false}
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script>
|
||||||
|
import RestAuthenticationBuilder from "./RestAuthenticationBuilder.svelte"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import SaveDatasourceButton from "../SaveDatasourceButton.svelte"
|
||||||
|
import Panel from "../Panel.svelte"
|
||||||
|
import Tooltip from "../Tooltip.svelte"
|
||||||
|
|
||||||
|
export let datasource
|
||||||
|
$: updatedDatasource = cloneDeep(datasource)
|
||||||
|
|
||||||
|
const updateAuthConfigs = newAuthConfigs => {
|
||||||
|
updatedDatasource.config.authConfigs = newAuthConfigs
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<SaveDatasourceButton slot="controls" {datasource} {updatedDatasource} />
|
||||||
|
<Tooltip
|
||||||
|
slot="tooltip"
|
||||||
|
title="REST Authentication"
|
||||||
|
href="https://docs.budibase.com/docs/rest-authentication"
|
||||||
|
/>
|
||||||
|
<RestAuthenticationBuilder
|
||||||
|
on:change={({ detail }) => updateAuthConfigs(detail)}
|
||||||
|
authConfigs={updatedDatasource.config.authConfigs}
|
||||||
|
/>
|
||||||
|
</Panel>
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script>
|
||||||
|
import { ActionButton } from "@budibase/bbui"
|
||||||
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
|
import {
|
||||||
|
getRestBindings,
|
||||||
|
readableToRuntimeBinding,
|
||||||
|
runtimeToReadableMap,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import SaveDatasourceButton from "./SaveDatasourceButton.svelte"
|
||||||
|
import Panel from "./Panel.svelte"
|
||||||
|
import Tooltip from "./Tooltip.svelte"
|
||||||
|
|
||||||
|
export let datasource
|
||||||
|
let restBindings = getRestBindings()
|
||||||
|
let addHeader
|
||||||
|
|
||||||
|
$: updatedDatasource = cloneDeep(datasource)
|
||||||
|
$: parsedHeaders = runtimeToReadableMap(
|
||||||
|
restBindings,
|
||||||
|
updatedDatasource?.config?.defaultHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const onDefaultHeaderUpdate = headers => {
|
||||||
|
const flatHeaders = cloneDeep(headers).reduce((acc, entry) => {
|
||||||
|
acc[entry.name] = readableToRuntimeBinding(restBindings, entry.value)
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
updatedDatasource.config.defaultHeaders = flatHeaders
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<SaveDatasourceButton slot="controls" {datasource} {updatedDatasource} />
|
||||||
|
<Tooltip
|
||||||
|
slot="tooltip"
|
||||||
|
title="REST Headers"
|
||||||
|
href="https://docs.budibase.com/docs/rest-queries#headers"
|
||||||
|
/>
|
||||||
|
<KeyValueBuilder
|
||||||
|
bind:this={addHeader}
|
||||||
|
object={parsedHeaders}
|
||||||
|
on:change={evt => onDefaultHeaderUpdate(evt.detail)}
|
||||||
|
noAddButton
|
||||||
|
bindings={restBindings}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<ActionButton icon="Add" on:click={() => addHeader.addEntry()}>
|
||||||
|
Add header
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="header">
|
||||||
|
<div class="controls">
|
||||||
|
<slot name="controls" />
|
||||||
|
</div>
|
||||||
|
<div class="tooltip">
|
||||||
|
<slot name="tooltip" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script>
|
||||||
|
import { Button, Modal } from "@budibase/bbui"
|
||||||
|
import ImportQueriesModal from "./RestImportQueriesModal.svelte"
|
||||||
|
|
||||||
|
let importQueriesModal
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={importQueriesModal}>
|
||||||
|
<ImportQueriesModal createDatasource={false} datasourceId={"todo"} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<div class="button">
|
||||||
|
<Button secondary on:click={() => importQueriesModal.show()}>Import</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,132 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import {
|
||||||
|
ModalContent,
|
||||||
|
notifications,
|
||||||
|
Body,
|
||||||
|
Layout,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
Heading,
|
||||||
|
TextArea,
|
||||||
|
Dropzone,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { datasources, queries } from "stores/backend"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
export let navigateDatasource = false
|
||||||
|
export let datasourceId
|
||||||
|
export let createDatasource = false
|
||||||
|
export let onCancel
|
||||||
|
|
||||||
|
const data = writable({
|
||||||
|
url: "",
|
||||||
|
raw: "",
|
||||||
|
file: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
let lastTouched = "url"
|
||||||
|
|
||||||
|
const getData = async () => {
|
||||||
|
let dataString
|
||||||
|
|
||||||
|
// parse the file into memory and send as string
|
||||||
|
if (lastTouched === "file") {
|
||||||
|
dataString = await $data.file.text()
|
||||||
|
} else if (lastTouched === "url") {
|
||||||
|
const response = await fetch($data.url)
|
||||||
|
dataString = await response.text()
|
||||||
|
} else if (lastTouched === "raw") {
|
||||||
|
dataString = $data.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataString
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importQueries() {
|
||||||
|
try {
|
||||||
|
const dataString = await getData()
|
||||||
|
|
||||||
|
if (!datasourceId && !createDatasource) {
|
||||||
|
throw new Error("No datasource id")
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
data: dataString,
|
||||||
|
datasourceId,
|
||||||
|
}
|
||||||
|
|
||||||
|
const importResult = await queries.import(body)
|
||||||
|
if (!datasourceId) {
|
||||||
|
datasourceId = importResult.datasourceId
|
||||||
|
}
|
||||||
|
|
||||||
|
// reload
|
||||||
|
await datasources.fetch()
|
||||||
|
await queries.fetch()
|
||||||
|
|
||||||
|
if (navigateDatasource) {
|
||||||
|
$goto(`./datasource/${datasourceId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications.success("Imported successfully")
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error importing queries")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
onConfirm={() => importQueries()}
|
||||||
|
{onCancel}
|
||||||
|
confirmText={"Import"}
|
||||||
|
cancelText="Back"
|
||||||
|
size="L"
|
||||||
|
>
|
||||||
|
<Layout noPadding>
|
||||||
|
<Heading size="S">Import</Heading>
|
||||||
|
<Body size="XS"
|
||||||
|
>Import your rest collection using one of the options below</Body
|
||||||
|
>
|
||||||
|
<Tabs selected="File">
|
||||||
|
<!-- Commenting until nginx csp issue resolved -->
|
||||||
|
<!-- <Tab title="Link">
|
||||||
|
<Input
|
||||||
|
bind:value={$data.url}
|
||||||
|
on:change={() => (lastTouched = "url")}
|
||||||
|
label="Enter a URL"
|
||||||
|
placeholder="e.g. https://petstore.swagger.io/v2/swagger.json"
|
||||||
|
/>
|
||||||
|
</Tab> -->
|
||||||
|
<Tab title="File">
|
||||||
|
<Dropzone
|
||||||
|
gallery={false}
|
||||||
|
value={$data.file ? [$data.file] : []}
|
||||||
|
on:change={e => {
|
||||||
|
$data.file = e.detail?.[0]
|
||||||
|
lastTouched = "file"
|
||||||
|
}}
|
||||||
|
fileTags={[
|
||||||
|
"OpenAPI 3.0",
|
||||||
|
"OpenAPI 2.0",
|
||||||
|
"Swagger 2.0",
|
||||||
|
"cURL",
|
||||||
|
"YAML",
|
||||||
|
"JSON",
|
||||||
|
]}
|
||||||
|
maximum={1}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Raw Text">
|
||||||
|
<TextArea
|
||||||
|
bind:value={$data.raw}
|
||||||
|
on:change={() => (lastTouched = "raw")}
|
||||||
|
label={"Paste raw text"}
|
||||||
|
placeholder={'e.g. curl --location --request GET "https://example.com"'}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</Layout>
|
||||||
|
</ModalContent>
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { Button, Table } from "@budibase/bbui"
|
||||||
|
import { queries } from "stores/backend"
|
||||||
|
import CapitaliseRenderer from "components/common/renderers/CapitaliseRenderer.svelte"
|
||||||
|
import RestImportButton from "./RestImportButton.svelte"
|
||||||
|
import Panel from "../Panel.svelte"
|
||||||
|
import Tooltip from "../Tooltip.svelte"
|
||||||
|
|
||||||
|
export let datasource
|
||||||
|
|
||||||
|
$: queryList = $queries.list.filter(
|
||||||
|
query => query.datasourceId === datasource._id
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<div slot="controls">
|
||||||
|
<Button cta on:click={() => $goto(`../../query/new/${datasource._id}`)}>
|
||||||
|
Create new query
|
||||||
|
</Button>
|
||||||
|
{#if datasource.source === "REST"}
|
||||||
|
<RestImportButton datasourceId={datasource._id} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
slot="tooltip"
|
||||||
|
title="Custom queries"
|
||||||
|
href="https://docs.budibase.com/docs/data-sources#custom-queries "
|
||||||
|
/>
|
||||||
|
<Table
|
||||||
|
on:click={({ detail }) => $goto(`../../query/${detail._id}`)}
|
||||||
|
schema={{
|
||||||
|
name: {},
|
||||||
|
queryVerb: { displayName: "Method" },
|
||||||
|
}}
|
||||||
|
data={queryList}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
customRenderers={[{ column: "queryVerb", component: CapitaliseRenderer }]}
|
||||||
|
/>
|
||||||
|
</Panel>
|
|
@ -0,0 +1,108 @@
|
||||||
|
<script>
|
||||||
|
import { Button, Table, notifications } from "@budibase/bbui"
|
||||||
|
import CreateEditRelationshipModal from "components/backend/Datasources/CreateEditRelationshipModal.svelte"
|
||||||
|
import {
|
||||||
|
tables as tablesStore,
|
||||||
|
integrations as integrationsStore,
|
||||||
|
datasources as datasourcesStore,
|
||||||
|
} from "stores/backend"
|
||||||
|
import { DatasourceFeature } from "@budibase/types"
|
||||||
|
import { API } from "api"
|
||||||
|
import Panel from "./Panel.svelte"
|
||||||
|
import Tooltip from "./Tooltip.svelte"
|
||||||
|
|
||||||
|
export let datasource
|
||||||
|
|
||||||
|
let modal
|
||||||
|
|
||||||
|
$: tables = Object.values(datasource.entities)
|
||||||
|
$: relationships = getRelationships(tables)
|
||||||
|
|
||||||
|
function getRelationships(tables) {
|
||||||
|
const relatedColumns = {}
|
||||||
|
|
||||||
|
tables.forEach(({ name: tableName, schema }) => {
|
||||||
|
Object.values(schema).forEach(column => {
|
||||||
|
if (column.type !== "link") return
|
||||||
|
|
||||||
|
relatedColumns[column._id] ??= {}
|
||||||
|
relatedColumns[column._id].through =
|
||||||
|
relatedColumns[column._id].through || column.through
|
||||||
|
|
||||||
|
relatedColumns[column._id][column.main ? "from" : "to"] = {
|
||||||
|
...column,
|
||||||
|
tableName,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.values(relatedColumns).map(({ from, to, through }) => {
|
||||||
|
return {
|
||||||
|
tables: `${from.tableName} ${through ? "↔" : "→"} ${to.tableName}`,
|
||||||
|
columns: `${from.name} to ${to.name}`,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRowClick = ({ detail }) => {
|
||||||
|
modal.show({ fromRelationship: detail.from, toRelationship: detail.to })
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeSave = async ({ datasource }) => {
|
||||||
|
const integration = $integrationsStore[datasource.source]
|
||||||
|
if (!integration.features[DatasourceFeature.CONNECTION_CHECKING]) return
|
||||||
|
|
||||||
|
const { connected } = await API.validateDatasource(datasource)
|
||||||
|
|
||||||
|
if (!connected) {
|
||||||
|
throw "Invalid connection"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const afterSave = async ({ action }) => {
|
||||||
|
await tablesStore.fetch()
|
||||||
|
await datasourcesStore.fetch()
|
||||||
|
notifications.success(`Relationship ${action} successfully`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onError = async ({ action, err }) => {
|
||||||
|
let notificationVerb = "creating"
|
||||||
|
|
||||||
|
if (action === "updated") {
|
||||||
|
notificationVerb = "updating"
|
||||||
|
} else if (action === "deleted") {
|
||||||
|
notificationVerb = "deleting"
|
||||||
|
}
|
||||||
|
notifications.error(`Error ${notificationVerb} datasource: ${err}`)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CreateEditRelationshipModal
|
||||||
|
bind:this={modal}
|
||||||
|
{datasource}
|
||||||
|
{beforeSave}
|
||||||
|
{afterSave}
|
||||||
|
{onError}
|
||||||
|
{tables}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<Button slot="controls" cta on:click={modal.show}>
|
||||||
|
Define relationships
|
||||||
|
</Button>
|
||||||
|
<Tooltip
|
||||||
|
slot="tooltip"
|
||||||
|
title="Relationships"
|
||||||
|
href="https://docs.budibase.com/docs/relationships"
|
||||||
|
/>
|
||||||
|
<Table
|
||||||
|
on:click={handleRowClick}
|
||||||
|
schema={{ tables: {}, columns: {} }}
|
||||||
|
data={relationships}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
/>
|
||||||
|
</Panel>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script>
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import { isEqual } from "lodash"
|
||||||
|
import { integrationForDatasource } from "stores/selectors"
|
||||||
|
import { integrations, datasources } from "stores/backend"
|
||||||
|
import { notifications, Button } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let datasource
|
||||||
|
export let updatedDatasource
|
||||||
|
|
||||||
|
$: hasChanged = !isEqual(datasource, updatedDatasource)
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
try {
|
||||||
|
const integration = integrationForDatasource(
|
||||||
|
get(integrations),
|
||||||
|
updatedDatasource
|
||||||
|
)
|
||||||
|
await datasources.update({ datasource: updatedDatasource, integration })
|
||||||
|
notifications.success(
|
||||||
|
`Datasource ${updatedDatasource.name} updated successfully`
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error(`Error saving datasource: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button disabled={!hasChanged} cta on:click={save}>Save</Button>
|
|
@ -0,0 +1,71 @@
|
||||||
|
<script>
|
||||||
|
import { Body, Button, notifications, Modal, Toggle } from "@budibase/bbui"
|
||||||
|
import { datasources, tables } from "stores/backend"
|
||||||
|
import CreateExternalTableModal from "./CreateExternalTableModal.svelte"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import ValuesList from "components/common/ValuesList.svelte"
|
||||||
|
|
||||||
|
export let datasource
|
||||||
|
|
||||||
|
let createExternalTableModal
|
||||||
|
let confirmDialog
|
||||||
|
let specificTables = null
|
||||||
|
let requireSpecificTables = false
|
||||||
|
|
||||||
|
async function updateDatasourceSchema() {
|
||||||
|
try {
|
||||||
|
await datasources.updateSchema(datasource, specificTables)
|
||||||
|
notifications.success(`Datasource ${name} tables updated successfully`)
|
||||||
|
await tables.fetch()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error(
|
||||||
|
`Error updating datasource schema ${
|
||||||
|
error?.message ? `: ${error.message}` : ""
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={createExternalTableModal}>
|
||||||
|
<CreateExternalTableModal {datasource} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={confirmDialog}
|
||||||
|
okText="Fetch tables"
|
||||||
|
onOk={updateDatasourceSchema}
|
||||||
|
onCancel={() => confirmDialog.hide()}
|
||||||
|
warning={false}
|
||||||
|
title="Confirm table fetch"
|
||||||
|
>
|
||||||
|
<Toggle
|
||||||
|
bind:value={requireSpecificTables}
|
||||||
|
on:change={e => {
|
||||||
|
requireSpecificTables = e.detail
|
||||||
|
specificTables = null
|
||||||
|
}}
|
||||||
|
thin
|
||||||
|
text="Fetch listed tables only (one per line)"
|
||||||
|
/>
|
||||||
|
{#if requireSpecificTables}
|
||||||
|
<ValuesList label="" bind:values={specificTables} />
|
||||||
|
{/if}
|
||||||
|
<br />
|
||||||
|
<Body>
|
||||||
|
If you have fetched tables from this database before, this action may
|
||||||
|
overwrite any changes you made after your initial fetch.
|
||||||
|
</Body>
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<Button cta on:click={createExternalTableModal.show}>Create new table</Button>
|
||||||
|
<Button secondary on:click={confirmDialog.show}>Fetch tables</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { Table } from "@budibase/bbui"
|
||||||
|
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
||||||
|
import Controls from "./Controls.svelte"
|
||||||
|
import Panel from "../Panel.svelte"
|
||||||
|
import Tooltip from "../Tooltip.svelte"
|
||||||
|
|
||||||
|
export let datasource
|
||||||
|
|
||||||
|
let tableSchema = {
|
||||||
|
name: {},
|
||||||
|
primary: { displayName: "Primary Key" },
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<Controls slot="controls" {datasource} />
|
||||||
|
<Tooltip
|
||||||
|
slot="tooltip"
|
||||||
|
title="Using data in your app"
|
||||||
|
href="https://docs.budibase.com/docs/data"
|
||||||
|
/>
|
||||||
|
<Table
|
||||||
|
on:click={({ detail: table }) => $goto(`../../table/${table._id}`)}
|
||||||
|
schema={tableSchema}
|
||||||
|
data={Object.values(datasource?.entities || {})}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
allowSelectRows={false}
|
||||||
|
customRenderers={[{ column: "primary", component: ArrayRenderer }]}
|
||||||
|
/>
|
||||||
|
</Panel>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script>
|
||||||
|
import { ActionButton } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let title = ""
|
||||||
|
export let href = null
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionButton quiet icon="Help" on:click={() => window.open(href, "_blank")}>
|
||||||
|
{title}
|
||||||
|
</ActionButton>
|
|
@ -1,14 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { Body, Table, BoldRenderer, CodeRenderer } from "@budibase/bbui"
|
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"
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
export let datasource
|
export let datasource
|
||||||
export let queries
|
|
||||||
|
|
||||||
let dynamicVariables = []
|
let dynamicVariables = []
|
||||||
|
|
||||||
$: enrichDynamicVariables(datasource, queries)
|
$: enrichDynamicVariables(datasource, $queries.list)
|
||||||
|
|
||||||
const dynamicVariableSchema = {
|
const dynamicVariableSchema = {
|
||||||
name: "",
|
name: "",
|
||||||
|
@ -18,7 +17,7 @@
|
||||||
|
|
||||||
const onClick = dynamicVariable => {
|
const onClick = dynamicVariable => {
|
||||||
const queryId = dynamicVariable.queryId
|
const queryId = dynamicVariable.queryId
|
||||||
queriesStore.select({ _id: queryId })
|
queries.select({ _id: queryId })
|
||||||
$goto(`./${queryId}`)
|
$goto(`./${queryId}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
<script>
|
||||||
|
import { Heading, Layout } from "@budibase/bbui"
|
||||||
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
|
import ViewDynamicVariables from "./ViewDynamicVariables.svelte"
|
||||||
|
import { getEnvironmentBindings } from "builderStore/dataBinding"
|
||||||
|
import { licensing } from "stores/portal"
|
||||||
|
import { queries } from "stores/backend"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import SaveDatasourceButton from "../SaveDatasourceButton.svelte"
|
||||||
|
import Panel from "../Panel.svelte"
|
||||||
|
import Tooltip from "../Tooltip.svelte"
|
||||||
|
|
||||||
|
export let datasource
|
||||||
|
|
||||||
|
$: updatedDatasource = cloneDeep(datasource)
|
||||||
|
|
||||||
|
$: queriesForDatasource = $queries.list.filter(
|
||||||
|
query => query.datasourceId === datasource?._id
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleChange = newUnparsedStaticVariables => {
|
||||||
|
const newStaticVariables = {}
|
||||||
|
|
||||||
|
newUnparsedStaticVariables.forEach(({ name, value }) => {
|
||||||
|
newStaticVariables[name] = value
|
||||||
|
})
|
||||||
|
|
||||||
|
updatedDatasource.config.staticVariables = newStaticVariables
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<SaveDatasourceButton slot="controls" {datasource} {updatedDatasource} />
|
||||||
|
<Tooltip
|
||||||
|
slot="tooltip"
|
||||||
|
title="REST variables"
|
||||||
|
href="https://docs.budibase.com/docs/rest-variables"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Layout>
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
|
<Heading size="S">Static</Heading>
|
||||||
|
<KeyValueBuilder
|
||||||
|
name="Variable"
|
||||||
|
keyPlaceholder="Name"
|
||||||
|
headings
|
||||||
|
object={updatedDatasource.config.staticVariables}
|
||||||
|
on:change={({ detail }) => handleChange(detail)}
|
||||||
|
bindings={$licensing.environmentVariablesEnabled
|
||||||
|
? getEnvironmentBindings()
|
||||||
|
: []}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
|
<Heading size="S">Dynamic</Heading>
|
||||||
|
<ViewDynamicVariables queries={queriesForDatasource} {datasource} />
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
</Panel>
|
|
@ -1,163 +1,89 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { Tabs, Tab, Heading, Body, Layout } from "@budibase/bbui"
|
||||||
import {
|
import { datasources, integrations } from "stores/backend"
|
||||||
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 ICONS from "components/backend/DatasourceNavigator/icons"
|
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||||
import CapitaliseRenderer from "components/common/renderers/CapitaliseRenderer.svelte"
|
import EditDatasourceConfig from "./_components/EditDatasourceConfig.svelte"
|
||||||
import { IntegrationTypes } from "constants/backend"
|
import TablesPanel from "./_components/panels/Tables/index.svelte"
|
||||||
import { isEqual } from "lodash"
|
import RelationshipsPanel from "./_components/panels/Relationships.svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import QueriesPanel from "./_components/panels/Queries/index.svelte"
|
||||||
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
import RestHeadersPanel from "./_components/panels/Headers.svelte"
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import RestAuthenticationPanel from "./_components/panels/Authentication/index.svelte"
|
||||||
|
import RestVariablesPanel from "./_components/panels/Variables/index.svelte"
|
||||||
|
|
||||||
const querySchema = {
|
let selectedPanel = null
|
||||||
name: {},
|
let panelOptions = []
|
||||||
queryVerb: { displayName: "Method" },
|
|
||||||
}
|
|
||||||
|
|
||||||
let importQueriesModal
|
// datasources.selected can return null temporarily on datasource deletion
|
||||||
let changed = false
|
$: datasource = $datasources.selected || {}
|
||||||
let isValid = true
|
|
||||||
let integration, baseDatasource, datasource
|
|
||||||
let queryList
|
|
||||||
let loading = false
|
|
||||||
|
|
||||||
$: baseDatasource = $datasources.selected
|
$: getOptions(datasource)
|
||||||
$: queryList = $queries.list.filter(
|
|
||||||
query => query.datasourceId === datasource?._id
|
|
||||||
)
|
|
||||||
$: hasChanged(baseDatasource, datasource)
|
|
||||||
$: updateDatasource(baseDatasource)
|
|
||||||
|
|
||||||
const hasChanged = (base, ds) => {
|
const getOptions = datasource => {
|
||||||
if (base && ds) {
|
if (datasource.plus) {
|
||||||
changed = !isEqual(base, ds)
|
// 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"]
|
||||||
const saveDatasource = async () => {
|
: ["Tables", "Relationships", "Queries"]
|
||||||
try {
|
// TODO what is this for?
|
||||||
// Create datasource
|
selectedPanel = panelOptions.includes(selectedPanel)
|
||||||
await datasources.update({ datasource, integration })
|
? selectedPanel
|
||||||
if (datasource?.plus) {
|
: "Tables"
|
||||||
await tables.fetch()
|
} else if (datasource.source === "REST") {
|
||||||
}
|
panelOptions = ["Queries", "Headers", "Authentication", "Variables"]
|
||||||
await datasources.fetch()
|
selectedPanel = panelOptions.includes(selectedPanel)
|
||||||
notifications.success(`Datasource ${name} updated successfully.`)
|
? selectedPanel
|
||||||
baseDatasource = cloneDeep(datasource)
|
: "Queries"
|
||||||
} catch (err) {
|
} else {
|
||||||
notifications.error(`Error saving datasource: ${err}`)
|
panelOptions = ["Queries"]
|
||||||
}
|
selectedPanel = "Queries"
|
||||||
}
|
|
||||||
|
|
||||||
const updateDatasource = base => {
|
|
||||||
if (base) {
|
|
||||||
datasource = cloneDeep(base)
|
|
||||||
integration = $integrations[datasource.source]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={importQueriesModal}>
|
<section>
|
||||||
{#if datasource.source === "REST"}
|
<Layout noPadding>
|
||||||
<ImportRestQueriesModal
|
<Layout gap="XS" noPadding>
|
||||||
createDatasource={false}
|
<header>
|
||||||
datasourceId={datasource._id}
|
<svelte:component
|
||||||
/>
|
this={ICONS[datasource.source]}
|
||||||
{/if}
|
height="26"
|
||||||
</Modal>
|
width="26"
|
||||||
|
/>
|
||||||
{#if datasource && integration}
|
<Heading size="M">{$datasources.selected?.name}</Heading>
|
||||||
<section>
|
</header>
|
||||||
<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}
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</section>
|
<EditDatasourceConfig {datasource} />
|
||||||
{/if}
|
<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>
|
<style>
|
||||||
section {
|
section {
|
||||||
|
@ -166,32 +92,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
|
margin-top: 35px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.query-header {
|
.tabs {
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -99,11 +99,14 @@ export function createDatasourcesStore() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const create = async ({ integration, config }) => {
|
const create = async ({ integration, config }) => {
|
||||||
|
const count = sourceCount(integration.name)
|
||||||
|
const nameModifier = count === 0 ? "" : ` ${count + 1}`
|
||||||
|
|
||||||
const datasource = {
|
const datasource = {
|
||||||
type: "datasource",
|
type: "datasource",
|
||||||
source: integration.name,
|
source: integration.name,
|
||||||
config,
|
config,
|
||||||
name: `${integration.friendlyName}-${sourceCount(integration.name) + 1}`,
|
name: `${integration.friendlyName}${nameModifier}`,
|
||||||
plus: integration.plus && integration.name !== IntegrationTypes.REST,
|
plus: integration.plus && integration.name !== IntegrationTypes.REST,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue