Update datasource config editing
This commit is contained in:
parent
1606ca0c84
commit
3771ab2ffc
|
@ -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>
|
|
|
@ -45,6 +45,9 @@
|
||||||
<Heading size="S">Headers</Heading>
|
<Heading size="S">Headers</Heading>
|
||||||
<Badge quiet grey>Optional</Badge>
|
<Badge quiet grey>Optional</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="headerRight">
|
||||||
|
<slot name="headerRight" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Headers enable you to provide additional information about the request, such
|
Headers enable you to provide additional information about the request, such
|
||||||
|
@ -69,6 +72,9 @@
|
||||||
<Heading size="S">Authentication</Heading>
|
<Heading size="S">Authentication</Heading>
|
||||||
<Badge quiet grey>Optional</Badge>
|
<Badge quiet grey>Optional</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="headerRight">
|
||||||
|
<slot name="headerRight" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Create an authentication config that can be shared with queries.
|
Create an authentication config that can be shared with queries.
|
||||||
|
@ -81,6 +87,9 @@
|
||||||
<Heading size="S">Variables</Heading>
|
<Heading size="S">Variables</Heading>
|
||||||
<Badge quiet grey>Optional</Badge>
|
<Badge quiet grey>Optional</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="headerRight">
|
||||||
|
<slot name="headerRight" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Body size="S"
|
<Body size="S"
|
||||||
>Variables enable you to store and re-use values in queries, with the choice
|
>Variables enable you to store and re-use values in queries, with the choice
|
||||||
|
@ -110,6 +119,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.section-header {
|
.section-header {
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -120,4 +130,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.headerRight {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
</style>
|
</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>
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
<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,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
notifications.send("Invalid fields", {
|
||||||
|
type: "error",
|
||||||
|
icon: "Alert",
|
||||||
|
autoDismiss: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Prevent modal closing
|
||||||
|
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,125 @@
|
||||||
|
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).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: $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,
|
faColumns,
|
||||||
faArrowsAlt,
|
faArrowsAlt,
|
||||||
faQuestionCircle,
|
faQuestionCircle,
|
||||||
|
faCircleCheck,
|
||||||
|
faGear,
|
||||||
} from "@fortawesome/free-solid-svg-icons"
|
} from "@fortawesome/free-solid-svg-icons"
|
||||||
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
||||||
|
|
||||||
|
@ -48,8 +50,11 @@
|
||||||
faEye,
|
faEye,
|
||||||
faColumns,
|
faColumns,
|
||||||
faArrowsAlt,
|
faArrowsAlt,
|
||||||
faQuestionCircle
|
faQuestionCircle,
|
||||||
// --
|
// --
|
||||||
|
|
||||||
|
faCircleCheck,
|
||||||
|
faGear
|
||||||
)
|
)
|
||||||
dom.watch()
|
dom.watch()
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -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
|
<ModalContent
|
||||||
{title}
|
{title}
|
||||||
cancelText="Cancel"
|
cancelText="Skip"
|
||||||
size="L"
|
size="L"
|
||||||
{confirmText}
|
{confirmText}
|
||||||
onConfirm={() => store.importSelectedTables(onComplete)}
|
onConfirm={() => store.importSelectedTables(onComplete)}
|
||||||
|
|
|
@ -4,12 +4,14 @@
|
||||||
import { IntegrationTypes } from "constants/backend"
|
import { IntegrationTypes } from "constants/backend"
|
||||||
import GoogleAuthPrompt from "./GoogleAuthPrompt.svelte"
|
import GoogleAuthPrompt from "./GoogleAuthPrompt.svelte"
|
||||||
|
|
||||||
|
import { get } from "svelte/store"
|
||||||
import TableImportSelection from "./TableImportSelection/index.svelte"
|
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 { datasources } from "stores/backend"
|
||||||
import { createOnGoogleAuthStore } from "./stores/onGoogleAuth.js"
|
import { createOnGoogleAuthStore } from "./stores/onGoogleAuth.js"
|
||||||
import { createDatasourceCreationStore } from "./stores/datasourceCreation.js"
|
import { createDatasourceCreationStore } from "./stores/datasourceCreation.js"
|
||||||
import { configFromIntegration } from "stores/selectors"
|
import { configFromIntegration } from "stores/selectors"
|
||||||
|
import { notifications } from "@budibase/bbui"
|
||||||
|
|
||||||
export let loading = false
|
export let loading = false
|
||||||
const store = createDatasourceCreationStore()
|
const store = createDatasourceCreationStore()
|
||||||
|
@ -55,6 +57,23 @@
|
||||||
store.setConfig(config)
|
store.setConfig(config)
|
||||||
store.editConfigStage()
|
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>
|
</script>
|
||||||
|
|
||||||
<Modal on:hide={store.cancel} bind:this={modal}>
|
<Modal on:hide={store.cancel} bind:this={modal}>
|
||||||
|
@ -64,7 +83,7 @@
|
||||||
<DatasourceConfigEditor
|
<DatasourceConfigEditor
|
||||||
integration={$store.integration}
|
integration={$store.integration}
|
||||||
config={$store.config}
|
config={$store.config}
|
||||||
onDatasourceCreated={store.setDatasource}
|
onSubmit={({ config }) => createDatasource(config)}
|
||||||
/>
|
/>
|
||||||
{:else if $store.stage === "selectTables"}
|
{:else if $store.stage === "selectTables"}
|
||||||
<TableImportSelection
|
<TableImportSelection
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
<script>
|
||||||
|
import { Modal, notifications } from "@budibase/bbui"
|
||||||
|
import { integrationForDatasource } from "stores/selectors"
|
||||||
|
import { datasources, integrations, tables } 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 },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (datasource?.plus) {
|
||||||
|
await tables.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
await datasources.fetch()
|
||||||
|
|
||||||
|
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,
|
Modal,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { datasources, integrations, queries, tables } from "stores/backend"
|
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 RestExtraConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/rest/RestExtraConfigForm.svelte"
|
||||||
import PlusConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/PlusConfigForm.svelte"
|
import PlusConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/PlusConfigForm.svelte"
|
||||||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||||
|
@ -20,8 +20,6 @@
|
||||||
import { isEqual } from "lodash"
|
import { isEqual } from "lodash"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
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"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
|
|
||||||
const querySchema = {
|
const querySchema = {
|
||||||
|
@ -49,32 +47,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 () => {
|
const saveDatasource = async () => {
|
||||||
loading = true
|
|
||||||
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
|
|
||||||
const valid = await validateConfig()
|
|
||||||
if (!valid) {
|
|
||||||
loading = false
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
// Create datasource
|
// Create datasource
|
||||||
await datasources.save(datasource)
|
await datasources.save(datasource)
|
||||||
|
@ -86,8 +59,6 @@
|
||||||
baseDatasource = cloneDeep(datasource)
|
baseDatasource = cloneDeep(datasource)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(`Error saving datasource: ${err}`)
|
notifications.error(`Error saving datasource: ${err}`)
|
||||||
} finally {
|
|
||||||
loading = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,30 +91,8 @@
|
||||||
/>
|
/>
|
||||||
<Heading size="M">{$datasources.selected?.name}</Heading>
|
<Heading size="M">{$datasources.selected?.name}</Heading>
|
||||||
</header>
|
</header>
|
||||||
<Body size="M">{integration.description}</Body>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider />
|
<EditDatasourceConfig {datasource} />
|
||||||
<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)}
|
|
||||||
/>
|
|
||||||
{#if datasource.plus}
|
{#if datasource.plus}
|
||||||
<PlusConfigForm bind:datasource save={saveDatasource} />
|
<PlusConfigForm bind:datasource save={saveDatasource} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -190,7 +139,21 @@
|
||||||
queries={queryList}
|
queries={queryList}
|
||||||
bind:datasource
|
bind:datasource
|
||||||
on:change={hasChanged}
|
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}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</section>
|
</section>
|
||||||
|
@ -208,12 +171,6 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin: 0 0 var(--spacing-xs) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.query-header {
|
.query-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { IntegrationTypes, DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
|
||||||
import { queries, tables } from "./"
|
import { queries, tables } from "./"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { DatasourceFeature } from "@budibase/types"
|
import { DatasourceFeature } from "@budibase/types"
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export class ImportTableError extends Error {
|
export class ImportTableError extends Error {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
|
@ -90,41 +89,56 @@ export function createDatasourcesStore() {
|
||||||
.length
|
.length
|
||||||
}
|
}
|
||||||
|
|
||||||
const create = async ({ integration, fields }) => {
|
const isDatasourceInvalid = async (integration, datasource) => {
|
||||||
try {
|
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||||
|
const { connected } = await API.validateDatasource(datasource)
|
||||||
|
console.log(connected)
|
||||||
|
if (!connected) return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const create = async ({ integration, config }) => {
|
||||||
const datasource = {
|
const datasource = {
|
||||||
type: "datasource",
|
type: "datasource",
|
||||||
source: integration.name,
|
source: integration.name,
|
||||||
config: fields,
|
config,
|
||||||
name: `${integration.friendlyName}-${
|
name: `${integration.friendlyName}-${sourceCount(integration.name) + 1}`,
|
||||||
sourceCount(integration.name) + 1
|
|
||||||
}`,
|
|
||||||
plus: integration.plus && integration.name !== IntegrationTypes.REST,
|
plus: integration.plus && integration.name !== IntegrationTypes.REST,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
|
if (await isDatasourceInvalid(integration, datasource)) {
|
||||||
const { connected } = await API.validateDatasource(datasource)
|
throw new Error("Unable to connect")
|
||||||
if (!connected) throw new Error("Unable to connect")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await API.createDatasource({
|
const response = await API.createDatasource({
|
||||||
datasource,
|
datasource,
|
||||||
fetchSchema:
|
fetchSchema:
|
||||||
integration.plus &&
|
integration.plus && integration.name !== IntegrationTypes.GOOGLE_SHEETS,
|
||||||
integration.name !== IntegrationTypes.GOOGLE_SHEETS,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
notifications.success("Datasource created successfully.")
|
return updateDatasource(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
return updateDatasource(response)
|
||||||
} catch (e) {
|
|
||||||
notifications.error(`Error creating datasource: ${e.message}`)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = async body => {
|
// TODO deprecate
|
||||||
const response = await API.updateDatasource(body)
|
const save = async datasource => {
|
||||||
|
if (await isDatasourceInvalid(datasource)) {
|
||||||
|
throw new Error("Unable to connect")
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await API.updateDatasource(datasource)
|
||||||
|
|
||||||
return updateDatasource(response)
|
return updateDatasource(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,6 +212,7 @@ export function createDatasourcesStore() {
|
||||||
select,
|
select,
|
||||||
updateSchema,
|
updateSchema,
|
||||||
create,
|
create,
|
||||||
|
update,
|
||||||
save,
|
save,
|
||||||
delete: deleteDatasource,
|
delete: deleteDatasource,
|
||||||
removeSchemaError,
|
removeSchemaError,
|
||||||
|
|
Loading…
Reference in New Issue