Merge pull request #1123 from Budibase/rest-api-integration

Rest api integration
This commit is contained in:
Martin McKeaveney 2021-02-19 15:22:29 +00:00 committed by GitHub
commit 8b248008c3
33 changed files with 1375 additions and 261 deletions

View File

@ -1,15 +1,37 @@
<script> <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 export let integration
let unsaved = false
</script> </script>
<form> <form>
{#each Object.keys(integration) as configKey} {#each Object.keys(integration) as configKey}
{#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 <Input
outline
type={integration[configKey].type} type={integration[configKey].type}
label={configKey} on:change
bind:value={integration[configKey]} /> bind:value={integration[configKey]} />
<Spacer large /> </div>
{/if}
{/each} {/each}
</form> </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>

View File

@ -2,7 +2,8 @@
import { onMount } from "svelte" import { onMount } from "svelte"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import api from "builderStore/api" 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" import ICONS from "../icons"
export let integration = {} export let integration = {}
@ -49,17 +50,6 @@
</div> </div>
{/each} {/each}
</div> </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> </section>
<style> <style>

View File

@ -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>

View File

@ -8,6 +8,7 @@ import Airtable from "./Airtable.svelte"
import SqlServer from "./SQLServer.svelte" import SqlServer from "./SQLServer.svelte"
import MySQL from "./MySQL.svelte" import MySQL from "./MySQL.svelte"
import ArangoDB from "./ArangoDB.svelte" import ArangoDB from "./ArangoDB.svelte"
import Rest from "./Rest.svelte"
export default { export default {
POSTGRES: Postgres, POSTGRES: Postgres,
@ -20,4 +21,5 @@ export default {
AIRTABLE: Airtable, AIRTABLE: Airtable,
MYSQL: MySQL, MYSQL: MySQL,
ARANGODB: ArangoDB, ARANGODB: ArangoDB,
REST: Rest,
} }

View File

@ -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>

View File

@ -3,7 +3,6 @@
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Input } from "@budibase/bbui" import { DropdownMenu, Button, Input } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns" import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let datasource export let datasource

View File

@ -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>

View File

@ -3,7 +3,6 @@
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Input } from "@budibase/bbui" import { DropdownMenu, Button, Input } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns" import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let query export let query

View File

@ -109,6 +109,7 @@
</div> </div>
<div class="drawer-contents" slot="body"> <div class="drawer-contents" slot="body">
<IntegrationQueryEditor <IntegrationQueryEditor
datasource={$backendUiStore.datasources.find(ds => ds._id === value.datasourceId)}
query={value} query={value}
schema={fetchDatasourceSchema(value)} schema={fetchDatasourceSchema(value)}
editable={false} /> editable={false} />

View File

@ -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>

View File

@ -1,5 +1,6 @@
<script> <script>
import CodeMirror from "./codemirror" import CodeMirror from "./codemirror"
import { Label, Spacer } from "@budibase/bbui"
import { onMount, createEventDispatcher } from "svelte" import { onMount, createEventDispatcher } from "svelte"
import { themeStore } from "builderStore" import { themeStore } from "builderStore"
import { handlebarsCompletions } from "constants/completions" import { handlebarsCompletions } from "constants/completions"
@ -11,6 +12,7 @@
LIGHT: "default", LIGHT: "default",
} }
export let label
export let value = "" export let value = ""
export let readOnly = false export let readOnly = false
export let lineNumbers = true export let lineNumbers = true
@ -169,6 +171,8 @@
} }
</script> </script>
<Label small>{label}</Label>
<Spacer medium />
<textarea tabindex="0" bind:this={refs.editor} readonly {value} /> <textarea tabindex="0" bind:this={refs.editor} readonly {value} />
<style> <style>

View File

@ -6,8 +6,10 @@
Input, Input,
Heading, Heading,
Select, Select,
Spacer,
} from "@budibase/bbui" } from "@budibase/bbui"
import Editor from "./QueryEditor.svelte" import Editor from "./QueryEditor.svelte"
import KeyValueBuilder from "./KeyValueBuilder.svelte"
export let fields = {} export let fields = {}
export let schema export let schema
@ -26,13 +28,33 @@
<form on:submit|preventDefault> <form on:submit|preventDefault>
<div class="field"> <div class="field">
{#each schemaKeys as field} {#each schemaKeys as 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 <Input
placeholder="Enter {field} name" placeholder="Enter {field}"
outline outline
disabled={!editable} disabled={!editable}
type={schema.fields[field]?.type} type={schema.fields[field]?.type}
required={schema.fields[field]?.required} required={schema.fields[field]?.required}
bind:value={fields[field]} /> bind:value={fields[field]} />
</div>
{/if}
{/each} {/each}
</div> </div>
</form> </form>
@ -49,8 +71,15 @@
.field { .field {
margin-bottom: var(--spacing-m); margin-bottom: var(--spacing-m);
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr;
grid-gap: var(--spacing-m); grid-gap: var(--spacing-m);
align-items: center; align-items: center;
} }
.horizontal {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
</style> </style>

View File

@ -1,5 +1,5 @@
<script> <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 BindableInput from "components/common/BindableInput.svelte"
import { import {
readableToRuntimeBinding, readableToRuntimeBinding,
@ -30,7 +30,16 @@
</script> </script>
<section> <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 /> <Spacer large />
<div class="parameters" class:bindable> <div class="parameters" class:bindable>
{#each parameters as parameter, idx} {#each parameters as parameter, idx}
@ -59,9 +68,6 @@
{/if} {/if}
{/each} {/each}
</div> </div>
{#if !bindable}
<Button secondary on:click={newQueryParameter}>Add Parameter</Button>
{/if}
</section> </section>
<style> <style>
@ -69,6 +75,13 @@
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
} }
.controls {
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
}
.parameters { .parameters {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 5%; grid-template-columns: 1fr 1fr 5%;

View File

@ -4,6 +4,7 @@
import { import {
Select, Select,
Button, Button,
Body,
Label, Label,
Input, Input,
TextArea, TextArea,
@ -15,7 +16,7 @@
import api from "builderStore/api" import api from "builderStore/api"
import IntegrationQueryEditor from "components/integration/index.svelte" import IntegrationQueryEditor from "components/integration/index.svelte"
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.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" import { backendUiStore } from "builderStore"
const PREVIEW_HEADINGS = [ const PREVIEW_HEADINGS = [
@ -59,10 +60,10 @@
$: datasourceType = datasource?.source $: datasourceType = datasource?.source
$: config = $backendUiStore.integrations[datasourceType]?.query $: integrationInfo = $backendUiStore.integrations[datasourceType]
$: docsLink = $backendUiStore.integrations[datasourceType]?.docs $: queryConfig = integrationInfo?.query
$: shouldShowQueryConfig = config && query.queryVerb $: shouldShowQueryConfig = queryConfig && query.queryVerb
function newField() { function newField() {
fields = [...fields, {}] fields = [...fields, {}]
@ -129,58 +130,86 @@
} }
</script> </script>
<header> <section class="config">
<div class="input"> <Heading medium lh>Query {integrationInfo?.friendlyName}</Heading>
<div class="label">Enter query name:</div> <hr />
<Input outline border bind:value={query.name} /> <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> </div>
{#if config} <Spacer medium />
<div class="props"> {#if queryConfig}
<div class="query-type"> <div class="config-field">
Query type: <Label small>Function</Label>
<span class="query-type-span">{config[query.queryVerb].type}</span> <Select primary outline thin bind:value={query.queryVerb}>
</div> {#each Object.keys(queryConfig) as queryVerb}
<div class="select"> <option value={queryVerb}>
<Select primary thin bind:value={query.queryVerb}> {queryConfig[queryVerb]?.displayName || queryVerb}
{#each Object.keys(config) as queryVerb} </option>
<option value={queryVerb}>{queryVerb}</option>
{/each} {/each}
</Select> </Select>
</div> </div>
</div> <hr />
<EditQueryParamsPopover <ParameterBuilder bind:parameters={query.parameters} bindable={false} />
bind:parameters={query.parameters} <hr />
bindable={false} />
{/if} {/if}
</header> </section>
<Spacer extraLarge />
{#if shouldShowQueryConfig} {#if shouldShowQueryConfig}
<section> <section>
<div class="config"> <div class="config">
<Heading small lh>Fields</Heading>
<Body small grey>Fill in the fields specific to this query.</Body>
<Spacer medium />
<IntegrationQueryEditor <IntegrationQueryEditor
{datasource}
{query} {query}
schema={config[query.queryVerb]} schema={queryConfig[query.queryVerb]}
bind:parameters /> bind:parameters />
<Spacer extraLarge /> <hr />
<Spacer large />
<div class="viewer-controls"> <div class="viewer-controls">
<Heading small lh>Query Results</Heading>
<div class="button-container">
<Button <Button
blue secondary
thin
disabled={data.length === 0 || !query.name} disabled={data.length === 0 || !query.name}
on:click={saveQuery}> on:click={saveQuery}>
Save Query Save Query
</Button> </Button>
<Button primary on:click={previewQuery}>Run Query</Button> <Spacer medium />
<Button thin primary on:click={previewQuery}>Run Query</Button>
</div> </div>
</div>
<Body small grey>
Below, you can preview the results from your query and change the
schema.
</Body>
<Spacer large />
<section class="viewer"> <section class="viewer">
{#if data} {#if data}
<Switcher headings={PREVIEW_HEADINGS} bind:value={tab}> <Switcher headings={PREVIEW_HEADINGS} bind:value={tab}>
{#if tab === 'JSON'} {#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'} {:else if tab === 'PREVIEW'}
<ExternalDataSourceTable {query} {data} /> <ExternalDataSourceTable {query} {data} />
{:else if tab === 'SCHEMA'} {:else if tab === 'SCHEMA'}
@ -215,33 +244,26 @@
{/if} {/if}
<style> <style>
.input { .config-field {
width: 500px; display: grid;
display: flex; grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center; 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 { .field {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 50px; grid-template-columns: 1fr 1fr 5%;
gap: var(--spacing-l); gap: var(--spacing-l);
} }
a { .button-container {
font-size: var(--font-size-s); display: flex;
}
hr {
margin-top: var(--layout-m);
margin-bottom: var(--layout-m);
} }
.config { .config {
@ -253,16 +275,6 @@
cursor: pointer; 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 { .preview {
width: 800px; width: 800px;
height: 100%; height: 100%;
@ -271,31 +283,12 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
header {
display: flex;
align-items: center;
}
.viewer-controls { .viewer-controls {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-left: auto; justify-content: space-between;
direction: rtl;
z-index: 5;
gap: var(--spacing-m); gap: var(--spacing-m);
min-width: 150px; min-width: 150px;
} align-items: center;
.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;
} }
</style> </style>

View File

@ -12,9 +12,14 @@
} }
export let query export let query
export let datasource
export let schema export let schema
export let editable = true export let editable = true
$: urlDisplay =
schema.urlDisplay &&
`${datasource.config.url}${query.fields.path}${query.fields.queryString}`
function updateQuery({ detail }) { function updateQuery({ detail }) {
query.fields[schema.type] = detail.value query.fields[schema.type] = detail.value
} }
@ -40,6 +45,21 @@
parameters={query.parameters} /> parameters={query.parameters} />
{:else if schema.type === QueryTypes.FIELDS} {:else if schema.type === QueryTypes.FIELDS}
<FieldsBuilder bind:fields={query.fields} {schema} {editable} /> <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} {/if}
{/key} {/key}
{/if} {/if}
<style>
.url-row {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
</style>

View File

@ -1,6 +1,6 @@
<script> <script>
import { params } from "@sveltech/routify" 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 TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte" import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte" import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
@ -8,11 +8,11 @@
const tabs = [ const tabs = [
{ {
title: "Tables", title: "Internal",
key: "table", key: "table",
}, },
{ {
title: "Data Sources", title: "External",
key: "datasource", key: "datasource",
}, },
] ]
@ -67,6 +67,7 @@
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
gap: var(--spacing-l); gap: var(--spacing-l);
background: var(--background);
} }
.nav { .nav {

View File

@ -36,6 +36,8 @@
<style> <style>
section { section {
overflow: scroll; overflow: scroll;
width: 800px;
margin: 0 auto;
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 0px; width: 0px;

View File

@ -1,18 +1,23 @@
<script> <script>
import { goto } from "@sveltech/routify" import { goto, beforeUrlChange } from "@sveltech/routify"
import { Button, Spacer, Icon } from "@budibase/bbui" import { Button, Heading, Body, Spacer, Icon } from "@budibase/bbui"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte" import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
import ICONS from "components/backend/DatasourceNavigator/icons"
let unsaved = false
$: datasource = $backendUiStore.datasources.find( $: datasource = $backendUiStore.datasources.find(
ds => ds._id === $backendUiStore.selectedDatasourceId ds => ds._id === $backendUiStore.selectedDatasourceId
) )
$: integration = datasource && $backendUiStore.integrations[datasource.source]
async function saveDatasource() { async function saveDatasource() {
// Create datasource // Create datasource
await backendUiStore.actions.datasources.save(datasource) await backendUiStore.actions.datasources.save(datasource)
notifier.success(`Datasource ${name} saved successfully.`) notifier.success(`Datasource ${name} saved successfully.`)
unsaved = false
} }
function onClickQuery(query) { function onClickQuery(query) {
@ -22,28 +27,60 @@
backendUiStore.actions.queries.select(query) backendUiStore.actions.queries.select(query)
$goto(`../${query._id}`) $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> </script>
{#if datasource} {#if datasource}
<section> <section>
<Spacer medium /> <Spacer medium />
<header> <header>
<div class="datasource-icon">
<svelte:component
this={ICONS[datasource.source]}
height="30"
width="30" />
</div>
<h3 class="section-title">{datasource.name}</h3> <h3 class="section-title">{datasource.name}</h3>
</header> </header>
<Spacer extraLarge />
<Body small grey lh>{integration.description}</Body>
<hr />
<div class="container"> <div class="container">
<div class="config-header"> <div class="config-header">
<h5>Configuration</h5> <Heading small>Configuration</Heading>
<Button secondary on:click={saveDatasource}>Save</Button> <Button secondary on:click={saveDatasource}>Save</Button>
</div> </div>
<Body small grey>
Connect your database to Budibase using the config below.
</Body>
<Spacer medium /> <Spacer medium />
<IntegrationConfigForm integration={datasource.config} /> <IntegrationConfigForm
</div> integration={datasource.config}
<Spacer extraLarge /> on:change={setUnsaved} />
<div class="container"> <Spacer medium />
<hr />
<div class="query-header"> <div class="query-header">
<h5>Queries</h5> <Heading small>Queries</Heading>
<Button blue on:click={() => $goto('../new')}>Create Query</Button> <Button secondary on:click={() => $goto('../new')}>Add Query</Button>
</div> </div>
<Spacer extraLarge /> <Spacer extraLarge />
<div class="query-list"> <div class="query-list">
@ -54,7 +91,6 @@
<p></p> <p></p>
</div> </div>
{/each} {/each}
<Spacer medium />
</div> </div>
</div> </div>
</section> </section>
@ -64,13 +100,20 @@
h3 { h3 {
margin: 0; margin: 0;
} }
section { section {
margin: 0 auto; margin: 0 auto;
width: 800px; width: 800px;
} }
hr {
margin-bottom: var(--layout-m);
}
header { header {
margin: 0 0 var(--spacing-xs) 0; margin: 0 0 var(--spacing-xs) 0;
display: flex;
gap: var(--spacing-m);
} }
.section-title { .section-title {
@ -85,13 +128,12 @@
.container { .container {
border-radius: var(--border-radius-m); border-radius: var(--border-radius-m);
background: var(--background);
padding: var(--layout-s);
margin: 0 auto; margin: 0 auto;
} }
h5 { h5 {
margin: 0 !important; margin: 0 !important;
font-size: var(--font-size-l);
} }
.query-header { .query-header {
@ -115,7 +157,8 @@
display: grid; display: grid;
grid-template-columns: 2fr 0.75fr 20px; grid-template-columns: 2fr 0.75fr 20px;
align-items: center; align-items: center;
padding: var(--spacing-m) var(--layout-xs); padding-left: var(--spacing-m);
padding-right: var(--spacing-m);
gap: var(--layout-xs); gap: var(--layout-xs);
transition: 200ms background ease; transition: 200ms background ease;
} }

File diff suppressed because it is too large Load Diff

View File

@ -58,13 +58,25 @@ async function enrichQueryFields(fields, parameters) {
// enrich the fields with dynamic parameters // enrich the fields with dynamic parameters
for (let key of Object.keys(fields)) { for (let key of Object.keys(fields)) {
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) enrichedQuery[key] = await processString(fields[key], parameters)
} }
}
if (enrichedQuery.json || enrichedQuery.customData) { if (
enrichedQuery.json ||
enrichedQuery.customData ||
enrichedQuery.requestBody
) {
try { try {
enrichedQuery.json = JSON.parse( enrichedQuery.json = JSON.parse(
enrichedQuery.json || enrichedQuery.customData enrichedQuery.json ||
enrichedQuery.customData ||
enrichedQuery.requestBody
) )
} catch (err) { } catch (err) {
throw { message: `JSON Invalid - error: ${err}` } throw { message: `JSON Invalid - error: ${err}` }

View File

@ -9,4 +9,6 @@ exports.FIELD_TYPES = {
NUMBER: "number", NUMBER: "number",
PASSWORD: "password", PASSWORD: "password",
LIST: "list", LIST: "list",
OBJECT: "object",
JSON: "json",
} }

View File

@ -3,6 +3,9 @@ const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
const SCHEMA = { const SCHEMA = {
docs: "https://airtable.com/api", 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: { datasource: {
apiKey: { apiKey: {
type: FIELD_TYPES.STRING, type: FIELD_TYPES.STRING,
@ -50,7 +53,7 @@ const SCHEMA = {
}, },
}, },
delete: { delete: {
type: FIELD_TYPES.JSON, type: QUERY_TYPES.JSON,
}, },
}, },
} }

View File

@ -3,6 +3,9 @@ const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
const SCHEMA = { const SCHEMA = {
docs: "https://github.com/arangodb/arangojs", 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: { datasource: {
url: { url: {
type: FIELD_TYPES.STRING, type: FIELD_TYPES.STRING,

View File

@ -3,6 +3,9 @@ const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
const SCHEMA = { const SCHEMA = {
docs: "https://docs.couchdb.org/en/stable/", docs: "https://docs.couchdb.org/en/stable/",
friendlyName: "CouchDB",
description:
"Apache CouchDB is an open-source document-oriented NoSQL database, implemented in Erlang.",
datasource: { datasource: {
url: { url: {
type: FIELD_TYPES.STRING, type: FIELD_TYPES.STRING,

View File

@ -3,6 +3,9 @@ const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
const SCHEMA = { const SCHEMA = {
docs: "https://github.com/dabit3/dynamodb-documentclient-cheat-sheet", 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: { datasource: {
region: { region: {
type: FIELD_TYPES.STRING, type: FIELD_TYPES.STRING,

View File

@ -4,6 +4,9 @@ const { QUERY_TYPES, FIELD_TYPES } = require("./Integration")
const SCHEMA = { const SCHEMA = {
docs: docs:
"https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html", "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: { datasource: {
url: { url: {
type: "string", type: "string",

View File

@ -8,6 +8,7 @@ const s3 = require("./s3")
const airtable = require("./airtable") const airtable = require("./airtable")
const mysql = require("./mysql") const mysql = require("./mysql")
const arangodb = require("./arangodb") const arangodb = require("./arangodb")
const rest = require("./rest")
const DEFINITIONS = { const DEFINITIONS = {
POSTGRES: postgres.schema, POSTGRES: postgres.schema,
@ -20,6 +21,7 @@ const DEFINITIONS = {
AIRTABLE: airtable.schema, AIRTABLE: airtable.schema,
MYSQL: mysql.schema, MYSQL: mysql.schema,
ARANGODB: arangodb.schema, ARANGODB: arangodb.schema,
REST: rest.schema,
} }
const INTEGRATIONS = { const INTEGRATIONS = {
@ -33,6 +35,7 @@ const INTEGRATIONS = {
AIRTABLE: airtable.integration, AIRTABLE: airtable.integration,
MYSQL: mysql.integration, MYSQL: mysql.integration,
ARANGODB: arangodb.integration, ARANGODB: arangodb.integration,
REST: rest.integration,
} }
module.exports = { module.exports = {

View File

@ -3,6 +3,9 @@ const { FIELD_TYPES } = require("./Integration")
const SCHEMA = { const SCHEMA = {
docs: "https://github.com/tediousjs/node-mssql", 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: { datasource: {
user: { user: {
type: FIELD_TYPES.STRING, type: FIELD_TYPES.STRING,

View File

@ -3,6 +3,9 @@ const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
const SCHEMA = { const SCHEMA = {
docs: "https://github.com/mongodb/node-mongodb-native", 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: { datasource: {
connectionString: { connectionString: {
type: FIELD_TYPES.STRING, type: FIELD_TYPES.STRING,

View File

@ -1,8 +1,11 @@
const mysql = require("mysql") const mysql = require("mysql")
const { FIELD_TYPES } = require("./Integration") const { FIELD_TYPES, QUERY_TYPES } = require("./Integration")
const SCHEMA = { const SCHEMA = {
docs: "https://github.com/mysqljs/mysql", docs: "https://github.com/mysqljs/mysql",
friendlyName: "MySQL",
description:
"MySQL Database Service is a fully managed database service to deploy cloud-native applications. ",
datasource: { datasource: {
host: { host: {
type: FIELD_TYPES.STRING, type: FIELD_TYPES.STRING,
@ -31,16 +34,16 @@ const SCHEMA = {
}, },
query: { query: {
create: { create: {
type: "sql", type: QUERY_TYPES.SQL,
}, },
read: { read: {
type: "sql", type: QUERY_TYPES.SQL,
}, },
update: { update: {
type: "sql", type: QUERY_TYPES.SQL,
}, },
delete: { delete: {
type: "sql", type: QUERY_TYPES.SQL,
}, },
}, },
} }

View File

@ -2,6 +2,9 @@ const { Client } = require("pg")
const SCHEMA = { const SCHEMA = {
docs: "https://node-postgres.com", 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: { datasource: {
host: { host: {
type: "string", type: "string",

View File

@ -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,
}

View File

@ -2,6 +2,9 @@ const AWS = require("aws-sdk")
const SCHEMA = { const SCHEMA = {
docs: "https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html", 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: { datasource: {
region: { region: {
type: "string", type: "string",