new datasource design

This commit is contained in:
Martin McKeaveney 2021-01-07 13:13:46 +00:00
parent 37ef32a173
commit 64e31189b7
14 changed files with 208 additions and 100 deletions

View File

@ -4,11 +4,10 @@
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import * as api from "./api" import * as api from "./api"
import Table from "./Table.svelte" import Table from "./Table.svelte"
import CreateQueryButton from "components/backend/DataTable/buttons/CreateQueryButton.svelte"
export let query = {} export let query = {}
export let data = []
let data = []
let loading = false let loading = false
let error = false let error = false
@ -17,7 +16,7 @@
loading = true loading = true
const response = await api.fetchDataForQuery( const response = await api.fetchDataForQuery(
$params.selectedDatasource, $params.selectedDatasource,
$params.query query._id
) )
data = response.rows || [] data = response.rows || []
error = false error = false
@ -30,15 +29,13 @@
} }
// Fetch rows for specified query // Fetch rows for specified query
$: query && fetchData() // $: query && fetchData()
</script> </script>
{#if error} {#if error}
<div class="errors">{error}</div> <div class="errors">{error}</div>
{/if} {/if}
<Table title={query.name} schema={query.schema} {data} {loading}> <Table title={''} schema={query.schema} {data} {loading} />
<CreateQueryButton {query} edit />
</Table>
<style> <style>
.errors { .errors {

View File

@ -199,7 +199,6 @@
align-items: stretch; align-items: stretch;
} }
.grid-wrapper :global(> *) { .grid-wrapper :global(> *) {
height: auto;
flex: 1 1 auto; flex: 1 1 auto;
} }
:global(.grid-wrapper) { :global(.grid-wrapper) {

View File

@ -13,7 +13,7 @@
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import api from "builderStore/api" import api from "builderStore/api"
import EditIntegrationConfig from "../modals/EditIntegrationConfig.svelte" import EditIntegrationConfig from "../modals/EditIntegrationConfig.svelte"
import CreateEditQuery from "components/backend/DataTable/modals/CreateEditQuery.svelte" // import CreateEditQuery from "components/backend/DataTable/modals/CreateEditQuery.svelte"
export let query = {} export let query = {}
export let edit export let edit
@ -38,12 +38,12 @@
{edit ? 'Edit' : 'Create'} Query {edit ? 'Edit' : 'Create'} Query
</Button> </Button>
</div> </div>
<Modal bind:this={modal}> <!-- <Modal bind:this={modal}> -->
<ModalContent <!-- <ModalContent
confirmText="Save" confirmText="Save"
cancelText="Cancel" cancelText="Cancel"
onConfirm={saveQuery} onConfirm={saveQuery}
title={edit ? 'Edit Query' : 'Create New Query'}> title={edit ? 'Edit Query' : 'Create New Query'}> -->
<CreateEditQuery bind:query /> <!-- <CreateEditQuery bind:query /> -->
</ModalContent> <!-- </ModalContent> -->
</Modal> <!-- </Modal> -->

View File

@ -19,7 +19,6 @@
} }
function onClickQuery(datasourceId, queryId) { function onClickQuery(datasourceId, queryId) {
console.log(backendUiStore.selectedQueryId, queryId)
if ($backendUiStore.selectedQueryId === queryId) { if ($backendUiStore.selectedQueryId === queryId) {
return return
} }

View File

@ -0,0 +1,9 @@
class Query {
constructor(source, schema, type) {
this.source = source
this.schema = schema
this.type = type
}
build(parameters) {}
}

View File

@ -10,6 +10,8 @@
let editor let editor
let codemirror let codemirror
// $: codemirror && codemirror.setValue(value)
onMount(async () => { onMount(async () => {
codemirror = cm.fromTextArea(editor, { codemirror = cm.fromTextArea(editor, {
lineNumbers: true, lineNumbers: true,
@ -26,9 +28,16 @@
codemirror.on("change", instance => { codemirror.on("change", instance => {
const code = instance.getValue() const code = instance.getValue()
value = code value = code
// dispatch("change", { value }) dispatch("change", code)
}) })
}) })
</script> </script>
<textarea bind:value bind:this={editor} /> <textarea bind:value bind:this={editor} />
<style>
textarea {
background: var(--background);
border-radius: var(--border-radius-m);
}
</style>

View File

@ -14,33 +14,40 @@
import api from "builderStore/api" import api from "builderStore/api"
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 { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
const PREVIEW_HEADINGS = [ const PREVIEW_HEADINGS = [
{ {
title: "Preview", title: "JSON",
key: "PREVIEW", key: "JSON",
}, },
{ {
title: "Schema", title: "Schema",
key: "SCHEMA", key: "SCHEMA",
}, },
{
title: "Results",
key: "RESULTS",
},
{
title: "Preview",
key: "PREVIEW",
},
] ]
export let query export let query
export let fields = [] export let fields = []
let config = {} let config = {}
let previewTab = "PREVIEW" let tab = "JSON"
let preview let parameters
let data
$: datasource = $backendUiStore.datasources.find( $: datasource = $backendUiStore.datasources.find(
ds => ds._id === query.datasourceId ds => ds._id === query.datasourceId
) )
$: query.datasourceId =
query.datasourceId || $backendUiStore.selectedDatasourceId
$: query.schema = fields.reduce( $: query.schema = fields.reduce(
(acc, next) => ({ (acc, next) => ({
...acc, ...acc,
@ -52,6 +59,9 @@
{} {}
) )
$: datasourceType = datasource.source
$: datasourceType && fetchQueryConfig()
function newField() { function newField() {
fields = [...fields, {}] fields = [...fields, {}]
} }
@ -76,17 +86,18 @@
async function previewQuery() { async function previewQuery() {
try { try {
const response = await api.post(`/api/queries/preview`, { const response = await api.post(`/api/queries/preview`, {
parameters: query.parameters,
datasourceId: datasource._id, datasourceId: datasource._id,
query: query.queryString, query: query.queryString,
}) })
const json = await response.json() const json = await response.json()
if (response.status !== 200) {
throw new Error(json.message) if (response.status !== 200) throw new Error(json.message)
}
preview = json[0] || {} data = json || []
// TODO: refactor // TODO: refactor
fields = Object.keys(preview).map(field => ({ fields = Object.keys(json[0]).map(field => ({
name: field, name: field,
type: "STRING", type: "STRING",
})) }))
@ -96,15 +107,21 @@
} }
} }
onMount(() => { async function saveQuery() {
fetchQueryConfig() 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> </script>
<section> <section>
<div class="config"> <div class="config">
<Label extraSmall grey>Query Name</Label> <Label extraSmall grey>Query Name</Label>
<Input type="text" thin bind:value={query.name} /> <Input thin bind:value={query.name} />
<Spacer medium /> <Spacer medium />
@ -118,21 +135,23 @@
<Spacer medium /> <Spacer medium />
<IntegrationQueryEditor <IntegrationQueryEditor {query} bind:parameters />
type={query.queryType}
bind:query={query.queryString} />
<Spacer small /> <Spacer medium />
<Button thin secondary on:click={previewQuery}>Preview Query</Button> <section class="viewer">
<div class="viewer-controls">
<Button wide thin blue on:click={previewQuery}>Run</Button>
<Button wide thin primary on:click={saveQuery}>Save</Button>
</div>
<Spacer small /> {#if data}
<Switcher headings={PREVIEW_HEADINGS} bind:value={tab}>
{#if preview} {#if tab === 'JSON'}
<Switcher headings={PREVIEW_HEADINGS} bind:value={previewTab}> <pre class="preview">{JSON.stringify(data[0], undefined, 2)}</pre>
{#if previewTab === 'PREVIEW'} {:else if tab === 'PREVIEW'}
<pre class="preview">{JSON.stringify(preview, undefined, 2)}</pre> <ExternalDataSourceTable {query} {data} />
{:else if previewTab === 'SCHEMA'} {:else if tab === 'SCHEMA'}
{#each fields as field, idx} {#each fields as field, idx}
<div class="field"> <div class="field">
<Input thin type={'text'} bind:value={field.name} /> <Input thin type={'text'} bind:value={field.name} />
@ -152,6 +171,7 @@
{/if} {/if}
</Switcher> </Switcher>
{/if} {/if}
</section>
</div> </div>
</section> </section>
@ -163,17 +183,6 @@
margin-bottom: var(--spacing-m); margin-bottom: var(--spacing-m);
} }
h6 {
font-family: var(--font-sans);
font-weight: 600;
text-rendering: var(--text-render);
color: var(--ink);
font-size: var(--heading-font-size-xs);
color: var(--ink);
margin-bottom: var(--spacing-xs);
margin-top: var(--spacing-xs);
}
.config { .config {
margin-bottom: var(--spacing-s); margin-bottom: var(--spacing-s);
} }
@ -185,7 +194,21 @@
.preview { .preview {
width: 800px; width: 800px;
height: 100%;
overflow-y: auto;
overflow-wrap: break-word; overflow-wrap: break-word;
white-space: pre-wrap; white-space: pre-wrap;
} }
.viewer {
position: relative;
}
.viewer-controls {
position: absolute;
right: 0;
display: grid;
grid-gap: var(--spacing-m);
grid-auto-flow: column;
}
</style> </style>

View File

@ -1,6 +1,7 @@
import CodeMirror from "codemirror" import CodeMirror from "codemirror"
import "codemirror/lib/codemirror.css" import "codemirror/lib/codemirror.css"
import "codemirror/mode/sql/sql" import "codemirror/mode/sql/sql"
import "codemirror/mode/css/css"
import "codemirror/mode/handlebars/handlebars" import "codemirror/mode/handlebars/handlebars"
import "codemirror/mode/javascript/javascript" import "codemirror/mode/javascript/javascript"

View File

@ -1,6 +1,6 @@
<script> <script>
import { TextArea, Label, Input } from "@budibase/bbui" import { TextArea, Label, Input, Heading } from "@budibase/bbui"
import Editor from "./Editor.svelte" import Editor from "./QueryEditor.svelte"
const CAPTURE_VAR_INSIDE_MUSTACHE = /{{([^}]+)}}/g const CAPTURE_VAR_INSIDE_MUSTACHE = /{{([^}]+)}}/g
@ -8,21 +8,58 @@
SQL: "sql", SQL: "sql",
} }
export let type
export let query export let query
// $: parameters = Array.from( // TODO: bind these to the query
// query let parameters = []
// .matchAll(CAPTURE_VAR_INSIDE_MUSTACHE)
// .map(([_, paramName]) => paramName) $: 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>
<!-- {#each parameters as param} <Heading extraSmall black>Parameters</Heading>
<Label grey extraSmall>{param}</Label>
<Input thin bind:value={query.params[param]} />
{/each} -->
{#if type === QueryTypes.SQL} {#if query.queryType === QueryTypes.SQL}
<Editor label="Query" bind:value={query} /> <section>
<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} />
{/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

@ -25,7 +25,7 @@
<Select secondary bind:value={parameters.queryId}> <Select 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}>{datasource.queries[query].name}</option> <option value={query._id}>{query.name}</option>
{/each} {/each}
</Select> </Select>
{/if} {/if}

View File

@ -1,12 +1,36 @@
<script> <script>
import { params } from "@sveltech/routify" import { params } from "@sveltech/routify"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte" import { Switcher } from "@budibase/bbui"
import QueryInterface from "components/integration/QueryViewer.svelte"
$: query = $backendUiStore.queries.find(query => query._id === $params.query) let query
$: console.log("updated", query)
$: {
if ($params.query !== "new") {
query = $backendUiStore.queries.find(query => query._id === $params.query)
} else {
// New query
query = {
datasourceId: $params.selectedDatasource,
name: "New Query",
parameters: {},
// TODO: set dynamically
}
}
}
</script> </script>
<section>
{#if $backendUiStore.selectedDatabase._id && query} {#if $backendUiStore.selectedDatabase._id && query}
<ExternalDataSourceTable {query} /> <QueryInterface {query} />
{/if} {/if}
</section>
<style>
section {
background: var(--background);
padding: var(--spacing-xl);
border-radius: var(--border-radius-m);
}
</style>

View File

@ -1,5 +1,6 @@
<script> <script>
import { Button, Spacer } from "@budibase/bbui" import { goto } from "@sveltech/routify"
import { Button, Spacer, Icon, TextButton } 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"
@ -17,7 +18,10 @@
</script> </script>
{#if datasource} {#if datasource}
<CreateQueryButton {datasource} /> <TextButton text small on:click={() => $goto('../new')}>
<Icon name="filter" />
Create Query
</TextButton>
<section> <section>
<h4>{datasource.name}: Configuration</h4> <h4>{datasource.name}: Configuration</h4>
<IntegrationConfigForm integration={datasource.config} /> <IntegrationConfigForm integration={datasource.config} />

View File

@ -60,6 +60,7 @@ exports.update = async function(ctx) {
} }
exports.destroy = async function(ctx) { exports.destroy = async function(ctx) {
// TODO: destroy all queries as well
const database = new CouchDB(ctx.user.appId) const database = new CouchDB(ctx.user.appId)
await database.destroy(ctx.params.datasourceId) await database.destroy(ctx.params.datasourceId)
ctx.message = `Datasource deleted.` ctx.message = `Datasource deleted.`

View File

@ -51,7 +51,12 @@ exports.save = async function(ctx) {
} }
exports.preview = async function(ctx) { exports.preview = async function(ctx) {
const { query, datasourceId } = ctx.request.body const { query, datasourceId, parameters } = ctx.request.body
const queryTemplate = handlebars.compile(query)
const parsedQuery = queryTemplate(parameters)
console.log(parsedQuery)
const db = new CouchDB(ctx.user.appId) const db = new CouchDB(ctx.user.appId)
@ -64,7 +69,7 @@ exports.preview = async function(ctx) {
return return
} }
ctx.body = await new Integration(datasource.config, query).query() ctx.body = await new Integration(datasource.config, parsedQuery).query()
} }
exports.execute = async function(ctx) { exports.execute = async function(ctx) {