Merge branch 'master' of github.com:Budibase/budibase into feature/handlebars-helpers

This commit is contained in:
mike12345567 2021-01-26 10:42:54 +00:00
commit 18f96deb00
45 changed files with 1268 additions and 346 deletions

View File

@ -20,7 +20,7 @@
<p align="center">
<img src="https://i.imgur.com/tMCahK8.png">
<img src="https://i.imgur.com/tPQHruf.png">
</p>
<p align="center">
@ -69,17 +69,10 @@ When other platforms chose the closed source route, we decided to go open source
- **Cloud hosting and self-hosting.** Users can self-host (see below), or host their apps with Budibase. Currently, our cloud hosting offering is limited to the free tier but we aim to change this in the future. For heavy usage, we advise users to self-host.
## 🤖 Self-hosting
<p align="center">
<img src="https://i.imgur.com/Z52cEvT.png?1" />
<img alt="Budibase design ui" src="https://imgur.com/v8m6v3q.png">
</p>
Budibase wants to make sure anyone can use the tools we develop and we know a lot of people need to be able to host the apps they make on their own systems - that is why we've decided to try and make self hosting as easy as possible!
Currently, you can host your apps using Docker. The documentation for self-hosting can be found [here](https://docs.budibase.com/self-hosting/introduction-to-self-hosting).
## ⌛ Status
- [x] Alpha: We are demoing Budibase to users and receiving feedback
@ -95,10 +88,6 @@ Watch "releases" of this repo to get notified of major updates, and give the sta
If you are having issues between updates of the builder, please use the guide [here](https://github.com/Budibase/budibase/blob/master/CONTRIBUTING.md#troubleshooting) to clear down your environment.
## Roadmap
Checkout our [Public Roadmap](https://github.com/Budibase/budibase/projects/10). If you would like to discuss some of the items on the roadmap, please feel to reach out on [Discord](https://discord.gg/rCYayfe), or via [Github discussions](https://github.com/Budibase/budibase/discussions)
## 🏁 Getting Started with Budibase
@ -111,10 +100,17 @@ The Budibase builder runs in Electron, on Mac, PC and Linux. Follow the steps be
[Here is a guided tutorial](https://docs.budibase.com/tutorial/tutorial-signing-up) if you need extra help.
## 🤖 Self-hosting
<p align="center">
<img alt="Budibase design ui" src="https://imgur.com/v8m6v3q.png">
<img src="https://i.imgur.com/Z52cEvT.png?1" />
</p>
Budibase wants to make sure anyone can use the tools we develop and we know a lot of people need to be able to host the apps they make on their own systems - that is why we've decided to try and make self hosting as easy as possible!
Currently, you can host your apps using Docker. The documentation for self-hosting can be found [here](https://docs.budibase.com/self-hosting/introduction-to-self-hosting).
## 🎓 Learning Budibase
@ -122,6 +118,12 @@ The Budibase [documentation lives here](https://docs.budibase.com).
You can also follow a quick tutorial on [how to build a CRM with Budibase](https://docs.budibase.com/tutorial/tutorial-introduction)
## Roadmap
Checkout our [Public Roadmap](https://github.com/Budibase/budibase/projects/10). If you would like to discuss some of the items on the roadmap, please feel to reach out on [Discord](https://discord.gg/rCYayfe), or via [Github discussions](https://github.com/Budibase/budibase/discussions)
## ❗ Code of Conduct
Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/master/.github/CODE_OF_CONDUCT.md). Please read it.

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

View File

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

View File

@ -103,6 +103,15 @@
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 {
position: relative;
top: 2px;

View File

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

View File

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

@ -1,6 +1,6 @@
<script>
import { notificationStore } from "builderStore/store/notifications"
import { onMount, onDestroy } from "svelte"
import { flip } from 'svelte/animate';
import { fly } from "svelte/transition"
export let themes = {
@ -27,6 +27,7 @@
<div class="notifications">
{#each $notificationStore.notifications as notification (notification.id)}
<div
animate:flip
class="toast"
style="background: {themes[notification.type]};"
transition:fly={{ y: -30 }}>

View File

@ -57,7 +57,7 @@
}
</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'}>
<heading slot="buttons">
<Button thin blue on:click={saveEventData}>Save</Button>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<script>
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 { notifier } from "builderStore/store/notifications"
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
@ -14,31 +14,129 @@
await backendUiStore.actions.datasources.save(datasource)
notifier.success(`Datasource ${name} saved successfully.`)
}
function onClickQuery(query) {
if ($backendUiStore.selectedQueryId === query._id) {
return
}
backendUiStore.actions.queries.select(query)
$goto(`../${query._id}`)
}
</script>
{#if datasource}
<TextButton text small on:click={() => $goto('../new')}>
<Icon name="filter" />
Create Query
</TextButton>
<section>
<h4>{datasource.name}: Configuration</h4>
<IntegrationConfigForm integration={datasource.config} />
<Spacer medium />
<footer>
<Button blue wide on:click={saveDatasource}>Save</Button>
</footer>
<header>
<h3 class="section-title">{datasource.name}</h3>
</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>
{/if}
<style>
h4 {
margin-top: var(--spacing-xl);
margin-bottom: var(--spacing-s);
h3 {
margin: 0;
}
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);
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>

View File

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

View File

@ -12,6 +12,7 @@
"@budibase/string-templates": "^0.5.3",
"deep-equal": "^2.0.1",
"regexparam": "^1.3.0",
"shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5"
},
"devDependencies": {

View File

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

View File

@ -1,10 +1,15 @@
import { notificationStore } from "../store/notification"
import API from "./api"
/**
* Executes an automation. Must have "App Action" trigger.
*/
export const triggerAutomation = async (automationId, fields) => {
return await API.post({
const res = await API.post({
url: `/api/automations/${automationId}/trigger`,
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"
/**
* Executes a query against an external data connector.
*/
export const executeQuery = async ({ queryId, parameters }) => {
const response = await API.post({
const res = await API.post({
url: `/api/queries/${queryId}`,
body: {
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 { fetchTableDefinition } from "./tables"
@ -15,42 +16,58 @@ export const fetchRow = async ({ tableId, rowId }) => {
* Creates a row in a table.
*/
export const saveRow = async row => {
return await API.post({
const res = await API.post({
url: `/api/${row.tableId}/rows`,
body: row,
})
res.error
? notificationStore.danger("An error has occurred")
: notificationStore.success("Row saved")
return res
}
/**
* Updates a row in a table.
*/
export const updateRow = async row => {
return await API.patch({
const res = await API.patch({
url: `/api/${row.tableId}/rows/${row._id}`,
body: row,
})
res.error
? notificationStore.danger("An error has occurred")
: notificationStore.success("Row updated")
return res
}
/**
* Deletes a row from a table.
*/
export const deleteRow = async ({ tableId, rowId, revId }) => {
return await API.del({
const res = await API.del({
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.
*/
export const deleteRows = async ({ tableId, rows }) => {
return await API.post({
const res = await API.post({
url: `/api/${tableId}/rows`,
body: {
rows,
type: "delete",
},
})
res.error
? notificationStore.danger("An error has occurred")
: notificationStore.success(`${rows.length} row(s) deleted`)
return res
}
/**

View File

@ -2,8 +2,9 @@
import { writable } from "svelte/store"
import { setContext, onMount } from "svelte"
import Component from "./Component.svelte"
import NotificationDisplay from './NotificationDisplay.svelte'
import SDK from "../sdk"
import { createDataStore, initialise, screenStore } from "../store"
import { createDataStore, initialise, screenStore, notificationStore } from "../store"
// Provide contexts
setContext("sdk", SDK)
@ -23,3 +24,4 @@
{#if loaded && $screenStore.activeLayout}
<Component definition={$screenStore.activeLayout.props} />
{/if}
<NotificationDisplay />

View File

@ -0,0 +1,60 @@
<script>
import { flip } from 'svelte/animate';
import { fly } from "svelte/transition"
import { getContext } from "svelte"
const { notifications } = getContext("sdk")
export let themes = {
danger: "#E26D69",
success: "#84C991",
warning: "#f0ad4e",
info: "#5bc0de",
default: "#aaaaaa",
}
</script>
<div class="notifications">
{#each $notifications as notification (notification.id)}
<div
animate:flip
class="toast"
style="background: {themes[notification.type]};"
transition:fly={{ y: -30 }}>
<div class="content">{notification.message}</div>
{#if notification.icon}<i class={notification.icon} />{/if}
</div>
{/each}
</div>
<style>
.notifications {
position: fixed;
top: 10px;
left: 0;
right: 0;
margin: 0 auto;
padding: 0;
z-index: 9999;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
pointer-events: none;
}
.toast {
flex: 0 0 auto;
margin-bottom: 10px;
border-radius: var(--border-radius-s);
/* The toasts now support being auto sized, so this static width could be removed */
width: 40vw;
}
.content {
padding: 10px;
display: block;
color: white;
font-weight: 500;
}
</style>

View File

@ -1,5 +1,12 @@
import * as API from "./api"
import { authStore, routeStore, screenStore, bindingStore } from "./store"
import {
authStore,
notificationStore,
routeStore,
screenStore,
bindingStore,
builderStore,
} from "./store"
import { styleable } from "./utils/styleable"
import { linkable } from "./utils/linkable"
import DataProvider from "./components/DataProvider.svelte"
@ -7,8 +14,10 @@ import DataProvider from "./components/DataProvider.svelte"
export default {
API,
authStore,
notifications: notificationStore,
routeStore,
screenStore,
builderStore,
styleable,
linkable,
DataProvider,

View File

@ -1,4 +1,5 @@
export { authStore } from "./auth"
export { notificationStore } from "./notification"
export { routeStore } from "./routes"
export { screenStore } from "./screens"
export { builderStore } from "./builder"

View File

@ -0,0 +1,42 @@
import { writable, derived } from "svelte/store"
import { generate } from "shortid"
const NOTIFICATION_TIMEOUT = 3000
const createNotificationStore = () => {
const _notifications = writable([])
const send = (message, type = "default") => {
_notifications.update(state => {
return [...state, { id: generate(), type, message }]
})
}
const notifications = derived(_notifications, ($_notifications, set) => {
set($_notifications)
if ($_notifications.length > 0) {
const timeout = setTimeout(() => {
_notifications.update(state => {
state.shift()
return state
})
set($_notifications)
}, NOTIFICATION_TIMEOUT)
return () => {
clearTimeout(timeout)
}
}
})
const { subscribe } = notifications
return {
subscribe,
send,
danger: msg => send(msg, "danger"),
warning: msg => send(msg, "warning"),
info: msg => send(msg, "info"),
success: msg => send(msg, "success"),
}
}
export const notificationStore = createNotificationStore()

View File

@ -1362,6 +1362,11 @@ minimatch@^3.0.4:
dependencies:
brace-expansion "^1.1.7"
nanoid@^2.1.0:
version "2.1.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==
nwsapi@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
@ -1803,6 +1808,13 @@ sha.js@^2.4.0, sha.js@^2.4.8:
inherits "^2.0.1"
safe-buffer "^5.0.1"
shortid@^2.2.15:
version "2.2.16"
resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.16.tgz#b742b8f0cb96406fd391c76bfc18a67a57fe5608"
integrity sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g==
dependencies:
nanoid "^2.1.0"
side-channel@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3"

View File

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

View File

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

View File

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

View File

@ -17,48 +17,40 @@ const SCHEMA = {
},
query: {
create: {
"Airtable Record": {
type: QUERY_TYPES.FIELDS,
customisable: true,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
type: QUERY_TYPES.FIELDS,
customisable: true,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},
read: {
Table: {
type: QUERY_TYPES.FIELDS,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
view: {
type: FIELD_TYPES.STRING,
required: true,
},
type: QUERY_TYPES.FIELDS,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
view: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},
update: {
Fields: {
type: QUERY_TYPES.FIELDS,
customisable: true,
fields: {
id: {
type: FIELD_TYPES.STRING,
required: true,
},
type: QUERY_TYPES.FIELDS,
customisable: true,
fields: {
id: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},
delete: {
"Airtable Ids": {
type: FIELD_TYPES.JSON,
},
type: FIELD_TYPES.JSON,
},
},
}

View File

@ -16,28 +16,20 @@ const SCHEMA = {
},
query: {
create: {
"CouchDB DSL": {
type: QUERY_TYPES.JSON,
},
type: QUERY_TYPES.JSON,
},
read: {
"CouchDB DSL": {
type: QUERY_TYPES.JSON,
},
type: QUERY_TYPES.JSON,
},
update: {
"CouchDB Document": {
type: QUERY_TYPES.JSON,
},
type: QUERY_TYPES.JSON,
},
delete: {
"Document ID": {
type: QUERY_TYPES.FIELDS,
fields: {
id: {
type: FIELD_TYPES.STRING,
required: true,
},
type: QUERY_TYPES.FIELDS,
fields: {
id: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},

View File

@ -20,56 +20,48 @@ const SCHEMA = {
},
query: {
create: {
DynamoConfig: {
type: QUERY_TYPES.FIELDS,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
customisable: true,
type: QUERY_TYPES.FIELDS,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
customisable: true,
},
},
read: {
DynamoConfig: {
type: QUERY_TYPES.FIELDS,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
index: {
type: FIELD_TYPES.STRING,
},
customisable: true,
type: QUERY_TYPES.FIELDS,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
index: {
type: FIELD_TYPES.STRING,
},
customisable: true,
},
},
update: {
DynamoConfig: {
type: QUERY_TYPES.FIELDS,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
customisable: true,
type: QUERY_TYPES.FIELDS,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
customisable: true,
},
},
delete: {
"Dynamo Partition Key": {
type: QUERY_TYPES.FIELDS,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
key: {
type: FIELD_TYPES.STRING,
required: true,
},
type: QUERY_TYPES.FIELDS,
fields: {
table: {
type: FIELD_TYPES.STRING,
required: true,
},
key: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},

View File

@ -13,57 +13,49 @@ const SCHEMA = {
},
query: {
create: {
"ES Query DSL": {
type: QUERY_TYPES.FIELDS,
customisable: true,
fields: {
index: {
type: FIELD_TYPES.STRING,
required: true,
},
type: QUERY_TYPES.FIELDS,
customisable: true,
fields: {
index: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},
read: {
"ES Query DSL": {
type: QUERY_TYPES.FIELDS,
customisable: true,
fields: {
index: {
type: FIELD_TYPES.STRING,
required: true,
},
type: QUERY_TYPES.FIELDS,
customisable: true,
fields: {
index: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},
update: {
"ES Query DSL": {
type: QUERY_TYPES.FIELDS,
customisable: true,
fields: {
id: {
type: FIELD_TYPES.STRING,
required: true,
},
index: {
type: FIELD_TYPES.STRING,
required: true,
},
type: QUERY_TYPES.FIELDS,
customisable: true,
fields: {
id: {
type: FIELD_TYPES.STRING,
required: true,
},
index: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},
delete: {
"Document ID": {
type: QUERY_TYPES.FIELDS,
fields: {
index: {
type: FIELD_TYPES.STRING,
required: true,
},
id: {
type: FIELD_TYPES.STRING,
required: true,
},
type: QUERY_TYPES.FIELDS,
fields: {
index: {
type: FIELD_TYPES.STRING,
required: true,
},
id: {
type: FIELD_TYPES.STRING,
required: true,
},
},
},

View File

@ -24,14 +24,10 @@ const SCHEMA = {
},
query: {
create: {
SQL: {
type: "sql",
},
type: "sql",
},
read: {
SQL: {
type: "sql",
},
type: "sql",
},
},
}

View File

@ -20,14 +20,10 @@ const SCHEMA = {
},
query: {
create: {
JSON: {
type: QUERY_TYPES.JSON,
},
type: QUERY_TYPES.JSON,
},
read: {
JSON: {
type: QUERY_TYPES.JSON,
},
type: QUERY_TYPES.JSON,
},
},
}

View File

@ -31,24 +31,16 @@ const SCHEMA = {
},
query: {
create: {
SQL: {
type: "sql",
},
type: "sql",
},
read: {
SQL: {
type: "sql",
},
type: "sql",
},
update: {
SQL: {
type: "sql",
},
type: "sql",
},
delete: {
SQL: {
type: "sql",
},
type: "sql",
},
},
}

View File

@ -19,13 +19,11 @@ const SCHEMA = {
},
query: {
read: {
Bucket: {
type: "fields",
fields: {
bucket: {
type: "string",
required: true,
},
type: "fields",
fields: {
bucket: {
type: "string",
required: true,
},
},
},

File diff suppressed because it is too large Load Diff

View File

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