Allow dynamic query execution from front end components

This commit is contained in:
Martin McKeaveney 2021-01-08 12:06:37 +00:00
parent f2abc7d351
commit 98a7085bbc
22 changed files with 344 additions and 198 deletions

View File

@ -40,6 +40,7 @@ const INITIAL_FRONTEND_STATE = {
libraries: null, libraries: null,
appId: "", appId: "",
routes: {}, routes: {},
bottomDrawerVisible: false,
} }
export const getFrontendStore = () => { export const getFrontendStore = () => {

View File

@ -1,49 +0,0 @@
<script>
import { goto } from "@sveltech/routify"
import {
DropdownMenu,
TextButton as Button,
Icon,
Label,
Modal,
ModalContent,
TextArea,
} from "@budibase/bbui"
import { notifier } from "builderStore/store/notifications"
import { backendUiStore } from "builderStore"
import api from "builderStore/api"
import EditIntegrationConfig from "../modals/EditIntegrationConfig.svelte"
// import CreateEditQuery from "components/backend/DataTable/modals/CreateEditQuery.svelte"
export let query = {}
export let edit
let modal
let fields = []
async function saveQuery() {
try {
await backendUiStore.actions.queries.save(query.datasourceId, query)
notifier.success(`Query created successfully.`)
} catch (err) {
console.error(err)
notifier.danger(`Error creating query. ${err.message}`)
}
}
</script>
<div>
<Button text small on:click={modal.show}>
<Icon name="filter" />
{edit ? 'Edit' : 'Create'} Query
</Button>
</div>
<!-- <Modal bind:this={modal}> -->
<!-- <ModalContent
confirmText="Save"
cancelText="Cancel"
onConfirm={saveQuery}
title={edit ? 'Edit Query' : 'Create New Query'}> -->
<!-- <CreateEditQuery bind:query /> -->
<!-- </ModalContent> -->
<!-- </Modal> -->

View File

@ -0,0 +1,29 @@
<script>
import { slide } from "svelte/transition"
import Portal from "svelte-portal"
export let title
export let onClose
</script>
<Portal>
<section class="drawer" transition:slide>
{#if title}
<heading>{title}</heading>
{/if}
<slot />
</section>
</Portal>
<style>
.drawer {
height: 50vh;
position: absolute;
bottom: 0;
width: 97%;
background: var(--background);
padding: var(--spacing-xl);
border-top: var(--border-light);
z-index: 2;
}
</style>

View File

@ -12,6 +12,10 @@
// $: codemirror && codemirror.setValue(value) // $: codemirror && codemirror.setValue(value)
console.log("Running init")
$: console.log("Running reactive")
onMount(async () => { onMount(async () => {
codemirror = cm.fromTextArea(editor, { codemirror = cm.fromTextArea(editor, {
lineNumbers: true, lineNumbers: true,

View File

@ -0,0 +1,52 @@
<script>
import { TextArea, Label, Input, Heading } from "@budibase/bbui"
import Editor from "./QueryEditor.svelte"
import BindableInput from "components/userInterface/BindableInput.svelte"
export let parameters = []
export let bindings = []
export let customParams = {}
function newQueryParameter() {
parameters = [...parameters, {}]
}
function deleteQueryParameter(idx) {
parameters.splice(idx, 1)
parameters = parameters
}
</script>
<Heading extraSmall black>Parameters</Heading>
<div class="parameters">
<Label extraSmall grey>Parameter Name</Label>
<Label extraSmall grey>Default</Label>
<Label extraSmall grey>Value</Label>
<div />
{#each parameters as parameter, idx}
<Input thin bind:value={parameter.name} />
<Input thin bind:value={parameter.default} />
<BindableInput
type="string"
thin
bind:value={customParams[parameter.name]}
{bindings} />
<i
class="ri-close-circle-line delete"
on:click={() => deleteQueryParameter(idx)} />
{/each}
</div>
<i class="ri-add-circle-line add" on:click={newQueryParameter} />
<style>
.parameters {
display: grid;
grid-template-columns: 1fr 1fr 1fr 5%;
grid-gap: 10px;
align-items: center;
}
.add {
margin-top: var(--spacing-m);
}
</style>

View File

@ -85,8 +85,18 @@
async function previewQuery() { async function previewQuery() {
try { try {
// parse all the parameters in the UI
// send them
const response = await api.post(`/api/queries/preview`, { const response = await api.post(`/api/queries/preview`, {
parameters: query.parameters, // TODO: revisit
parameters: query.parameters.reduce(
(acc, next) => ({
...acc,
[next.name]: next.default,
}),
{}
),
datasourceId: datasource._id, datasourceId: datasource._id,
query: query.queryString, query: query.queryString,
}) })
@ -96,7 +106,8 @@
data = json || [] data = json || []
// TODO: refactor // Assume all the fields are strings and create a basic schema
// from the first record returned by the query
fields = Object.keys(json[0]).map(field => ({ fields = Object.keys(json[0]).map(field => ({
name: field, name: field,
type: "STRING", type: "STRING",

View File

@ -1,65 +1,16 @@
<script> <script>
import { TextArea, Label, Input, Heading } from "@budibase/bbui" import { TextArea, Label, Input, Heading } from "@budibase/bbui"
import Editor from "./QueryEditor.svelte" import Editor from "./QueryEditor.svelte"
import ParameterBuilder from "./QueryParameterBuilder.svelte"
const CAPTURE_VAR_INSIDE_MUSTACHE = /{{([^}]+)}}/g
const QueryTypes = { const QueryTypes = {
SQL: "sql", SQL: "sql",
} }
export let query export let query
// TODO: bind these to the query
let parameters = []
$: query.parameters = parameters.reduce(
(acc, next) => ({ [next.key]: next.value, ...acc }),
{}
)
function newQueryParameter() {
parameters = [...parameters, {}]
}
function deleteQueryParameter(idx) {
parameters.splice(idx, 1)
parameters = parameters
}
</script> </script>
<Heading extraSmall black>Parameters</Heading>
{#if query.queryType === QueryTypes.SQL} {#if query.queryType === QueryTypes.SQL}
<section> <ParameterBuilder bind:parameters={query.parameters} />
<div class="parameters">
<Label extraSmall grey>Parameter Name</Label>
<Label extraSmall grey>Default Value</Label>
<!-- CLEAR ALL PARAMS OR SOMETHING -->
<i class="ri-close-circle-line delete" on:click={console.log} />
{#each parameters as parameter, idx}
<Input thin bind:value={parameter.key} />
<Input thin bind:value={parameter.value} />
<i
class="ri-close-circle-line delete"
on:click={() => deleteQueryParameter(idx)} />
{/each}
<i class="ri-add-circle-line" on:click={newQueryParameter} />
</div>
</section>
<Editor label="Query" bind:value={query.queryString} /> <Editor label="Query" bind:value={query.queryString} />
{/if} {/if}
<style>
.parameters {
display: grid;
grid-template-columns: 1fr 1fr 5%;
grid-gap: 10px;
align-items: center;
}
section {
margin-bottom: var(--spacing-xl);
}
</style>

View File

@ -0,0 +1,35 @@
<script>
import BottomDrawer from "components/common/BottomDrawer.svelte"
import { store, backendUiStore } from "builderStore"
import { slide } from "svelte/transition"
import QueryInterface from "components/integration/QueryViewer.svelte"
$: query = $backendUiStore.queries.find(
query => query._id === $backendUiStore.selectedQueryId
)
function closeDatabindingDrawer() {
store.update(state => {
state.bindingDrawerVisible = false
return state
})
}
</script>
{#if query}
<BottomDrawer>
<div class="drawer-contents">
<i class="ri-close-fill close" on:click={closeDatabindingDrawer} />
<QueryInterface {query} />
</div>
</BottomDrawer>
{/if}
<style>
i {
position: absolute;
top: var(--spacing-xl);
right: var(--spacing-xl);
font-size: var(--font-size-m);
}
</style>

View File

@ -1,5 +1,11 @@
<script> <script>
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui" import {
Button,
TextButton,
Body,
DropdownMenu,
ModalContent,
} from "@budibase/bbui"
import { AddIcon, ArrowDownIcon } from "components/common/Icons/" import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
import actionTypes from "./actions" import actionTypes from "./actions"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
@ -49,8 +55,7 @@
} }
</script> </script>
<ModalContent title="Actions" confirmText="Save" onConfirm={saveEventData}> <div>
<div slot="header">
<div bind:this={addActionButton}> <div bind:this={addActionButton}>
<TextButton text small blue on:click={addActionDropdown.show}> <TextButton text small blue on:click={addActionDropdown.show}>
<div style="height: 20px; width: 20px;"> <div style="height: 20px; width: 20px;">
@ -98,12 +103,10 @@
</div> </div>
{/each} {/each}
{/if} {/if}
<Button thin blue on:click={saveEventData}>Save</Button>
</div> </div>
<div slot="footer">
<a href="https://docs.budibase.com">Learn more about Actions</a> <a href="https://docs.budibase.com">Learn more about Actions</a>
</div>
</ModalContent>
<style> <style>
.action-header { .action-header {

View File

@ -1,17 +1,26 @@
<script> <script>
import { Button, Modal } from "@budibase/bbui" import { Button, Modal } from "@budibase/bbui"
import EventEditorModal from "./EventEditorModal.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { store } from "builderStore"
import EventEditorModal from "./EventEditorModal.svelte"
import BottomDrawer from "components/common/BottomDrawer.svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let value export let value
export let name export let name
let modal let drawerVisible
function showDrawer() {
drawerVisible = true
}
</script> </script>
<Button secondary small on:click={modal.show}>Define Actions</Button> <Button secondary small on:click={showDrawer}>Define Actions</Button>
<Modal bind:this={modal} width="600px"> {#if drawerVisible}
<BottomDrawer>
<EventEditorModal event={value} eventType={name} on:change /> <EventEditorModal event={value} eventType={name} on:change />
</Modal> </BottomDrawer>
{/if}

View File

@ -1,17 +1,36 @@
<script> <script>
import { Select, Label, Spacer } from "@budibase/bbui" import { Select, Label, Spacer } from "@budibase/bbui"
import { backendUiStore } from "builderStore" import { store, backendUiStore, currentAsset } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import ParameterBuilder from "../../../integration/QueryParameterBuilder.svelte"
export let parameters export let parameters
$: datasource = $backendUiStore.datasources.find( $: datasource = $backendUiStore.datasources.find(
ds => ds._id === parameters.datasourceId ds => ds._id === parameters.datasourceId
) )
// TODO: binding needs a significant refactor and needs to
// be centralised
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
screen: $currentAsset,
tables: $backendUiStore.tables,
}).map(property => ({
...property,
category: property.type === "instance" ? "Component" : "Table",
label: property.readableBinding,
path: property.runtimeBinding,
}))
$: query =
parameters.queryId &&
$backendUiStore.queries.find(query => query._id === parameters.queryId)
</script> </script>
<div class="root"> <div class="root">
<Label size="m" color="dark">Datasource</Label> <Label size="m" color="dark">Datasource</Label>
<Select secondary bind:value={parameters.datasourceId}> <Select thin secondary bind:value={parameters.datasourceId}>
<option value="" /> <option value="" />
{#each $backendUiStore.datasources as datasource} {#each $backendUiStore.datasources as datasource}
<option value={datasource._id}>{datasource.name}</option> <option value={datasource._id}>{datasource.name}</option>
@ -22,11 +41,22 @@
{#if parameters.datasourceId} {#if parameters.datasourceId}
<Label size="m" color="dark">Query</Label> <Label size="m" color="dark">Query</Label>
<Select secondary bind:value={parameters.queryId}> <Select thin secondary bind:value={parameters.queryId}>
<option value="" /> <option value="" />
{#each $backendUiStore.queries.filter(query => query.datasourceId === datasource._id) as query} {#each $backendUiStore.queries.filter(query => query.datasourceId === datasource._id) as query}
<option value={query._id}>{query.name}</option> <option value={query._id}>{query.name}</option>
{/each} {/each}
</Select> </Select>
{/if} {/if}
<Spacer medium />
<!-- TODO: Need to render defaults, but allow interpolation of frontend values -->
{#if query}
<ParameterBuilder
bind:customParams={parameters.queryParams}
parameters={query.parameters}
bindings={bindableProperties} />
<pre>{query.queryString}</pre>
{/if}
</div> </div>

View File

@ -20,6 +20,8 @@
let getCaretPosition let getCaretPosition
$: console.log(bindings)
$: categories = Object.entries(groupBy("category", bindings)) $: categories = Object.entries(groupBy("category", bindings))
function onClickBinding(binding) { function onClickBinding(binding) {
@ -50,7 +52,9 @@
<span class="binding__label">{binding.label}</span> <span class="binding__label">{binding.label}</span>
<span class="binding__type">{binding.type}</span> <span class="binding__type">{binding.type}</span>
<br /> <br />
<div class="binding__description">{binding.description}</div> <div class="binding__description">
{binding.description || ''}
</div>
</div> </div>
{/each} {/each}
{/each} {/each}

View File

@ -120,6 +120,7 @@
flex-flow: row; flex-flow: row;
align-items: center; align-items: center;
} }
.label { .label {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -14,6 +14,17 @@
dropdownRight.hide() dropdownRight.hide()
} }
function openBindingDrawer() {
backendUiStore.update(state => {
state.selectedQueryId = value._id
return state
})
store.update(state => {
state.bottomDrawerVisible = true
return state
})
}
$: tables = $backendUiStore.tables.map(m => ({ $: tables = $backendUiStore.tables.map(m => ({
label: m.name, label: m.name,
name: `all_${m._id}`, name: `all_${m._id}`,
@ -67,6 +78,9 @@
<span>{value.label ? value.label : 'Table / View / Query'}</span> <span>{value.label ? value.label : 'Table / View / Query'}</span>
<Icon name="arrowdown" /> <Icon name="arrowdown" />
</div> </div>
{#if value.type === "query"}
<i class="ri-settings-3-line" on:click={openBindingDrawer} />
{/if}
<DropdownMenu bind:this={dropdownRight} anchor={anchorRight}> <DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
<div class="dropdown"> <div class="dropdown">
<div class="title"> <div class="title">

View File

@ -14,10 +14,11 @@
query = { query = {
datasourceId: $params.selectedDatasource, datasourceId: $params.selectedDatasource,
name: "New Query", name: "New Query",
parameters: {}, parameters: [],
// TODO: set dynamically // TODO: set dynamically
} }
} }
console.log("The query changes", query)
} }
</script> </script>

View File

@ -4,7 +4,6 @@
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 CreateQueryButton from "components/backend/DataTable/buttons/CreateQueryButton.svelte"
$: datasource = $backendUiStore.datasources.find( $: datasource = $backendUiStore.datasources.find(
ds => ds._id === $backendUiStore.selectedDatasourceId ds => ds._id === $backendUiStore.selectedDatasourceId

View File

@ -10,6 +10,7 @@
import ComponentPropertiesPanel from "components/userInterface/ComponentPropertiesPanel.svelte" import ComponentPropertiesPanel from "components/userInterface/ComponentPropertiesPanel.svelte"
import ComponentSelectionList from "components/userInterface/ComponentSelectionList.svelte" import ComponentSelectionList from "components/userInterface/ComponentSelectionList.svelte"
import FrontendNavigatePane from "components/userInterface/FrontendNavigatePane.svelte" import FrontendNavigatePane from "components/userInterface/FrontendNavigatePane.svelte"
import DataBindingDrawer from "components/userInterface/DataBindingDrawer/index.svelte"
$: instance = $store.appInstance $: instance = $store.appInstance
@ -47,6 +48,10 @@
{/if} {/if}
</div> </div>
{#if $store.bottomDrawerVisible}
<DataBindingDrawer />
{/if}
{#if $selectedComponent != null} {#if $selectedComponent != null}
<div class="components-pane"> <div class="components-pane">
<ComponentPropertiesPanel /> <ComponentPropertiesPanel />
@ -103,6 +108,15 @@
padding: var(--spacing-l) var(--spacing-xl); padding: var(--spacing-l) var(--spacing-xl);
} }
.binding-drawer-container {
height: 50vh;
position: absolute;
bottom: 0;
width: 100%;
background: var(--background);
padding: var(--spacing-xl);
}
.nav-group-header > div:nth-child(1) { .nav-group-header > div:nth-child(1) {
padding: 0rem 0.5rem 0rem 0rem; padding: 0rem 0.5rem 0rem 0rem;
vertical-align: bottom; vertical-align: bottom;

View File

@ -13,10 +13,12 @@ export const fetchQueryData = async ({ _id }) => {
/** /**
* Executes a query against an external data connector. * Executes a query against an external data connector.
*/ */
export const executeQuery = async ({ _id }) => { export const executeQuery = async ({ queryId, params }) => {
const response = await API.post({ const response = await API.post({
url: `/api/queries/${_id}`, url: `/api/queries/${queryId}`,
// body: params, body: {
params,
},
}) })
return response.rows return response.rows
} }

View File

@ -26,19 +26,12 @@ const navigationHandler = action => {
} }
const queryExecutionHandler = async (action, context) => { const queryExecutionHandler = async (action, context) => {
const { datasourceId, queryId, params } = action.parameters const { datasourceId, queryId, queryParams } = action.parameters
console.log(context)
// TODO: allow context based bindings for query params
// const enrichedQueryParameters = enrichDataBindings(params, context)
// console.log({ // TODO: allow context based bindings for query params
// action, const enrichedQueryParameters = enrichDataBindings(queryParams, context)
// context,
// // enrichedQueryParameters, await executeQuery({ datasourceId, queryId, params: enrichedQueryParameters })
// datasourceId,
// // queryId
// })
await executeQuery({ datasourceId, queryId })
} }
const handlerMap = { const handlerMap = {

View File

@ -75,9 +75,13 @@ exports.preview = async function(ctx) {
exports.execute = async function(ctx) { exports.execute = async function(ctx) {
const db = new CouchDB(ctx.user.appId) const db = new CouchDB(ctx.user.appId)
const datasource = await db.get(ctx.params.datasourceId) const query = await db.get(ctx.params.queryId)
const datasource = await db.get(query.datasourceId)
const query = datasource.queries[ctx.params.queryId] const queryTemplate = handlebars.compile(query.queryString)
// TODO: Take the default params into account
const parsedQuery = queryTemplate(ctx.request.body.params)
const Integration = integrations[datasource.source] const Integration = integrations[datasource.source]
@ -85,17 +89,7 @@ exports.execute = async function(ctx) {
ctx.throw(400, "Integration type does not exist.") ctx.throw(400, "Integration type does not exist.")
return return
} }
const response = await new Integration(datasource.config, parsedQuery).query()
// TODO: allow the ability to POST parameters down when executing the query
// const customParams = ctx.request.body
const queryTemplate = handlebars.compile(query.queryString)
const response = await new Integration(
datasource.config,
queryTemplate({
// pass the params here from the UI and backend contexts
})
).query()
ctx.body = response ctx.body = response
} }

View File

@ -38,6 +38,6 @@ function replicateLocal() {
}) })
} }
replicateLocal() // replicateLocal()
module.exports = Pouch module.exports = Pouch

View File

@ -1,5 +1,47 @@
const { Client } = require("pg") const { Client } = require("pg")
const SCHEMA = {
datasource: {
host: {
type: "string",
default: "localhost",
required: true,
},
port: {
type: "number",
required: true,
default: 5432,
},
database: {
type: "string",
default: "postgres",
required: true,
},
username: {
type: "string",
default: "root",
required: true,
},
password: {
type: "password",
default: "root",
required: true,
},
},
editor: {
sql: {
type: "sql",
},
gui: {
type: "config",
fields: {
something: "",
other: "",
},
},
},
}
const DATASOURCE_CONFIG = { const DATASOURCE_CONFIG = {
host: { host: {
type: "string", type: "string",
@ -34,10 +76,16 @@ const QUERY_CONFIG = {
}, },
gui: { gui: {
type: "config", type: "config",
fields: { fields: [
something: "", {
other: "", name: "",
type: "",
}, },
{
name: "",
type: "",
},
],
}, },
} }