Merge branch 'master' of github.com:Budibase/budibase into form-builder

This commit is contained in:
Andrew Kingston 2021-01-26 09:56:50 +00:00
commit 25036d2d1b
37 changed files with 1329 additions and 323 deletions

20
hosting/bootstrap.sh Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
GITHUB_BASE_URL=https://raw.githubusercontent.com/Budibase/budibase/master/hosting
if ! [ -x "$(command -v wget)" ]; then
echo 'Error: wget is not installed. Please install it for your operating system.' >&2
exit 1
fi
fetch_config_files() {
wget $GITHUB_BASE_URL/docker-compose.yaml
wget $GITHUB_BASE_URL/envoy.yaml
wget $GITHUB_BASE_URL/hosting.properties
wget $GITHUB_BASE_URL/start.sh
}
fetch_config_files
# Start budibase
docker-compose --env-file hosting.properties up -d

View File

@ -2,6 +2,7 @@ version: "3"
services: services:
app-service: app-service:
restart: always
image: budibase/budibase-apps image: budibase/budibase-apps
ports: ports:
- "${APP_PORT}:4002" - "${APP_PORT}:4002"
@ -18,6 +19,7 @@ services:
- worker-service - worker-service
worker-service: worker-service:
restart: always
image: budibase/budibase-worker image: budibase/budibase-worker
ports: ports:
- "${WORKER_PORT}:4003" - "${WORKER_PORT}:4003"
@ -36,6 +38,7 @@ services:
- couch-init - couch-init
minio-service: minio-service:
restart: always
image: minio/minio image: minio/minio
volumes: volumes:
- minio_data:/data - minio_data:/data
@ -53,6 +56,7 @@ services:
retries: 3 retries: 3
proxy-service: proxy-service:
restart: always
image: envoyproxy/envoy:v1.16-latest image: envoyproxy/envoy:v1.16-latest
volumes: volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml - ./envoy.yaml:/etc/envoy/envoy.yaml
@ -66,6 +70,7 @@ services:
- couchdb-service - couchdb-service
couchdb-service: couchdb-service:
restart: always
image: apache/couchdb:3.0 image: apache/couchdb:3.0
environment: environment:
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD} - COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}

View File

@ -63,7 +63,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.54.0", "@budibase/bbui": "^1.54.1",
"@budibase/client": "^0.5.3", "@budibase/client": "^0.5.3",
"@budibase/colorpicker": "^1.0.1", "@budibase/colorpicker": "^1.0.1",
"@budibase/string-templates": "^0.5.3", "@budibase/string-templates": "^0.5.3",

View File

@ -103,6 +103,15 @@
opacity: 1; opacity: 1;
} }
.column-header-name {
white-space: normal !important;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
.sort-icon { .sort-icon {
position: relative; position: relative;
top: 2px; top: 2px;

View File

@ -43,8 +43,8 @@
<div class="datasource-icon" slot="icon"> <div class="datasource-icon" slot="icon">
<svelte:component <svelte:component
this={ICONS[datasource.source]} this={ICONS[datasource.source]}
height="15" height="18"
width="15" /> width="18" />
</div> </div>
<EditDatasourcePopover {datasource} /> <EditDatasourcePopover {datasource} />
</NavItem> </NavItem>
@ -61,3 +61,10 @@
{/each} {/each}
</div> </div>
{/if} {/if}
<style>
.datasource-icon {
margin-right: 3px;
padding-top: 3px;
}
</style>

View File

@ -7,10 +7,9 @@
<form> <form>
{#each Object.keys(integration) as configKey} {#each Object.keys(integration) as configKey}
<Input <Input
thin
type={integration[configKey].type} type={integration[configKey].type}
label={configKey} label={configKey}
bind:value={integration[configKey]} /> bind:value={integration[configKey]} />
<Spacer medium /> <Spacer large />
{/each} {/each}
</form> </form>

View File

@ -0,0 +1,42 @@
<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

@ -57,7 +57,7 @@
} }
</script> </script>
<Button secondary small on:click={drawer.show}>Define Actions</Button> <Button secondary wide on:click={drawer.show}>Define Actions</Button>
<Drawer bind:this={drawer} title={'Actions'}> <Drawer bind:this={drawer} title={'Actions'}>
<heading slot="buttons"> <heading slot="buttons">
<Button thin blue on:click={saveEventData}>Save</Button> <Button thin blue on:click={saveEventData}>Save</Button>

View File

@ -9,12 +9,17 @@
export let onStyleChanged = () => {} export let onStyleChanged = () => {}
export let open = false export let open = false
$: style = componentInstance["_styles"][styleCategory] || {}
$: changed = properties.some(prop => hasPropChanged(style, prop))
const hasPropChanged = (style, prop) => { const hasPropChanged = (style, prop) => {
return style[prop.key] != null && style[prop.key] !== "" return style[prop.key] != null && style[prop.key] !== ""
} }
$: style = componentInstance["_styles"][styleCategory] || {} const getControlProps = props => {
$: changed = properties.some(prop => hasPropChanged(style, prop)) const { label, key, control, ...otherProps } = props || {}
return otherProps || {}
}
</script> </script>
<DetailSummary name={`${name}${changed ? ' *' : ''}`} on:open show={open} thin> <DetailSummary name={`${name}${changed ? ' *' : ''}`} on:open show={open} thin>
@ -28,7 +33,7 @@
key={prop.key} key={prop.key}
value={style[prop.key]} value={style[prop.key]}
onChange={value => onStyleChanged(styleCategory, prop.key, value)} onChange={value => onStyleChanged(styleCategory, prop.key, value)}
props={{ options: prop.options, placeholder: prop.placeholder }} /> props={getControlProps(prop)} />
{/each} {/each}
</div> </div>
{/if} {/if}

View File

@ -78,9 +78,7 @@
const source = $backendUiStore.datasources.find( const source = $backendUiStore.datasources.find(
ds => ds._id === query.datasourceId ds => ds._id === query.datasourceId
).source ).source
return $backendUiStore.integrations[source].query[query.queryVerb][ return $backendUiStore.integrations[source].query[query.queryVerb]
query.queryType
]
} }
</script> </script>

View File

@ -155,8 +155,9 @@
} }
:global(.CodeMirror) { :global(.CodeMirror) {
height: auto !important; height: 500px !important;
border-radius: var(--border-radius-m); border-radius: var(--border-radius-s);
font-family: var(--font-sans) !important; font-family: monospace !important;
line-height: 1.3;
} }
</style> </style>

View File

@ -24,18 +24,18 @@
</script> </script>
<form on:submit|preventDefault> <form on:submit|preventDefault>
<div class="field">
{#each schemaKeys as field} {#each schemaKeys as field}
<Label extraSmall grey>{field}</Label>
<div class="field">
<Input <Input
placeholder="Enter {field} name"
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>
{/each} {/each}
</div>
</form> </form>
<Label extraSmall grey>Data</Label>
{#if schema.customisable} {#if schema.customisable}
<Editor <Editor
label="Query" label="Query"
@ -49,7 +49,7 @@
.field { .field {
margin-bottom: var(--spacing-m); margin-bottom: var(--spacing-m);
display: grid; display: grid;
grid-template-columns: 1fr 2%; grid-template-columns: 1fr 1fr;
grid-gap: var(--spacing-m); grid-gap: var(--spacing-m);
align-items: center; align-items: center;
} }

View File

@ -1,5 +1,5 @@
<script> <script>
import { Button, Label, Input, Heading } from "@budibase/bbui" import { 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,
@ -31,19 +31,22 @@
<section> <section>
<Heading extraSmall black>Parameters</Heading> <Heading extraSmall black>Parameters</Heading>
<Spacer large />
<div class="parameters" class:bindable> <div class="parameters" class:bindable>
<Label extraSmall grey>Parameter Name</Label>
<Label extraSmall grey>Default</Label>
{#if bindable}
<Label extraSmall grey>Value</Label>
{:else}
<div />
{/if}
{#each parameters as parameter, idx} {#each parameters as parameter, idx}
<Input thin disabled={bindable} bind:value={parameter.name} /> <Input
<Input thin disabled={bindable} bind:value={parameter.default} /> placeholder="Parameter Name"
thin
disabled={bindable}
bind:value={parameter.name} />
<Input
placeholder="Default"
thin
disabled={bindable}
bind:value={parameter.default} />
{#if bindable} {#if bindable}
<BindableInput <BindableInput
placeholder="Value"
type="string" type="string"
thin thin
on:change={evt => onBindingChange(parameter.name, evt.detail)} on:change={evt => onBindingChange(parameter.name, evt.detail)}
@ -57,9 +60,7 @@
{/each} {/each}
</div> </div>
{#if !bindable} {#if !bindable}
<Button thin secondary small on:click={newQueryParameter}> <Button secondary on:click={newQueryParameter}>Add Parameter</Button>
Add Parameter
</Button>
{/if} {/if}
</section> </section>

View File

@ -16,6 +16,7 @@
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
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 { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
const PREVIEW_HEADINGS = [ const PREVIEW_HEADINGS = [
@ -40,6 +41,7 @@
let tab = "JSON" let tab = "JSON"
let parameters let parameters
let data = [] let data = []
let popover
$: datasource = $backendUiStore.datasources.find( $: datasource = $backendUiStore.datasources.find(
ds => ds._id === query.datasourceId ds => ds._id === query.datasourceId
@ -61,7 +63,7 @@
$: config = $backendUiStore.integrations[datasourceType]?.query $: config = $backendUiStore.integrations[datasourceType]?.query
$: docsLink = $backendUiStore.integrations[datasourceType]?.docs $: docsLink = $backendUiStore.integrations[datasourceType]?.docs
$: shouldShowQueryConfig = config && query.queryVerb && query.queryType $: shouldShowQueryConfig = config && query.queryVerb
function newField() { function newField() {
fields = [...fields, {}] fields = [...fields, {}]
@ -129,62 +131,44 @@
</script> </script>
<header> <header>
<Heading small>{query.name}</Heading> <div class="input">
<Input placeholder="✎ Edit Query Name" bind:value={query.name} />
</div>
{#if config} {#if config}
<div class="queryVerbs"> <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} {#each Object.keys(config) as queryVerb}
<div <option value={queryVerb}>{queryVerb}</option>
class="queryVerb"
class:selected={queryVerb === query.queryVerb}
on:click={() => {
query.queryVerb = queryVerb
}}>
{queryVerb}
</div>
{/each}
</div>
{#if query.queryVerb}
<Select thin secondary bind:value={query.queryType}>
<option value={''}>Select an option</option>
{#each Object.keys(config[query.queryVerb]) as queryType}
<option value={queryType}>{queryType}</option>
{/each} {/each}
</Select> </Select>
{/if} </div>
<Spacer medium /> </div>
<Button primary href={docsLink} target="_blank"> <EditQueryParamsPopover bind:parameters={query.parameters} bindable={false} />
<i class="ri-book-2-line" />
</Button>
{/if} {/if}
</header> </header>
<Spacer extraLarge />
<Spacer large />
{#if shouldShowQueryConfig} {#if shouldShowQueryConfig}
<section> <section>
<div class="config"> <div class="config">
<Label extraSmall grey>Query Name</Label>
<Input thin bind:value={query.name} />
<Spacer medium />
<IntegrationQueryEditor <IntegrationQueryEditor
{query} {query}
schema={config[query.queryVerb][query.queryType]} schema={config[query.queryVerb]}
bind:parameters /> bind:parameters />
<Spacer medium /> <Spacer extraLarge />
<Spacer large />
<div class="viewer-controls"> <div class="viewer-controls">
<Button <Button
wide
thin
blue blue
disabled={data.length === 0} disabled={data.length === 0}
on:click={saveQuery}> on:click={saveQuery}>
Save Save Query
</Button> </Button>
<Button wide thin primary on:click={previewQuery}>Run</Button> <Button primary on:click={previewQuery}>Run Query</Button>
</div> </div>
<section class="viewer"> <section class="viewer">
@ -196,10 +180,11 @@
<ExternalDataSourceTable {query} {data} /> <ExternalDataSourceTable {query} {data} />
{:else if tab === 'SCHEMA'} {:else if tab === 'SCHEMA'}
{#each fields as field, idx} {#each fields as field, idx}
<Spacer small />
<div class="field"> <div class="field">
<Input thin type={'text'} bind:value={field.name} /> <Input outline placeholder="Field Name" type={'text'} bind:value={field.name} />
<Select secondary thin bind:value={field.type}> <Select thin border bind:value={field.type}>
<option value={''}>Select an option</option> <option value={''}>Select a field type</option>
<option value={'STRING'}>Text</option> <option value={'STRING'}>Text</option>
<option value={'NUMBER'}>Number</option> <option value={'NUMBER'}>Number</option>
<option value={'BOOLEAN'}>Boolean</option> <option value={'BOOLEAN'}>Boolean</option>
@ -210,6 +195,7 @@
on:click={() => deleteField(idx)} /> on:click={() => deleteField(idx)} />
</div> </div>
{/each} {/each}
<Spacer small />
<Button thin secondary on:click={newField}>Add Field</Button> <Button thin secondary on:click={newField}>Add Field</Button>
{/if} {/if}
</Switcher> </Switcher>
@ -220,11 +206,28 @@
{/if} {/if}
<style> <style>
.input {
width: 300px;
}
.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-gap: 10px;
grid-template-columns: 1fr 1fr 50px; grid-template-columns: 1fr 1fr 50px;
margin-bottom: var(--spacing-m); gap: var(--spacing-l);
} }
a { a {
@ -240,6 +243,16 @@
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%;
@ -253,32 +266,18 @@
align-items: center; align-items: center;
} }
.queryVerbs {
display: flex;
flex: 1;
font-size: var(--font-size-m);
align-items: center;
margin-left: var(--spacing-l);
}
.queryVerb {
text-transform: capitalize;
margin-right: var(--spacing-m);
color: var(--grey-5);
cursor: pointer;
}
.selected {
color: var(--white);
font-weight: 500;
}
.viewer-controls { .viewer-controls {
display: grid; display: flex;
grid-gap: var(--spacing-m); flex-direction: row;
grid-auto-flow: column; margin-left: auto;
direction: rtl; direction: rtl;
grid-template-columns: 10% 10% 1fr; z-index: 5;
margin-bottom: var(--spacing-m); gap: var(--spacing-m);
min-width: 150px;
}
.viewer {
margin-top: -28px;
z-index: -2;
} }
</style> </style>

View File

@ -20,13 +20,6 @@
} }
</script> </script>
{#if editable}
<ParameterBuilder bind:parameters={query.parameters} bindable={false} />
<Spacer large />
{/if}
<Heading extraSmall black>Query</Heading>
<Spacer medium />
{#if schema} {#if schema}
{#key query._id} {#key query._id}
@ -38,7 +31,6 @@
readOnly={!editable} readOnly={!editable}
value={query.fields.sql} /> value={query.fields.sql} />
{:else if schema.type === QueryTypes.JSON} {:else if schema.type === QueryTypes.JSON}
<Spacer large />
<Editor <Editor
label="Query" label="Query"
mode="json" mode="json"

View File

@ -73,7 +73,7 @@
<a <a
target="_blank" target="_blank"
href="https://github.com/Budibase/budibase/discussions"> href="https://github.com/Budibase/budibase/discussions">
<i class="ri-question-line" /> <i class="ri-github-fill" />
</a> </a>
</div> </div>
<SettingsLink /> <SettingsLink />
@ -89,8 +89,8 @@
<div class="beta"> <div class="beta">
<Button <Button
secondary secondary
href="https://www.budibase.com/blog/budibase-public-beta/"> href="https://github.com/Budibase/budibase/discussions/categories/ideas">
Budibase is in Beta Request feature
</Button> </Button>
</div> </div>

View File

@ -21,9 +21,9 @@
query => query._id === $backendUiStore.selectedQueryId query => query._id === $backendUiStore.selectedQueryId
) || { ) || {
datasourceId: $params.selectedDatasource, datasourceId: $params.selectedDatasource,
name: "New Query",
parameters: [], parameters: [],
fields: {}, fields: {},
queryVerb: "read",
} }
</script> </script>
@ -35,8 +35,10 @@
<style> <style>
section { section {
background: var(--background); overflow: scroll;
padding: var(--spacing-xl); }
border-radius: var(--border-radius-m); ::-webkit-scrollbar {
width: 0px;
background: transparent; /* make scrollbar transparent */
} }
</style> </style>

View File

@ -1,6 +1,6 @@
<script> <script>
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { Button, Spacer, Icon, TextButton } from "@budibase/bbui" import { Button, 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"
@ -14,31 +14,129 @@
await backendUiStore.actions.datasources.save(datasource) await backendUiStore.actions.datasources.save(datasource)
notifier.success(`Datasource ${name} saved successfully.`) notifier.success(`Datasource ${name} saved successfully.`)
} }
function onClickQuery(query) {
if ($backendUiStore.selectedQueryId === query._id) {
return
}
backendUiStore.actions.queries.select(query)
$goto(`../${query._id}`)
}
</script> </script>
{#if datasource} {#if datasource}
<TextButton text small on:click={() => $goto('../new')}>
<Icon name="filter" />
Create Query
</TextButton>
<section> <section>
<h4>{datasource.name}: Configuration</h4>
<IntegrationConfigForm integration={datasource.config} />
<Spacer medium /> <Spacer medium />
<footer> <header>
<Button blue wide on:click={saveDatasource}>Save</Button> <h3 class="section-title">{datasource.name}</h3>
</footer> </header>
<Spacer extraLarge />
<div class="container">
<div class="config-header">
<h5>Configuration</h5>
<Button secondary on:click={saveDatasource}>Save</Button>
</div>
<Spacer medium />
<IntegrationConfigForm integration={datasource.config} />
</div>
<Spacer extraLarge />
<div class="container">
<div class="query-header">
<h5>Queries</h5>
<Button blue on:click={() => $goto('../new')}>Create Query</Button>
</div>
<Spacer extraLarge />
<div class="query-list">
{#each $backendUiStore.queries.filter(query => query.datasourceId === datasource._id) as query}
<div class="query-list-item" on:click={() => onClickQuery(query)}>
<p class="query-name">{query.name}</p>
<p>{query.queryVerb}</p>
<p>4000 records</p>
<p></p>
</div>
{/each}
<Spacer medium />
</div>
</div>
</section> </section>
{/if} {/if}
<style> <style>
h4 { h3 {
margin-top: var(--spacing-xl); margin: 0;
margin-bottom: var(--spacing-s);
} }
section { section {
background: var(--background); margin: 0 auto;
width: 800px;
}
header {
margin: 0 0 var(--spacing-xs) 0;
}
.section-title {
text-transform: capitalize;
}
.config-header {
display: flex;
justify-content: space-between;
margin: 0 0 var(--spacing-xs) 0;
}
.container {
border-radius: var(--border-radius-m); border-radius: var(--border-radius-m);
padding: var(--spacing-xl); background: var(--background);
padding: var(--layout-s);
margin: 0 auto;
}
h5 {
margin: 0 !important;
}
.query-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin: 0 0 var(--spacing-s) 0;
}
.query-list {
display: flex;
flex-direction: column;
gap: var(--spacing-m);
}
.query-list-item {
border-radius: var(--border-radius-m);
background: var(--background);
border: var(--border-grey);
display: grid;
grid-template-columns: 2fr 0.75fr 0.75fr 1fr 20px;
align-items: center;
padding: var(--spacing-m) var(--layout-xs);
gap: var(--layout-xs);
transition: 200ms background ease;
}
.query-list-item:hover {
background: var(--grey-1);
cursor: pointer;
}
p {
font-size: var(--font-size-xs);
color: var(--grey-8);
}
.query-name {
color: var(--ink);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: var(--font-size-s);
} }
</style> </style>

View File

@ -842,10 +842,10 @@
lodash "^4.17.19" lodash "^4.17.19"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@budibase/bbui@^1.54.0": "@budibase/bbui@^1.54.1":
version "1.54.0" version "1.54.1"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.54.0.tgz#60e6c0faa3d8f1781c503e74f8b8990f75ba2c40" resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.54.1.tgz#ad0439c0be6a4dc818cd9dacda00f053b0daa9d5"
integrity sha512-98koXkueqda6oQT6q0NPNvdL878ETRevtmmm34aSz9C6B4Oz68VVCsiFzRWuHvP/7wiNaAxMgY1nsEsCwP3LpQ== integrity sha512-ZY2OP/tF+ReMSyzZIGZV6wpQ4eIEzYGxZV3n+C+oNjzK5u3rwWPCDEVDlZgJSqJ61z+sEf2zuIyAh88lq9RTaA==
dependencies: dependencies:
markdown-it "^12.0.2" markdown-it "^12.0.2"
quill "^1.3.7" quill "^1.3.7"

View File

@ -1,15 +1,13 @@
import { notificationStore } from "../store/notification"
/** /**
* API cache for cached request responses. * API cache for cached request responses.
*/ */
import { notificationStore } from "../store/notification"
let cache = {} let cache = {}
/** /**
* Handler for API errors. * Handler for API errors.
*/ */
const handleError = error => { const handleError = error => {
notificationStore.danger(error)
return { error } return { error }
} }
@ -38,10 +36,12 @@ const makeApiCall = async ({ method, url, body, json = true }) => {
case 200: case 200:
return response.json() return response.json()
case 404: case 404:
notificationStore.danger("Not found")
return handleError(`${url}: Not Found`) return handleError(`${url}: Not Found`)
case 400: case 400:
return handleError(`${url}: Bad Request`) return handleError(`${url}: Bad Request`)
case 403: case 403:
notificationStore.danger("Forbidden")
return handleError(`${url}: Forbidden`) return handleError(`${url}: Forbidden`)
default: default:
if (response.status >= 200 && response.status < 400) { if (response.status >= 200 && response.status < 400) {
@ -77,7 +77,7 @@ const makeCachedApiCall = async params => {
const requestApiCall = method => async params => { const requestApiCall = method => async params => {
const { url, cache = false } = params const { url, cache = false } = params
const fixedUrl = `/${url}`.replace("//", "/") const fixedUrl = `/${url}`.replace("//", "/")
const enrichedParams = { ...params, method, fixedUrl } const enrichedParams = { ...params, method, url: fixedUrl }
return await (cache ? makeCachedApiCall : makeApiCall)(enrichedParams) return await (cache ? makeCachedApiCall : makeApiCall)(enrichedParams)
} }

View File

@ -1,10 +1,15 @@
import { notificationStore } from "../store/notification"
import API from "./api" import API from "./api"
/** /**
* Executes an automation. Must have "App Action" trigger. * Executes an automation. Must have "App Action" trigger.
*/ */
export const triggerAutomation = async (automationId, fields) => { export const triggerAutomation = async (automationId, fields) => {
return await API.post({ const res = await API.post({
url: `/api/automations/${automationId}/trigger`, url: `/api/automations/${automationId}/trigger`,
body: { fields }, body: { fields },
}) })
res.error
? notificationStore.danger("An error has occurred")
: notificationStore.success("Automation triggered")
return res
} }

View File

@ -1,14 +1,18 @@
import { notificationStore } from "../store/notification"
import API from "./api" import API from "./api"
/** /**
* Executes a query against an external data connector. * Executes a query against an external data connector.
*/ */
export const executeQuery = async ({ queryId, parameters }) => { export const executeQuery = async ({ queryId, parameters }) => {
const response = await API.post({ const res = await API.post({
url: `/api/queries/${queryId}`, url: `/api/queries/${queryId}`,
body: { body: {
parameters, parameters,
}, },
}) })
return response if (res.error) {
notificationStore.danger("An error has occurred")
}
return res
} }

View File

@ -1,3 +1,4 @@
import { notificationStore } from "../store/notification"
import API from "./api" import API from "./api"
import { fetchTableDefinition } from "./tables" import { fetchTableDefinition } from "./tables"
@ -15,42 +16,58 @@ export const fetchRow = async ({ tableId, rowId }) => {
* Creates a row in a table. * Creates a row in a table.
*/ */
export const saveRow = async row => { export const saveRow = async row => {
return await API.post({ const res = await API.post({
url: `/api/${row.tableId}/rows`, url: `/api/${row.tableId}/rows`,
body: row, body: row,
}) })
res.error
? notificationStore.danger("An error has occurred")
: notificationStore.success("Row saved")
return res
} }
/** /**
* Updates a row in a table. * Updates a row in a table.
*/ */
export const updateRow = async row => { export const updateRow = async row => {
return await API.patch({ const res = await API.patch({
url: `/api/${row.tableId}/rows/${row._id}`, url: `/api/${row.tableId}/rows/${row._id}`,
body: row, body: row,
}) })
res.error
? notificationStore.danger("An error has occurred")
: notificationStore.success("Row updated")
return res
} }
/** /**
* Deletes a row from a table. * Deletes a row from a table.
*/ */
export const deleteRow = async ({ tableId, rowId, revId }) => { export const deleteRow = async ({ tableId, rowId, revId }) => {
return await API.del({ const res = await API.del({
url: `/api/${tableId}/rows/${rowId}/${revId}`, url: `/api/${tableId}/rows/${rowId}/${revId}`,
}) })
res.error
? notificationStore.danger("An error has occurred")
: notificationStore.success("Row deleted")
return res
} }
/** /**
* Deletes many rows from a table. * Deletes many rows from a table.
*/ */
export const deleteRows = async ({ tableId, rows }) => { export const deleteRows = async ({ tableId, rows }) => {
return await API.post({ const res = await API.post({
url: `/api/${tableId}/rows`, url: `/api/${tableId}/rows`,
body: { body: {
rows, rows,
type: "delete", type: "delete",
}, },
}) })
res.error
? notificationStore.danger("An error has occurred")
: notificationStore.success(`${rows.length} row(s) deleted`)
return res
} }
/** /**

View File

@ -5,6 +5,7 @@ import {
routeStore, routeStore,
screenStore, screenStore,
bindingStore, bindingStore,
builderStore,
} from "./store" } from "./store"
import { styleable } from "./utils/styleable" import { styleable } from "./utils/styleable"
import { linkable } from "./utils/linkable" import { linkable } from "./utils/linkable"
@ -16,6 +17,7 @@ export default {
notifications: notificationStore, notifications: notificationStore,
routeStore, routeStore,
screenStore, screenStore,
builderStore,
styleable, styleable,
linkable, linkable,
DataProvider, DataProvider,

View File

@ -31,7 +31,6 @@ function generateQueryValidation() {
default: Joi.string() default: Joi.string()
})), })),
queryVerb: Joi.string().allow(...Object.values(QueryVerb)).required(), queryVerb: Joi.string().allow(...Object.values(QueryVerb)).required(),
queryType: Joi.string().required(),
schema: Joi.object({}).required().unknown(true) schema: Joi.object({}).required().unknown(true)
})) }))
} }

View File

@ -26,7 +26,6 @@ const TEST_QUERY = {
fields:{}, fields:{},
schema:{}, schema:{},
queryVerb:"read", queryVerb:"read",
queryType:"Table",
} }
describe("/datasources", () => { describe("/datasources", () => {

View File

@ -26,7 +26,6 @@ const TEST_QUERY = {
fields:{}, fields:{},
schema:{}, schema:{},
queryVerb:"read", queryVerb:"read",
queryType:"Table",
} }
describe("/queries", () => { describe("/queries", () => {

View File

@ -17,7 +17,6 @@ const SCHEMA = {
}, },
query: { query: {
create: { create: {
"Airtable Record": {
type: QUERY_TYPES.FIELDS, type: QUERY_TYPES.FIELDS,
customisable: true, customisable: true,
fields: { fields: {
@ -27,9 +26,7 @@ const SCHEMA = {
}, },
}, },
}, },
},
read: { read: {
Table: {
type: QUERY_TYPES.FIELDS, type: QUERY_TYPES.FIELDS,
fields: { fields: {
table: { table: {
@ -42,9 +39,7 @@ const SCHEMA = {
}, },
}, },
}, },
},
update: { update: {
Fields: {
type: QUERY_TYPES.FIELDS, type: QUERY_TYPES.FIELDS,
customisable: true, customisable: true,
fields: { fields: {
@ -54,13 +49,10 @@ const SCHEMA = {
}, },
}, },
}, },
},
delete: { delete: {
"Airtable Ids": {
type: FIELD_TYPES.JSON, type: FIELD_TYPES.JSON,
}, },
}, },
},
} }
class AirtableIntegration { class AirtableIntegration {

View File

@ -16,22 +16,15 @@ const SCHEMA = {
}, },
query: { query: {
create: { create: {
"CouchDB DSL": {
type: QUERY_TYPES.JSON, type: QUERY_TYPES.JSON,
}, },
},
read: { read: {
"CouchDB DSL": {
type: QUERY_TYPES.JSON, type: QUERY_TYPES.JSON,
}, },
},
update: { update: {
"CouchDB Document": {
type: QUERY_TYPES.JSON, type: QUERY_TYPES.JSON,
}, },
},
delete: { delete: {
"Document ID": {
type: QUERY_TYPES.FIELDS, type: QUERY_TYPES.FIELDS,
fields: { fields: {
id: { id: {
@ -41,7 +34,6 @@ const SCHEMA = {
}, },
}, },
}, },
},
} }
class CouchDBIntegration { class CouchDBIntegration {

View File

@ -20,7 +20,6 @@ const SCHEMA = {
}, },
query: { query: {
create: { create: {
DynamoConfig: {
type: QUERY_TYPES.FIELDS, type: QUERY_TYPES.FIELDS,
fields: { fields: {
table: { table: {
@ -30,9 +29,7 @@ const SCHEMA = {
customisable: true, customisable: true,
}, },
}, },
},
read: { read: {
DynamoConfig: {
type: QUERY_TYPES.FIELDS, type: QUERY_TYPES.FIELDS,
fields: { fields: {
table: { table: {
@ -45,9 +42,7 @@ const SCHEMA = {
customisable: true, customisable: true,
}, },
}, },
},
update: { update: {
DynamoConfig: {
type: QUERY_TYPES.FIELDS, type: QUERY_TYPES.FIELDS,
fields: { fields: {
table: { table: {
@ -57,9 +52,7 @@ const SCHEMA = {
customisable: true, customisable: true,
}, },
}, },
},
delete: { delete: {
"Dynamo Partition Key": {
type: QUERY_TYPES.FIELDS, type: QUERY_TYPES.FIELDS,
fields: { fields: {
table: { table: {
@ -73,7 +66,6 @@ const SCHEMA = {
}, },
}, },
}, },
},
} }
class DynamoDBIntegration { class DynamoDBIntegration {

View File

@ -13,7 +13,6 @@ const SCHEMA = {
}, },
query: { query: {
create: { create: {
"ES Query DSL": {
type: QUERY_TYPES.FIELDS, type: QUERY_TYPES.FIELDS,
customisable: true, customisable: true,
fields: { fields: {
@ -23,9 +22,7 @@ const SCHEMA = {
}, },
}, },
}, },
},
read: { read: {
"ES Query DSL": {
type: QUERY_TYPES.FIELDS, type: QUERY_TYPES.FIELDS,
customisable: true, customisable: true,
fields: { fields: {
@ -35,9 +32,7 @@ const SCHEMA = {
}, },
}, },
}, },
},
update: { update: {
"ES Query DSL": {
type: QUERY_TYPES.FIELDS, type: QUERY_TYPES.FIELDS,
customisable: true, customisable: true,
fields: { fields: {
@ -51,9 +46,7 @@ const SCHEMA = {
}, },
}, },
}, },
},
delete: { delete: {
"Document ID": {
type: QUERY_TYPES.FIELDS, type: QUERY_TYPES.FIELDS,
fields: { fields: {
index: { index: {
@ -67,7 +60,6 @@ const SCHEMA = {
}, },
}, },
}, },
},
} }
class ElasticSearchIntegration { class ElasticSearchIntegration {

View File

@ -24,16 +24,12 @@ const SCHEMA = {
}, },
query: { query: {
create: { create: {
SQL: {
type: "sql", type: "sql",
}, },
},
read: { read: {
SQL: {
type: "sql", type: "sql",
}, },
}, },
},
} }
class SqlServerIntegration { class SqlServerIntegration {

View File

@ -20,16 +20,12 @@ const SCHEMA = {
}, },
query: { query: {
create: { create: {
JSON: {
type: QUERY_TYPES.JSON, type: QUERY_TYPES.JSON,
}, },
},
read: { read: {
JSON: {
type: QUERY_TYPES.JSON, type: QUERY_TYPES.JSON,
}, },
}, },
},
} }
class MongoIntegration { class MongoIntegration {

View File

@ -31,26 +31,18 @@ const SCHEMA = {
}, },
query: { query: {
create: { create: {
SQL: {
type: "sql", type: "sql",
}, },
},
read: { read: {
SQL: {
type: "sql", type: "sql",
}, },
},
update: { update: {
SQL: {
type: "sql", type: "sql",
}, },
},
delete: { delete: {
SQL: {
type: "sql", type: "sql",
}, },
}, },
},
} }
class PostgresIntegration { class PostgresIntegration {

View File

@ -19,7 +19,6 @@ const SCHEMA = {
}, },
query: { query: {
read: { read: {
Bucket: {
type: "fields", type: "fields",
fields: { fields: {
bucket: { bucket: {
@ -29,7 +28,6 @@ const SCHEMA = {
}, },
}, },
}, },
},
} }
class S3Integration { class S3Integration {

File diff suppressed because it is too large Load Diff

View File

@ -2,12 +2,13 @@
import { getContext } from "svelte" import { getContext } from "svelte"
import { isEmpty } from "lodash/fp" import { isEmpty } from "lodash/fp"
const { API, styleable, DataProvider } = getContext("sdk") const { API, styleable, DataProvider, builderStore } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
export let datasource = [] export let datasource = []
let rows = [] let rows = []
let loaded = false
$: fetchData(datasource) $: fetchData(datasource)
@ -15,21 +16,22 @@
if (!isEmpty(datasource)) { if (!isEmpty(datasource)) {
rows = await API.fetchDatasource(datasource) rows = await API.fetchDatasource(datasource)
} }
loaded = true
} }
</script> </script>
<div use:styleable={$component.styles}> <div use:styleable={$component.styles}>
{#if rows.length > 0} {#if rows.length > 0}
{#if $component.children === 0 && $builderStore.inBuilder}
<p>Add some components too</p>
{:else}
{#each rows as row} {#each rows as row}
<DataProvider {row}> <DataProvider {row}>
{#if $component.children === 0}
<p>Add some components too.</p>
{:else}
<slot /> <slot />
{/if}
</DataProvider> </DataProvider>
{/each} {/each}
{:else} {/if}
{:else if loaded && $builderStore.inBuilder}
<p>Feed me some data</p> <p>Feed me some data</p>
{/if} {/if}
</div> </div>