Finishing up front-end, getting variable creation and management up and working from within the query schema/header menus.

This commit is contained in:
mike12345567 2021-12-15 19:20:19 +00:00
parent f0dfb2241c
commit 0a6754b13c
9 changed files with 196 additions and 107 deletions

View File

@ -125,7 +125,7 @@
<Icon size="S" hoverable name="MoreSmallList" /> <Icon size="S" hoverable name="MoreSmallList" />
</div> </div>
{#each menuItems as item} {#each menuItems as item}
<MenuItem on:click={item.onClick}> <MenuItem on:click={() => item.onClick(field)}>
{item.text} {item.text}
</MenuItem> </MenuItem>
{/each} {/each}
@ -162,4 +162,7 @@
.readOnly-menu { .readOnly-menu {
grid-template-columns: 1fr 1fr 20px; grid-template-columns: 1fr 1fr 20px;
} }
.control {
margin-top: 4px;
}
</style> </style>

View File

@ -119,3 +119,62 @@ export function flipHeaderState(headersActivity) {
}) })
return enabled return enabled
} }
// convert dynamic variables list to simple key/val object
export function variablesToObject(datasource) {
const variablesList = datasource?.config?.dynamicVariables
if (variablesList && variablesList.length > 0) {
return variablesList.reduce(
(acc, next) => ({ ...acc, [next.name]: next.value }),
{}
)
}
return {}
}
// convert dynamic variables object back to a list, enrich with query id
export function rebuildVariables(queryId, variables) {
let vars = []
if (variables) {
vars = Object.entries(variables).map(entry => {
return {
name: entry[0],
value: entry[1],
queryId,
}
})
}
return vars
}
export function shouldShowVariables(dynamicVariables, variablesReadOnly) {
return !!(
dynamicVariables &&
// show when editable or when read only and not empty
(!variablesReadOnly || Object.keys(dynamicVariables).length > 0)
)
}
export function buildAuthConfigs(datasource) {
if (datasource?.config?.authConfigs) {
return datasource.config.authConfigs.map(c => ({
label: c.name,
value: c._id,
}))
}
return []
}
export default {
breakQueryString,
buildQueryString,
fieldsToSchema,
flipHeaderState,
keyValueToQueryParameters,
queryParametersToKeyValue,
schemaToFields,
variablesToObject,
rebuildVariables,
shouldShowVariables,
buildAuthConfigs,
}

View File

@ -0,0 +1,46 @@
<script>
import { Input, ModalContent, Modal } from "@budibase/bbui"
export let datasource
export let dynamicVariables
export let binding
let name, modal
export const show = () => {
modal.show()
}
export const hide = () => {
modal.hide()
}
function checkValid(vars, name) {
if (!name) {
return false
}
const varKeys = Object.keys(vars || {})
return varKeys.find(key => key.toLowerCase() === name.toLowerCase()) == null
}
$: valid = checkValid(dynamicVariables, name)
$: error = name && !valid ? "Variable name is already in use." : null
async function saveVariable() {
const copiedName = name,
copiedBinding = binding
name = null
binding = null
dynamicVariables[copiedName] = copiedBinding
}
</script>
<Modal bind:this={modal}>
<ModalContent
title="Add dynamic variable"
confirmText="Save"
onConfirm={saveVariable}
disabled={!valid}
>
<Input label="Variable name" bind:value={name} on:input {error} />
</ModalContent>
</Modal>

View File

@ -1,22 +1,22 @@
<script> <script>
import { params } from "@roxi/routify" import { params } from "@roxi/routify"
import { datasources, integrations, queries, flags } from "stores/backend" import { datasources, flags, integrations, queries } from "stores/backend"
import { import {
Layout,
Input,
Select,
Tabs,
Tab,
Banner, Banner,
Divider,
Button,
Heading,
RadioGroup,
Label,
Body, Body,
TextArea, Button,
Table, Divider,
Heading,
Input,
Label,
Layout,
notifications, notifications,
RadioGroup,
Select,
Tab,
Table,
Tabs,
TextArea,
} from "@budibase/bbui" } from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte" import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import EditableLabel from "components/common/inputs/EditableLabel.svelte" import EditableLabel from "components/common/inputs/EditableLabel.svelte"
@ -26,33 +26,24 @@
import RestBodyInput from "../../_components/RestBodyInput.svelte" import RestBodyInput from "../../_components/RestBodyInput.svelte"
import { capitalise } from "helpers" import { capitalise } from "helpers"
import { onMount } from "svelte" import { onMount } from "svelte"
import { import restUtils from "helpers/data/utils"
fieldsToSchema,
schemaToFields,
breakQueryString,
buildQueryString,
keyValueToQueryParameters,
queryParametersToKeyValue,
flipHeaderState,
} from "helpers/data/utils"
import { import {
RestBodyTypes as bodyTypes, RestBodyTypes as bodyTypes,
SchemaTypeOptions, SchemaTypeOptions,
} from "constants/backend" } from "constants/backend"
import JSONPreview from "components/integration/JSONPreview.svelte" import JSONPreview from "components/integration/JSONPreview.svelte"
import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte" import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte"
import DynamicVariableModal from "../../_components/DynamicVariableModal.svelte"
import Placeholder from "assets/bb-spaceship.svg" import Placeholder from "assets/bb-spaceship.svg"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
let query, datasource let query, datasource
let breakQs = {}, let breakQs = {},
bindings = {} bindings = {}
let url = "" let saveId, url
let saveId, isGet
let response, schema, enabledHeaders let response, schema, enabledHeaders
let datasourceType, integrationInfo, queryConfig, responseSuccess
let authConfigId let authConfigId
let dynamicVariables let dynamicVariables, addVariableModal, varBinding
$: datasourceType = datasource?.source $: datasourceType = datasource?.source
$: integrationInfo = $integrations[datasourceType] $: integrationInfo = $integrations[datasourceType]
@ -61,10 +52,13 @@
$: checkQueryName(url) $: checkQueryName(url)
$: responseSuccess = response?.info?.code >= 200 && response?.info?.code < 400 $: responseSuccess = response?.info?.code >= 200 && response?.info?.code < 400
$: isGet = query?.queryVerb === "read" $: isGet = query?.queryVerb === "read"
$: authConfigs = buildAuthConfigs(datasource) $: authConfigs = restUtils.buildAuthConfigs(datasource)
$: schemaReadOnly = !responseSuccess $: schemaReadOnly = !responseSuccess
$: variablesReadOnly = !responseSuccess $: variablesReadOnly = !responseSuccess
$: showVariablesTab = shouldShowVariables(dynamicVariables, variablesReadOnly) $: showVariablesTab = restUtils.shouldShowVariables(
dynamicVariables,
variablesReadOnly
)
function getSelectedQuery() { function getSelectedQuery() {
return cloneDeep( return cloneDeep(
@ -92,7 +86,7 @@
if (!base) { if (!base) {
return base return base
} }
const qs = buildQueryString(qsObj) const qs = restUtils.buildQueryString(qsObj)
let newUrl = base let newUrl = base
if (base.includes("?")) { if (base.includes("?")) {
newUrl = base.split("?")[0] newUrl = base.split("?")[0]
@ -100,29 +94,15 @@
return qs.length > 0 ? `${newUrl}?${qs}` : newUrl return qs.length > 0 ? `${newUrl}?${qs}` : newUrl
} }
const buildAuthConfigs = datasource => {
if (datasource?.config?.authConfigs) {
return datasource.config.authConfigs.map(c => ({
label: c.name,
value: c._id,
}))
}
return []
}
function learnMoreBanner() {
window.open("https://docs.budibase.com/building-apps/data/transformers")
}
function buildQuery() { function buildQuery() {
const newQuery = { ...query } const newQuery = { ...query }
const queryString = buildQueryString(breakQs) const queryString = restUtils.buildQueryString(breakQs)
newQuery.fields.path = url.split("?")[0] newQuery.fields.path = url.split("?")[0]
newQuery.fields.queryString = queryString newQuery.fields.queryString = queryString
newQuery.fields.authConfigId = authConfigId newQuery.fields.authConfigId = authConfigId
newQuery.fields.disabledHeaders = flipHeaderState(enabledHeaders) newQuery.fields.disabledHeaders = restUtils.flipHeaderState(enabledHeaders)
newQuery.schema = fieldsToSchema(schema) newQuery.schema = restUtils.fieldsToSchema(schema)
newQuery.parameters = keyValueToQueryParameters(bindings) newQuery.parameters = restUtils.keyValueToQueryParameters(bindings)
return newQuery return newQuery
} }
@ -135,8 +115,10 @@
notifications.success(`Request saved successfully.`) notifications.success(`Request saved successfully.`)
if (dynamicVariables) { if (dynamicVariables) {
const dynamicVars = rebuildVariables(saveId) datasource.config.dynamicVariables = restUtils.rebuildVariables(
datasource.config.dynamicVariables = dynamicVars saveId,
dynamicVariables
)
await datasources.save(datasource) await datasources.save(datasource)
} }
} catch (err) { } catch (err) {
@ -175,57 +157,21 @@
return id return id
} }
// convert dynamic variables list to simple key/val object
const variablesToObject = datasource => {
const variablesList = datasource?.config?.dynamicVariables
if (variablesList && variablesList.length > 0) {
return variablesList.reduce(
(acc, next) => ({ ...acc, [next.name]: next.value }),
{}
)
}
return {}
}
// convert dynamic variables object back to a list, enrich with query id
const rebuildVariables = queryId => {
let variables = []
if (dynamicVariables) {
variables = Object.entries(dynamicVariables).map(entry => {
return {
name: entry[0],
value: entry[1],
queryId,
}
})
}
return variables
}
const shouldShowVariables = (dynamicVariables, variablesReadOnly) => {
if (
dynamicVariables &&
// show when editable or when read only and not empty
(!variablesReadOnly || Object.keys(dynamicVariables).length > 0)
) {
return true
}
return false
}
const schemaMenuItems = [ const schemaMenuItems = [
{ {
text: "Create dynamic variable", text: "Create dynamic variable",
onClick: () => { onClick: input => {
console.log("create variable") varBinding = `{{ data.0.[${input.name}] }}`
addVariableModal.show()
}, },
}, },
] ]
const responseHeadersMenuItems = [ const responseHeadersMenuItems = [
{ {
text: "Create dynamic variable", text: "Create dynamic variable",
onClick: () => { onClick: input => {
console.log("create variable") varBinding = `{{ info.headers.[${input.name}] }}`
addVariableModal.show()
}, },
}, },
] ]
@ -237,14 +183,14 @@
datasource = $datasources.list.find(ds => ds._id === query?.datasourceId) datasource = $datasources.list.find(ds => ds._id === query?.datasourceId)
const datasourceUrl = datasource?.config.url const datasourceUrl = datasource?.config.url
const qs = query?.fields.queryString const qs = query?.fields.queryString
breakQs = breakQueryString(qs) breakQs = restUtils.breakQueryString(qs)
if (datasourceUrl && !query.fields.path?.startsWith(datasourceUrl)) { if (datasourceUrl && !query.fields.path?.startsWith(datasourceUrl)) {
const path = query.fields.path const path = query.fields.path
query.fields.path = `${datasource.config.url}/${path ? path : ""}` query.fields.path = `${datasource.config.url}/${path ? path : ""}`
} }
url = buildUrl(query.fields.path, breakQs) url = buildUrl(query.fields.path, breakQs)
schema = schemaToFields(query.schema) schema = restUtils.schemaToFields(query.schema)
bindings = queryParametersToKeyValue(query.parameters) bindings = restUtils.queryParametersToKeyValue(query.parameters)
authConfigId = getAuthConfigId() authConfigId = getAuthConfigId()
if (!query.fields.disabledHeaders) { if (!query.fields.disabledHeaders) {
query.fields.disabledHeaders = {} query.fields.disabledHeaders = {}
@ -255,7 +201,7 @@
query.fields.disabledHeaders[header] = false query.fields.disabledHeaders[header] = false
} }
} }
enabledHeaders = flipHeaderState(query.fields.disabledHeaders) enabledHeaders = restUtils.flipHeaderState(query.fields.disabledHeaders)
if (query && !query.transformer) { if (query && !query.transformer) {
query.transformer = "return data" query.transformer = "return data"
} }
@ -267,10 +213,16 @@
if (query && !query.fields.bodyType) { if (query && !query.fields.bodyType) {
query.fields.bodyType = "none" query.fields.bodyType = "none"
} }
dynamicVariables = variablesToObject(datasource) dynamicVariables = restUtils.variablesToObject(datasource)
}) })
</script> </script>
<DynamicVariableModal
{datasource}
{dynamicVariables}
bind:binding={varBinding}
bind:this={addVariableModal}
/>
{#if query && queryConfig} {#if query && queryConfig}
<div class="inner"> <div class="inner">
<div class="top"> <div class="top">
@ -340,7 +292,10 @@
{#if !$flags.queryTransformerBanner} {#if !$flags.queryTransformerBanner}
<Banner <Banner
extraButtonText="Learn more" extraButtonText="Learn more"
extraButtonAction={learnMoreBanner} extraButtonAction={() =>
window.open(
"https://docs.budibase.com/building-apps/data/transformers"
)}
on:change={() => on:change={() =>
flags.updateFlag("queryTransformerBanner", true)} flags.updateFlag("queryTransformerBanner", true)}
> >
@ -449,9 +404,9 @@
name="Variable" name="Variable"
headings headings
keyHeading="Name" keyHeading="Name"
keyPlaceholder="e.g. cookie" keyPlaceholder="Variable name"
valueHeading={`Value`} valueHeading={`Value`}
valuePlaceholder={`e.g. {{ headers.set-cookie }}`} valuePlaceholder={`{{ value }}`}
readOnly={variablesReadOnly} readOnly={variablesReadOnly}
/> />
</Layout> </Layout>

View File

@ -2457,6 +2457,10 @@
"label": "Rows", "label": "Rows",
"key": "rows" "key": "rows"
}, },
{
"label": "Extra Info",
"key": "info"
},
{ {
"label": "Rows Length", "label": "Rows Length",
"key": "rowsLength" "key": "rowsLength"
@ -3178,6 +3182,10 @@
"label": "Rows", "label": "Rows",
"key": "rows" "key": "rows"
}, },
{
"label": "Extra Info",
"key": "info"
},
{ {
"label": "Rows Length", "label": "Rows Length",
"key": "rowsLength" "key": "rowsLength"

View File

@ -15,7 +15,8 @@ export const fetchDatasource = async dataSource => {
// Fetch all rows in data source // Fetch all rows in data source
const { type, tableId, fieldName } = dataSource const { type, tableId, fieldName } = dataSource
let rows = [] let rows = [],
info = {}
if (type === "table") { if (type === "table") {
rows = await fetchTableData(tableId) rows = await fetchTableData(tableId)
} else if (type === "view") { } else if (type === "view") {
@ -28,7 +29,12 @@ export const fetchDatasource = async dataSource => {
parameters[param.name] = param.default parameters[param.name] = param.default
} }
} }
rows = await executeQuery({ queryId: dataSource._id, parameters }) const { data, ...rest } = await executeQuery({
queryId: dataSource._id,
parameters,
})
info = rest
rows = data
} else if (type === FieldTypes.LINK) { } else if (type === FieldTypes.LINK) {
rows = await fetchRelationshipData({ rows = await fetchRelationshipData({
rowId: dataSource.rowId, rowId: dataSource.rowId,
@ -38,7 +44,7 @@ export const fetchDatasource = async dataSource => {
} }
// Enrich the result is always an array // Enrich the result is always an array
return Array.isArray(rows) ? rows : [] return { rows: Array.isArray(rows) ? rows : [], info }
} }
/** /**

View File

@ -30,6 +30,7 @@
// Provider state // Provider state
let rows = [] let rows = []
let allRows = [] let allRows = []
let info = {}
let schema = {} let schema = {}
let bookmarks = [null] let bookmarks = [null]
let pageNumber = 0 let pageNumber = 0
@ -120,6 +121,7 @@
// Build our data context // Build our data context
$: dataContext = { $: dataContext = {
rows, rows,
info,
schema, schema,
rowsLength: rows.length, rowsLength: rows.length,
@ -206,7 +208,9 @@
} else { } else {
// For other data sources like queries or views, fetch all rows from the // For other data sources like queries or views, fetch all rows from the
// server // server
allRows = await API.fetchDatasource(dataSource) const data = await API.fetchDatasource(dataSource)
allRows = data.rows
info = data.info
} }
loading = false loading = false
loaded = true loaded = true

View File

@ -182,13 +182,13 @@ exports.execute = async function (ctx) {
// call the relevant CRUD method on the integration class // call the relevant CRUD method on the integration class
try { try {
const { rows } = await Runner.run({ const { rows, extra } = await Runner.run({
datasource, datasource,
queryVerb: query.queryVerb, queryVerb: query.queryVerb,
query: enrichedQuery, query: enrichedQuery,
transformer: query.transformer, transformer: query.transformer,
}) })
ctx.body = rows ctx.body = { data: rows, ...extra }
} catch (err) { } catch (err) {
ctx.throw(400, err) ctx.throw(400, err)
} }

View File

@ -35,6 +35,11 @@ exports.definition = {
type: "object", type: "object",
description: "The response from the datasource execution", description: "The response from the datasource execution",
}, },
info: {
type: "object",
description:
"Some query types may return extra data, like headers from a REST query",
},
success: { success: {
type: "boolean", type: "boolean",
description: "Whether the action was successful", description: "Whether the action was successful",
@ -68,13 +73,16 @@ exports.run = async function ({ inputs, appId, emitter }) {
try { try {
await queryController.execute(ctx) await queryController.execute(ctx)
const { data, ...rest } = ctx.body
return { return {
response: ctx.body, response: data,
info: rest,
success: true, success: true,
} }
} catch (err) { } catch (err) {
return { return {
success: false, success: false,
info: {},
response: automationUtils.getError(err), response: automationUtils.getError(err),
} }
} }