Merge pull request #1123 from Budibase/rest-api-integration
Rest api integration
This commit is contained in:
commit
8b248008c3
|
@ -1,15 +1,37 @@
|
|||
<script>
|
||||
import { Input, TextArea, Spacer } from "@budibase/bbui"
|
||||
import { Label, Input, TextArea, Spacer } from "@budibase/bbui"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
|
||||
export let integration
|
||||
|
||||
let unsaved = false
|
||||
</script>
|
||||
|
||||
<form>
|
||||
{#each Object.keys(integration) as configKey}
|
||||
<Input
|
||||
type={integration[configKey].type}
|
||||
label={configKey}
|
||||
bind:value={integration[configKey]} />
|
||||
<Spacer large />
|
||||
{#if typeof integration[configKey] === 'object'}
|
||||
<Label small>{configKey}</Label>
|
||||
<Spacer small />
|
||||
<KeyValueBuilder bind:object={integration[configKey]} on:change />
|
||||
{:else}
|
||||
<div class="form-row">
|
||||
<Label small>{configKey}</Label>
|
||||
<Input
|
||||
outline
|
||||
type={integration[configKey].type}
|
||||
on:change
|
||||
bind:value={integration[configKey]} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
import { onMount } from "svelte"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import api from "builderStore/api"
|
||||
import { Input, TextArea, Spacer } from "@budibase/bbui"
|
||||
import { Input, Label, TextArea, Spacer } from "@budibase/bbui"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
import ICONS from "../icons"
|
||||
|
||||
export let integration = {}
|
||||
|
@ -49,17 +50,6 @@
|
|||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if schema}
|
||||
{#each Object.keys(schema) as configKey}
|
||||
<Input
|
||||
thin
|
||||
type={schema[configKey].type}
|
||||
label={configKey}
|
||||
bind:value={integration[configKey]} />
|
||||
<Spacer medium />
|
||||
{/each}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<script>
|
||||
export let width = "100"
|
||||
export let height = "100"
|
||||
</script>
|
||||
|
||||
<svg
|
||||
{width}
|
||||
{height}
|
||||
viewBox="0 0 120 120"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M103.125 20.625H68.25L60.375
|
||||
36.375H16.5V99.375H111V20.625H103.125ZM103.125 36.375H72.375L76.5
|
||||
28.5H103.125V36.375Z"
|
||||
fill="#FFBA58" />
|
||||
<path
|
||||
d="M75 46.875V52.5H60C58.0127 52.5059 56.1085 53.298 54.7033 54.7033C53.298
|
||||
56.1085 52.5059 58.0127 52.5 60V75H46.875C44.3886 75 42.004 75.9877 40.2459
|
||||
77.7459C38.4877 79.504 37.5 81.8886 37.5 84.375C37.5 86.8614 38.4877 89.246
|
||||
40.2459 91.0041C42.004 92.7623 44.3886 93.75 46.875
|
||||
93.75H52.5V108.75C52.5059 110.737 53.298 112.642 54.7033 114.047C56.1085
|
||||
115.452 58.0127 116.244 60 116.25H74.25V110.625C74.25 107.94 75.3167 105.364
|
||||
77.2155 103.466C79.1143 101.567 81.6897 100.5 84.375 100.5C87.0603 100.5
|
||||
89.6357 101.567 91.5345 103.466C93.4333 105.364 94.5 107.94 94.5
|
||||
110.625V116.25H108.75C110.737 116.244 112.642 115.452 114.047
|
||||
114.047C115.452 112.642 116.244 110.737 116.25 108.75V94.5H110.625C107.94
|
||||
94.5 105.364 93.4333 103.466 91.5345C101.567 89.6357 100.5 87.0603 100.5
|
||||
84.375C100.5 81.6897 101.567 79.1143 103.466 77.2155C105.364 75.3167 107.94
|
||||
74.25 110.625 74.25H116.25V60C116.244 58.0127 115.452 56.1085 114.047
|
||||
54.7033C112.642 53.298 110.737 52.5059 108.75 52.5H93.75V46.875C93.75
|
||||
44.3886 92.7623 42.004 91.0041 40.2459C89.246 38.4877 86.8614 37.5 84.375
|
||||
37.5C81.8886 37.5 79.504 38.4877 77.7459 40.2459C75.9877 42.004 75 44.3886
|
||||
75 46.875Z"
|
||||
fill="#E76A00" />
|
||||
</svg>
|
|
@ -8,6 +8,7 @@ import Airtable from "./Airtable.svelte"
|
|||
import SqlServer from "./SQLServer.svelte"
|
||||
import MySQL from "./MySQL.svelte"
|
||||
import ArangoDB from "./ArangoDB.svelte"
|
||||
import Rest from "./Rest.svelte"
|
||||
|
||||
export default {
|
||||
POSTGRES: Postgres,
|
||||
|
@ -20,4 +21,5 @@ export default {
|
|||
AIRTABLE: Airtable,
|
||||
MYSQL: MySQL,
|
||||
ARANGODB: ArangoDB,
|
||||
REST: Rest,
|
||||
}
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
<script>
|
||||
import { goto, params } from "@sveltech/routify"
|
||||
import { backendUiStore, store } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import { Input, Label, ModalContent, Button, Spacer } from "@budibase/bbui"
|
||||
import TableIntegrationMenu from "../TableIntegrationMenu/index.svelte"
|
||||
import analytics from "analytics"
|
||||
|
||||
let modal
|
||||
let error = ""
|
||||
|
||||
let name
|
||||
let source
|
||||
let integration
|
||||
let datasource
|
||||
|
||||
function checkValid(evt) {
|
||||
const datasourceName = evt.target.value
|
||||
if (
|
||||
$backendUiStore.datasources?.some(
|
||||
datasource => datasource.name === datasourceName
|
||||
)
|
||||
) {
|
||||
error = `Datasource with name ${tableName} already exists. Please choose another name.`
|
||||
return
|
||||
}
|
||||
error = ""
|
||||
}
|
||||
|
||||
async function saveDatasource() {
|
||||
const { type, ...config } = integration
|
||||
|
||||
// Create datasource
|
||||
await backendUiStore.actions.datasources.save({
|
||||
name,
|
||||
source: type,
|
||||
config,
|
||||
})
|
||||
notifier.success(`Datasource ${name} created successfully.`)
|
||||
analytics.captureEvent("Datasource Created", { name })
|
||||
|
||||
// Navigate to new datasource
|
||||
$goto(`./datasource/${datasource._id}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title="Create Datasource"
|
||||
confirmText="Create"
|
||||
onConfirm={saveDatasource}
|
||||
disabled={error || !name}>
|
||||
<Input
|
||||
data-cy="datasource-name-input"
|
||||
thin
|
||||
label="Datasource Name"
|
||||
on:input={checkValid}
|
||||
bind:value={name}
|
||||
{error} />
|
||||
<Label grey extraSmall>Create Integrated Table from External Source</Label>
|
||||
<TableIntegrationMenu bind:integration />
|
||||
</ModalContent>
|
|
@ -3,7 +3,6 @@
|
|||
import { notifier } from "builderStore/store/notifications"
|
||||
import { DropdownMenu, Button, Input } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
|
||||
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
|
||||
|
||||
export let datasource
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
<script>
|
||||
import { backendUiStore, store, allScreens } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import { DropdownMenu, Button, Input, TextButton, Icon } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
|
||||
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
|
||||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
||||
|
||||
export let bindable
|
||||
export let parameters
|
||||
|
||||
let anchor
|
||||
let dropdown
|
||||
let confirmDeleteDialog
|
||||
|
||||
function hideEditor() {
|
||||
dropdown?.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div on:click|stopPropagation bind:this={anchor}>
|
||||
<TextButton text on:click={dropdown.show} active={false}>
|
||||
<Icon name="add" />
|
||||
Add Parameters
|
||||
</TextButton>
|
||||
<DropdownMenu align="right" {anchor} bind:this={dropdown}>
|
||||
<div class="wrapper">
|
||||
<ParameterBuilder bind:parameters {bindable} />
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
padding: var(--spacing-xl);
|
||||
min-width: 600px;
|
||||
}
|
||||
</style>
|
|
@ -3,7 +3,6 @@
|
|||
import { notifier } from "builderStore/store/notifications"
|
||||
import { DropdownMenu, Button, Input } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
|
||||
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
|
||||
|
||||
export let query
|
||||
|
|
|
@ -109,6 +109,7 @@
|
|||
</div>
|
||||
<div class="drawer-contents" slot="body">
|
||||
<IntegrationQueryEditor
|
||||
datasource={$backendUiStore.datasources.find(ds => ds._id === value.datasourceId)}
|
||||
query={value}
|
||||
schema={fetchDatasourceSchema(value)}
|
||||
editable={false} />
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<script>
|
||||
import { Button, Input } from "@budibase/bbui"
|
||||
|
||||
export let object = {}
|
||||
export let readOnly
|
||||
|
||||
let fields = Object.entries(object).map(([name, value]) => ({ name, value }))
|
||||
|
||||
$: object = fields.reduce(
|
||||
(acc, next) => ({ ...acc, [next.name]: next.value }),
|
||||
{}
|
||||
)
|
||||
|
||||
function addEntry() {
|
||||
fields = [...fields, {}]
|
||||
}
|
||||
|
||||
function deleteEntry(idx) {
|
||||
fields.splice(idx, 1)
|
||||
fields = fields
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Builds Objects with Key Value Pairs. Useful for building things like Request Headers. -->
|
||||
<div class="container" class:readOnly>
|
||||
{#each fields as field, idx}
|
||||
<Input placeholder="Key" thin outline bind:value={field.name} />
|
||||
<Input placeholder="Value" thin outline bind:value={field.value} />
|
||||
{#if !readOnly}
|
||||
<i class="ri-close-circle-fill" on:click={() => deleteEntry(idx)} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{#if !readOnly}
|
||||
<Button secondary thin outline on:click={addEntry}>Add</Button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 20px;
|
||||
grid-gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
|
||||
.ri-close-circle-fill {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import CodeMirror from "./codemirror"
|
||||
import { Label, Spacer } from "@budibase/bbui"
|
||||
import { onMount, createEventDispatcher } from "svelte"
|
||||
import { themeStore } from "builderStore"
|
||||
import { handlebarsCompletions } from "constants/completions"
|
||||
|
@ -11,6 +12,7 @@
|
|||
LIGHT: "default",
|
||||
}
|
||||
|
||||
export let label
|
||||
export let value = ""
|
||||
export let readOnly = false
|
||||
export let lineNumbers = true
|
||||
|
@ -169,6 +171,8 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Label small>{label}</Label>
|
||||
<Spacer medium />
|
||||
<textarea tabindex="0" bind:this={refs.editor} readonly {value} />
|
||||
|
||||
<style>
|
||||
|
|
|
@ -6,8 +6,10 @@
|
|||
Input,
|
||||
Heading,
|
||||
Select,
|
||||
Spacer,
|
||||
} from "@budibase/bbui"
|
||||
import Editor from "./QueryEditor.svelte"
|
||||
import KeyValueBuilder from "./KeyValueBuilder.svelte"
|
||||
|
||||
export let fields = {}
|
||||
export let schema
|
||||
|
@ -26,13 +28,33 @@
|
|||
<form on:submit|preventDefault>
|
||||
<div class="field">
|
||||
{#each schemaKeys as field}
|
||||
<Input
|
||||
placeholder="Enter {field} name"
|
||||
outline
|
||||
disabled={!editable}
|
||||
type={schema.fields[field]?.type}
|
||||
required={schema.fields[field]?.required}
|
||||
bind:value={fields[field]} />
|
||||
{#if schema.fields[field]?.type === 'object'}
|
||||
<div>
|
||||
<Label small>{field}</Label>
|
||||
<Spacer small />
|
||||
<KeyValueBuilder readOnly={!editable} bind:object={fields[field]} />
|
||||
</div>
|
||||
{:else if schema.fields[field]?.type === 'json'}
|
||||
<div>
|
||||
<Label extraSmall grey>{field}</Label>
|
||||
<Editor
|
||||
mode="json"
|
||||
on:change={({ detail }) => (fields[field] = detail.value)}
|
||||
readOnly={!editable}
|
||||
value={fields[field]} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="horizontal">
|
||||
<Label small>{field}</Label>
|
||||
<Input
|
||||
placeholder="Enter {field}"
|
||||
outline
|
||||
disabled={!editable}
|
||||
type={schema.fields[field]?.type}
|
||||
required={schema.fields[field]?.required}
|
||||
bind:value={fields[field]} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</form>
|
||||
|
@ -49,8 +71,15 @@
|
|||
.field {
|
||||
margin-bottom: var(--spacing-m);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.horizontal {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Button, Input, Heading, Spacer } from "@budibase/bbui"
|
||||
import { Body, Button, Input, Heading, Spacer } from "@budibase/bbui"
|
||||
import BindableInput from "components/common/BindableInput.svelte"
|
||||
import {
|
||||
readableToRuntimeBinding,
|
||||
|
@ -30,7 +30,16 @@
|
|||
</script>
|
||||
|
||||
<section>
|
||||
<Heading extraSmall black>Parameters</Heading>
|
||||
<div class="controls">
|
||||
<Heading small lh>Parameters</Heading>
|
||||
{#if !bindable}
|
||||
<Button secondary on:click={newQueryParameter}>Add Param</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<Body small grey>
|
||||
Parameters come in two parts: the parameter name, and a default/fallback
|
||||
value.
|
||||
</Body>
|
||||
<Spacer large />
|
||||
<div class="parameters" class:bindable>
|
||||
{#each parameters as parameter, idx}
|
||||
|
@ -59,9 +68,6 @@
|
|||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{#if !bindable}
|
||||
<Button secondary on:click={newQueryParameter}>Add Parameter</Button>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
|
@ -69,6 +75,13 @@
|
|||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.parameters {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 5%;
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import {
|
||||
Select,
|
||||
Button,
|
||||
Body,
|
||||
Label,
|
||||
Input,
|
||||
TextArea,
|
||||
|
@ -15,7 +16,7 @@
|
|||
import api from "builderStore/api"
|
||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
|
||||
import EditQueryParamsPopover from "components/backend/DatasourceNavigator/popovers/EditQueryParamsPopover.svelte"
|
||||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
||||
import { backendUiStore } from "builderStore"
|
||||
|
||||
const PREVIEW_HEADINGS = [
|
||||
|
@ -59,10 +60,10 @@
|
|||
|
||||
$: datasourceType = datasource?.source
|
||||
|
||||
$: config = $backendUiStore.integrations[datasourceType]?.query
|
||||
$: docsLink = $backendUiStore.integrations[datasourceType]?.docs
|
||||
$: integrationInfo = $backendUiStore.integrations[datasourceType]
|
||||
$: queryConfig = integrationInfo?.query
|
||||
|
||||
$: shouldShowQueryConfig = config && query.queryVerb
|
||||
$: shouldShowQueryConfig = queryConfig && query.queryVerb
|
||||
|
||||
function newField() {
|
||||
fields = [...fields, {}]
|
||||
|
@ -129,58 +130,86 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<div class="input">
|
||||
<div class="label">Enter query name:</div>
|
||||
<Input outline border bind:value={query.name} />
|
||||
<section class="config">
|
||||
<Heading medium lh>Query {integrationInfo?.friendlyName}</Heading>
|
||||
<hr />
|
||||
<Heading small lh>Config</Heading>
|
||||
<Body small grey>Provide a name for your query and select its function.</Body>
|
||||
<Spacer medium />
|
||||
<div class="config-field">
|
||||
<Label small>Query Name</Label>
|
||||
<Input thin outline bind:value={query.name} />
|
||||
</div>
|
||||
{#if config}
|
||||
<div class="props">
|
||||
<div class="query-type">
|
||||
Query type:
|
||||
<span class="query-type-span">{config[query.queryVerb].type}</span>
|
||||
</div>
|
||||
<div class="select">
|
||||
<Select primary thin bind:value={query.queryVerb}>
|
||||
{#each Object.keys(config) as queryVerb}
|
||||
<option value={queryVerb}>{queryVerb}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
<Spacer medium />
|
||||
{#if queryConfig}
|
||||
<div class="config-field">
|
||||
<Label small>Function</Label>
|
||||
<Select primary outline thin bind:value={query.queryVerb}>
|
||||
{#each Object.keys(queryConfig) as queryVerb}
|
||||
<option value={queryVerb}>
|
||||
{queryConfig[queryVerb]?.displayName || queryVerb}
|
||||
</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
<EditQueryParamsPopover
|
||||
bind:parameters={query.parameters}
|
||||
bindable={false} />
|
||||
<hr />
|
||||
<ParameterBuilder bind:parameters={query.parameters} bindable={false} />
|
||||
<hr />
|
||||
{/if}
|
||||
</header>
|
||||
<Spacer extraLarge />
|
||||
</section>
|
||||
|
||||
{#if shouldShowQueryConfig}
|
||||
<section>
|
||||
<div class="config">
|
||||
<Heading small lh>Fields</Heading>
|
||||
<Body small grey>Fill in the fields specific to this query.</Body>
|
||||
<Spacer medium />
|
||||
<IntegrationQueryEditor
|
||||
{datasource}
|
||||
{query}
|
||||
schema={config[query.queryVerb]}
|
||||
schema={queryConfig[query.queryVerb]}
|
||||
bind:parameters />
|
||||
|
||||
<Spacer extraLarge />
|
||||
<Spacer large />
|
||||
<hr />
|
||||
|
||||
<div class="viewer-controls">
|
||||
<Button
|
||||
blue
|
||||
disabled={data.length === 0 || !query.name}
|
||||
on:click={saveQuery}>
|
||||
Save Query
|
||||
</Button>
|
||||
<Button primary on:click={previewQuery}>Run Query</Button>
|
||||
<Heading small lh>Query Results</Heading>
|
||||
<div class="button-container">
|
||||
<Button
|
||||
secondary
|
||||
thin
|
||||
disabled={data.length === 0 || !query.name}
|
||||
on:click={saveQuery}>
|
||||
Save Query
|
||||
</Button>
|
||||
<Spacer medium />
|
||||
<Button thin primary on:click={previewQuery}>Run Query</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Body small grey>
|
||||
Below, you can preview the results from your query and change the
|
||||
schema.
|
||||
</Body>
|
||||
|
||||
<Spacer large />
|
||||
|
||||
<section class="viewer">
|
||||
{#if data}
|
||||
<Switcher headings={PREVIEW_HEADINGS} bind:value={tab}>
|
||||
{#if tab === 'JSON'}
|
||||
<pre class="preview">{JSON.stringify(data[0], undefined, 2)}</pre>
|
||||
<pre class="preview">
|
||||
{#if !data[0]}
|
||||
|
||||
|
||||
|
||||
Please run your query to fetch some data.
|
||||
|
||||
|
||||
|
||||
{:else}
|
||||
{JSON.stringify(data[0], undefined, 2)}
|
||||
{/if}
|
||||
</pre>
|
||||
{:else if tab === 'PREVIEW'}
|
||||
<ExternalDataSourceTable {query} {data} />
|
||||
{:else if tab === 'SCHEMA'}
|
||||
|
@ -215,33 +244,26 @@
|
|||
{/if}
|
||||
|
||||
<style>
|
||||
.input {
|
||||
width: 500px;
|
||||
display: flex;
|
||||
.config-field {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select {
|
||||
width: 200px;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
.props {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: auto;
|
||||
align-items: center;
|
||||
gap: var(--layout-l);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 50px;
|
||||
grid-template-columns: 1fr 1fr 5%;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: var(--font-size-s);
|
||||
.button-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: var(--layout-m);
|
||||
margin-bottom: var(--layout-m);
|
||||
}
|
||||
|
||||
.config {
|
||||
|
@ -253,16 +275,6 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.query-type {
|
||||
font-family: var(--font-sans);
|
||||
color: var(--grey-8);
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
|
||||
.query-type-span {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.preview {
|
||||
width: 800px;
|
||||
height: 100%;
|
||||
|
@ -271,31 +283,12 @@
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.viewer-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: auto;
|
||||
direction: rtl;
|
||||
z-index: 5;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-m);
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.viewer {
|
||||
margin-top: -28px;
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-family: var(--font-sans);
|
||||
color: var(--grey-8);
|
||||
font-size: var(--font-size-s);
|
||||
margin-right: 8px;
|
||||
font-weight: 600;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,9 +12,14 @@
|
|||
}
|
||||
|
||||
export let query
|
||||
export let datasource
|
||||
export let schema
|
||||
export let editable = true
|
||||
|
||||
$: urlDisplay =
|
||||
schema.urlDisplay &&
|
||||
`${datasource.config.url}${query.fields.path}${query.fields.queryString}`
|
||||
|
||||
function updateQuery({ detail }) {
|
||||
query.fields[schema.type] = detail.value
|
||||
}
|
||||
|
@ -40,6 +45,21 @@
|
|||
parameters={query.parameters} />
|
||||
{:else if schema.type === QueryTypes.FIELDS}
|
||||
<FieldsBuilder bind:fields={query.fields} {schema} {editable} />
|
||||
{#if schema.urlDisplay}
|
||||
<div class="url-row">
|
||||
<Label small>URL</Label>
|
||||
<Input thin outline disabled value={urlDisplay} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.url-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { params } from "@sveltech/routify"
|
||||
import { Switcher, Modal } from "@budibase/bbui"
|
||||
import { Button, Switcher, Modal } from "@budibase/bbui"
|
||||
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
|
||||
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
||||
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
|
||||
|
@ -8,11 +8,11 @@
|
|||
|
||||
const tabs = [
|
||||
{
|
||||
title: "Tables",
|
||||
title: "Internal",
|
||||
key: "table",
|
||||
},
|
||||
{
|
||||
title: "Data Sources",
|
||||
title: "External",
|
||||
key: "datasource",
|
||||
},
|
||||
]
|
||||
|
@ -67,6 +67,7 @@
|
|||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-l);
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.nav {
|
||||
|
|
|
@ -36,6 +36,8 @@
|
|||
<style>
|
||||
section {
|
||||
overflow: scroll;
|
||||
width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
<script>
|
||||
import { goto } from "@sveltech/routify"
|
||||
import { Button, Spacer, Icon } from "@budibase/bbui"
|
||||
import { goto, beforeUrlChange } from "@sveltech/routify"
|
||||
import { Button, Heading, Body, Spacer, Icon } from "@budibase/bbui"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||
|
||||
let unsaved = false
|
||||
|
||||
$: datasource = $backendUiStore.datasources.find(
|
||||
ds => ds._id === $backendUiStore.selectedDatasourceId
|
||||
)
|
||||
$: integration = datasource && $backendUiStore.integrations[datasource.source]
|
||||
|
||||
async function saveDatasource() {
|
||||
// Create datasource
|
||||
await backendUiStore.actions.datasources.save(datasource)
|
||||
notifier.success(`Datasource ${name} saved successfully.`)
|
||||
unsaved = false
|
||||
}
|
||||
|
||||
function onClickQuery(query) {
|
||||
|
@ -22,28 +27,60 @@
|
|||
backendUiStore.actions.queries.select(query)
|
||||
$goto(`../${query._id}`)
|
||||
}
|
||||
|
||||
function setUnsaved() {
|
||||
unsaved = true
|
||||
}
|
||||
|
||||
$beforeUrlChange((event, store) => {
|
||||
if (unsaved) {
|
||||
notifier.danger(
|
||||
"Unsaved changes. Please save your datasource configuration before leaving."
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if datasource}
|
||||
<section>
|
||||
<Spacer medium />
|
||||
<header>
|
||||
<div class="datasource-icon">
|
||||
<svelte:component
|
||||
this={ICONS[datasource.source]}
|
||||
height="30"
|
||||
width="30" />
|
||||
</div>
|
||||
<h3 class="section-title">{datasource.name}</h3>
|
||||
</header>
|
||||
<Spacer extraLarge />
|
||||
|
||||
<Body small grey lh>{integration.description}</Body>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="container">
|
||||
<div class="config-header">
|
||||
<h5>Configuration</h5>
|
||||
<Heading small>Configuration</Heading>
|
||||
<Button secondary on:click={saveDatasource}>Save</Button>
|
||||
</div>
|
||||
|
||||
<Body small grey>
|
||||
Connect your database to Budibase using the config below.
|
||||
</Body>
|
||||
|
||||
<Spacer medium />
|
||||
<IntegrationConfigForm integration={datasource.config} />
|
||||
</div>
|
||||
<Spacer extraLarge />
|
||||
<div class="container">
|
||||
<IntegrationConfigForm
|
||||
integration={datasource.config}
|
||||
on:change={setUnsaved} />
|
||||
<Spacer medium />
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="query-header">
|
||||
<h5>Queries</h5>
|
||||
<Button blue on:click={() => $goto('../new')}>Create Query</Button>
|
||||
<Heading small>Queries</Heading>
|
||||
<Button secondary on:click={() => $goto('../new')}>Add Query</Button>
|
||||
</div>
|
||||
<Spacer extraLarge />
|
||||
<div class="query-list">
|
||||
|
@ -54,7 +91,6 @@
|
|||
<p>→</p>
|
||||
</div>
|
||||
{/each}
|
||||
<Spacer medium />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -64,13 +100,20 @@
|
|||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 0 auto;
|
||||
width: 800px;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-bottom: var(--layout-m);
|
||||
}
|
||||
|
||||
header {
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
|
@ -85,13 +128,12 @@
|
|||
|
||||
.container {
|
||||
border-radius: var(--border-radius-m);
|
||||
background: var(--background);
|
||||
padding: var(--layout-s);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 0 !important;
|
||||
font-size: var(--font-size-l);
|
||||
}
|
||||
|
||||
.query-header {
|
||||
|
@ -115,7 +157,8 @@
|
|||
display: grid;
|
||||
grid-template-columns: 2fr 0.75fr 20px;
|
||||
align-items: center;
|
||||
padding: var(--spacing-m) var(--layout-xs);
|
||||
padding-left: var(--spacing-m);
|
||||
padding-right: var(--spacing-m);
|
||||
gap: var(--layout-xs);
|
||||
transition: 200ms background ease;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -58,13 +58,25 @@ async function enrichQueryFields(fields, parameters) {
|
|||
|
||||
// enrich the fields with dynamic parameters
|
||||
for (let key of Object.keys(fields)) {
|
||||
enrichedQuery[key] = await processString(fields[key], parameters)
|
||||
if (typeof fields[key] === "object") {
|
||||
// enrich nested fields object
|
||||
enrichedQuery[key] = await enrichQueryFields(fields[key], parameters)
|
||||
} else {
|
||||
// enrich string value as normal
|
||||
enrichedQuery[key] = await processString(fields[key], parameters)
|
||||
}
|
||||
}
|
||||
|
||||
if (enrichedQuery.json || enrichedQuery.customData) {
|
||||
if (
|
||||
enrichedQuery.json ||
|
||||
enrichedQuery.customData ||
|
||||
enrichedQuery.requestBody
|
||||
) {
|
||||
try {
|
||||
enrichedQuery.json = JSON.parse(
|
||||
enrichedQuery.json || enrichedQuery.customData
|
||||
enrichedQuery.json ||
|
||||
enrichedQuery.customData ||
|
||||
enrichedQuery.requestBody
|
||||
)
|
||||
} catch (err) {
|
||||
throw { message: `JSON Invalid - error: ${err}` }
|
||||
|
|
|
@ -9,4 +9,6 @@ exports.FIELD_TYPES = {
|
|||
NUMBER: "number",
|
||||
PASSWORD: "password",
|
||||
LIST: "list",
|
||||
OBJECT: "object",
|
||||
JSON: "json",
|
||||
}
|
||||
|
|
|
@ -3,6 +3,9 @@ const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
|||
|
||||
const SCHEMA = {
|
||||
docs: "https://airtable.com/api",
|
||||
description:
|
||||
"Airtable is a spreadsheet-database hybrid, with the features of a database but applied to a spreadsheet.",
|
||||
friendlyName: "Airtable",
|
||||
datasource: {
|
||||
apiKey: {
|
||||
type: FIELD_TYPES.STRING,
|
||||
|
@ -50,7 +53,7 @@ const SCHEMA = {
|
|||
},
|
||||
},
|
||||
delete: {
|
||||
type: FIELD_TYPES.JSON,
|
||||
type: QUERY_TYPES.JSON,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -3,6 +3,9 @@ const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
|||
|
||||
const SCHEMA = {
|
||||
docs: "https://github.com/arangodb/arangojs",
|
||||
friendlyName: "ArangoDB",
|
||||
description:
|
||||
"ArangoDB is a scalable open-source multi-model database natively supporting graph, document and search. All supported data models & access patterns can be combined in queries allowing for maximal flexibility. ",
|
||||
datasource: {
|
||||
url: {
|
||||
type: FIELD_TYPES.STRING,
|
||||
|
|
|
@ -3,6 +3,9 @@ const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
|||
|
||||
const SCHEMA = {
|
||||
docs: "https://docs.couchdb.org/en/stable/",
|
||||
friendlyName: "CouchDB",
|
||||
description:
|
||||
"Apache CouchDB is an open-source document-oriented NoSQL database, implemented in Erlang.",
|
||||
datasource: {
|
||||
url: {
|
||||
type: FIELD_TYPES.STRING,
|
||||
|
|
|
@ -3,6 +3,9 @@ const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
|||
|
||||
const SCHEMA = {
|
||||
docs: "https://github.com/dabit3/dynamodb-documentclient-cheat-sheet",
|
||||
description:
|
||||
"Amazon DynamoDB is a key-value and document database that delivers single-digit millisecond performance at any scale.",
|
||||
friendlyName: "DynamoDB",
|
||||
datasource: {
|
||||
region: {
|
||||
type: FIELD_TYPES.STRING,
|
||||
|
|
|
@ -4,6 +4,9 @@ const { QUERY_TYPES, FIELD_TYPES } = require("./Integration")
|
|||
const SCHEMA = {
|
||||
docs:
|
||||
"https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html",
|
||||
description:
|
||||
"Elasticsearch is a search engine based on the Lucene library. It provides a distributed, multitenant-capable full-text search engine with an HTTP web interface and schema-free JSON documents.",
|
||||
friendlyName: "ElasticSearch",
|
||||
datasource: {
|
||||
url: {
|
||||
type: "string",
|
||||
|
|
|
@ -8,6 +8,7 @@ const s3 = require("./s3")
|
|||
const airtable = require("./airtable")
|
||||
const mysql = require("./mysql")
|
||||
const arangodb = require("./arangodb")
|
||||
const rest = require("./rest")
|
||||
|
||||
const DEFINITIONS = {
|
||||
POSTGRES: postgres.schema,
|
||||
|
@ -20,6 +21,7 @@ const DEFINITIONS = {
|
|||
AIRTABLE: airtable.schema,
|
||||
MYSQL: mysql.schema,
|
||||
ARANGODB: arangodb.schema,
|
||||
REST: rest.schema,
|
||||
}
|
||||
|
||||
const INTEGRATIONS = {
|
||||
|
@ -33,6 +35,7 @@ const INTEGRATIONS = {
|
|||
AIRTABLE: airtable.integration,
|
||||
MYSQL: mysql.integration,
|
||||
ARANGODB: arangodb.integration,
|
||||
REST: rest.integration,
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -3,6 +3,9 @@ const { FIELD_TYPES } = require("./Integration")
|
|||
|
||||
const SCHEMA = {
|
||||
docs: "https://github.com/tediousjs/node-mssql",
|
||||
description:
|
||||
"Microsoft SQL Server is a relational database management system developed by Microsoft. ",
|
||||
friendlyName: "MS SQL Server",
|
||||
datasource: {
|
||||
user: {
|
||||
type: FIELD_TYPES.STRING,
|
||||
|
|
|
@ -3,6 +3,9 @@ const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
|||
|
||||
const SCHEMA = {
|
||||
docs: "https://github.com/mongodb/node-mongodb-native",
|
||||
friendlyName: "MongoDB",
|
||||
description:
|
||||
"MongoDB is a general purpose, document-based, distributed database built for modern application developers and for the cloud era.",
|
||||
datasource: {
|
||||
connectionString: {
|
||||
type: FIELD_TYPES.STRING,
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
const mysql = require("mysql")
|
||||
const { FIELD_TYPES } = require("./Integration")
|
||||
const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
||||
|
||||
const SCHEMA = {
|
||||
docs: "https://github.com/mysqljs/mysql",
|
||||
friendlyName: "MySQL",
|
||||
description:
|
||||
"MySQL Database Service is a fully managed database service to deploy cloud-native applications. ",
|
||||
datasource: {
|
||||
host: {
|
||||
type: FIELD_TYPES.STRING,
|
||||
|
@ -31,16 +34,16 @@ const SCHEMA = {
|
|||
},
|
||||
query: {
|
||||
create: {
|
||||
type: "sql",
|
||||
type: QUERY_TYPES.SQL,
|
||||
},
|
||||
read: {
|
||||
type: "sql",
|
||||
type: QUERY_TYPES.SQL,
|
||||
},
|
||||
update: {
|
||||
type: "sql",
|
||||
type: QUERY_TYPES.SQL,
|
||||
},
|
||||
delete: {
|
||||
type: "sql",
|
||||
type: QUERY_TYPES.SQL,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -2,6 +2,9 @@ const { Client } = require("pg")
|
|||
|
||||
const SCHEMA = {
|
||||
docs: "https://node-postgres.com",
|
||||
friendlyName: "PostgreSQL",
|
||||
description:
|
||||
"PostgreSQL, also known as Postgres, is a free and open-source relational database management system emphasizing extensibility and SQL compliance.",
|
||||
datasource: {
|
||||
host: {
|
||||
type: "string",
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
const fetch = require("node-fetch")
|
||||
const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
|
||||
|
||||
const SCHEMA = {
|
||||
docs: "https://github.com/node-fetch/node-fetch",
|
||||
description:
|
||||
"Representational state transfer (REST) is a de-facto standard for a software architecture for interactive applications that typically use multiple Web services. ",
|
||||
friendlyName: "REST API",
|
||||
datasource: {
|
||||
url: {
|
||||
type: FIELD_TYPES.STRING,
|
||||
default: "localhost",
|
||||
required: true,
|
||||
},
|
||||
defaultHeaders: {
|
||||
type: FIELD_TYPES.OBJECT,
|
||||
required: false,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
query: {
|
||||
create: {
|
||||
displayName: "POST",
|
||||
type: QUERY_TYPES.FIELDS,
|
||||
urlDisplay: true,
|
||||
fields: {
|
||||
path: {
|
||||
type: FIELD_TYPES.STRING,
|
||||
},
|
||||
queryString: {
|
||||
type: FIELD_TYPES.STRING,
|
||||
},
|
||||
headers: {
|
||||
type: FIELD_TYPES.OBJECT,
|
||||
},
|
||||
requestBody: {
|
||||
type: FIELD_TYPES.JSON,
|
||||
},
|
||||
},
|
||||
},
|
||||
read: {
|
||||
displayName: "GET",
|
||||
type: QUERY_TYPES.FIELDS,
|
||||
urlDisplay: true,
|
||||
fields: {
|
||||
path: {
|
||||
type: FIELD_TYPES.STRING,
|
||||
},
|
||||
queryString: {
|
||||
type: FIELD_TYPES.STRING,
|
||||
},
|
||||
headers: {
|
||||
type: FIELD_TYPES.OBJECT,
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {
|
||||
displayName: "PUT",
|
||||
type: QUERY_TYPES.FIELDS,
|
||||
urlDisplay: true,
|
||||
fields: {
|
||||
path: {
|
||||
type: FIELD_TYPES.STRING,
|
||||
},
|
||||
queryString: {
|
||||
type: FIELD_TYPES.STRING,
|
||||
},
|
||||
headers: {
|
||||
type: FIELD_TYPES.OBJECT,
|
||||
},
|
||||
requestBody: {
|
||||
type: FIELD_TYPES.JSON,
|
||||
},
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
displayName: "DELETE",
|
||||
type: QUERY_TYPES.FIELDS,
|
||||
urlDisplay: true,
|
||||
fields: {
|
||||
path: {
|
||||
type: FIELD_TYPES.STRING,
|
||||
},
|
||||
queryString: {
|
||||
type: FIELD_TYPES.STRING,
|
||||
},
|
||||
headers: {
|
||||
type: FIELD_TYPES.OBJECT,
|
||||
},
|
||||
requestBody: {
|
||||
type: FIELD_TYPES.JSON,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
class RestIntegration {
|
||||
constructor(config) {
|
||||
this.config = config
|
||||
}
|
||||
|
||||
async parseResponse(response) {
|
||||
switch (this.headers.Accept) {
|
||||
case "application/json":
|
||||
return await response.json()
|
||||
case "text/html":
|
||||
return await response.text()
|
||||
default:
|
||||
return await response.json()
|
||||
}
|
||||
}
|
||||
|
||||
async create({ path, queryString, headers = {}, json }) {
|
||||
this.headers = {
|
||||
...this.config.defaultHeaders,
|
||||
...headers,
|
||||
}
|
||||
|
||||
const response = await fetch(this.config.url + path + queryString, {
|
||||
method: "POST",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(json),
|
||||
})
|
||||
|
||||
return await this.parseResponse(response)
|
||||
}
|
||||
|
||||
async read({ path, queryString, headers = {} }) {
|
||||
this.headers = {
|
||||
...this.config.defaultHeaders,
|
||||
...headers,
|
||||
}
|
||||
|
||||
const response = await fetch(this.config.url + path + queryString, {
|
||||
headers: this.headers,
|
||||
})
|
||||
|
||||
return await this.parseResponse(response)
|
||||
}
|
||||
|
||||
async update({ path, queryString, headers = {}, json }) {
|
||||
this.headers = {
|
||||
...this.config.defaultHeaders,
|
||||
...headers,
|
||||
}
|
||||
|
||||
const response = await fetch(this.config.url + path + queryString, {
|
||||
method: "POST",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(json),
|
||||
})
|
||||
|
||||
return await this.parseResponse(response)
|
||||
}
|
||||
|
||||
async delete({ path, queryString, headers = {} }) {
|
||||
this.headers = {
|
||||
...this.config.defaultHeaders,
|
||||
...headers,
|
||||
}
|
||||
|
||||
const response = await fetch(this.config.url + path + queryString, {
|
||||
method: "DELETE",
|
||||
headers: this.headers,
|
||||
})
|
||||
|
||||
return await this.parseResponse(response)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
schema: SCHEMA,
|
||||
integration: RestIntegration,
|
||||
}
|
|
@ -2,6 +2,9 @@ const AWS = require("aws-sdk")
|
|||
|
||||
const SCHEMA = {
|
||||
docs: "https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html",
|
||||
description:
|
||||
"Amazon Simple Storage Service (Amazon S3) is an object storage service that offers industry-leading scalability, data availability, security, and performance.",
|
||||
friendlyName: "Amazon S3",
|
||||
datasource: {
|
||||
region: {
|
||||
type: "string",
|
||||
|
|
Loading…
Reference in New Issue