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