Query UI improvements (#11881)
* wip * wip * wip * wip * wip * wip * wip * wip * wip * linting * sidebar close icon, fix overflow issue with sidebar * better schema auto populating behavior * rename newqueryviewer * remove external datasource table * linting * wip * align header * fix needing to press the validate button twice * table min width * wip * remove loading * remove disabled * remove validation * lint * wip * fix json panel
This commit is contained in:
parent
de7fbbb9ef
commit
62acbc43fd
|
@ -1,33 +0,0 @@
|
||||||
<script>
|
|
||||||
import Table from "./Table.svelte"
|
|
||||||
|
|
||||||
export let query = {}
|
|
||||||
export let data = []
|
|
||||||
export let editRows = false
|
|
||||||
|
|
||||||
let loading = false
|
|
||||||
let error = false
|
|
||||||
let type = "external"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if error}
|
|
||||||
<div class="errors">{error}</div>
|
|
||||||
{/if}
|
|
||||||
<Table
|
|
||||||
schema={query.schema}
|
|
||||||
{data}
|
|
||||||
{loading}
|
|
||||||
{type}
|
|
||||||
rowCount={5}
|
|
||||||
allowEditing={editRows}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.errors {
|
|
||||||
color: var(--red);
|
|
||||||
background: var(--red-light);
|
|
||||||
padding: var(--spacing-m);
|
|
||||||
border-radius: var(--border-radius-m);
|
|
||||||
margin-bottom: var(--spacing-m);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -12,11 +12,19 @@
|
||||||
export let borderLeft = false
|
export let borderLeft = false
|
||||||
export let borderRight = false
|
export let borderRight = false
|
||||||
export let wide = false
|
export let wide = false
|
||||||
|
export let extraWide = false
|
||||||
|
export let closeButtonIcon = "Close"
|
||||||
|
|
||||||
$: customHeaderContent = $$slots["panel-header-content"]
|
$: customHeaderContent = $$slots["panel-header-content"]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="panel" class:wide class:borderLeft class:borderRight>
|
<div
|
||||||
|
class="panel"
|
||||||
|
class:wide
|
||||||
|
class:extraWide
|
||||||
|
class:borderLeft
|
||||||
|
class:borderRight
|
||||||
|
>
|
||||||
<div class="header" class:custom={customHeaderContent}>
|
<div class="header" class:custom={customHeaderContent}>
|
||||||
{#if showBackButton}
|
{#if showBackButton}
|
||||||
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
<Icon name="ArrowLeft" hoverable on:click={onClickBackButton} />
|
||||||
|
@ -33,7 +41,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showCloseButton}
|
{#if showCloseButton}
|
||||||
<Icon name="Close" hoverable on:click={onClickCloseButton} />
|
<Icon name={closeButtonIcon} hoverable on:click={onClickCloseButton} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -70,6 +78,10 @@
|
||||||
width: 310px;
|
width: 310px;
|
||||||
flex: 0 0 310px;
|
flex: 0 0 310px;
|
||||||
}
|
}
|
||||||
|
.panel.extraWide {
|
||||||
|
width: 450px;
|
||||||
|
flex: 0 0 450px;
|
||||||
|
}
|
||||||
.header {
|
.header {
|
||||||
flex: 0 0 48px;
|
flex: 0 0 48px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -18,31 +18,20 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each extraFields as { key, displayName, type }}
|
{#each extraFields as { key, displayName, type }}
|
||||||
<div class="config-field">
|
<Label>{displayName}</Label>
|
||||||
<Label>{displayName}</Label>
|
{#if type === "string"}
|
||||||
{#if type === "string"}
|
<Input
|
||||||
<Input
|
on:change={() => populateExtraQuery(extraQueryFields)}
|
||||||
on:change={() => populateExtraQuery(extraQueryFields)}
|
bind:value={extraQueryFields[key]}
|
||||||
bind:value={extraQueryFields[key]}
|
/>
|
||||||
/>
|
{/if}
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if type === "list"}
|
{#if type === "list"}
|
||||||
<Select
|
<Select
|
||||||
on:change={() => populateExtraQuery(extraQueryFields)}
|
on:change={() => populateExtraQuery(extraQueryFields)}
|
||||||
bind:value={extraQueryFields[key]}
|
bind:value={extraQueryFields[key]}
|
||||||
options={config[key].data[query.queryVerb]}
|
options={config[key].data[query.queryVerb]}
|
||||||
getOptionLabel={current => current}
|
getOptionLabel={current => current}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<style>
|
|
||||||
.config-field {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 20% 1fr;
|
|
||||||
grid-gap: var(--spacing-l);
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let bindingDrawerLeft
|
export let bindingDrawerLeft
|
||||||
export let allowHelpers = true
|
export let allowHelpers = true
|
||||||
|
export let customButtonText = null
|
||||||
|
|
||||||
let fields = Object.entries(object || {}).map(([name, value]) => ({
|
let fields = Object.entries(object || {}).map(([name, value]) => ({
|
||||||
name,
|
name,
|
||||||
|
@ -158,9 +159,13 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if !readOnly && !noAddButton}
|
{#if !readOnly && !noAddButton}
|
||||||
<div>
|
<div>
|
||||||
<ActionButton icon="Add" secondary thin outline on:click={addEntry}
|
<ActionButton icon="Add" secondary thin outline on:click={addEntry}>
|
||||||
>Add{name ? ` ${lowercase(name)}` : ""}</ActionButton
|
{#if customButtonText}
|
||||||
>
|
{customButtonText}
|
||||||
|
{:else}
|
||||||
|
{`Add${name ? ` ${lowercase(name)}` : ""}`}
|
||||||
|
{/if}
|
||||||
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
@ -1,364 +1,443 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto, beforeUrlChange } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
import { datasources, integrations, queries } from "stores/backend"
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Select,
|
Select,
|
||||||
Button,
|
|
||||||
ButtonGroup,
|
|
||||||
Body,
|
|
||||||
Label,
|
|
||||||
Layout,
|
|
||||||
Input,
|
Input,
|
||||||
Heading,
|
Label,
|
||||||
Tabs,
|
|
||||||
Tab,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
notifications,
|
notifications,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
Divider,
|
Divider,
|
||||||
|
Button,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
|
import { capitalise } from "helpers"
|
||||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
|
||||||
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
|
|
||||||
import BindingBuilder from "components/integration/QueryViewerBindingBuilder.svelte"
|
|
||||||
import { datasources, integrations, queries } from "stores/backend"
|
|
||||||
import { capitalise } from "../../helpers"
|
|
||||||
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
|
||||||
import JSONPreview from "./JSONPreview.svelte"
|
|
||||||
import { SchemaTypeOptions } from "constants/backend"
|
|
||||||
import KeyValueBuilder from "./KeyValueBuilder.svelte"
|
|
||||||
import { fieldsToSchema, schemaToFields } from "helpers/data/utils"
|
|
||||||
import AccessLevelSelect from "./AccessLevelSelect.svelte"
|
import AccessLevelSelect from "./AccessLevelSelect.svelte"
|
||||||
|
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||||
|
import QueryViewerSidePanel from "./QueryViewerSidePanel/index.svelte"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import BindingBuilder from "components/integration/QueryViewerBindingBuilder.svelte"
|
||||||
|
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
|
||||||
import { ValidQueryNameRegex } from "@budibase/shared-core"
|
import { ValidQueryNameRegex } from "@budibase/shared-core"
|
||||||
|
import ExtraQueryConfig from "./ExtraQueryConfig.svelte"
|
||||||
|
import QueryViewerSavePromptModal from "./QueryViewerSavePromptModal.svelte"
|
||||||
|
import { Utils } from "@budibase/frontend-core"
|
||||||
|
|
||||||
export let query
|
export let query
|
||||||
|
let queryHash
|
||||||
|
|
||||||
const resumeNavigation = () => {
|
let loading = false
|
||||||
if (typeof navigateTo == "string") {
|
let modified = false
|
||||||
$goto(typeof navigateTo == "string" ? `${navigateTo}` : navigateTo)
|
let scrolling = false
|
||||||
}
|
let showSidePanel = false
|
||||||
|
let nameError
|
||||||
|
|
||||||
|
let newQuery
|
||||||
|
|
||||||
|
let datasource
|
||||||
|
let integration
|
||||||
|
let schemaType
|
||||||
|
|
||||||
|
let autoSchema = {}
|
||||||
|
let rows = []
|
||||||
|
|
||||||
|
const parseQuery = query => {
|
||||||
|
modified = false
|
||||||
|
|
||||||
|
datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
|
||||||
|
integration = $integrations[datasource.source]
|
||||||
|
schemaType = integration.query[query.queryVerb].type
|
||||||
|
|
||||||
|
newQuery = cloneDeep(query)
|
||||||
|
// Set the location where the query code will be written to an empty string so that it doesn't
|
||||||
|
// get changed from undefined -> "" by the input, breaking our unsaved changes checks
|
||||||
|
newQuery.fields[schemaType] ??= ""
|
||||||
|
|
||||||
|
queryHash = JSON.stringify(newQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformerDocs = "https://docs.budibase.com/docs/transformers"
|
$: parseQuery(query)
|
||||||
|
|
||||||
let fields = query?.schema ? schemaToFields(query.schema) : []
|
const checkIsModified = newQuery => {
|
||||||
let parameters
|
const newQueryHash = JSON.stringify(newQuery)
|
||||||
let data = []
|
modified = newQueryHash !== queryHash
|
||||||
let saveId
|
|
||||||
let currentTab = "JSON"
|
|
||||||
let saveModal
|
|
||||||
let override = false
|
|
||||||
let navigateTo = null
|
|
||||||
let nameError = null
|
|
||||||
|
|
||||||
// seed the transformer
|
return modified
|
||||||
if (query && !query.transformer) {
|
|
||||||
query.transformer = "return data"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialise a new empty schema
|
const debouncedCheckIsModified = Utils.debounce(checkIsModified, 1000)
|
||||||
if (query && !query.schema) {
|
|
||||||
query.schema = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let queryStr = JSON.stringify(query)
|
$: debouncedCheckIsModified(newQuery)
|
||||||
|
|
||||||
$beforeUrlChange(event => {
|
async function runQuery({ suppressErrors = true }) {
|
||||||
const updated = JSON.stringify(query)
|
|
||||||
|
|
||||||
if (updated !== queryStr && !override) {
|
|
||||||
navigateTo = event.type == "pushstate" ? event.url : null
|
|
||||||
saveModal.show()
|
|
||||||
return false
|
|
||||||
} else return true
|
|
||||||
})
|
|
||||||
|
|
||||||
$: datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
|
|
||||||
$: query.schema = fieldsToSchema(fields)
|
|
||||||
$: datasourceType = datasource?.source
|
|
||||||
$: integrationInfo = datasourceType ? $integrations[datasourceType] : null
|
|
||||||
$: queryConfig = integrationInfo?.query
|
|
||||||
$: shouldShowQueryConfig = queryConfig && query.queryVerb
|
|
||||||
$: readQuery = query.queryVerb === "read" || query.readable
|
|
||||||
$: queryInvalid = !query.name || nameError || (readQuery && data.length === 0)
|
|
||||||
|
|
||||||
//Cast field in query preview response to number if specified by schema
|
|
||||||
$: {
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
let row = data[i]
|
|
||||||
for (let fieldName of Object.keys(fields)) {
|
|
||||||
if (fields[fieldName] === "number" && !isNaN(Number(row[fieldName]))) {
|
|
||||||
row[fieldName] = Number(row[fieldName])
|
|
||||||
} else {
|
|
||||||
row[fieldName] = row[fieldName]?.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetDependentFields() {
|
|
||||||
if (query.fields.extra) {
|
|
||||||
query.fields.extra = {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateExtraQuery(extraQueryFields) {
|
|
||||||
query.fields.extra = extraQueryFields
|
|
||||||
}
|
|
||||||
|
|
||||||
async function previewQuery() {
|
|
||||||
try {
|
try {
|
||||||
const response = await queries.preview(query)
|
showSidePanel = true
|
||||||
|
loading = true
|
||||||
|
const response = await queries.preview(newQuery)
|
||||||
if (response.rows.length === 0) {
|
if (response.rows.length === 0) {
|
||||||
notifications.info(
|
notifications.info(
|
||||||
"Query results empty. Please execute a query with results to create your schema."
|
"Query results empty. Please execute a query with results to create your schema."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data = response.rows
|
|
||||||
// need to merge fields that already exist/might have changed
|
if (Object.keys(newQuery.schema).length === 0) {
|
||||||
if (fields) {
|
// Assign this to a variable instead of directly to the newQuery.schema so that a user
|
||||||
for (let key of Object.keys(response.schema)) {
|
// can change the table they're querying and have the schema update until they first
|
||||||
if (fields[key]) {
|
// edit it
|
||||||
response.schema[key] = fields[key]
|
autoSchema = response.schema
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fields = response.schema
|
|
||||||
currentTab = "JSON"
|
rows = response.rows
|
||||||
|
|
||||||
notifications.success("Query executed successfully")
|
notifications.success("Query executed successfully")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(`Query Error: ${error.message}`)
|
notifications.error(`Query Error: ${error.message}`)
|
||||||
|
|
||||||
|
if (!suppressErrors) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the query.
|
|
||||||
async function saveQuery() {
|
async function saveQuery() {
|
||||||
try {
|
try {
|
||||||
const response = await queries.save(query.datasourceId, query)
|
showSidePanel = true
|
||||||
saveId = response._id
|
loading = true
|
||||||
|
const response = await queries.save(newQuery.datasourceId, {
|
||||||
if (response?._rev) {
|
...newQuery,
|
||||||
queryStr = JSON.stringify(query)
|
schema:
|
||||||
}
|
Object.keys(newQuery.schema).length === 0
|
||||||
|
? autoSchema
|
||||||
|
: newQuery.schema,
|
||||||
|
})
|
||||||
|
|
||||||
notifications.success("Query saved successfully")
|
notifications.success("Query saved successfully")
|
||||||
return response
|
return response
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(error.message || "Error saving query")
|
notifications.error(error.message || "Error saving query")
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetDependentFields() {
|
||||||
|
if (newQuery.fields.extra) {
|
||||||
|
newQuery.fields.extra = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateExtraQuery(extraQueryFields) {
|
||||||
|
newQuery.fields.extra = extraQueryFields
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScroll = e => {
|
||||||
|
scrolling = e.target.scrollTop !== 0
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal
|
<QueryViewerSavePromptModal
|
||||||
bind:this={saveModal}
|
checkIsModified={() => checkIsModified(newQuery)}
|
||||||
on:hide={() => {
|
attemptSave={() => runQuery({ suppressErrors: false }).then(saveQuery)}
|
||||||
navigateTo = null
|
/>
|
||||||
}}
|
<div class="queryViewer">
|
||||||
>
|
<div class="main">
|
||||||
<ModalContent
|
<div class="header" class:scrolling>
|
||||||
title="You have unsaved changes"
|
<div class="title">
|
||||||
confirmText="Save and Continue"
|
|
||||||
cancelText="Discard Changes"
|
|
||||||
size="L"
|
|
||||||
onConfirm={async () => {
|
|
||||||
await saveQuery()
|
|
||||||
override = true
|
|
||||||
resumeNavigation()
|
|
||||||
}}
|
|
||||||
onCancel={async () => {
|
|
||||||
override = true
|
|
||||||
resumeNavigation()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Body>Leaving this section will mean losing and changes to your query</Body>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<div class="wrapper">
|
|
||||||
<Layout gap="S" noPadding>
|
|
||||||
<Heading size="M">Query {integrationInfo?.friendlyName}</Heading>
|
|
||||||
<Divider />
|
|
||||||
<Heading size="S">Config</Heading>
|
|
||||||
<div class="config">
|
|
||||||
<div class="config-field">
|
|
||||||
<Label>Query Name</Label>
|
|
||||||
<Input
|
|
||||||
value={query.name}
|
|
||||||
on:input={e => {
|
|
||||||
let newValue = e.target.value || ""
|
|
||||||
if (newValue.match(ValidQueryNameRegex)) {
|
|
||||||
query.name = newValue.trim()
|
|
||||||
nameError = null
|
|
||||||
} else {
|
|
||||||
nameError = "Invalid query name"
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
error={nameError}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{#if queryConfig}
|
|
||||||
<div class="config-field">
|
|
||||||
<Label>Function</Label>
|
|
||||||
<Select
|
|
||||||
bind:value={query.queryVerb}
|
|
||||||
on:change={resetDependentFields}
|
|
||||||
options={Object.keys(queryConfig)}
|
|
||||||
getOptionLabel={verb =>
|
|
||||||
queryConfig[verb]?.displayName || capitalise(verb)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="config-field">
|
|
||||||
<AccessLevelSelect {saveId} {query} label="Access Level" />
|
|
||||||
</div>
|
|
||||||
{#if integrationInfo?.extra && query.queryVerb}
|
|
||||||
<ExtraQueryConfig
|
|
||||||
{query}
|
|
||||||
{populateExtraQuery}
|
|
||||||
config={integrationInfo.extra}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#key query.parameters}
|
|
||||||
<div class="binding-wrap">
|
|
||||||
<BindingBuilder
|
|
||||||
queryBindings={query.parameters}
|
|
||||||
bindable={false}
|
|
||||||
on:change={e => {
|
|
||||||
query.parameters = e.detail.map(binding => {
|
|
||||||
return {
|
|
||||||
name: binding.name,
|
|
||||||
default: binding.value,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/key}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if shouldShowQueryConfig}
|
|
||||||
<Divider />
|
|
||||||
<div class="config">
|
|
||||||
<Heading size="S">Fields</Heading>
|
|
||||||
<Body size="S">Fill in the fields specific to this query.</Body>
|
|
||||||
<IntegrationQueryEditor
|
|
||||||
{datasource}
|
|
||||||
{query}
|
|
||||||
height={200}
|
|
||||||
schema={queryConfig[query.queryVerb]}
|
|
||||||
bind:parameters
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
</div>
|
|
||||||
<div class="config">
|
|
||||||
<div class="help-heading">
|
|
||||||
<Heading size="S">Transformer</Heading>
|
|
||||||
<Icon
|
|
||||||
on:click={() => window.open(transformerDocs)}
|
|
||||||
hoverable
|
|
||||||
name="Help"
|
|
||||||
size="L"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
Add a JavaScript function to transform the query result.
|
{newQuery.name || "Untitled query"}<span class="unsaved"
|
||||||
|
>{modified ? "*" : ""}</span
|
||||||
|
>
|
||||||
</Body>
|
</Body>
|
||||||
<CodeMirrorEditor
|
|
||||||
height={200}
|
|
||||||
label="Transformer"
|
|
||||||
value={query.transformer}
|
|
||||||
resize="vertical"
|
|
||||||
on:change={e => (query.transformer = e.detail)}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="viewer-controls">
|
<div class="controls">
|
||||||
<Heading size="S">Results</Heading>
|
<Button disabled={loading} on:click={runQuery} overBackground>
|
||||||
<ButtonGroup gap="S">
|
<Icon size="S" name="Play" />
|
||||||
|
Run query</Button
|
||||||
|
>
|
||||||
|
<div class="tooltip" title="Run your query to enable saving">
|
||||||
<Button
|
<Button
|
||||||
cta
|
|
||||||
disabled={queryInvalid}
|
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
await saveQuery()
|
const response = await saveQuery()
|
||||||
// Go to the correct URL if we just created a new query
|
|
||||||
if (!query._rev) {
|
// When creating a new query the initally passed in query object will have no id.
|
||||||
$goto(`../../${query._id}`)
|
if (response._id && !newQuery._id) {
|
||||||
|
// Set the comparison query hash to match the new query so that the user doesn't
|
||||||
|
// get nagged when navigating to the edit view
|
||||||
|
queryHash = JSON.stringify(newQuery)
|
||||||
|
$goto(`../../${response._id}`)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled={loading ||
|
||||||
|
!newQuery.name ||
|
||||||
|
nameError ||
|
||||||
|
rows.length === 0}
|
||||||
|
overBackground
|
||||||
>
|
>
|
||||||
Save Query
|
<Icon size="S" name="SaveFloppy" />
|
||||||
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
<Button secondary on:click={previewQuery}>Run Query</Button>
|
</div>
|
||||||
</ButtonGroup>
|
|
||||||
</div>
|
</div>
|
||||||
<Body size="S">
|
</div>
|
||||||
Below, you can preview the results from your query and change the
|
|
||||||
schema.
|
<div class="body" on:scroll={handleScroll}>
|
||||||
</Body>
|
<div class="bodyInner">
|
||||||
<section class="viewer">
|
<div class="configField">
|
||||||
{#if data}
|
<Label>Name</Label>
|
||||||
<Tabs bind:selected={currentTab}>
|
<Input
|
||||||
<Tab title="JSON">
|
value={newQuery.name}
|
||||||
<JSONPreview data={data[0]} minHeight="120" />
|
on:input={e => {
|
||||||
</Tab>
|
let newValue = e.target.value || ""
|
||||||
<Tab title="Schema">
|
if (newValue.match(ValidQueryNameRegex)) {
|
||||||
<KeyValueBuilder
|
newQuery.name = newValue.trim()
|
||||||
bind:object={fields}
|
nameError = null
|
||||||
name="field"
|
} else {
|
||||||
headings
|
nameError = "Invalid query name"
|
||||||
options={SchemaTypeOptions}
|
}
|
||||||
|
}}
|
||||||
|
error={nameError}
|
||||||
|
/>
|
||||||
|
{#if integration.query}
|
||||||
|
<Label>Function</Label>
|
||||||
|
<Select
|
||||||
|
bind:value={newQuery.queryVerb}
|
||||||
|
on:change={resetDependentFields}
|
||||||
|
options={Object.keys(integration.query)}
|
||||||
|
getOptionLabel={verb =>
|
||||||
|
integration.query[verb]?.displayName || capitalise(verb)}
|
||||||
|
/>
|
||||||
|
<Label>Access</Label>
|
||||||
|
<AccessLevelSelect query={newQuery} />
|
||||||
|
{#if integration?.extra && newQuery.queryVerb}
|
||||||
|
<ExtraQueryConfig
|
||||||
|
query={newQuery}
|
||||||
|
{populateExtraQuery}
|
||||||
|
config={integration.extra}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
{/if}
|
||||||
<Tab title="Preview">
|
{/if}
|
||||||
<ExternalDataSourceTable {query} {data} />
|
</div>
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
<Divider />
|
||||||
{/if}
|
|
||||||
</section>
|
<div class="heading">
|
||||||
{/if}
|
<Heading weight="L" size="XS">Query</Heading>
|
||||||
</Layout>
|
</div>
|
||||||
|
<div class="copy">
|
||||||
|
<Body size="S">
|
||||||
|
{#if schemaType === "sql"}
|
||||||
|
Add some SQL to query your data
|
||||||
|
{:else if schemaType === "json"}
|
||||||
|
Add some JSON to query your data
|
||||||
|
{:else if schemaType === "fields"}
|
||||||
|
Add some fields to query your data
|
||||||
|
{:else}
|
||||||
|
Enter your query below
|
||||||
|
{/if}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
<IntegrationQueryEditor
|
||||||
|
noLabel
|
||||||
|
{datasource}
|
||||||
|
bind:query={newQuery}
|
||||||
|
height={200}
|
||||||
|
schema={integration.query[newQuery.queryVerb]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<div class="heading">
|
||||||
|
<Heading weight="L" size="XS">Bindings</Heading>
|
||||||
|
</div>
|
||||||
|
<div class="copy">
|
||||||
|
<Body size="S">
|
||||||
|
Bindings come in two parts: the binding name, and a default/fallback
|
||||||
|
value. These bindings can be used as Handlebars expressions
|
||||||
|
throughout the query.
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
{#key newQuery.parameters}
|
||||||
|
<BindingBuilder
|
||||||
|
hideHeading
|
||||||
|
queryBindings={newQuery.parameters}
|
||||||
|
on:change={e => {
|
||||||
|
newQuery.parameters = e.detail.map(binding => {
|
||||||
|
return {
|
||||||
|
name: binding.name,
|
||||||
|
default: binding.value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<div class="heading">
|
||||||
|
<Heading weight="L" size="XS">Transformer</Heading>
|
||||||
|
</div>
|
||||||
|
<div class="copy">
|
||||||
|
<Body size="S">
|
||||||
|
Add a JavaScript function to transform the query result.
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
<CodeMirrorEditor
|
||||||
|
height={200}
|
||||||
|
value={newQuery.transformer}
|
||||||
|
resize="vertical"
|
||||||
|
on:change={e => (newQuery.transformer = e.detail)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class:showSidePanel class="sidePanel">
|
||||||
|
<QueryViewerSidePanel
|
||||||
|
onClose={() => (showSidePanel = false)}
|
||||||
|
onSchemaChange={newSchema => {
|
||||||
|
newQuery.schema = newSchema
|
||||||
|
}}
|
||||||
|
{rows}
|
||||||
|
schema={Object.keys(newQuery.schema).length === 0
|
||||||
|
? autoSchema
|
||||||
|
: newQuery.schema}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.wrapper {
|
.unsaved {
|
||||||
width: 640px;
|
color: var(--grey-5);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queryViewer {
|
||||||
|
height: 100%;
|
||||||
|
margin: -28px -40px -40px -40px;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queryViewer :global(.spectrum-Divider) {
|
||||||
|
margin: 35px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 10px 8px 16px;
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: border-bottom 130ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header.scrolling {
|
||||||
|
border-bottom: var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding: 23px 23px 80px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bodyInner {
|
||||||
|
max-width: 520px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config {
|
.title {
|
||||||
display: grid;
|
/* width 0 paired with flex-grow necessary here for the truncation to work properly*/
|
||||||
grid-gap: var(--spacing-s);
|
width: 0;
|
||||||
z-index: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-field {
|
.title :global(p) {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls :global(button) {
|
||||||
|
border: none;
|
||||||
|
color: var(--grey-7);
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls :global(button):hover {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls :global(.is-disabled) {
|
||||||
|
pointer-events: none;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--grey-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls :global(span) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls :global(.icon) {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.configField {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 20% 1fr;
|
grid-template-columns: 20% 1fr;
|
||||||
grid-gap: var(--spacing-l);
|
grid-gap: var(--spacing-l);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.help-heading {
|
.configField :global(label) {
|
||||||
display: flex;
|
color: var(--grey-6);
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.viewer {
|
.heading {
|
||||||
min-height: 200px;
|
margin-bottom: 8px;
|
||||||
width: 640px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.viewer-controls {
|
.copy {
|
||||||
display: flex;
|
margin-bottom: 14px;
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--spacing-m);
|
|
||||||
min-width: 150px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.binding-wrap :global(div.container) {
|
.copy :global(p) {
|
||||||
padding-left: 0px;
|
color: var(--grey-7);
|
||||||
padding-right: 0px;
|
}
|
||||||
|
|
||||||
|
.sidePanel {
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: width 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidePanel :global(.panel) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showSidePanel {
|
||||||
|
width: 450px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { Body, Heading, Layout } from "@budibase/bbui"
|
|
||||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
import { getUserBindings } from "builderStore/dataBinding"
|
import { getUserBindings } from "builderStore/dataBinding"
|
||||||
export let bindable = true
|
export let bindable = true
|
||||||
export let queryBindings = []
|
export let queryBindings = []
|
||||||
|
export let hideHeading = false
|
||||||
|
|
||||||
const userBindings = getUserBindings()
|
const userBindings = getUserBindings()
|
||||||
|
|
||||||
|
@ -13,44 +13,16 @@
|
||||||
}, {})
|
}, {})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding={bindable} gap="S">
|
<KeyValueBuilder
|
||||||
<div class="controls" class:height={!bindable}>
|
bind:object={internalBindings}
|
||||||
<Heading size="XS">Bindings</Heading>
|
tooltip="Set the name of the binding which can be used in Handlebars statements throughout your query"
|
||||||
</div>
|
name="binding"
|
||||||
<Body size="S">
|
customButtonText="Bindings"
|
||||||
{#if !bindable}
|
headings
|
||||||
Bindings come in two parts: the binding name, and a default/fallback
|
keyPlaceholder="Binding name"
|
||||||
value. These bindings can be used as Handlebars expressions throughout the
|
valuePlaceholder="Default"
|
||||||
query.
|
bindings={[...userBindings]}
|
||||||
{:else}
|
bindingDrawerLeft="260px"
|
||||||
Enter a value for each binding. The default values will be used for any
|
allowHelpers={false}
|
||||||
values left blank.
|
on:change
|
||||||
{/if}
|
/>
|
||||||
</Body>
|
|
||||||
<div class="bindings" class:bindable>
|
|
||||||
<KeyValueBuilder
|
|
||||||
bind:object={internalBindings}
|
|
||||||
tooltip="Set the name of the binding which can be used in Handlebars statements throughout your query"
|
|
||||||
name="binding"
|
|
||||||
headings
|
|
||||||
keyPlaceholder="Binding name"
|
|
||||||
valuePlaceholder="Default"
|
|
||||||
bindings={[...userBindings]}
|
|
||||||
bindingDrawerLeft="260px"
|
|
||||||
allowHelpers={false}
|
|
||||||
on:change
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.controls {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.height {
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script>
|
||||||
|
import { goto, beforeUrlChange } from "@roxi/routify"
|
||||||
|
import { Body, Modal, ModalContent } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let checkIsModified = () => {}
|
||||||
|
export let attemptSave = () => {}
|
||||||
|
let modal
|
||||||
|
let navigateTo
|
||||||
|
let override = false
|
||||||
|
|
||||||
|
$beforeUrlChange(event => {
|
||||||
|
if (checkIsModified() && !override) {
|
||||||
|
navigateTo = event.type == "pushstate" ? event.url : null
|
||||||
|
modal.show()
|
||||||
|
return false
|
||||||
|
} else return true
|
||||||
|
})
|
||||||
|
|
||||||
|
const resumeNavigation = () => {
|
||||||
|
if (typeof navigateTo == "string") {
|
||||||
|
$goto(typeof navigateTo == "string" ? `${navigateTo}` : navigateTo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
bind:this={modal}
|
||||||
|
on:hide={() => {
|
||||||
|
navigateTo = null
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="You have unsaved changes"
|
||||||
|
confirmText="Save and Continue"
|
||||||
|
cancelText="Discard Changes"
|
||||||
|
size="L"
|
||||||
|
onConfirm={async () => {
|
||||||
|
try {
|
||||||
|
await attemptSave()
|
||||||
|
override = true
|
||||||
|
resumeNavigation()
|
||||||
|
} catch (e) {
|
||||||
|
navigateTo = false
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCancel={async () => {
|
||||||
|
override = true
|
||||||
|
resumeNavigation()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Body>Leaving this section will mean losing any changes to your query</Body>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script>
|
||||||
|
export let data
|
||||||
|
|
||||||
|
$: string = JSON.stringify(data || {}, null, 2)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<textarea class="json" disabled value={string} />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.json {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--ink);
|
||||||
|
padding: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
resize: none;
|
||||||
|
background-color: var(
|
||||||
|
--spectrum-textfield-m-background-color,
|
||||||
|
var(--spectrum-global-color-gray-50)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script>
|
||||||
|
import Table from "components/backend/DataTable/Table.svelte"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
export let schema = {}
|
||||||
|
export let rows = []
|
||||||
|
|
||||||
|
$: rowsCopy = cloneDeep(rows)
|
||||||
|
|
||||||
|
// Cast field in query preview response to number if specified by schema
|
||||||
|
$: {
|
||||||
|
for (let i = 0; i < rowsCopy.length; i++) {
|
||||||
|
let row = rowsCopy[i]
|
||||||
|
for (let fieldName of Object.keys(schema)) {
|
||||||
|
if (schema[fieldName] === "number" && !isNaN(Number(row[fieldName]))) {
|
||||||
|
row[fieldName] = Number(row[fieldName])
|
||||||
|
} else {
|
||||||
|
row[fieldName] = row[fieldName]?.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="table">
|
||||||
|
<Table {schema} data={rowsCopy} type="external" allowEditing={false} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.table :global(.spectrum-Table-cell) {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script>
|
||||||
|
import KeyValueBuilder from "../KeyValueBuilder.svelte"
|
||||||
|
import { SchemaTypeOptions } from "constants/backend"
|
||||||
|
|
||||||
|
export let schema
|
||||||
|
export let onSchemaChange = () => {}
|
||||||
|
|
||||||
|
const handleChange = e => {
|
||||||
|
let newSchema = {}
|
||||||
|
|
||||||
|
// KeyValueBuilder on change event returns an array of objects with each object
|
||||||
|
// containing a field
|
||||||
|
e.detail.forEach(({ name, value }) => {
|
||||||
|
newSchema[name] = value
|
||||||
|
})
|
||||||
|
|
||||||
|
onSchemaChange(newSchema)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#key schema}
|
||||||
|
<KeyValueBuilder
|
||||||
|
on:change={handleChange}
|
||||||
|
object={schema}
|
||||||
|
name="field"
|
||||||
|
headings
|
||||||
|
options={SchemaTypeOptions}
|
||||||
|
/>
|
||||||
|
{/key}
|
|
@ -0,0 +1,66 @@
|
||||||
|
<script>
|
||||||
|
import Panel from "components/design/Panel.svelte"
|
||||||
|
import { ActionButton } from "@budibase/bbui"
|
||||||
|
import JSONPanel from "./JSONPanel.svelte"
|
||||||
|
import SchemaPanel from "./SchemaPanel.svelte"
|
||||||
|
import PreviewPanel from "./PreviewPanel.svelte"
|
||||||
|
|
||||||
|
export let rows
|
||||||
|
export let schema
|
||||||
|
export let onSchemaChange = () => {}
|
||||||
|
export let onClose = () => {}
|
||||||
|
|
||||||
|
const tabs = ["JSON", "Schema", "Preview"]
|
||||||
|
let activeTab = "JSON"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Panel
|
||||||
|
showCloseButton
|
||||||
|
closeButtonIcon="RailRightClose"
|
||||||
|
onClickCloseButton={onClose}
|
||||||
|
title="Query results"
|
||||||
|
icon={"SQLQuery"}
|
||||||
|
borderLeft
|
||||||
|
extraWide
|
||||||
|
>
|
||||||
|
<div slot="panel-header-content">
|
||||||
|
<div class="settings-tabs">
|
||||||
|
{#each tabs as tab}
|
||||||
|
<ActionButton
|
||||||
|
size="M"
|
||||||
|
quiet
|
||||||
|
selected={activeTab === tab}
|
||||||
|
on:click={() => {
|
||||||
|
activeTab = tab
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</ActionButton>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
{#if activeTab === "JSON"}
|
||||||
|
<JSONPanel data={rows[0] || {}} />
|
||||||
|
{:else if activeTab === "Schema"}
|
||||||
|
<SchemaPanel {onSchemaChange} {schema} />
|
||||||
|
{:else}
|
||||||
|
<PreviewPanel {schema} {rows} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.settings-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
padding: 0 var(--spacing-l);
|
||||||
|
padding-bottom: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 14px;
|
||||||
|
height: 100%;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -23,6 +23,7 @@
|
||||||
export let schema
|
export let schema
|
||||||
export let editable = true
|
export let editable = true
|
||||||
export let height = 500
|
export let height = 500
|
||||||
|
export let noLabel = false
|
||||||
|
|
||||||
let stepEditors = []
|
let stepEditors = []
|
||||||
|
|
||||||
|
@ -75,7 +76,7 @@
|
||||||
{#if schema.type === QueryTypes.SQL}
|
{#if schema.type === QueryTypes.SQL}
|
||||||
<Editor
|
<Editor
|
||||||
editorHeight={height}
|
editorHeight={height}
|
||||||
label="Query"
|
label={noLabel ? null : "Query"}
|
||||||
mode="sql"
|
mode="sql"
|
||||||
on:change={updateQuery}
|
on:change={updateQuery}
|
||||||
readOnly={!editable}
|
readOnly={!editable}
|
||||||
|
@ -85,7 +86,7 @@
|
||||||
{:else if shouldDisplayJsonBox}
|
{:else if shouldDisplayJsonBox}
|
||||||
<Editor
|
<Editor
|
||||||
editorHeight={height}
|
editorHeight={height}
|
||||||
label="Query"
|
label={noLabel ? null : "Query"}
|
||||||
mode="json"
|
mode="json"
|
||||||
on:change={updateQuery}
|
on:change={updateQuery}
|
||||||
readOnly={!editable}
|
readOnly={!editable}
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
|
|
||||||
const buildNewQuery = isRestQuery => {
|
const buildNewQuery = isRestQuery => {
|
||||||
let query = {
|
let query = {
|
||||||
|
name: "Untitled query",
|
||||||
|
transformer: "return data",
|
||||||
|
schema: {},
|
||||||
datasourceId: $params.datasourceId,
|
datasourceId: $params.datasourceId,
|
||||||
parameters: [],
|
parameters: [],
|
||||||
fields: {},
|
fields: {},
|
||||||
|
|
Loading…
Reference in New Issue