Merge pull request #11029 from Budibase/use-modal-for-editing-datasources-v3
Use modal for editing datasources v3
This commit is contained in:
commit
4d3ea4cc3e
|
@ -1,54 +0,0 @@
|
|||
import { datasources, tables } from "../stores/backend"
|
||||
import { IntegrationNames } from "../constants/backend"
|
||||
import { get } from "svelte/store"
|
||||
import cloneDeep from "lodash/cloneDeepWith"
|
||||
import { API } from "api"
|
||||
|
||||
function prepareData(config) {
|
||||
let datasource = {}
|
||||
let existingTypeCount = get(datasources).list.filter(
|
||||
ds => ds.source === config.type
|
||||
).length
|
||||
|
||||
let baseName = IntegrationNames[config.type] || config.name
|
||||
let name =
|
||||
existingTypeCount === 0 ? baseName : `${baseName}-${existingTypeCount + 1}`
|
||||
|
||||
datasource.type = "datasource"
|
||||
datasource.source = config.type
|
||||
datasource.config = config.config
|
||||
datasource.name = name
|
||||
datasource.plus = config.plus
|
||||
|
||||
return datasource
|
||||
}
|
||||
|
||||
export async function saveDatasource(config, { skipFetch, tablesFilter } = {}) {
|
||||
const datasource = prepareData(config)
|
||||
// Create datasource
|
||||
const fetchSchema = !skipFetch && datasource.plus
|
||||
const resp = await datasources.save(datasource, { fetchSchema, tablesFilter })
|
||||
|
||||
// update the tables incase datasource plus
|
||||
await tables.fetch()
|
||||
await datasources.select(resp._id)
|
||||
return resp
|
||||
}
|
||||
|
||||
export async function createRestDatasource(integration) {
|
||||
const config = cloneDeep(integration)
|
||||
return saveDatasource(config)
|
||||
}
|
||||
|
||||
export async function validateDatasourceConfig(config) {
|
||||
const datasource = prepareData(config)
|
||||
return await API.validateDatasource(datasource)
|
||||
}
|
||||
|
||||
export async function getDatasourceInfo(config) {
|
||||
let datasource = config
|
||||
if (!config._id) {
|
||||
datasource = prepareData(config)
|
||||
}
|
||||
return await API.fetchInfoForDatasource(datasource)
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
<script>
|
||||
import { get } from "svelte/store"
|
||||
import { ActionButton, Modal, notifications } from "@budibase/bbui"
|
||||
import CreateEditRelationship from "../../Datasources/CreateEditRelationship.svelte"
|
||||
import { datasources } from "../../../../stores/backend"
|
||||
import { datasources, integrations } from "../../../../stores/backend"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { integrationForDatasource } from "stores/selectors"
|
||||
|
||||
export let table
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -27,7 +29,10 @@
|
|||
async function saveRelationship() {
|
||||
try {
|
||||
// Create datasource
|
||||
await datasources.save(datasource)
|
||||
await datasources.update({
|
||||
datasource,
|
||||
integration: integrationForDatasource(get(integrations), datasource),
|
||||
})
|
||||
notifications.success(`Relationship information saved.`)
|
||||
dispatch("updatecolumns")
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,219 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Label,
|
||||
Input,
|
||||
Layout,
|
||||
Toggle,
|
||||
Button,
|
||||
TextArea,
|
||||
Modal,
|
||||
EnvDropdown,
|
||||
Accordion,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
import { IntegrationTypes } from "constants/backend"
|
||||
import { createValidationStore } from "helpers/validation/yup"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import { environment, licensing, auth } from "stores/portal"
|
||||
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
|
||||
|
||||
export let datasource
|
||||
export let schema
|
||||
export let creating
|
||||
|
||||
let createVariableModal
|
||||
let selectedKey
|
||||
|
||||
const validation = createValidationStore()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function filter([key, value]) {
|
||||
if (!value) {
|
||||
return false
|
||||
}
|
||||
return !(
|
||||
(datasource.source === IntegrationTypes.REST &&
|
||||
key === "defaultHeaders") ||
|
||||
value.deprecated
|
||||
)
|
||||
}
|
||||
|
||||
$: config = datasource?.config
|
||||
$: configKeys = Object.entries(schema || {})
|
||||
.filter(el => filter(el))
|
||||
.map(([key]) => key)
|
||||
|
||||
// setup the validation for each required field
|
||||
$: configKeys.forEach(key => {
|
||||
if (schema[key].required) {
|
||||
validation.addValidatorType(key, schema[key].type, schema[key].required)
|
||||
}
|
||||
})
|
||||
// run the validation whenever the config changes
|
||||
$: validation.check(config)
|
||||
// dispatch the validation result
|
||||
$: dispatch(
|
||||
"valid",
|
||||
Object.values($validation.errors).filter(val => val != null).length === 0
|
||||
)
|
||||
|
||||
let addButton
|
||||
|
||||
function getDisplayName(key, fieldKey) {
|
||||
let name
|
||||
if (fieldKey && schema[key]["fields"][fieldKey]?.display) {
|
||||
name = schema[key]["fields"][fieldKey].display
|
||||
} else if (fieldKey) {
|
||||
name = fieldKey
|
||||
} else if (schema[key]?.display) {
|
||||
name = schema[key].display
|
||||
} else {
|
||||
name = key
|
||||
}
|
||||
return capitalise(name)
|
||||
}
|
||||
|
||||
function getDisplayError(error, configKey) {
|
||||
return error?.replace(
|
||||
new RegExp(`${configKey}`, "i"),
|
||||
getDisplayName(configKey)
|
||||
)
|
||||
}
|
||||
|
||||
function getFieldGroupKeys(fieldGroup) {
|
||||
return Object.entries(schema[fieldGroup].fields || {})
|
||||
.filter(el => filter(el))
|
||||
.map(([key]) => key)
|
||||
}
|
||||
|
||||
async function save(data) {
|
||||
try {
|
||||
await environment.createVariable(data)
|
||||
config[selectedKey] = `{{ env.${data.name} }}`
|
||||
createVariableModal.hide()
|
||||
} catch (err) {
|
||||
notifications.error(`Failed to create variable: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
function showModal(configKey) {
|
||||
selectedKey = configKey
|
||||
createVariableModal.show()
|
||||
}
|
||||
|
||||
async function handleUpgradePanel() {
|
||||
await environment.upgradePanelOpened()
|
||||
$licensing.goToUpgradePage()
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await environment.loadVariables()
|
||||
if ($auth.user) {
|
||||
await licensing.init()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<form>
|
||||
<Layout noPadding gap="S">
|
||||
{#if !creating}
|
||||
<div class="form-row">
|
||||
<Label>Name</Label>
|
||||
<Input on:change bind:value={datasource.name} />
|
||||
</div>
|
||||
{/if}
|
||||
{#each configKeys as configKey}
|
||||
{#if schema[configKey].type === "object"}
|
||||
<div class="form-row ssl">
|
||||
<Label>{getDisplayName(configKey)}</Label>
|
||||
<Button secondary thin outline on:click={addButton.addEntry()}
|
||||
>Add</Button
|
||||
>
|
||||
</div>
|
||||
<KeyValueBuilder
|
||||
bind:this={addButton}
|
||||
defaults={schema[configKey].default}
|
||||
bind:object={config[configKey]}
|
||||
on:change
|
||||
noAddButton={true}
|
||||
/>
|
||||
{:else if schema[configKey].type === "boolean"}
|
||||
<div class="form-row">
|
||||
<Label>{getDisplayName(configKey)}</Label>
|
||||
<Toggle text="" bind:value={config[configKey]} />
|
||||
</div>
|
||||
{:else if schema[configKey].type === "longForm"}
|
||||
<div class="form-row">
|
||||
<Label>{getDisplayName(configKey)}</Label>
|
||||
<TextArea
|
||||
type={schema[configKey].type}
|
||||
on:change
|
||||
bind:value={config[configKey]}
|
||||
error={getDisplayError($validation.errors[configKey], configKey)}
|
||||
/>
|
||||
</div>
|
||||
{:else if schema[configKey].type === "fieldGroup"}
|
||||
<Accordion
|
||||
itemName={configKey}
|
||||
initialOpen={getFieldGroupKeys(configKey).some(
|
||||
fieldKey => !!config[fieldKey]
|
||||
)}
|
||||
header={getDisplayName(configKey)}
|
||||
>
|
||||
<Layout gap="S">
|
||||
{#each getFieldGroupKeys(configKey) as fieldKey}
|
||||
<div class="form-row">
|
||||
<Label>{getDisplayName(configKey, fieldKey)}</Label>
|
||||
<Input
|
||||
type={schema[configKey]["fields"][fieldKey]?.type}
|
||||
on:change
|
||||
bind:value={config[fieldKey]}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
</Accordion>
|
||||
{:else}
|
||||
<div class="form-row">
|
||||
<Label>{getDisplayName(configKey)}</Label>
|
||||
<EnvDropdown
|
||||
showModal={() => showModal(configKey)}
|
||||
variables={$environment.variables}
|
||||
type={configKey === "port" ? "string" : schema[configKey].type}
|
||||
on:change
|
||||
bind:value={config[configKey]}
|
||||
error={getDisplayError($validation.errors[configKey], configKey)}
|
||||
environmentVariablesEnabled={$licensing.environmentVariablesEnabled}
|
||||
{handleUpgradePanel}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</Layout>
|
||||
</form>
|
||||
|
||||
<Modal bind:this={createVariableModal}>
|
||||
<CreateEditVariableModal {save} />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-row.ssl {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 20%;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -16,7 +16,6 @@
|
|||
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { getDatasourceInfo } from "builderStore/datasource"
|
||||
|
||||
export let datasource
|
||||
export let save
|
||||
|
@ -170,8 +169,7 @@
|
|||
<Button
|
||||
secondary
|
||||
on:click={async () => {
|
||||
const info = await getDatasourceInfo(datasource)
|
||||
tableList = info.tableNames
|
||||
tableList = await datasources.getTableNames(datasource)
|
||||
confirmDialog.show()
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -45,6 +45,9 @@
|
|||
<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
|
||||
|
@ -69,6 +72,9 @@
|
|||
<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.
|
||||
|
@ -81,6 +87,9 @@
|
|||
<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
|
||||
|
@ -110,6 +119,7 @@
|
|||
|
||||
<style>
|
||||
.section-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
@ -120,4 +130,8 @@
|
|||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.headerRight {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,141 +0,0 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import {
|
||||
ModalContent,
|
||||
notifications,
|
||||
Body,
|
||||
Layout,
|
||||
FancyCheckboxGroup,
|
||||
} from "@budibase/bbui"
|
||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||
import { IntegrationNames } from "constants/backend"
|
||||
import cloneDeep from "lodash/cloneDeepWith"
|
||||
import {
|
||||
saveDatasource as save,
|
||||
validateDatasourceConfig,
|
||||
getDatasourceInfo,
|
||||
} from "builderStore/datasource"
|
||||
import { DatasourceFeature } from "@budibase/types"
|
||||
|
||||
export let integration
|
||||
|
||||
// kill the reference so the input isn't saved
|
||||
let datasource = cloneDeep(integration)
|
||||
let isValid = false
|
||||
let fetchTableStep = false
|
||||
let selectedTables = []
|
||||
let tableList = []
|
||||
|
||||
$: name =
|
||||
IntegrationNames[datasource?.type] || datasource?.name || datasource?.type
|
||||
$: datasourcePlus = datasource?.plus
|
||||
$: title = fetchTableStep ? "Fetch your tables" : `Connect to ${name}`
|
||||
$: confirmText = fetchTableStep
|
||||
? "Continue"
|
||||
: datasourcePlus
|
||||
? "Connect"
|
||||
: "Save and continue to query"
|
||||
|
||||
async function validateConfig() {
|
||||
if (!integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||
return true
|
||||
}
|
||||
const displayError = message =>
|
||||
notifications.error(message ?? "Error validating datasource")
|
||||
|
||||
let connected = false
|
||||
try {
|
||||
const resp = await validateDatasourceConfig(datasource)
|
||||
if (!resp.connected) {
|
||||
displayError(`Unable to connect - ${resp.error}`)
|
||||
}
|
||||
connected = resp.connected
|
||||
} catch (err) {
|
||||
displayError(err?.message)
|
||||
}
|
||||
return connected
|
||||
}
|
||||
|
||||
async function saveDatasource() {
|
||||
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||
const valid = await validateConfig()
|
||||
if (!valid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (!datasource.name) {
|
||||
datasource.name = name
|
||||
}
|
||||
const opts = {}
|
||||
if (datasourcePlus && selectedTables) {
|
||||
opts.tablesFilter = selectedTables
|
||||
}
|
||||
const resp = await save(datasource, opts)
|
||||
$goto(`./datasource/${resp._id}`)
|
||||
notifications.success("Datasource created successfully.")
|
||||
} catch (err) {
|
||||
notifications.error(err?.message ?? "Error saving datasource")
|
||||
// prevent the modal from closing
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function nextStep() {
|
||||
let connected = true
|
||||
if (datasourcePlus) {
|
||||
connected = await validateConfig()
|
||||
}
|
||||
if (!connected) {
|
||||
return false
|
||||
}
|
||||
if (datasourcePlus && !fetchTableStep) {
|
||||
notifications.success("Connected to datasource successfully.")
|
||||
const info = await getDatasourceInfo(datasource)
|
||||
tableList = info.tableNames
|
||||
fetchTableStep = true
|
||||
return false
|
||||
} else {
|
||||
await saveDatasource()
|
||||
return true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
{title}
|
||||
onConfirm={() => nextStep()}
|
||||
{confirmText}
|
||||
cancelText={fetchTableStep ? "Cancel" : "Back"}
|
||||
showSecondaryButton={datasourcePlus}
|
||||
size="L"
|
||||
disabled={!isValid}
|
||||
>
|
||||
<Layout noPadding>
|
||||
<Body size="XS">
|
||||
{#if !fetchTableStep}
|
||||
Connect your database to Budibase using the config below
|
||||
{:else}
|
||||
Choose what tables you want to sync with Budibase
|
||||
{/if}
|
||||
</Body>
|
||||
</Layout>
|
||||
{#if !fetchTableStep}
|
||||
<IntegrationConfigForm
|
||||
schema={datasource?.schema}
|
||||
bind:datasource
|
||||
creating={true}
|
||||
on:valid={e => (isValid = e.detail)}
|
||||
/>
|
||||
{:else}
|
||||
<div class="table-checkboxes">
|
||||
<FancyCheckboxGroup options={tableList} bind:selected={selectedTables} />
|
||||
</div>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
.table-checkboxes {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -1,7 +1,9 @@
|
|||
<script>
|
||||
import { datasources } from "stores/backend"
|
||||
import { get } from "svelte/store"
|
||||
import { datasources, integrations } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { Input, ModalContent, Modal } from "@budibase/bbui"
|
||||
import { integrationForDatasource } from "stores/selectors"
|
||||
|
||||
let error = ""
|
||||
let modal
|
||||
|
@ -32,7 +34,10 @@
|
|||
...datasource,
|
||||
name,
|
||||
}
|
||||
await datasources.save(updatedDatasource)
|
||||
await datasources.update({
|
||||
datasource: updatedDatasource,
|
||||
integration: integrationForDatasource(get(integrations), datasource),
|
||||
})
|
||||
notifications.success(`Datasource ${name} updated successfully.`)
|
||||
hide()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
<script>
|
||||
import {
|
||||
Modal,
|
||||
notifications,
|
||||
Body,
|
||||
Layout,
|
||||
ModalContent,
|
||||
} from "@budibase/bbui"
|
||||
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
|
||||
import ConfigInput from "./ConfigInput.svelte"
|
||||
import { createValidatedConfigStore } from "./stores/validatedConfig"
|
||||
import { createValidatedNameStore } from "./stores/validatedName"
|
||||
import { get } from "svelte/store"
|
||||
import { environment } from "stores/portal"
|
||||
|
||||
export let integration
|
||||
export let config
|
||||
export let onSubmit = () => {}
|
||||
export let showNameField = false
|
||||
export let nameFieldValue = ""
|
||||
|
||||
$: configStore = createValidatedConfigStore(integration, config)
|
||||
$: nameStore = createValidatedNameStore(nameFieldValue, showNameField)
|
||||
|
||||
const handleConfirm = async () => {
|
||||
configStore.markAllFieldsActive()
|
||||
nameStore.markActive()
|
||||
|
||||
if ((await configStore.validate()) && (await nameStore.validate())) {
|
||||
return await onSubmit({
|
||||
config: get(configStore).config,
|
||||
name: get(nameStore).name,
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
let createVariableModal
|
||||
let configValueSetterCallback = () => {}
|
||||
|
||||
const showModal = setter => {
|
||||
configValueSetterCallback = setter
|
||||
createVariableModal.show()
|
||||
}
|
||||
|
||||
async function saveVariable(data) {
|
||||
try {
|
||||
await environment.createVariable(data)
|
||||
configValueSetterCallback(`{{ env.${data.name} }}`)
|
||||
createVariableModal.hide()
|
||||
} catch (err) {
|
||||
notifications.error(`Failed to create variable: ${err.message}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={`Connect to ${integration.friendlyName}`}
|
||||
onConfirm={handleConfirm}
|
||||
confirmText={integration.plus ? "Connect" : "Save and continue to query"}
|
||||
cancelText="Back"
|
||||
disabled={$configStore.preventSubmit || $nameStore.preventSubmit}
|
||||
size="L"
|
||||
>
|
||||
<Layout noPadding>
|
||||
<Body size="XS">
|
||||
Connect your database to Budibase using the config below.
|
||||
</Body>
|
||||
</Layout>
|
||||
|
||||
{#if showNameField}
|
||||
<ConfigInput
|
||||
type="string"
|
||||
value={$nameStore.name}
|
||||
error={$nameStore.error}
|
||||
name="Name"
|
||||
showModal={() => showModal(nameStore.updateValue)}
|
||||
on:blur={nameStore.markActive}
|
||||
on:change={e => nameStore.updateValue(e.detail)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#each $configStore.validatedConfig as { type, key, value, error, name }}
|
||||
<ConfigInput
|
||||
{type}
|
||||
{value}
|
||||
{error}
|
||||
{name}
|
||||
showModal={() =>
|
||||
showModal(newValue => configStore.updateFieldValue(key, newValue))}
|
||||
on:blur={() => configStore.markFieldActive(key)}
|
||||
on:change={e => configStore.updateFieldValue(key, e.detail)}
|
||||
/>
|
||||
{/each}
|
||||
</ModalContent>
|
||||
|
||||
<Modal bind:this={createVariableModal}>
|
||||
<CreateEditVariableModal save={saveVariable} />
|
||||
</Modal>
|
|
@ -0,0 +1,129 @@
|
|||
import { derived, writable, get } from "svelte/store"
|
||||
import { getValidatorFields } from "./validation"
|
||||
import { capitalise } from "helpers"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { object } from "yup"
|
||||
|
||||
export const createValidatedConfigStore = (integration, config) => {
|
||||
const configStore = writable(config)
|
||||
const allValidators = getValidatorFields(integration)
|
||||
const selectedValidatorsStore = writable({})
|
||||
const errorsStore = writable({})
|
||||
|
||||
const validate = async () => {
|
||||
try {
|
||||
await object()
|
||||
.shape(get(selectedValidatorsStore))
|
||||
.validate(get(configStore), { abortEarly: false })
|
||||
|
||||
errorsStore.set({})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
// Yup error
|
||||
if (error.inner) {
|
||||
const errors = {}
|
||||
|
||||
error.inner.forEach(innerError => {
|
||||
errors[innerError.path] = capitalise(innerError.message)
|
||||
})
|
||||
|
||||
errorsStore.set(errors)
|
||||
} else {
|
||||
// Non-yup error
|
||||
notifications.error("Unexpected validation error")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const updateFieldValue = (key, value) => {
|
||||
configStore.update($configStore => {
|
||||
const newStore = { ...$configStore }
|
||||
|
||||
if (integration.datasource[key].type === "fieldGroup") {
|
||||
value.forEach(field => {
|
||||
newStore[field.key] = field.value
|
||||
})
|
||||
} else {
|
||||
newStore[key] = value
|
||||
}
|
||||
|
||||
return newStore
|
||||
})
|
||||
validate()
|
||||
}
|
||||
|
||||
const markAllFieldsActive = () => {
|
||||
selectedValidatorsStore.set(allValidators)
|
||||
validate()
|
||||
}
|
||||
|
||||
const markFieldActive = key => {
|
||||
selectedValidatorsStore.update($validatorsStore => ({
|
||||
...$validatorsStore,
|
||||
[key]: allValidators[key],
|
||||
}))
|
||||
validate()
|
||||
}
|
||||
|
||||
const combined = derived(
|
||||
[configStore, errorsStore, selectedValidatorsStore],
|
||||
([$configStore, $errorsStore, $selectedValidatorsStore]) => {
|
||||
const validatedConfig = []
|
||||
|
||||
Object.entries(integration.datasource).forEach(([key, properties]) => {
|
||||
if (integration.name === "REST" && key !== "rejectUnauthorized") {
|
||||
return
|
||||
}
|
||||
|
||||
const getValue = () => {
|
||||
if (properties.type === "fieldGroup") {
|
||||
return Object.entries(properties.fields).map(
|
||||
([fieldKey, fieldProperties]) => {
|
||||
return {
|
||||
key: fieldKey,
|
||||
name: capitalise(fieldProperties.display || fieldKey),
|
||||
type: fieldProperties.type,
|
||||
value: $configStore[fieldKey],
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return $configStore[key]
|
||||
}
|
||||
|
||||
validatedConfig.push({
|
||||
key,
|
||||
value: getValue(),
|
||||
error: $errorsStore[key],
|
||||
name: capitalise(properties.display || key),
|
||||
type: properties.type,
|
||||
})
|
||||
})
|
||||
|
||||
const allFieldsActive =
|
||||
Object.keys($selectedValidatorsStore).length ===
|
||||
Object.keys(allValidators).length
|
||||
|
||||
const hasErrors = Object.keys($errorsStore).length > 0
|
||||
|
||||
return {
|
||||
validatedConfig,
|
||||
config: $configStore,
|
||||
errors: $errorsStore,
|
||||
preventSubmit: allFieldsActive && hasErrors,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
subscribe: combined.subscribe,
|
||||
updateFieldValue,
|
||||
markAllFieldsActive,
|
||||
markFieldActive,
|
||||
validate,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import { derived, get, writable } from "svelte/store"
|
||||
import { capitalise } from "helpers"
|
||||
import { string } from "yup"
|
||||
|
||||
export const createValidatedNameStore = (name, isVisible) => {
|
||||
const nameStore = writable(name)
|
||||
const isActiveStore = writable(false)
|
||||
const errorStore = writable(null)
|
||||
|
||||
const validate = async () => {
|
||||
if (!isVisible || !get(isActiveStore)) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
await string().required().validate(get(nameStore), { abortEarly: false })
|
||||
|
||||
errorStore.set(null)
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
errorStore.set(capitalise(error.message))
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const updateValue = value => {
|
||||
nameStore.set(value)
|
||||
validate()
|
||||
}
|
||||
|
||||
const markActive = () => {
|
||||
isActiveStore.set(true)
|
||||
validate()
|
||||
}
|
||||
|
||||
const combined = derived(
|
||||
[nameStore, errorStore, isActiveStore],
|
||||
([$nameStore, $errorStore, $isActiveStore]) => ({
|
||||
name: $nameStore,
|
||||
error: $errorStore,
|
||||
preventSubmit: $errorStore !== null && $isActiveStore,
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
subscribe: combined.subscribe,
|
||||
updateValue,
|
||||
markActive,
|
||||
validate,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { string, number } from "yup"
|
||||
|
||||
const propertyValidator = type => {
|
||||
if (type === "number") {
|
||||
return number().nullable()
|
||||
}
|
||||
|
||||
if (type === "email") {
|
||||
return string().email().nullable()
|
||||
}
|
||||
|
||||
return string().nullable()
|
||||
}
|
||||
|
||||
export const getValidatorFields = integration => {
|
||||
const validatorFields = {}
|
||||
|
||||
Object.entries(integration?.datasource || {}).forEach(([key, properties]) => {
|
||||
if (properties.required) {
|
||||
validatorFields[key] = propertyValidator(properties.type).required()
|
||||
} else {
|
||||
validatorFields[key] = propertyValidator(properties.type).notRequired()
|
||||
}
|
||||
})
|
||||
|
||||
return validatorFields
|
||||
}
|
|
@ -21,6 +21,8 @@
|
|||
faColumns,
|
||||
faArrowsAlt,
|
||||
faQuestionCircle,
|
||||
faCircleCheck,
|
||||
faGear,
|
||||
} from "@fortawesome/free-solid-svg-icons"
|
||||
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
||||
|
||||
|
@ -48,8 +50,11 @@
|
|||
faEye,
|
||||
faColumns,
|
||||
faArrowsAlt,
|
||||
faQuestionCircle
|
||||
faQuestionCircle,
|
||||
// --
|
||||
|
||||
faCircleCheck,
|
||||
faGear
|
||||
)
|
||||
dom.watch()
|
||||
</script>
|
||||
|
|
|
@ -176,7 +176,10 @@
|
|||
notifications.success(`Request saved successfully`)
|
||||
if (dynamicVariables) {
|
||||
datasource.config.dynamicVariables = rebuildVariables(saveId)
|
||||
datasource = await datasources.save(datasource)
|
||||
datasource = await datasources.update({
|
||||
integration: integrationInfo,
|
||||
datasource,
|
||||
})
|
||||
}
|
||||
prettifyQueryRequestBody(
|
||||
query,
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Modal,
|
||||
notifications,
|
||||
Body,
|
||||
Layout,
|
||||
ModalContent,
|
||||
} from "@budibase/bbui"
|
||||
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
|
||||
import ConfigInput from "./ConfigInput.svelte"
|
||||
import { createConfigStore } from "./stores/config"
|
||||
import { createValidationStore } from "./stores/validation"
|
||||
import { createValidatedConfigStore } from "./stores/validatedConfig"
|
||||
import { datasources } from "stores/backend"
|
||||
import { get } from "svelte/store"
|
||||
import { environment } from "stores/portal"
|
||||
|
||||
export let integration
|
||||
export let config
|
||||
export let onDatasourceCreated = () => {}
|
||||
|
||||
$: configStore = createConfigStore(integration, config)
|
||||
$: validationStore = createValidationStore(integration)
|
||||
$: validatedConfigStore = createValidatedConfigStore(
|
||||
configStore,
|
||||
validationStore,
|
||||
integration
|
||||
)
|
||||
|
||||
const handleConfirm = async () => {
|
||||
validationStore.markAllFieldsActive()
|
||||
const config = get(configStore)
|
||||
|
||||
try {
|
||||
if (await validationStore.validate(config)) {
|
||||
const datasource = await datasources.create({
|
||||
integration,
|
||||
fields: config,
|
||||
})
|
||||
await onDatasourceCreated(datasource)
|
||||
} else {
|
||||
notifications.send("Invalid fields", {
|
||||
type: "error",
|
||||
icon: "Alert",
|
||||
autoDismiss: true,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// Do nothing on errors, alerts are handled by `datasources.create`
|
||||
}
|
||||
|
||||
// Prevent modal closing
|
||||
return false
|
||||
}
|
||||
|
||||
const handleBlur = key => {
|
||||
validationStore.markFieldActive(key)
|
||||
validationStore.validate(get(configStore))
|
||||
}
|
||||
|
||||
const handleChange = (key, newValue) => {
|
||||
configStore.updateFieldValue(key, newValue)
|
||||
validationStore.validate(get(configStore))
|
||||
}
|
||||
|
||||
let createVariableModal
|
||||
let selectedConfigKey
|
||||
|
||||
const showModal = key => {
|
||||
selectedConfigKey = key
|
||||
createVariableModal.show()
|
||||
}
|
||||
|
||||
async function save(data) {
|
||||
try {
|
||||
await environment.createVariable(data)
|
||||
configStore.updateFieldValue(selectedConfigKey, `{{ env.${data.name} }}`)
|
||||
createVariableModal.hide()
|
||||
} catch (err) {
|
||||
notifications.error(`Failed to create variable: ${err.message}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={`Connect to ${integration.friendlyName}`}
|
||||
onConfirm={handleConfirm}
|
||||
confirmText={integration.plus ? "Connect" : "Save and continue to query"}
|
||||
cancelText="Back"
|
||||
disabled={$validationStore.allFieldsActive && $validationStore.invalid}
|
||||
size="L"
|
||||
>
|
||||
<Layout noPadding>
|
||||
<Body size="XS">
|
||||
Connect your database to Budibase using the config below.
|
||||
</Body>
|
||||
</Layout>
|
||||
|
||||
{#each $validatedConfigStore as { type, key, value, error, name }}
|
||||
<ConfigInput
|
||||
{type}
|
||||
{value}
|
||||
{error}
|
||||
{name}
|
||||
showModal={() => showModal(key)}
|
||||
on:blur={() => handleBlur(key)}
|
||||
on:change={e => handleChange(key, e.detail)}
|
||||
/>
|
||||
{/each}
|
||||
</ModalContent>
|
||||
|
||||
<Modal bind:this={createVariableModal}>
|
||||
<CreateEditVariableModal {save} />
|
||||
</Modal>
|
|
@ -1,26 +0,0 @@
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
export const createConfigStore = (integration, config) => {
|
||||
const configStore = writable(config)
|
||||
|
||||
const updateFieldValue = (key, value) => {
|
||||
configStore.update($configStore => {
|
||||
const newStore = { ...$configStore }
|
||||
|
||||
if (integration.datasource[key].type === "fieldGroup") {
|
||||
value.forEach(field => {
|
||||
newStore[field.key] = field.value
|
||||
})
|
||||
} else {
|
||||
newStore[key] = value
|
||||
}
|
||||
|
||||
return newStore
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: configStore.subscribe,
|
||||
updateFieldValue,
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import { capitalise } from "helpers"
|
||||
import { derived } from "svelte/store"
|
||||
|
||||
export const createValidatedConfigStore = (
|
||||
configStore,
|
||||
validationStore,
|
||||
integration
|
||||
) => {
|
||||
return derived(
|
||||
[configStore, validationStore],
|
||||
([$configStore, $validationStore]) => {
|
||||
return Object.entries(integration.datasource).map(([key, properties]) => {
|
||||
const getValue = () => {
|
||||
if (properties.type === "fieldGroup") {
|
||||
return Object.entries(properties.fields).map(
|
||||
([fieldKey, fieldProperties]) => {
|
||||
return {
|
||||
key: fieldKey,
|
||||
name: capitalise(fieldProperties.display || fieldKey),
|
||||
type: fieldProperties.type,
|
||||
value: $configStore[fieldKey],
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return $configStore[key]
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
value: getValue(),
|
||||
error: $validationStore.errors[key],
|
||||
name: capitalise(properties.display || key),
|
||||
type: properties.type,
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
import { capitalise } from "helpers"
|
||||
import { object, string, number } from "yup"
|
||||
import { derived, writable, get } from "svelte/store"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
const propertyValidator = type => {
|
||||
if (type === "number") {
|
||||
return number().nullable()
|
||||
}
|
||||
|
||||
if (type === "email") {
|
||||
return string().email().nullable()
|
||||
}
|
||||
|
||||
return string().nullable()
|
||||
}
|
||||
|
||||
const getValidatorFields = integration => {
|
||||
const validatorFields = {}
|
||||
|
||||
Object.entries(integration?.datasource || {}).forEach(([key, properties]) => {
|
||||
if (properties.required) {
|
||||
validatorFields[key] = propertyValidator(properties.type).required()
|
||||
} else {
|
||||
validatorFields[key] = propertyValidator(properties.type).notRequired()
|
||||
}
|
||||
})
|
||||
|
||||
return validatorFields
|
||||
}
|
||||
|
||||
export const createValidationStore = integration => {
|
||||
const allValidators = getValidatorFields(integration)
|
||||
const selectedValidatorsStore = writable({})
|
||||
const errorsStore = writable({})
|
||||
|
||||
const markAllFieldsActive = () => {
|
||||
selectedValidatorsStore.set(allValidators)
|
||||
}
|
||||
|
||||
const markFieldActive = key => {
|
||||
selectedValidatorsStore.update($validatorsStore => ({
|
||||
...$validatorsStore,
|
||||
[key]: allValidators[key],
|
||||
}))
|
||||
}
|
||||
|
||||
const validate = async config => {
|
||||
try {
|
||||
await object()
|
||||
.shape(get(selectedValidatorsStore))
|
||||
.validate(config, { abortEarly: false })
|
||||
|
||||
errorsStore.set({})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
// Yup error
|
||||
if (error.inner) {
|
||||
const errors = {}
|
||||
|
||||
error.inner.forEach(innerError => {
|
||||
errors[innerError.path] = capitalise(innerError.message)
|
||||
})
|
||||
|
||||
errorsStore.set(errors)
|
||||
} else {
|
||||
// Non-yup error
|
||||
notifications.error("Unexpected validation error")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const combined = derived(
|
||||
[errorsStore, selectedValidatorsStore],
|
||||
([$errorsStore, $selectedValidatorsStore]) => {
|
||||
return {
|
||||
errors: $errorsStore,
|
||||
invalid: Object.keys($errorsStore).length > 0,
|
||||
allFieldsActive:
|
||||
Object.keys($selectedValidatorsStore).length ===
|
||||
Object.keys(allValidators).length,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
subscribe: combined.subscribe,
|
||||
markAllFieldsActive,
|
||||
markFieldActive,
|
||||
validate,
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<ModalContent
|
||||
{title}
|
||||
cancelText="Cancel"
|
||||
cancelText="Skip"
|
||||
size="L"
|
||||
{confirmText}
|
||||
onConfirm={() => store.importSelectedTables(onComplete)}
|
||||
|
|
|
@ -4,12 +4,14 @@
|
|||
import { IntegrationTypes } from "constants/backend"
|
||||
import GoogleAuthPrompt from "./GoogleAuthPrompt.svelte"
|
||||
|
||||
import { get } from "svelte/store"
|
||||
import TableImportSelection from "./TableImportSelection/index.svelte"
|
||||
import DatasourceConfigEditor from "./DatasourceConfigEditor/index.svelte"
|
||||
import DatasourceConfigEditor from "components/backend/Datasources/ConfigEditor/index.svelte"
|
||||
import { datasources } from "stores/backend"
|
||||
import { createOnGoogleAuthStore } from "./stores/onGoogleAuth.js"
|
||||
import { createDatasourceCreationStore } from "./stores/datasourceCreation.js"
|
||||
import { configFromIntegration } from "stores/selectors"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
export let loading = false
|
||||
const store = createDatasourceCreationStore()
|
||||
|
@ -55,6 +57,23 @@
|
|||
store.setConfig(config)
|
||||
store.editConfigStage()
|
||||
})
|
||||
|
||||
const createDatasource = async config => {
|
||||
try {
|
||||
const datasource = await datasources.create({
|
||||
integration: get(store).integration,
|
||||
config,
|
||||
})
|
||||
store.setDatasource(datasource)
|
||||
|
||||
notifications.success("Datasource created successfully")
|
||||
} catch (e) {
|
||||
notifications.error(`Error creating datasource: ${e.message}`)
|
||||
}
|
||||
|
||||
// Prevent modal closing
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal on:hide={store.cancel} bind:this={modal}>
|
||||
|
@ -64,7 +83,7 @@
|
|||
<DatasourceConfigEditor
|
||||
integration={$store.integration}
|
||||
config={$store.config}
|
||||
onDatasourceCreated={store.setDatasource}
|
||||
onSubmit={({ config }) => createDatasource(config)}
|
||||
/>
|
||||
{:else if $store.stage === "selectTables"}
|
||||
<TableImportSelection
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
<script>
|
||||
import { Modal, notifications } from "@budibase/bbui"
|
||||
import { integrationForDatasource } from "stores/selectors"
|
||||
import { datasources, integrations } from "stores/backend"
|
||||
import DatasourceConfigEditor from "components/backend/Datasources/ConfigEditor/index.svelte"
|
||||
import EditDatasourceConfigButton from "./EditDatasourceConfigButton.svelte"
|
||||
|
||||
export let datasource
|
||||
|
||||
$: integration = integrationForDatasource($integrations, datasource)
|
||||
|
||||
let modal
|
||||
|
||||
async function saveDatasource({ config, name }) {
|
||||
try {
|
||||
await datasources.update({
|
||||
integration,
|
||||
datasource: { ...datasource, config, name },
|
||||
})
|
||||
|
||||
notifications.success(
|
||||
`Datasource ${datasource.name} updated successfully`
|
||||
)
|
||||
} catch (err) {
|
||||
notifications.error(err?.message ?? "Error saving datasource")
|
||||
// prevent the modal from closing
|
||||
return false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<EditDatasourceConfigButton on:click={modal.show} {datasource} />
|
||||
<Modal bind:this={modal}>
|
||||
<DatasourceConfigEditor
|
||||
{integration}
|
||||
config={datasource.config}
|
||||
showNameField
|
||||
nameFieldValue={datasource.name}
|
||||
onSubmit={saveDatasource}
|
||||
/>
|
||||
</Modal>
|
|
@ -0,0 +1,117 @@
|
|||
<script>
|
||||
import { Body } from "@budibase/bbui"
|
||||
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
||||
import { IntegrationTypes } from "constants/backend"
|
||||
export let datasource
|
||||
const getSubtitle = datasource => {
|
||||
if (datasource.source === IntegrationTypes.REST) {
|
||||
return datasource.name
|
||||
}
|
||||
if (
|
||||
datasource.source === IntegrationTypes.POSTGRES ||
|
||||
datasource.source === IntegrationTypes.MYSQL ||
|
||||
datasource.source === IntegrationTypes.ORACLE ||
|
||||
datasource.source === IntegrationTypes.REDIS
|
||||
) {
|
||||
return `${datasource.config.host}:${datasource.config.port}`
|
||||
}
|
||||
if (datasource.source === IntegrationTypes.SQL_SERVER) {
|
||||
return `${datasource.config.server}:${datasource.config.port}`
|
||||
}
|
||||
if (datasource.source === IntegrationTypes.SNOWFLAKE) {
|
||||
return `${datasource.config.warehouse}:${datasource.config.database}:${datasource.config.schema}`
|
||||
}
|
||||
if (datasource.source === IntegrationTypes.ARANGODB) {
|
||||
return `${datasource.config.url}:${datasource.config.databaseName}`
|
||||
}
|
||||
if (datasource.source === IntegrationTypes.COUCHDB) {
|
||||
return datasource.config.database
|
||||
}
|
||||
if (
|
||||
datasource.source === IntegrationTypes.DYNAMODB ||
|
||||
datasource.source === IntegrationTypes.S3
|
||||
) {
|
||||
return `${datasource.config.endpoint}:${datasource.config.region}`
|
||||
}
|
||||
if (datasource.source === IntegrationTypes.ELASTICSEARCH) {
|
||||
return datasource.config.url
|
||||
}
|
||||
if (datasource.source === IntegrationTypes.FIRESTORE) {
|
||||
return datasource.config.projectId
|
||||
}
|
||||
if (datasource.source === IntegrationTypes.MONGODB) {
|
||||
return datasource.config.db
|
||||
}
|
||||
if (datasource.source === IntegrationTypes.AIRTABLE) {
|
||||
return datasource.config.base
|
||||
}
|
||||
if (datasource.source === IntegrationTypes.GOOGLE_SHEETS) {
|
||||
return datasource.config.spreadsheetId
|
||||
}
|
||||
}
|
||||
$: subtitle = getSubtitle(datasource)
|
||||
</script>
|
||||
|
||||
<div class="button" on:click>
|
||||
<div class="left">
|
||||
{#if datasource.source !== IntegrationTypes.REST}
|
||||
<div class="connected">
|
||||
<FontAwesomeIcon name="fa-solid fa-circle-check" />
|
||||
<Body size="S">Connected</Body>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="truncate">
|
||||
<Body>{getSubtitle(datasource)}</Body>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<FontAwesomeIcon name="fa-solid fa-gear" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.button {
|
||||
display: flex;
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: 5px;
|
||||
width: 100%;
|
||||
background-color: #00000047;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
padding: 12px 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.left {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 16px;
|
||||
}
|
||||
.right :global(svg) {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
.button:hover {
|
||||
cursor: pointer;
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
.connected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.connected :global(svg) {
|
||||
margin-right: 6px;
|
||||
color: #009562;
|
||||
}
|
||||
.connected :global(p) {
|
||||
color: #009562;
|
||||
}
|
||||
.truncate :global(p) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
|
@ -11,7 +11,7 @@
|
|||
Modal,
|
||||
} from "@budibase/bbui"
|
||||
import { datasources, integrations, queries, tables } from "stores/backend"
|
||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||
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"
|
||||
|
@ -20,8 +20,6 @@
|
|||
import { isEqual } from "lodash"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
||||
import { API } from "api"
|
||||
import { DatasourceFeature } from "@budibase/types"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
|
||||
const querySchema = {
|
||||
|
@ -49,35 +47,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function validateConfig() {
|
||||
const displayError = message =>
|
||||
notifications.error(message ?? "Error validating datasource")
|
||||
|
||||
let connected = false
|
||||
try {
|
||||
const resp = await API.validateDatasource(datasource)
|
||||
if (!resp.connected) {
|
||||
displayError(`Unable to connect - ${resp.error}`)
|
||||
}
|
||||
connected = resp.connected
|
||||
} catch (err) {
|
||||
displayError(err?.message)
|
||||
}
|
||||
return connected
|
||||
}
|
||||
|
||||
const saveDatasource = async () => {
|
||||
loading = true
|
||||
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||
const valid = await validateConfig()
|
||||
if (!valid) {
|
||||
loading = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
try {
|
||||
// Create datasource
|
||||
await datasources.save(datasource)
|
||||
await datasources.update({ datasource, integration })
|
||||
if (datasource?.plus) {
|
||||
await tables.fetch()
|
||||
}
|
||||
|
@ -86,8 +59,6 @@
|
|||
baseDatasource = cloneDeep(datasource)
|
||||
} catch (err) {
|
||||
notifications.error(`Error saving datasource: ${err}`)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -120,30 +91,8 @@
|
|||
/>
|
||||
<Heading size="M">{$datasources.selected?.name}</Heading>
|
||||
</header>
|
||||
<Body size="M">{integration.description}</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
<div class="config-header">
|
||||
<Heading size="S">Configuration</Heading>
|
||||
<Button
|
||||
disabled={!changed || !isValid || loading}
|
||||
cta
|
||||
on:click={saveDatasource}
|
||||
>
|
||||
<div class="save-button-content">
|
||||
{#if loading}
|
||||
<Spinner size="10">Save</Spinner>
|
||||
{/if}
|
||||
Save
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<IntegrationConfigForm
|
||||
on:change={hasChanged}
|
||||
schema={integration.datasource}
|
||||
bind:datasource
|
||||
on:valid={e => (isValid = e.detail)}
|
||||
/>
|
||||
<EditDatasourceConfig {datasource} />
|
||||
{#if datasource.plus}
|
||||
<PlusConfigForm bind:datasource save={saveDatasource} />
|
||||
{/if}
|
||||
|
@ -190,7 +139,21 @@
|
|||
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>
|
||||
</section>
|
||||
|
@ -208,12 +171,6 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.config-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.query-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -3,7 +3,6 @@ import { IntegrationTypes, DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
|
|||
import { queries, tables } from "./"
|
||||
import { API } from "api"
|
||||
import { DatasourceFeature } from "@budibase/types"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
export class ImportTableError extends Error {
|
||||
constructor(message) {
|
||||
|
@ -90,41 +89,44 @@ export function createDatasourcesStore() {
|
|||
.length
|
||||
}
|
||||
|
||||
const create = async ({ integration, fields }) => {
|
||||
try {
|
||||
const isDatasourceInvalid = async (integration, datasource) => {
|
||||
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||
const { connected } = await API.validateDatasource(datasource)
|
||||
if (!connected) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const create = async ({ integration, config }) => {
|
||||
const datasource = {
|
||||
type: "datasource",
|
||||
source: integration.name,
|
||||
config: fields,
|
||||
name: `${integration.friendlyName}-${
|
||||
sourceCount(integration.name) + 1
|
||||
}`,
|
||||
config,
|
||||
name: `${integration.friendlyName}-${sourceCount(integration.name) + 1}`,
|
||||
plus: integration.plus && integration.name !== IntegrationTypes.REST,
|
||||
}
|
||||
|
||||
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||
const { connected } = await API.validateDatasource(datasource)
|
||||
if (!connected) throw new Error("Unable to connect")
|
||||
if (await isDatasourceInvalid(integration, datasource)) {
|
||||
throw new Error("Unable to connect")
|
||||
}
|
||||
|
||||
const response = await API.createDatasource({
|
||||
datasource,
|
||||
fetchSchema:
|
||||
integration.plus &&
|
||||
integration.name !== IntegrationTypes.GOOGLE_SHEETS,
|
||||
integration.plus && integration.name !== IntegrationTypes.GOOGLE_SHEETS,
|
||||
})
|
||||
|
||||
notifications.success("Datasource created successfully.")
|
||||
|
||||
return updateDatasource(response)
|
||||
} catch (e) {
|
||||
notifications.error(`Error creating datasource: ${e.message}`)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const save = async body => {
|
||||
const response = await API.updateDatasource(body)
|
||||
const update = async ({ integration, datasource }) => {
|
||||
if (await isDatasourceInvalid(integration, datasource)) {
|
||||
throw new Error("Unable to connect")
|
||||
}
|
||||
|
||||
const response = await API.updateDatasource(datasource)
|
||||
|
||||
return updateDatasource(response)
|
||||
}
|
||||
|
||||
|
@ -198,7 +200,7 @@ export function createDatasourcesStore() {
|
|||
select,
|
||||
updateSchema,
|
||||
create,
|
||||
save,
|
||||
update,
|
||||
delete: deleteDatasource,
|
||||
removeSchemaError,
|
||||
replaceDatasource,
|
||||
|
|
Loading…
Reference in New Issue